_FORTIFY_SOURCE: explained
C programmers absolutely know that dealing with functions that copy memory from one place to another can be quite painful: if proper bound checks are not performed, an attacker may be able to trigger a buffer overflow on a vulnerable program, thus hijacking its normal execution flow.
Luckily, modern compilers make available the _FORTIFY_SOURCE
macro, to further protect binaries against the most common buffer overflow attacks. Extra checks on memory are performed, and in case of imminent buffer overflow, the fortified program throws an exception that immediately terminates itself to avoid further damages.
From the attacker’s point of view, one can check whether a program has been fortified during the compilation by using the checksec
command on GDB with peda/gef.
Fortifying a program means that we instruct the compiler to:
-
Print warnings at compile time, if the destination buffer of one the supported functions (see below) is smaller than the source. This can be done only if the size of source and destination are known at compile time, otherwise no warning is raised.
-
Replace the calls to supported functions with calls their safer counterparts (easily recognisable since they are all named
__*_chk
). These alternative functions are semantically equivalent to the original ones, but they also take the size of the destination buffer as parameter, in order to abort the program at runtime if an overflow occurs.Note that, since executing bound checks sligthly impacts on the performance, the substitution is takes place only when needed: in case a call to a function is recognised as safe (source and destination size are known and correct), the call site remains untouched.
This macro is meant to protect the program only in presence of functions for the manipulation of strings and memory, such as:
memcpy, memset, stpcpy, strcpy, strncpy, strcat, strncat, sprintf, snprintf, vsprintf, vsnprintf, gets, ...
The complete list of supported functions can be deduced by looking at the contents of the debug/
folder in the glibc source tree.
To take advantage of this macro, we must follow the steps below:
- Ensure that all the needed headers have been specified: fortify doesn’t work in presence of implicit declarations.
- Instruct the compiler to use an optimisation level equal or higher than 1 (
-O1
). - Pass the flag
-D_FORTIFY_SOURCE=1
, to perform just basic checks that shouldn’t change the behaviour of programs, or-D_FORTIFY_SOURCE=2
, to add some more checking, but with the possibility that some conforming programs might fail. - Profit!
Proof
Here we can show the difference between a fortified program and an unprotected one. We can use the unsecure program below as example:
#include <stdio.h>
#include <string.h>
int main(int argc, char** argv) {
char buf[5];
strcpy(buf, argv[1]);
puts(buf);
}
It simply takes the 1st argument, copies it in a buffer of 5 bytes, then prints its contents. A buffer overflow is triggered as soon as we supply an argument which is longer than the buffer.
We can now compile the program twice: one with the D_FORTIFY_SOURCE
flag, the other without it. To obtain comparable results, we can use -O1
as optimisation level:
$ gcc example.c -o example -O1
$ gcc example.c -o example_fortified -D_FORTIFY_SOURCE=1 -O1
When compiling the program without the fortify macro, the obtained disassembly of the main function is:
0x0000000000001145 <+0>: push rbx
0x0000000000001146 <+1>: sub rsp,0x10
0x000000000000114a <+5>: mov rsi,QWORD PTR [rsi+0x8]
0x000000000000114e <+9>: lea rbx,[rsp+0xb]
0x0000000000001153 <+14>: mov rdi,rbx
0x0000000000001156 <+17>: call 0x1030 <strcpy@plt>
0x000000000000115b <+22>: mov rdi,rbx
0x000000000000115e <+25>: call 0x1040 <puts@plt>
0x0000000000001163 <+30>: mov eax,0x0
0x0000000000001168 <+35>: add rsp,0x10
0x000000000000116c <+39>: pop rbx
0x000000000000116d <+40>: ret
On the other hand, when compiling the fortified program, no warning is shown by the compiler, since the size of the source buffer (argv[1]
) cannot be known at compile time. However, looking at disassembly we have:
0x0000000000001145 <+0>: push rbx
0x0000000000001146 <+1>: sub rsp,0x10
0x000000000000114a <+5>: mov rsi,QWORD PTR [rsi+0x8]
0x000000000000114e <+9>: lea rbx,[rsp+0xb]
0x0000000000001153 <+14>: mov edx,0x5 # length of buf
0x0000000000001158 <+19>: mov rdi,rbx
0x000000000000115b <+22>: call 0x1040 <__strcpy_chk@plt>
0x0000000000001160 <+27>: mov rdi,rbx
0x0000000000001163 <+30>: call 0x1030 <puts@plt>
0x0000000000001168 <+35>: mov eax,0x0
0x000000000000116d <+40>: add rsp,0x10
0x0000000000001171 <+44>: pop rbx
0x0000000000001172 <+45>: ret
The call to strcpy
has been replaced with __strcpy_chk
, which checks if the destination can hold all the source data even at runtime. Looking at the source code, the inner workings of this new function seem pretty straightforward:
/* Copy SRC to DEST with checking of destination buffer overflow. */
char * __strcpy_chk (char *dest, const char *src, size_t destlen) {
size_t len = strlen (src);
if (len >= destlen)
__chk_fail ();
return memcpy (dest, src, len + 1);
}
Let’s test the behaviour at runtime:
$ ./example AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Segmentation fault
$ ./example_fortified AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
*** buffer overflow detected ***: ./example_fortified terminated
Aborted
Limitations
Even if the macro is highly effective in cases like the one above, not all the possible scenarios in which a buffer overflow occurs can be handled.
Let’s suppose to have the following program:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int main(int argc, char** argv) {
char buf[ strtol(argv[1], NULL, 10) ];
strcpy(buf, argv[2]);
puts(buf);
}
This program takes two arguments: the first is used as length for buf
, the second is just the string we want to copy in it. Hence, this time, the buffer size is chosen at runtime.
Compiling with gcc limited.c -o limited -O1 -D_FORTIFY_SOURCE=2
, no warning is showed, and disassemblying the binary we obtain:
0x0000000000001155 <+0>: push rbp
0x0000000000001156 <+1>: mov rbp,rsp
0x0000000000001159 <+4>: push rbx
0x000000000000115a <+5>: sub rsp,0x8
0x000000000000115e <+9>: mov rbx,rsi
0x0000000000001161 <+12>: mov rdi,QWORD PTR [rsi+0x8]
0x0000000000001165 <+16>: mov edx,0xa
0x000000000000116a <+21>: mov esi,0x0
0x000000000000116f <+26>: call 0x1050 <strtol@plt>
0x0000000000001174 <+31>: add rax,0xf
0x0000000000001178 <+35>: and rax,0xfffffffffffffff0
0x000000000000117c <+39>: sub rsp,rax
0x000000000000117f <+42>: mov rsi,QWORD PTR [rbx+0x10]
0x0000000000001183 <+46>: mov rdi,rsp
0x0000000000001186 <+49>: call 0x1030 <strcpy@plt>
0x000000000000118b <+54>: mov rdi,rsp
0x000000000000118e <+57>: call 0x1040 <puts@plt>
0x0000000000001193 <+62>: mov eax,0x0
0x0000000000001198 <+67>: mov rbx,QWORD PTR [rbp-0x8]
0x000000000000119c <+71>: leave
0x000000000000119d <+72>: ret
As you may notice, this time the call for strcpy
hasn’t been replaced, leaving the binary vulnerable:
$ ./limited 3 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Segmentation fault
Hence, the macro does not help in case the size of the destination buffer cannot be known at compile time!
Conclusion
When compiling a program, using the _FORTIFY_SOURCE
flag is a good programming practice to be aware of possible vulnerabilities reading the compiler warnings, and to automatically replace vulnerable functions with safer ones. However, this only adds an extra level of security, leaving unprotected functions that take parameters not known at compile time.