In the first part of this series we discussed the mechanics of an exploit, the general concept of hardening, and the stack protector hardening technique in particular. Some of the concepts explained there will be used here, too, so you might want to read at least the first few paragraphs.
As mentioned in the first part, exploits often inject code into a program by overwriting data structures, like
char buffers. This code is then jumped to, yielding for example a root shell, which the attacker can use further (hence the term “shell code”). Note that although the code resides in an area meant for program data (the stack or the heap), it can still be executed. Executable-space protection changes this by only marking these memory pages as executable that need it. This is supported on most modern processors and most operating systems. The names of the implementations differ, but the basic concepts remain the same.
On Linux specifically, these measures are taken:
- When the program is loaded into memory, only those memory pages containing code get execute permissions. The loader determines this by looking at the ELF headers. These sections contain code:
.fini: Code that runs at initialization and teardown.
.plt.got: Trampoline code needed for access to functions located in other shared libraries.
.text: Everything else, i.e. the “real” code of the program.
- The heap does not get execute permissions.
- The ELF metadata of executables and shared libraries also contains the
GNU_STACKprogram header, denoting the permissions of the stack memory pages. By default the execute flag is not set, and the stack will only have read/write permissions. There are three exceptions:
-z execstacklinker flag was passed explicitly when linking an executable or shared library.
- At least one of the object files was produced by the assembler. In that case it is unknown whether the stack may be mapped without execute permissions. An explicit annotation is needed for that:
- You are using nested functions, a GNU C extension (not available in GNU C++).
Executable-space protection is important, and fortunately many things are already taken care of. What remains to be done is the correct
GNU_STACK setting, so we will focus on this.
Linux uses the lowest common denominator for
GNU_STACK. That means even if only one of your shared library dependencies is flagged to require an executable stack, the whole program will run that way. This is even true for
dlopen()‘ed libraries – the kernel will change the permission at runtime! From a security perspective this is a bit of a disaster: You have to be really careful not to throw a library into the mix with the wrong setting. When you build everything yourself, and no assembler sources are involved, you should be fine. Shared libraries that may be loaded from the distribution should always have this flag set correctly. That said, I recommend to have two tests:
- For all executables and shared libraries, check in the output of
readelf -lthat the
GNU_STACKentry has only the flags
RWset. It should look like this:
ZSH12345$ readelf -l libm.so.6...GNU_STACK 0x0000000000000000 0x0000000000000000 0x00000000000000000x0000000000000000 0x0000000000000000 RW 10...
It should not look like this (note:
E(Execute) is now present in flags):
ZSH12345$ readelf -l unprotected.so...GNU_STACK 0x0000000000000000 0x0000000000000000 0x00000000000000000x0000000000000000 0x0000000000000000 RWE 10...
- Have a test that starts your program, and loads all dynamically loadable libraries available. Then check in
PIDis replaced by the numeric process id of your running program) whether the
[stack]is mapped correctly. As mentioned above, this test is to rule out that some library changes the stack permissions at runtime. It should look like this:
ZSH1234$ cat /proc/11684/maps...7ffe3401a000-7ffe3403c000 rw-p 00000000 00:00 0 [stack]...
It should not look like this (note:
x(eXecute) is now present):
ZSH1234$ cat /proc/11687/maps...7ffea187d000-7ffea189d000 rwxp 00000000 00:00 0 [stack]...
Note: A loophole remains for JIT compilers needing to generate code at runtime. They usually take care to map the memory write-only/non-executable, and then switch to executable-only/write-protected. But when the JIT compiler can be tricked into generating code the attacker can use later, this will not help.
Address Space Layout Randomization
Executable-space protection eliminates a large attack vector by preventing executable code to be added to the program. But what if some code useful to an attacker is already present? For example, all programs linked with
libc can potentially call
system() to start a shell. The attacker could just overwrite the return address on the stack to point to the address of
system() and prepare the stack to contain a pointer to
/bin/sh or even just
sh. While the stack protector can effectively defend against this, it might not be present in the exploited function, or the attacker might have extracted the special value by exploiting a bug in the program and succeeded with an undetected memory overwrite. But we can make life much harder for the attacker by randomizing the location of
libc in memory, and therefore the address of
system(). That’s the essence of address space layout randomization: Randomize as many memory mappings as possible to make the system unpredictable. On a modern Linux system these are affected:
- Main executable’s code
- Shared library code
- Heap and stack(s)
- mmap base
- vDSO page
- Parts of the kernel itself
ASLR is implemented by the Linux kernel. It works best on 64-bit systems, since the address space available for randomization is that much larger, dramatically lowering the chances of a guessing attack. ASLR can be disabled by
# echo 0 > /proc/sys/kernel/randomize_va_space
This may be useful for debugging, but of course it should never stay permanently disabled.
gdb itself will also disable ASLR by default so that addresses remain stable across subsequent debug sessions.
Heap and stack randomization happen automatically, there is nothing to be done on modern systems. To be able to load the main executable and shared libraries at random addresses, their code must be position-independent. This is achieved as follows:
- Compile with
-fpicuses a limited GOT (Global Offset Table) size on some architectures. The linker will tell you when it is exceeded, then you need to switch to
-fPIC, which may incur a little overhead. On x86 there is no difference between the options.
- Link with
-sharedand specify the same option you used during compilation (
Linking will fail if one of the object files was not built with
-fPIC. Shared libraries must be position-independent, otherwise there would be collisions between libraries from different vendors mapping to the same memory location.
- Compile with
-fPIE(same distinction as for shared libraries, see above).
- Link with
-pieand specify the same option you used during compilation (
Unfortunately, in many popular build systems it is cumbersome to set up differing compilation options for source code that is linked into an executable or a shared library. In that case you may always use
-fPIC during compilation, even when the code is linked into an executable later. The only difference is that symbols will be overridable, but that will only matter when you are using
LD_PRELOAD, since that is the only code loaded before the main executable that could override anything. This approach is also recommended by at least one GCC developer.
Here is a full example of a program that links in a shared library, and is itself position-independent:
# Build and link shared library
$ g++ -c -fPIC shared.cpp -o shared.o
$ g++ -shared -fPIC shared.o -o libshared.so
# Build and link main executable
$ g++ -c -fPIE main.cpp -o main.o
$ g++ -pie -fPIE main.o -L. -lshared -o main
main is an executable, but since it is fully relocatable, it will appear to
file like a shared library:
$ file main
main: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 3.0.0, BuildID[sha1]=9fabc139c49f308b87809e08139676661fd25290, not stripped
For comparison, without the
$ g++ main.o -L. -lshared -o main_no_pie
$ file main_no_pie
main_no_pie: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 3.0.0, BuildID[sha1]=8d77924e8fe74a0609d070d0bbd731f779d75806, not stripped
This is also how your test for this option should look like: Check all executables with
file and look for the “shared object” string.
There have been notable attacks on ASLR in research papers:
That said, these attacks require markedly more effort than exploiting an unprotected binary. The overhead of ASLR is very low, so there is no reason to ship binaries without this protection.
With gcc7, a
-static-pie linker option was added. Such executables do not depend on other shared libraries, and can be loaded at an arbitrary address.