En esta entrega vamos a hablar de dos opciones que podemos pasarle al linker para hacer más seguras ciertas secciones de nuestro ejecutable: -Wl,-z,relro,-z,now. Cuando un programa llama a una función no definida en el programa, que está en una biblioteca compartida, las secciones GOT (Global Offset Table) y PLT (Procedure Linkage Table) entran en juego, que son las encargadas de especificar dónde está esa función que necesitamos (ver este link para una explicación más detallada). Por ejemplo, si utilizamos printf en un programa y miramos qué está pasando en ensamblador:
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.