Lab 11 - Buffer Management. Buffer Overflow
Task: Buffer in the .data
Section
Navigate to the drills/tasks/data-buffer/support/
directory in the laboratory's resource archive and open the data_buffer.asm
file.
This file contains a program that populates a buffer with information and then displays it.
Carefully review the program, then compile it using the command:
make
Notice that after running the above compilation command, both an object file and an executable file are generated. You can verify this by running the command:
ls
Run the program using the executable file, using the command:
./data_buffer
Observe the behavior of the program based on its code.
Task: Buffer on the Stack
Access the drills/tasks/stack-buffer/support/
directory from the lab resource archive and consult the stack_buffer.asm
file.
This file contains a program that populates a buffer with information and then displays it.
It is similar to the one above, but now the buffer is allocated on the stack.
The task will contain 3 parts.
Part 1
Carefully review the program, then compile it using the command:
make
then run it using the command:
./stack_buffer
Observe the behavior of the program according to its code.
In addition to the buffer, we have also allocated a local variable of 4 bytes, accessible at the address ebp - 4
.
It is initialized to the value 0xCAFEBABE
.
This variable will be important later on.
What is relevant now is to know that this variable is in memory immediately after the buffer: when the buffer limit is exceeded, you reach this variable.
What is the difference between the two programs inspected so far?
Part 2
Now that we have seen what the buffer looks like in memory and where the variable is placed,
update the stack_buffer.asm
program so that the buffer display sequence
(Look for TODO 1) also leads to the display of the variable's bytes.
It is a case of read buffer overflow, with the objective of information leak: finding out information from memory.
HINT It's not complicated, you just need to "instruct" the display sequence to use a different limit for display, not the current limit of 64 bytes.
Follow TODO 2 and display other information beyond the local variable. What information comes on the stack after the local variable (the next 4 bytes)? And the next 4 bytes after?
Part 3
Based on the experience above, make changes so that the variable's value is 0xDEADBEEF
(instead of 0xCAFEBABE
as it is initially) without, however, explicitly modifying the variable's value.
Look for TODO 3 and use the buffer modification and the ebx
register in which we stored the start address of the buffer.
TIP Again, it's not complicated. You need to use the
ebx
value and an offset to write the0xDEADBEEF
value at that address. That is, use a construction like:
mov byte [ebx + TODO], TODO
Do this after the buffer initialization sequence (after the
jl fill_byte
instruction).
With a correct solution to this exercise, the program will display the 0xDEADBEEF
value for the local variable.
If you're having difficulties solving this exercise, go through this reading material.
Task: Reading Data from Standard Input
Access the drills/tasks/read-stdin-gets/support/
directory from the lab resource archive and consult the read_stdin.asm
file.
In this file, there is a program that uses the gets
call to read information from standard input into a buffer on the stack.
As in the previous case, we have allocated a local variable of 4 bytes immediately after the stack buffer.
Carefully review the program, then compile it using the command:
make
then run it using the command:
./read_stdin
Observe the behavior of the program depending on the received input.
Buffer Overflow with Data from Standard Input
The gets function is practically prohibited in C programs because of its high vulnerability: it does not check the limits of the buffer where the reading is done, and can easily be used for buffer overflow.
For this, transmit the corresponding input string so that the displayed value for the local variable is not 0xCAFEBABE
, but 0x574F4C46
(the ASCII hexadecimal values for FLOW
).
IMPORTANT Do not modify the assembly language code. Transmit the input string in the appropriate format to the standard input to generate a buffer overflow and to obtain the required result. WARNING Do not write the string
"574F4C46"
. This is a string that occupies8
bytes. You must write the ASCII representation of the number0x574F4C46
, which isFLOW
:0x57
isW
,0x4F
isO
,0x4C
isL
, and0x46
isF
. HINT x86 is a little endian architecture. That means the string"FLOW"
, having the character-ASCII code correspondence. If it seems unclear, check out this linkF
:0x46
,L
:0x4C
,O
:0x4F
,W
:0x57
will be stored in memory on4
bytes as0x574F4C46
. So at the bigger address we will haveW
, while at the lower address there will beF
. HINT To transmit the input string, it is recommended to write it in a file and then redirect that file to the corresponding program command. You can use an editor such asgedit
orvim
to edit the file. The advantage is that they also display the column you are on, and you can know how many characters you have written in the file. Alternatively, you can use python to more easily generate your payload. For example, to generate a payload that overwrites a value in the code with the value0xDEADBEEF
, you can execute the following command:
python2.7 -c 'print "A" * 32 + "\xEF\xBE\xAD\xDE"' > payload
NOTE number
32
is only an example and it represents the size of buffer that needs to be bypassed. It is recommended to name the filepayload
. Redirecting thepayload
file to the program is done using a command like:
./read_stdin < payload
If done correctly, you will see:
var is 0x574F4C46
If you're having difficulties solving this exercise, go through this reading material.
Task: Buffer Overflow with Data from Standard Input and fgets()
As mentioned in the Read Using gets task, the gets()
function is prohibited in current programs.
Instead, the fgets()
function can be used.
Open the source code file read_stdin_fgets.asm
from the drills/tasks/read-stdin-fgets/support/
.
In the read_stdin_fgets.asm
source file, follow TODO 1 and change the gets()
function call to the fgets()
function call.
For the fgets()
call, read from standard input.
As an argument for the third parameter of fgets()
(of type FILE *
) you will use standard input.
To specify standard input, use the stdin
stream.
You will need to mark it as external using, at the beginning of the assembly language file, the construction:
extern stdin
stdin
is an address; to call fgets()
with standard input,
it is sufficient to pass on the stack the value from the stdin
address, i.e., using the construction:
push dword [stdin]
HINT Follow the manual page of the
fgets()
function to find out what parameters it receives. TIP Since thefgets()
function has 3 parameters (which occupy3 × 4 = 12
bytes) you will need to useadd esp, 12
after the function call, in restoring the stack, instead ofadd esp, 4
as in the case of the program above that usedgets()
. IMPORTANT Do not modify the assembly language code. Transmit the input string in the appropriate format to the standard input to generate a buffer overflow and to obtain the required result.
Follow TODO 2 in code and call fgets() instead of gets, but keep in mind that you will have to push a value big enough for the buffer size in order to produce an overflow. The result should be the same like in the read_stdin_gets task.
TIP As above, to transmit the input string for the program, it is recommended to write it in a file and then redirect that file to the corresponding program command. Redirecting the
payload
file to the program is done using a command like:
./read_stdin_fgets < payload
What happens if you push buffer_length - 1, instead of buffer_length. Why var is 0x004F4C46 now?
If you're having difficulties solving this exercise, go through this reading material.
Task: Buffer Overflow for Program Written in C
Most often, we will identify buffer overflow vulnerabilities in programs written in C. There we need to see what buffers are and what is the distance from the buffer to the desired variable to be able to overwrite it.
IMPORTANT It is important to consider that the distance between a buffer and another variable in C may not correspond to the "real-world" distance; the compiler can make updates, reorders, may leave free spaces between variables, etc.
Overwrite Variable Using a Buffer
For the current exercise, access the drills/tasks/overflow-in-c/support/
directory from the lab resource archive and observe the corresponding C source code.
For the case where you do not want to compile the code yourself, you have in the archive the equivalent assembly language file and the executable file.
Discover the difference between the buffer's address and the variable's address, create an input file (also called payload
) with which to trigger the overflow and make it so that the message Full of win is displayed.
It is recommended to first take a look at the assembly file, then understand the offsets.
HINT To see the "real-world" reality, i.e., to find out what the difference is between the buffer and the variable we want to overwrite, consult the equivalent assembly language file (
do_overflow.asm
), obtained by assembling the C code. In this file, you can find the relative address of the buffer toebp
and the variable toebp
; follow the sequence between lines36
and47
; you have a mapping between the variable name and the relative offset toebp
. With this information, you can create the string to transmit as a payload to the standard input of the program. NOTE If you want to recompile the files run:
make clean && make
HINT As above, to transmit the input string for the program, it is recommended to write it in a file and then redirect that file to the corresponding program command. Redirecting the payload file to the program is done using a command like:
./do_overflow < payload
HINT If you are confused about the value in the C code:
if (sexy_var == 0x5541494D)
You can use this site to convert from hexadecimal to ASCII.
BONUS: Stack Canary
Now that you displayed Full of win!, let's take a look at the Makefile
cat Makefile
Carefully analyze the compilation options. What do you notice?
As observed above, despite exceeding the buffer size and overwriting another variable in the program, it terminated normally.
This is undesirable when working with buffers because they are a potential source of easy attacks.
Use objdump
to inspect the main
function of the executable.
HINT To inspect the source, use the following command:
objdump -M intel -d do_overflow
Now, go into the Makefile
and modify the CFLAGS
parameters by replacing -fno-stack-protector
with -fstack-protector
.
Recompile the program and run it.
What do you observe?
NOTE With the
-fstack-protector
option or flag, we instructed the compiler to enable Stack Smashing Protection for our executable. Therefore, any buffer overflow attack will be detected in the code, and the program execution will terminate with an error.
Inspect the recompiled executable again with the new flag using objdump
.
What has changed?
NOTE The compiler introduced a randomly generated value called a canary onto the stack, which it checks before exiting the current function's execution. Through buffer overflow, this canary was overwritten upon exceeding the buffer's size, resulting in a mismatch between the initial canary value and the one at the end of the function execution.
Recompile the files and run:
make clean && make
If you try the same payload, it will not work because of the canary generated. You will most likely see a message like this:
Not quite there. Try again!
*** stack smashing detected ***: terminated
Aborted (core dumped)
If you're having difficulties solving this exercise, go through this reading material.
Task: Overwrite Return Address
In the previous exercise, we observed how values of variables stored on the stack can be overwritten.
Recalling how function calls are made Laboratory 9, the return address from a function callee
back to the caller
function is also saved on the stack.
Exploiting this behavior and starting from the resources in the drills/tasks/overwrite-ret-addr/support/
directory, use a buffer overflow to call the void magic_function()
by overwriting the return address in the read_buffer()
function.
IMPORTANT The
void magic_function()
calls thecowsay
utility, which you need to install with the following command:
sudo apt install cowsay
HINT To inspect the source, use the following command:
objdump -M intel -d break_this
HINT In the
read_buffer()
function, both the size of the input string and the string itself are read from the keyboard. Although the buffer is defined aschar buffer[64]
, using the valuen
in thefgets(buffer, n, stdin)
call allows for a buffer overflow. Also,fgets()
will read a maximum ofn - 1
characters;n
can be set to a value larger than the actual length of the input string.
If you're having difficulties solving this exercise, go through this reading material.
Task: Buffer Overflow for Binary
Often, we don't have access to source code and want to discover vulnerabilities in executable files.
In the drills/tasks/overflow-for-binary/support/
directory of the laboratory resource archive, you will find an executable file.
Using ghidra
or gdb
for investigation, discover how to exploit the buffer overflow vulnerability to make the program display the message Great success.
IMPORTANT To run
ghidra
on theoverflow_in_binary
executable file, you need to create a new project and import the executable file. Ghidra will automatically detect the file format. Run the analysis of the executable, then search in the Symbol Tree for themain
function. HINT Identify in the disassembled code how input is passed to the program. Identify where the buffer overflow occurs. Identify the comparison condition you want to trigger. Then build the corresponding payload and deliver it in the appropriate format to the program.
If you're having difficulties solving this exercise, go through this reading material.
WARNING If you try using a payload generated with python and it doesn't work, try simply copying its content in the terminal
Resources
If you found the laboratory interesting in a positive way, you can learn more about this type of attack, as well as cybersecurity in general, on this channel.
Introduction to Buffers
This laboratory aims to present the concept of buffers in C and assembly language along with their specific operations, as well as the vulnerabilities they pose and how they can be exploited by a potential attacker using a program to attack a system or obtain information to which they would not normally have access.
Objectives
- Introducing the concepts of buffers and buffer overflow
- Examples of buffer overflow attacks
- Presentation of ways to secure programs to prevent buffer overflow attacks
Buffer. Buffer Overflow
What is a Buffer?
A buffer is a memory area defined by a start address and a size. Let N be the size of the buffer, for example, the number of elements. The total size of the buffer is N times the size of one element. A string is a specific case of a buffer.
What is a Buffer Overflow?
A buffer overflow occurs when the upper limit of a buffer is exceeded during traversal, for example, the position of the last element (v[N - 1]). A buffer overflow is a specific case of an index out of bounds, where the vector can be accessed using negative indices. Many functions in C do not check the size of the buffers they work with, leading to buffer overflow errors when called. Some examples of such functions are:
A classic example of a buffer overflow is shown in the following code:
char buffer[32];
fgets(buffer, 64, stdin);
Executing this code will result in a buffer overflow, potentially leading to a Segmentation Fault error, although this is not guaranteed. Everything depends on the buffer's position in the stack and what will be overwritten by the 32 bytes that exceed the buffer's size. More details on what will be overwritten and how this will be done will be discovered when solving the proposed exercises.
How to make a Buffer Overflow?
In the previous example we see that we can use a buffer to write more than "we should". But what exactly can we do with this? In the following example we can use fgets() to overwrite very_important_var and enter jackpot() function. All we have to do is change the value of very_important_var to 0xDEADCODE, when reading the buffer.
int very_important_var = 0xDEADBEEF;
char buffer[32];
fgets(buffer, 64, stdin);
if (very_important_var == 0xDEADC0DE) {
jackpot();
}
Infrastructure Preparation
Throughout the laboratory, in the command-line, we will use:
nasm
assemblergcc
command as a linkerobjdump
andghidra
for disassembling object and executable filesgdb
for dynamic analysis, investigation, and debugging
This setup will allow us to perform assembly programming, linking, disassembly, and dynamic analysis using the specified tools in the command-line environment.
Buffer Overflow Attacks
How is Buffer Overflow Used?
Buffer overflow can be exploited by a potential attacker to overwrite certain data within a program, affecting the execution flow and providing specific benefits to the attacker. Most often, an attacker initiates a buffer overflow attack with the goal of gaining access to confidential data that a normal user would not typically have access to.
Buffer overflow attacks are generally used on static buffers stored at the stack level. This is because the stack, in addition to program data, also stores return addresses following function calls (see laboratory 7). These addresses can be overwritten through a buffer overflow attack, in which case the program's execution flow can be altered. By overwriting the return address, upon completion of the current function's execution, the program will not return to the calling function's execution but will instead "jump" to another address within the executable where execution will continue. This event can lead to undefined behavior of the program if the jump address has not been calculated correctly.
The goal of an attacker is to take control of a system by gaining access to a shell from which they can run commands. This can be achieved by overwriting the return address, using a system call through which a shell can be opened on the system where the executable is running (more details in the OS course).
How Do We Protect Against Buffer Overflow Attacks?
There are many ways to protect an executable from these types of attacks, most of which you will study in detail in the OS course next year. A good practice against this type of attack is to avoid using insecure functions, such as those mentioned above. More details on best practices against buffer overflow attacks can be found here.
Often, best practices prove to be insufficient in the "battle" against hackers, which is why several executable protection mechanisms have been invented by manipulating the code and its position within the executable (Position Independent Code - PIC), through address randomization (Address Space Layout Randomization - ASLR), or by introducing additional checks in the code to detect potential attacks.
These checks are performed by introducing special values, called canaries, on the stack between the buffer and the function's return address. These values are generated and placed within the executable by the compiler and differ with each run of the executable. When an attacker tries to overwrite the return address, they will also overwrite the canary value, and before exiting the current function call, it will be checked whether that value has been modified or not. If it has been modified, it means that a buffer overflow has occurred, and the program execution will be interrupted.
This mechanism is called Stack Smashing Protection or Stack Guard. More details about Stack Guard and buffer overflow attacks can be found here.