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++).
- The
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 -l
that theGNU_STACK
entry has only the flagsRW
set. It should look like this:
12345$ 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):
12345$ 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
/proc/PID/maps
(wherePID
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:
1234$ 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):
1234$ 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 root
:
1 |
# 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:
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:
1 2 3 4 5 6 |
double pi(); int main() { return (int)pi(); } |
1 2 3 4 |
double pi() { return 3.14f; } |
1 2 3 4 5 6 7 |
# 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:
1 2 |
$ 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 -pie
option:
1 2 3 |
$ 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.
One Reply to “Hardening C/C++ Programs Part II – Executable-Space Protection and ASLR”