Solución al Gipuzkoa Encounter 2010 Hack-It! (ELF)
El pasado fin de semana, coincidiendo con la RootedCon, se celebraba en Guipúzcoa este encuentro anual que, entre los eventos que presenta incluye un pequeño CTF. Parte de ese CTF consiste en obtener el password para autenticarse en dos binarios, un ELF y un PE. Estos binarios fueron creados por la gente de morenops.com, y podéis descargarlos desde aquí. No os recomiendo que leais los comentarios porque dan muchas pistas sobre la solución.
AVISO: Voy a poner la solución al reto paso a paso, tratando de que sea un solucionario completo para que tanto la gente con pocos conocimientos del tema como los más expertos puedan seguirlo. No obstante, lo ideal es que trates de resolverlo por tu cuenta, y que si te atascas continues leyendo. Dicho queda.
Método para el ELF
En primer lugar decir que esta es sólo una posible solución, existen otras formas de solucionar este reto, y estaré encantado de que las dejéis en los comentarios para dar con la mejor solución. Tras descargar el binario y darle permisos de ejecución, podemos tratar de obtener algo de información acerca del mismo con la orden file.
adrian@Andromeda:~$ file tolosa2010_unix tolosa2010_unix: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), statically linked, for GNU/Linux 2.6.8, not stripped
Con esto ya sabemos que, puesto que está enlazado de manera estática, un ltrace del programa no nos dará ninguna información. Sin embargo, no está stripeado, por lo que es posible que encontremos símbolos de depuración en el programa. El siguiente paso es darle permisos de ejecución y ejecutarlo un par de veces para hacernos una idea inicial de cómo funciona.
Bueno, está claro que no vale cualquier clave, ni una clave en blanco, ni una clave demasiado larga. Así que nos va a tocar currar un poco más. Lo siguiente que a mí se me ocurre es utilizar strace para averiguar qué llamadas al sistema realiza y qué señales está recibiendo el programa.
adrian@Andromeda:~/pre$ strace ./tolosa2010_unix
execve("./tolosa2010_unix", ["./tolosa2010_unix"], [/* 35 vars */]) = 0
[ Process PID=5364 runs in 32 bit mode. ]
old_mmap(0xc40000, 4096, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0xc40000) = 0xc40000
readlink("/proc/self/exe", "/home/adrian/pre/tolosa2010_unix", 4096) = 32
old_mmap(0x8048000, 493385, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x8048000
mprotect(0x8048000, 493382, PROT_READ|PROT_EXEC) = 0
old_mmap(0x80c1000, 3843, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0x78000) = 0x80c1000
mprotect(0x80c1000, 3840, PROT_READ|PROT_WRITE) = 0
old_mmap(0x80c2000, 6764, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x80c2000
brk(0x80c4000) = 0x9f2a000
munmap(0xc01000, 262144) = 0
uname({sys="Linux", node="Andromeda", ...}) = 0
brk(0) = 0x9f2a000
brk(0x9f2acb0) = 0x9f2acb0
set_thread_area(0xffac203c) = 0
brk(0x9f4bcb0) = 0x9f4bcb0
brk(0x9f4c000) = 0x9f4c000
fstat64(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 0), ...}) = 0
mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xfffffffff7775000
write(1, "\n", 1
) = 1
write(1, "\t[Gipuzkoa Encounter 2010 - Hack"..., 38 [Gipuzkoa Encounter 2010 - Hack It!]
) = 38
write(1, "\n", 1
) = 1
ptrace(PTRACE_TRACEME, 0, 0x1, 0) = -1 EPERM (Operation not permitted)
write(1, "\tPassw0rd: \tomgz0r debugger? bye"..., 40 Passw0rd: omgz0r debugger? bye bye...
) = 40
exit_group(0) = ?
adrian@Andromeda:~/pre$
Sin embargo parece que por aquí tampoco llegamos a ninguna parte. Vemos que realiza alguna reserva de memoria, que llama a uname y a write para mostrar el mensaje de bienvenida. Después hace una llamada a ptrace y nos escribe un mensaje: “Passw0rd: omgz0r debugger? bye bye..”. Al tratar de tracear el programa y ya estar siendo traceado por nosotros, ptrace devuelve un error y el programa finaliza. Vamos a tratar de utilizar gdb para seguir un poco el funcionamiento interno del programa, con la idea de averiguar qué está pasando y de tratar de localizar la función que comprueba si una contraseña es válida o no.
adrian@Andromeda:~/pre$ gdb -q tolosa2010_unix Leyendo símbolos desde /home/adrian/pre/tolosa2010_unix...(no debugging symbols found)...hecho. (gdb) list No hay tabla de símbolos cargada. Use la orden "file". (gdb) br main No hay tabla de símbolos cargada. Use la orden "file". Make breakpoint pending on future shared library load? (y o [n]) y Punto de interrupción 1 (main) pendiente. (gdb) run Starting program: /home/adrian/pre/tolosa2010_unix [Gipuzkoa Encounter 2010 - Hack It!] Passw0rd: omgz0r debugger? bye bye... Program exited normally. (gdb) disass main No symbol table is loaded. Use the "file" command.
Otra cosa que falla. Está claro que al binario le pasa algo, ya que aparte de evitar la depuración, no nos encuentra el punto de entrada del binario en main. Llegados a este punto (los lectores avezados lo habrán visto antes) cabe pensar que el binario tiene algún tipo de cifrado o empaquetado. Volcaremos el binario, ya sea en hexadecimal con objdump o buscando solo las cadenas de texto con strings en busca de alguna pista como la que se ve a continuación:
adrian@Andromeda:~/pre$ hexdump -C tolosa2010_unix | head
00000000 7f 45 4c 46 01 01 01 03 00 00 00 00 00 00 00 00 |.ELF............|
00000010 02 00 03 00 01 00 00 00 60 f1 c3 00 34 00 00 00 |........`...4...|
00000020 00 00 00 00 00 00 00 00 34 00 20 00 02 00 28 00 |........4. ...(.|
00000030 00 00 00 00 01 00 00 00 00 00 00 00 00 10 c0 00 |................|
00000040 00 10 c0 00 68 e9 03 00 68 e9 03 00 05 00 00 00 |....h...h.......|
00000050 00 10 00 00 01 00 00 00 6c 0a 00 00 6c 3a 0c 08 |........l...l:..|
00000060 6c 3a 0c 08 00 00 00 00 00 00 00 00 06 00 00 00 |l:..............|
00000070 00 10 00 00 4d a8 73 e7 55 50 58 21 08 08 0d 0c |....M.s.UPX!....|
00000080 00 00 00 00 5a a9 08 00 5a a9 08 00 d4 00 00 00 |....Z...Z.......|
00000090 79 00 00 00 08 00 00 00 77 1f a4 f9 7f 45 4c 46 |y.......w....ELF|
adrian@Andromeda:~/pre$ strings tolosa2010_unix | head
UPX!
[]g;;
PTRh
=]<r
mnfM
F>wp
< wD
1]>ng
FzFM
{0c;]_V
Si nos fijamos bien, encontraremos la cadena UPX!, lo que nos indica que el binario ha sido empaquetado con esta utilidad. Esto ha sido fácil, en otras ocasiones habría sido necesario detectar en el binario el código que realiza el empaquetado y tratar de deshacerlo o identificar el empaquetador. Por lo tanto instalaremos el packer, bien desde su web, bien desde el sistema de paquetes de la distribución que utilicemos. Una vez instalado procederemos a desempaquetar el binario.
adrian@Andromeda:~/pre$ upx -d tolosa2010_unix Ultimate Packer for eXecutables Copyright (C) 1996,1997,1998,1999,2000,2001,2002,2003,2004,2005,2006,2007 UPX 3.01 Markus Oberhumer, Laszlo Molnar & John Reiser Jul 31st 2007 File size Ratio Format Name -------------------- ------ ----------- ----------- 567642 <- 256396 45.17% linux/elf386 tolosa2010_unix Unpacked 1 file.
Ahora que hemos desempaquetado el binario podemos retomar la idea de depurarlo y tratar de localizar el punto en el que verifica la clave. Vamos a ver qué sucede:
drian@Andromeda:~/pre$ gdb -q tolosa2010_unix Leyendo símbolos desde /home/adrian/pre/tolosa2010_unix...hecho. (gdb) br main Punto de interrupción 1 at 0x80482c9 (gdb) run Starting program: /home/adrian/pre/tolosa2010_unix Breakpoint 1, 0x080482c9 in main () Idioma actual: auto The current source language is "auto; currently asm". (gdb) list 1 /tmp/cc1PTP7g.s: No existe el fichero ó directorio. in /tmp/cc1PTP7g.s (gdb) n Single stepping until exit from function main, which has no line number information. [Gipuzkoa Encounter 2010 - Hack It!] Passw0rd: omgz0r debugger? bye bye...
Era de esperar que sucediera esto, pero ahora encuentra los símbolos, lo cual está bien, ya que nos va a permitir hacer esto:
(gdb) disass main Dump of assembler code for function main: 0x080482bb <main+0>: lea ecx,[esp+0x4] 0x080482bf <main+4>: and esp,0xfffffff0 0x080482c2 <main+7>: push DWORD PTR [ecx-0x4] 0x080482c5 <main+10>: push ebp 0x080482c6 <main+11>: mov ebp,esp 0x080482c8 <main+13>: push ecx 0x080482c9 <main+14>: sub esp,0x14 0x080482cc <main+17>: mov DWORD PTR [esp],0x80a4f08 0x080482d3 <main+24>: call 0x8048f70 <puts> 0x080482d8 <main+29>: mov DWORD PTR [esp],0x80a4f2f 0x080482df <main+36>: call 0x8048d80 <printf> 0x080482e4 <main+41>: call 0x8048273 <check> 0x080482e9 <main+46>: mov DWORD PTR [esp],0x80c3840 0x080482f0 <main+53>: call 0x8048db0 <gets> 0x080482f5 <main+58>: mov DWORD PTR [esp],0xa 0x080482fc <main+65>: call 0x8049110 <putchar> 0x08048301 <main+70>: mov DWORD PTR [esp],0x80c3840 0x08048308 <main+77>: call 0x8048210 <balioztau> 0x0804830d <main+82>: test eax,eax 0x0804830f <main+84>: je 0x804831f <main+100> 0x08048311 <main+86>: mov DWORD PTR [esp],0x80a4f3c 0x08048318 <main+93>: call 0x8048f70 <puts> 0x0804831d <main+98>: jmp 0x804832b <main+112> 0x0804831f <main+100>: mov DWORD PTR [esp],0x80a4f46 0x08048326 <main+107>: call 0x8048f70 <puts> 0x0804832b <main+112>: add esp,0x14 0x0804832e <main+115>: pop ecx 0x0804832f <main+116>: pop ebp 0x08048330 <main+117>: lea esp,[ecx-0x4] 0x08048333 <main+120>: ret End of assembler dump.
Al principio hace unas llamadas a printf y puts para mostrar el mensaje inicial en la terminal, y despúes (main+41) realiza una llamada a la función check. Tras esto, lee lo que le pasamos como clave (gets), imprime un salto de línea (putchar 0xa) y llama a una función “balioztau” con un argumento, que es donde ha almacenado lo que hemos tecleado. En función del resultado de esa función (main+84) saltará a nuestro FAIL o al mensaje de éxito. Todos los valores de memoria que aparecen contienen las cadenas que maneja el programa, podemos verlo rápidamente:
(gdb) x/s 0x80a4f08 0x80a4f08: "\n\t[Gipuzkoa Encounter 2010 - Hack It!]" (gdb) x/s 0x80a4f2f 0x80a4f2f: "\n\tPassw0rd: " (gdb) x/s 0x80c3840 0x80c3840 <pasahitza>: "" (gdb) x/s 0x80a4f3c 0x80a4f3c: "\tSuccess!" (gdb) x/s 0x80a4f46 0x80a4f46: "\t#EPIC FAIL"
Tal y como yo lo veo, una vez aquí tenemos dos opciones: tratar de saltarnos la protección antidepuración para poder examinar la memoria en ejecución y localizar el password válido, o desensamblar la función balioztau y comprender de manera estática qué comprobaciones hace sobre el password. Ambos métodos son muy interesantes, pero de momento expondremos el segundo, explicando además una técnica utilizada para la realización de keygens y para la ingeniería inversa en general. Veamos el código de la función balioztau:
(gdb) disass balioztau Dump of assembler code for function balioztau: 0x08048210 <balioztau+0>: push ebp 0x08048211 <balioztau+1>: mov ebp,esp 0x08048213 <balioztau+3>: sub esp,0x14 0x08048216 <balioztau+6>: mov DWORD PTR [ebp-0x4],0x0 0x0804821d <balioztau+13>: mov BYTE PTR ds:0x80c17de,0x0 0x08048224 <balioztau+20>: mov DWORD PTR [ebp-0x8],0x0 0x0804822b <balioztau+27>: jmp 0x8048261 <balioztau+81> 0x0804822d <balioztau+29>: mov edx,DWORD PTR [ebp-0x8] 0x08048230 <balioztau+32>: mov eax,edx 0x08048232 <balioztau+34>: shr eax,0x1f 0x08048235 <balioztau+37>: add eax,edx 0x08048237 <balioztau+39>: sar eax,1 0x08048239 <balioztau+41>: mov DWORD PTR [ebp-0x4],eax 0x0804823c <balioztau+44>: mov eax,DWORD PTR [ebp-0x8] 0x0804823f <balioztau+47>: movzx edx,BYTE PTR [eax+0x80c17c8] 0x08048246 <balioztau+54>: mov eax,DWORD PTR [ebp-0x4] 0x08048249 <balioztau+57>: movzx eax,BYTE PTR [eax+0x80c3840] 0x08048250 <balioztau+64>: cmp dl,al 0x08048252 <balioztau+66>: je 0x804825d <balioztau+77> 0x08048254 <balioztau+68>: mov DWORD PTR [ebp-0x14],0x0 0x0804825b <balioztau+75>: jmp 0x804826e <balioztau+94> 0x0804825d <balioztau+77>: add DWORD PTR [ebp-0x8],0x2 0x08048261 <balioztau+81>: cmp DWORD PTR [ebp-0x8],0x16 0x08048265 <balioztau+85>: jle 0x804822d <balioztau+29> 0x08048267 <balioztau+87>: mov DWORD PTR [ebp-0x14],0x1 0x0804826e <balioztau+94>: mov eax,DWORD PTR [ebp-0x14] 0x08048271 <balioztau+97>: leave 0x08048272 <balioztau+98>: ret End of assembler dump.
Con el código delante el proceso consiste en leerlo e ir anotándolo con comentarios hasta que se haya comprendido su funcionamiento. Lo primero será averiguar qué hay en la memoria en 0x80c17c8, puesto que en 0x80c3840 ya sabemos que se encuentra lo que hemos introducido en la terminal. Os recomiendo que tratéis de hacer vosotros la anotación antes de leer la mía. Tras un par de pasadas anotando deberíais tener unas anotaciones similares a estas:
Os las subo también como fichero adjunto, porque ponerlas en forma de código quedaba descolocado debido al tamaño de la caja de texto. Con esto debería ser evidente qué está realizando la función y cual es el password que debemos introducir. Aún así, cuando se trata de conseguir un keygen o de hacer más claro el problema (esta función no es especialmente complicada) se procede a traducir el código a C. En primer lugar una traducción directa del ensamblador, y finalmente se refactoriza el código para obtener algo más parecido a lo que escribiría un programador. La traducción directa a C desde ese código podría ser algo como esto (la función main ha sido añadida por comodidad y para que el programa sea usable):
#include <stdio.h>
#include <stdlib.h>
char pass_valido[20] = "t_0/l=0(s?@ah:40c7k#";
int balioztau(char* pass_usuario){
unsigned int var1,var2,ret;
unsigned char *pp,*pu;
var1=0;
var2=0;
ret = 0;
pp = pass_valido;
pu = pass_usuario;
do{
var1 = var1 >>> 31; // var1 = 0;
var1 = var2 + var1;
var1 = var2/2;
if (*(pu+var1) == *(pp+var2)){
var2 = var2 +2;
}else{
ret =0;
return ret;
}
} while (var2 <= 20);
ret = 1;
return ret;
}
int main(int argc, char* argv[]){
if (argc != 2){
printf("Uso: %s <password a comprobar>\n",argv[0]);
exit(0);
}
if (balioztau(argv[1])){
printf("WIN\n");
}else{
printf("FAIL\n");
}
return 0;
}
Aquí se ve mejor que el funcionamiento consiste en recorrer la cadena en memoria (pass_valido) de dos en dos e ir comparándola con lo que ha introducido el usuario (recorrido de uno en uno). Por lo tanto, el caracter en la posición 0 del usuario se validará contra el 0 de la password, el primero del usuario contra el segundo de la password, y así hasta el final. Por tanto la password para el programa es “t0l0s@h4ck“, como se muestra a continuación:
adrian@Andromeda:~/pre$ ./tolosa2010_unix [Gipuzkoa Encounter 2010 - Hack It!] Passw0rd: t0l0s@h4ck Success! adrian@Andromeda:~/pre$ ./a.out Uso: ./a.out <password a comprobar> adrian@Andromeda:~/pre$ ./a.out hola FAIL adrian@Andromeda:~/pre$ ./a.out t0l0s@h4ck WIN
Queda como tarea vuestra pasar el código C a algo más habitual, convertir el while en un for, eliminar las variables sobrantes, los desplazamientos extraños, etc. En otra entrada veremos cómo solucionar el binario de windows, y quizá cómo solucionar este mismo parcheando el binario. Si tenéis cualquier duda o aportación ya sabéis


Aquí la función balioztau en C:
http://pastebin.com/02kgzy1Q
Hola Vierito,
Así es como me quedaba a mí, pero me pica la curiosidad sobre como has obtenido los nombres de las variables. Por cierto, ¿has intentado el challenge de windows? A ver si saco un rato y lo intento
Con IDA puedes ver directamente los nombres de esas variables. La verdad es que con gdb te sale el offset donde están y punto xDD no sé cómo se hace para mirarlo con gdb.
El de windows lo tengo unpackeado, me puse 10 minutos y lo dejé. Me da pereza porque en windows me pierdo mucho más jeje
En gdb no sé, pero mirando un poco con Objdump podemos encontrar los nombres de las variables
(Si IDA lo saca es que en alguna parte está esa info):
adrian@Andromeda:~/Escritorio/gipuzkoa encounter$ objdump -D tolosa2010_unix | grep -i 080c3840
080c3840 pasahitza:
adrian@Andromeda:~/Escritorio/gipuzkoa encounter$ objdump -D tolosa2010_unix | grep -i 080c17c8
080c17c8 gakoa:
Sí, así está claro que sale, el tema es que gdb seguro que tiene alguna forma de verlo pero la desconozco
Saludos!
“info variables” desde la linea de gdb? no se si es eso lo que buscas, de todas formas si tienes el nombre o parte del nombre de una variable puedes añadir una palabra mas que actua como regexp para la busqueda.
Saludos
Hola Civan,
con ‘info variables’, según la ayuda:
Print the names and data types of all variables that are declared outside of functions (i.e. excluding local variables).
Así que para variables locales no nos serviría, pero hurgando un poco, ‘info symbol ADDR’ debería bastar:
Print the name of a symbol which is stored at the address addr. If no symbol is stored exactly at addr, GDB prints the nearest symbol and an offset from it.
Gracias por la pista!