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.
- Download and install Virtualbox/Vagrant
- Download and install the latest version of virtualbox at https://www.virtualbox.org/wiki/Downloads
- Download and install the latest version of vagrant at http://www.vagrantup.com/downloads.html
- Windows 10: install the Windows Subsystem for Linux (WSL 2 is preferred) https://docs.microsoft.com/en-us/windows/wsl/install-win10
- Windows 10 (optional): install Windows Terminal https://www.microsoft.com/en-us/p/windows-terminal-preview/9n0dx20hk701
- Windows (pre-10): Download and install git at http://git-scm.com/download/win
- Windows (pre-10): SSH client PuTTY or Cygwin.
- 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:~$
- 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
- 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.