Irene Díez – S3lab http://s3lab.deusto.es S3lab Security Blog Wed, 06 May 2020 12:51:35 +0000 es hourly 1 https://wordpress.org/?v=5.1.5 Optimizaciones fatales http://s3lab.deusto.es/optimizaciones-fatales/ Tue, 21 Nov 2017 10:55:32 +0000 http://s3lab.deusto.es/?p=9563 Los compiladores transforman el código que escribimos para hacerlo más eficiente, sin embargo, hay veces en las que estas optimizaciones hechas por el compilador pueden causar problemas de seguridad. Un claro ejemplo de esto es la eliminación de código muerto

The post Optimizaciones fatales appeared first on S3lab.

]]>
Los compiladores transforman el código que escribimos para hacerlo más eficiente, sin embargo, hay veces en las que estas optimizaciones hechas por el compilador pueden causar problemas de seguridad. Un claro ejemplo de esto es la eliminación de código muerto (dead code elimination), una optimización que tiene como objetivo eliminar trozos de código que no afectan a los resultados del programa, y por tanto son considerados irrelevantes. Sin embargo, estos trozos de código, que el compilador considera irrelevantes, pueden llegar a ocasionar grandes fallos de diseño en una aplicación si son eliminados. Concretamente, la eliminación de los almacenamientos muertos (dead stores) es un fallo de seguridad en muchas aplicaciones. Una sentencia se considera un dead store cuando en esa sentencia almacenamos un valor en que posteriormente no va a ser leído, un ejemplo trivial sería:

int a, b, c;
a = 10;
b = a + 1;
c = 0;
printf(“a + b = %d\n”, a + b);

En este trozo de código, claramente la sentencia c = 0 sobra, y la variable c en sí es irrelevante. Un buen compilador daría un warning de que c no se usa, e internamente no produciría el código máquina equivalente a c = 0. Por otro lado, el siguiente código tiene un dead store y sí es necesario, aunque el compilador piense igualmente que el dead store debería eliminarse:

char *password = malloc(tamaño_password);
// la aplicación lee la contraseña y hace alguna operación con ella...
memset(password, 0, tamaño_passsword);
free(password); 

En este segundo ejemplo, aunque el memset parece inútil, tiene unas consideraciones de seguridad importantes, ya que si no escribiéramos ceros en las direcciones de la heap en las que password está almacenada, en el caso de que se diera algún error de memoria explotable en la aplicación, se podría leakear la contraseña. Si el compilador no quitase el dead store y hubiese una vulnerabilidad, la contraseña estaría protegida.

Evitar que se eliminen los dead stores no es una tarea fácil, ya que las aplicaciones que se usan en producción se suelen compilar con las optimizaciones habilitadas, porque se necesita que el código generado sea el óptimo. Algunas optimizaciones pueden depender de que previamente se limpie el código muerto, o simplemente se quiere eliminar el código muerto siempre, salvo en contadas excepciones.

Este problema aún sigue estudiándose, y no existe una solución multiplataforma universal para evitar que los dead stores se eliminen en casos concretos. Esto ha llevado a que los desarrolladores usen funciones específicas de su sistema operativo diseñadas tieniendo en cuenta estos problemas u ofusquen las operaciones de dead store para que el compilador no las entienda y no pueda optimizarlas (eliminarlas).

The post Optimizaciones fatales appeared first on S3lab.

]]>
¿Cómo funciona un linker? (III) – Tipos de símbolos http://s3lab.deusto.es/como-funciona-linker-3/ Sat, 14 Oct 2017 10:00:37 +0000 http://s3lab.deusto.es/?p=9508 Tras aprender qué información contiene la tabla de símbolos en la entrega anterior, ahora podemos hablar más detenidamente de los atributos que puede tener un símbolo y qué papel tienen en la resolución de símbolos desde el punto de vista

The post ¿Cómo funciona un linker? (III) – Tipos de símbolos appeared first on S3lab.

]]>
Tras aprender qué información contiene la tabla de símbolos en la entrega anterior, ahora podemos hablar más detenidamente de los atributos que puede tener un símbolo y qué papel tienen en la resolución de símbolos desde el punto de vista del linker. Recordemos que los símbolos pueden ser globales (global), locales (local) o débiles (weak). Los globales son las funciones definidas sin static además de las variables inicializadas definidas sin static. Un símbolo local es aquel definido con un static, y finalmente los símbolos débiles son las variables globales no inicializadas.

Utilizando la información que nos dan las tablas de símbolos de los diferentes ficheros a convertir en un ejecutable, el linker trata de asociar cada referencia con una única definición de símbolo. Siguiendo el ejemplo que vimos en la entrega anterior, nuestro programa test2.c tenía una función ‘multiply’ que aparecía en la tabla de símbolos como símbolo global y no definido:

$ readelf -s test2.o
Symbol table '.symtab' contains 13 entries:
   Num:	Value  Size Type	Bind   Vis  	Ndx Name
	...
	12: 00000000 	0 NOTYPE  GLOBAL DEFAULT  UND multiply

Para definir este símbolo vamos a implementar la función multiply.

$ cat test2-multiply.c
int multiply(int x, int y)
{
	return x * y;
}

Si inspeccionamos la tabla de símbolos de test2-multiply.o veremos que tenemos el símbolo de la función que le falta a test2.o

$ readelf -s test2-multiply.o

Symbol table '.symtab' contains 9 entries:
   Num:	Value  Size Type	Bind   Vis  	Ndx Name
 	0: 00000000 	0 NOTYPE  LOCAL  DEFAULT  UND
 	1: 00000000 	0 FILE	LOCAL  DEFAULT  ABS test2-multiply.c
 	2: 00000000 	0 SECTION LOCAL  DEFAULT	1
 	3: 00000000 	0 SECTION LOCAL  DEFAULT	2
 	4: 00000000 	0 SECTION LOCAL  DEFAULT	3
 	5: 00000000 	0 SECTION LOCAL  DEFAULT	5
 	6: 00000000 	0 SECTION LOCAL  DEFAULT	6
 	7: 00000000 	0 SECTION LOCAL  DEFAULT	4
 	8: 00000000	12 FUNC	GLOBAL DEFAULT	1 multiply

Ahora ya podríamos generar un ejecutable en el que todos los símbolos estuvieran resueltos:

$ gcc -m32 test2.o test2-multiply.o -o program
$ readelf -s program
...
Symbol table '.symtab' contains 72 entries:
   Num:	Value  Size Type	Bind   Vis  	Ndx Name
	...
	37: 00000000 	0 FILE	LOCAL  DEFAULT  ABS test2.c
	38: 080483db	13 FUNC	LOCAL  DEFAULT   14 add
	39: 00000000 	0 FILE	LOCAL  DEFAULT  ABS test2-multiply.c
	...
	53: 0804847b	12 FUNC	GLOBAL DEFAULT   14 multiply
	...
	66: 080483f3   136 FUNC	GLOBAL DEFAULT   14 main
	67: 080483e8	11 FUNC	GLOBAL DEFAULT   14 subtract
	...

El linker ha utilizado la tabla de símbolos para dar sentido al símbolo global ‘multiply’ que antes no estaba definido. Sin embargo no siempre el proceso de resolución de símbolos es tan obvio, ya que por ejemplo puede haber varios símbolos globales con el mismo nombre. Para ello existen dos prioridades que pueden tener los símbolos, fuerte (strong) y débil (weak) que son dadas por el assembler.

Teniendo en cuenta esto, el linker tiene en cuenta que: es un error si hay dos símbolos fuertes iguales, tiene más prioridad un símbolo fuerte frente a los débiles, y entre varios símbolos débiles iguales no hay una norma sobre cual elegir. Por ejemplo, si cambiamos test2-multiply.c de esta forma:

$ cat test2-multiply-modified.c
int multiply(int x, int y)
{
	return x * y;
}

float subtract(float x, float y)
{
	return y - x;
}

Al tratar de generar el ejecutable final el linker se quejaría diciendo que hay múltiples definiciones de ‘subtract’, ya que el linker no puede elegir entre dos símbolos fuertes iguales. Una de los fallos que suele costar más encontrar es cuando tenemos un error en nuestro programa porque dos variables globales se están sobrescribiendo:

$ cat strongvsweak-1.c
#include 

int x;

int main()
{
    
	printf("%d\n", x);
}

$ cat strongvsweak-2.c
int x = 100;

En este caso nuestro programa imprimiría 100, ya que las variables globales inicializadas son fuertes y las no inicializadas débiles, y por tanto el linker da prioridad a int x = 100 en vez del valor no inicializado (que podría ser cualquier cosa). Podemos tener otro fallo sútil similar cuando hay varias definiciones débiles sobre las que el linker tiene que elegir. Para evitar esto, podemos hacer que el compilador nos avise como si fuera un error cuando se están sobrescribiendo definiciones utilizando la opción -fno-common de GCC.

The post ¿Cómo funciona un linker? (III) – Tipos de símbolos appeared first on S3lab.

]]>
¿Cómo funciona un linker? (II) – La tabla de símbolos http://s3lab.deusto.es/como-funciona-linker-2/ Fri, 15 Sep 2017 16:00:11 +0000 http://s3lab.deusto.es/?p=9338 En entregas anteriores hablábamos de que los linkers son los encargados de combinar diferentes ficheros de código objeto trasladable en un ejecutable. Para ello tenían dos tareas fundamentales, la resolución de símbolos y el traslado; en esta entrega vamos a

The post ¿Cómo funciona un linker? (II) – La tabla de símbolos appeared first on S3lab.

]]>
En entregas anteriores hablábamos de que los linkers son los encargados de combinar diferentes ficheros de código objeto trasladable en un ejecutable. Para ello tenían dos tareas fundamentales, la resolución de símbolos y el traslado; en esta entrega vamos a empezar a hablar de la resolución de símbolos, que recordemos que consiste en asociar cada referencia de un símbolo con su definición.

Para la resolución de símbolos al linker le interesa la sección symtab (symbol table), la tabla de símbolos. Esta tabla ha sido creada por el assembler, y contiene información sobre las referencias y definiciones de símbolos. Vamos a utilizar test2.c para ver los diferentes tipos de símbolos que se pueden definir y referenciar.

$ cat test2.c
static int add(int num1, int num2)
{
    return num1 + num2;
}

float subtract(float num1, float num2)
{
    return num1 - num2;
}

int multiply(int, int);

int main()
{
    int i = 1, j = 2;
    int k = add(i, j);
    float a = 2.0, b = 4.0;
    float c = substract(a, b);
    int x = 2, y = 2;
    int z = multiply(x, y);
}
$ gcc -c -m32 test2.c -o test2.o
$ readelf -S test2.o
Section Headers:
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            00000000 000000 000000 00      0   0  0
  [ 1] .text             PROGBITS        00000000 000034 0000a0 00  AX  0   0  1
  [ 2] .rel.text         REL             00000000 00028c 000020 08   I 11   1  4
  [ 3] .data             PROGBITS        00000000 0000d4 000000 00  WA  0   0  1
  [ 4] .bss              NOBITS          00000000 0000d4 000000 00  WA  0   0  1
  [ 5] .rodata           PROGBITS        00000000 0000d4 000008 00   A  0   0  4
  [ 6] .comment          PROGBITS        00000000 0000dc 000035 01  MS  0   0  1
  [ 7] .note.GNU-stack   PROGBITS        00000000 000111 000000 00      0   0  1
  [ 8] .eh_frame         PROGBITS        00000000 000114 000084 00   A  0   0  4
  [ 9] .rel.eh_frame     REL             00000000 0002ac 000018 08   I 11   8  4
  [10] .shstrtab         STRTAB          00000000 0002c4 00005f 00      0   0  1
  [11] .symtab           SYMTAB          00000000 000198 0000d0 10     12  10  4
  [12] .strtab           STRTAB          00000000 000268 000024 00      0   0  1

La sección 11 (.symtab) es la que nos interesa, vamos a ver su contenido.

$ readelf -s test2.o
Symbol table '.symtab' contains 13 entries:
   Num:    Value  Size Type    Bind   Vis      Ndx Name
     0: 00000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 00000000     0 FILE    LOCAL  DEFAULT  ABS test2.c
     2: 00000000     0 SECTION LOCAL  DEFAULT    1 
     3: 00000000     0 SECTION LOCAL  DEFAULT    3 
     4: 00000000     0 SECTION LOCAL  DEFAULT    4 
     5: 00000000    13 FUNC    LOCAL  DEFAULT    1 add
     6: 00000000     0 SECTION LOCAL  DEFAULT    5 
     7: 00000000     0 SECTION LOCAL  DEFAULT    7 
     8: 00000000     0 SECTION LOCAL  DEFAULT    8 
     9: 00000000     0 SECTION LOCAL  DEFAULT    6 
    10: 0000000d    11 FUNC    GLOBAL DEFAULT    1 subtract
    11: 00000018   136 FUNC    GLOBAL DEFAULT    1 main
    12: 00000000     0 NOTYPE  GLOBAL DEFAULT  UND multiply

