Have you played DiceCTF 2021? We sure did! We even solved a challenge!
Here, have some writeups: https://ctftime.org/task/14692
Note: the server of course runs the “noflippidy” binary.
This challenge is running on Ubuntu 18.04.
noflippidy seemed to be a remake of the flippidy challenge, but the binary was patched.
When trying to flip the notebook, it would first check, if the canary is 0x0 and otherwise just leaves the function, effectively disable the usage of flip completely.
The only thing, we can do, is adding new entries to our notebook, which would not give us much to work with, if there wasn’t another bug in the allocation of the notebook itself.
Since NOTEBOOK_SIZE is an int, 8 * NOTEBOOK_SIZE can overflow, which would lead in a big NOTEBOOK_SIZE but a smaller allocated NOTEBOOK chunk, by which we could then add notebook entries outside of the allocated chunk.
But as long, as we stay on the heap, this would also not be very useful, since there are no calls to free or anything useful on the heap to overwrite. Allocating a notebook with a size, which will not be served by heap but by an mmapped region on the other hand might make this oob access more useful.
The notebook is now allocated directly before libc, thus all relative offsets will be the same (despite ASLR) to the libc rw region as also to ld rw region.
Now the oob access in notebook becomes more useful. Since offsets will always be the same, we can create notes and have their pointers overwrite something in either libc or ld. exitfuncs of libc are mangled, so there’s no point in overwriting them, but we can abuse _dl_fini from ld to get rip control.
This gives us two primitives to do arbitrary calls.
One is by overwriting DT_FINI_ARRAY, which will result in calling our function by ((fini_t)array[i])();. Though, we can only define an address which points to the function we want to call.
The second call is DL_CALL_DT_FINI (l, l->l_addr + l->l_info[DT_FINI]->d_un.d_ptr);, where we can store an arbitrary address, which will be called.
At first, I tried combining those two (also since at first, I only mmapped a region before ld, thus having no access to libc), to use the first call to leak something and the second call to jump back into main. But we’ll not be able to trigger _dl_fini again after that and we would only be able to leak a heap address, which didn’t really helped much.
After a while, I changed the notebook size, as shown at the beginning, and with having a region before libc, we have a better chance to get a leak before going into _dl_fini. When discussing this with hk, he brought up the idea, that we could abuse freelist in main_arena to allocate arbitrary chunks, which worked out pretty well.
For getting a leak, I created a chunk, which looked like a freed fastbin and used the relative offset oob, so that the pointer to this chunk would be put into 0x40 fastbin in main_arena.
Now, we just have to allocate another 0x40 chunk and the next chunk would overwrite the menu pointers, so we just put a pointer to stdout into it. By doing that, the challenge will print stdout on every menu print, thus we can just read it and calculate libc base.
Since we now still have the overwrite for l->l_info[DT_FINI], we can now just call a one_gadget: