Exploit Mitigation Techniques on Linux Systems


« 📅 published on 10/Jul/2012 »

🔖 tagged exploit


Introduction

Each year we see phenomenal research work being presented in a number of security conferences and events around the world. Vendors/communities present solutions for existing issues and those on the offensive side present workarounds against these solutions. Eventually this race, between those working on either sides of the coin, helps to make our world a safer place.

Over the last decade, reliable exploitation of memory corruption bugs has become extremely difficult. This has happened, primarily, due to the introduction of various exploit mitigation techniques. In this post, we'll be looking at the current state of exploitation within the Linux environment.The following security/mitigation techniques are commonly available on most recent distributions:

Except for ASLR, which effects system-wide configuration, all of the other techniques are user-space mitigation features that have to be enabled on a per-binary basis. Here is the sample program that we'll be using for our tests:

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

int main () {
  char array[80];
  printf ("&array: %p\n", &array);
  gets (array);
  printf ("*array: {\"%s\"}\n", array);
  return 0;
}

Let's have a detailed look at each of the above techniques:


Techniques

Address Space Layout Randomization (ASLR)

Enables randomization of  various memory allocation segments (stack, mmap, exec, brk, and vdso). When enabled, each invocation of a binary will have its memory allocations randomized within the available virtual address space on the system. As such, an exploit technique like Ret2libc, that requires static memory addresses of common library functions, is no longer effective:

$ sysctl kernel.randomize_va_space
kernel.randomize_va_space = 2
$
$ sysctl -w kernel.randomize_va_space=0
kernel.randomize_va_space = 0
$
$ cat /proc/sys/kernel/randomize_va_space
0
$ echo 2 > /proc/sys/kernel/randomize_va_space

The randomize_va_space kernel parameter defines system-wide configuration setting for ASLR. This parameter could be set to the following values:

Once enabled, each invocation of a program will have different memory locations assigned to it:

