Protección de ejecutables: FORTIFY_SOURCE

La macro FORTIFY_SOURCE es una característica de GCC y GLIBC que intenta detectar algunas clases de buffer overflows. Aunque esta activada por defecto si compilamos con GCC, esta característica esta estrechamente relaciona con otros parámetros, principalmente el optimizador, que hará variar su funcionamiento.

El objetivo de FORTIFY_SOURCE es calcular el número de bytes que se copian de un origen a un destino para verificar que no se excede la capacidad del buffer de origen. Aunque no puede detectar todos los buffer overflows, si añade un nivel más de protección para algunas funciones especialmente sensibles a este tipo de problema. Cuando esta macro detecta que un buffer a sido sobrepasado interrumpe la ejecución del programa, muestra un error y aborta la ejecución:

*** buffer overflow detected ***: terminated
Abortado (`core' generado)

Vamos a ver como funciona la comprobación de buffer overflow.

Si compilamos un código como este:

#include <stdio.h> 
#include <stdlib.h>  
#include <string.h> 
 
#define CADENA "123456" 
 
int main(int argc, char **argv) { 
 
    char mbuffer[5]; 
     
    strcpy (mbuffer, CADENA); 
 
    return EXIT_SUCCESS;  
} 

Con las opciones por defecto o con -Wall el resultado es el mismo:

$ gcc esdesbuff.c -o esdesbuff
esdesbuff.c: In function ‘main’:
esdesbuff.c:11:2: warning: ‘__builtin_memcpy’ writing 7 bytes into a region of size 5 overflows the destination [-Wstringop-overflow=]
11 | strcpy (mbuffer, CADENA);
| ^~~~~~~~~~~~~~~~~~~~~~~~

Esta comprobación en tiempo de compilación se produce porque la opción -Wstringop-overflow=2 esta habilitada por defecto. Pero esta es una comprobación básica y un simple puntero puede hacer que no detecte el buffer overflow:

#include <stdio.h> 
#include <stdlib.h>  
#include <string.h> 
 
#define CADENA "123456" 
 
int main(int argc, char **argv) { 
 
    char mbuffer[5]; 
    char *cpoint; 
     
    cpoint = CADENA; 
     
    strcpy (mbuffer, cpoint); 
 
    return EXIT_SUCCESS;  
} 

$ gcc -Wall esdesbuffpun.c -o esdesbuffpun

En los dos casos aunque por defecto la macro FORTIFY_SOURCE está habilitada con opción 2 (-D_FORTIFY_SOURCE=2) no se esta usando. La razón es que no se esta usando el optimizador.

Para poder hacer uso de la protección de FORTIFY_SOURCE es necesario habilitar las opciones de optimización en la construcción de los ejecutables

Lanzar gcc por defecto sería equivalente en este caso a hacerlo con estos parámetros:

$ gcc -O0 -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=2 -Wstringop-overflow=2 -Warray-bounds=2 esdesbuffpun.c -o esdesbuffpun

Por defecto gcc utiliza el nivel de optimización -O0, es decir ninguna optimización en el código. Para que FORTIFY_SOURCE se active necesita que se active la optimización de código. Si lanzamos:

$ gcc -Wall -O1 esdesbuffpun.c -o esdesbuffpun
In file included from /usr/include/string.h:495,
from esdesbuffpun.c:3:
In function ‘strcpy’,
inlined from ‘main’ at esdesbuffpun.c:14:2:
/usr/include/x86_64-linux-gnu/bits/string_fortified.h:90:10: warning: ‘__builtin___strcpy_chk’ writing 7 bytes into a region of size 5 overflows the destination [-Wstringop-overflow=]
90 | return __builtin___strcpy_chk (__dest, __src, __bos (__dest));
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Vemos que detecta el error, indicando que se va a escribir 7 bytes en una zona habilitada para 5. El resultado no puede ser otro que un overflow de la variable de origen.

Pero este tipo de comprobación en tiempo de compilación solo va a detectar variables estáticas asignadas en tiempo de compilación, que producirán un desbordamiento y un fallo en la aplicación, muy probablemente una violación de segmento en algún lugar cercano. Son en cambio las asignaciones en tiempo de ejecución las más fácilmente explotables en caso de un buffer overflow, y es donde FORTIFY_SOURCE tiene, a mi parecer, su máxima utilidad.

Vamos a utilizar otra aplicación sencilla que utiliza el primer parámetro de la línea de argumentos para asignar dos variables estáticas en tiempo de ejecución, una directa mbuffer[5] y otra dentro de una estructura var.t.buf[4]:

/*  
 * desfuff.c 
 * 
 * (c) Author: David Quiroga 
 * e-mail: david [at] clibre [dot] io 
 *  
 **************************************************************** 
 * Descripción: 
 * 
 * Comprobar el uso de FORTIFY_SOURCE en un buffer overflow  
 * 
 * Compiar con con diferentes opciones:  
 * -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0 -O2 
 * -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=1 -O2 
 * -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=2 -O2 
 *  
 * SPDX-License-Identifier: GPL-3.0 
 */ 
 
#include <stdio.h> 
#include <stdlib.h>  
#include <string.h> 
#include <locale.h> 
 
#define VERDE "\033[1;32m" 
#define ROJO "\033[0;31m" 
#define NEUTRO "\033[0m" 
 
int main(int argc, char **argv) { 
 
    struct S {  
        struct T {  
            char buf[4];  
            int x;  
        } t;  
        char buf[20];  
    } var; 
 
    char mbuffer[5] = ""; 
     
    setlocale(LC_ALL, ""); 
 
    if (argc != 2) { 
        printf ("uso <%s> parametro\n", argv[0]); 
        exit (EXIT_FAILURE); 
    } 
 
    printf ("Antes...\n"); 
    printf ("\tmbuffer en %p (%zd bytes) contiene: [%s]\n", 
                                mbuffer,sizeof(mbuffer),mbuffer); 
    printf ("\tvar.t.buf en %p (%zd bytes) contiene: [%s]\n", 
                                var.t.buf,sizeof(var.t.buf),var.t.buf);  
    printf ("Copia...\n");  
    printf ("\targv[1] = %s (%zd bytes)\n", argv[1], strlen(argv[1])+1);  
    printf ("\tCopiamos argv[1] (%zd bytes) en mbuffer (%zd bytes)\n",  
                                strlen(argv[1])+1, sizeof(mbuffer));  
    strcpy(mbuffer, argv[1]); 
    printf("\t\t%s......copiado%s\n",  
            (strlen(argv[1])+1)<=sizeof(mbuffer)?VERDE:ROJO, NEUTRO);  
    printf ("\tCopiamos argv[1] (%zd bytes) en var.t.buf (%zd bytes)\n",  
                                strlen(argv[1])+1, sizeof(var.t.buf));  
    strcpy(var.t.buf, argv[1]); 
    printf("\t\t%s......copiado%s\n",  
                    (strlen(argv[1])+1)<=sizeof(var.t.buf)?VERDE:ROJO, NEUTRO); 
 
    printf ("Después...\n");  
    printf ("\tBuffer en %p (%zd bytes) contiene: [%s]\n", 
                     mbuffer,sizeof(mbuffer),mbuffer); 
     printf ("\tvar.t.buf en %p (%zd bytes) contiene: [%s]\n", 
                            var.t.buf,sizeof(var.t.buf),var.t.buf);  
                                 
    exit (EXIT_SUCCESS);  
} 

Vamos primero a construir el ejecutable con tres opciones de FORTIFY_SOURCE, 0 (que al igual que al construir sin añadir la opción de optimización deja a FORTIFY_SOURCE sin efecto), 1 y 2:

$ gcc -Wall -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0 -O2 desbuff.c -o desbuffF0
$ gcc -Wall -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=1 -O2 desbuff.c -o desbuffF1
$ gcc -Wall -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=2 -O2 desbuff.c -o desbuffF2

No tenemos ninguna alerta en tiempo de compilación, ya que el código es correcto (a nivel de compilación). Si ejecutamos el programa sin protección FORTIFY_SOURCE tenemos:

$ ./desbuffF0 123
Antes...
mbuffer en 0x7fff3c13ec33 (5 bytes) contiene: []
var.t.buf en 0x7fff3c13ec10 (4 bytes) contiene: [@�%��U]
Copia...
argv[1] = 123 (4 bytes)
Copiamos argv[1] (4 bytes) en mbuffer (5 bytes)
......copiado
Copiamos argv[1] (4 bytes) en var.t.buf (4 bytes)
......copiado
Después...
Buffer en 0x7fff3c13ec33 (5 bytes) contiene: [123]
var.t.buf en 0x7fff3c13ec10 (4 bytes) contiene: [123]

Tenemos una copia correcta del parámetro 123 en las variables mbuffer[5] y var.t.buf[4] ya que en ambos casos el tamaño es suficiente para el parámetro pasado (4 bytes teniendo en cuenta siempre el carácter de fin de cadena).

Si le pasamos un parámetro mayor tenemos:

$ ./desbuffF0 1234
Antes...
mbuffer en 0x7fff3c9b9ca3 (5 bytes) contiene: []
var.t.buf en 0x7fff3c9b9c80 (4 bytes) contiene: [@��$�U]
Copia...
argv[1] = 1234 (5 bytes)
Copiamos argv[1] (5 bytes) en mbuffer (5 bytes)
......copiado
Copiamos argv[1] (5 bytes) en var.t.buf (4 bytes)
......copiado
Después...
Buffer en 0x7fff3c9b9ca3 (5 bytes) contiene: [1234]
var.t.buf en 0x7fff3c9b9c80 (4 bytes) contiene: [1234]

Aunque se ha copiado los datos a las dos variables, clamente se indica que en la segunda variable se ha copiado 5 bytes en una variable que solo admite 4 (se muestra en rojo). Tenemos un desbordamiento de buffer que como el programa finaliza no reporta ningún problema aparente. Se trata de un error grave.

Si añadimos un dígito más al parametro de entrada, desbordamos el buffer tambien de mbuffer[5] con el mismo resultado de aparente normalidad:

$ ./desbuffF0 12345
Antes...
mbuffer en 0x7fff2856bb63 (5 bytes) contiene: []
var.t.buf en 0x7fff2856bb40 (4 bytes) contiene: [@@��wU]
Copia...
argv[1] = 12345 (6 bytes)
Copiamos argv[1] (6 bytes) en mbuffer (5 bytes)
......copiado
Copiamos argv[1] (6 bytes) en var.t.buf (4 bytes)
......copiado
Después...
Buffer en 0x7fff2856bb63 (5 bytes) contiene: [12345]
var.t.buf en 0x7fff2856bb40 (4 bytes) contiene: [12345]

Esta vez dos desbordamientos al precio de uno. Si ejecutamos la aplicación compilada con la opción -D_FORTIFY_SOURCE=1 y el párametro 1234, tenemos desbordada la variable dentro de la estructura pero FORTIFY_SOURCE no detecta nada:

$ ./desbuffF1 1234
Antes...
mbuffer en 0x7ffca61288a3 (5 bytes) contiene: []
var.t.buf en 0x7ffca6128880 (4 bytes) contiene: [@pE!�U]
Copia...
argv[1] = 1234 (5 bytes)
Copiamos argv[1] (5 bytes) en mbuffer (5 bytes)
......copiado
Copiamos argv[1] (5 bytes) en var.t.buf (4 bytes)
......copiado
Después...
Buffer en 0x7ffca61288a3 (5 bytes) contiene: [1234]
var.t.buf en 0x7ffca6128880 (4 bytes) contiene: [1234]

En cambio si desbordamos la variable mbuffer[5] tendremos la intervención de FORTIFY_SOURCE :

$ ./desbuffF1 12345
Antes...
mbuffer en 0x7fff71339fe3 (5 bytes) contiene: []
var.t.buf en 0x7fff71339fc0 (4 bytes) contiene: [@�헧U]
Copia...
argv[1] = 12345 (6 bytes)
Copiamos argv[1] (6 bytes) en mbuffer (5 bytes)
*** buffer overflow detected ***: terminated
Abortado (`core' generado)

Si queremos que FORTIFY_SOURCE sea capaz de detectar el desbordamiento de nuestra variable var.t.buf[4]dentro de la estructura, tenemos que utilizar -D_FORTIFY_SOURCE=2 :

$ ./desbuffF2 1234
Antes...
mbuffer en 0x7ffc1b127103 (5 bytes) contiene: []
var.t.buf en 0x7ffc1b1270e0 (4 bytes) contiene: [@`�VU]
Copia...
argv[1] = 1234 (5 bytes)
Copiamos argv[1] (5 bytes) en mbuffer (5 bytes)
......copiado
Copiamos argv[1] (5 bytes) en var.t.buf (4 bytes)
*** buffer overflow detected ***: terminated
Abortado (`core' generado)

Como funciona FORTIFY_SOURCE

Como vimos también que ocurria en el caso del stack protector, FORTIFY_SOURCE nos añade una función de comprobación, en este caso para strcpy, que calcula el número de bytes que se copian de un origen a un destino para verificar que no se excede la capacidad del buffer de origen. Podemos ver esta función añadida a nuestro ejecutable con readelf:

readelf -s desbuffF2 |grep '__strcpy_chk'
6: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __strcpy_chk@GLIBC_2.3.4 (3)
61: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __strcpy_chk@@GLIBC_2.3.4

En el código de main tendremos la llamada a la función de comprobación __strcpy_chk@plt:

    1213:    e8 c8 fe ff ff           callq  10e0 <__printf_chk@plt>
1218: 48 8b 6b 08 mov 0x8(%rbx),%rbp
121c: ba 05 00 00 00 mov $0x5,%edx
1221: 4c 89 ef mov %r13,%rdi
1224: 48 89 ee mov %rbp,%rsi
1227: e8 94 fe ff ff callq 10c0 <__strcpy_chk@plt>
122c: 48 89 ef mov %rbp,%rdi
122f: 48 8d 2d ce 0d 00 00 lea 0xdce(%rip),%rbp # 2004 <_IO_stdin_used+0x4>
1236: e8 75 fe ff ff callq 10b0 <strlen@plt>
123b: 4c 89 f2 mov %r14,%rdx
123e: bf 01 00 00 00 mov $0x1,%edi
1243: 48 8d 0d 0b 0e 00 00 lea 0xe0b(%rip),%rcx # 2055 <_IO_stdin_used+0x55>
124a: 48 83 c0 01 add $0x1,%rax
124e: 48 8d 35 05 0e 00 00 lea 0xe05(%rip),%rsi # 205a <_IO_stdin_used+0x5a>
1255: 48 83 f8 06 cmp $0x6,%rax
1259: 48 0f 42 d5 cmovb %rbp,%rdx
125d: 31 c0 xor %eax,%eax
125f: e8 7c fe ff ff callq 10e0 <__printf_chk@plt>
1264: 48 8b 7b 08 mov 0x8(%rbx),%rdi
1268: e8 43 fe ff ff callq 10b0 <strlen@plt>
126d: b9 04 00 00 00 mov $0x4,%ecx
1272: bf 01 00 00 00 mov $0x1,%edi
1277: 48 8d 35 9a 0e 00 00 lea 0xe9a(%rip),%rsi # 2118 <_IO_stdin_used+0x118>
127e: 48 8d 50 01 lea 0x1(%rax),%rdx
1282: 31 c0 xor %eax,%eax
1284: e8 57 fe ff ff callq 10e0 <__printf_chk@plt>
1289: 4c 8b 7b 08 mov 0x8(%rbx),%r15
128d: ba 04 00 00 00 mov $0x4,%edx
1292: 4c 89 e7 mov %r12,%rdi
1295: 4c 89 fe mov %r15,%rsi
1298: e8 23 fe ff ff callq 10c0 <__strcpy_chk@plt>
129d: 4c 89 ff mov %r15,%rdi
12a0: e8 0b fe ff ff callq 10b0 <strlen@plt>
12a5: 4c 89 f2 mov %r14,%rdx
12a8: bf 01 00 00 00 mov $0x1,%edi

Podemos observar también la función __printf_chk@plt para comprobar de igual modo las llamadas a las funciones de la familia printf().

FORTIFY_SOURCE proporciona comprobaciones de desbordamiento de buffer para las siguientes funciones:

memcpy, mempcpy, memmove, memset, strcpy, stpcpy, strncpy, strcat, strncat, gets
la familia de funciones printf() (sprintf, vsprintf, snprintf, vsnprintf ..)

Es interesante destacar que es muy frecuente cuando se utiliza la opción -g para incluir información adicional de depuración en los ficheros objeto y los ejecutables, no incluir la opción de optimización de código. El motivo es que las opciones de optimización modifican el código y es posible que cuando estemos depurando el programa no sea fácil de seguir ya que pueden haberse eleminado variables temporales, el orden de las declaraciones se ha podido alterar, partes del codigo elinadas etc.

Para aplicaciones finales es necesario utilizar el optimizador. Cuando se compila con el optimizador habilitado GCC puede producir avisos adicionales que no aparecen cuando se compila sin el optimizador. Por ejemplo, como parte del proceso de optimización el compilador examina el uso de variables y sus valores iniciales. Es muy habitual utilizar -O2 e incluir la opción de depuración -g en las releases finales de muchas aplicaciones. Esto realiza la optimización del código, añade comprobaciones adicionales, habilita la protección FORTIFY_SOURCE y si el programa falla de forma inexperada siempre es preferible poder contar con alguna información de depuración.

 

Modificado por última vez enViernes, 14 Agosto 2020 20:12
(0 votos)
Etiquetado como :

Deja un comentario

Asegúrese de introducir toda la información requerida, indicada por un asterisco (*). No se permite código HTML.