WHY2025 CTF: Gamerboy

Écrit par l’équipe HackGyver – Août 2025

1: Premières informations

$ file chall.gb
chall.gb: Game Boy ROM image (Rev.01) [ROM ONLY], ROM: 256Kbit


On se retrouve face à une ROM pour Game Boy. On va essayer de la lancer avec un émulateur.
mGBA au hasard. bgb existe aussi et contient un debugger.

> Press START
On appuie donc sur la touche START (`Entrée` sur clavier) :

> NOW THE CHEAT PATTERN!!
Ensuite, le programme semble retranscrire les touches sur lesquelles nous appuyons.
Quand on appuie sur deux touches en même temps, on obtient MULTI.



2: Analyse statique

Prérequis

- Ghidra (version 11.4.1 à l'écriture de cet article)
- GhidraBoy en suivant les instructions d'installation, (veillez à bien installer la version correspondante à la version de Ghidra)

Par défaut, Ghidra ne gère pas les ROMs de Game Boy.
GhidraBoy permet de correctement mapper les zones mémoires de la ROMs ou encore, de prendre en charge les instructions du CPU SM83.

Chaînes de caractères

Généralement, j'aime bien regarder les chaînes de caractères, cela permet de s'aiguiller dans le programme.
> Window > Defined Strings

PCB2

On retrouve bien les deux premières lignes de texte ainsi que les touches.
On remarque qu'il nous manque UP, DOWN, LEFT, A et B.
Parfois, les textes courts ont du mal a être détectés, on va les ajouter à la main.
On va double-cliquer sur RIGHT et regarder autour.


029f    55              ??         55h    U
02a0    50              ??         50h    P
02a1    00              ??         00h
02a2    44              ??         44h    D
02a3    4f              ??         4Fh    O
02a4    57              ??         57h    W
02a5    4e              ??         4Eh    N
02a6    00              ??         00h
02a7    4c              ??         4Ch    L
02a8    45              ??         45h    E
02a9    46              ??         46h    F
02aa    54              ??         54h    T
02ab    00              ??         00h
02ac    52  49  47      ds         "RIGHT"
        48  54  00
02b2    41              ??         41h    A
02b3    00              ??         00h
02b4    42              ??         42h    B
02b5    00              ??         00h
02b6    53  45  4c      ds         "SELECT"
        45  43  54 
        00
02bd    53  54  41      ds         "START"
        52  54  00
02c3    4d  55  4c      ds         "MULTI"
        54  49  00

On retrouve bien les autres touches comme DOWN à l'adresse 0x02a2.
Afin d'ajouter ces chaînes, on va faire un clic droit sur le début de chaque chaîne et les définir
comme des chaînes qui terminent par un 0x00 (*null-terminated string* ou *C string*) : Data > TerminatedCString.

ℹ️ Vous pouvez utiliser le raccourci `Y` pour répéter le dernier type défini.


029f    55  50  00      ds         "UP"
02a2    44  4f  57      ds         "DOWN"
        4e  00    
02a7    4c  45  46      ds         "LEFT"
        54  00    
02ac    52  49  47      ds         "RIGHT"
        48  54  00
02b2    41  00          ds         "A"
02b4    42  00          ds         "B"
02b6    53  45  4c      ds         "SELECT"
        45  43  54 
        00
02bd    53  54  41      ds         "START"
        52  54  00
02c3    4d  55  4c      ds         "MULTI"
        54  49  00

Cela nous sera utile un peu plus tard.




3: Point d'entrée

À partir d'ici, on aimerait savoir où sont utilisées ces chaînes de caractères, en particulier celle qui est affichée au début du programme.
Il est normalement possible de faire clic droit puis References > Show References To Address
pour afficher tous les endroits qui utilisent une donnée, mais les références ne fonctionne actuellement pas.
Le programme étant assez petit, il est très facile de trouver ces références.

Commençons par regarder ce que le decompilateur nous donne :


void FUN_036b(void)
{
  char cStack_10;
  undefined2 uStack_f;
  undefined1 auStack_d [8];
  char local_5;
  byte local_4;
  undefined1 local_1;
  
  local_1 = 0;
  uStack_f = 0xb;
  FUN_04fa(&UNK_023c,auStack_d);
  FUN_0200(&uStack_f);
  FUN_05e1("Press START");
  FUN_05a6(0x80);
  FUN_05e1("NOW THE CHEAT PATTERN!!\n");
  do {
    local_5 = FUN_057e();
    if (local_5 != '\0') {
      FUN_0247();
    }
    if (local_5 != '\0') {
      if (local_5 == (&cStack_10)[local_4]) {
        local_4 = local_4 + 1;
        if (local_4 == 0xb) {
          FUN_02c9(&cStack_10);
        }
      }
      else {
        local_4 = 0;
      }
    }
    FUN_0606(0x50);
  } while( true );
}


On retrouve bien nos deux chaînes de caractères affichées au début du programme.
On peut déjà se dire qu'il s'agit de la fonction principale du programme.
Renommons donc celle-ci en main par exemple.
Pour ceci, clic droit sur le nom de la fonction dans la vue de décompilation, Rename Function ou par son raccourci, la touche L (pour Label).

On peut également dire que la fonction FUN_05e1 sert à afficher une chaîne de caractères. On va l'appeler print_string.

Voici la boucle principale du programme :


do {
  local_5 = FUN_057e();
  if (local_5 != '\0') {
    FUN_0247();
  }
  if (local_5 != '\0') {
    if (local_5 == (&cStack_10)[local_4]) {
      local_4 = local_4 + 1;
      if (local_4 == 0xb) {
        FUN_02c9(&cStack_10);
      }
    }
    else {
      local_4 = 0;
    }
  }
  FUN_0606(0x50);
} while( true );

4: Lecture des boutons


local_5 = FUN_057e();

byte FUN_057e(void)
{
  byte bVar1;
  byte bVar2;
  
  P1 = 0x20;
  bVar1 = P1;
  bVar1 = P1;

  P1 = 0x10;
  bVar2 = P1;
  bVar2 = P1;
  bVar2 = P1;
  bVar2 = P1;
  bVar2 = P1;
  bVar2 = P1;

  P1 = 0x30;

  return ~(bVar2 << 4 | bVar1 & 0xf);
}

On voit l'utilisation du registre P1, utilisé pour lire les entrées du joueur, comprendre les boutons pressés.

On remarque que le registre est lu plusieurs fois à la suite.
La documentation nous indique que ceci sert à "stabiliser" la prise d'information :

Most programs read from this port several times in a row (the first reads are used as a short delay, allowing the inputs to stabilize, and only the value from the last read is actually used).

Toujours en lisant la documentation, on comprend qu'il est possible d'écrire dans les bits 5 et 4 de ce registre de 8 bits,
puis de le lire les 4 premiers bits pour récupérer les boutons pressées :

- 0x20 ou 0b0010 0000 : récupère les boutons du D-pad (DOWN, UP, LEFT et RIGHT);
- 0x10 ou 0b0001 0000 : récupère les autres boutons (START, SELECT, A et B);
- 0x30 ou 0b0011 0000 : les 4 premiers bits de P1 seront à 1, soit 0xf ou 0b1111.

À noter qu'un bit à 0 indique que le bouton est actuellement enfoncé.

bVar1 contient ainsi les boutons DOWN, UP, LEFT et RIGHT et bVar2 les boutons START, SELECT, A et B.

Renommons les variables bVar1 et bVar2 respectivement en dpads et buttons en faisant clic droit puis Rename Variable (ou comme pour une fonction, le raccourci L).


return ~(buttons << 4 | dpads & 0xf);

La fonction retourne un octet (soit 8 bits) dont les 4 premiers bits correspondent à la valeur de buttons et les 4 derniers, ceux de dpads.
Cette valeur est ensuite inversé grâce à l'opérateur ~ (NOT), ce qui comme décrit plus haut, permet d'obtenir les boutons enfoncés comme des 1 plutôt que des 0.

On peut donc renommer cette fonction en quelque chose comme read_pressed_buttons.


byte read_pressed_buttons(void)
{
  byte buttons;
  byte dpads;
  
  P1 = 0x20;
  dpads = P1;
  dpads = P1;

  P1 = 0x10;
  buttons = P1;
  buttons = P1;
  buttons = P1;
  buttons = P1;
  buttons = P1;
  buttons = P1;

  P1 = 0x30;

  return ~(buttons << 4 | dpads & 0xf);
}

Voici un tableau qui donne les valeurs en fonction des touches appuyés :

ToucheDécimalHexadécimalBinaire
RIGHT10x10000 0001
LEFT20x20000 0010
UP40x40000 0100
DOWN80x80000 1000
A160x100001 0000
B320x200010 0000
SELECT640x400100 0000
START1280x801000 0000

Dans la fonction main, on peut maintenant renommer la variable local_5 pour lui donner un nom plus descriptif comme pressed_buttons.



5: Affichage des boutons


if (pressed_buttons != '\0') {
  FUN_0247();
}

Dès qu'un bouton est appuyé, la fonction FUN_0247 est appelée :


void FUN_0247(char param_1)
{
  if (param_1 == '\x01') {
    print_string("RIGHT");
    return;
  }
  if (param_1 == '\x02') {
    print_string("LEFT");
    return;
  }
  if (param_1 == '\x04') {
    print_string("UP");
    return;
  }
  if (param_1 == '\b') {
    print_string("DOWN");
    return;
  }
  if (param_1 == '\x10') {
    print_string("A");
    return;
  }
  if (param_1 == ' ') {
    print_string("B");
    return;
  }
  if (param_1 == '@') {
    print_string("SELECT");
    return;
  }
  if ((char)(param_1 + -0x80) != '\0') {
    print_string(param_1 + -0x80,"MULTI");
    return;
  }
  print_string(0,"START");
  return;
}

On y retrouve des références aux boutons comme A ou encore DOWN qu'on a défini plus tôt.
Sans cela, nous aurions plutôt eu quelque chose comme :


if (param_1 == '\b') {
  print_string(&UNK_02a2);
  return;
}

ℹ️ Il est possible de changer la manière dont sont affichées les constantes.
Par exemple, on aimerait afficher 8 plutôt que \b.
Pour cela, clic droit sur la valeur et choisir Decimal: 8.
Répétez sur toutes le constantes pour un affichage plus cohérent.

On comprend rapidement que c'est la fonction en charge d'afficher les boutons appuyés.

Changeons le nom de la fonction en display_pressed_buttons et le nom du paramètre en button.
Pour cela, clic droit sur le nom de la fonction dans la vue de décompilation, puis Edit Function Signature.

PCB2

Lors de l'appel de la fonction FUN_0247 depuis main, aucun paramètre n'est passé dans la décompilation.
Le fait de cocher la case Commit all return/parameter details propage les changements, ce qui répare le problème.

6: Suite de touches


if (pressed_buttons != '\0') {
  if (pressed_buttons == (&cStack_10)[local_4]) {
    local_4 = local_4 + 1;
    if (local_4 == 0xb) {
      FUN_02c9(&cStack_10);
    }
  } else {
    local_4 = 0;
  }
}

cStack_10 semble être un tableau de char.
Si la valeur à l'indice local_4 correspond au bouton appuyé, on incrémente local_4.
Sinon on met local_4 à 0. Dès que local_4 est égal à 11 (0xb), la fonction FUN_02c9 est appelée.

On dirait une suite de touches à pressés dans un ordre précis.

On va renommer cStack_10 en expected_buttons et local_4 en `i`.


char expected_buttons;
undefined2 uStack_f;
undefined1 auStack_d [8];

On remarque que expected_buttons est défini comme char.
Cependant, on sait qu'il s'agit d'un tableau de 11 élements.
undefined2 correspond à 2 octets et undefined1 à 1 octet.
Ces trois variables réunis font bien 11 octets, il s'agit d'une erreur de décompilation.
La décompilation fait au mieux avec les informations qu'elle arrive à récupérer, mais il peut arriver parfois qu'elle fasse des choix discutables.
Le code reste correct car la mémoire est sensée être contigüe, mais il serait mieux d'avoir une seule variable de type char[11].

Pour changer le type d'une variable, clic droit puis Retype Variable (ou CTRL+L).


void main(void)
{
  char pressed_buttons;
  char expected_buttons [11];
  byte i;
  
  expected_buttons[1] = 11;
  expected_buttons[2] = 0;
  FUN_04fa(&UNK_023c,expected_buttons + 3);
  FUN_0200(expected_buttons + 1);
  print_string("Press START");
  FUN_05a6(0x80);
  print_string("NOW THE CHEAT PATTERN!!\n");
  do {
    pressed_buttons = read_pressed_buttons();
    if (pressed_buttons != 0) {
      display_pressed_buttons(pressed_buttons);
      if (pressed_buttons == expected_buttons[i]) {
        i = i + 1;
        if (i == 11) {
          FUN_02c9(expected_buttons);
        }
      }
      else {
        i = 0;
      }
    }
    FUN_0606(0x50);
  } while( true );
}

Voici le code actuel, plus lisible n'est-ce pas ?

7: Résoudre le challenge

Maintenant, l'objectif semble clair : appeler la fonction FUN_02c9.

Deux méthodes :

1. Patch la ROM pour appeler la fonction ;
2. Trouver ce qui se trouve dans expected_buttons pour reconstituer la suite à saisir.

8: Patch de la ROM

La manière la plus simple serait de sauter directement à 0x03d2.


03d2     3e 0b        LD         pressed_buttons, 0xb
03d4     21 00 00     LD         HL, 0x0
03d7     39           ADD        HL, SP
03d8     5d           LD         E, L
03d9     54           LD         D, H
03da     cd c9 02     CALL       FUN_02c9

On peut sauter directement dès qu'une touche est enfoncée :


03ac     f8 0b     LD     HL, SP+0xb
03ae     7e        LD     pressed_buttons, (HL => local_5)
03af     b7        OR     pressed_buttons
03b0     28 31     JR     Z, LAB_03e3                       ; changer ce saut

03b0     28 20     JR     Z, LAB_03d2

9: Trouver la suite

10: Lecture de la mémoire

⚠️ TODO.


0x08	  DOWN
0x04	  UP
0x04	  UP
0x08	  DOWN
0x01	  RIGHT
0x01	  RIGHT
0x02	  LEFT
0x01	  RIGHT
0x20	  B
0x10	  A
0x10	  A
PCB2

Le crackme gameboy du CTF est disponible ici:
📥 gamerboy.tgz