CI2C: Resolviendo el desafío del número 2
INGENIERIA_INVERSA
CI2C: Resolviendo el desafío del número 2
2017-05-20
Por
Wh1t3 D3M0n

Como sabréis, el número dos de Occam's Razor segunda época, contenía un chackme, un desafío para entrenar vuestras habilidades como ingenieros inversos. Si todavía no lo has resuelto y quieres intentarlo, mejor que dejes de leer inmediatamente... Si por el contrario ya lo has resuelto y quieres ver como lo hacemos nosotros o te has dado por vencido... Entonces este artículo es PARA TÍ.
En el caso de que algún distraído no haya leído la página del número anterior en el que se explica como extraer el desafio, aquí están unas instrucciones rápidas. El desafío esta almacenado en el propio PDF de la revista, y se puede extraer utilizando la utilidad ZIP con un comando como el siguiente:

$ unzip -x roor-N02.pdf 
  o
$ unzip -x roor-N02.pdf  roor-N02/desafio/desafio
    
FIGURA 1. Extrayendo el desafío.

El primer comando desempaqueta todo el contenido almacenado en el PDF (fuentes de la revista, código fuente de los artículos, el programa Java y el desafío), mientras que el segundo solamente extrae el desafío que vamos a resolver.

Comenzando

Vamos a comenzar viendo que es lo que tenemos entre manos:

$  file desafio
desafio: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, for GNU/Linux 2.6.24, BuildID[sha1]=0x3af1ed2c13f6ccf68a1970481743abf8e3722a5d, not stripped
    
FIGURA 2. Identificando el binario

Bien, se trata de un ejecutable estático de 64bits para Linux al que no se le han eliminado los símbolos (not stripped). Eso nos va a facilitar las cosas. Ahora veamos si hay alguna cadena de caracteres que nos pueda resultar útil:

$ strings desafio
(mogollón de líneas)
    

Claro, un binario estático con todos los símbolos nos va a volcar un mogollón de cadenas. Intentemos ser más selectivos. Las cadenas estáticos se almacenan en la sección .rodata como todos sabemos :). RO viene de Read Only, solo lectura, y ahí acaba cualquier literal que añadamos a nuestro programa... Intentemos volcar los datos de esa parte del programa. Primero vamos a echar un ojo a la estructura de este ELF (Figura1)

$  readelf -S desafio
There are 31 section headers, starting at offset 0xc2de8:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .note.ABI-tag     NOTE             0000000000400190  00000190
       0000000000000020  0000000000000000   A       0     0     4
  [ 2] .note.gnu.build-i NOTE             00000000004001b0  000001b0
       0000000000000024  0000000000000000   A       0     0     4
  [ 3] .rela.plt         RELA             00000000004001d8  000001d8
       00000000000000d8  0000000000000018   A       0     5     8
  [ 4] .init             PROGBITS         00000000004002b0  000002b0
       000000000000001a  0000000000000000  AX       0     0     4
  [ 5] .plt              PROGBITS         00000000004002d0  000002d0
       0000000000000090  0000000000000000  AX       0     0     16
  [ 6] .text             PROGBITS         0000000000400360  00000360
       0000000000092194  0000000000000000  AX       0     0     16
  [ 7] __libc_freeres_fn PROGBITS         0000000000492500  00092500
       0000000000001c07  0000000000000000  AX       0     0     16
  [ 8] __libc_thread_fre PROGBITS         0000000000494110  00094110
       00000000000000a8  0000000000000000  AX       0     0     16
  [ 9] .fini             PROGBITS         00000000004941b8  000941b8
       0000000000000009  0000000000000000  AX       0     0     4
  [10] .rodata           PROGBITS         00000000004941e0  000941e0
       000000000001ee40  0000000000000000   A       0     0     32
  [11] __libc_subfreeres PROGBITS         00000000004b3020  000b3020
       0000000000000058  0000000000000000   A       0     0     8
  [12] __libc_atexit     PROGBITS         00000000004b3078  000b3078
       0000000000000008  0000000000000000   A       0     0     8
  [13] __libc_thread_sub PROGBITS         00000000004b3080  000b3080
       0000000000000008  0000000000000000   A       0     0     8
  [14] .eh_frame         PROGBITS         00000000004b3088  000b3088
       000000000000d25c  0000000000000000   A       0     0     8
  [15] .gcc_except_table PROGBITS         00000000004c02e4  000c02e4
       00000000000000d3  0000000000000000   A       0     0     1
  [16] .tdata            PROGBITS         00000000006c0ea0  000c0ea0
       0000000000000020  0000000000000000 WAT       0     0     16
  [17] .tbss             NOBITS           00000000006c0ec0  000c0ec0
       0000000000000038  0000000000000000 WAT       0     0     16
  [18] .init_array       INIT_ARRAY       00000000006c0ec0  000c0ec0
       0000000000000010  0000000000000000  WA       0     0     8
  [19] .fini_array       FINI_ARRAY       00000000006c0ed0  000c0ed0
       0000000000000010  0000000000000000  WA       0     0     8
  [20] .jcr              PROGBITS         00000000006c0ee0  000c0ee0
       0000000000000008  0000000000000000  WA       0     0     8
  [21] .data.rel.ro      PROGBITS         00000000006c0f00  000c0f00
       00000000000000e4  0000000000000000  WA       0     0     32
  [22] .got              PROGBITS         00000000006c0fe8  000c0fe8
       0000000000000010  0000000000000008  WA       0     0     8
  [23] .got.plt          PROGBITS         00000000006c1000  000c1000
       0000000000000060  0000000000000008  WA       0     0     8
  [24] .data             PROGBITS         00000000006c1060  000c1060
       0000000000001c10  0000000000000000  WA       0     0     32
  [25] .bss              NOBITS           00000000006c2c80  000c2c70
       0000000000002518  0000000000000000  WA       0     0     32
  [26] __libc_freeres_pt NOBITS           00000000006c5198  000c2c70
       0000000000000030  0000000000000000  WA       0     0     8
  [27] .comment          PROGBITS         0000000000000000  000c2c70
       000000000000002b  0000000000000001  MS       0     0     1
  [28] .shstrtab         STRTAB           0000000000000000  000c2c9b
       000000000000014d  0000000000000000           0     0     1
  [29] .symtab           SYMTAB           0000000000000000  000c35a8
       000000000000c720  0000000000000018          30   924     8
  [30] .strtab           STRTAB           0000000000000000  000cfcc8
       0000000000007c03  0000000000000000           0     0     1
    
Figura3. Secciones de desafio

De la salida del comando anterior (Figura 1), sabemos que la sección está en el offset 0x0941e0 del fichero, y que tiene un tamaño de 0x1ee40. Si no sabéis porque es un buen momento para consular la página del manual de readelf. Así que vamos a extraer el contenido de la sección y pasarlo por strings.

$  echo "ibase=16;941E0; 1EE40" | bc
606688
126528
$  dd if=desafio skip=606688 bs=1 count=606688 | strings | head -n 20
HOME
%s/.ims
%s/%x%x
Este mensaje ya ha sido le
Ghost Mail v 1.1.5
+ Inicializando DarkM4nt1s 2.0...
+ Entorno seguro listo
  Clave (4 or mas caracteres):
%4x%4x
---[ TU MENSAJE ]---------------------------------
--------------------------------------------------
Este mensaje se autodestruir
 en 5 segundos...
Acceso Denegado!
libc-start.c
FATAL: kernel too old
/dev/urandom
__ehdr_start.e_phentsize == sizeof *_dl_phdr
FATAL: cannot determine kernel version
unexpected reloc type in static binary
    
FIGURA 4. Salida de strings sobre desafío
Como podemos ver hacia el final ya aparecen los datos generados por el compilador, lo cuales no nos interesan. Pero bueno, en este caso, no aparece nada que se asemeje a una clave... Así que tendremos que buscar más a fondo.

Mirando el código

Puesto que no hemos encontrado nada evidente, vamos a echar un ojo al código. Comencemos volcando el contenido de la función main (Listado 1):
$  objdump -d desafio | grep -A 50 "<main>"
00000000004012c8 <main>:
  4012c8:	55                   	push   %rbp
  4012c9:	48 89 e5             	mov    %rsp,%rbp
  4012cc:	48 83 ec 20          	sub    $0x20,%rsp
  4012d0:	89 7d ec             	mov    %edi,-0x14(%rbp)
  4012d3:	48 89 75 e0          	mov    %rsi,-0x20(%rbp)
  4012d7:	48 8b 45 e0          	mov    -0x20(%rbp),%rax
  4012db:	48 8b 00             	mov    (%rax),%rax
  4012de:	48 89 05 eb 19 2c 00 	mov    %rax,0x2c19eb(%rip)        # 6c2cd0 <name>
  4012e5:	be ca b4 00 00       	mov    $0xb4ca,%esi
  4012ea:	bf 3a fc 00 00       	mov    $0xfc3a,%edi
  4012ef:	e8 1d fe ff ff       	callq  401111 <s1>
  4012f4:	bf 80 43 49 00       	mov    $0x494380,%edi
  4012f9:	e8 92 79 00 00       	callq  408c90 <_IO_puts>
  4012fe:	bf d0 43 49 00       	mov    $0x4943d0,%edi
  401303:	b8 00 00 00 00       	mov    $0x0,%eax
  401308:	e8 a3 6e 00 00       	callq  4081b0 <_IO_printf>
  40130d:	48 8d 45 f0          	lea    -0x10(%rbp),%rax
  401311:	48 8d 50 04          	lea    0x4(%rax),%rdx
  401315:	48 8d 45 f0          	lea    -0x10(%rbp),%rax
  401319:	48 89 c6             	mov    %rax,%rsi
  40131c:	bf f0 43 49 00       	mov    $0x4943f0,%edi
  401321:	b8 00 00 00 00       	mov    $0x0,%eax
  401326:	e8 45 70 00 00       	callq  408370 <__isoc99_scanf>
  40132b:	48 8d 45 f0          	lea    -0x10(%rbp),%rax
  40132f:	48 89 c7             	mov    %rax,%rdi
  401332:	e8 27 fd ff ff       	callq  40105e <check_pass>
  401337:	85 c0                	test   %eax,%eax
  401339:	75 5a                	jne    401395 <main+0xcd>
  40133b:	bf 77 43 49 00       	mov    $0x494377,%edi
  401340:	b8 00 00 00 00       	mov    $0x0,%eax
  401345:	e8 66 6e 00 00       	callq  4081b0 <_IO_printf>
  40134a:	bf f8 43 49 00       	mov    $0x4943f8,%edi
  40134f:	e8 3c 79 00 00       	callq  408c90 <_IO_puts>
  401354:	8b 15 3e fd 2b 00    	mov    0x2bfd3e(%rip),%edx        # 6c1098 <_ms>
  40135a:	48 8b 35 27 fd 2b 00 	mov    0x2bfd27(%rip),%rsi        # 6c1088 <_k>
  401361:	48 8b 05 28 fd 2b 00 	mov    0x2bfd28(%rip),%rax        # 6c1090 <_m>
  401368:	b9 04 00 00 00       	mov    $0x4,%ecx
  40136d:	48 89 c7             	mov    %rax,%rdi
  401370:	e8 17 fd ff ff       	callq  40108c <print_xor>
  401375:	bf 30 44 49 00       	mov    $0x494430,%edi
  40137a:	e8 11 79 00 00       	callq  408c90 <_IO_puts>
  40137f:	bf 68 44 49 00       	mov    $0x494468,%edi
  401384:	e8 07 79 00 00       	callq  408c90 <_IO_puts>
  401389:	bf 05 00 00 00       	mov    $0x5,%edi
  40138e:	e8 e4 fe ff ff       	callq  401277 <destroy>
  401393:	eb 0a                	jmp    40139f <main+0xd7>
  401395:	bf 98 44 49 00       	mov    $0x494498,%edi
  40139a:	e8 f1 78 00 00       	callq  408c90 <_IO_puts>
  40139f:	c9                   	leaveq
  4013a0:	c3                   	retq
    
Listado1. Desensamblado de la función main

Bien, como todos los símbolos están ahí, resulta mucho mas sencillo interpretar el programa. Un rápido vistazo y una línea llama fuertemente nuestra atención:

  401332:	e8 27 fd ff ff       	callq  40105e <check_pass>    

Veamos que esconde esa función de nombre tan sugerente (Listado 2):

$  objdump -d desafio | grep -A 16 "<check_pass>:"
000000000040105e <check_pass>:
  40105e:	55                   	push   %rbp
  40105f:	48 89 e5             	mov    %rsp,%rbp
  401062:	48 89 7d f8          	mov    %rdi,-0x8(%rbp)
  401066:	48 8b 45 f8          	mov    -0x8(%rbp),%rax
  40106a:	8b 10                	mov    (%rax),%edx
  40106c:	8b 05 fe ff 2b 00    	mov    0x2bfffe(%rip),%eax        # 6c1070 <p>
  401072:	89 d1                	mov    %edx,%ecx
  401074:	31 c1                	xor    %eax,%ecx
  401076:	48 8b 45 f8          	mov    -0x8(%rbp),%rax
  40107a:	48 83 c0 04          	add    $0x4,%rax
  40107e:	8b 10                	mov    (%rax),%edx
  401080:	8b 05 ee ff 2b 00    	mov    0x2bffee(%rip),%eax        # 6c1074 <p+0x4>
  401086:	31 d0                	xor    %edx,%eax
  401088:	01 c8                	add    %ecx,%eax
  40108a:	5d                   	pop    %rbp
  40108b:	c3                   	retq
    
Listado 2. Desensamblado de la funcion check_pass

ABI Linux 64bits
En sistemas Linux de 64bits los parámetros de las funciones se pasan en registros:
Par1 -> RDI
Par2 -> RSI
Par3 -> RDX
Par4 -> RCX
...
Retorno -> RAX
Es una función bastante corta así que pongámonos manos a la obra. El parámetro que recibe en el registro RDI lo almacena en una variable local en la pila (RBP -0x08), para a continuación copiar en el registro RAX ese valor.

Acto seguido, almacena en EDX el contenido de la posición de memoria apuntada for RAX. Vamos que lo que se le pasa a la función es un puntero a unos ciertos datos en memoria.

A continuación, leemos en RAX el contenido de una determinada posición de memoria. Si compruebas el puntero que nos muestra objdump con la lista de secciones que extrajimos más arriba, verás que este dato que estamos leyendo en RAX se encuentra en el segmento de datos del programa...

Comprobando la variable global

Quizás la forma más sencilla de comprobar el contenido de esas variables globales es abrir el programa con gdb o radare2, pero a modo didáctico, y para familiarizarnos con el formato ELF, vamos a hacerlo a pelo.

Así que queremos saber que valor está almacenado en las direcciónes 0x6c1070 y 0x6c1074. Bien, lo primero que debemos averiguar es en que sección se encuentran estos valores para poder determinar el desplazamiento en el fichero donde encontrarlos. Si examinamos la salida de readelf, veremos que esas direcciones se encuentran en la sección /data (como ya nos imaginábamos :).

$readelf -S desafio
(...)
  [24] .data             PROGBITS         00000000006c1060  000c1060
       0000000000001c10  0000000000000000  WA       0     0     32
(...)
  
FIGURA 5. Identificando la posición del segmento de datos

Como podemos ver, esta sección se encuentra en el offset 0x000c1060 dentro del fichero y tiene un tamaño de 0x1c10. Así que vamos a ver que hay en los 8 bytes que comprueba la función check_pass.

$  echo "ibase=16;C1070" | bc
790640
$  dd if=desafio skip=790640 bs=1 count=8 2> /dev/null | xxd -p
feca000070c10000
  

Parece que el programa está almacenando valores de 32 bits en un array de enteros que por defecto serán de 64 bits (para una plataforma de 64 bits como la que nos ocupa). Por esa razón la parte alta de los dos valores es zero... Si la eliminamos y reordenamos los bytes para una plataforma extremista menor (Little Endian) como es Intel... lo que obtenemos es:

cafec170

Hurra!. Así que la clave es CAFECITO, pero escrito en plan 31173. Hora de probar la clave.

Se fué

Bien, si has ejecutado el programa y has introducido la clave, habrás visto el mensaje, pero este habrá desaparecido en unos poco segundos.. y junto con él también ha desaparecido el programa... Afortunadamente somos unos máquinas y estábamos trabajando con una copia de nuestro espécimen... verdad?, verdad?...

Así que recuperamos nuestro programa y vamos con el plan B... una captura de pantalla. Sólo tenemos que volver a ejecutar el programa, introducir la clave que ya sabemos y ser rápidos pulsando la tecla PrintScrn...Maldita sea, ahora el programa dice que el mensaje ya ha sido leído y no nos deja ni introducir la clave... y además se borra cada vez que lo ejecutamos.

En este caso tenemos dos opciones:

  • Hacer una análisis dinámico... vamos mientras el programa se ejecuta
  • O continuar la ingeniería inversa de todo el código
Vamos primero con el análisis dinámico que es más fácil.

Straceando

Para el análisis dinámico podemos cargar el programa en nuestro depurador favorito y empezar a ejecutarlo paso a paso. El hecho de que el programa es capaz de detectar que ya a sido ejecutado, significa que está almacenando en algún lugar cierta información. Esa información puede estar almacenada, fundamentalmente, en 3 posibles escondites:
  • En memoria. Para comprobar esto lo más sencillo es reiniciar la máquina (porque estáis haciendo todo esto en una máquina virtual verdad?). Si tras el reboot el programa se puede ejecutar de nuevo, estaríamos en este caso.
  • En la red. Por ejemplo el programa podría estar conectándose a algún servidor. Para comprobar esto podríamos utilizar un sniffer y ver si el programa transmite algo.
  • En el disco. En este caso un reboot no cambiaría la situación y el sniffer no mostraría nada.
Bueno, como hemos dicho, podéis utilizar el depurador o podéis utilizar strace. En general, reservar memoria, crear conexiones de red o acceder al disco son tareas que se llevan a cabo a través de llamadas al sistema... y en esos casos, strace es nuestro colega.

$  strace ./desafio 2> traza
$  cat traza
execve("./desafio", ["./desafio"], [/* 39 vars */]) = 0
uname({sys="Linux", node="razor", ...}) = 0
brk(0)                                  = 0x1464000
brk(0x14651c0)                          = 0x14651c0
arch_prctl(ARCH_SET_FS, 0x1464880)      = 0
readlink("/proc/self/exe", "/tmp/desafio", 4096) = 17
brk(0x14861c0)                          = 0x14861c0
brk(0x1487000)                          = 0x1487000
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
mkdir("/home/occam/.ims", 0700)           = -1 EEXIST (File exists)
stat("/home/occam/.ims/fc3ab4ca", {st_mode=S_IFDIR|0700, st_size=4096, ...}) = 0
fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 12), ...}) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f1e06102000
write(1, "Este mensaje ya ha sido le\303\255do\n", 31) = 31
mkdir("/home/occam/.ims/fc3ab4ca", 0700)  = -1 EEXIST (File exists)
rt_sigprocmask(SIG_BLOCK, [CHLD], [], 8) = 0
rt_sigaction(SIGCHLD, NULL, {SIG_DFL, [], 0}, 8) = 0
rt_sigprocmask(SIG_SETMASK, [], NULL, 8) = 0
nanosleep({3, 0}, 0x7ffc9698b800)       = 0
unlink("./desafio.ORIG")                = 0
write(1, "\33[H\33[J", 6)               = 6
exit_group(1)                           = ?
FIGURA 6. Salida del comando strace

Lo veis?... Claro que sí. El programa crea un directorio oculto llamado (.ims) en el directorio HOME del usuario y comprueba si existe un fichero llamado fc3ab4ca. Vamos a borrar el directorio y probar de nuevo... listos para pulsar PrintScrn!!!

Reverseando

Si lo que nos gusta es obtener el mensaje utilizando ingeniería inversa a tope, tenemos de nuevo dos opciones. La primera es analizar la función s1 que se ejecuta al principio del programa... seguro que te fijaste en ella hace ya algún rato. Siguiendo ese camino terminéis encontrando el fichero oculto y llegaríais al mismo punto al que llegamos con strace.

La otra opción es tratar de extraer el mensaje directamente del fichero. Para ello vamos a ver que es lo que hace el programa justo después de comprobar la clave.

(...)
  40133b:	bf 77 43 49 00       	mov    $0x494377,%edi
  401340:	b8 00 00 00 00       	mov    $0x0,%eax
  401345:	e8 66 6e 00 00       	callq  4081b0 <_IO_printf>
  40134a:	bf f8 43 49 00       	mov    $0x4943f8,%edi
  40134f:	e8 3c 79 00 00       	callq  408c90 <_IO_puts>
  401354:	8b 15 3e fd 2b 00    	mov    0x2bfd3e(%rip),%edx        # 6c1098 <_ms>
  40135a:	48 8b 35 27 fd 2b 00 	mov    0x2bfd27(%rip),%rsi        # 6c1088 <_k>
  401361:	48 8b 05 28 fd 2b 00 	mov    0x2bfd28(%rip),%rax        # 6c1090 <_m>
  401368:	b9 04 00 00 00       	mov    $0x4,%ecx
  40136d:	48 89 c7             	mov    %rax,%rdi
  401370:	e8 17 fd ff ff       	callq  40108c <print_xor>
  401375:	bf 30 44 49 00       	mov    $0x494430,%edi
  40137a:	e8 11 79 00 00       	callq  408c90 <_IO_puts>
  40137f:	bf 68 44 49 00       	mov    $0x494468,%edi
  401384:	e8 07 79 00 00       	callq  408c90 <_IO_puts>
  401389:	bf 05 00 00 00       	mov    $0x5,%edi
  40138e:	e8 e4 fe ff ff       	callq  401277 <destroy>
  401393:	eb 0a                	jmp    40139f <main+0xd7>
(...)
LISTADO 3. La parte que nos interesa.

Bueno, podemos ver algunos printfs y puts, pero la función que nos interesa es, obviamente print_xor. Como dijimos al principio hemos tenido suerte ya que el programa conserva todos los símbolos :). Y además el programador ha utilizado nombres bastante descriptivos para ayudarnos en nuestra titánica tarea.

Del desensamblado del Listado 3 podemos deducir que la función recibe 4 parámetros... veámos como se utilizan.

Analizando print_xor

Bien, la función print_xor es algo tal que asín:

000000000040108c <print_xor>:
  40108c:	55                   	push   %rbp
  40108d:	48 89 e5             	mov    %rsp,%rbp
  401090:	48 83 ec 30          	sub    $0x30,%rsp
  401094:	48 89 7d e8          	mov    %rdi,-0x18(%rbp)
  401098:	48 89 75 e0          	mov    %rsi,-0x20(%rbp)
  40109c:	89 55 dc             	mov    %edx,-0x24(%rbp)
  40109f:	89 4d d8             	mov    %ecx,-0x28(%rbp)
  4010a2:	c7 45 f8 00 00 00 00 	movl   $0x0,-0x8(%rbp)
  4010a9:	c7 45 fc 00 00 00 00 	movl   $0x0,-0x4(%rbp)
  4010b0:	eb 4b                	jmp    4010fd <print_xor+0x71>
  4010b2:	8b 45 f8             	mov    -0x8(%rbp),%eax
  4010b5:	48 63 d0             	movslq %eax,%rdx
  4010b8:	48 8b 45 e8          	mov    -0x18(%rbp),%rax
  4010bc:	48 01 d0             	add    %rdx,%rax
  4010bf:	0f b6 08             	movzbl (%rax),%ecx
  4010c2:	8b 45 fc             	mov    -0x4(%rbp),%eax
  4010c5:	99                   	cltd
  4010c6:	f7 7d d8             	idivl  -0x28(%rbp)
  4010c9:	89 d0                	mov    %edx,%eax
  4010cb:	48 63 d0             	movslq %eax,%rdx
  4010ce:	48 8b 45 e0          	mov    -0x20(%rbp),%rax
  4010d2:	48 01 d0             	add    %rdx,%rax
  4010d5:	0f b6 00             	movzbl (%rax),%eax
  4010d8:	31 c8                	xor    %ecx,%eax
  4010da:	88 45 f7             	mov    %al,-0x9(%rbp)
  4010dd:	0f b6 45 f7          	movzbl -0x9(%rbp),%eax
  4010e1:	89 c7                	mov    %eax,%edi
  4010e3:	e8 88 7d 00 00       	callq  408e70 <putchar>
  4010e8:	80 7d f7 0a          	cmpb   $0xa,-0x9(%rbp)
  4010ec:	75 07                	jne    4010f5 <print_xor+0x69>
  4010ee:	c7 45 fc ff ff ff ff 	movl   $0xffffffff,-0x4(%rbp)
  4010f5:	83 45 f8 01          	addl   $0x1,-0x8(%rbp)
  4010f9:	83 45 fc 01          	addl   $0x1,-0x4(%rbp)
  4010fd:	8b 45 f8             	mov    -0x8(%rbp),%eax
  401100:	3b 45 dc             	cmp    -0x24(%rbp),%eax
  401103:	7c ad                	jl     4010b2 <print_xor+0x26>
  401105:	bf 0a 00 00 00       	mov    $0xa,%edi
  40110a:	e8 61 7d 00 00       	callq  408e70 <putchar>
  40110f:	c9                   	leaveq
  401110:	c3                   	retq
