InfoSec Write-ups

A collection of write-ups from the best hackers in the world on topics ranging from bug bounties…

Follow publication

The toddler’s introduction to Heap exploitation, Overflows (Part 3)

In the previous parts (1, 2) of these posts I had been discussing about the heap structure and I went through some basic concepts, in order to build a better background for this and the upcoming posts. You probably have realised by now the broad attack surface that the heap allocation mechanisms bring with, mainly due to their complexity. Adding to this the fact that we only touched the ptmalloc implementation (which is just one out of many that are used out there), it is not surprising that heap exploitation concepts literally monopolise the interest of the security research community.

I will be using the how2heap repository as a reference in many of my posts as it contains a variety of updated heap exploitation techniques regarding the ptmalloc, many of which are applicable to other implementations too. In this post we are going to explore two typical Heap Overflows on a x64 system with the latest (at the moment that I am writing this post) Ubuntu release 20.04.4.

What we have seen so far… In practice

Let’s put in practice what we have seen so far using a simple example. In the program below we have two allocations of 12 and 30 bytes respectively. Subsequently we copy the LoremIpsum1and LoremIpsum2 values to the variables val1 and val2 and finally we free the allocated memory using the free function:

Now lets compile and load the program in gdb setting one breakpoint after the malloc calls and one more after the calls to the free function:

b *main+89, b *main+109

Before Free

I will be using gef (which can be installed by following the instructions here), in order to enhance my gdb . So first run the program using the GNU debugger and type heap arenas, to trace the malloc_state struct. Since we are dealing with just the main thread (thus just one arena), you will get a result similar to the one below:

As we have only one arena, the next points back to the base of the main arena at0x7ffff7fb9b80. The system_mem corresponds to the size of the allocated heap (0x21000) and the top variable points to the last chunk at 0x5555555592e0 . Next, lets examine the allocated chunks by typing gef> heap chunks :

We requested 12 and 30 bytes and we got 0x21 and 0x31 (hex), but why ? Because of the alignment! Remember that the size of the allocation must be aligned on an 8-byte (or 16-byte on 64-bit) boundary ? So, for the 12 bytes, we have 32 allocated (0x20 hex), and for the 30 bytes, we got 32+16 allocated. The ‘1’ in the added size refers to the PREV_INUSE flag, thus it doesn’t “count” in the actual size. Let’s, for example, examine the chunk at 0x5555555592a0:

As we see above, the initial request was val1 = malloc(12), the chunk size is 32 bytes out of which 8 are used to indicate the chunk’s size as well as if the chunk belongs to secondary arena, if it was allocated off-heap via mmap and if previous chunk is a free chunk.

Allocated Chunk, source: https://azeria-labs.com/heap-exploitation-part-1-understanding-the-glibc-heap-implementation/

You probably also noticed our encoded data (LoremIpsum1 and LoremIpsum2) stored at 0x5555555592a0 and 0x5555555592c0 . Finally, we have a chunk of size 0x0000000000000290 in the beginning of the heap and another one of size 0x0000000000020d20 (the top chunk) at the end.

After Free

To get a better idea on the aftermaths of the free function, let’s modify the initial program with few more allocations and let’s examine how ptmalloc handles them.

Compile the program above and load it to gdb, setting a breakpoint after the free calls.

using tcache

Remember: Each thread has a per-thread cache (called the tcache) containing a small collection of chunks which can be accessed without needing to lock an arena. These chunks are stored as an array of singly-linked lists, like fastbins, but with links pointing to the payload (user area) not the chunk header. Each bin contains one size chunk, so the array is indexed (indirectly) by chunk size. Unlike fastbins, the tcache is limited in how many chunks are allowed in each bin (tcache_count). If the tcache bin is empty for a given requested size, the next larger sized chunk is not used (could cause internal fragmentation), instead the fallback is to use the normal malloc routines i.e. locking the thread’s arena and working from there [1].

Every additional allocation of 0x10 or less bytes will be integrated to the Fastbins as it exceeds the maximum allowed (which is 7 in our case) in the tcache for this size:

The heap now looks like bellow:

Basic Heap Overflow

Take a look at the code snippet below and notice that the program allows an input of size higher than the allocated space (Lines 9,10,15). Conveniently for us, the system function will take as a parameter a string, pointed by the p1 variable and execute it as a system command. The pwd is the hardcoded command that will be executed, implying that the the system function will simply print the current directory:

This being said, if we compile and run the program we will see the following output:

