We are given a custom compiled qemu-system-x86_64 with a coded-in custom PCI device. Whenever the device is being written to or read from a corresponding ooo_write and ooo_read functions are executed on the host process. These functions take parameters the read/write address and size. We only abused the ooo_write function to achieve RCE. The ooo_write function can malloc a buffer of controlled size which saves in a pointer array as a global variable, meaning it’s a static address in the target process. It can also free any of the pointers in that array and also allows us to write data to any allocated buffers. The vulnerability is a Use-After-Free which lets us write to any of the buffers after they have been freed. To exploit this we will use a fastbin attack to allocate a buffer on top of the pointers array and create our own pointer that points to malloc@GOT of qemu-system-x86_64. Once we have inserted a pointer to GOT we are going to overwrite malloc with a pointer to a function created to make exploitation easier that simply does “system(‘cat ./flag’)”. So whenever the binary does malloc the flag will be printed on screen.
The address parameter here contains the command to be executed (malloc/free/copy) as well as the index in the ptr_array. As you can see there is no NULLing of the pointer after free (command in case 1).
If you are not familiar with fastbin attack you can read about it here and here
Step-by-step guide to achieve RCE.
Allocate a chunk of any size and store it in the 6th index in the ptr_array. This pointer’s MSB will be used to satisfy the size check of malloc for serving a fastbin of arbitrary address (the MSB is always 0x7f in our case).
Allocate a 0x60 or 0x68 bytes so it will match with our specially crafted pointer’s MSB and save it at index 0.
Free index 0
Abuse the UAF and write at the address that was just freed. Potentially overwriting the FD pointer of the chunk at the top of the free list for size 0x60-0x68. We need to overwrite the FD with the address of the MSB of the pointer stored at index 6, plus 8.
Allocate 2 buffers of 0x60-0x68 bytes so malloc returns an address on top of the pointer array.
Write a fake pointer in the pointer array that points to malloc@GOT
Overwrite malloc@GOT with the address of the function that does “system(‘cat ./flag’)”
The biggest challenge encountered with this task was the fact that we didn’t have that much control over the pointers in any of the free lists (at least not for the 0x60-0x68 size). Qemu will often allocate our fake chunk somewhere before us if we take too much time or it will free a bunch of other same sized buffers which will move our fake pointer from the top of the free list. Because our fake chunk will not always be served in the first or second controlled allocation we don’t know at which index we are going to store this fake chunk pointer. This is the reason why we are allocating multiple 0x68 sized chunks in a loop in the final exploit as well as the reason why we are writing to all of the pointers.
If you are still unsure why we wrote 0x000000000131796d, it’s because currently 0x7fffc85c7dc0 is at the top of the free list for 0x60-0x68 bytes (0x70 with metadata). When we do an allocation of this size 0x7fffc85c7dd0 (the user buffer) will be served and it’s FD will be placed at the top of the free list, that’s our pointer 0x000000000131796d. And why exactly this address, it’s because the MSB of ptr_array will serve as it’s size which is a check that malloc performs for fastbin allocations (see below for memory layout).
Once we manage to serve our controlled pointer via malloc the pointer array will mostly look like this
Because qemu did not let us completely control the order of malloc calls in the whole program for this arena, our pointer will be at different indexes every time. We can overcome this issue by writing to all the pointers so we would eventually write to ours. What we will write is the address of malloc@GOT at index 8.
At last we overwrite malloc’s address with 0x6E65F9 which if u look in IDA it’s a function that does system("cat ./flag").
Unnecessary to show python script just solves the POW and sends the binary in base64 form to the remote machine.
Thanks to aegis and dropkick for providing the means of communication with the device