La tabla de símbolos es una estructura que define el nombre, valor, tamaño, tipo, atributos de binding, visibilidad y sección a la que pertenece cada símbolo. Los símbolos con nombre tienen una entrada en ‘Name’, que representa un índice en la tabla de strings (sección 12, .strtab). readelf nos da el trabajo hecho y nos pone directamente el nombre del símbolo en vez de tener que buscarlo nosotros en la tabla de strings. De todas formas vamos a ver el contenido de la tabla de símbolos y la tabla de strings en hexadecimal.

$ readelf -x .symtab test2.o
Hex dump of section '.symtab':
  0x00000000 00000000 00000000 00000000 00000000 ................
  0x00000010 01000000 00000000 00000000 0400f1ff ................
  0x00000020 00000000 00000000 00000000 03000100 ................
  0x00000030 00000000 00000000 00000000 03000300 ................
  0x00000040 00000000 00000000 00000000 03000400 ................
->0x00000050 09000000 00000000 0d000000 02000100 ................
  0x00000060 00000000 00000000 00000000 03000500 ................
  0x00000070 00000000 00000000 00000000 03000700 ................
  0x00000080 00000000 00000000 00000000 03000800 ................
  0x00000090 00000000 00000000 00000000 03000600 ................
  0x000000a0 0d000000 0d000000 0b000000 12000100 ................
  0x000000b0 16000000 18000000 88000000 12000100 ................
  0x000000c0 1b000000 00000000 00000000 10000000 ................

Nos vamos a fijar en la entrada 5. Los primeros 8 bytes (0x09000000) son el índice de la tabla de strings que tenemos que mirar, en este caso el 9.

$ readelf -x .strtab test2.o
Hex dump of section '.strtab':
  0x00000000 00746573 74322e63 00616464 00737562 .test2.c.add.sub
  0x00000010 74726163 74006d61 696e006d 756c7469 tract.main.multi
  0x00000020 706c7900                            ply.

El campo ‘Type’ denota el tipo de símbolo, los valores posibles son NOTYPE: no especificado, OBJECT: variable, array etc. datos en general, FUNC: función o código ejecutable, FILE: nos dice el nombre del archivo del código fuente asociado con el código objeto en el que está esta tabla de símbolos, en nuestro caso test2.c, SECTION: denota que el símbolo está asociado con una sección (nos preocuparemos de esto cuando hablemos de la fase de relocation en siguientes entradas del blog), además existen varios valores reservados. En nuestro ejemplo ‘add’, ‘subtract’ y ‘main’ son definidos como funciones, como ‘multiply’ no está definido en este archivo, y por tanto el linker no sabe lo que es, tiene un valor de NOTYPE.

El siguiente campo interesante de la tabla de símbolos es ‘Ndx’, que nos dice a qué sección está asociado un símbolo. Por ejemplo, el símbolo 5 ‘add’ pertenece a la sección 1, que es .text. Si miramos nuestro código, ‘add’ es una función, y por tanto tiene sentido que pertenezca a la sección de código.

Si nos fijamos en el símbolo 12, ‘multiply’; la tabla nos dice que no está definido en ninguna sección (UND), ya que efectivamente no lo hemos definido en test2.c y será el linker el encargado de buscar una definición. Si tratáramos de compilar el programa en este estado el linker se quejaría diciéndonos que la referencia ‘multiply’ no está definida.

‘Bind’ se refiere al tipo de binding (atadura u adhesión) que tiene el símbolo, esto afecta a su visibilidad y qué pasará en la fase de relocation. Los símbolos pueden ser globales (GLOBAL), locales (LOCAL) o débiles (WEAK). En nuestro ejemplo hemos definido la función ‘add’ con un ‘static’ por delante, que en C se utiliza para que esa función solamente sea visible dentro del archivo en el que está definida (utilizaríamos los modificadores public y private en C++), por tanto, en la tabla de símbolos tendrá un binding LOCAL. Por otro lado, tanto ‘main’, ‘subtract’ y ‘multiply’ tienen binding GLOBAL, y son visibles por todos los archivos que linkemos de forma conjunta. Finalmente, un símbolo WEAK es equivalente a un GLOBAL, solo que tiene una prioridad menor que los GLOBAL. Estas prioridades serán necesarias para saber qué hacer cuando tengamos varios símbolos definidos con un mismo nombre.

En la siguiente entrada de esta serie hablaremos de cómo se resuelven los símbolos con la tabla de símbolos. Consultar “Executable and Linkable Format (ELF)” para mas detalles de la tabla de símbolos.

