C代写:CS354 Enhancing Kernel IPC Services




The objective of this lab is to enhance XINU’s IPC kernel services, and utilize ROP and process context manipulation to alter the run-time behavior of processes.


Read Chapters 6 and 7 of the XINU textbook.

Blocking and non-blocking attributes of IPC send() and receive()

This problem concerns the implementation of a blocking version of send() by using IPC attributes to affect the behavior of send() which, by default, is non-blocking. The framework equally applies to receive() but we will only implement IPC attribute selection for send() since it is more challenging/interesting and a non-blocking version of receive() (as a separate system call recvclr()) already exists.

xfcntl() system call

Introduce a new system call, syscall xfcntl(uint16 selector, uint mode), similar to UNIX/Linux fcntl() which allows modifying the attributes of open file descriptors. In our case, we only care about making send() or receive() blocking or non-blocking. The first argument, selector, is defined for two values: 0 selecting send() and 1 specifying receive(). The second argument, mode, also takes on binary values: 0 meaning blocking and 1 meaning non-blocking. Use #define to set IPCSND as 0, IPCRCV as 1, IPCBLK as 0, IPCNBK as 1 in header file ipc.h under include/ so that symbols can be used for the two arguments of xfcntl(). For example, xfcntl(IPCSND, IPCBLK) requests that the kernel set the attribute of send() to blocking mode. By default, send() is non-blocking and receive() blocking. xfcntl() returns OK upon success and SYSERR when an error occurs.

Kernel mods of send()

To represent the attribute of send(), add process table field, uint16 prsendmode, that takes on values IPCBLK and IPCNBK. Initialize the values so that they take on the default dispositions of send(). Modify send() so that it behaves as before in non-blocking mode if prsendmode equals IPCNBK but becomes blocking if prsendmode equals IPCBLK. For send() to behave in blocking mode means if the receiver’s 1-word message buffer is empty, send() copies the message to the receiver buffer and sets the receiver’s prhasmsg flag to 1. If the receiver’s buffer is full, send() blocks until the buffer becomes free. Upon blocking, XINU’s upper half context switches out the sending process and puts it into a waiting state PR_SENDBLK to be added in process.h with value 15. The 1-word message to be sent is stored in the sending process’s process table entry where two new fields are defined:

umsg32 prsendmsg; /* Buffer to hold message attempting to send */
bool8 prsendflag; /* Nonzero if blocking to send */

prsendmsg holds the 1-word message to be sent and prsendflag is a Boolean flag that is set to 1 if prsendmsg is valid. Before being context switched out, the blocking sender process is inserted in a FIFO queue of (0 or more) blocking processes waiting to transmit a message to the same receiver. Every process has a blocking sender queue associated with it given by a new process table field, qid16 prsenderqueue, which is the queue ID. We introduce two additional fields in the process table structure procent

qid16 prsenderqueue; /* Index to FIFO queue of blocked senders */
bool8 prrcvblkflag; /* Set to 1 if one or more process are blocking to send */

where prrcvblkflag indicates whether there are blocked sender processes. To implement insert/extract of FIFO queue prsenderqueue, reuse the enqueue()/dequeue() kernel functions in queue.c which are used by XINU’s semaphore system calls wait() and signal() to manage FIFO semaphore queues. Since in XINU a process can only block in one queue (e.g., readylist, semaphore queue, blocking sender queue), the legacy XINU approach for managing blocked processes which is space efficient can be reused.

Kernel mods of receive()

When a process makes a receive() call (ignore rcvclear() which can be implemented analogously), the receive() system call must check if prrcvblkflag is set to 1. If so, one or more sender processes are waiting in the receiver’s blocked sender queue. Using dequeue() the process at the front of the queue is extracted and inserted into the ready list and XINU’s scheduler is called. Before calling resched(), the dequeued sender’s message is copied from prsendmsg to the receiver’s prmsg buffer. The receiver’s prhasmsg field is set to 1 (since it has a new message) and the sender’s prsendflag flag is set to 0. The receiver process has to make a new call to receive() to receive the new message which is the responsibility of the app programmer to code accordingly.

