Pages

domingo, 29 de julio de 2018

GameBoy ROM Reversing con radare2

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:

game boy rom con mednafen

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:

game boy rom con mednafen


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":

game boy rom con mednafen

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 harder.gb. Ya veremos si somos tan valientes de lanzarnos a la aventura...

No hay comentarios:

Publicar un comentario

Protostar CTF - stack5

En ./stack5 continuamos con la dinámica de los dos últimos retos: dpc@kernelinside:~/protostar/bin$ ./stack5 test dpc@kernelinside:~/p...