sandbox.tar.gz attachment, which should contain a directory with all your source
code, tests, and Makefile. You can find instructions on how to create a .tar.gz archive at the bottom
of the assignment.
For this introductory lab, you will implement a sandboxing system using the ptrace API.
Your program must be written in C or C++.
Starting a program in your sandbox should look something like this:
$ ./sandbox -- ./test-program
This command should start the program test-program inside your sandbox, which will prevent it from accessing any files other than libraries loaded as part of program startup.
The sandbox should also block system calls that would allow test-program to perform externally-visible operations like calling fork or exec.
To do this, you will need to use ptrace to catch system calls and allow or disallow them based on the sandboxing restrictions.
It should be possible to grant a sandboxed program some additional capabilities.
You are free to design whatever interface you like for this feature, but I should be able to grant a sandboxed program read-only or read-write access to specific directories, the ability to fork or exec, and at least one other capability you believe might be important to grant to some sandboxed programs that you will need to block by default.
One example way of granting capabilities is with command line arguments:
$ ./sandbox --read /usr/share/dict --read-write . --exec --fork -- ./another-test
Processing options from the command line or some sort of configuration file is not the primary goal of this project, so it should not consume too much of your time.
You are welcome to use example code or libraries to deal with options.
You may want to use getopt, especially if you write your program in C.
Regardless of what you choose, your implementaiton should work on MathLAN.
If you would like to use a library that is not currently available on MathLAN I will do my best to request it before the deadline, but I cannot guarantee the package will be available so you may need to find a workaround.
You will need to turn in the code for your sandboxing program, a Makefile, a directory with thorough tests for your sandboxing, and a README file.
Your README must include:
You will have some flexibility in how you limit the capabilities of sandboxed programs, but the goal is to run an untrusted program in a way that prevents it from interfering with the rest of your system. Programs must be blocked from performing the following operations by default:
forkexecYou must also have the ability to grant specific capabilities to a sandboxed program. You must be able to…
forkexecWhen choosing what capability to allow, you should be able to argue that this capability is useful for running sandboxed programs. You are free to block additional operations by default and lift them with options; your additional capability does not need to be one of the blocked operations listed above.
If a program running in your sandbox tries to perform a blocked operation, your sandbox should stop the program and report an error. This error report should include a human-readable description of what the program tried to do that was blocked.
ptrace for SandboxingThere are likely several different good approaches to sandboxing with ptrace, and you are free to use any of them.
One that is likely to work well is with the PTRACE_SYSCALL option, which runs the traced program until it makes a system call.
The tracing program (your sandbox) can then check the system call and its arguments.
You will need to refer to a table of system calls to use this approach.
Double-clicking a system call on the previously linked page will show you which registers hold each argument to the system call.
There are some good guides on how to use ptrace online, though some are a bit outdated.
The following example should be a good starting point for tracing system calls:
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <asm/unistd.h>
#include <sys/ptrace.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/user.h>
int main(int argc, char** argv) {
// Call fork to create a child process
pid_t child_pid = fork();
if(child_pid == -1) {
perror("fork failed");
exit(2);
}
// If this is the child, ask to be traced
if(child_pid == 0) {
if(ptrace(PTRACE_TRACEME, 0, NULL, NULL) == -1) {
perror("ptrace traceme failed");
exit(2);
}
// Stop the process so the tracer can catch it
raise(SIGSTOP);
// TODO: Do some work in the sandboxed child process here
// As an example, just run `ls`.
if(execlp("ls", "ls", NULL)) {
perror("execlp failed");
exit(2);
}
} else {
// Wait for the child to stop
int status;
int result;
do {
result = waitpid(child_pid, &status, 0);
if(result != child_pid) {
perror("waitpid failed");
exit(2);
}
} while(!WIFSTOPPED(status));
// We are now attached to the child process
printf("Attached!\n");
// Now repeatedly resume and trace the program
bool running = true;
int last_signal = 0;
while(running) {
// Continue the process, delivering the last signal we received (if any)
if(ptrace(PTRACE_SYSCALL, child_pid, NULL, last_signal) == -1) {
perror("ptrace CONT failed");
exit(2);
}
// No signal to send yet
last_signal = 0;
// Wait for the child to stop again
if(waitpid(child_pid, &status, 0) != child_pid) {
perror("waitpid failed");
exit(2);
}
if(WIFEXITED(status)) {
printf("Child exited with status %d\n", WEXITSTATUS(status));
running = false;
} else if(WIFSIGNALED(status)) {
printf("Child terminated with signal %d\n", WTERMSIG(status));
running = false;
} else if(WIFSTOPPED(status)) {
// Get the signal delivered to the child
last_signal = WSTOPSIG(status);
// If the signal was a SIGTRAP, we stopped because of a system call
if(last_signal == SIGTRAP) {
// Read register state from the child process
struct user_regs_struct regs;
if(ptrace(PTRACE_GETREGS, child_pid, NULL, ®s)) {
perror("ptrace GETREGS failed");
exit(2);
}
// Get the system call number
size_t syscall_num = regs.orig_rax;
// Print the systam call number and register values
// The meanings of registers will depend on the system call.
// Refer to the table at https://filippo.io/linux-syscall-table/
printf("Program made system call %lu.\n", syscall_num);
printf(" %%rdi: 0x%llx\n", regs.rdi);
printf(" %%rsi: 0x%llx\n", regs.rsi);
printf(" %%rdx: 0x%llx\n", regs.rdx);
printf(" ...\n");
last_signal = 0;
}
}
}
return 0;
}
}
You should be able to run the code above and see it trace the execution of the ls command.
You’ll notice that ptrace stops the process immediately before and after each system call;
you only need to check if a system call is allowed before it executes, so you may need to change or rewrite the loop in the code above.
Remember that processes do not share memory.
For system calls that use pointer parameters, the pointers will refer to memory in the sandboxed process, not the tracer.
It is always unsafe to dereference these pointers in the sandbox process.
Instead, you will need to use ptrace to access memory in the traced process.
Please send your code by email in an archive named sandbox.tar.gz. I will not accept labs submitted with multiple attachments, so please read the instructions carefully.
Your code, tests, and Makefile should be in a directory.
These instructions assume that directory is named sandbox, and is placed in your home directory.
First, make sure you are submitting all of the following components:
README file that explains how to use your sandboxing program and run your tests.Makefile required to build your sandboxing program. Running the command make with no arguments should build your sandboxing process.tests directory. Each test should be in its own directory with a Makefile that builds the test. I will refer to your README file for descriptions of tests and directions for running them.The following steps will create an archive to submit:
$ tar cvzf sandbox.tar.gz sandbox