$ cat /proc/self/maps
08048000-08054000 r-xp 00000000 08:01 261660     /bin/cat
08054000-08055000 r--p 0000b000 08:01 261660     /bin/cat
08055000-08056000 rw-p 0000c000 08:01 261660     /bin/cat
0827b000-0829c000 rw-p 00000000 00:00 0          [heap]
b7588000-b75c7000 r--p 00000000 08:01 415795     /usr/lib/locale/en_US.utf8/LC_CTYPE
b75c7000-b75c8000 r--p 00000000 08:01 415800     /usr/lib/locale/en_US.utf8/LC_NUMERIC
b75c8000-b76e6000 r--p 00000000 08:01 415794     /usr/lib/locale/en_US.utf8/LC_COLLATE
b76e6000-b76e7000 rw-p 00000000 00:00 0
b76e7000-b783a000 r-xp 00000000 08:01 5428       /lib/tls/i686/cmov/libc-2.11.1.so
b783a000-b783b000 ---p 00153000 08:01 5428       /lib/tls/i686/cmov/libc-2.11.1.so
b783b000-b783d000 r--p 00153000 08:01 5428       /lib/tls/i686/cmov/libc-2.11.1.so
b783d000-b783e000 rw-p 00155000 08:01 5428       /lib/tls/i686/cmov/libc-2.11.1.so
b783e000-b7841000 rw-p 00000000 00:00 0
b7841000-b7842000 r--p 00000000 08:01 415834     /usr/lib/locale/en_US.utf8/LC_TIME
b7842000-b7843000 r--p 00000000 08:01 415833     /usr/lib/locale/en_US.utf8/LC_MONETARY
b7843000-b7844000 r--p 00000000 08:01 415837     /usr/lib/locale/en_US.utf8/LC_MESSAGES/SYS_LC_MESSAGES
b7844000-b7845000 r--p 00000000 08:01 415710     /usr/lib/locale/en_US.utf8/LC_PAPER
b7845000-b7846000 r--p 00000000 08:01 415669     /usr/lib/locale/en_US.utf8/LC_NAME
b7846000-b7847000 r--p 00000000 08:01 415832     /usr/lib/locale/en_US.utf8/LC_ADDRESS
b7847000-b7848000 r--p 00000000 08:01 415838     /usr/lib/locale/en_US.utf8/LC_TELEPHONE
b7848000-b7849000 r--p 00000000 08:01 415667     /usr/lib/locale/en_US.utf8/LC_MEASUREMENT
b7849000-b7850000 r--s 00000000 08:01 294899     /usr/lib/gconv/gconv-modules.cache
b7850000-b7851000 r--p 00000000 08:01 415835     /usr/lib/locale/en_US.utf8/LC_IDENTIFICATION
b7851000-b7853000 rw-p 00000000 00:00 0
b7853000-b7854000 r-xp 00000000 00:00 0          [vdso]
b7854000-b786f000 r-xp 00000000 08:01 969        /lib/ld-2.11.1.so
b786f000-b7870000 r--p 0001a000 08:01 969        /lib/ld-2.11.1.so
b7870000-b7871000 rw-p 0001b000 08:01 969        /lib/ld-2.11.1.so
bfa54000-bfa75000 rw-p 00000000 00:00 0          [stack]
$ cat /proc/self/maps
08048000-08054000 r-xp 00000000 08:01 261660     /bin/cat
08054000-08055000 r--p 0000b000 08:01 261660     /bin/cat
08055000-08056000 rw-p 0000c000 08:01 261660     /bin/cat
08f09000-08f2a000 rw-p 00000000 00:00 0          [heap]
b755b000-b759a000 r--p 00000000 08:01 415795     /usr/lib/locale/en_US.utf8/LC_CTYPE
b759a000-b759b000 r--p 00000000 08:01 415800     /usr/lib/locale/en_US.utf8/LC_NUMERIC
b759b000-b76b9000 r--p 00000000 08:01 415794     /usr/lib/locale/en_US.utf8/LC_COLLATE
b76b9000-b76ba000 rw-p 00000000 00:00 0
b76ba000-b780d000 r-xp 00000000 08:01 5428       /lib/tls/i686/cmov/libc-2.11.1.so
b780d000-b780e000 ---p 00153000 08:01 5428       /lib/tls/i686/cmov/libc-2.11.1.so
b780e000-b7810000 r--p 00153000 08:01 5428       /lib/tls/i686/cmov/libc-2.11.1.so
b7810000-b7811000 rw-p 00155000 08:01 5428       /lib/tls/i686/cmov/libc-2.11.1.so
b7811000-b7814000 rw-p 00000000 00:00 0
b7814000-b7815000 r--p 00000000 08:01 415834     /usr/lib/locale/en_US.utf8/LC_TIME
b7815000-b7816000 r--p 00000000 08:01 415833     /usr/lib/locale/en_US.utf8/LC_MONETARY
b7816000-b7817000 r--p 00000000 08:01 415837     /usr/lib/locale/en_US.utf8/LC_MESSAGES/SYS_LC_MESSAGES
b7817000-b7818000 r--p 00000000 08:01 415710     /usr/lib/locale/en_US.utf8/LC_PAPER
b7818000-b7819000 r--p 00000000 08:01 415669     /usr/lib/locale/en_US.utf8/LC_NAME
b7819000-b781a000 r--p 00000000 08:01 415832     /usr/lib/locale/en_US.utf8/LC_ADDRESS
b781a000-b781b000 r--p 00000000 08:01 415838     /usr/lib/locale/en_US.utf8/LC_TELEPHONE
b781b000-b781c000 r--p 00000000 08:01 415667     /usr/lib/locale/en_US.utf8/LC_MEASUREMENT
b781c000-b7823000 r--s 00000000 08:01 294899     /usr/lib/gconv/gconv-modules.cache
b7823000-b7824000 r--p 00000000 08:01 415835     /usr/lib/locale/en_US.utf8/LC_IDENTIFICATION
b7824000-b7826000 rw-p 00000000 00:00 0
b7826000-b7827000 r-xp 00000000 00:00 0          [vdso]
b7827000-b7842000 r-xp 00000000 08:01 969        /lib/ld-2.11.1.so
b7842000-b7843000 r--p 0001a000 08:01 969        /lib/ld-2.11.1.so
b7843000-b7844000 rw-p 0001b000 08:01 969        /lib/ld-2.11.1.so
bfb89000-bfbaa000 rw-p 00000000 00:00 0          [stack]

