The binary is a menu-driven program that allows users to alloc chunks and place them into 1 of initially 10 available slots.
As we can see, only chunks of size 0x300 are allocated.
When a chunk is allocated, the program asks the user to specify a slot from 0-9, which is used as an index into the allocs global, to which, a pointer to the chunk is written.
The user is also able to write to these chunks, print them, and free them.
The binary uses the read() and write() syscalls calls directly throughout the program, so _IO_2_1_stdin_ and _IO_2_1_stdout_ are not used.
There are several vulnerabilities in the program in the free_it() function.
When a chunk is freed, its content is not zero’d out, and a pointer to the chunk is not zero’d out in the allocs global.
Additionally, when a chunk is written to, there is no check to see whether the chunk is currently allocated, allowing us to write into freed chunks.
We can use these vulnerabilites to leak a libc and heap address, and to also perform an unsorted bin attack.
Infoleak
Getting an infoleak for this binary is fairly trivial given the aforementioned vulnerabilities.
We can leak both a heap and libc pointer by doing the following:
allocating 4 smallbin sized chunks
freeing the 1st and 3rd chunk we allocate
printing out and parsing the resulting pointers contained within the chunks
Crafting a “ghost” chunk
From here, we can perform an unsorted bin attack, since we can only allocate small chunks, but are able to write into free chunks.
What is a good target to overwrite using this attack, though?
Usually, a good target would be IO_list_all, but since we can only allocate chunks of size 0x300, we can’t remove a chunk from the unsorted bin and place it into smallbin[4] where it would be positioned to serve as the new IO_list_all->_chain pointer. Or can we?
As it turns out, we can actually craft a fake “ghost” chunk of size 0x61, place it in the unsorted bin, and remove it from the unsorted bin so that it is placed it into smallbin[4], where it would be treated as the IO_list_all->_chain pointer to an _IO_FILE_plus object.
To do this, we need to use our ability to write into freed chunks to overwrite unsorted_bin->TAIL->BK with the address of our “ghost” chunk.
Doing this will trick the memory allocator into thinking there is an extra chunk in the unsorted_bin when there actually isn’t.
Then, after a malloc(0x300) call, our unsorted_bin->TAIL chunk will be able to satisfy the request and be removed from the unsorted bin. At the same time, the memory allocator will set what it thinks is the previous unsorted bin chunk as the new unsorted_bin->TAIL. However, since we’ve overwritten unsorted_bin->TAIL->BK with the address of our “ghost” chunk, this means the new unsorted_bin->TAIL will actually now point to our “ghost” chunk.
After another malloc(0x300) call, our “ghost” chunk will actually be removed from the unsorted bin and placed in smallbin[4], since it is too small to satisfy the allocation request. This is exactly where we want a pointer to our “ghost” chunk to reside. Why? Because now we can perform an unsorted bin attack to overwrite IO_list_all with &main_arena.top, positioning smallbin[4] in same location where IO_list_all->_chain will reside.
Now that we know how to overwrite IO_list_all->_chain with a pointer to a chunk whose contents we control, we can proceed with crafting the actual contents of this chunk.
Normally, we would craft this fake _IO_FILE_plus object so that the vtable member would point to a fake vtable that we control, but starting from glibc 2.24, we can no longer do this due to an additional call to _IO_vtable_check() where the _IO_FILE_plus->vtable pointer is validated.
As we can see from the above snippet, the vtable pointer must be within the __libc_IO_vtables range, or it will eventually trigger a glibc detected an invalid stdio handle fatal error in _IO_vtable_check().
So, our vtable must lay somewhere between 0x7ffff7dcd8c0 and 0x7ffff7dce628, preventing us from simply using a heap chunk we control as the vtable pointer.
So given this, is there any way to bypass this check to get RCE still?
Bypassing _IO_vtable_check
Fortunately, there is a nice vtable we can use that exists in this valid vtable range.
The reason why this particular address is such a good target, is because if we set our fake _IO_FILE_plus->vtable to 0x7ffff7dcdc78, we will call _IO_wstr_finish() if _IO_OVERFLOW(fp) in _IO_flush_all_lockp() is called!
And what is so special about _IO_wstr_finish(), you ask?
Well, since we can control its first argument, fp, we can also control the function pointer that it calls if some conditions are met:
They key factor in this whole attack is being able to craft our _IO_FILE_plus object such that the conditions are met in _IO_flush_all_lockp() that are needed to call _IO_OVERFLOW(fp), and such that the conditions are met in _IO_wstr_finish(fp) that are needed to call (((_IO_strfile *) fp)->_s._free_buffer) (fp->_wide_data->_IO_buf_base);.
In _IO_flush_all_lockp(), the conditions we need to satisfy are:
fp->_mode <= 0
fp->_IO_write_ptr > fp->_IO_write_base
And in _IO_wstr_finish(), the conditions we need to satisfy are:
fp->_wide_data->_IO_buf_base
!(fp->_flags2 & _IO_FLAGS2_USER_WBUF)
One trick to note is that when fp->_wide_data->_IO_buf_base is checked, fp->_wide_data is implicitly treated as a _IO_wide_data object, and not as a _IO_FILE object.
Because of this,fp->_wide_data->_IO_buf_base will actually be located where fp->write_end is located, if we point fp->_wide_data back to fp itself, due to the lack of a int flags member in the _IO_wide_data struct.
And if we craft our _IO_file_plus object such that (((_IO_strfile *) fp)->_s._free_buffer) contains one_shot, we should be able to get ourselves a shell.
To put everything together, this is what our fake _IO_FILE object should look like:
So now, that we have a good idea of how we’re going to gain control of RIP, we can proceed with the unsorted bin attack
Unsorted bin attack
To carry out the unsorted bin attack, we can simply free a chunk to place it in the unsorted bin, overwrite its BK ptr with IO_list_all-0x10, and then malloc() it out to overwrite IO_list_all with &main_arena.top.
After this is done, we simply free a chunk twice to trigger a double-free error which will start the abort sequence and eventually call our one_shot “magic” address.
Putting everything together, the following exploit will give us a shell.