CI2C:Curso de Ingeniería Inversa para Chainiks. Parte I
INGENIERIA_INVERSA
CI2C:Curso de Ingeniería Inversa para Chainiks. Parte I
2016-09-27
Por
Wh1t3 D3M0n

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.

Bueno, suficiente introducción. Vamos al tajo a ver que tal se nos da. Si esto no funciona, probaremos con el Duero o el Ebro :P

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

Veamos como conseguir acceder al mensaje que nos interesa en cada unos de estos tres casos:

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 c1

Y 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 c1

Ahora 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 0x400688

La 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>:	leaveq

Wow, 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.txt

Ahora 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-mod

Como 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!


SOBRE Wh1t3 D3M0n
Me conocen como Wh1th3 D3M0n y soy un hacker. Me oculto entre las sombras de la red, paseándome entre máquinas y datos como un espectro… invisible a los ojos de los usuarios…

Que va es coña!

Twitter: @ChainkMaster | Blog: https://thehackerkid.tumblr.com/