In the above output note that all the segments of the /bin/cat process are mapped at different memory locations with each invocation. However, closer look provides an interesting observation. The first three segments that contain .text section of the binary (notice the r-x permissions) are still mapped at similar locations each time. We have enabled ASLR and it is indeed active, so why aren't the .text segments not mapped randomly? We will talk about this behavior in much detail within the PIE section.

Non-Executable Bit (NX)

This feature disallows code execution from marked memory pages/segments. It is also referred to as W^X (W XOR X) due to the fact that the pages marked with this feature could either be writable or executable but not both at the same time. When enabled, a process's memory allocations, that do not contain instructions, will have only rw- permissions assigned to them by default. As such, even if an attacker successfully injects code into a writable memory region through an overflow bug, an attempt to execute code from this section would still fail. NX is enabled through the MMU by setting bit 63 of the page directory entry. Important thing to note here is that this feature is available only on those systems that have 64bit capability or on those systems that use a PAE-enabled kernel. This is because on a regular 32bit kernel without PAE support, a page directory entry is just 32bit wide and hence there is no room to store additional meta-information about memory pages it points to. The Execshield and Grsecurity set of kernel patches could also be used to simulate this behavior when the above requirements could not be met:

$ gcc -o test test.c 2>/dev/null && readelf -l test | grep GNU_STACK
  GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RW  0x4
$
$ gcc -z execstack -o test test.c 2>/dev/null && readelf -l test | grep GNU_STACK
  GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RWE 0x4
$
$ gcc -z noexecstack -o test test.c 2>/dev/null && readelf -l test | grep GNU_STACK
  GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RW  0x4

Note the permissions of the GNU_STACK section in the above output. When we request executable stack through the linker option, GCC marks the stack as executable with RWE permissions. On my test system, which is an Ubuntu 10.04 derivative with GCC version 4.4.3, the default command-line disables executable stack markings as evident in the output of first command-line. This behavior of implicitly enabling safeguards makes an application immune to stack-based execution and other such attacks even if the developer fails to include them during compilation.

Stack Canaries/Stack-Smashing Protection (SSP)

Stack Canaries are a protection feature that safeguard critical program metadata information located on call stack. When enabled, a random canary value is placed on the stack, just below the saved registers from the function prologue. Before a program returns control to its parent, the saved canary value is checked. Any attempts to overwrite the saved return address on the stack will also overwrite the saved cookie and as such the above check would fail. In such cases, the __stack_chk_fail function is called, which displays a friendly stack smashing detected message and aborts the execution of the program.This mitigation technique also reorders the placement of local variables on the stack. This is done to ensure that any variable, that directly influences the program control and redirects its normal flow, is placed below a buffer that accepts user-supplied input. Such a placement prevents overwriting of variables placed adjacent to buffers. To read more about other such novel ideas implemented in this protection technique, visit this link: SSP

$ gcc -fstack-protector -o test test.c 2>/dev/null && readelf -r test | grep _chk
0804a010  00000507 R_386_JUMP_SLOT   00000000   __stack_chk_fail
$
$ gcc -fstack-protector-all -o test test.c 2>/dev/null && readelf -r test | grep _chk
0804a010  00000507 R_386_JUMP_SLOT   00000000   __stack_chk_fail
$
$ gcc -fno-stack-protector -o test test.c 2>/dev/null && readelf -r test | grep _chk

