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

  1. Ensure that all the needed headers have been specified: fortify doesn’t work in presence of implicit declarations.
  2. Instruct the compiler to use an optimisation level equal or higher than 1 (-O1).
  3. 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.
  4. 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.