Tags: pwn cpp 

Rating:

Raddest DB

There are two bugs we used

Type confusion

When a (key,value) is added to a db, the db checks if there exists a value with that key. If this is true, the db only changes the value field of the object and leaves the vtable field initialized. This leads to a type confusion bugs, giving us 3 primitvies:

1. Type confusion from `string`->`int`: consider any string pointer as an int, giving us leaks
2. Type confusion from `float`->`string`: consider any address as a string, which gives use an arbitrary read primitive

With these two powerful infoleak bugs, we can get the libc base as well as the heap base.

Fakeobj???

We found a bug that wasn't found via analysis. It occurs if we do the following (One of my teammates found it and I still don't know how it was triggered, but we can make some guesses that it is a UAF)

cmd("create db") 

cmd("store db int 1 1337")

cmd("getter db 1 2")
p.sendline("echo {}".format("A"*0x10+p64(fakeobj_addr).strip("\x00")))
p.sendline("empty")

cmd("print db")

I made some speculations about how this happened:

  1. Each entry (key,value) is stored as a 0x18 sized structure, which contains the pointer to the object. So in code it is like this:
struct entry {
    char unknown[0x10];
    struct object *obj;
}
struct object {
    funcptr_t *vtable;
    union value {
        unsigned long int_form;
        char *string_form;
        double float_form;
    }
}

When we empty a db, the entry structure is free'd and the BUF in echo{BUF} takes that chunk if BUF is of appropriate size. Afterwards the reference is not removed and we have a fakeobj primitive, which gives us a single RIP control with no arguments controlled. In code, it is like this: this->vtable->func1(this, aux);

We have control to the entire this structure, so we can control func1. I changed func1 to all one shot gadgets (found using david942j's one_gadget) but none of them popped a shell.

I decided to expand this primitive to an arbitrary write primitive by changing func1 to gets.

Before triggering the vtable function we free the chunk right under where the this object is located, turning it into a freed, size 0x20 tcache bin. Overflowing this enables us to overwrite a tcache bin, which gives use arbitrary allocation. So, I changed the fd field of the next tcache chunk entry to __free_hook and got __free_hook allocated and wrote system to it.

I tried some commands that would trigger free('/bin.sh;'. (The way was to enter /bin/sh;[LOTS OF SPACES] as the command because when the string is extended to some length its original buffer is freed (property of c++'s std::vector)

Original writeup (https://github.com/pr0cf5/CTF-writeups/tree/master/2019/fbctf/raddest_db).