Hardening C/C++ Programs Part II – Executable-Space Protection and ASLR

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.

Executable-space protection

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:
    • .init and .fini: Code that runs at initialization and teardown.
    • .plt and .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_STACK program 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:
    • The -z execstack linker 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: .section .note.GNU-stack,"",@progbits
    • 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:

  1. For all executables and shared libraries, check in the output of readelf -l that the GNU_STACK entry has only the flags RW set. It should look like this:

    It should not look like this (note: E (Execute) is now present in flags):
  2. Have a test that starts your program, and loads all dynamically loadable libraries available. Then check in /proc/PID/maps (where PID is 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:

    It should not look like this (note:x (eXecute) is now present):

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 root:

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:

Shared libraries
  • Compile with -fpic or -fPIC. -fpic uses 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 -shared and specify the same option you used during compilation (-fpic or -fPIC).
    Linking will fail if one of the object files was not built with -fpic/-fPIC. Shared libraries must be position-independent, otherwise there would be collisions between libraries from different vendors mapping to the same memory location.
Executables
  • Compile with -fpie or -fPIE (same distinction as for shared libraries, see above).
  • Link with -pie and specify the same option you used during compilation (-fpie or -fPIE)

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/ -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:

main is an executable, but since it is fully relocatable, it will appear to file like a shared library:

For comparison, without the -pie option:

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.

 

References

One Reply to “Hardening C/C++ Programs Part II – Executable-Space Protection and ASLR”

Leave a Reply