Our Blog

Our news

All you need to know

Linux Heap Exploitation Intro Series: Set you free() – part 2

Reading time ~15 min

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.

  1. The value returned by png_get_uint_32 is of type unsigned integer
  2. For a 32 bit integer, the following happens: 0xffffffff + 1 = 0
  3. fread will read values into the destination unless it can’t read from source memory (spoiler: it can)
  4. 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.

Magic number into APNG file.

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.

Error represented by villoc
Error represented by villoc

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.

Searching for address on villoc trace
Searching for address on villoc trace

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
Inspecting the bins structure with pwndbg
Inspecting the bins structure with pwndbg

Nothing would stand out here until one realises how much of “ascii” the 0x826042ae444e address contains.

Hex decoding address
Hex decoding address

Look at that! Now, get your little-endian glasses and look at this!

Little-endian representation of overflown values
Little-endian representation of overflown values

Let’s change those bytes into eight “A” characters and see what happens.

Controlling address in unsorted bins
Controlling address in unsorted bins

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.

Showing controllable memory area
Controlling FD pointer (Green). Malloc size checks against size (Red).

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.

Ascii contents of test case after removing crashing bytes
Ascii contents of testcase

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:

PLTE Section crafted for a 0x70 fastchunk
PLTE Section crafted for a 0x70 fastchunk

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
Fastbins analysis after memory corruption
Fastbins analysis after memory corruption

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 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.

Variable over1 is on a controlled position by the attacker
Variable over1 near __malloc_hook

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 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 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.