This challenge fits into the category of babyheap challenges which are small heap puzzles with arbitrary limitations. Here a small pad was implemented which can hold eight notes at a time but after reading a note it was destroyed. The vulnerability was a classical use after free (UAF) in
rewrite that did not check whether a note was valid or not. This bug however could only be triggered once.
Please forgive me the meta joke in the flag. The first eight characters should look like a (ASCII) heap address.
The UAF can be used to corrupt the free lists of glibc’s malloc. Only sorted and unsorted bins may point to the libc. Additionally, they are subject to a bunch of sanity checks directly going for the free hook is therefore not possible. At the same time, we would not know what to write there as there is no free leak coming with this challenge. So instead we go for a generic solution: Gain persistent control over a chunk header, specifically the size field and the pointers. Since input is copied to the heap via
strcpy I tried to create such scenario by only writing the empty string. Writing more bytes would overwrite some byte affected by
ASLR resulting in a worse exploit performance.
4f0 500 510 530 ─┼───────┼───────┼───────────────┼──── │ 0 ┊<fake> │ 1 │ 2 ─┴───┬───┴───────┴───────────────┴──── 4f8 ▲ │ ╰────────╯
To gain control over a chunk by only overwriting the lowest address with a null byte I arrange the heap like displayed above. I then free
A in that order and use my one
UAF to change the
tcache pointer in
1 to point to a fake chunk directly in front of it. Please note that the forward pointer of
tcaches points to the chunks data, not the struct beginning. The second allocation now returns the fake chunk at offset
500. We now can control the chunk at
510 with our fake chunk and change the fake chunk struct with our note
0. As we cannot rewrite notes any more the persistent control is implemented by freeing and then allocating that chunk again.
Next, we want to generate a leak. After printing a note, the chunk will be freed and since version
2.28 a simple double free protection was implemented for
tcaches, therefore we have to do some additional trickery. If we look at that consistency check we see, that it triggers when the second pointer in the chunk points to the threads
tcache management and the pointer is already in the list of cached chunks for that size. Since this is a single threaded application the second check seems as the way to go. To circumvent that check we must change the size field of the chunk we want to leak between both reads. With that said, we first need to set up the heap so two notes point to the same memory. I thought writing a
null byte into the
tcache nxt pointer is easy and yields very consistent results and we already have one chunk at
500 we use the same trick again.
(not to scale) 4f0 500 510 530 550 590 ─┼───────┼───────┼───┼───┼───────┼─── │ 0 ┊<fake> │ 1 │ 2 │ │ 3 ─┴───┬───┴───────┴───┴───┴───────┴─── 4f8 ▲ │ │ ├────────╯ │ ╰────────────────────────╯
For convenience the size of the second chunk was chosen to be
90, a size large enough to later be handled by
unsorted bins and leaking a
libc address. As I cannot use
rewrite any more I need to place the forward pointer at a location I control. So I change the chunk of
1 to be of
90 size by overflowing from the fake chunk. As eight notes are just enough to fill up the
tcaches and have one additional free, the used chunks have to be juggled a bit. Additionally before freeing the last chunk (at address
500) we need to hold a second active allocation to that chunk, as adding a note later would write a
null byte and destroy the leak. We can now generate the leak by:
500again (chunk size
From now on this is a generic
tcache poison exploit. We know the location of the libc and have control over a
tcache forward pointer from the previous steps. I went for the good old classic