The post ¿Cómo funciona un linker? (III) – Tipos de símbolos appeared first on S3lab.
]]>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.
]]>The post ¿Cómo funciona un linker? (II) – La tabla de símbolos appeared first on S3lab.
]]>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.
]]>The post ¿Cómo funciona un linker? (I) appeared first on S3lab.
]]>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.
]]>The post Hardening de binarios (VI) – RELRO appeared first on S3lab.
]]>4005b9: e8 72 fe ff ff callq 400430 <printf@plt>
El programa está llamando a printf en 400430, si vemos que pasa en esa posición tenemos:
0000000000400430 <printf@plt>:
400430: ff 25 e2 0b 20 00 jmpq *0x200be2(%rip) #601018<_GLOBAL_OFFSET_TABLE_+0x18>
400436: 68 00 00 00 00 pushq $0x0
40043b: e9 e0 ff ff ff jmpq 400420 <_init+0x20>
que a su vez nos muestra que estamos saltando a otra dirección, 601018 dentro de la GOT.
La GOT, en grandes rasgos, sirve para especificar dónde se encuentran determinados símbolos y es rellenada dinámicamente por el linker cuando el programa se está ejecutando, por tanto tiene que ser escribible. Y que tenga permisos de escritura puede ser un problema de seguridad. Por ejemplo podríamos modificar la GOT para que en vez de llamar a printf el programa llame a otra función con objetivos maliciosos, y para evitar esto tenemos las opciones de -z relro, -z now en en linker (-Wl para pasar opciones al linker en el compilador) para marcar las RELocations, como Read Only RELRO. Por ejemplo, dado este ejemplo propuesto.
Vamos a hacer un puntero a cierta dirección pasada por parámetro (algo dentro de la GOT), y a escribir 0x41414141, en ella. Para saber dónde está la GOT podemos hacer un readelf -r para ver las relocations del programa:
Que entre otras cosas nos dice que la dirección de printf en la GOT está en 0x…0601018, así que vamos a tratar de escribir ahí.
En el primer rectángulo examinamos los contenidos de 0x0601018 antes de escribir, y en el segundo podemos ver que hemos conseguido escribir en la GOT. Vamos a comparar qué pasa cuando compilamos con gcc -Wl,-z,relro,-z,now. Primero localizamos dónde está printf, en 0x600fe0.
Y lanzamos el programa:
Como podemos ver, gracias a -z relro las relocations se han vuelto de solo lectura cuando el programa tiene el control, y con -z now le pedimos al linker que resuelva todos los símbolos dinámicos antes de pasar el control al programa, y así no podemos sobreescribir la GOT.
The post Hardening de binarios (VI) – RELRO appeared first on S3lab.
]]>