代写操作系统作业,优化 XINU 的 IPC
服务。
![IPC](https://upload.wikimedia.org/wikipedia/commons/thumb/e/ef/ArchitectureCloudLinksSameSite.png/260px-
ArchitectureCloudLinksSameSite.png)
Objectives
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.
Readings
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
- 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. - Before submitting your work, make sure to double-check the TA Notes to ensure that additional requirements and instructions have been followed.
- 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.