Esta semana ha sido muy ajetreada (trabajos, exámenes, prácticas…) y para liberar un poco de stress, aparte de ir a hacer un poco de footing, he desempolvado un viejo problema del concurso de HackIt! de la Euskal Encounter 2006 y le he dedicado algunos minutos (bueno, vale, tal vez algo más O:-) El reto consiste en ‘romper’ un fichero ejecutable Linux (formato ELF) para obtener una clave. Como ayuda, partimos del siguiente programa C (lógicamente no es exactamente el que genera el ejecutable, pero nos sirve como pista para ver por dónde van los tiros):
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char *argv[]){
char serial[15] = "UNREGISTERED";
char login[20]; printf("Introduzca su nombre de usuario: ");
gets(login);
printf("Comprobando número de serie... %sn", serial);
if(!strcmp(serial, "00-11-22-33-44"))
{
printf("Hola %s, bienvenido al sistema...n", login);
printf("La contraseña del siguiente nivel es LALALALAn");
} else
{
printf("Lo siento %s, su numero de serie ha caducadon", login);
exit(-1);
}
}
Al ejecutar el fichero ELF vemos que efectivamente sigue el flujo especificado en la plantilla de código C anterior: se pide el nombre de usuario y se comprueba el nº de serie 00-11-22-33-44 contra el que el ejecutable guarda en memoria. Lógicamente los números son distintos y por tanto, se informa al usuario del error:
La forma más rápida de saltarse la protección consiste en darse cuenta de que se utiliza una función gets() para obtener el nombre del usuario SIN hacer ninguna comprobacióndel tamaño de la entrada. Teniendo esto en cuenta, y viendo cómo se almacenan las variables login y serial, ¿qué ocurrirá si tecleamos un nombre (login) de más de 20 caracteres? ¿y si esos caracteres de más son justo 00-11-22-33-44? El resultado es que la zona de memoria reservada para guardar el login se desborda y machaca la zona de memoria reservada para guardar el número de serie. Es decir, provocaremos un simple buffer overflow. Simple pero lo suficientemente efectivo como para romper el flujo de ejecución a nuestro gusto y obtener la clave deseada.
No obstante, hay más soluciones para resolver este reto. Por ejemplo, podemos abrir el fichero con VIM y retocar el nº de serie almacenado (00-11-22-33-44) para que en su lugar aparezca el que deseamos (UNREGISTERED). Hay que tener cuidado de no cambiar el tamaño del ejecutable, para evitar un error del tipo «Violación de Segmento».
Pero el procedimiento general que me interesaba es otro. Quería desensamblar el ejecutable, analizar el código ASM y modificar las instrucciones necesarias para romper el flujo normal de ejecución de tal forma que consigamos la clave directamente. O sea, droga dura.
Vayamos paso a paso :
1) Desempolvando GDB (desensamblar el ELF):
Para esta parte, usaremos GDB, el debugger GNU. Podemos abrir el ejecutable directamente con gdb, así:
$ gdb nivel04
(gdb) break main <--- poner un punto de ruptura en el método main
(gdb) run <-- comenzar a ejecutar el ELF
Starting program: /tmp/nivel04/nivel04
Breakpoint 1, 0x080483ca in main () <--- interesante, main comienza en esa dirección de memoria
(gdb) disassemble 0x080483ca 0x80484ff <-- desensamblar desde main() unas cuantas instrucciones
… saltemos a la parte interesante …
0x08048498 <main+212>: mov 0xfffff7dc(%ebp),%edi
0x0804849e <main+218>: mov 0xfffff7d8(%ebp),%ecx
0x080484a4 <main+224>: repz cmpsb %es:(%edi),%ds:(%esi) <--- comparación de strings, mmmhhh...
0x080484a6 <main+226>: seta %dl
0x080484a9 <main+229>: setb %al
0x080484ac <main+232>: mov %dl,%cl
0x080484ae <main+234>: sub %al,%cl
0x080484b0 <main+236>: mov %cl,%al
0x080484b2 <main+238>: movsbl %al,%eax
0x080484b5 <main+241>: test %eax,%eax
0x080484b7 <main+243>: jne 0x8048583 <main+447> <--- bifurcación en función de la comparación ...
0x080484bd <main+249>: lea 0xfffff7f8(%ebp),%eax
...
Vale… ¿qué estamos comparando exactamente en la instrucción situada en 0x080484a4 ? Veámoslo.
(gdb) break *0x080484a4 Breakpoint 2 at 0x80484a4
(gdb) continue Continuing. Introduzca su nombre de usuario: Juanan Comprobando n�mero de serie… UNREGISTERED
Breakpoint 2, 0x080484a4 in main ()
(gdb) x/14c $edi <--- veamos qué tenemos en la zona apuntada por %edi 0x804873f <_IO_stdin_used+75>: 48 ‘0’ 48 ‘0’ 45 ‘-‘ 49 ‘1’ 49 ‘1’ 45 ‘-‘ 50 ‘2’ 50 ‘2’ 0x8048747 <_IO_stdin_used+83>: 45 ‘-‘ 51 ‘3’ 51 ‘3’ 45 ‘-‘ 52 ‘4’ 52 ‘4’
(gdb) x/s $edi <--- lo mismo de antes, dicho de otra forma más human-friendly ;-) 0x804873f <_IO_stdin_used+75>: «00-11-22-33-44»
Anda! el número de serie con el que comparamos… por tanto, en %esi tendremos…
(gdb) x/s $esi 0xbfeeae14: «UNREGISTERED»
Olé! Justo lo que suponía.
Así que si esta comparación se cumple, habrá un salto (bifurcación if) a alguna zona de memoria, y en caso contrario, seguirá el flujo normal (sin salto). Según el código ASM, el primer salto tras esa comparación se produce en 0x080484b7 (salto JNE a 0x8048583). El objetivo es cambiar esa instrucción para hacer justo lo contrario de lo que hace ahora, es decir, que si antes saltábamos si UNREGISTERED y 00-11-22-33-44 eran distintas, ahora no saltaremos (y viceversa). ¿Cómo? Cambiando el JNE – Jump if Not Equivalent – por la instrucción JE – Jump if Equivalent. 2) Cambiando el curso del río (cómo retocar el fichero binario a nuestro gusto) Quiero cambiar la instrucción situada en la zona de memoria 0x080484b7, JNE por un JE. Para ello, tengo que calcular el offset a esa instrucción dentro del fichero. Es decir, cuando abra el fichero binario, ¿dónde estará el byte que tengo que cambiar? ¿y a qué valor lo cambio? Las direcciones del código main() empiezan todas por 0x08048xxx. Ese desplazamiento (xxx) es el que me interesa. En nuestro caso, 4b7. Ese será el offset dentro del fichero binario. Abrimos con un editor hexadecimal el fichero ejecutable nivel04. Por ejemplo, con ghex2, el editor hexadecimal de GNOME.
Qué pasada, parece juego de niños 😀
Lástima que cuando estudié en la FISS no había profesores así…
Gracias, Jon 😉
Algunas anotaciones post-mortem:
1) no es necesario ponerse en modo inserción dentro de ghex2 para hacer los cambios.
2) si el ejecutable necesita parámetros, basta con
$ gdb ./ejecutable
(gdb) run parámetro1 parámetro2 ….
3) en lugar de direcciones de memoria, para el desensamblado se puede poner:
(gdb) disassemble main
Este año también habrá hack-it, así que id repasando todas estas técnicas 😉
txipi: gracias por tu trabajo, la verdad es que es impresionante. Personalmente sólo por competir en el HackIt! ya me merece la pena ir a la Euskal (aparte de las charlas, estar con viejos amigos y gozar del ambiente) ¡Ánimo y apúntate unas cervezas en la Euskal a mi cuenta para comentar la jugada! (después del HackIt, no vaya a ser que genere suspicacias entre el resto de participantes 😉