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.

binario en funcionamiento

Probando lo básico

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:

balioztau anotada

anotaciones de balioztau

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 😉

Anuncios
Tagged with: ,
Publicado en Reverse, wargame
7 comments on “Solución al Gipuzkoa Encounter 2010 Hack-It! (ELF)
  1. vierito5 dice:

    Aquí la función balioztau en C:
    http://pastebin.com/02kgzy1Q

    • Adrián dice:

      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 🙂

  2. vierito5 dice:

    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

  3. Adrián dice:

    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:

  4. vierito5 dice:

    Sí, así está claro que sale, el tema es que gdb seguro que tiene alguna forma de verlo pero la desconozco 😦

    Saludos!

  5. Civan dice:

    “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

  6. Adrián dice:

    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!

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s

Archive
A %d blogueros les gusta esto: