Understand the Powerful ROP Attack From Zero!
Implementing an ROP attack is to execute gadgets one by one that are scattered in the code space of the target program; this tutorial replicates this process.
Join the DZone community and get the full member experience.
Join For FreeWhat Is ROP?
First, let's describe a gadget. A gadget is a sequence of assembly code that ends with a jump instruction: for example, pop rax; ret;
. Jump instructions include ret
, jmp
, call
, etc. If you use the last jump instruction of each gadget to execute many gadgets one by one, that’s return-oriented programming (ROP): gadget1 -(jump)> gadget2 -(jump)> gadget3 -(jump)>…
Gadgets extensively exist in the vulnerable binary executable. You need to scan the binary executable, find its gadgets, exploit a vulnerability to execute some useful gadgets, and eventually finish your attack.
Implement a Real ROP Attack
Environment
Download the necessary files from here. Bug
is the vulnerable binary executable. exploit_gen.c
generates a binary data file called “exploit." The data file is the input of bug
. exploit_gen.c
may not be able to exploit the bug on your machine. Follow the steps in the next section. Do your experiments and modify exploit_gen.c
.
Here is the source code of bug
:
xxxxxxxxxx
include <stdio.h> int fun(FILE* f) { char buf[2]; char i = 0; char c; while (1) { c = fgetc(f); if (c != EOF) buf[i] = c; else break; i++; } return 0; }
int main(int argc, char* argv[]) { FILE* f = fopen("exploit", "rb"); printf("Address of printf: %p\n", printf); printf("Address of fun: %p\n", fun); fun(f); fclose(f); return 0; }
In function fun
, we write whatever in the file f
into the buffer buf
until the end of the file. This is definitely a stack overflow bug.
Here is how we will exploit the bug: First, we search for gadgets within bug
. Second, we search for one gadget within the libc
file it uses. Third, we use the bug to overwrite the return address of fun
. The program will be redirected to execute the gadgets. The gadgets will prepare the memory and satisfy the constraints of the one gadget. Eventually, the last gadget will direct the program to execute one gadget. And we will have a shell. The above exploitation process can be summarized by this workflow: bug -(overwrite)> return address -(jump)> gadgets -(jump)> one gadget
.
Steps To Spawn a Shell
- Find the gadgets within
bug
by runningROPgadget –binary bug
. ROPgadget is a tool for discovering gadgets within binary executables. Install it from the internet. The following picture shows part of the output. Each line is a gadget. They all end with a jump instruction.
- Find the
libc
file name the bug program uses. Use the following commands and you will find out thelibc
file name it uses: 1)gdb bug
2)r
. This command should be run withingdb
. 3)ctrl + c
. Press the two keys together on your keyboard. 4)vmmap
. To use this command, you need to install a plugin such asgef
togdb
. You will find out thelibc
name from the output of this command:
- Find one gadget within the
libc
file. Open your terminal and runone_gadget /usr/lib/x86_64-linux-gnu/libc-2.31.so
. Remember to replace thelibc
file name with yours. You will get an output like the following. This is a one-gadget. Its offset is0xcbcda
.
- Choose gadgets that satisfy the constraints of the one gadget. Clearly, the above gadget requires
r12
andr13
to be0
. So you need to choose some gadgets to set their values before calling the one-gadget. Look at what gadgets we have now. You will find that the first gadget is perfect to setr12
andr13
:pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
. And its offset within the bug program is0x1273
. - Inspect the stack. Run bug within
gdb
. Put a breakpoint at function fun. Observe the stack:
There are 3 variables in function fun
. Their addresses are given below.
From the above two pictures, you can clearly tell the structure of the stack:
xxxxxxxxxx
0x7fffffffdf5c buf[0] 0x7fffffffdf5e c 0x7fffffffdf5f i 0x7fffffffdf60 rbp 0x7fffffffdf68 return address
- Prepare the stack. We want to overwrite the return address and jump to execute the selected gadget, and eventually jump to execute the selected one gadget. So this is how we overwrite the stack:
The data structure in the exploit array is the same as the stack structure we observed. We will use buf
to overwrite c
, i
, rbp
, return address, and more. At the first line of the array, we use three characters to overwrite buf
and c
. It doesn’t matter what the characters are because we don’t use them in the exploitation. They are just placeholders. Next, we use 0x03
to overwrite i
. This is crucial. We use i
to access buf
and overwrite the stack byte by byte. So we don’t want to change i
. When we use buf
to overwrite i
, i
is exactly 0x03
. At the second line, the data will overwrite the 8 bytes at address 0x7fffffffdf60
. We don’t care about what the data is, because we don’t use it in the exploitation.
The third line overwrites the return address so that the program will jump to execute our selected gadget. We need to calculate the address of the gadget. It equals to bug’s code segment base address + the gadget offset
. And the base address equals to address of fun – fun’s offset
. Let’s see how we get the values. If you look at the source code of bug
, you will see that the address of fun
is given by the program. It’s 0x555555555155
for me. And we have already acquired the offset of the chosen gadget. It’s 0x1273
. fun
is a function symbol in bug
. Its offset is recorded in bug’s
symbol table. Run objdump -t bug | grep fun
and you will find the offset of fun in the output: 0x1155
. So the gadget’s address = fun’s address – fun’s offset + the gadget’s offset = 0x555555555155 – 0x1155 + 0x1273 = 0x555555555273
. This is exactly our third line.
The ret
instruction in function fun
will return to 0x555555555273
and execute our gadget: pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
. The gadget takes data from the stack. So we need to prepare the stack accordingly. Beginning from the fourth line in the exploit array, each line provides a value to an instruction in the gadget. You see that the values set r12
and r13
to 0
. It satisfies the requirements of our one gadget. Eventually, we use the last ret
instruction in the gadget to jump to execute our one gadget. So we need to give the ret
instruction our one gadget’s address.
One gadget’s address = libc base address + one gadget’s offset = printf’s address – printf’s offset + one gadget’s offset
. printf’s
address is given by the bug
program: 0x7ffff7e3bcb0
. The one gadget’s offset is acquired in the above steps: 0xcbcda
. printf
is a function symbol that exists in libc’s
symbol table. Observe libc’s
symbol table with this command: objdump -T /usr/lib/x86_64-linux-gnu/libc-2.31.so | grep
printf
and you will find the offset of printf
is 0x56cb0
. Remember to replace the path to libc
with yours. So the one gadget’s address = 0x7ffff7e3bcb0 – 0x56cb0 + 0xcbcda = 0x7ffff7eb0cda
. This value is exactly the last line of the exploit array. The last ret
instruction in the gadget will use this value and return to execute our one gadget.
- Observe the exploitation. After correctly setting the exploit array in
exploit_gen.c
, compile and run it. A data file namedexploit
will be generated. Runbug
withingdb
. Put a breakpoint at line 14 (the return instruction offun
). Run the program. Now the stack becomes this:
xxxxxxxxxx
0x7fffffffdf5c 0x41 buf[0] 0x7fffffffdf5e 0xff c 0x7fffffffdf5f 0x40 i 0x7fffffffdf60 0x00 rbp 0x7fffffffdf68 0x555555555273 ret -> gadget 0x7fffffffdf70 0x00 pop rbp 0x7fffffffdf78 0x00 pop r12 0x7fffffffdf80 0x00 pop r13 0x7fffffffdf88 0x00 pop r14 0x7fffffffdf90 0x00 pop r15 0x7fffffffdf98 0x7ffff7eb0cda ret -> one gadget
The first column is the address. The second is the value at the address. The third is the variable or instruction that uses the value. Now if we use the command n
to run the program line by line, we will see how the instructions and values lead us to a shell. After the ret
instruction of fun
is executed, the program goes to execute at address 0x555555555273
:
This is our gadget! The gadget takes data from the stack and puts them to the registers. If we execute until the last ret
instruction, we will see from rbp
to r15
all become 0
. After we execute the ret
instruction, the program goes to execute our one gadget:
r12
and r13
are arguments of execve
. Now they are correctly set to 0
. Run the 4 instruction and we will have a shell:
- Spawn a shell without
gdb
. Our exploit cannot bypassASLR
. So you need to shut downASLR
. First, usesu
to get into root mode. Second, shut downASLR
withecho 0 > /proc/sys/kernel/randomize_va_space
. Now if you executebug
, you will see a shell is spawned:
Summary
The post used a concrete example showing you how to implement an ROP attack. Implementing an ROP attack is to execute gadgets one by one that are scattered in the code space of the target program. The execution from one gadget to another is connected by the last jump instruction of each gadget. The jump instructions include jmp, ret, call,
etc.
Published at DZone with permission of Kenny P.B.. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments