Bienvenidos a la primera parte de el curso acelerado de ingeniería inversa para chainiks. En este curso aprenderéis todo lo que se necesita saber para convertirse en un experto ingeniero inverso... y eso no tiene nada que ver con vuestras tendencias sexuales.
Empecemos por el principio. La ingeniería inversa es la disciplina que estudia las técnicas necesarias para averiguar como funciona un determinado sistema. En general, veréis el término aplicado al SW, pero en realidad, podéis hacer ingeniería inversa sobre un dispositivo mecánico, un dispositivo electrónico o un algoritmo. En cierto modo, el cryptoanálisis se puede ver como una forma de ingeniería inversa.
Podéis verlo, si queréis como el proceso contrario a la producción de algo. Obtener la información de diseño a partir de una implementación.
Nuestra intención es que este curso sea eminentemente práctico. Con poca teoría. La suficiente para que no seáis unos Script Kiddie, pero no demasiada para aburriros.
Para este curso necesitaréis tener una base de algunas cosas, de lo contrario, seamos sinceros, no os vais a enterar de mucho:
■
-
- Conocimientos básicos de programación en C
- Conocimientos básicos de programación en Ensamblador
- Conocimientos básicos sobre procesos y formatos ejecutables en GNU/Linux
- ... otras cosas que ahora no se me ocurren, pero que seguro que os harán falta.
El típico Crackme 0
Todos los desafíos de ingeniería inversa siempre comienzan con un desafío muy sencillo con el que ponerse manos a la obra. Este desafío se puede superar sin hacer mucha ingeniería inversa, pero es una especie de "Hola Mundo" de esta disciplina. El desafío consiste en acceder a una cierta parte de un programa protegida por una contraseña. Este sería el típico programa en C al que nos tendremos que enfrentar:#include <stdio.h> #include <string.h> char *the_pass = "MyVoiceIsMyPassport"; int main (void) { char user_pass[1024]; printf ("Introduce la contraseña: "); fgets (user_pass, 1024, stdin); if (!strncmp (user_pass, the_pass, strlen(the_pass))) { printf ("Bien Hecho!\n"); } else printf ("Sigue Intentándolo\n"); }
CRACKME #0
Nuestro objetivo es que el programa genere como salida el mensaje "Bien Hecho!". Esto lo podemos hacer de tres formas:
-
1. Averiguando la contraseña
2. Crackeando el programa para que siempre muestre el mensaje incluso cuando la contraseña no es correcta
3. Cambiando la contraseña almacenada en el programa por una de nuestra elección
Averiguando la contraseña
Hay dos formas fundamentales de averiguar la contraseña en este caso. Esto es así porque la contraseña está almacenada como texto plano en el fichero compilado, y por lo tanto es visible. Para obtener las cadenas de caracteres imprimibles almacenadas en un cierto fichero podemos utilizar el comando strings. De hecho, esto es algo que deberéis probar siempre ya que suele desvelar información interesante. Copiad el código de nuestro programa ejemplo más arriba, y copiadlo en un fichero llamado c1.c. Ahora compilemos el programa:$ make c1Y veamos que tiene que decirnos strings sobre este ejecutable:
$ strings c1 (... mogollón de líneas aquí) |$0H MyVoiceIsMyPassport Introduce la contrase Bien Hecho! Sigue Intent ndolo ;*3$" GCC: (Ubuntu/Linaro 4.6.3-1ubuntu5) 4.6.3 .symtab (... más líneas aquí ....)Como podéis observar en medio de la salida de strings podemos ver todas las cadenas de caracteres que utiliza el programa, incluyendo la clave que buscamos. Llegados a este punto probaríamos las claves candidatas a ver si alguna funciona. Para filtrar un poco la salida de strings podemos usar grep con el flag -C y una de las cadenas que sabemos se encuentran en el fichero. Por ejemplo "Sigue". El flag -C debe ir seguido de un número que indica cuantas líneas antes y después de la cadena buscada deben mostrarse. Por ejemplo con un valor de 5 obtendríamos la siguiente salida:
$ strings c1 | grep -C 5 Sigue t$(L |$0H MyVoiceIsMyPassport Introduce la contrase Bien Hecho! Sigue Intent ndolo ;*3$" GCC: (Ubuntu/Linaro 4.6.3-1ubuntu5) 4.6.3 .symtab .strtab
Averiguando la contraseña II
Si habéis prestado atención, os estaréis preguntando... cuál es la otra forma de averiguar la contraseña de este programa?... Bueno, pues haciendo ingeniería inversa. Sí esta vez si. Para ello, deberemos desensamblar el programa. Destriparlo. Hurgar en sus tripas para encontrar lo que estamos buscando.... Sin piedad... ARRGGG!!. Bueno ya estoy más relajado. Para desensamblar nuestro programa podemos utilizar diferentes herramientas. Para este primer ejemplo vamos a ir con algo intermedio... gdb Primero abrimos el programa con gdb$ gdb c1Ahora vamos a poner un breakpoint en la función main. Un breakpoint nos permite parar la ejecución del programa en el punto que nosotros queramos, y utilizar el desensamblador para examinar el proceso.
(gdb) b main Breakpoint 1 at 0x400688La dirección que aparece en la respuesta de gdb puede ser diferente en vuestro sistema. Ahora podemos ejecutar el programa. El cual se parará al principio de la función main.
(gdb) r Starting program: /home/wd/ci2c/c1 Breakpoint 1, 0x0000000000400688 in main () (gdb)Ahora echemos un ojo al ensamblador de la función main
disassem Dump of assembler code for function main: 0x0000000000400684 <+0>: push %rbp 0x0000000000400685 <+1>: mov %rsp,%rbp => 0x0000000000400688 <+4>: sub $0x420,%rsp 0x000000000040068f <+11>: mov %fs:0x28,%rax 0x0000000000400698 <+20>: mov %rax,-0x8(%rbp) 0x000000000040069c <+24>: xor %eax,%eax 0x000000000040069e <+26>: mov $0x400850,%eax 0x00000000004006a3 <+31>: mov %rax,%rdi 0x00000000004006a6 <+34>: mov $0x0,%eax 0x00000000004006ab <+39>: callq 0x400570 <printf@plt> 0x00000000004006b0 <+44>: mov 0x200991(%rip),%rax # 0x601048 <stdin@@GLIBC_2.2.5> 0x00000000004006b7 <+51>: mov %rax,%rdx 0x00000000004006ba <+54>: lea -0x410(%rbp),%rax 0x00000000004006c1 <+61>: mov $0x400,%esi 0x00000000004006c6 <+66>: mov %rax,%rdi 0x00000000004006c9 <+69>: callq 0x400590 <fgets@plt> 0x00000000004006ce <+74>: mov 0x20096b(%rip),%rax # 0x601040 <the_pass> 0x00000000004006d5 <+81>: movq $0xffffffffffffffff,-0x418(%rbp) 0x00000000004006e0 <+92>: mov %rax,%rdx 0x00000000004006e3 <+95>: mov $0x0,%eax 0x00000000004006e8 <+100>: mov -0x418(%rbp),%rcx 0x00000000004006ef <+107>: mov %rdx,%rdi 0x00000000004006f2 <+110>: repnz scas %es:(%rdi),%al 0x00000000004006f4 <+112>: mov %rcx,%rax 0x00000000004006f7 <+115>: not %rax 0x00000000004006fa <+118>: lea -0x1(%rax),%rdx 0x00000000004006fe <+122>: mov 0x20093b(%rip),%rcx # 0x601040 <the_pass> 0x0000000000400705 <+129>: lea -0x410(%rbp),%rax 0x000000000040070c <+136>: mov %rcx,%rsi 0x000000000040070f <+139>: mov %rax,%rdi 0x0000000000400712 <+142>: callq 0x400540 <strncmp@plt> 0x0000000000400717 <+147>: test %eax,%eax 0x0000000000400719 <+149>: jne 0x400727 <main+163> 0x000000000040071b <+151>: mov $0x40086b,%edi 0x0000000000400720 <+156>: callq 0x400550 <puts@plt> 0x0000000000400725 <+161>: jmp 0x400731 <main+173> 0x0000000000400727 <+163>: mov $0x400877,%edi 0x000000000040072c <+168>: callq 0x400550 <puts@plt> 0x0000000000400731 <+173>: mov -0x8(%rbp),%rdx 0x0000000000400735 <+177>: xor %fs:0x28,%rdx 0x000000000040073e <+186>: je 0x400745 <main+193> 0x0000000000400740 <+188>: callq 0x400560 <__stack_chk_fail@plt> 0x0000000000400745 <+193>: leaveqWow, wow, woow... para el carro tío. Si, impresiona no?. Pero es mucho menos de lo que parece. Echadle un ojo al código, y veréis que en la dirección 0x0400712 encontramos la llamada a strncmp. Esa es la función que compara la contraseña que introducimos desde el teclado, con la que tenemos almacenada en nuestro programa. Además, como no hemos eliminado los símbolos de este binario, gdb nos muestra incluso la dirección de memoria en la que se encuentra la variable the_pass (0x601040)... Echemos un ojo:
(gdb) x/s *0x601040 0x40083c: "MyVoiceIsMyPassport"Bien, el comando x nos permite hacer un volcado de memoria. Después de la barra podemos indicarle cuantos elementos queremos que vuelque y en que formato. En este caso hemos indicado 's' que significa que queremos ver el volcado como una cadena de caracteres. Podríamos haber usado x/20x *0x601040 para volcar veinte bytes en hexadecimal. Escribid help x en gdb para obtener más información sobre lo que es posible. Bueno, hemos encontrado la clave esta vez usando el ensamblador y analizando los parámetros con los que llamamos a una función. Bueno, esto no lo hemos explicado todavía, pero en realidad es lo que hemos hecho... Por el momento deberéis hacer un pequeño acto de fé.
Crackeando el programa
Ya hemos visto dos formas con las que averiguar la clave que el programa espera que introduzcamos. Ahora es el momento de crackearlo para que de igual la clave que introduzcamos. Para ello tendremos que fijarnos más atentamente en el ensamblador. Especialmente en estas lineas:0x0000000000400712 <+142>: callq 0x400540 <strncmp@plt> 0x0000000000400717 <+147>: test %eax,%eax 0x0000000000400719 <+149>: jne 0x400727 <main+163> 0x000000000040071b <+151>: mov $0x40086b,%edi 0x0000000000400720 <+156>: callq 0x400550 <puts@plt> 0x0000000000400725 <+161>: jmp 0x400731 <main+173> 0x0000000000400727 <+163>: mov $0x400877,%edi 0x000000000040072c <+168>: callq 0x400550 <puts@plt>Vemos que tras ejecutar strncmp, el programa comprueba el valor del registro eax. Si el resulta de ese test es distinto de cero (jne Jump Not Equal) saltaremos al offset +163 y llamaremos a puts (que imprime una cadena de caracteres en la pantalla) para que imprima el contenido de 0x400877, sino, imprimiremos el contenido de 0x49986b. Veamos que tenemos en esas dos direcciones:
(gdb) x/s 0x40086b 0x40086b: "Bien Hecho!" (gdb) x/s 0x400877 0x400877: "Sigue Intentándolo"Así que lo que queremos es que el código en el offset +151 sea el que se ejecute siempre. Para ello podemos hacer varias cosas. Cambiar el salto condicional por un salto absoluto, cambiar la operación (test %eax, %eax) de forma que el salto se comporte siempre como queramos. O simplemente eliminar el salto. La solución dependerá del programa, de cuantos bytes de memoria podamos escribir y de cuantos bytes de memoria requiera la instrucción que queremos insertar. En este caso, lo más sencillo es eliminar el salto (jne). Para ello sustituiremos todos sus bytes por NOPs. El comando NOP no hace nada, es decir, estamos borrando el jne de memoria. Así que lo que tenemos que hacer es encontrar ese salto en el fichero y cambiar el código máquina asociado por una secuencia de instrucciones NOP.
Parcheando un binario
Vamos a dejar gdb por un momento y volver a nuestra línea de comandos para parchear el programa. Lo primero que tenemos que hacer es averiguar cual es el código máquina asociado al salto que queremos eliminar. Para ello vamos a utilizar la utilizad objdump. Veamos como:$ objdump -d c1 | grep -C 5 "jne.*400727" 400705: 48 8d 85 f0 fb ff ff lea -0x410(%rbp),%rax 40070c: 48 89 ce mov %rcx,%rsi 40070f: 48 89 c7 mov %rax,%rdi 400712: e8 29 fe ff ff callq 400540 <strncmp@plt> 400717: 85 c0 test %eax,%eax 400719: 75 0c jne 400727 <main+0xa3> 40071b: bf 6b 08 40 00 mov $0x40086b,%edi 400720: e8 2b fe ff ff callq 400550 <puts@plt> 400725: eb 0a jmp 400731 <main+0xad> 400727: bf 77 08 40 00 mov $0x400877,%edi 40072c: e8 1f fe ff ff callq 400550 <puts@plt>La utilizad objdump nos permite, entre otras cosas, volcar tanto el ensamblador como el código máquina de un programa. Podríamos haberlo hecho desde gdb, pero así vemos otra herramienta. El flag -d nos permite desensamblar el programa. Luego, utilizamos grep para localizar el salto en el que estamos interesados. Una vez más utilizamos el flag -C para mostrar las 10 líneas (5 arriba y 5 abajo) que se encuentran en torno a la instrucción en la que estamos interesados. En el volcado proporcionado por objdump, podemos ver que el código máquina que tenemos que buscar y sustituir por NOPs (0x90) es 0x75 0x0c. Vamos allá. Si disponéis de un editor hexadecimal... pues adelante, solo tenéis que buscar la cadena hexadecimal del salto y cambiar por NOPs. Si no disponéis de un editor hexadecimal, podéis utilizar la utilidad xxd. Esta utilidad permite hacer volcados hexadecimales de ficheros binarios y convertir a formato binarios volcados hexadecimales. Veamos como utilizarla: Primero generamos nuestro volcado hexadecimal:
$ cat c1 | xxd -g 1 > c1.txtAhora podemos abrir el fichero c1.txt con cualquier editor de texto y buscar la secuencia de bytes. Observad. El flag -g 1 le dice a xxd que vuelque bytes, ya que, por defecto, volcaría palabras. Aseguraros de que habéis encontrado el salto correcto comprobando las secuencia hexadecimal de las instrucciones anteriores y posteriores... Para eso le hemos dicho a grep que nos muestre lo que hay alrededor del salto un poco más arriba. Ahora cambia "75 0c" por "90 90" y graba el fichero. Una vez modificado nuestro volcado hexadecimal podremos utilizar de nuevo la utilizad xxd para general el fichero binario de nuevo.
$ cat c1.txt | xxd -r > c1-modComo os podéis imaginar, el flag -r es el encargado de hacer el proceso inverso (Reverse) para convertir el volcado hexadecimal en un fichero binario de nuevo. Ahora podéis probar a ejecutar c1-mod y introducir cualquier clave que queráis :)
Cambiando la contraseña
Si recordáis, al principio del artículo comentamos tres posibles soluciones al desafío. La ultima opción para solucionar este desafío era modificar la contraseña de forma que sepamos cual es. Ahora que sabéis como hacer volcados hexadecimales de ficheros y como parchear los ficheros... esta última opción la dejamos como ejercicio para el lector... No deberíais tener ningún problema. De lo contrario no dudéis en preguntar! Hasta la siguiente entrega!... Que será mucho más emocionante!■
CLICKS: 2176