pwnfreehard

velvet-table

umdctf

Task: a stripped PIE casino allocator exposes seat-based heap operations, encrypted inspection output, and a payout path guarded by a stack checksum. Solution: combine UAF and post-ledger OOB write for safe-linked tcache poisoning onto the payout stack frame, leak and decrypt stack data, replace the reject callback with the hidden win function, repair the checksum, and trigger payout.

$ ls tags/ techniques/
tcache_poisoningstack_target_allocationchecksum_repairhidden_function_callreversible_keystream_leak

$ cat /etc/rate-limit

Rate limit reached (20 reads/hour per IP). Showing preview only — full content returns at the next hour roll-over.

velvet-table — UMDCTF

Challenge

Remote: nc challs.umdctf.io 30304
Binary: velvet-table

Protections:

  • PIE
  • Full RELRO
  • NX
  • no canary
  • SHSTK
  • IBT

The program is a stripped menu-driven allocator with 16 seat records. Each seat stores:

  • ptr
  • size
  • occupied

Two nearby internal callbacks are especially important:

  • 0x1b50 prints ticket rejected.
  • 0x1b60 prints yay. and calls system("/bin/sh")

The goal is to redirect the payout path to the hidden function.

Analysis

Reversing the binary shows three key primitives.

1. Use-after-free via cashout

For 0x80 chunks, cashout frees the chunk but leaves the seat pointer behind. That gives a reliable dangling-pointer primitive.

2. Unchecked write after settle-ledger

After enough actions, settle-ledger enables a stronger update path that performs:

memcpy(ptr, buf, len)

with no bounds check. Combined with the dangling pointer, this lets us overwrite freed tcache metadata.

3. Reversible leak via inspect

inspect prints up to 0x40 bytes from a seat, but XORed with an internal keystream. That is still useful: if we first write known plaintext into a live chunk, then inspect it, we recover the keystream with:

keystream = ciphertext ^ plaintext

Then later inspect outputs can be decrypted the same way.

Payout target on the stack

The service prints a table marker. That value is enough to reconstruct the payout frame location on the stack:

low32 = marker ^ 0x9ac90307 stackbuf = 0x7ff000000000 | (low32 << 4) saved_highbits = ((low32 ^ 0x5a17c3d9) & 0xffffffff) << 32

Relevant fields inside that stack buffer:

  • function pointer: stackbuf + 0x20
  • checksum: stackbuf + 0x28

The integrity formula is:

checksum = saved_highbits ^ funcptr ^ 0x686f7573655f6564

So the full plan is:

...

$ grep --similar

Similar writeups

  • [pwn][free]bookmakerumdctf
  • [reverse][free]rouletteumdctf
  • [pwn][Pro]Tastegrodno_new_year_2026
  • [pwn][free]Voidhackthebox
  • [pwn][free]ipv8umdctf