Let’s load the program in gdb and set a breakpoint after the scanf function. By giving a valid input will yield the following result:

Continuing the program, everything goes as expected:

As it is depicted bellow, we need 32 bytes the reach the pwd string and overwrite it:

So, lets use the following string and see what happens:

Look at this mess !! First notice at the program’s code that the the buffer is allocated after a buffer that can be overflowed. Between else, the size and flags have been affected, but the most importantly the pwd command has now been replace by/bin/sh :

Running the program and giving the aforementioned input will force the program to return a shell:

From Integer Overflow to Code Execution

Let’s now see a more interesting case of an integer overflow:

Starting from Line 5, we define fp as a pointer to a function which takes a const char * as a parameter and returns an integer. The toInt function at Lines 7–13 allocates space for the ti variable(Line 10 ) and assigns to it the address of the int atoi(const char *str) C function. Later in main we define the nt variable at Line 24 which can be used to invoke the atoi using the(*nt)(const char *str) declaration.

Additionally, the main function creates an array of unsigned integers of length argv[1] and fills this array at Lines 27–33 . The while loop breaks when the user inputs the value 1 or when i == length. Finally at Line 35 the atoi gets invoked with a parameter taken by the argv[2]argument.

The integer overflow occurs from the fact that the array size has been defined as an unsigned integer but since there is no relative check during its assignment at Line 21 the user may insert an arbitrary high arithmetic value and trigger an overflow. In the demo bellow, I will be using a slight different version of the program above to print some additional info, for the shake of demonstration.

Let’s load the program in gdb setting two breakpoints near the start and end of the main function. Now run the program and give some valid parameters (e.g., gef> r 5 3 ):

After hitting the second breakpoint the heap will looks like bellow:

Take a note of the array pointer at 0x5555555592a0 as well as the function pointer at 0x5555555592c0 . Let’s examine the heap contents at these addresses:

For a length equal to 5, the requested size is 5*4 bytes, thus malloc allocated (the minimum) 32 bytes:

Similarly, the second call to malloc (see toInt() function) resulted the following allocation:

The size of unsigned int for this system is 4 bytes, so the maximum allowed value will be 0xffffffff == 4294967295 . As the input of the user is multiplied with the size of unsigned integer see Line 21, we presume that a size higher than 4294967295/4 will trigger the overflow:

Indeed, notice that at the second case, the allocated size is 0 but the while loop at Line 27 will allow us to write beyond the allocated space. Lets verify that with gdb:

Notice that the second call to malloc has literally disappeared from the heap since the allocated space has been overwritten by the values we inserted (2,3,4,5,..). This includes the address of the atoi function at which has been replaced by the value 0x0000000100000000. Continuing the execution we will get a SIGSEGV error as our program will use the function pointer which is stored at 0x00005555555592c0 // Line 35: (*nt)(argv[2]) . As this pointer has been replaced with an invalid address, the result will be to crash the program:

Exploitation

Can you think of a function similar to atoi that takes a string as a parameter and executes a command ?

We are going to replace the atoi call with a call to the system function. As we also control the input of atoi (since it is fetched by the argv[2] argument), we will achieve code execution.

Assuming that ASLR is disabled, we have to replace the address of atoi (0x7ffff7e115e0 in my case) with 0x7ffff7e1f2c0 which is the system function’s address. Since we have 6 bytes in total and we can overwrite only 4 on a single input (due to the unsigned int defined array) we will first overwrite the f7e1f2c0 (decimal: 4158780096) part and subsequently the 7ffff (decimal:32767) part. Let’s run the program with gef➤ r 1073741824 /bin/sh and insert the following values:

The address of atoi has been overwrited by the address of the system function and the /bin/sh has been given as a parameter:

So, hitting the c in gef, will finally pop up a shell:

That was all for this part. Stay tuned for the next !

References

[1] https://sourceware.org/glibc/wiki/MallocInternals#:~:text=Each%20thread%20has%20a%20per,area)%20not%20the%20chunk%20header.

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

Published in InfoSec Write-ups

A collection of write-ups from the best hackers in the world on topics ranging from bug bounties and CTFs to vulnhub machines, hardware challenges and real life encounters. Subscribe to our weekly newsletter for the coolest infosec updates: https://weekly.infosecwriteups.com/

Responses (1)

Write a response

Hi, thanks for the tutorial ! However i'm wondering when u state: we have to replace the address of atoi (0x7ffff7e115e0 in my case) with 0x7ffff7e1f2c0 which is the system function’s address. What makes you guess the address of "system" ?

--