The post ¿Cómo funciona un linker? (II) – La tabla de símbolos appeared first on S3lab.

]]>
¿Cómo funciona un linker? (I) http://s3lab.deusto.es/como-funciona-linker-1/ Tue, 11 Jul 2017 19:36:37 +0000 http://s3lab.deusto.es/?p=9255 En el proceso de convertir el código de un lenguaje de alto nivel a un ejecutable que entienda nuestro sistema, los linkers tienen un rol que muchas veces pasa desapercibido, pero que es importante de conocer. Cuando queremos convertir un archivo

The post ¿Cómo funciona un linker? (I) appeared first on S3lab.

]]>
En el proceso de convertir el código de un lenguaje de alto nivel a un ejecutable que entienda nuestro sistema, los linkers tienen un rol que muchas veces pasa desapercibido, pero que es importante de conocer. Cuando queremos convertir un archivo .c en un ejecutable entran en juego varias herramientas: primero el preprocesador de C (llamado cpp) genera un archivo intermedio, donde entre otras cosas se expanden las directivas #define y se borran los comentarios. Después, como hemos visto en anteriores entradas del blog, este archivo es leído por el compilador de C (cc1), y produce un archivo .s, que es código en ensamblador. Seguidamente el assembler (as) lee este .s y genera un  .o, un “archivo de código objeto trasladable” (relocatable object file) que finalmente es procesado por el linker para crear un ejecutable que puede ser cargado en memoria.

En general el linker es el encargado de combinar diferentes archivos con código objeto en un único archivo, y para esto hay dos tareas fundamentales, la resolución de símbolos y el “traslado” (relocation). Por un lado, los archivos de código objeto referencian y usan símbolos, el objetivo de la resolución de símbolos es asociar cada referencia con una única definición del símbolo. Por otro lado, en la tarea de relocation, ya que los compiladores y assemblers generan código objeto en el que las secciones empiezan en la dirección 0, el linker traslada las secciones que empiezan en la dirección 0 asociando una dirección a cada definición de un símbolo y después haciendo referencia a esa dirección en cada referencia del símbolo.

Vamos a ver esto con un pequeño ejemplo:

$ cat test.c
#include
int main()
{
printf("hello world\n");
}

Compilamos para generar un relocatable object file

$ gcc -c -m32 test.c -o test.o
$ readelf -S test.o 
There are 13 section headers, starting at offset 0x11c:
Section Headers:
  [Nr] Name          	Type        	Addr 	Off	Size      ES Flg Lk Inf Al
  [ 0]               	NULL        	00000000 000000 000000 00      0   0  0
  [ 1] .text         	PROGBITS    	00000000 000034 000017 00  AX  0   0  1
  [ 2] .rel.text     	REL         	00000000 0003e8 000010 08      11   1  4
  [ 3] .data         	PROGBITS    	00000000 00004b 000000 00  WA  0   0  1
  [ 4] .bss          	NOBITS      	00000000 00004b 000000 00  WA  0   0  1
  [ 5] .rodata       	PROGBITS    	00000000 00004b 00000c 00   A  0   0  1
  [ 6] .comment      	PROGBITS    	00000000 000057 00002c 01  MS  0   0  1
  [ 7] .note.GNU-stack   PROGBITS    	00000000 000083 000000 00      0   0  1
  [ 8] .eh_frame     	PROGBITS    	00000000 000084 000038 00   A  0   0  4
  [ 9] .rel.eh_frame 	REL         	00000000 0003f8 000008 08      11   8  4
  [10] .shstrtab     	STRTAB      	00000000 0000bc 00005f 00      0   0  1
  [11] .symtab       	SYMTAB      	00000000 000324 0000b0 10      12   9  4
  [12] .strtab       	STRTAB      	00000000 0003d4 000012 00      0   0  1

Y podemos ver que todas las secciones empiezan en la dirección 0, así que vamos a seguir con la fase de linkado ( no hace falta que le pasemos al linker la biblioteca estándar de C porque se linka por defecto)

$ gcc -m32 test.o -o test
$ readelf -S test
There are 30 section headers, starting at offset 0x117c:

