Each year we see phenomenal research works 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 above 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:
Let's have a detailed look at each of the above techniques:
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. As such, an exploit technique like Ret2libc, that requires static memory addresses of common library functions, is no longer effective.
The randomize_va_space kernel parameter defines system-wide configuration setting for ASLR. This parameter could be set to the following values:
0 - ASLR is turned OFF
1 - ASLR is turned ON (stack randomization)
2 - ASLR is turned ON (stack, heap, and mmap allocation randomization)
Once enabled, each invocation of a program will have different memory locations assigned to it:
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.
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:
-z execstack - request the linker to mark program stack as executable
-z noexecstack - request the linker to mark program stack as non-executable (recommended)
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 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
The following options enable/disable this check:
-fstack-protector - enable checks for functions with character buffers of size 8B or higher
-fstack-protector-all - enable checks for all functions (recommended)
-fno-stack-protector - disable stack protection checks
-Wstack-protector - emit warnings for all unprotected functions (recommended)
--parm=ssp-buffer-size=<size_in_bytes> - modifies the default 8B buffer length
GCC versions 4.x include SSP techniques in their native implementations. Prior 3.x versions had this feature enabled through a patch.
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:
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:
-D_FORIFY_SOURCE=1 - to enable checks against buffer overflow attacks
-D_FORIFY_SOURCE=2 - to enable checks against buffer overflow and format string attacks (recommended)
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:
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:
-Wl,-z,relro - enables Partial RELRO support
-Wl,-z,relro,-z,now - enables Full-RELRO support (recommended)
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:
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
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.