Implementation and testing

Implement xfcntl() and the kernels mods to send() and receive() so that send()’s blocking/non-blocking attributes can be set by the app programmer. Create test cases whose output demonstrate the correct functioning of the blocking/non-blocking message send extension of kernel services. Include in your test cases sluggish receivers that sleep so that its FIFO sender queue builds up. In Lab4Answers.pdf describe how you set up your test cases and how your verified correctness of your implementation. Although there is no need to implement, describe what you think should happen if a receiver process terminates when one or more processes are blocked in its blocked sender queue. Explain the rationale behind your design decision. Following our usual convention, deposit your code in system/ and include/.

Call-by-reference receive()

XINU’s receive() system call is blocking, returns a message of type umsg32 and never fails (i.e., does not return SYSERR). Implement a version of receive(), syscall preceive(struct pmessage *p), that returns OK upon success and SYSERR when an error occurs. In addition to communicating a message received (but not as return value), preceive() also provides the PID of the sender. It does so by taking a pointer to a data structure

struct pmessage {
umsg32 sendermsg;
pid32 senderid;

as argument where sendermsg communicates the message received and senderid identifies the sender’s PID. Add a process table field, pid32 prsenderid, which is updated by send() to specify the sender’s PID in the receiver’s process table entry. When addresses are passed as arguments in system calls, it is important that a system call executing in kernel mode verify that the address specified belongs to the address space of the user mode calling process. Otherwise a process may trick a kernel (system call running in kernel mode) into writing in user or kernel address space that the calling process does not have access to. That would be pretty bad.

In XINU where we do not implement user mode/kernel mode separation, we will follow a design of preceive() that follows the design principle of isolation/protection. Specifically, preceive() will check that the address passed lies within the stack area of the calling process. For example, a process that passes the address of a local variable of a function from wherein preceive() is called will successfully pass the kernel’s memory access check. On the other hand, if the address falls within the range of XINU’s data segment, preceive() will return SYSERR. The same goes for XINU’s text segment. Explain in Lab4Answers.pdf how you go about performing the memory access check. Define struct pmessage in ipc.h. Use XINU’s enhanced receive() and send() from Problem 3.3 with blocking/non-blocking attribute support as the basis for implementing preceive(). Test that preceive() works correctly and deposit your code in system/ and include/ following our usual file naming convention.

Modifying process run-time behavior via ROP and context manipulation

Basic idea

An important technique for modifying the run-time behavior of a process is ROP (return-oriented programming) based process context manipulation where return addresses in a process’s run-time stack are changed to affect its control flow. The technique can be used for good and bad. In the latter, a hacker may find ways to change a return address in a stack/buffer overflow attack (also called stack smashing) which has been the single biggest security exploit for compromising computing systems. We will consider a surgical version of this technique in XINU that allows an attacker process to “hijack” the context of a victim process.

Borrowing the context of a victim process

Spawn two app processes from main() back-to-back using create()/resume(), one running progA() (“victim”) and the other running progB() (“attacker”). Use the legacy scheduler of XINU and assign priority 25 to progA() and priority 20 to progB(). The code of progA(), void progA(void), outputs “I am A: part 1\n” by calling kprintf(), then calls sleepms() to sleep for 500 msec, and then prints “I am A: part 2\n” before terminating.

The process running progB() is an attacker that aims to hijack the process executing progA() by borrowing its context. The attacker possesses specialized knowledge such as knowing that it will get a chance to run when progA() calls sleepms(). When the victim process running progA() is context-switched out, its stack will contain the stack frames of sleepms(), resched(), and ctxsw(). progB()’s goal, when it is context-switched in, is to modify the return address (EIP) of resched() – following CDECL EIP was pushed at the boundary of the stack frames of sleepms() (caller) and resched() (callee) – in the stack of the context-switched out victim process. The attacker, if he/she knows how to find the return address of resched() in the stack of the victim process, surgically modifies the return address so that when the victim process is context-switched in after sleeping for 500 msec it will not return to sleepms() but jump to code belonging to the attacker, funcB(). That is, funcB() which was not called by the victim process nor the attacker process, will run in the context of the victim process with ESP pointing to the stack frame of sleepms() as if funcB() – not sleepms() – had called resched(). Taking the position of an attacker who is knowledgable about XINU, explain in Lab4Answers.pdf how you go about finding out where in the stack of the victim the return address is located. Although in XINU we do not implement user mode/kernel mode separation, if the attack had been successfully carried out in Linux/Windows it would have allowed the attacker to run his/her code funcB() in kernel mode since the victim has yet to return from the sleep system call.

The attacker’s “malware” function, void funcB(void), outputs “I am B\n” by calling kprintf(). When the victim process wakes up from sleep and outputs “I am B\n” we know that the attacker has been successful in hijacking the victim process. That is, the attacker has successfully induced the victim process to execute code that is not part of the victim’s code. If funcB() were to call kill() to terminate the victim process after printing “I am B\n”, the remainder of progA() which prints “I am A: part 2\n” would not be executed.

Clandestine attacker

The attack in 5.2 would have successfully borrowed the context of the victim process, however, by terminating the victim process without executing the remainder of the victim’s code (i.e., return from sleepms() which prints “I am A: part 2\n”) the fact that an attack had taken place would have been easily detectable. A more clandestine attacker would seek to hide his/her tracks by printing “I am B\n” (of course, in an actual clandestine attack the attacker does not announce itself on the console) and returning from funcB() back to the next instruction in progA() following the call to sleepms(). That is, funcB() runs by using the stack frame of sleepms() to call kprintf(), but then returns to the function that called sleepms() – progA() – which then prints “I am A: part 2\n”. Hence the victim process, except for the brief interlude during which its context was intercepted by the attacker, executes normally and remains oblivious to the attack.

Implement the ROP stack attack by coding main(), progA(), progB(), and funcB() and placing them in separate files following our usual convention in system/. progB() and funcB() should be written in C with in-line assembly. Explain in Lab4Answers.pdf how you go about accomplishing the goals of 5.2 and 5.3. When testing your code, first run it without activating the return address manipulation by progB() so that everything proceeds normally. Then activate progB() to play the role of a rogue attacker, first by terminating the victim after printing “I am B\n” (5.2), and then returning from funcB() as if it were sleepms() so that the victim proceeds to print “I am A: part 2\n” (5.3).

Bonus problem

Choosing to modify the return address of resched() in Problem 5 was one way to modify the run-time behavior of process progA(). The other two options were modifying the return address of ctxsw() or sleepms(). From an attacker’s viewpoint, is there a fundamental difference in modifying the return address of sleepms() versus resched()? Discuss your reasoning for both XINU and Linux/Windows in Lab4Answers.pdf. Reimplement 5.2 (without worrying about making the attack clandestine) by modifying the return address of ctxsw() to execute funcB(). Save the new attacker code, void progBB(void), in progBB.c, and void funcBB(void) in funcBB.c under system/. funcBB() is a simplified variant of funcB() that prints “I am B.\n” and calls kill() to terminate the victim process.

Turn-in instructions

  1. Format for submitting written lab answers and kprintf() added for testing and debugging purposes in kernel code:
    • Provide your answers to the questions in Lab4Answers.pdf and place the file in system/. You may use any document editing software but your final output must be exported and submitted as a pdf file.
    • For problems where you are asked to print values using kprintf(), use conditional compilation (C preprocessor directives #ifdef and #define) with macros, please follow the instructions specified in the TA Notes.
  2. Before submitting your work, make sure to double-check the TA Notes to ensure that additional requirements and instructions have been followed.
  3. Electronic turn-in instructions:
    • i) Go to the xinu-spring2019/compile directory and run “make clean”.
    • ii) Go to the directory of which your xinu-spring2019 directory is a subdirectory. (Note: please do not rename xinu-spring2019 or any of its subdirectories.)
    • iii) Type the following command

You can check/list the submitted files using

turnin -c cs354le1 -p lab4 -v

Please make sure to disable all debugging output before submitting your code.