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.
type | info |
---|---|
target | 88.198.154.140 31336 |
flag | hxp{HsIuUU__g-will-5e1f-d3s7rUct-af7er-R3adlnG} |
author | iead (me) |
code | https://github.com/leetonidas/onetimepad/ |
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 B
and 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:
500
(chunk size 90
)500
by ghetto-rewriting
0
500
again (chunk size 20
)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 __free_hook
.