Well, this was one of the challenges on angstromCTF 2018, which kept me busy for a while (almost sure, I just overcomplicated it, but well ;-))
It contained an obvious UAF in the store_object function:
But where to go from with that…
Well, first things first, we need some leaks. For this, we just create some objects in the deposit box and then retrieve some of them to fill the unsorted bin list and their FD/BK pointers.
We can then recreate an entry in the deposit box, but this time, we’ll specify an invalid object size, so the chunk gets freed, before we fill it.
When store_object calls write_account after this, it will open the .account file and fopen will allocate a chunk for it’s file structure. Since our deposit chunk doesn’t match the size of the requested chunk of fopen, it will be put into the bin list and linked accordingly (overwriting the FD pointer of our chunk with the address of one of the previously freed chunks, which happens to be the name of our stored object).
Thus, we can leak a heap address from the name of the last deposited entry.
After this, we’ll store another object in the deposit box, which will then get served one of the initial freed chunks, and thus our freed chunk will get linked to main_arena (and we can now leak a pointer to main_arena via the name of this chunk).
As a side note, since many people seemed to have trouble with the challenge on the remote machine. You couldn’t execute the bank_roppery binary in the folder /problems/bank_roppery, since it would segfault, because it wasn’t allowed to write in that folder.
Easy solution for this, just create a symbolic link in your home directory to the binary, and it will write all its data to the home dir (where you obviously have write permissions).
So, we have the leaks, but still no useful overwrites. No pointers on the heap, which we could edit, even if we would be able to overwrite them. What a bummer…
What could we use that UAF for, then. Well, let’s take a look, how the new objects gets inserted into our deposit list.
It’s a single linked list, and to add an object to the list, the function store_object iterates over all next pointers, until it finds an empty one, and links our chunk there.
Let’s just think about the current situation in our heap.
We currently have 4 objects linked in the sdbox list.
The object 3 is in our sdbox list and also sits around in unsorted bin list (since it was freed before filling it), just waiting to get allocated on the next malloc. If we create another object now, malloc will serve us the address of chunk 3, which we can then overwrite. But if we just store another object there, we can only overwrite name, owner, etc. Not very useful.
What other objects can we create?
Both types are of the same size.
If we examine both structs, you can see that the memo field of check_t is overlapping the same area, which would be covered by the next pointer in a sdobj_t object.
Thus, if we now create a check, it will be created in the chunk, that is object 3 and we can overwrite the next pointer of chunk 3 via the memo field of the check. On the next store_object it would create an object on the heap, and store the address of this object at next+0x84 (next->next).
There is only one problem with this: We cannot put an arbitrary address there. It has to point to a value of 0x0, so store_object will stop iterating and write the address of the next chunk there.
But let’s sum this up
Overwrite next ptr of object 3 with a check
On the next time, we call store_object it will create a chunk on the heap
and store the address of this object at next->next of object 3 (which we overwrote with the previous check)
Thus, we can overwrite an arbitrary address (as long as it contains 0x0) with the address of our object.
Most interesting pointers (got, IO_list_all, etc.) will already contain a value, so we won’t be able to use them.
But alas, we’re dealing with libc 2.23, so _IO_FILE struct is going to help us out :)
When the application exits, it will call _IO_flush_all_lockp to clean up file handles.
For iterating all open file structures, it takes the first file pointer from _IO_list_all (pointing to stderr). It will do some checks, eventually calling some cleanup methods and then try to continue with the next file structure. The next open file structure should be stored in the _chain ptr of the current one, so it will continue with that until _chain is NULL (no more files to check).
Did the last sentence sound intriguing? Right, the _chain ptr of stdin will be 0x0, since it’s the last open file structure. And we are able to write a heap address to an arbitrary address, which contains 0x0. Gotcha…
Thus, we can create a check to overwrite the next ptr in our object 3 with the address of _chain in stdin (better said _chain-0x84, since we overwrite its next ptr). The next object allocation will then create an object on the heap and store its address in stdins _chain.
On a later exit, _IO_flush_all_lockp will thus handle the _chain ptr of stdin as another file structure and happily apply the code above on it.
If we’re able to fulfill
it will execute
which is just a macro to call the overflow function from the vtable of the file structure.
Let’s sum it up:
Overwrite next ptr of object 3 with _chain-0x84 of stdin
Next object allocation will create an object on the heap and write its address into _chain of stdin
If _IO_write_ptr in our fake file struct on the heap is bigger than _IO_write_base it will try to use the (fake) vtable entry in our fake structure to call vtable->__overflow
Thus, we just have to forge a proper fake file struct on the heap, to be able to call an arbitrary function, which will get the address of the file struct as its first argument.
At this point, I just tried this to call system("/bin/sh"), which will give you a shell on the system… But you won’t be in the right usergroup and not able to access the flag. We have to call setresgid before. In the end I opted to use the _IO_file approach to do a gets(rsp), writing a ropchain, which will call setresgid and then execve("/bin/sh").
Storing the ret gadget in _IO_write_ptr was just coincidentally, since I needed to put it somewhere for later use in the rop chain. As long as _IO_write_ptr > _IO_write_base, everything’s fine ;)
This will create the following fake file structure on the heap.
Sooooooo. When we exit the program, it will
call _IO_flush_all_lockp
iterate through the file handles arriving at our fake chunk
comparing _IO_write_ptr to _IO_write_base, and since it’s bigger, calls vtable->overflow
our vtable points to 0x60403b0
The function pointer for overflow is at vtable + 0x18 (in this example 0x6043c8)
We put the address to our call gadget at 0x6043c8, so this will now call
At this point rax will contain our fake vtable, so it will call vtable + 0x20, which is 0x6043d0. That’s the reason we stored gets there.
So this will basically just call gets(rsp), allowing us to put a huge ropchain there, which makes exploitation a lot easier now.
After the call to get, the gadget will do an
So, we just put the address of a ret gadget at rsp+0x8 and fillup the ropchain because of add rsp, 0x30; pop rbx.
The problem with just executing system("/bin/sh") was, that we were missing the right user group to read the flag, but with being able to rop, this shouldn’t be a problem anymore.
The ropchain calls setresgid(1011, 1011, 1011) to fix our usergroup and then execve("/bin/sh"), which pops a shell.
If trying this locally, you’ll see that stdin and stdout will already be closed, so you won’t be able to get any output from the shell.
But since we’re connecting remotely via ssh, we’ll still have a socket fd for input/output, so this isn’t a problem.
Only problem left, the webpage didn’t want to accept the flag.
Thanks to kmh11 from irc for fixing this issue, resulting in the last pwn challenge being solved :)