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.

/* Perform vtable pointer validation. If validation fails, terminate the process. */staticinlineconststruct_IO_jump_t*IO_validate_vtable(conststruct_IO_jump_t*vtable){/* Fast path: The vtable pointer is within the __libc_IO_vtables section. */uintptr_tsection_length=__stop___libc_IO_vtables-__start___libc_IO_vtables;constchar*ptr=(constchar*)vtable;uintptr_toffset=ptr-__start___libc_IO_vtables;if(__glibc_unlikely(offset>=section_length))/* The vtable pointer is not in the expected section. Use the slow path, which will terminate the process if necessary. */_IO_vtable_check();returnvtable;}

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().

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!

if(last_stamp!=_IO_list_all_stamp){/* Something was added to the list. Start all over again. */fp=(_IO_FILE*)_IO_list_all;last_stamp=_IO_list_all_stamp;}elsefp=fp->_chain;// get next _IO_FILE object to close }}

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:

void_IO_wstr_finish(_IO_FILE*fp,intdummy){if(fp->_wide_data->_IO_buf_base&&!(fp->_flags2&_IO_FLAGS2_USER_WBUF))(((_IO_strfile*)fp)->_s._free_buffer)(fp->_wide_data->_IO_buf_base);//we can control this!fp->_wide_data->_IO_buf_base=NULL;

_IO_wdefault_finish(fp,0);}

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.