Lab: Program Sandboxing

Assigned
  • February 4, 2019
Due
  • February 19, 2019 by 10:30pm
Collaboration
You may work individually or in pairs on this lab. If you choose to work with a partner you may choose your own group. At the end of class on our first day with this lab I will collect a list of groups for this lab. You may not change your decision to work individually or your with your group after our first lab day.
Submitting
Send your code in a single 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.

Requirements

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:

  1. A description of how to build and run your sandboxing program
  2. A description of the capabilities you restrict with your sandbox, and the method for lifting restrictions when running a program in the sandbox.
  3. A description of why you chose the one additional program capability that your sandbox can be configured to allow.
  4. A description of each test you included, with instructions on how to build and run tests

Capabilities

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:

  • Changing the current directory
  • Creating or removing directories
  • Reading files anywhere on the system
  • Writing files anywhere on the system
  • Removing files anywhere on the system
  • Sending signals to other processes on the system
  • Performing socket operations
  • Creating new processes with fork
  • Executing new files with exec

You must also have the ability to grant specific capabilities to a sandboxed program. You must be able to…

  • Grant the sandboxed program read-only access in a specific directory and its subdirectories
  • Grant the sandboxed program read-write access (and the ability to remove files, create directories, etc.) in a specific directory and its subdirectories
  • Allow the program to call fork
  • Allow the program to call exec
  • Grant the program one additional restricted capability that is blocked by default

When 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.

Error Reporting

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.

Using ptrace for Sandboxing

There 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, &regs)) {
            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.

Submitting your work

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:

  1. A README file that explains how to use your sandboxing program and run your tests.
  2. The source code for your sandboxing program
  3. The Makefile required to build your sandboxing program. Running the command make with no arguments should build your sandboxing process.
  4. Any tests you used to verify that your sandbox works correctly should be in the 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