Day One - Buffer Overflow Introduction
The Follow YouTube provides a brief introduction on what a buffer overflow actually is, before we begin to look at them technically. #
The Assembly Instruction Cycle #
Instruction | Description |
---|---|
1. FETCH | The next machine instruction address is read from theInstruction Address Register (IAR ). It is then loaded from theCache orRAM into theInstruction Register (IR ). |
2. DECODE | The instruction decoder converts the instructions and starts the necessary circuits to execute the instruction. |
3. FETCH OPERANDS | If further data have to be loaded for execution, these are loaded from the cache orRAM into the working registers. |
4. EXECUTE | The instruction is executed. This can be, for example, operations in theALU , a jump in the program, the writing back of results into the working registers, or the control of peripheral devices. Depending on the result of some instructions, the status register is set, which can be evaluated by subsequent instructions. |
5. UPDATE INSTRUCTION POINTER | If no jump instruction has been executed in the EXECUTE phase, theIAR is now increased by the length of the instruction so that it points to the next machine instruction. |
The Program’s Memory Layout #
.text #
The.text
section contains the actual assembler instructions of the program. This area can be read-only to prevent the process from accidentally modifying its instructions. Any attempt to write to this area will inevitably result in a segmentation fault.
.data #
The.data
section contains global and static variables that are explicitly initialized by the program.
.bss #
Several compilers and linkers use the.bss
section as part of the data segment, which contains statically allocated variables represented exclusively by 0 bits.
The Heap #
Heap memory
is allocated from this area. This area starts at the end of the “.bss” segment and grows to the higher memory addresses.
The Stack #
Stack memory
is aLast-In-First-Out
data structure in which the return addresses, parameters, and, depending on the compiler options, frame pointers are stored.C/C++
local variables are stored here, and you can even copy code to the stack. TheStack
is a defined area inRAM
. The linker reserves this area and usually places the stack in RAM’s lower area above the global and static variables. The contents are accessed via thestack pointer
, set to the upper end of the stack during initialization. During execution, the allocated part of the stack grows down to the lower memory addresses.
Modern memory protections (DEP
/ASLR
) would prevent the damage caused by buffer overflows. DEP (Data Execution Prevention), marked regions of memory “Read-Only”. The read-only memory region is where some user-input is stored (Example: The Stack), so the idea behind DEP was to prevent users from uploading shellcode to memory and then setting the instruction pointer to the shellcode. Hackers started utilizing ROP (Return Oriented Programming) to get around this, as it allowed them to upload the shellcode to an executable space and use existing calls to execute it. With ROP, the attacker needs to know the memory addresses where things are stored, so the defense against it was to implement ASLR (Address Space Layout Randomization) which randomizes where everything is stored making ROP more difficult.
Users can get around ASLR by leaking memory addresses, but this makes exploits less reliable and sometimes impossible. For example the“Freefloat FTP Server”is trivial to exploit on Windows XP (before DEP/ASLR). However, if the application is ran on a modern Windows operating system, the buffer overflow exists but it is currently non-trivial to exploit due to DEP/ASLR (as there’s no known way to leak memory addresses.)
CPU Registers #
Registers are the essential components of a CPU. Almost all registers offer a small amount of storage space where data can be temporarily stored. However, some of them have a particular function.
These registers will be divided into General registers, Control registers, and Segment registers. The most critical registers we need are the General registers. In these, there are further subdivisions into Data registers, Pointer registers, and Index registers.
Data registers #
32-bit Register | 64-bit Register | Description |
---|---|---|
EAX | RAX | Accumulator is used in input/output and for arithmetic operations |
EBX | RBX | Base is used in indexed addressing |
ECX | RCX | Counter is used to rotate instructions and count loops |
EDX | RDX | Data is used for I/O and in arithmetic operations for multiply and divide operations involving large values |
Pointer registers #
32-bit Register | 64-bit Register | Description |
---|---|---|
EIP | RIP | Instruction Pointer stores the offset address of the next instruction to be executed |
ESP | RSP | Stack Pointer points to the top of the stack |
EBP | RBP | Base Pointer is also known asStack Base Pointer orFrame Pointer thats points to the base of the stack |
The vulnerable program #
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
int bowfunc(char *string) {
char buffer[1024];
strcpy(buffer, string);
printf("Buffer: %s\n", buffer);
return 1;
}
int main(int argc, char *argv[]) {
printf("Input: %s\n",argv[1]);
bowfunc(argv[1]);
printf("Done.\n");
return 1;
}
Disabling ASLR #
Address Space Layout Randomization (ASLR) is a modern protection that randomizes the memory addresses used within program to prevent buffer overflow vulnerabilities. For our purposes, we’ll disable this.
In a real-life scenario, you’d likely need to write code in your exploit to map out memory of the system and attack the specific memory addresses that you are looking for.
student@lab-machine:~$ sudo su
root@lab-machine:/home/student# echo 0 > /proc/sys/kernel/randomize_va_space
root@lab-machine:/home/student# cat /proc/sys/kernel/randomize_va_space
Vulnerable Binary Compilation #
We can now compile our executable into a 32-bit ELF Binary.
In order to compile for 32-bit linux, we first need to install gcc-multilib, which provides the ability to compile for 32-bit and 64-bit.
Within the compilation command, we’re also disabling several additional protections.
sudo apt update
sudo apt install gcc-multilib -y
gcc bow.c -o bow32 -fno-stack-protector -z execstack -m32
Vulnerable Functions to Avoid #
strcpy(dest, src)
- Copies a string (src) into another (dest), including the null terminator ‘\0’.
char src[] = "Hello";
char dest[10];
strcpy(dest, src); // dest now contains "Hello"
gets(buffer)
- Reads a line from standard input (stdin) into buffer until a newline or EOF, then appends ‘\0’.
char name[20];
gets(name);
printf("Hello %s\n", name);
sprintf(buffer, format, ...)
- Writes formatted output (like printf) into a string buffer instead of printing to the screen.
char message[50];
int age = 25;
sprintf(message, "I am %d years old", age);
// message contains "I am 25 years old"
scanf(format, ...)
- Reads formatted input from standard input (stdin).
int age;
scanf("%d", &age);
strcat(dest, src)
- Appends the string src to the end of dest (overwriting the ‘\0’ in dest).
char dest[20] = "Hello, ";
char src[] = "world!";
strcat(dest, src); // dest becomes "Hello, world!"
GNU Debugger #
GDB, or the GNU Debugger, is the standard debugger of Linux systems developed by the GNU Project. It has been ported to many systems and supports the programming languages C, C++, Objective-C, FORTRAN, Java, and many more.
GDB provides us with the usual traceability features like breakpoints or stack trace output and allows us to intervene in the execution of programs. It also allows us, for example, to manipulate the variables of the application or to call functions independently of the normal execution of the program.
GNU Debugger
(GDB
) is used to view the created binary on the assembler level.
We can run the debugger on our program using the following command:
gdb -q bow32
To view the assembly instructions of the main function, we run:
(gdb) disassemble main
- The first column is the memory address
- The second column is the address jump in memory in Bytes, used for the respective instruction. Note that this is based on our current position in the program.
- The third column is the assembler instructions (mnemonics)
- The forth column is the register for the instruction and any operation suffixes.
By default, gdb uses AT&T which can be difficult to interpret, so we can switch it to Intel syntax, by exiting and then setting the default format
q
(gdb) exit
echo 'set disassembly-flavor intel' > ~/.gdbinit
gdb ./bow32 -q
Enable debuginfod for this session? (y or [n]) y
(gdb) disassemble main
n the intel Syntax, assembly is written destination first, then source
0x0000058d <+11>: mov ebp,esp
The destination register is ebp, the source is esp. In this line, 0x0000058d
refers to the location in memory and the action is 11bytes into the start of our current function.
Stack Frames #
Since the stack starts with a high address and grows down to low memory addresses as values are added, theBase Pointer
points to the beginning (base) of the stack in contrast to theStack Pointer
, which points to the top of the stack.
As the stack grows, it is logically divided into regions calledStack Frames
, which allocate the required memory in the stack for the corresponding function. A stack frame defines a frame of data with the beginning (EBP
) and the end (ESP
) that is pushed onto the stack when a function is called.
Since the stack memory is built on aLast-In-First-Out
(LIFO
) data structure, the first step is to store theprevious EBP
position on the stack, which can be restored after the function completes. If we now look at thebowfunc
function, it looks like following in GDB:
(gdb) disas bowfunc
Dump of assembler code for function bowfunc:
0x0000054d <+0>: push ebp # <---- 1. Stores previous EBP
0x0000054e <+1>: mov ebp,esp # <---- 2. Creates new Stack Frame
0x00000550 <+3>: push ebx
0x00000551 <+4>: sub esp,0x404 # <---- 3. Moves ESP to the top
<...SNIP...>
0x00000580 <+51>: leave
0x00000581 <+52>: ret
- The
EBP
in the stack frame is set first when a function is called and contains theEBP
of the previous stack frame. - Next, the value of the
ESP
is copied to theEBP
, creating a new stack frame. - Then some space is created in the stack, moving the
ESP
to the top for the operations and variables needed and processed.
Epilogue #
(gdb) disas bowfunc
Dump of assembler code for function bowfunc:
0x0000054d <+0>: push ebp
0x0000054e <+1>: mov ebp,esp
0x00000550 <+3>: push ebx
0x00000551 <+4>: sub esp,0x404
<...SNIP...>
0x00000580 <+51>: leave # <----------------------
0x00000581 <+52>: ret # <--- Leave stack frame
During the epilogue, theESP
is replaced by the currentEBP
, and its value is reset to the value it had before in the prologue.
Index registers #
Register 32-bit | Register 64-bit | Description |
---|---|---|
ESI | RSI | Source Index is used as a pointer from a source for string operations |
EDI | RDI | Destination is used as a pointer to a destination for string operations |
Another important point concerning the representation of the assembler is the naming of the registers. This depends on the format in which the binary was compiled. We have used GCC to compile thebow.c
code in 32-bit format. Now let’s compile the same code into a64-bit
format.
gcc bow.c -o bow64 -fno-stack-protector -z execstack -m64
gdb -q bow64
(gdb) disassemble main
You’ll notice when looking at this, that the addresses are twice as large as the 32-bit binary, and there are half as many instructions as with the 32-bit binary.
When looking for buffer overflow possibilities, the instructions we are most focused on are the call
instructions as these are used to execute functions by performing two operations:
- it pushes the return address onto the
stack
so that the execution of the program can be continued after the function has successfully fulfilled its goal, - it changes the
instruction pointer
(EIP
) to the call destination and starting execution there.
Endianness #
During load and save operations in registers and memories, the bytes are read in a different order. This byte order is calledendianness
. Endianness is distinguished between thelittle-endian
format and thebig-endian
format.
Big-endian
andlittle-endian
are about the order of valence. Inbig-endian
, the digits with the highest valence are initially. Inlittle-endian
, the digits with the lowest valence are at the beginning. Mainframe processors use thebig-endian
format, some RISC architectures, minicomputers, and in TCP/IP networks, the byte order is also inbig-endian
format.
Now, let us look at an example with the following values:
- Address:
0xffff0000
- Word:
\xAA\xBB\xCC\xDD
Memory Address | 0xffff0000 | 0xffff0001 | 0xffff0002 | 0xffff0003 |
---|---|---|---|---|
Big-Endian | AA | BB | CC | DD |
Little-Endian | DD | CC | BB | AA |
This will be very important for us when entering our code in the right order later on when we have to tell the CPU to which address it should point to. This doesn’t matter much for today’s lesson.
Causing a Crash #
How the program is expected to run:
./bow32 "Hello world"
So how do we exploit this buffer overflow? Simply provide it more characters than it can handle.
./bow32 $(python3 -c "print('\x55' * 1200)")
Now why did this actually happen? #
Let’s open this binary in gdb again
gdb -q bow32
We’ll then tell the debugger to run it with the same input as before:
(gdb) run $(python3 -c "print('\x55' * 1200)")
This confirms that it did crash again (duh), but let’s look at the CPU registers to determine what happened.
(gdb) info registers
Looking at the data in the current memory registers, you see that the eip register, which is used to store the offset address to the next instruction to executed has been overwritten with uuuuu
or hex 0x55
.
This is what ultimately causes the crash because the CPU doesn’t know what instruction to execute next.
In the next lesson, we’ll work on taking over the EIP register in order to insert our own code and achieve code execution.