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_32is 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)
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
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
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
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
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
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
0x5a so we need to fill the PNG chunk’s data with further
0x5a - 0x18 bytes, hence
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:
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 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
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
FD pointer for
0xfff6dabe20000000, which is an invalid memory address.
All is not lost and if we observe which part of the code is consuming these
fastchunks 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
0xfff6dabe20000000. How can we prevent this crash? Apparently, easy. Since the variable
over1 is our first fake chunk, we would need to create a fake heap of four consecutive
fastchunks of size
0x70 so that, our controllable
fastchunks 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
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
malloc calls as well and give us more information than needed. Information that is actually not related to the apngopt program.
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.