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.
Nos situamos en el offset 4b7 y apuntamos el código de operación: 0F 85. Toda operación tiene un código unívoco. Según la siguiente URL, el 0F 85 corresponde a JNE. Bingo! En gHex2 nos ponemos en modo inserción (Edición / Modo insertar) y cambiamos el 0F 85 por 0F 84 (siguiendo la página anterior, 0F 84 es el código de operación de JE (Jump if Equivalent).
Grabamos el resultado con otro nombre (por ejemplo, nivel04_hacked) y ejecutamos:
$ chmod +x nivel04_hacked
$ ./nivel04_hacked
Introduzca su nombre de usuario: j
Comprobando n�mero de serie… UNREGISTERED
Hola j, bienvenido al sistema…
La contrase�a del siguiente nivel es k0m13nz0s
Magia! 🙂 Bueno, ya me he desestrado un buen rato aprendiendo a resolver este problema, desempolvando mis conocimientos de GDB, y ASM. Si alguien quiere profundizar más en este arte casi perdido de la ingeniería inversa, puede empezar por los enlaces que acompañan a este artículo… o probando las pruebas de mayor dificultad que encontrará en el blog de txipi, el autor del HackIt! 2006 al que hay que agradecer el enorme trabajo que hizo (y animar a que nos vuelva a retar en el 2007 !)
Para finalizar, indicar que hemos montado un subdominio hackit.diariolinux.com para todo aquel que quiera echar unas partiditas 😉
[1] BULMA: Introducción a GDB http://bulma.net/body.phtml?nIdNoticia=1805
[2] Análisis de un ejecutable toUpper con GDB: http://www.csee.umbc.edu/~cpatel2/links/310/projects/toupper.txt
[3] Exploiting your 1st buffer overflow vulnerability : http://www.devtarget.org/downloads/wolfgarten-your-first-buffer-overflow.pdf
[4] HackIt! 2006 : http://blog.txipinet.com/index.php/2006/08/15/20-solucion-de-los-5-primeros-niveles-del-hack-it-de-la-euskal-encounter-2006
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 😉