LISTADO 4. Función print_xor

Empecemos identificando nuestras variables locales. Ya que se trata de un binario de 64bits, la función se llama de la siguiente forma (esto ya os lo contamos un poco más arriba):

print_xor (rdi, rsi, rdx, ecx)

Si utilizamos los símbolos que objdump nos muestra de la función main (Listado 3), la llamada a la función sería algo como esto:

print_xor (_m, _k, _ms, 4)

Ahora identifiquemos nuestras variables locales. Al principio de la función vemos que se reserva espacio en la pila, exactamente 0x30 bytes. Esto se consigue restando ese valor al puntero de pila... como la pila crece hacía abajo (direcciones bajas), al menos en los procesadores Intel, lo que el comando sub rsp, 0x30 consigue es hacer hueco en la pila para almacenar las variables locales, preservando cualquier cosa que se haya almacenado anteriormente.

A continuación nos encontramos un fragmento de código que almacena estos parámetros en variables locales. Aquí lo tenéis con algunas anotaciones

  401094:	mov    %rdi,-0x18(%rbp)  -> var1 = rdi = _m  (64bits)
  401098:	mov    %rsi,-0x20(%rbp)  -> var2 = rsi = _k  (64bits)
  40109c:	mov    %edx,-0x24(%rbp)  -> var3 = edx = _ms (32bits)
  40109f:	mov    %ecx,-0x28(%rbp)  -> var4 = ecx = 4  (32bits)
  4010a2:	movl   $0x0,-0x8(%rbp)   -> var5 = 0
  4010a9:	movl   $0x0,-0x4(%rbp)   -> var6 = 0
  4010b0:	jmp    4010fd <print_xor+0x71>
LISTADO 5. Asignación de variables locales

Por el momento esos son los mejores nombre que le podemos dar a nuestras variables locales. Intentaremos refinarlos un poco según vayamos avanzando.

Justo después de la inicialización de las variables locales nos encontramos un salto incondicional... muchas veces esto significa el inicio de un bucle... Vamos a ver si este es el caso.

El bucle

Vamos a limpiar un poquillo el código para intentar ver el bucle. Para ello nos quedaremos, por un momento, con las partes de print_xor relevantes (las que se encuentran al principio y al final del bloque principal), y añadiremos algunas etiquetas para poder verlo todo mucho más fácilmente:

  4010b0:	jmp    b0 <4010fd>
  4010b2: b1:	mov    -0x8(%rbp),%eax    => var5 = eax
(...)
  4010f5: b2:	addl   $0x1,-0x8(%rbp)    -> var5++;
  4010f9:	addl   $0x1,-0x4(%rbp)    -> var6++;
  4010fd: b0:	mov    -0x8(%rbp),%eax    -> eax = var5
  401100:	cmp    -0x24(%rbp),%eax   -> compara var5 con _ms
  401103:	jl     b1  <4010b2>       => if var5 < _ms goto b1

LISTADO 6. El bucle de print_xor

Del ensamblador del Listado 6 podemos inferir que var5 y var6 podrían ser contadores de un bucle (ya que se incrementan en una unidad en cada iteración). También vemos que el bucle principal, se ejecuta mientras que var5 sea menor que var3. Así, si llamamos i a var5 y llamamos j a var6, el código de arriba se podría traducir por:

for (i=0; i < _ms; i++, j++) { ...}
LISTADO 7. Bucle principal en C

Ahora tenemos que ver que es lo que hace este bucle.

Desensamblando el cuerpo del bucle

Hora de ver que es lo que hace esta función print_xor. Echemos un ojo y añadamos nuestras primeras anotaciones.

