Statically Linked Executable Hardening with PIE
Statically Linked Executable Hardening with PIE
In a nutshell
Target triplets | static-pie | pie | Linux | glibc |
---|---|---|---|---|
aarch64-linux-gnu-gcc | OK | OK | Linux 3.7 | GLIBC_2.17 |
arm-linux-gnueabihf-gcc | rcrt1.o | OK | Linux 3.2 | GLIBC_2.4 |
mips64-linux-gnuabi64-gcc | rcrt1.o | OK | Linux 3.2 | GLIBC_2.2 |
mipsisa64r6-linux-gnuabi64-gcc | rcrt1.o | OK | Linux 4.5 | GLIBC_2.2 |
riscv64-linux-gnu-gcc | rcrt1.o | OK | Linux 4.15 | GLIBC_2.27 |
s390x-linux-gnu-gcc | rcrt1.o | OK | Linux 3.2 | GLIBC_2.2 |
x86_64-linux-gnu-gcc | OK | OK | Linux 3.2 | GLIBC_2.2.5 |
Hello World
1 | cat << EOF > HelloWorld.c |
Static PIE Generation
1 | $ export HARDENING_FLAGS=" -D_FORTIFY_SOURCE=2 -D_GLIBCXX_ASSERTIONS -Wall -Wextra -Wformat=2 -Wdate-time -Werror=format-security -Wstack-protector -fasynchronous-unwind-tables -fexceptions -fstack-protector-strong -fstack-clash-protection -pedantic -z defs -z noexecstack -z now -z relro -z text -fPIE" |
Notes on Build Hardening
Stack guards
To enable this stack protection, code is compiled with the option -fstack-protector. This looks for functions that have typical buffers, inserting the guard/canary on the stack when the function is entered, and then verifying the value hasn’t been overwritten before exit.
ASLR
When a buffer-overflow is exploited, a hacker will overwrite values pointing to specific locations in memory. That’s because locations, the layout of memory, are predictable. It’s a detail that programmers don’t know when they write the code, but something hackers can reverse-engineer when trying to figure how to exploit code.
Obviously a useful mitigation step would be to randomize the layout of memory, so nothing is in a predictable location. This is known as address space layout randomization or ASLR.
The word layout comes from the fact that the when a program runs, it’ll consist of several segments of memory. The basic list of segments are:
- the executable code
- static values (like strings)
- global variables
- libraries
- heap (growable)
- stack (growable)
- mmap()/VirtualAlloc() (random location)
The problem for executable code is that for ASLR to work, it must be made position independent. Historically, when code would call a function, it would jump to the fixed location in memory where that function was know to be located, thus it was dependent on the position in memory.
To fix this, code can be changed to jump to relative positions instead, where the code jumps at an offset from wherever it was jumping from.
To enable this on the compiler, the flag -fPIE (position independent executable) is used. Or, if building just a library and not a full executable program, the flag -fPIC (position independent code) is used.
Then, when linking a program composed of compiled files and libraries, the flag -pie or –static-pie is used. In other words, use -fPIE -pie when compiling executables, and -fPIC when compiling for libraries.
When compiled this way, exploits will no longer be able to jump directly into known locations for code.
RELRO
Modern systems have dynamic/shared libraries. Most of the code of a typical program consists of standard libraries. As mentioned above, the GNU standard library glibc is 8-megabytes in size. Linking that into every one of hundreds of programs means gigabytes of disk space may be needed to store all the executables. It’s better to have a single file on the disk, libglibc.so, that all programs can share it.
The problem is that every program will load libraries into random locations. Therefore, code cannot jump to functions in the library, either with a fixed or relative address. To solve this, what position independent code does is jump to an entry in a table, relative to its own position. That table will then have the fixed location of the real function that it’ll jump to. When a library is loaded, that table is filled in with the correct values.
The problem is that hacker exploits can also write to that table. Therefore, what you need to do is make that table read-only after it’s been filled in. That’s done with the “relro” flag, meaning “relocation read-only”. An additional flag, “now”, must be set to force this behavior at program startup, rather than waiting until later.
When passed to the linker, these flags would be “**-z relro -z now“. However, we usually call the linker directly from the compiler, and pass the flags through. This is done in gcc by doing “-Wl,-z,relro -Wl,-z,now**“.
Non-executable stack
Exploiting a stack buffer overflow has three steps:
- figure out where the stack is located (mitigated by ASLR)
- overwrite the stack frame control structure (mitigated by stack guards)
- execute code in the buffer
We can mitigate the third step by preventing code from executing from stack buffers. The stack contains data, not code, so this shouldn’t be a problem. In much the same way that we can mark memory regions as read-only, we can mark them no-execute. This should be the default, of course, but as the paper above points out, there are some cases where code is actually placed on the stack.
This open can be set with -Wl,-z,noexecstack, when compiling both the executable and the libraries. This is the default, so you shouldn’t need to do anything special. However, as the paper points out, there are things that get in the way of this if you aren’t careful. The setting is more what you’d call “guidelines” than actual “rules”. Despite setting this flag, building software may result in an executable stack.
FORTIFY_SOURCE
One reason for so many buffer-overflow bugs is that the standard functions that copy buffers have no ability to verify whether they’ve gone past the end of a buffer. A common recommendation for code is to replace those inherently unsafe functions with safer alternatives that include length checks. For example the notoriously unsafe function strcpy() can be replaced with strlcpy(), which adds a length check.
Instead of editing the code, the GNU compiler can do this automatically. This is done with the build option -O2 -D_FORTIFY_SOURCE=2.
This is only a partial solution. The compiler can’t always figure out the size of the buffer being copied into, and thus will leave the code untouched. Only when the compiler can figure things out does it make the change.
Format string bugs
Because this post is about build hardening, I want to mention format-string bugs. This is a common bug in old code that can be caught by adding warnings for it in the build options, namely: -Wformat=2 -Wformat-security -Werror=format-security
Warnings and static analysis
When building code, the compiler will generate warnings about confusion, possible bugs, or bad style. The compiler has a default set of warnings. Robust code is compiled with the -Wall, meaning “all” warnings, though it actually doesn’t enable all of them. Paranoid code uses the -Wextra warnings to include those not included with -Wall. There is also the -pedantic or -Wpedantic flag, which warns on C compatibility issues.
All of these warnings can be converted into errors, which prevents the software from building, using the -Werror flag. As shown above, this can also be used with individual error names to make only some warnings into errors.
Optimization level
Compilers can optimize code, looking for common patterns, to make it go faster. You can set your desired optimization level.
Some things, namely the FORTIFY_SOURCE feature, don’t work without optimization enabled. That’s why in the above example, -O2 is specified, to set optimization level 2.
Higher levels aren’t recommended. For one thing, this doesn’t make code faster on modern, complex processors. The advanced processors you have in your desktop/mobile-phone themselves do extensive optimization. What they want is small code size that fits within cache. The -O3 level make code bigger, which is good for older, simpler processors, but is bad for modern, advanced processors.
In addition, the aggressive settings of -O3 have lead to security problems over “undefined behavior”. Even -O2 is a little suspect, with some guides suggesting the proper optimization is -O1. However, some optimizations are actually more secure than no optimizations, so for the security paranoid, -O1 should be considered the minimum.
Summary
If you are building code using gcc on Linux, here are the options/flags you should use: -Wall -Wformat -Wformat-security -Werror=format-security -fstack-protector -pie -fPIE -D_FORTIFY_SOURCE=2 -O2 -z defs -z noexecstack -z now -z relro -z text
If you are more paranoid, these options would be: -O2 -D_FORTIFY_SOURCE=2 -Wall -Wextra -Wformat=2 -Wformat-security -Wstack-protector -Wdate-time -Werror -pedantic -fstack-protector-all -fstack-clash-protection -fPIE -pie -z defs -z noexecstack -z now -z relro -z text
Hardening Check
1 | hardening-check HelloWorld |