Section Headers:
  [Nr] Name          	Type        	Addr 	Off	Size   ES Flg Lk Inf Al
  [ 0]               	NULL        	00000000 000000 000000 00      0   0  0
  [ 1] .interp       	PROGBITS    	08048154 000154 000013 00   A  0   0  1
  [ 2] .note.ABI-tag 	NOTE        	08048168 000168 000020 00   A  0   0  4
  [ 3] .note.gnu.build-i NOTE        	08048188 000188 000024 00   A  0   0  4
  [ 4] .gnu.hash     	GNU_HASH    	080481ac 0001ac 000020 04   A  5   0  4
  [ 5] .dynsym       	DYNSYM      	080481cc 0001cc 000050 10   A  6   1  4
  [ 6] .dynstr       	STRTAB      	0804821c 00021c 00004a 00   A  0   0  1
  [ 7] .gnu.version  	VERSYM      	08048266 000266 00000a 02   A  5   0  2
  [ 8] .gnu.version_r	VERNEED     	08048270 000270 000020 00   A  6   1  4
  [ 9] .rel.dyn      	REL         	08048290 000290 000008 08   A  5   0  4
  [10] .rel.plt      	REL         	08048298 000298 000018 08   A  5  12  4
  [11] .init         	PROGBITS    	080482b0 0002b0 000023 00  AX  0   0  4
  [12] .plt          	PROGBITS    	080482e0 0002e0 000040 04  AX  0   0 16
  [13] .text         	PROGBITS    	08048320 000320 000192 00  AX  0   0 16
  [14] .fini         	PROGBITS    	080484b4 0004b4 000014 00  AX  0   0  4
  [15] .rodata       	PROGBITS    	080484c8 0004c8 000014 00   A  0   0  4
  [16] .eh_frame_hdr 	PROGBITS    	080484dc 0004dc 00002c 00   A  0   0  4
  [17] .eh_frame     	PROGBITS    	08048508 000508 0000b0 00   A  0   0  4
  [18] .init_array   	INIT_ARRAY  	08049f08 000f08 000004 00  WA  0   0  4
  [19] .fini_array   	FINI_ARRAY  	08049f0c 000f0c 000004 00  WA  0   0  4
  [20] .jcr          	PROGBITS    	08049f10 000f10 000004 00  WA  0   0  4
  [21] .dynamic      	DYNAMIC     	08049f14 000f14 0000e8 08  WA  6   0  4
  [22] .got          	PROGBITS    	08049ffc 000ffc 000004 04  WA  0   0  4
  [23] .got.plt      	PROGBITS    	0804a000 001000 000018 04  WA  0   0  4
  [24] .data         	PROGBITS    	0804a018 001018 000008 00  WA  0   0  4
  [25] .bss          	NOBITS      	0804a020 001020 000004 00  WA  0   0  1
  [26] .comment      	PROGBITS    	00000000 001020 000056 01  MS  0   0  1
  [27] .shstrtab     	STRTAB      	00000000 001076 000106 00      0   0  1
  [28] .symtab       	SYMTAB      	00000000 00162c 000430 10      29  45  4
  [29] .strtab       	STRTAB      	00000000 001a5c 00024f 00      0   0  1

Una vez el código objeto ha sido linkado, tenemos un ejecutable que puede ser copiado para su ejecución en memoria por el loader. Por otro lado el linker también se ha encargado de asignar una dirección al símbolo de printf antes no definido:

$ objdump -d test.o -j .text
test.o: 	file format elf32-i386
Disassembly of section .text:
00000000 :
   0:    55              		 push   %ebp
   1:    89 e5           		 mov	%esp,%ebp
   3:    83 e4 f0        		 and	$0xfffffff0,%esp
   6:    83 ec 10        		 sub	$0x10,%esp
   9:    c7 04 24 00 00 00 00     movl   $0x0,(%esp)
  10:    e8 fc ff ff ff  		 call   11 <main+0x11>
  15:    c9              		 leave  
  16:    c3              		 ret

con una dirección válida, que hace referencia al símbolo de printf.

$ gdb -batch -ex 'file test' -ex 'disassemble main'
Dump of assembler code for function main:
   0x0804841d <+0>:    push   %ebp
   0x0804841e <+1>:    mov    %esp,%ebp
   0x08048420 <+3>:    and    $0xfffffff0,%esp
   0x08048423 <+6>:    sub    $0x10,%esp
   0x08048426 <+9>:    movl   $0x80484d0,(%esp)
   0x0804842d <+16>:   call   0x80482f0 <puts@plt>
   0x08048432 <+21>:   leave  
   0x08048433 <+22>:   ret    
End of assembler dump.

En siguientes entregas hablaremos de cómo el linker organiza las secciones para resolver las dependencias y los tipos de símbolos que reconoce el linker para comprender mejor el ejemplo anterior.

