Heap

fastbin-attack

Environment setup

We are given the loader because there may be inconsistencies with the given libc and the system loader:

> ls
fastbin_attack  ld-2.23.so  libc-2.23.so  port

How do we bind the given loader and libc to the binary?

  1. Fastest way: set the LD_PRELOAD environmental variable:

    LD_PRELOAD=./libc-2.23.so ./binary

    This solution uses system's loader.

  2. To use both a different loader and a different library:

    ./ld-2.23.so --library-path ./lib ./binary
  3. Most sofisticated way:

    NixOS/patchelf: A small utility to modify the dynamic linker and RPATH of ELF executables (github.com)

     patchelf --set-interpreter ./ld-2.23.so --replace-needed libc.so.6 ./libc-2.23.so ./binary

    To check which library a binary (./binary) we can use ldd ./binary.

If we run fastbin-attack with the 1st method we get a SEGFAULT, because we need also the loader. Also the following:

goes in SEGFAULT.

note on Ghidra pseudo C readability

While exploring the pseudo C generated by Ghidra, we notice that some pieces of code are very hard to read, like disassembled while loops:

Since we are incresing the size of the single item, we need to decrease the number of array cells ($16001=2008$). Result:

What does it do

Basically the binary manipulates lists that have the following structure:

Then we have another list that contains zero or one depending if the pointer contained in the first column is valid or not. Actually we have no second list, retyping in Ghidra fixed that for us. It allows us to allocate, write and free some text by using the heap.

A problem with read_entry

The read_entry function has a security vulnerability:

It prints the entries variable after checking if the pointer is valid, but it does not check if the pointer has been freed. This is a memory leak. If you are wondering what that check should look like, this is the relevent snippet from the write_entry function:

The check is performed against the state list that was introduced before.

Note: entries is a global variable.

how to put breakpoints in PIE binaries

Given the address of a certain instruction in a disassembled binary, for example we can call it <ADDR>, we can still put a breakpoint in it:

more improvements on disassembled code readability

After retyping the structure used by the binary, we get the following code for the write_entry function:

Which is much better if you ask me. Still something is wrong:

This probably means that size actually is not a long (8 bytes), but it is an int (4 bytes). If we edit the structure according to this, it would have a char* and two int. New pseudo code of the same function:

Which is actually readable.

A problem with free_entry

Same as the other function: there is no check in place. We can free a chunk more than once. This means that we can basically print everything we want, which means that we can also leak the address of libc.

The exploit in theory

  1. vulnerability: read_entry, can be used to leak stuff.

  2. vulnerability: free_entry, we can free more than once chunks -> fastbin attack to make malloc allocate something in memory: the address of system() into either __free_hook or __malloc_hook.

    More specifically when overwriting __free_hook we overwrite it with system and we pass /bin/sh to it. Instead when we overwrite __malloc_hook we use one_gadget since the parameter of the malloc is a number, not a string.

The exploit in practice

simplifying the process

To change loader and library with pwntools (if we do not want to use the patched binary):

Assuming that the parent folder containing all the needed files is called fa.

To simplify the work we will build functions in python to interact with the functions of the binary, which are:

  1. alloc

  2. write_entry

  3. read_entry

  4. free_entry

So, for example:

We will also need to parse the index which gets printed by the function:

Same for writing chunks:

And for reading and freeing:

Now that we can interact easily with the functions we can build the actual exploit:

Part 1: leaking libc address

We need to leak libc, how do we do it? We allocate a small bin and free it, since after the free it will contain an address to a location which is is located into libc:

This happens because when we have only one chunk it will contain a random libc address, from which we can compute the address of the base of the libc. To find the offset we will use vmmap in gdb.

Part 2: exploiting __free_hook __malloc_hook to spawn a shell

By allocating two chunks and freeing both of them, and again the first one, we will get a loop: chunk_1 points to chunk_2 and vice versa. Then

Will result in 0x4141414141414141 becoming ?. Now we can put the address of __free_hook in its place.

Still the program crashes because the malloc realizes that there's a mismatch with the size of the bin. We need to find some bytes not zeroed that are located before our target and try to match the size with those bytes, since they will be our new size for the chunk.

If we do not find those bytes we can search elsewhere, for example near __malloc_hook.

We find that before __malloc_hook we have an address, but we know that after translating it to decimal it would be a gigantic number. We could exploit the alignment to take only part of it. For example we can go from 0x7ffff7... to 0x7f. To align we just add some bytes to the address of the cell to 'cut' its content according to our needs.

Then we take the address of that cell, we compute its offset, and put it in our script in place of __malloc_hook. Full exploit:

playground

What does it do

It has only the main function which encompasses all the functionality. There are some nested loops that executes the following functionalities:

Basically it can:

  • Allocate a chunk of size n and return its address

  • Free a chunk, given its address p

  • Show the content of a chunk given p. By default it shows the first 8 bytes of data, unless a size n is specified.

  • Write some data into a chunk, given p.

There are some obvious vulnerabilities: for e.g. the chunk freeing is achieved without checking if the chunk is allocated or not, which could be used as an attack surface to carry a fast bin attack.

libc version

