domingo, 1 de mayo de 2011

WriteUp - Desafío 16 - H4ckc0nt3st GSIC 2011

Nos presentan un binario llamado ‘resiste’, a ver qué podemos hacer con él.


Si llamamos al binario sin parámetros o con más de uno, nos responde “¿Así, sin más?”, mientras que si ponemos solo un parámetro con cualquier valor tampoco le gusta demasiado y nos lo indica con un “:(”.


Abrimos el binario con el IDA y vemos que no tiene definida la función main, por lo que en su lugar nos lleva a start, que llama a __libc_start_main.



Vamos a sub_8048470 para observar el código llamado y vemos el siguiente esquema:



El programa descifra una sección del código en memoria y salta a él, por lo que sólo con análisis estático no vamos a poder hacer mucho y decidimos usar gdb.
Arrancamos con un parámetro para que el flujo del programa nos lleve hasta donde nos interesa:


# gdb -q --args ./resiste AAAAAAAAAA
(no debugging symbols found)

Con mis limitados conocimientos de gdb, suelo poner ‘start’ para cargar el programa y poder desensamblar el programa, pero haciéndolo en este caso lanza el programa completo, así que para evitarlo tengo que poner algún punto de ruptura. Como hemos visto antes una llamada a mmap() y está situada cerca del punto que nos interesa, la utilizo para fijar el breakpoint:


(gdb) b mmap
Function "mmap" not defined.
Make breakpoint pending on future shared library load? (y or [n]) y

Breakpoint 1 (mmap) pending.
(gdb) run
Starting program: /root/resiste AAAAAAAAAA
(no debugging symbols found)
(no debugging symbols found)
(no debugging symbols found)

Breakpoint 1, 0xb7ef3f80 in mmap () from /lib/tls/i686/cmov/libc.so.6

Comprobamos dónde nos encontramos para situarnos y poder fijar el siguiente punto de ruptura donde nos interese, en la zona de memoria desde la que se ha llamado a mmap():


(gdb) where
#0 0xb7ef3f80 in mmap () from /lib/tls/i686/cmov/libc.so.6
#1 0x080484ec in ?? ()
#2 0xb7e2c685 in __libc_start_main () from /lib/tls/i686/cmov/libc.so.6
#3 0x080483c1 in ?? ()

Intento obtener el código desensamblado con el comando que utilizo habitualmente para ello ‘disas’, pero no lo acepta por encontrase esa zona de memoria fuera de las funciones definidas, por lo que toca googlear un poco hasta localizar que podemos mostrar la memoria decodificándola como instrucciones, y este método sí nos funciona en este caso:


(gdb) disas 0x080484ec
No function contains specified address.
(gdb) x/20i 0x080484ec
0x80484ec: xor %edx,%edx
0x80484ee: cmp $0xffffffff,%eax
0x80484f1: je 0x8048576
0x80484f7: nop
0x80484f8: movzbl 0x80486c0(,%edx,4),%ecx
0x8048500: xor %edx,%ecx
0x8048502: mov %cl,(%eax,%edx,1)
0x8048505: add $0x1,%edx
0x8048508: cmp $0xbe,%edx
0x804850e: jne 0x80484f8
0x8048510: mov 0x4(%ebx),%edx
0x8048513: mov %eax,0x8049b84
0x8048518: mov %edx,(%esp)
0x804851b: call *%eax
0x804851d: test %eax,%eax
0x804851f: jne 0x8048547
0x8048521: mov 0x4(%ebx),%eax
0x8048524: movl $0x8048664,0x4(%esp)
0x804852c: mov %eax,0x8(%esp)
0x8048530: mov 0x8049b80,%eax

De esta forma ya podemos localizar la llamada al código desempaquetado (call *%eax) y fijamos un punto de ruptura en esa instrucción para poder entrar posteriormente en el método llamado.


(gdb) b *0x804851b
Breakpoint 2 at 0x804851b
(gdb) continue
Continuing.

Breakpoint 2, 0x0804851b in ?? ()
(gdb) stepi
0xb7f82000 in ?? ()

Ya estamos dentro del código desempaquetado y podemos ver el código desensamblado del método:


(gdb) x/70i 0xb7f82000
0xb7f82000: push %ebp
0xb7f82001: mov %esp,%ebp
0xb7f82003: sub $0x20,%esp
0xb7f82006: movb $0xf4,-0x17(%ebp)
0xb7f8200a: movb $0xee,-0x1f(%ebp)
0xb7f8200e: movb $0xcb,-0x20(%ebp)
0xb7f82012: movb $0xd3,-0x14(%ebp)
0xb7f82016: movb $0xce,-0xd(%ebp)
0xb7f8201a: movb $0xe2,-0x1d(%ebp)
0xb7f8201e: movb $0xc4,-0x1c(%ebp)
0xb7f82022: movb $0x0,-0x5(%ebp)
0xb7f82026: movb $0xec,-0x1e(%ebp)
0xb7f8202a: movb $0xe9,-0xc(%ebp)
0xb7f8202e: movb $0xe8,-0x15(%ebp)
0xb7f82032: movb $0xcf,-0xb(%ebp)
0xb7f82036: movb $0xe6,-0x1b(%ebp)
0xb7f8203a: movb $0xe8,-0x13(%ebp)
0xb7f8203e: movb $0xf4,-0x17(%ebp)
0xb7f82042: movb $0xee,-0x18(%ebp)
0xb7f82046: movb $0xc6,-0x12(%ebp)
0xb7f8204a: movb $0xe2,-0xa(%ebp)
0xb7f8204e: movb $0xeb,-0x1a(%ebp)
0xb7f82052: movb $0xeb,-0x19(%ebp)
0xb7f82056: movb $0xf3,-0x16(%ebp)
0xb7f8205a: movb $0xe6,-0x9(%ebp)
0xb7f8205e: movb $0xf1,-0x8(%ebp)
0xb7f82062: movb $0xf3,-0x10(%ebp)
0xb7f82066: movb $0xe6,-0xf(%ebp)
0xb7f8206a: movb $0xe2,-0x7(%ebp)
0xb7f8206e: movb $0xf5,-0xe(%ebp)
0xb7f82072: movb $0xd4,-0x11(%ebp)
0xb7f82076: movb $0xe9,-0x6(%ebp)
0xb7f8207a: push %ecx
0xb7f8207b: push %edi
0xb7f8207c: push %esi
0xb7f8207d: xor %eax,%eax
0xb7f8207f: mov $0xffffffff,%ecx
0xb7f82084: mov 0x8(%ebp),%edi
0xb7f82087: xor %eax,%eax
0xb7f82089: cld
0xb7f8208a: repnz scas %es:(%edi),%al
0xb7f8208c: not %ecx
0xb7f8208e: dec %ecx
0xb7f8208f: cmp $0x1b,%ecx
0xb7f82092: je 0xb7f8209b
0xb7f82094: mov $0xffffffff,%ecx
0xb7f82099: jmp 0xb7f820b7
0xb7f8209b: mov $0x1b,%ecx
0xb7f820a0: xor %eax,%eax
0xb7f820a2: lea -0x20(%ebp),%eax
0xb7f820a5: mov %eax,%edi
0xb7f820a7: mov 0x8(%ebp),%esi
0xb7f820aa: mov (%edi),%al
0xb7f820ac: xor (%esi),%al
0xb7f820ae: xor $0x87,%al
0xb7f820b0: jne 0xb7f820b7
0xb7f820b2: inc %edi
0xb7f820b3: inc %esi
0xb7f820b4: dec %ecx
0xb7f820b5: jne 0xb7f820aa
0xb7f820b7: mov %ecx,%eax
0xb7f820b9: pop %esi
0xb7f820ba: pop %edi
0xb7f820bb: pop %ecx
0xb7f820bc: leave
0xb7f820bd: ret
0xb7f820be: add %al,(%eax)
0xb7f820c0: add %al,(%eax)
0xb7f820c2: add %al,(%eax)
0xb7f820c4: add %al,(%eax)

Vemos cómo se colocan una serie de bytes en un array, en lo que probablemente sea la solución que necesitamos cifrada, pero en su momento no veíamos claro dónde o si se descifraba esta cadena, aunque sí parece que coge la cadena byte a byte y hace algún xor con cada byte, así que fijamos un breakpoint y recogemos la cadena cifrada de la memoria:


(gdb) b *0xb7f820b7
Breakpoint 3 at 0xb7f820b7
(gdb) continue
Continuing.

Breakpoint 3, 0xb7f820b7 in ?? ()
(gdb) x/32b $ebp-0x20
0xbfa37bd8: 0xcb 0xee 0xec 0xe2 0xc4 0xe6 0xeb 0xeb
0xbfa37be0: 0xee 0xf4 0xf3 0xe8 0xd3 0xe8 0xc6 0xd4
0xbfa37be8: 0xf3 0xe6 0xf5 0xce 0xe9 0xcf 0xe2 0xe6
0xbfa37bf0: 0xf1 0xe2 0xe9 0x00 0xc4 0x7c 0xa3 0xbf
(gdb)

Cogemos la cadena hasta el byte nulo y le aplicamos fuerza bruta buscando la respuesta que necesitamos mediante el script:


import hashlib
import binascii
from itertools import cycle, izip

def mixor (ss, key):
 key = cycle(key)
 return ''.join(chr(ord(x) ^ ord(y)) for (x,y) in izip(ss, key))

cadena='cbeeece2c4e6ebebeef4f3e8d3e8c6d4f3e6f5cee9cfe2e6f1e2e9'

for a in range(0,256):
 print str(a)+' '+mixor(binascii.unhexlify(cadena),chr(a))

Revisando la salida encontramos la respuesta, correspondiente al xor con 135.



Podemos comprobar que funciona introduciéndola como parámetro:


# ./resiste LikeCallistoToAStarInHeaven
Efectivamente, la clave es LikeCallistoToAStarInHeaven. Ahí te he visto fino yo ;)

Analizando posteriormente el código, comprobamos que la cadena no se descifra en memoria en ningún momento, sino que se hace un xor byte a byte con el parámetro introducido por el usuario, y posteriormente con 0x87(135). Dadas las propiedades de la operación xor, si la clave es correcta, el resultado será 0 para cada byte.


PD: Este es el último de los retos que resolví en este concurso, así que aprovecho para agradecérselo a los "culpables" del reto y unas excelentes jornadas... ¡¡Muchas gracias a Miguel Gesteiro y a la gente de GSI Coruña!!. Gracias también a la gente de HackPlayers y BatchDrake, que sé que aportaron pruebas. Si me he dejado a alguien no dudéis en decírmelo, por favor :-).

No hay comentarios:

Publicar un comentario