The post ¿Cómo funciona un linker? (I) appeared first on S3lab.

]]>
Crea un binario ilegible con estas técnicas http://s3lab.deusto.es/binario-ilegible-tecnicas/ Fri, 23 Jun 2017 09:55:39 +0000 http://s3lab.deusto.es/?p=9190 Ya bien sea por fines maliciosos, como el caso de los autores de malware, por fines corporativos, o por otras razones, las técnicas de ofuscación son utilizadas para proteger un programa haciendo que una vez compilado, el análisis estático del

The post Crea un binario ilegible con estas técnicas appeared first on S3lab.

]]>
Ya bien sea por fines maliciosos, como el caso de los autores de malware, por fines corporativos, o por otras razones, las técnicas de ofuscación son utilizadas para proteger un programa haciendo que una vez compilado, el análisis estático del binario sea más costoso. La ofuscación por tanto, consiste en transformar un programa de forma que sea más difícil de entender, pero que al mismo tiempo mantenga la semántica original.

Estas técnicas de ofuscación pueden aplicarse a nivel de datos, de instrucciones o de flujo de control del programa, pasamos a explicar algunas de ellas. Estas son algunas de las técnicas de ofuscación de datos:

  • Cegar las constantes (constant blinding): en vez de tener un valor en claro, esta técnica busca sustituir los valores de las constantes, generalmente mediante una XOR con un valor aleatorio, por datos aparentemente sin sentido.
  • Cambio del encoding de las variables: de forma similar al constant blinding, cambiando el encoding de las variables se busca ocultar el valor original de la variable, por ejemplo un string, por su valor equivalente en otro encoding. Ejemplos típicos son la codificación en Base64 o los cifrados sencillos mediante XOR, ROT13 etc.
  • Agregación de datos: consiste en agrupar variables en estructuras más complejas, por ejemplo juntando enteros en un mismo struct.
  • Separación de datos: en contraposición a la agregación de datos, la separación de datos consiste en dividir los datos en unidades más pequeñas, por ejemplo transformando un int de 4 bytes en dos shorts de 2 bytes.

Para ofuscar el flujo de control del programa existen diversas técnicas:

  • Inserción de código muerto (dead code insertion): una de las fases de optimización que hacen los compiladores modernos por defecto es la eliminación de código que no afecta al programa  (opción -fdce en GCC); la inserción de código muerto es todo lo contrario, es decir, inserta código en el programa que es redundante. Los ejemplos más sencillos son la inserción de nops, guardar el valor de una variable, hacer operaciones matemáticas con ella para luego restaurar el valor original etc.
  • Uso de predicados opacos: un predicado es una expresión lógica que se evalúa a true o false, y que generalmente se usa para dirigir el flujo de un programa. Los predicados opacos tienen como objetivo hacer que esa decisión de si algo se evalúa a true o false no sea posible saberse en tiempo de compilación o de forma estática, aunque siempre vaya a tener el mismo resultado. De esta forma se generan más ramas en el control flow graph (CFG) y se molesta en la fase de reversing. Tratar de detectar predicados opacos y desofuscarlos es una línea de investigación abierta. Se pueden ver ejemplos en estas respuestas de Reverse Engineering Stack Exchange.
  • Aplanado del flujo de control (control flow flattening): esta técnica consiste en modificar y reordenar los bloques básicos de un programa  (basic block, BB) de forma que, en vez de tener en el CFG una estructura de decisión if-else típica en la que se sigue la ejecución a un BB o se salta a otro BB dependiendo del valor de los flags, se pasa a una estructura aplanada, en la que un BB llamado dispatcher decide a qué BB saltar en base al valor de una variable artificial. Cada BB de las ramas de decisión tiene el dispatcher como predecesor y sucesor, dificultando así averiguar la lógica del programa detrás del CFG aplanado.
  • Desenroscado de bucles (loop unrolling): en las fases de optimización de la compilación de un programa se utiliza el loop unrolling para evitar añadir instrucciones de salto condicional y variables adicionales para emular bucles. El loop unrolling, además de ser computacionalmente menos costoso (se activa con la opción -O1 en adelante en GCC) genera bloques de instrucciones muy parecidos entre ellos, lo que aumenta la confusión en el análisis estático del código.

Finalmente a nivel de instrucción se busca sustituir la instrucción original por otra serie de instrucciones equivalentes más complicadas, generalmente mediante el uso de operaciones matemáticas. Algunas de estas técnicas pueden aplicarse con Obfuscator-LLVM, una suit de compilación sobre LLVM que en vez de aplicar las optimizaciones típicas del middle-end de LLVM, permite ofuscar el programa. 