for (; i < _ms; i++, j++) {
  4010b2:	mov    -0x8(%rbp),%eax     -> eax = i
  4010b5:	movslq %eax,%rdx           -> rdx = i
  4010b8:	mov    -0x18(%rbp),%rax    -> rax = _m
  4010bc:	add    %rdx,%rax           -> rax = rax + rdx = _m + i 
  4010bf:	movzbl (%rax),%ecx         -> ecx = *(_m + i) = _m[i]

  4010c2:	mov    -0x4(%rbp),%eax     -> eax = j
  4010c5:	cltd                       -> eax -> edx/eax
  4010c6:	idivl  -0x28(%rbp)         -> edx:eax / 4 = j /4 (ax=dividendo dx=resto
  4010c9:	mov    %edx,%eax           -> eax = j % 4
  4010cb:	movslq %eax,%rdx           -> rdx = eax = j % 4
  4010ce:	mov    -0x20(%rbp),%rax    -> rax = _k
  4010d2:	add    %rdx,%rax           -> rdx + rax = _k + (j % 4)
  4010d5:	movzbl (%rax),%eax         -> eax = *(_k + (j %4)) = _k[j%4]

  4010d8:	xor    %ecx,%eax           -> eax = _m[i] ^ _k[j%4]
  4010da:	mov    %al,-0x9(%rbp)      -> var7 = al = (_m[i] ^k[j%4]) %0xff
  4010dd:	movzbl -0x9(%rbp),%eax     -> eax = var7
  4010e1:	mov    %eax,%edi           -> edi = var7
  4010e3:	callq  408e70 <putchar>    -> putchar (var7)

  4010e8:	cmpb   $0xa,-0x9(%rbp)     -> if (var7 == 0x0a)
  4010ec:	jne    b2 <4010f5>         -> continue 
  4010ee:	movl   $0xffffffff,-0x4(%rbp)-> else j = -1
}
LISTADO 8. Cuerpo del bucle de print_xor

Bueno... con las anotaciones de arriba la cosa es bastante evidente ahora no?. De todas formas vamos a aclarar un par de cosillas para los lectores menos familiarizados con el lenguaje ensamblador.

La primera parte del programa no tiene mucho que comentar, simplemente accede a las posiciones de memoria apuntadas por el parámetro _m, que hemos pasado a la función desde el programa principal. El acceso se hace en base al contador del bucle. Así, en cada iteración almacenaremos en el registro ecx el valor de _m[i].

El siguiente bloque utiliza algunas instrucciones nuevas. La primera que nos encontramos es ctld. Lo que hace esta instrucción es expandir el valor de 32bits almacenado en eax en un valor de 64 bits que se almacenara en los registros edx:eax. Lo que hace la instrucción es poner ceros en edx si el valor de eax es positivo, o unos si eax es negativo... de forma que el valor extendido siga siendo negativo. Echadle un ojo a esta página para más detalles.

El caso es que esto nos viene muy bien para poder usar la instrucción de división idivl que, casualmente divide el valor almacenado en edx:eax por el número que indica el argumento. Como podéis ver en las anotaciones del código, lo que el programa está haciendo es indexar las posiciones de memoria apuntadas por el parámetro _k pero módulo 4. Si recordáis, nuestra clave era de 4 bytes, así que, lo que el programa hace, es recorrer una y otra vez cada uno de los valores de la clave y volver al principio cuando se le acaben los datos.

Una vez que el programa ha obtenido los dos valores (_m[i] y k[j % 4]), hace un xor y los almacena en una variable local. Efectivamente, esta variable local no ha sido inicializada y por eso no ha hemos visto antes.

Los mov y movzx que encontramos a continuación lo que hacen es convertir el caracter almacenado en la variable local, el que acabamos de calcular, en un entero (una palabra), para poder llamar a la función putchar... os preguntáis porqué?... es un buen momento para consultar el manual (man putchar).

Tras imprimir el caracter, la función comprueba si acaba de escribir un retorno de carro (0x0a = \n), y si es así, resetea el segundo contador, el utilizado para recorrer la clave. En otras palabras, cada línea del mensaje resetea el password. Para ello, pone el contador a -1, pero, inmediatamente es incrementado por el bucle principal con lo que nuestro nuevo índice en la clave para la siguiente iteracción es 0.

Así que, con todo esto, el código C original de la función print_xor debería ser algo como esto:

print_xor (_m, _k, _ms, 4)
{
   int i, j;
   char a

  for (i =0, j = 0; i < _ms; i++, j++)
  {
    a = _m[i] ^ _k[j % 4];
    putchar ((int)a);
    if (a == '\n') j = -1;
  }
}
LISTADO 9. La funcion print_xor descompilada!

Buf!... el típico codificador XOR.

Extrayendo los datos

Al fin tenemos todos los elementos, incluida la función de decodificación, ya solo nos queda extraer los datos del fichero y pasarlos por nuestra función. Ya sabemos como sacar datos del segmento de datos (si no te acuerdas, revisa el principio del artículo). Recordemos cuales son los datos que nos interesan, echándole de nuevo un vistazo a main.

  401354:	mov    0x2bfd3e(%rip),%edx        # 6c1098 <_ms>
  40135a:	mov    0x2bfd27(%rip),%rsi        # 6c1088 <_k>
  401361:	mov    0x2bfd28(%rip),%rax        # 6c1090 <_m>
  401368:	mov    $0x4,%ecx
  40136d:	mov    %rax,%rdi
  401370:	callq  40108c <print_xor>
LISTADO 10. Detalle de la llamada a print_xor

Veamos como extraer esa información. Empecemos con la longitud del mensaje, el parámetro _ms según nuestra descompilación manual.

$  echo "ibase=16;C1098" | bc        
790680
$  dd if=desafio skip=790680 bs=1 count=4 2> /dev/null | xxd -p
50010000
-> 00000150 -> (336 bytes)
FIGURA 7. Extrayendo el tamaño del mensaje

Como podéis ver, como nuestro procesador es extremista menor, tenemos que reordenar los bytes de cara palabra. De esta forma, ya sabemos que el tamaño del mensaje es de 336 bytes!. (Quizás haya una forma de decirle a xxd que nos saque el valor que queremos directamente... si alguien sabe como hacerlo que nos envíe una nota :))