A note about libc version: the program is using libc version 2.27 which incorporates a backport of the tcache key.

The exploit

Here's what we need to do in order to exploit this binary:

  1. Set to zero min_heap

  2. Overwrite max_heap with a high value

  3. Overwrite __malloc_hook with the magic gadget.

Note: min_heap and max_heap are two global variables on which a sort of boundary check is performed before working with the heap.

Setting min_heap to zero

We need to exploit the presence of the key in the tcache, since it allows us to overwrite any DWORD with zeros. Here's an example:

We can see that the program segfaulted because it tried to allocate 0x4141414141414141.

Note about one_gadget

If we use the following flag: --level 1 when running one_gadget it will find a lot more gadgets.

pkm

A recall on the heap

Allocated chunk

Free chunk

Note about gdb ptype command

ptype is a gdb command that allows to print variables of binaries that include symbols. For example in this binary we have the pkm variable, which is a struct. Thanks to that command we can print it and implement it in Ghidra for much easier code readability:

More on that:

ptype typename Print a description of data type typename. typename may be the name of a type, or for C code it may have the form class class-name', struct struct-tag', union union-tag' or enum enum-tag'.

Some informations about the heap management

If we try to allocate a new pokemon, and to rename it, we get the following heap configuration:

As we can see the chunk 0x101 bytes big is the chunk containing the pokemon, since it is allocated with a malloc(0xf8). The 0x21 chunk instead is the chunk allocated to read from stdin the pokemon name, since the get_string function allocates a chunk of arbitrary size, and I passed to it len('pikachu'). Content of pokemon in the heap:

Note that the chunk, as said before, is 257 bytes long, which is 32 WORDS. From 0x405100 and on we have another chunk, which in this case is the one containing the string which represents the pokemon name, and then we have the top chunk.

As for the content itself, we've got the statistics, which is decimal values look like this:

And then there's the pointer to the pokemon name, which is 0x405110. As for the second chunk we've got:

Which makes sense, since its 0x21 bytes long, which is 4 WORDS long.

Exploit idea

We need to put in place a null byte poisoning exploit, which can be achieved trough the function used to assign names to pokemons:

To bypass the null byte overflow mitigation we need to exploit the rename_pokemon function, and fill a buffer which should contain a pokemon name with fake previous sizes. This is the function that calls get_string:

Where UNKNOWN is a global variable containing an address to the 'PKM' hardcoded string. This means that we can avoid to free a chunk if we previously wrote that in it.

How to perform the null byte poisoning

If we create two pokemon and allocate two chunks of 0x200, that's the situation on the heap:

Let's call those chunk as follows:

  1. Chunk 1: first chunk in the heap, it's pokemon 1

  2. Chunk 2: second chunk, it's pokemon 2

  3. Chunk 3: third chunk, it corresponds to pokemon 1 name

  4. Chunk 4: third chunk, it corresponds to pokemon 2 name

Let's remember that the vulnerable function is the one that allocates the chunks of arbitrary length, which is the one we used to allocate the two 0x200 chunks. here's their content:

In the last row we have the header of the following chunk. As for the chunk at 0x405200, we have the usual structure:

Now, if we delete the first pokemon (chunk 1), we free both chunk 1 and chunk 3, since the binary will delete both the chunk containing the pokemon itself, and the chunk containing its name. We end up in this situation:

That's needed to trigger the null byte: in fact if we allocate again pokemon 1 and its name, we should get two identical sized chunks in the same spots as before, but the allocation of the chunk representing the pokemon name (chunk 3) would result in a null byte overflow into the name of pokemon 2 (chunk 4), which is immediatly after.

After executing the poisoning

This is the script stdout that led to the heap setup below:

As you can see from 0x4056b0, if we do the heap command in gdb, it does not report the overlapped chunk which we get printed when we manually print heap addresses. Looks like we managed to correctly overflow and allocate two overlapping chunks. Now we can exploit that to carry the attack.

And now?

Basically up to now we just did things to trick malloc into giving us two chunks allocated and overlapping. Now we can actuate the exploit, which consists in replacing the function pointer of some move of a pokemon (chunk B2) with our exploit to get rce. We can do that by writing a target address in the overlapped chunk resulting from the null byte overflow. The binary helps us because each pkm struct has an array of function pointer which gets called by a function of the binary. So, to recap:

  1. We perform the null byte exploit;

  2. We use the overlapping chunk to write an address of interest in the moves array of the overlapped pokemon;

  3. We call the function which executes the code at the given address to gain RCE.

We could write a one_gadget address, or somethigs else. In my case one_gadget was not working, so I wrote the following into the overlapped pokemon:

With binsh written in the chunk header, since by checking with gdb I saw that the argument passed to the address called during the fight_pkm was in the beginning of its chunk. Problem is, first of all we need to leak an address belonging to libc... This can be done via some symbols already resolved and present in the GOT, since the binary is not PIE. If we exploit the overlapping chunks to put an adress of the GOT into one of the overlapped pokemon chunk, and in particular into the name field of the pkm struct, we can print a libc runtime address. Code for this:

So we have a two stage exploit:

Script's stdout of the exploit in action:

Last updated