ROP Chaining: Return Oriented Programming
The purpose of this lab is to familiarize with a binary exploitation technique called Return Oriented Programming (ROP), ROP chains / ROP gadgets. The technique is used to bypass Data Execution Protection (DEP).
Don't forget to disable the ASLR for this lab to work:
1st ROP Chain
Vulnerable Code
We wil exploit the following code in a program rop1a
that is intentionally vulnerable with a classic stack-based overflow:
The above program starts executing at main()
, which calls vulnerable()
where the user supplied buffer will be copied into the variable buffer[100]
.
Note that there are 3 functions rop1
, rop2
and rop3
that are never called during the normal program execution, but that's about to change and this is the purpose of this lab - we're going to exploit the stack-based overflow and force the program to call all those rop functions one after another.
Objective
We're going to exploit the classic stack-based overflow vulnerability in the function vulnerable
in the above code to trigger the functions rop1()
, rop2()
and rop3()
sequentially, that are otherwise not called during the vulnerable program's runtime. Additionally, after the rop3()
function completes, we will make the program call the libc function exit()
, so that after the exploit completes its job, the program exits gracefully rather than with a crash.
The sequence of called functions rop1() --> rop2() --> rop3() --> exit()
forms a chain and this is where the term ROP chains come from.
Stack Layout
The key thing to understand with ROP chaining is the stack layout. In our case, the payload that we send to the vulnerable program needs to overflow the stack and populate it in such a way, that the exploited program calls our wanted functions in the following order:
rop1()
rop2()
rop3()
exit()
In other words, we need to ensure that the stack in our vulnerable program rop1a
, when the vulnerable
function completes and is about to execute the ret
instruction to return to the caller function main
, is organized like this:
If we think about the above graphic, we will realize that once the stack is overflowed, the following will happen when the vulnerable program continues its execution:
The
vulnerable
function will return/jump to therop1()
. Note that before we overflowed the stack, this would have been a return address back to themain
function, to be precise - thereturn 0
statement in line 26 as seen in the Vulnerable Code secion;Once
rop1()
completes, it will execute theret
instruction, which will pop therop2()
function address off the stack and jump to it;Once
rop2()
completes, it will execute theret
instruction, which will pop therop3()
function address off the stack and jump to it;Once
rop3()
completes, it will execute theret
instruction, which will pop theexit()
function address off the stack and jump to it;
We will later confirm this with gdb in the Inspecting the Stack section.
Payload
Based on the above graphic and stack understanding so far, our payload should look something like this:
...or for easier cross-reference - using the same colours as those seen in the above stack layout diagram:
Let's find out the values we need to populate our payload with. Compile our vulnerable program rop1a
:
Start debugging it with gdb-peda and put a breakpoint on main()
and continue execution:
Now, let's find out addresses for our functions rop1
, rop2
, rop3
and exit
:
Below shows the function addresses in gdb:
Having found the function addreses, our payload visualization can now be updated like this:
The last thing we need to know is how many AAAAs we must send in to the vulnerable
program before we can take over the EIP and overwrite the return address of the vulnerable
function and point it to our first ROP chain function - rop1
.
Below screenshot indicates that the offset of interest is 112 (0x70), or in other words, we need to send 112 A characters to smash the stack:
See below notes for more details on how to find the offset at which we can overwrite the vulnerable
function's return address:
Knowing the EIP offset, we can now now visualize the full payload like this:
Exploit
We can now construct the full payload in python and send it to our vulnerable program rop1a
like this:
If we execute it, we can see thatrop1
, rop2
and rop3
functions are called successfully as they each call their respective printf()
statements:
Note how the program did not crash with some segfault - this is because rop3
called exit
upon return. To re-inforce this understanding, we will see how that came to be in the below section.
Inspecting the Stack Layout
Let's explore the stack layout of the vulnerable program rop1a
when the vulnerable()
function gets exploited and is about to return after it completes executing - when the CPU is about to execute the ret
instruction.
Below screenshot shows the initial diagram on the left, indicating how we needed the stack to look like during the exploitation and gdb screenshots on the right, that confirm we successfully built the required stack:
From the above screenshot, note the following key points:
vulnerable()
function is about to execute theret
instruction at0x56556254
;ret
instruction will pop the top-most value from the stack, which is a memory address of therop1()
function and jump to it, this way kicking off our ROP chain execution.
Next, once rop1()
is about to return, the ret
instruction will pop the top-most value from the stack, which is a memory location of rop2()
and jump to it:
Once rop2()
is about to return, the ret
instruction will pop the top-most value from the stack, which is a memory location of rop3()
and jump to it:
Once rop3()
is about to return, the ret
instruction will pop the top-most value from the stack, which is a memory location of exit()
and jump to it:
This illustrates how we managed to build our first ROP chain by organizing the stack in such a way that forced the vulnerable program to call rop1
, which upon return called rop2
, which upon return called rop3
, which upon return called exit
:
2nd ROP Chain
Our first ROP chain called 4 functions and none of them were called with arguments. Let's build our second ROP chain that will call functions with some arguments and see how we need to build the stack this time around.
Vulnerable Code
We're going to re-use the same code, but modify it so that rop2
and rop3
functions will take 1 and 2 arguments respectively and will print them out accordingly when called:
Objective
The objective is to subvert our vulnerable program rop1b
and make it call functions rop1
, rop2
, rop3
and exit
the same way we did it with our first ROP chain, however, this time rop2
function is declared as rop2(int a)
and rop3
as rop3(int a, int b)
, meaning we will have to somehow (hint: using stack) pass 1 argument to rop2
and 2 arguments to rop3
.
Stack Layout
Below shows what the stack needs to look like this time. Annotations explain the purpose of each memory address or value on the stack:
To re-inforce, stack for our second ROP chain has the following key differences when compared to the stack of the first ROP chain:
Stack contains arguments for functions
rop2
androp3
;Stack contains 2 additional memory addresses, called ROP gadgets:
pop ret
- for popping off thearg1
argument that was passed torop2
function and then jumping torop3
(becauseret
instruction will pop therop3
address off the stack that will be at the top once thearg1
is removed from the stack, and jump to it);pop pop ret
- for popping off the 2 argumentsarg1
andarg2
(hence 2 pops) that were passed to therop3
function and then jumpt toexit
(becauseret
instruction will pop theexit
address off the top of the stack that will be there after the 2 arguments are removed).
ROP Gadgets
ROP gadgets are sequences of CPU instructions that are already present in the program being exploited or its loaded shared libraries and can be used to execute almost any arbitrary code;
ROP gagdgets most often end with the
ret
instruction;ROP gadgets bypass the DEP (NX bit protection), since there is no executable code being injected to and executed from the stack, instead existing executable code is used to achieve the same malicious intent.
In gdb-peda, we can find addresses of the 2 gadgets that we are interested in (popret
for rop2
and pop2ret
for rop3
) by issuing the ropgadet
command:
To confirm that the rop gadget does what it says it will, we can inspect the instructions for the rop gadget popret = 0x5655601e
and we will see that it indeed contains 2 CPU instrutions pop ebx & ret
:
Payload
Now that we know how the stack should look like, let's build the payload for our second ROP chain.
First off, let's get addresses of our rop1
, rop2
, rop3
and the libc exit
functions:
Let's also note the popret
and pop2ret
gagdet addresses:
Since we now know how the stack needs to look like and we have addresses for our functions and ROP gadgets, we can visualize our payload like this:
Exploit
We can now translate the above visualized payload to python like so:
Below shows how the above payload is sent to the vulnerable program rop1b
, that executes rop1
, rop2
with argument 0xbeefbeef
that gets printed out and rop3
with 2 arguments 0xdeaddead
and 0xc0d3cod3
which too get printed and finally gracefully exits:
Inspecting the Stack Layout
To avoid repeating what we saw in the Stack Layout section for our first ROP chain, let's just see how the pop2ret
ROP gadget works and how it affects the stack during execution, since that is the only difference worth mentioning:
Note the following key points from the above gif:
We're on a breakpoint inside the
rop3
function, where it's about to return by executing theret
instruction;At the top of the stack, there's an address of a
pop2ret
ROP gadget withpop edi; pop ebp; ret
instructions inside a libc shared library loaded by our vulnerable program;0xdeaddead
and0xc0d3c0d3
are on the stop of the stack, just below thepop2ret
address;Once the
ret
is executed, the code jumps to the saidpop2ret
ROP gagdet;pop2ret
instructionspop edi; pop ebp
execute and0xdeaddead
and0xc0d3c0d3
are popped from the stack;Address of the libc
exit()
function is now on top of the stack;Finally,
ret
instruction executes, which pops theexit()
address from the stack and jumps to it, completing our second ROP chain execution and gracefully closing the vulnerable program.
We could have chosen to inspect the popret
gadget and we would have seen a nearly identical behaviour to the one noted above, except that popret
would have popped only one value from the stack before executing the ret
instruction.
Useful Python
Little Endian Converter
Below is a useful python snippet that converts a given memory address, i.e 0x565561d4
, to it's little-endian format, i.e \\xd4\\x61\\x55\\x56
:
Payload Executor
Below shows an easy way to build the stack using struct.pack
that does not require us to deal with little-endiannes when specifying memory addresses in the exploit:
Once the payload is constructed, we can execute it:
References
Last updated