Skip to main content

Demonstration of Attack

Introduction

This document presents a demonstration of an attack of type heap corruption. The demo will run on a docker container in an x86 64-bit Linux environment, simulating a device, with Sternum Linux EIV installed. Using the information shown on the Sternum Platform alerts and events, we analyze the attack and find the root cause of the heap corruption.

info

Read more about heap memory corruption here: CWE - CWE-787: Out-of-bounds Write (4.12).

Attack Demonstration

With EIV installed, we use the Sternum attack_simulation_kit to run an attack of type “string copy causing memory corruption“:

Trigerring the string copy causing memory corruption
Using the attack kit to triger the string copy causing memory corruption

Sternum EIV detects and prevents this attack in real-time. It also generates a security alert to the Sternum Platform with the attack details. The alert received on the Sternum Platform is:

Attack Attempt alert on the Sternum Platform (attack of type Heap Corruption)
Attack Attempt alert on the Sternum Platform showing the attack of type Heap Corruption

The alert indicates that the attack Attempt is of type Heap Corruption.

We can also identify the execution of the binary that caused the corruption: attack_simulation_kit:

Execute Event on the Sternum Platform showing the execution of the binary which causes the corruption
Execute Event on the Sternum Platform showing the execution of the binary which causes the corruption

Let's investigate what caused this “heap corruption" alert in the binary.


Finding the Source of the Heap Corruption in the Binary

In the Heap Corruption Attack Attempt event, we can see what binary and where exactly is the code which caused the corruption - the Originating Code Address 0x56344d4014d5. The value of the Originating Code Address is a virtual address in the processes' memory, which (according to the Module Name field) corresponds to somewhere in the attack_simulation_kit binary (rather than in, for example, a shared library used by the process).

The code that causes the corruption will be found using the value of the offset in the elf, which equals originating_code_address - module_start_address. Specifically, the address 0x56344d4014d5 in the process' memory is offset 0x14d5 in the attack_simulation_kit elf (which starts at 0x56344d400000 in the process' memory as shown by the Module Start Address field in the event).

Now, we can open the binary for examination (for example, by using IDA Pro). The elf base address in this case is 0, which we can see in IDA (and obtain using elf parsing tools such as readelf), so offset 0x14d5 is also the address in the binary. In the address 0x14d5, we find the following:

attack_simulation_kit elf at address 0x14d5
attack_simulation_kit elf at address 0x14d5

This shows us that the last thing that ran before the corruption event is a call to the function strcpy in libc. The call originates from a function named string_copy_causing_heap_corruption:

Screenshot from IDA Pro showing the address in which the corruption occurs, in the function string_copy_causing_heap_corruption()
Screenshot from IDA Pro showing the address in which the corruption occurs, in the function string_copy_causing_heap_corruption

What Caused the Heap Corruption Event?

According to the Linux Programmer's Manual (man):

The strcpy() function copies the string pointed to by src, including the terminating null byte ('\0'), to the buffer pointed to by dest. “

--- STRCPY(3) in man pages

strcpy doesn't check whether the size of the destination buffer is large enough to contain the copied bytes, and as mentioned in the man, this is a source for many buffer overruns (which can cause a heap corruption if the buffer was in the heap). For the sake of demonstration, let's check if this is indeed the case here.

To do that, we must find the sizes of the source and destination buffers, and verify that the amount of bytes written from the source to the dest is more than can be contained in the dest buffer. The buffer will then be overwritten and overflowed.

The sizes can be found by dynamically debugging the binary while it reaches this flow (for example, by using gdb), and printing all the relevant data needed. Let's assume the event we received (whether caused by a bug or an actual attack attempt) occurred in production, so rerunning it for the sake of debugging might not be possible.

From here on out, the corruption will continue to be researched only using static research methods (i.e. statically reverse-engineering the binary using IDA Pro, and reviewing the source code).

In this specific case, we also have the source code of the attack_simulation_kit binary, and we will use it to follow up on the event. This is for the sake of simplicity and convenience and isn't truly necessary as we can also obtain all the necessary information from the compiled binary.

The function string_copy_causing_heap_corruption in the source code. Notice the call to strcpy, which causes the corruption
The function string_copy_causing_heap_corruption in the source code. Notice the call to strcpy, which causes the corruption

