Introduction

This article provides an overview of various techniques that can be used to mitigate buffer overflow vulnerabilities. In addition to the mitigations offered by the compilers and operating systems, we will also discuss preventive measures that can be used while writing programs in languages susceptible to buffer overflow vulnerabilities. 

Techniques to prevent or mitigate buffer overflows vulnerabilities

Following are various common ways we can use to prevent or mitigate buffer overflow vulnerabilities. Let’s discuss each of them in detail.

  1. Writing secure code.
  2. Making use of compiler warnings
  3. Stack canaries.
  4. Data execution prevention
  5. Address space layout randomization

Writing secure code

Writing secure code is the best way to prevent buffer overflow vulnerabilities. When programs are written in languages that are susceptible to buffer overflow vulnerabilities, developers must be aware of risky functions and avoid using them wherever possible. For example, avoid using functions such as gets and use fgets instead, which allows the developer to specify how much buffer is expected. 

While this is the best way to prevent buffer overflows, it may be hard to change legacy applications and applications that work only on legacy operating systems. Because of these challenges, we may have to rely on other protections offered by compilers and operating systems.

Compiler warnings

When developing new software with vulnerable functions, compilers often provide warnings and recommend use of secure alternatives of the functions used. Developers can quickly make these changes during their development phase. 

The following excerpt shows the compiler warning about use of the gets function.

$ gcc -fno-stack-protector vulnerable.c -o vulnerable -z execstack -D_FORTIFY_SOURCE=0

vulnerable.c: In function ‚Äėvuln_func‚Äô:

vulnerable.c:15:1: warning: implicit declaration of function ‚Äėgets‚Äô; did you mean ‚Äėfgets‚Äô? [-Wimplicit-function-declaration]

   15 | gets(buffer);

      | ^~~~

      | fgets

/usr/bin/ld: /tmp/ccWNPiro.o: in function `vuln_func’:

vulnerable.c:(.text+0x4f): warning: the `gets’ function is dangerous and should not be used.

$

Stack canaries

When stack-based buffer overflows became popular, compilers introduced new options to protect important data on the stack such as return addresses. These canaries are random values generated on every run of the program; they are placed on the stack and usually verified just before returning to the caller functions. 

If there is stack overflow and the canary is overwritten with user-supplied input, the execution of the program stops and an error will be thrown. The following excerpt shows how stack overflows are spotted by stack canaries.

$ ./vulnerable test

test

AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 

*** stack smashing detected ***: terminated

Aborted (core dumped)

$

While these stack canaries protect applications from buffer overflows, these can be brute-forced on 32-bit systems. They can also be leaked using other memory read vulnerabilities such as format string vulnerabilities. Thus, stack canaries should only be treated as a defense-in-depth option but not be treated as a bulletproof technique to protect applications from buffer overflow attacks.

Data execution prevention

When exploiting buffer overflows, attackers often place malicious code in places like stack and heap and achieve unauthorized execution in the context of the target application. Because of this, a new buffer overflow mitigation technique called data execution prevention is introduced. In Linux, this is known as NX (No Execute). 

DEP can be enabled at both hardware level and software level. When NX is enabled, the stack becomes non-executable and any malicious code placed on the stack as part of an attack will be deemed to be non-executable. 

The following excerpt shows that stack became non-executable (rw-) on a binary due to the NX bit enabled.

0x00007ffff7ffd000 0x00007ffff7ffe000 0x000000000002d000 rw- /usr/lib/x86_64-linux-gnu/ld-2.31.so

0x00007ffff7ffe000 0x00007ffff7fff000 0x0000000000000000 rw- 

0x00007ffffffde000 0x00007ffffffff000 0x0000000000000000 rw- [stack]

0xffffffffff600000 0xffffffffff601000 0x0000000000000000 –x [vsyscall]

Just like stack canaries, the NX bit does not provide complete protection against buffer overflow vulnerabilities and there are bypasses available. A determined attacker can make use of the buffer overflow vulnerability to turn the stack to an executable one by using a concept called return-oriented programming. Attackers often utilize executable instructions from other areas of memory using return-oriented programming. 

Address space layout randomization

Address space layout randomization (ASLR) makes it harder for an attacker by randomizing the base addresses of the libraries and other memory areas such as stack. This makes it harder for an attacker to build ROP chains, as ROP chains heavily rely on addresses of instructions from these libraries being loaded into the program. 

To check if ASLR is enabled on a Linux machine (specifically on Ubuntu), the following command can be used.

$ sudo cat /proc/sys/kernel/randomize_va_space

2

$

As you can see, the value of the file shown in the preceding excerpt is set to 2, which means ASLR is fully enabled. The effect of this value looks as follows:

$ ldd ./vulnerable

linux-vdso.so.1 (0x00007ffff050b000)

libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007ff28818b000)

/lib64/ld-linux-x86-64.so.2 (0x00007ff288390000)

$ ldd ./vulnerable

linux-vdso.so.1 (0x00007fffda077000)

libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f895852d000)

/lib64/ld-linux-x86-64.so.2 (0x00007f8958732000)

$ ldd ./vulnerable

linux-vdso.so.1 (0x00007ffd88177000)

libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f58a4dd2000)

/lib64/ld-linux-x86-64.so.2 (0x00007f58a4fd7000)

The preceding excerpt shows the usage of the ldd command to check what libraries are being loaded by the binary in runtime along with their base addresses. We ran the command three times and the addresses associated with libc.so.6 library are different every time the binary is loaded. This is due to ASLR, and this makes it harder for an attacker to guess these addresses.

Conclusion

While exploit mitigation techniques such as ASLR, NX and Canaries are commonly used, it is always recommended to write secure code to prevent buffer overflow attacks. As we have discussed with examples, exploit mitigations should be treated as defense-in-depth techniques to mitigate buffer overflow attacks. 

All of these exploit mitigation techniques should be enabled wherever possible, to make it harder for an attacker to successfully exploit these vulnerabilities.

 

Sources

  1. Buffer Overflow, OWASP
  2. Stack-Based Buffer Overflow Attacks: Explained and Examples, Rapid7
  3. What Is a Buffer Overflow, Acunetix

Be Safe

Section Guide

Srinivas

View more articles from Srinivas

As you grow in your cybersecurity career, Infosec Skills is the platform to ensure your skills are scaled to outsmart the latest cyber threats.

Section Guide

Srinivas

View more articles from Srinivas