Continuemos con la clave. Parámetro _k de nuestra función almacenado en 0x6c1088.

$  echo "ibase=16;C1088" | bc        
790664
$  dd if=desafio skip=790664 bs=1 count=4 2> /dev/null | xxd -p
e8414900
-> 004941e8
$  echo "ibase=16;941E8" | bc
606696
$  dd if=desafio skip=606696 bs=1 count=4 2> /dev/null | xxd -p
c01a10c4
FIGURA 8. Extrayendo la clave de cifrado del mensaje

La clave _k es un puntero. El puntero se almacena en la sección .data (0x06c1088), pero el valor al que apunta se encuentra en la sección .rodata (0x04941e8). Por esa razón tenemos que extraer dos valores. Primero la posición del puntero, para conocer su valor, y segundo la posición real de los datos.

Dicho esto, podemos ver que la clave es ColaLoca :)

Ahora, de la misma forma, extraeremos el mensaje para descodificarlo.

$  echo "ibase=16;C1090" | bc        
790672
$  dd if=desafio skip=790672 bs=1 count=4 2> /dev/null | xxd -p
f0414900
-> 004941f0 
$  echo "ibase=16;941F0" | bc
606704
$  dd if=desafio skip=606704 bs=1 count=336 2> /dev/null > msg.dump
  
FIGURA 9. Extrayendo el mensaje a decodificar

Listos para decodificar el mensaje!

Decodificando el mensaje

Aquí podemos utilizar el método que más rabia nos dé. Ya tenemos el código C, así que podríamos escribir un pequeño programa para leer nuestro dump y decodificarlo. Si vamos a escribir un decodificador en C, lo mejor es utilizar el flag -i de xxd.

$ dd if=desafio skip=606704 bs=1 count=336 2> /dev/null | xxd -i
(volcado hexadecimal)
  

Si lo preferís podéis utilizar vuestro lenguaje de script preferido (Perl, Python, Lua, ...). En ese caso, probablemente os resulte más práctico volcar el mensaje con algo como esto.

$  dd if=desafio skip=606704 bs=1 count=336 2> /dev/null | xxd -p | sed -e 's/\([0-9a-f][0-9a-f]\)/\\x\1/g' | tr -d '\n'
(volcado hexadecimal aquí.
  
FIGURA 10. Generando una cadena hexadecimal para incluir en nuestro programa

Ahora lo podemos pegar como una cadena de caracteres en un programilla C como este:

#include <stdio.h>

/* Información obtenida del fichero */
unsigned char *msg={(volcado hexadecimal que acabamos de obtener)};
int size = 336;
unsigned char *key="\xc0\x1a\x10\xc4";

void
print_xor (unsigned char *_m, unsigned char *_k, int _ms)
{
  int i, j;
  unsigned char a;
    
  for (i =0, j = 0; i < _ms; i++, j++)
    {
      a = _m[i] ^ _k[j % 4];
      putchar ((int)a);
      if (a == '\n') j = -1;
    }
}

int main ()
{
  print_xor (msg, key, 336);
}
  
LISTADO 11. Programa decodificador de mensajes

Compilar, ejecutar.... y decodificar. Ahí esta nuestro mensaje secreto, que por supuesto no vamos a poner aquí. Si quieres leerlo al menos tendrás que seguir parte de este artículo :P.

Conclusiones

Bueno, hemos destripado completamente el desafío del número anterior, mientras aprendíamos algunas cosillas.

Hemos visto como utilizando una clave en hexadecimal ya no podemos verla directamente con el comando strings. También hemos jugueteado con el formato ELF, extrayendo información directamente del fichero en lugar de cargar el programa en un depurador. Hemos visto algunas instrucciones de ensamblador un poco menos comunes y como utilizando herramientas como strace nos podemos ahorrar bastante trabajo manual.

Estad atentos a la siguiente entrega con la que profundizaremos un poquillo más en el inquietante mundo de la ingeniería inversa.

Header Image Credits: Samuel Clara

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/

 
Tu publicidad aquí :)