Intro
Hello there! On this part we are focusing on abusing chunk creation and heap massaging in hope of overwriting the __malloc_hook
weak pointer. Before getting into all the juicy stuff let’s remember some key things from last post.
- The value returned by
png_get_uint_32
is of type unsigned integer - For a 32 bit integer, the following happens:
0xffffffff + 1 = 0
- fread will read values into the destination unless it can’t read from source memory (spoiler: it can)
- fread will return the number of elements read from the source
Points 1 and 2 were made clear but 3 and 4 were left unanswered.
The exploit (pt. 2)
Let’s recap
First of all, have a look at the following image and think that the PNG chunks in it, will get translated into heap chunks.
Now, as analysed in the previous post, there are two vulnerabilities happening that lead to a buffer overflow. Said overflow happens in the following line:
... if (fread(pChunk->p + 4, pChunk->size - 4, 1, f) == 1) ...
fread
is a tricky function to use here as it will return the number of elements read even if it cannot read one element or errors out, it will copy the elements into the destination. The direct implication of this has been shown already: It will read as many bytes as there is in the file into the destination!
Keep in mind that our goal still is to overwrite the __malloc_hook pointer in order to control the instruction pointer.
Visualising the culprit
It is highly recommended to do it all with gdb by inspecting the memory manually in order to feel comfortable within the debugger and with memory inspection. However, for the sake of this posts’ simplicity, let’s run our crash straight into villoc:
LD_PRELOAD="$(locate libvilloc.so)" ./apngopt images/6frames-AnimatedPNG-blogpost.png |& $(locate villoc.py) - trace.html
If we start inspecting the trace generated by villoc, we will notice that there has been an error at the very bottom of it.
Check the memory-corruption-trace that was printed on the terminal. There, we should have an address that was corrupted on the line stating something like:
*** Error in `./apngopt': malloc(): memory corruption: 0x1ffe4a0 ***
If you ran the command, you should have a different address than 0x1ffe4a0
due to ASLR. Can you find it? As a hint, when searching for it, try to search just for the equivalent of the 1ffe4
part instead of the whole address. It’s left as an exercise for the reader to inspect why the addresses are shown differently on villoc vs. gdb.
With this, we can conclude that the culprit of causing memory corruptions has to be on the left side of 0x1ffe490
, perhaps maybe, the fastchunk
(purple) at 0x1ffe470
that was malloc
‘d with a size of 0x1
?
Mr. (unsorted) Bin
Let’s inspect some of the latter in gdb and see what’s happening with the corruption. Do add libc symbols via this link and remember to also enable debugging symbols for apngopt by making use of the CFLAGS
variable and setting it to -ggdb
. With this in mind we are ready to make it crash and inspect what is the state of memory as of the crash. Run:
pwndbg> r images/6frames-AnimatedPNG-blogpost.png
Nothing would stand out here until one realises how much of “ascii” the 0x826042ae444e
address contains.
Look at that! Now, get your little-endian glasses and look at this!
Let’s change those bytes into eight “A” characters and see what happens.
Cool! We can control stuff but, why? On this specific case we are overflowing into a free chunk right from the chunk that’s behind 0x63b620
(flashbacks to villoc!). We are overwriting enough to get to the FD
pointer of that chunk but, in the end, it will not be possible to “fool” the allocator because of the implemented checks in malloc.c
.
Without going into too much detail, the allocator first checks if the size of the free chunk is less than the maximum possible (highlighted in red) and, afterwards, checks the validity of the next free chunk (the fake one we would put instead of 0x4141414141414141
). I have fallen into that rabbit hole long ago and didn’t find anything interesting – maybe you have better luck or know more heap exploitation techniques! If so, please share these!
The Size Is Right
Inspecting the heap on the previous state we can tell that it is not what we wanted since the chunks we control are stored in the unsortedbin
list. This would be the first headache of them all since, something happened during the course of the application that coalesced chunks into a free space, which in turn, ended in the unsortedbin
list. Lot’s of debugging (application and malloc.c) needed. This is not ideal for our exploitation since what we need is to control the FD
pointer of a fastchunk
of size 0x70
. For this matter, we just need to remember where the vulnerability resides, which will help us crafting chunks of our wanted size. Again, have a look into the following lines inside apngopt.cpp:
... pChunk->size = png_get_uint_32(len) + 12; pChunk->p = new unsigned char[pChunk->size]; ...
The new operator is allocating chunks of pChunk->size
. Doing some maths, we need len
to contain:
(0x70-0x8) = len + 0xc len = 0x70-0x8-0xc len = 0x5c
Being, 0x70
the allocation we want to trigger, 0x08
for the meta-data of the chunk and 0xc
the value that gets subtracted in the code. This should generate a fastchunk
of size 0x70
.
APNG Massage right in the heap
To practice, let’s try to generate the aforementioned fastchunk
of size 0x70
through an APNG chunk. As we know by know, the first four bytes of a PNG chunk describe the size of the data contained within. We are going to use this size to craft memory chunks of our desired size. In our specific case, the PNG chunk has to have a size of 0x5c
as this will be the len
variable which later, will allocate a fastchunk
of 0x70
.
First thing to do is to cut the bytes we added previously in order to let the APNG Optimizer program to properly go through all the file without hitting a crash. Remove the last few bytes until the letter “w”. The file should be 490 bytes long now.
Conforming to the PNG specification, IHDR
is the first chunk to appear in a PNG right after the magic bytes header. We are not going to mess with this one as of yet – we are targeting the PLTE
chunk as it’s an easy one to tamper with. This chunk is flagged as a critical chunk on a PNG file, meaning it is necessary for the fine display of the file. However, this PNG chunk is optional and it only has two constraints: to appear just before the first IDAT
chunk (Image DATa chunk) and its length to be divisible by 3.
Doing more maths, since 0x5c mod 0x3 != 0
, the next value divisible by 3 that will still trigger a malloc of size 0x70
will be 0x5a (90)
. Remember that this value can be chosen because it’s going to be padded to the next 16 byte chunk size. The PLTE section should now look like this and the apngopt program should “optimize” it just fine:
See that the size change was done from 0x18
to 0x5a
so we need to fill the PNG chunk’s data with further 0x5a - 0x18
bytes, hence AAAAA...
.
To check this new allocation you can either run villoc or inspect within gdb. Let’s show our created chunk of size 0x70
on gdb:
The playground
FD->FD->FD->FD->PoC
Now that we know how the PNG chunks are formed, how these translate into the heap and know how to control these within the program itself it’s time to show some proof of concepts. Please note that this is the point I am stuck in for this exploit and haven’t got the time to advance any longer towards control of the instruction pointer.
The PoC
The following Proof of Concept effectively massages the heap to place a fastchunk
of size 0x70
after a smallchunk
. It does so by writing the same contents of the following chunk until it gets to the fastchunk
of size 0x70
and overwrites its FD
pointer. You can download the proof of concept file from here: fastchunk FD PoC file.
In theory, apparently, etc.
We could set the FD
pointer to be near __malloc_hook
having in mind that to where we point in memory it has to resemble a fake size of 0x70. This is “no problem” because near__malloc_hook
we have:
pwndbg> x/8gx &__malloc_hook - 6 0x7ffff70eaaf0 <_IO_wide_data_0+304>: 0x7ffff70e9260 0x00000000000 0x7ffff70eaaf0 <_IO_wide_data_0+304>: 0x7ffff70e9260 0x00000000000 0x7ffff70eab00 <__memalign_hook>: 0x7ffff6dabe20 0x7ffff6daba00 0x7ffff70eab10 <__malloc_hook>: 0x000000000000 0x000000000000
In theory, it can be possible to set the pointer to that address since 0x7f =~ 0x70
(both values are almost the same) to the eyes of the allocator. The deep details of why this tricks the allocator can be found inside malloc.c on the portion of code that does the look up for the index of the required fastbin
list.
Truth is, with ASLR enabled, that address is going to change and, without any memory leaks, it’s not going to be possible getting the right address. Also, if we choose a byte in memory that doesn’t have a null pointer afterwards, the allocator will think that there is another free fastchunk
at that address. See:
pwndbg> x/6gx 0x7ffff70eaaed 0x7ffff70eaaed: 0xfff70e9260000000 0x000000000000007f 0x7ffff70eaafd: 0xfff6dabe20000000 0xfff6daba0000007f 0x7ffff70eab0d: 0x000000000000007f 0x0000000000000000
TheFD
pointer for 0x7ffff70eaaed
is 0xfff6dabe20000000
, which is an invalid memory address.
Finally, another caveat that we need to think about when trying to exploit programs that take binary formats is that we don’t have a scripting environment. This is, we are constrained to tamper with bytes within a file and not writing some exploit in JavaScript against a browser where we can store leaked addresses into a variable and then use it.
Further steps
All is not lost and if we observe which part of the code is consuming these fastchunk
s something will catch our eye. Set a break point on apngopt.cpp:1181
and run the debugger with our PoC file:
... 1181 unsigned char * temp = new unsigned char[imagesize]; 1182 unsigned char * over1 = new unsigned char[imagesize]; 1183 unsigned char * over2 = new unsigned char[imagesize]; 1184 unsigned char * over3 = new unsigned char[imagesize]; 1185 unsigned char * rest = new unsigned char[imagesize]; ...
Can’t see it yet? Hint, the imagesize
variable is 0x66
bytes. This is set through the PNG chunk IHDR’s width and height.
Now it’s clear that the variable over1
is not in the heap and it’s in the position we want. At this point when trying to allocate a fastchunk
for the variable over2
it will crash on trying to allocate a fastchunk
at 0xfff6dabe20000000
. How can we prevent this crash? Apparently, easy. Since the variableover1
is our first fake chunk, we would need to create a fake heap of four consecutive fastchunk
s of size 0x70
so that, our controllable fastchunk
s lands on the variable rest. This does indeed work (without ASLR!) but, we will hit another condition I haven’t managed to overcome yet: The PNG file needs to have more than 4 frames after the optimisation has happened. This is why this post has started providing a template with 6 frames!
Controlling the instruction pointer
As a last short note it was mandatory to show that, theoretically it might be possible (with huge amounts of work) to control code execution. With the help of gdb this can be checked by manually filling the variable over1
with AAAA...
:
You can get all the files that I have used throughout this blog post here: apngopt1.4-exploit-dev.tar
Villoc, setarch and ASLR
If you want your villoc traces to be consistent with gdb addresses run the villoc command as follows:
LD_PRELOAD="$(locate libvilloc.so)" setarch `uname -m` -R ./apngopt images/6frames-AnimatedPNG-blogpost.png |& $(locate villoc.py) - trace-setarch.html
Bear in mind that, because of preloading the libvilloc.so
library to setarch
too, it will hook setarch
malloc
calls as well and give us more information than needed. Information that is actually not related to the apngopt program.
Conclusion
Trying to develop exploits for programs without a scripting environment is quite hard due to recent mitigations. Furthermore, it can even get trickier if we are working on an userland heap based exploit, at least that is what this experience has shown.
If we look at heap exploitation from the perspective of ptmalloc2 by Wolfram Gloger (originally Doug Lea) we can see that, there is no formula to follow (maybe like we are used to see on stack based vulnerabilities) and development is heavily influenced by how the program uses the memory. Not only a deep understanding of how the allocator works is needed but a deep understanding of the program’s functionality itself.
Should you have any further questions or anything do not hesitate on writing an email to me: javier at the domain name dot com.
It is the way one treats his inferiors more than the way he treats his equals which reveals one’s real character.