The following options enable/disable this check:

FORTIFY_SOURCE

There are cases when a compiler can correctly estimate the size of a destination operand used in a certain string operation. For such cases, the compiler could be requested to replace any vulnerable function calls in the program source with their equivalent safer counterparts. This would eventually make the compiled binary resilient to most overflow attempts without significantly impacting its performance:

$ gcc -O1 -D_FORTIFY_SOURCE=2 -o test test.c 2>/dev/null && readelf -r test | grep _chk
0804a004  00000207 R_386_JUMP_SLOT   00000000   __printf_chk
0804a008  00000307 R_386_JUMP_SLOT   00000000   __gets_chk
0804a010  00000507 R_386_JUMP_SLOT   00000000   __stack_chk_fail

In the above output you could see that the GCC option -D_FORIFY_SOURCE has been used to include fortifying checks. The call for function printf and gets were replaced with their safer equivalents, printf_chk and gets_chk respectively. This option can accept two values:

RELocation Read-Only (RELRO)

Another mitigation technique that safeguard against those exploits that require Global Offset Table (GOT) modifications. For this to work, all dynamic symbol resolutions, requested by a binary, have to be carried out before the program execution begins. Once this is done, the GOT could be marked as read-only, thus preventing any runtime modifications.

By default, when we use the GCC linker option -Wl,-z,relro, PLT (Procedure Linking Table) entries, which include references for library functions within a process's memory allocation, are marked as writable (lazy-linking). All other GOT entries apart from PLT remain read-only, providing what is know as Partial-RELRO support:

$ gcc -Wl,-z,norelro -o test test.c 2>/dev/null && readelf -l test | grep GNU_RELRO
$
$ gcc -Wl,-z,relro -o test test.c 2>/dev/null && readelf -l test | grep GNU_RELRO
  GNU_RELRO      0x000f0c 0x08049f0c 0x08049f0c 0x000f4 0x000f4 R   0x1
$
$ gcc -Wl,-z,relro,-z,now -o test test.c 2>/dev/null && readelf -l test | grep GNU_RELRO && readelf -d test | grep BIND_NOW
  GNU_RELRO      0x000ee8 0x08049ee8 0x08049ee8 0x00118 0x00118 R   0x1
 0x00000018 (BIND_NOW)

The -z,now option ensures that PLT entries are resolved immediately before execution, thus allowing the entire GOT to be marked as read-only. This ensure that Full-RELRO support is enabled for the compiled program. The summary for these options is:

Position-Independent Executable (PIE)

This feature helps to load a program at a random memory location on each invocation. With ASLR enabled, the stack, heap, and mmap allocations are automatically randomized. However, like we saw earlier with the /bin/cat binary, the .text and other sections of a program are still loaded at static addresses. To make all sections of a program to load at random addresses, we need to compile it with PIE support:

$ gcc -o test test.c 2>/dev/null | file test | cut -d, -f1
test: ELF 32-bit LSB executable
$
$ gcc -fpie -pie -o test test.c 2>/dev/null | file test | cut -d, -f1
test: ELF 32-bit LSB executable

The following GCC options could be used to enable PIE support as evident from the above output: -fpie -pie (recommended). Programs compiled with this feature are marked as shared relocatable, much similar to shared object libraries used in dynamic linking. To read more, visit this link: PIE


Conclusion

Enabling these mitigation techniques will definitely improve the overall security posture of a system, it still does not make it bullet-proof. Some of these techniques might break compatibility with legacy applications, while others might not work as expected. Different distributions use different default configuration settings and as such you can not simply standardize. The most suitable option would be to test your application code first hand with each of these options, carefully considering the tradeoffs and using only those that provide that rare mix of security and usability.


« Vulnerable Weekends #8: HP Loa... «

» Gera's Warming Up on Stack #1 ... »

  