Statically Verifying Buffer Sizes

Destination Buffer Size

Firstly, I will find the size of the destination buffer (named temp_buffer in the code). This is relatively simple, as it is being malloc'd right before the strcpy call. The size of the allocation is passed to the function as an argument by the calling function (as seen in the screenshot of the code of string_copy_causing_heap_corruption above). The calling function is exploit_string_copy_causing_heap_corruption, and it passes the value 64 (or 0x40).

The call to string_copy_causing_heap_corruption with sizeof(custom_object_t) which equals 64 bytes
The call to string_copy_causing_heap_corruption with sizeof(custom_object_t) which equals 64 bytes

The same call in the binary, showing 0x40, which is 64
The same call in the binary, showing 0x40, which is 64

So the destination buffer is 64 bytes long. Including 8 bytes of allocation header (exact size is system dependent, but the header is found in all heap allocations), this is 72 bytes. Indeed, this is what we see in the Heap Corruption event which appears on the Sternum Platform under Allocation Size.


Source Buffer Size

Secondly, the source buffer. This buffer is passed on to the function as an argument named src, which can be seen in the screenshot of the code above. It can also be seen in the screenshot of the assembly code as var_260. This variable is a buffer initialized on the stack earlier in the function, and it is 0x250 bytes long.

Initialization of src buffer in the code
Initialization of src buffer in the code

What we need to know, is where in this buffer is the null terminator at the time of the call to strcpy, because that will be the last byte copied into the destination buffer.

The data in the source buffer is set in a function named prepare_buffer, using arguments calculated by a call to get_distance_between_allocations. A closer look at the latter will show some suspicious looking code. In short, we can see the function attempts to find the distance between 2 given allocations, and return it. Shown in the screenshot below, the function subtracts the values in second and first which are the allocations.

Excerpt from get_distance_between_allocations
Excerpt from get_distance_between_allocations

info

Why is the distance between 2 allocations which are potentially consecutive in the heap interesting? In the wrong hands, an attacker can use this information to reach from writing into one allocation (without bounds checking) straight to the adjacent allocation, which could be an important stepping stone to code execution (depending on what the second allocation is used for).


The allocations used here are 0x40 bytes long (the call to malloc can be seen earlier in the same function, and the size used is given to the function hard-coded as an argument), and, as seen earlier, we can expect the difference between them to be 72 bytes (64 bytes of data and 8 of the allocation header).

The source buffer (which, as we know, is on the stack and 0x250 bytes long) is then populated by the function prepare_buffer. This function used the value returned by get_distance_between_allocations (72), and writes that amount of the byte 'A'. It then finishes off with writing the address of a malicious function at the end of the 72 'A' bytes.

Excerpt from get_distance_between_allocations
Excerpt from get_distance_between_allocations

The function prepare_buffer writes 72 times the byte 'A' into the src buffer, followed by a function pointer. Since we are working on a 64-bit system, the function pointer is an additional 8 bytes, meaning we have 84 bytes of data in the source buffer. Including the null terminator, this is 85 bytes, just as the Sternum Platform reported.


Conclusion

The function strcpy does not perform any bounds-checking and assumes the destination buffer is large enough to contain the data in the source buffer. The function calling it (string_copy_causing_heap_corruption, shown below) also does not perform any verifications, and thus opens itself up to memory corruptions caused by buffer overflows.

The function string_copy_causing_heap_corruption in the source code. Notice the call to strcpy, which causes the corruption.
The function string_copy_causing_heap_corruption in the source code. Notice the call to strcpy, which causes the corruption.

As we've verified, the call to strcpy does indeed cause 85 bytes to be written to a 72 bytes-long allocation, causing an overflow. This is in fact shown clearly in the alert on the Sternum Platform.


Further Explanation of the Exploitation Shown

info

Linux EIV installed on the device identified the heap corruption and blocked the out-of-bounds write from happening. Had this not been the case, the data written would be designed just right so that a malicious function pointer from the source buffer will override an existing function pointer in the object variable (in the field named func), which resides on the heap directly after the temp_buffer destination buffer. As seen in the source code pictured above, the malicious function would have been called after the strcpy, instead of the expected function, had EIV not interfered.