Reverse Engineering a Gameboy ROM

Agradecimientos a n0tM4l4f4ma 

Este reto se resolvió en el UTCTF 2021. El nombre del reto daba a entender que se analizaría un ROM, el cual consiste en un archivo llamado maze.gb, un juego que se puede ejecutar desde cualquier emulador de Gameboy.

Conociendo el ROM

Para ejecutar el ROM y analizarlo, inicialmente utilizé el emulador Visual Boy Advance con cual pude ver una simple pantalla de inicio (Figura 1).

Pantalla principal.
  Figura 1. Pantalla principal. 
 
Los controles del juego son sencillo y estan mapeados a las siguientes teclas:
Tecla Función
Enter Inicio(Start)
Flecha Arriba Movimiento Arriba
Flecha Abajo Movimiento Abajo
Flecha Izquierda Movimiento Izquierda
Flecha Derecha Movimiento Derecha

Despues de presionar "Start", obtenemos una pantalla donde hay un personaje en el centro que se encuentra en una especie de laberinto. Al ver en una esquina de la pantalla un icono de llave con un contador y una llave visible en el mapa, fue deducible que tenia que recolectar llaves como mi objetivo (Figura 2).
 
Inicio del juego.
Figura 2. Inicio del juego. 

El personaje se controla con las teclas de movimiento, los bloque negros sirven como pared y las llaves son objetos recolectables, cada vez que se recolecta una llave el contador incrementa en 1 (Figura 3).


Figura 3. Recoleccion de llave.

Es un laberinto relativamente pequeño y existen 10 llaves repartidas en el laberinto. Esta distribución esta ilustrada en el mapa de la Figura 4.

Figura 4. Mapa del laberinto y llaves (Puntos amarillos).

Al recolectar las 10 llaves en el laberinto se termina el juego, a lo que muestra un mensaje en pantalla (Figura 5).

YOU LOST OOPS SORRY LMOA

Figura 5. YOU LOST OOPS SORRY LMOA.

Analizando el codigo

Para analizar el código del ROM utilizé la extención GhidraBoy que me permitió analizar de forma rápida el código. Para agregar la extención en Ghidra usé los siguientes comandos en la terminal:

#Primero descargar el repositorio
git clone https://github.com/Gekkio/GhidraBoy.git

#Accedemos a los archivos descargados
cd GhidraBoy/

#Ejecutamos el archivo gradlew en la carpeta descargada
#/opt/ghidra es el directorio donde tengo Ghidra
./gradlew -Pghidra.dir=/opt/ghidra/

#Tras esto se genera un archivo .zip en build/distributions/
#En mi caso es ghidra_9.2.1_PUBLIC_20210326_GhidraBoy.zip
#Este se puede ver con el siguiente comando
ls build/distributions/

#Movemos el archivo .zip generado a la subcarpeta de ghidra: Extensions/Ghidra
mv build/distributions/ghidra_9.2.1_PUBLIC_20210326_GhidraBoy.zip /opt/ghidra/Extensions/Ghidra

Despues de esto solo hace falta abrir Ghidra y en el menú File seleccionar la opción Install Extensions.... Si todo salio bien, se puede ver la extención de GhidraBoy disponible para activarse (Figura 6).

Figura 6. Añadiendo GhidraBoy.

Al añadir extenciones en Ghidra te pide que reinicies el programa, despues de hacerlo se puede analizar nuestro ROM. Ahora, tras el análisis que realiza Ghidra, podemos ver las distintas funciones en el ROM (Figura 7).

Figura 7. Decompilación del ROM.

