Ahora que me declaro un enamorado de
r2
(radare2), y que día a día intento mejorar mis habilidades con el mismo tanto en la faceta del análisis estático como en los enrevesados caminos del debugging, encontré por casualidad los desafíos (crackmes) presentados en la última r2con del 2017.
Entre ellos llamó especialmente mi atención lo que parecía ser un archivo ROM de la famosa consola GameBoy (aquí). Decídí probar suerte, comprobar si podía resolver este reto por mis propios medios, y averiguar cuán elevado había sido el nivel de la competición en esta convención que, tarde o temprano, se hará un nombre dentro de la historia de las CoN's.
El primer paso, por supuesto, es comprobar si efectivamente estamos tratando con un juego de la mencionada consola. Para ello debemos instalar un emulador capaz de cargar el archivo con extensión
.gb
. Después de instalar vba mediante apt en mi Linux box el resultado no fue el deseado, por lo que probé con otro famoso emulador llamado mednafen, también obtenido mediante el gestor de paquetes apt
. En la imágen vemos el resultado de invocar mednafen ./simple.gb
:
La finalidad del juego es muy simple, descubrir cuál es la secuencia o password correcto. Con las teclas
W
y S
modificamos los dígitos aumentándolos o disminuyéndolos. Con A
y D
nos desplazamos a izquierda y derecha entre las 5 posiciones del código. Probamos a introducir un valor arbitrario y pulsamos ENTER
:
Por los pelos... :)
Ya que de eso trata la competición, debemos hacer uso de radare2 para destripar el binario y analizar el algoritmo que genera la secuéncia de números correcta:
dpc@kernelinside:~$ r2 ./simple.gb
[0x00000100]> aaa
[x] Analyze all flags starting with sym. and entry0 (aa)
[ ]
[Value from 0x00000000 to 0x00008000
aav: 0x00000000-0x00008000 in 0x0-0x8000
[x] Analyze function calls (aac)
[x] Analyze len bytes of instructions for references (aar)
[x] Constructing a function name for fcn.* and sym.func.* functions (aan)
[x] Type matching analysis for all functions (afta)
[x] Use -AA or aaaa to perform additional experimental analysis.
[0x00000100]> iI
arch gb
binsz 32768
bits 16
canary false
crypto false
endian little
havecode true
linenum false
lsyms false
machine Gameboy
nx false
os any
pic false
relocs false
static true
stripped false
va true
Efectivamente
r2
identifica a simple.gb
como una imágen de GameBoy y nos recuerda que estamos tratando con una arquitectura de 16 bits. En este momento, y antes de entrar a desensamblar el binario, convendría buscar alguna documentación sobre programación en assembler para el microprocesador de esta consola. Se trata de un subset del famoso Z80, y realmente los mnemónicos se pueden entender sin necesidad de referencia alguna.
A la tarea. Ya que contamos con el mensaje de chico malo
"FAIL!"
, tratamos de localizarlo dentro del espacio de direcciones:
[0x00000000]> / FAIL!
Searching 5 bytes in [0x0-0x8000]
hits: 1
0x000002f3 hit20_0 .!fWIN!FAIL!6#66#6.
Esto se llama un dos por uno, ya tenemos chico bueno y chico malo. Otra opción era buscar todas las strings del binario:
[0x00000100]> izz
000 0x00000111 0x00000111 4 10 (rombank00) utf16le \f\rᄈ蠟 blocks=Basic Latin,Hangul Jamo,CJK Unified Ideographs
001 0x00000132 0x00000132 8 9 (rombank00) ascii 3>SIMPLE
...
010 0x000002ee 0x000002ee 4 5 (rombank00) ascii WIN!
011 0x000002f3 0x000002f3 5 6 (rombank00) ascii FAIL!
...
026 0x0000051e 0x0000051e 10 11 (rombank00) ascii 0123456789
...
090 0x0000107d 0x0000107d 16 17 (rombank00) ascii 0123456789ABCDEF
...
128 0x0000150e 0x0000150e 7 8 (rombank00) ascii \a\b\t\n\v\f\r
129 0x00001527 0x00001527 95 96 (rombank00) ascii !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~
...
He recortado el listado por contener hasta 260 resultados casi todos irrelevantes. En todo caso ahora ya podemos preguntar a
r2
quién hace referencia a nuestros mensajes. Las cross-references no funcionarán (es lo que tiene tratar con una plataforma marginal), pero nada nos impide buscar la dirección del string dentro del código ejecutable:
[0x00000100]> /x f302
Searching 2 bytes in [0x0-0x8000]
hits: 1
0x000002e5 hit0_0 f302
Una coincidencia. Desensamblamos la función que la contiene (con
e asm.bytes=false
dehabilitamos la impresión de opcodes para un resultado más legible):
[0x000002e5]> e asm.bytes=false
[0x000002e5]> pdf
/ (fcn) fcn.00000274 107
| fcn.00000274 ();
| ; var int local_2h @ sp+0x2
| ; CALL XREF from hit0_0 (+0x22f)
| 0x00000274 ld hl, sp + 0x02
| 0x00000276 ld e, [hl]
| 0x00000277 inc hl
| 0x00000278 ld d, [hl]
| 0x00000279 ld hl, 0x0004
| 0x0000027c add hl, de
| 0x0000027d ld c, l
| 0x0000027e ld b, h
| 0x0000027f ld a, [bc]
| 0x00000280 ld c, a
| 0x00000281 cp 0x03
| ,=< 0x00000283 jp nZ, 0x02e4
| ,==< 0x00000286 jr 0x03
..
| ||| ; CODE XREF from fcn.00000274 (0x286)
| |`--> 0x0000028b ld hl, sp + 0x02
| | | 0x0000028d ld c, [hl]
| | | 0x0000028e inc hl
| | | 0x0000028f ld b, [hl]
| | | 0x00000290 inc bc
| | | 0x00000291 inc bc
| | | 0x00000292 ld a, [bc]
| | | 0x00000293 ld c, a
| | | 0x00000294 cp 0x07
| |,==< 0x00000296 jp nZ, 0x02e4
| ,====< 0x00000299 jr 0x03
..
| ||||| ; CODE XREF from fcn.00000274 (0x299)
| |`----> 0x0000029e ld hl, sp + 0x02
| | ||| 0x000002a0 ld c, [hl]
| | ||| 0x000002a1 inc hl
| | ||| 0x000002a2 ld b, [hl]
| | ||| 0x000002a3 inc bc
| | ||| 0x000002a4 ld a, [bc]
| | ||| 0x000002a5 ld c, a
| | ||| 0x000002a6 cp 0x05
| |,====< 0x000002a8 jp nZ, 0x02e4
| ,======< 0x000002ab jr 0x03
..
| ||||||| ; CODE XREF from fcn.00000274 (0x2ab)
| |`------> 0x000002b0 ld hl, sp + 0x02
| | ||||| 0x000002b2 ld e, [hl]
| | ||||| 0x000002b3 inc hl
| | ||||| 0x000002b4 ld d, [hl]
| | ||||| 0x000002b5 ld hl, 0x0003
| | ||||| 0x000002b8 add hl, de
| | ||||| 0x000002b9 ld c, l
| | ||||| 0x000002ba ld b, h
| | ||||| 0x000002bb ld a, [bc]
| | ||||| 0x000002bc ld c, a
| | ||||| 0x000002bd cp 0x01
| |,======< 0x000002bf jp nZ, 0x02e4
| ========< 0x000002c2 jr 0x03
..
| ||||||| ; CODE XREF from fcn.00000274 (0x2c2)
| --------> 0x000002c7 ld hl, sp + 0x02
| ||||||| 0x000002c9 ld c, [hl]
| ||||||| 0x000002ca inc hl
| ||||||| 0x000002cb ld b, [hl]
| ||||||| 0x000002cc ld a, [bc]
| ||||||| 0x000002cd ld c, a
| ||||||| 0x000002ce cp 0x09
| ========< 0x000002d0 jp nZ, 0x02e4
| ========< 0x000002d3 jr 0x03
..
| ||||||| ; CODE XREF from fcn.00000274 (0x2d3)
| --------> 0x000002d8 ld hl, 0x02ee
| ||||||| 0x000002db push hl
| ||||||| 0x000002dc call fcn.00000f66
| ||||||| 0x000002df add sp, 0x02
| ========< 0x000002e1 jp 0x02ed
| ||||||| ; XREFS: CODE 0x00000283 CODE 0x00000288 CODE 0x00000296 CODE 0x0000029b
| ||||||| ; XREFS: CODE 0x000002a8 CODE 0x000002ad CODE 0x000002bf CODE 0x000002c4
| ||||||| ; XREFS: CODE 0x000002d0 CODE 0x000002d5
| ```````-> 0x000002e4 ~ ld hl, 0x02f3
| ;-- hit0_0:
| 0x000002e5 di
| 0x000002e6 ld [bc], a
| 0x000002e7 push hl
| 0x000002e8 call fcn.00000f66
| 0x000002eb add sp, 0x02
| ; CODE XREF from fcn.00000274 (0x2e1)
\ --------> 0x000002ed ret
Es en esta última parte del código donde la dirección 0x2f3
"FAIL!"
es cargada en el registro hl
, introducida en la pila mediante push hl
y finalmente se llama a fcn.00000f66()
quien imprimirá el mensaje por pantalla.
Un poco más arriba observamos una estructura de código idéntica pero que utiliza la dirección 0x2ee como argumento para
fcn.00000f66()
.
[0x000002e5]> ps @ 0x2ee
WIN!
Pareque que estamos donde queríamos. Existen varias referencias que conducen a chico malo, entre ellas nos llaman la atención las siguientes:
0x00000281 cp 0x03
0x00000283 jp nZ, 0x02e4
...
0x00000294 cp 0x07
0x00000296 jp nZ, 0x02e4
...
0x000002a6 cp 0x05
0x000002a8 jp nZ, 0x02e4
...
0x000002bd cp 0x01
0x000002bf jp nZ, 0x02e4
...
0x000002ce cp 0x09
0x000002d0 jp nZ, 0x02e4
Los dígitos 3, 7, 5, 1 y 9 son significativos desde luego, pero debemos estudiar la función desde el principio para descubrir en qué orden:
| fcn.00000274 ();
| ; var int local_2h @ sp+0x2
| ; CALL XREF from hit0_0 (+0x22f)
| 0x00000274 ld hl, sp + 0x02
| 0x00000276 ld e, [hl]
| 0x00000277 inc hl
| 0x00000278 ld d, [hl]
| 0x00000279 ld hl, 0x0004
| 0x0000027c add hl, de
| 0x0000027d ld c, l
| 0x0000027e ld b, h
| 0x0000027f ld a, [bc]
| 0x00000280 ld c, a
| 0x00000281 cp 0x03
| ,=< 0x00000283 jp nZ, 0x02e4
| ,==< 0x00000286 jr 0x03
Recordamos que estamos trabajando con una arquitectura de 16 bits (word registers).
Mediante
ld hl, sp+0x02
se carga en hl
el argumento recibido por nuestra función (este fue pusheado anteriormente en la pila). Parece que tratamos con un puntero, pues a continuación se mueve al registro e
el primer byte contenido en hl
y en d
el segundo byte de hl
. La instrucción add
añade 4 bytes a la dirección ahora contenida en de
y el resultado se almacena de nuevo en hl
. Esta dirección se descompone de nuevo en dos introduciendo el byte más significativo en b
y el menos significativo en c
. Finalmente, y en dos pasos, se mueve el valor contenido en [bc]
al registro c
y se compara con la constante 0x03.
Rápidamente nos damos cuenta que se trata más de un intento de ofuscación de código y no un algoritmo de cifrado o algo por el estilo. En pseudocódigo toda esta maraña de ensamblador se puede resumir en:
if (buffer[4] != 0x03) jmp chico_malo
El resto del código en
fcn.00000274()
es idéntico al fragmento que acabamos de estudiar salvo que en cada caso se calcula un índice distinto. Lo que dejamos como tarea para el lector (no le llevará más de medio minuto). Si asignamos los índices con las constantes a comparar obtenemos:
idx=4 -> 0x03 idx=2 -> 0x07 idx=1 -> 0x05 idx=3 -> 0x01 idx=0 -> 0x09
O lo que es igual:
if (buffer[4] != 0x03 || buffer[2] != 0x07 || buffer[1] != 0x05 || buffer[3] != 0x01 || buffer[0] != 0x09) print "FAIL!" else print "WIN!"
Ordenamos los índices y obtenemos la secuencia "95713":
Pwned!
Ha sido sencillo, instructivo y divertido al mismo tiempo. Como indica el propio nombre del binario, se trata de un desafío "simple" que sirve de calentamiento para aquellos que desconozcan esta clase de plataforma o código para consolas antiguas. Entre los demás crackmes los organizadores plantearon otro reto llamado
Ha sido sencillo, instructivo y divertido al mismo tiempo. Como indica el propio nombre del binario, se trata de un desafío "simple" que sirve de calentamiento para aquellos que desconozcan esta clase de plataforma o código para consolas antiguas. Entre los demás crackmes los organizadores plantearon otro reto llamado
harder.gb
. Ya veremos si somos tan valientes de lanzarnos a la aventura...
No hay comentarios:
Publicar un comentario