In Part 1 of this series, we laid the foundation of memory corruption exploitation and presented the basic exploitation framework:
This post will cover the implementation of Overwrite and Redirect in the context of stack based buffer overflow vulnerabilities.
Memory Address Space Revisited
In its simplest form, the memory space is divided by the executable code region and the data region. The executable region contains both the program's unique code as well as the DLLs the operating system provides to all processes. Data region, as its name implies, contains the data on which the code operates. The data region is comprised of the stack and the heap, which we will describe in detail below.
The attacker is interested in the data region since the shellcode by definition will be embedded in what will be loaded to the data region. This means once the file with the shellcode runs, the shellcode resides either in the stack or in the heap.
The Attacker's Challenge
From the attacker's perspective, inserting the shellcode to the data region is still far from satisfactory because the shellcode is an executable code. Remember – the shellcode's role is to be executed and to open a connection between the attacker and the targeted machine. This is the fundamental exploitation challenge:
- The shellcode is by default loaded to data region.
- The shellcode needs to be executed.
- Residing in the data region means that the memory addresses populated by the shellcode will never be fetched to the CPU for execution.
The Attacker's Solution
The attacker’s main objective is to manipulate the CPU into executing content of memory addresses which under normal circumstances do not get executed. This is where vulnerabilities come into play.
To recap: when we say that an application has a vulnerability we mean that a crafted input file will cause the execution flow to deviate from its predesignated course. In other words, the CPU is fetched an address it was not meant to receive.
Let's tie it all together now. The attacker has managed to insert a shellcode to the data region of the process memory, and what it seeks now is a way to get that shellcode executed. To achieve that, the attacker will craft the file in such a way that the deviated address will contain instructions to jump to the shellcode address. Now, the CPU receives an address containing executable code and it will follow the instructions, jump to the shellcode address and execute it.
(Very) Brief Vulnerabilities Overview
Vulnerabilities are tightly related to the overwrite part in the exploitation flow. Different vulnerabilities enable the attacker to overwrite addresses in different parts of the process address space.
The first type of vulnerability we will cover is stack based buffer overflow. This class of vulnerability can be considered a classic exploitation pattern. It is also one of the oldest patterns to be exploited in the wild and is still a prominent part of the current threat landscape.
A typical computer program is comprised of a main program and functions or subroutines. When a subroutine is called, it performs its task and returns control to the main program. From the memory address space perspective, the addresses of the main program reside in the code region. When a subroutine is called, a stack is invoked to store its local variables (which roughly correlates to what we refer to as the data). The subroutine then performs its designated task and when it is done, hands over control back to the main program.
From the attacker's perspective there are three interesting features:
- Fixed size: The size of the stack is fixed and determined at the time of the call. For example, let's assume that the subroutine declared an array of 10 characters. This will be the size of the stack regardless of the arguments we will pass to it.
- Return address: The return control mechanism works like this: the stack is invoked with a fixed memory size. Let’s say our 10 characters stack is assigned to address 100. This means that addresses 91 to 100 are assigned to this stack. In addition, address 90 contains the address in the main program to which the CPU should return after the subroutine has fulfilled its task. This memory location is known as the return address.
- The stack grows downward: when we provide the actual arguments to the stack, the first goes to the highest address and is then pushed downwards by its followers. So if we provide a 3 character input to our simplified stack, the first one will go to address 100. After that the second one will populate 100 pushing the first to 99. Then the third will go to 100 pushing the second to 99 and the first to 98. Since our input ends here and there are no more arguments, the return address will be fetched to the CPU, which will follow its instructions and jump back to the main program.
Stack Based Buffer Overflow
So far we have described the stack architecture with no malicious context. Now we will explain how this architecture can be maliciously leveraged.
The inherent security flaw in the stack architecture is that it implicitly assumes that the input will match the predesignated size. It works well when the input is either smaller than or identical to this size. The problem arises when the input is larger than the predesignated stack boundaries.
Let's go back to our simplified stack. Suppose we give the subroutine an input larger than 10 characters. Remember that addresses 91 to 100 are assigned for the input and that address 90 is already populated with the return address. If our input is 11 characters, the first character will go to address 100 and will be pushed downwards. When it reaches 90 it will overwrite the return address. The CPU will try to follow the instructions in address 90 but because they do not exist anymore, it will break the execution flow and the process will crash. This is known as Stack Overflow.
Let's also remember that the shellcode resides in the stack and the attacker attempts to cause it to be executed.
In order to leverage stack overflow for its purposes, the attacker will craft the subroutine input in a way that the return address will be overwritten with new instructions which will redirect the CPU to the shellcode location.
In our simplified stack example, the attacker will craft an 11 character size input. The shellcode resides in characters 11 and 10. Character 1 contains instructions to jump to 11 and execute. In that case when the subroutine is called, character 1 will be pushed down, overwrite the return address and redirect the CPU to address 100 where the shellcode is. The CPU will blindly follow the instructions and execute the shellcode.
Zoom Out on Exploitation Architecture
As you can see in our example above, the exploitation parts are not connected to each other: embedding a shellcode is totally decoupled from crafting the input file to trigger a certain vulnerability. The triggered vulnerability enables the return address to be overwritten. The instructions, which overwrite the return address, redirect the CPU to the shellcode but other than do not relate to the shellcode's functionalities in any way.
The art of exploits is to orchestrate these independent parts to work together. In a similar way, the art of protection against exploits is to obstruct either the independent parts directly or the orchestration among them.
We have learned how the basic exploitation framework is implemented on stack based buffer overflows vulnerabilities. Despite its age (the earliest documented attack was the Morris Worm in 1988), this class is still a prominent part of the threat landscape. We encounter exploitations of these vulnerabilities in various readers, players and Microsoft office documents but also in industrial protocols and services.
In the next Exploitation Demystified post, we’ll cover implementation of the exploitation framework on heap-based vulnerabilities.