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:
Hurra!. Así que la clave es CAFECITO, pero escrito en plan 31173. Hora de probar la clave.
Analizando
Bien, la función
■
$ unzip -x roor-N02.pdf o $ unzip -x roor-N02.pdf roor-N02/desafio/desafioEl 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 strippedBien, 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 1De 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 binaryComo 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 retqBien, 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
ABI Linux 64bits
En sistemas Linux de 64bits los parámetros de las funciones se pasan en registros:
Es una función bastante corta así que pongámonos manos a la obra. El parámetro que recibe en el registro 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
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 congdb
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 (...)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 feca000070c10000Parece 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
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 teclaPrintScrn
...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
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.
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) = ?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óns1
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> (...)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 retqEmpecemos 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>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 deprint_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 b1Del 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++) { ...}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ónprint_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 }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; } }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>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)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 c01a10c4La 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.dumpListos 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í.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); }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 comandostrings
. 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
■
CLICKS: 3361