babyrop is a simple heap-use-after-free exploitation challenge in glibc 2.34, meaning no allocator hooks to be used as function pointers for PC control. We are also stuck w/ seccomp and can't /bin/sh.
The controlled chunks are tied to a metadata (safe_string struct) chunk which is where the pointer to ther controlled chunks are stored, along with their respective sizes.
The ideal scenario would be to abuse the UAF to overlap a safe_string struct with a controlled chunk so we can manipulate the string pointer and length, pretty much given us straight up arb read/write.
This can be easily achieved with a correct sequence of allocations and deallocations as follows. (Keep in mind that the size of the safe_string struct is 0x10, considering the chunk size field and alignment, they go in the 0x20 caches).
First I allocate a string with the same size of the safe_string structure and another string with a different size.
Then I free'd both of them, first the 0x20 string, then the 0x10 string. So now our tcache should look like this: (Consider that tcache is LIFO and also that each chunk has 8 bytes for size and is aligned to 0x10).
Now we know for sure that our next 0x20 sized allocations (0x10 sized strings) will go in the A chunk and then B and C.
Then, if we allocate another 0x20 string we'll consume chunk A (safe_string) and D (string), which mean our next safe_string will go in B and it's string chunk will go in C. Since C is the safe_string of the second allocation and also the controllable string of the fourth allocation, when we edit the fourth allocated string we will be editing the lenght and pointer of the second one, constructing a very reliable arb read/write interface.
Glibc 2.34 doesn't have free/malloc hooks, so we'll have to ROP to get PC control, to do that we can find the return address of some function and overwrite it with a rop chain, then have this function return. The easiest way to get a stack address in order to do that is using the environ symbol from libc.
Due to seccomp filters we won't be able to do an execve("/bin/sh",0,0) rop chain, so we'll need to open ./flag.txt, read the data from a file descriptor and retrieve that data. To do all that we need a memory segment that we can write to and read from, so we can store the "./flag.txt" string and also store the actual flag upon reading it. In this challenge we could use either .bss (using arb read/write), or the heap. I chose to use the heap so I can later retrieve the flag using read_safe_string during the rop chain.