Lab1: Shadow Stack

Download the testing case from here. You need a Linux environment for all the labs.

0. Preparation

Most labs require a posix environment (Linux/MacOS). In case you don’t have one, you can setup a virtual machine as follows. Even if you already have a Linux machine, I still recommend using a virtual machine to avoid potential problems caused by libraries.

  1. Download and install Virtualbox/Vagrant
  1. Add guest OS and run the VM

We will use Ubuntu 18.04 as an example.

# download a 64-bit VM
[host] $ vagrant box add ubuntu/bionic64
==> box: Loading metadata for box 'ubuntu/bionic64'
    box: URL: https://vagrantcloud.com/ubuntu/bionic64
==> box: Adding box 'ubuntu/bionic64' (v20210325.0.0) for provider: virtualbox
    box: Downloading: https://vagrantcloud.com/ubuntu/boxes/bionic64/versions/20210325.0.0/providers/virtualbox.box
==> box: Successfully added box 'ubuntu/bionic64' (v20210325.0.0) for 'virtualbox'!

# move to your working directory
[host] $ mkdir cs250
[host] $ cd cs250

# initialize the VM
[host] $ vagrant init ubuntu/bionic64
A `Vagrantfile` has been placed in this directory. You are now
ready to `vagrant up` your first virtual environment! Please read
the comments in the Vagrantfile as well as documentation on
`vagrantup.com` for more information on using Vagrant.

# launch!
[host] $ vagrant up
Bringing machine 'default' up with 'virtualbox' provider...
==> default: Importing base box 'ubuntu/bionic64'...
...

[host] $ vagrant ssh
Welcome to Ubuntu 18.04.5 LTS (GNU/Linux 4.15.0-140-generic x86_64)
...
vagrant@ubuntu-bionic:~$
  1. Install gcc and 32-bit libraries
$ sudo dpkg --add-architecture i386
$ sudo apt-get update
$ sudo apt-get install build-essential wget libc6:i386 libncurses5:i386 libstdc++6:i386 gcc-multilib
  1. Install Intel Pin
$ wget https://software.intel.com/sites/landingpage/pintool/downloads/pin-3.18-98332-gaebd7b1e6-gcc-linux.tar.gz
$ tar zxf pin-3.18-98332-gaebd7b1e6-gcc-linux.tar.gz

# try build the example
$ cd pin-3.18-98332-gaebd7b1e6-gcc-linux/source/tools/SimpleExamples
$ make obj-intel64/opcodemix.so

# check if the tool works
$ ../../../pin -t obj-intel64/opcodemix.so -- /bin/ls
$ less opcodemix.out

Intel has also provided version for MacOS, you can follow the instructions to setup.

You are highly recommended to read the user guide at https://software.intel.com/sites/landingpage/pintool/docs/98332/Pin/html/index.html carefully, to understand how to run an existing pintool, how an existing pintool is implemented, and how to write your own pintool. Whenever you have questions, this is the best place to refer to.

1. Objectives

Our objective for this lab assignment is to implement a shadow stack through dynamic binary instrumentation using Pin. Shadow stack is a well-known and effective defense mechanism to defeat control-flow hijacking attacks that aim to overwrite a return address on the stack. The general algorithm works like below: for each call instruction, identify the return address (or the next instruction after the call instruction), and push it onto the shadow stack; for each ret instruction, identify the return target and see if it matches with the value on the top of the shadow stack. If so, pop up the value from the shadow stack; otherwise, report an attack.

2. Workflow

First of all, you need initialize Pin, and then register an instrumentation function with Pin, and start the program. Normally, you will do it in the main function, like below:

int main(int argc, char *argv[]) {
  PIN_InitSymbols();
  if (PIN_Init(argc,argv)) {
    return Usage();
  }
  TRACE_AddInstrumentFunction(Trace, 0);
  PIN_AddFiniFunction(Fini, 0);
  PIN_StartProgram();
  return 0;
}

In this example, we use Trace_AddInstrumentFunction to instrument a trace at a time. You can also use INS_AddInstrumentFunction to instrument an instruction at a time. Then in your instrumentation function, you will enumerate each instruction to identify call and ret instructions. In particular, INS_IsCall and INS_IsRet can be used to determine if the specified instruction is a call or ret.

Once a call or ret instruction is identified, use INS_InsertCall to instrument that instruction. Please read the documentation for INS_InsertCall carefully.

VOID LEVEL_PINCLIENT::INS_InsertCall ( INS      ins,
                                       IPOINT   action,
                                       AFUNPTR  funptr,
                                       ...
                                     )

Especially after the first three arguments, you can specify a list of arguments to be passed to funptr. For example, IARG_INST_PTR will pass the address of the instrumented instruction, and IARG_BRANCH_TARGET_ADDR will pass the target address of this branch instruction.

A possible way to instrument a call instruction is like below:

INS_InsertCall(ins, IPOINT_TAKEN_BRANCH, AFUNPTR(do_call),
               IARG_BRANCH_TARGET_ADDR, IARG_RETURN_IP,
               IARG_THREAD_ID, IARG_END);

In this example, Pin will pass the call target, the return address, and the thread ID to a function called do_call specified by the developer. So in the do_call function, you would push the return address on the shadow stack.

Similarly, you can instrument a ret instruction like below:

INS_InsertCall(insn, IPOINT_BEFORE, AFUNPTR(do_ret),
               IARG_INST_PTR, IARG_BRANCH_TARGET_ADDR,
               IARG_THREAD_ID, IARG_END);

In this example, you register a do_ret function for each ret instruction, and pass the address of the instrumented ret instruction, the branch target (which is the return address), and the thread ID. Since in the do_ret function, you would check the top of the shadow stack and see if it matches with the branch target. If so, you pop up from the shadow stack.

Then you are pretty much done with a basic version. Of course, a more complete implementation would need to handle multiple threads. It means that one shadow stack must be associated with each thread. More specifically, you need to create shadow stack in the thread local storage. In this lab assignment, you are not required to deal with multiple threads.

3. Submission

Please submit your report through iLearn, preferably in PDF format. In the report, please list your complete source code with sufficient explanation and output messages.

Run some normal programs like ls and ps, to show that your pintool won’t raise any false alarm. Compile example01.c as a 64-bit binary

gcc –g –fno-stack-protector –z execstack –o example01 example01.c

Run this binary with your pintool with a very long input and show that your pintool can detect the stack overflow before the program crashes.