Por otro lado, las técnicas de ofuscación también pueden servir para dar seguridad. Por ejemplo, PointGuard es una antigua extensión de GCC para proteger punteros. PointGuard cifra los valores de los punteros cuando estos están en memoria aplicando una XOR con una clave generada aleatoriamente cuando el proceso del programa arranca. Cuando un puntero se va a dereferenciar se descifra el valor del puntero, de esta forma, si un atacante consigue sobrescribir el valor del puntero, cuando este se dereferencie y por PointGuard se descifre, el atacante estará accediendo a una dirección de memoria aleatoria, y muy posiblemente si el acceso no es válido hará que el programa falle, frustrando así la explotación del programa.

 

The post Crea un binario ilegible con estas técnicas appeared first on S3lab.

]]>
¿Cómo funciona un compilador? http://s3lab.deusto.es/como-funciona-compilador/ Sat, 13 May 2017 18:28:41 +0000 http://s3lab.deusto.es/?p=9069 En la serie de posts de Hardening de binarios hemos visto que muchas defensas vienen implantadas en los propios compiladores, pero ¿cómo se implementan? Tomando el caso de GCC, The GNU Compiler Collection, vamos a explicar la infraestructura general de

The post ¿Cómo funciona un compilador? appeared first on S3lab.

]]>
En la serie de posts de Hardening de binarios hemos visto que muchas defensas vienen implantadas en los propios compiladores, pero ¿cómo se implementan? Tomando el caso de GCC, The GNU Compiler Collection, vamos a explicar la infraestructura general de GCC y en grandes rasgos, cómo funciona un compilador.

Si hemos tenido una asignatura de Compiladores en nuestra vida, en muchos casos nos habrán simplificado el proceso de compilación diciéndonos que se divide en dos componentes, por un lado el front-end y por otro el back-end. El front-end es el encargado de parsear el código fuente y traducirlo a un lenguaje intermedio que el compilador entienda; finalmente en el back-end es donde se traduce ese lenguaje intermedio a ensamblador.

La realidad, en el caso de GCC, es algo más complicada, siendo la primera diferencia el número de componentes de los que se compone el compilador y la segunda que utiliza varios lenguajes intermedios. GCC se divide en tres componentes, el front-end, el middle-end y el back-end.

En el front-end se parsean los diferentes códigos fuente y se traducen a un lenguaje independiente de la máquina, GIMPLE. GIMPLE es un lenguaje basado en la representación de un código de tres direcciones (three-address code), donde todas las instrucciones tienen como máximo tres operandos. En esta fase es donde el compilador nos avisará si tenemos errores sintácticos en el programa a compilar. Cuando GCC compila C ó C++ la traducción pasa directamente de C/C++ a GIMPLE, si se trata de otro lenguaje, como FORTRAN, se traduce primero a GENERIC, otro lenguaje intermedio de GCC basado en árboles de sintaxis abstracta (ASTs de sus siglas inglesas), que a su vez es traducido a GIMPLE.

En el middle-end es donde se producen todas las optimizaciones, tanto intra-procedurales (propias de cada función) como inter-procedurales (que tienen en cuenta el estado de todas las funciones). En esta fase el compilador nos avisará si hemos cometido errores semánticos, si tenemos variables sin usar etc. En el middle-end se utilizan tres lenguajes diferentes, que son tres formas de GIMPLE: high-GIMPLE, low-GIMPLE y SSA-GIMPLE. En el high-GIMPLE los programas no tienen una representación de tres direcciones pura y en el low-GIMPLE sí. Finalmente el SSA-GIMPLE es un lenguaje intermedio también de tres direcciones, en el que se utiliza la forma de Static Single Assignment (SSA form) para hacer optimizaciones de forma más sencilla. La forma SSA tiene la particularidad de que las asignaciones “generan” una versión de la variable nueva, por ejemplo:

a = 10; a_1 = 10;
a = a + 1; a_2 = a_1 + 1;
b = a; b_1 = a_2;

Las funciones típicas de este componente son la propagación de constantes y la optimización de tail-calls, además UBSan, VTV y ASan son implementadas en este componente. Finalmente, en el back-end se traduce el SSA-GIMPLE a un lenguaje dependiente de la máquina, RTL (Register Transfer Language) que ya se va pareciendo más a el lenguaje ensamblador, pero con la particularidad de que es funcional y deriva de Lisp.

The post ¿Cómo funciona un compilador? appeared first on S3lab.

]]>