En este punto me puse a checar las distintas funciones del juego para ver si encontraba algo que me llamara la atencion, logrando encontrar sin mayor problema la siguiente función:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
void FUN_032f(void) {
  short sVar1;
  undefined *puVar2;
  char extraout_E;
  char extraout_E_00;
  byte bVar3;
  char cVar4;
  ushort uStack2;
  
  if (DAT_cb7c == '\0') {
    DAT_cd4c = 0;
    do {
      DAT_cd4d = 0;
      do {
        DAT_cd4e = DAT_cd4c + (DAT_cbe1 - 10);
        DAT_cd4f = (9 < DAT_cbe1) + -1 + CARRY1(DAT_cd4c,DAT_cbe1 - 10);
        uStack2 = (ushort)DAT_cd4d;
        _DAT_cd50 = uStack2 + CONCAT11((8 < DAT_cbe2) + -1,DAT_cbe2 - 9);
        sVar1 = ((ushort)DAT_cd4d * 4 + uStack2) * 4 + (ushort)DAT_cd4c;
        bVar3 = (byte)sVar1;
        puVar2 = (undefined *)
                 CONCAT11((char)((ushort)sVar1 >> 8) + -0x35 + (0x1b < bVar3),bVar3 - 0x1c);
        if ((DAT_cd4f < (DAT_cd4e < 0x32)) &&
           ((byte)((ushort)_DAT_cd50 >> 8) < ((byte)_DAT_cd50 < 0x32))) {
          DAT_cd52 = (&DAT_c0a0)[CONCAT11(DAT_cd4f,DAT_cd4e) + _DAT_cd50 * 0x32];
          if (DAT_cd52 == '\0') {
            *puVar2 = DAT_cd53;
          }
          else {
            if (DAT_cd52 == '\x02') {
              *puVar2 = DAT_cd55;
            }
            else {
              *puVar2 = DAT_cd54;
            }
          }
        }
        else {
          *puVar2 = DAT_cd54;
        }
        DAT_cd4d = DAT_cd4d + 1;
      } while (DAT_cd4d < 0x12);
      DAT_cd4c = DAT_cd4c + 1;
    } while (DAT_cd4c < 0x14);
    DAT_cbe4 = DAT_cd55;
    FUN_0f1a(DAT_cb7a,10);
    DAT_cd56 = extraout_E;
    cVar4 = extraout_E;
    FUN_0f2d(10);
    DAT_cbe5 = cVar4 + '/';
    DAT_cbe6 = extraout_E_00 + '/';
    DAT_cd57 = extraout_E_00;
    FUN_1254(&DAT_1214,&DAT_cbe4);
  }
  else {
    if (DAT_cb7b == '\0') {
      FUN_0a45("YOU LOST",0x305);
      FUN_0a45(&DAT_0564,0x407);
      FUN_0a45("SORRY",0x506);
      FUN_0a45(&DAT_056f,0x607);
    }
    else {
      FUN_0a45("A WINNER IS YOU",0x302);
      FUN_0a45("CONGRATULATIONS",0x402);
      FUN_0a45("THE FLAG IS",0x603);
      FUN_0a45(&DAT_0551,&DAT_0702);
      DAT_cd58 = 0;
      do {
        *(byte *)CONCAT11((0xa6 < DAT_cd58) + -0x33,DAT_cd58 + 0x59) =
             *(byte *)CONCAT11((0x2a < DAT_cd58) + -0x35,DAT_cd58 - 0x2b) ^
             *(byte *)CONCAT11((0x32 < DAT_cd58) + -0x35,DAT_cd58 - 0x33);
        DAT_cd58 = DAT_cd58 + 1;
      } while (DAT_cd58 < 8);
      DAT_cd61 = 0;
      FUN_0a45(&DAT_cd59,&DAT_0709);
      FUN_0a45(&DAT_0559,&DAT_0711);
    }
  }
  return;
}

Esta función llamo mi atencion debido a los mensajes que se encontraban a partir de la línea 56 en adelante. Donde reconocía el YOU LOST (línea 57) y el SORRY (línea 59), y tras ver la informacion de las variables que los acompañaban en las lineas 58 y 60 no me quedo duda de que era el mensaje que obtuve al conseguir las 10 llaves.

En este momento sabía que tenia dos posibles caminos para llegar a la solución; continuar analizando el código, donde podia ver una vertiente que se generaba a partir de la línea 63, que mostraba la impresion de un mensaje de victoria seguida de una especie de decodificación de datos mediante XOR (lineas 67 a la 74), o mi otra opción, la cual me parecio más atractiva en el momento, descubrir que secuencia de recollección de llaves me llevaría a la victoria. A partir de esta decisión le tome especial importancia a la variable de nombre DAT_cb7b (línea 56) al ser la determinadora del mensaje final. 

LLegando a la Flag

Para encontrar esta secuencia utilizé la herramienta Wasmboy con la cual podría usar sus widgets en el ROM a mi favor. Utilizé 2 de sus widgets para esta tarea, el Memory Viewer y Playback Controls, los cuales se pueden agregar en el menú Widgets (Figura 8).

Figura 8. Preparativos para análisis.

En Memory Viewer, con ayuda del campo Jump To Addres, se puede llegar rápidamente a la dirección de la variable DAT_cb7b (0xcb7b), una vez ahí solo hace falta seleccionar el campo W correspondeinte para agregar el Breakpoint en esta variable cuando vaya a ser escrita (Figura 9).

Figura 9. Añadiendo el Breakpoint.
 
Para hacer mi tarea más sencilla me aproveche de la función de guardado y cargado de estados del Widget Playback Controls con los botones de Save y Load respectivamente (Figura 10). Cuando guardas un estado aparece un pequeño mensaje en la parte inferior derecha y cuando seleccionas cargar, se puede ver un menú para seleccionar el estado a cargar.


  Figura 10. Controles de Guardado y Cargado.

Una vez en el juego y tras éstos preparativos, cuando tomas una llave se activa nuestro breakpoint, donde podremos ver su valor en el momento en el memory viewer, así podemos detectar cambios en la variable DAT_cb7b, si es la llave erronea la variable toma el valor de 0, lo que nos indica que debemos tomar una llave distinta (Figura 11). Para volver a un estado de nuestro agrado, solo entramos al menú de carga en Playback Controls (Load State) y seleccionamos el botón Play para continuar desde el estado seleccionado.

Figura 11. Llave erronea.

En cambio, si es la llave correcta, el valor de la variable continua siendo 1, a lo que se puede considerar un buen momento para guardar el estado, esto para no perder el progreso y no tener que repetir pasos. Para continuar en estos puntos solo se necesita seleccionar el botón Play (Figura 12).

Figura 12. Obtención de llave correcta.

Realizando esto con las demás llaves, se puede llegar a la secuencia que nos mostrará la flag. Esta secuencia esta ilustrada en la Figura 13.

Secuencia de llaves
Figura 13. Secuencia para obtener la flag.

Tras tomar la última llave, manteniendo la variable DAT_cb7b con valor de 1, obtenemos el siguiente mensaje de victoria que incluye la flag (Figura 14):

Figura 14. Obtención de la flag.

Y así se llegó finalmente la flag:

UTFLAG{VRAQ8C9A}

Go Mayas!!

Comentarios