vous pouvez retrouver les fichiers joint avec le chall ici

énoncée du challenge

Découverte du challenge

Pour ce challenge 2 fichiers nous ont été donnés, un dossier compressé contenant un emulateur ainsi qu’un dump de la flash d’une montre.

╭─user@arch-vmware ~/shared/fcsc2023/reverse/sensor_watch/writeup
╰─➤  file watch.uf2
watch.uf2: UF2 firmware image, file size 00000000, address 0x002000, 360 total blocks

après quelques recherches sur le format de fichier on comprend que c’est un format dévellopé par microsoft afin de flasher des micro controleur

on trouve sur ce même github un script python permettant packer et à la fois d’unpacker des fichiers UF2.

on va donc télécharger ce script et extraire le firmware.

wget https://github.com/microsoft/uf2/raw/master/utils/uf2conv.py
wget https://github.com/microsoft/uf2/raw/master/utils/uf2families.json

Une fois téléchargé on va pouvoir récuperer notre firmware avec la commande :

╭─user@arch-vmware ~/shared/fcsc2023/reverse/sensor_watch/writeup
╰─➤  python3 uf2conv.py watch.uf2 --convert --output firmware.bin                                     1 ↵
--- UF2 File Header Info ---
All block flag values consistent, 0x0000
----------------------------
Converted to bin, output size: 92160, start address: 0x2000
Wrote 92160 bytes to firmware.bin

Dans l’archive “émulateur”, on peut trouver un docker contenant un fichier web-assembly qui semble émuler notre montre.

╭─user@arch-vmware ~/shared/fcsc2023/reverse/sensor_watch/writeup
╰─➤  tar -xvf emulateur.tar.gz
emulateur/
emulateur/src/
emulateur/src/favicon.ico
emulateur/src/watch.html
emulateur/src/watch.js
emulateur/src/watch.wasm
emulateur/docker-compose.yml
emulateur/Dockerfile
emulateur/README.md

On peut lancer l’émulateur avec la commande docker-compose up et se rendre sur la page http://localhost:8000/watch.html

emulateur

On joue un peu avec l’émulateur et on tombe rapidemment sur une interface “PIN” qui semble prendre en entrée un code PIN en incrémentant les chiffres présents sur le cadran de la montre.

pin

Si l’on tente de rentrer un mot de passe la montre nous répond “BADPIN”

bad pin

Sensor Watch

Après quelques recherches sur le modèle de la montre CASIO F-91W, un nom revient très souvent : celui de Joey Castillo.

En effet je tombe sur un github intéressant : GitHub - joeycastillo/Sensor-Watch

Je n’ai pas remarqué de suite, mais ce github porte le nom du challenge ! On est donc sur la bonne voie.

En parcourant le github on comprend que la Sensor Watch est une carte de remplacement pour la montre Casio F-91W sur laquelle on peut intégrer en quelque sorte des mods etc.

On peut d’ailleurs y retrouver notre émulateur quand on jette un oeil au framework Movement.

On a le code source du framework utilisé !

On peut donc maintenant passer au passer au reverse engineering de notre montre.

Reverse engineering

Afin de decompiler le firmware je vais utiliser le framework ghidra.

On peut lire sur le github de la sensor watch :

  • ARM Cortex M0+ microcontroller

Afin d’en savoir plus on cherche la datasheet du micro controleur. https://www.st.com/resource/en/datasheet/stm32g081rb.pdf

On est donc sur de l’ARM compilé en 32 bits.

l’outil binbloom peut lui aussi nous donner des informations à propos du firmware.

╭─user@arch-vmware ~/shared/fcsc2023/reverse/sensor_watch/writeup
╰─➤  binbloom -a 32 firmware.bin                                                                    255 ↵
[i] 32-bit architecture selected.
[i] File read (92160 bytes)
[i] Endianness is LE
[i] 143 strings indexed
[i] Found 580 base addresses to test
[i] Base address found (valid array): 0x00002000.
 More base addresses to consider (just in case):
  0x1ffeb000 (0.03)
  0x1fff8000 (0.01)
  0x46bfa000 (0.00)
  0x001fb000 (0.00)

Cet outil nous apprend que les octets dans le firmware sont en little endian et nous propose des addresse de bases afin de mapper correctement notre binaire.

On est donc maintenant prêt à charger notre firmware dans Ghidra.

import firmware dans ghidra

Une fois notre binaire chargé on va aller dans Window -> Memory Map et cliquer sur la petite maison afin de modifier l’addresse de base du binaire.

et l’on va la mettre à 0x2000. A ce moment là vous vous rendrez compte que ghidra vous a maintenant trouvé plein de fonctions. En effet, grâce à l’addresse de base les fonctions ainsi que les symboles présents dans le binaire vont pouvoir être résolus par notre désassembleur car ils seront à leur addresse correcte. Je vous conseille tout de même de lancer à nouveau une auto-analyse du firmware ainsi qu’un “ARM agressive instruction finder”.

Commençons donc à investiguer.

Pour ma part j’ai d’abord commencé par regarder les chaines de caractères présentes dans le binaire. Certaines telles que “BADPIN”, “PI N%c%c%c%c%c%c”

A la suite de ça on peut trouver les fonctions qui utilise ces chaines de caractère.

On trouve cette fonction qui fait reference à la chaine “BADPIN” :

undefined8 UndefinedFunction_0000b308(uint param_1,uint param_2,char *param_3)

{
  int iVar1;
  undefined *puVar2;
  char cVar3;
  char extraout_r1;
  int extraout_r1_00;
  int iVar4;
  uint uStack_14;

  iVar1 = DAT_0000b3fc;
  uStack_14 = param_2 & 0xffff0000 | param_1 & 0xffff;
  switch(param_1 & 0xff) {
  case 1:
    if (*param_3 != '\0') break;
    goto LAB_0000b376;
  case 2:
    cVar3 = *param_3;
    if (cVar3 == '\0') {
      if ((int)((uStack_14 >> 8) << 0x1f) < 0) {
        cVar3 = param_3[2] + '\x04';
        puVar2 = DAT_0000b3f8;
LAB_0000b340:
        FUN_00005a84(puVar2,cVar3);
        break;
      }
    }
    else if (cVar3 == '\x01') {
      FUN_00006a88(param_3 + 3,6,DAT_0000b3fc,0);
      iVar4 = 0;
      do {
        if (PTR_DAT_0000b400[iVar4] != (*(byte *)(iVar1 + iVar4) ^ 0x15)) {
          cVar3 = '\0';
          goto LAB_0000b370;
        }
        iVar4 = iVar4 + 1;
      } while (iVar4 != 0x40);
      cVar3 = '\x01';
LAB_0000b370:
      *param_3 = '\x03' - cVar3;
    }
    else {
      if (cVar3 != '\x03') {
        if (cVar3 != '\x02') break;
        cVar3 = '\0';
        puVar2 = PTR_DAT_0000b408;
        goto LAB_0000b340;
      }
      FUN_0000e31c(param_3 + 3,PTR_s_BADPIN_0000b404);
      param_3[1] = '\0';
      param_3[2] = '\0';
    }
LAB_0000b376:
    FUN_0000b29c(param_3);
    break;
  default:
    FUN_0000a700(uStack_14);
    break;
  case 5:
    FUN_0000a6c4(0);
    break;
  case 6:
    if (*param_3 == '\0') {
      if ((byte)param_3[2] == 5) {
        *param_3 = '\x01';
      }
      FUN_0000b744((byte)param_3[2] + 1,6);
      param_3[2] = extraout_r1;
      param_3[1] = '\0';
    }
    break;
  case 0xe:
    if (*param_3 == '\0') {
      FUN_0000b744((byte)param_3[1] + 1,10);
      puVar2 = PTR_s_0123456789_0000b40c;
      param_3[1] = (char)extraout_r1_00;
      param_3[(byte)param_3[2] + 3] = puVar2[extraout_r1_00];
    }
  }
  return CONCAT44(param_1,1);
}

Malheureusement elle n’est pas très compréhensible.

Sachant que nous avons une partie du code source de notre firmware, nous pouvons nous en servir afin de retrouver des symboles.

J’ai donc procédé de la même manière suivante, pour chaque fonctions présentes dans le code source de la Sensor-Watch et en particulier du framework movement, je modifie le nom de la fonction.

Quelques exemples de fonctions que j’ai utilisé sont :

  • app_init

  • app_loop

  • file_system_init

  • filesystem_process_command

  • file_system_write_file

Grâce à cela, j’ai pu retrouver plusieurs symboles et avoir une meilleure compréhension de notre fonction.

La documentation de Movement fût également d’une grande aide :

Sensor-Watch/README.md at main · joeycastillo/Sensor-Watch · GitHub

On comprend donc que le code trouvé précedemment équivaut à la fonction face_loop

En s’aidant de cette page de documentation, de quelques exemples présent sur le github ainsi que des symboles que l’on a résolu je réussis à réécrire un code un peu plus compréhensible.

undefined8 face_loop(event_type event,astruct_1 *param_2,struct_context *context)

{
  undefined *puVar1;
  char cVar2;
  uchar extraout_r1;
  int extraout_r1_00;
  int iVar3;
  uint local_14;
  int state;

  buffer = DAT_0000b3fc;
  local_14 = (uint)param_2 & 0xffff0000 | (uint)(ushort)event;
  switch(event.event_type) {
  case '\x01':
    if (context->action != '\0') break;
    goto LAB_0000b376;
  case '\x02':
    /*
    si context-> action n'est pas égal à 0 on va appeler
    */
    cVar2 = context->action;
    if (cVar2 == '\0') {
      if ((int)((local_14 >> 8) << 0x1f) < 0) {
        cVar2 = context->index + '\x04';
        puVar1 = DAT_0000b3f8;
LAB_0000b340:
        watch_display_string(puVar1,cVar2);
        break;
      }
    }
    else if (cVar2 == '\x01') {
     /* on dirait qu'il va mettre des données à l'adresse de DAT_0000b3fc 
      et va ensuite itérer dessus en faisait un xor 0x15 sur chaque valeur
      à cette addresse et la comparer avec des valeurs d'une array de 64 bytes */
      unknow_function(context->PIN,6,DAT_0000b3fc,0);
      iVar3 = 0;
      do {
        if (PTR_DAT_0000b400[iVar3] != (*(byte *)(state + iVar3) ^ 0x15)) {
          cVar2 = '\0';
          goto LAB_0000b370;
        }
        iVar3 = iVar3 + 1;
      } while (iVar3 != 0x40);
      cVar2 = '\x01';
LAB_0000b370:
      context->action = '\x03' - cVar2;
    }
    else {
      if (cVar2 != '\x03') {
        if (cVar2 != '\x02') break;
        cVar2 = '\0';
        puVar1 = FCSC;
        goto LAB_0000b340;
      }
      maybe_print(context->PIN,PTR_s_BADPIN_0000b404);
      context->ticks = '\0';
      context->index = '\0';
    }
LAB_0000b376:
    update_cadran(context);
    break;
  default:
    movement_default_loop_handler(local_14);
    break;
  case '\x05':
    FUN_0000a6c4(0);
    break;
  case '\x06':
    /* 
       bouton en haut à gauche de la montre, on incrémente 
       notre index et si il est égal à 5 on passe context à 1
    */
    if (context->action == '\0') {
      if (context->index == 5) {
        context->action = '\x01';
      }
      FUN_0000b744(context->index + 1,6);
      context->index = extraout_r1;
      context->ticks = '\0';
    }
    break;
  case '\x0e':
    if (context->action == '\0') {
    /* incremente le password à la position password[param[2]] 
    (bouton en bas à droite) */
      FUN_0000b744(context->ticks + 1,10);
      puVar1 = PTR_s_0123456789_0000b40c; 
      /*
      PTR_s_0123456789_0000b40c pointe vers "0123456789"
      */
      context->ticks = (uchar)extraout_r1_00;
      context->PIN[context->index] = puVar1[extraout_r1_00];
    }
  }
  return CONCAT44((uint)(ushort)event,1);
}

J’ai aussi réimplémenter les structures suivantes :

struct struct_context {
    uint8 ticks;
    uint8 index;
    char  PIN[6]
}
struct event_type {
    uint8 no_idea;
    uint8 event;
}

la structure contexte contient le code PIN entré ainsi que l’index du code PIN que l’on est actuellement en train de modifier.

La structure event contient l’action réalisé par l’utilisateur.

A partir de là, le code reste plutôt sale mais j’espère que vous le comprendrez aussi grâce à mes commentaires.

A partir de là une chose était sure pour moi. Il fallait qu’une fois unknow_function était appelée avec en paramètre notre PIN ainsi que sa taille DAT_0000b3fc soit égal au 64 bytes présent à l’addresse PTR_DAT_0000b400 xor par 15.

Je me suis donc mis à reverse la fonction unknow function pendant un très long moment sans comprendre son fonctionnement.

Puis une idée m’est venue, googler les constantes ! Et effectivemment on tombe rapidemment sur des liens parlant de sha512.

constantes google

A ce moment là, tout deviens plus clair. En effet la taille d’un hash sha512 est de 64 bytes ! On cherche donc un peu et on retrouve dans le code source de la Sensor Watch notre fonction de hashage.

sha512 function

Je récupère donc le tableau de 64 bytes

[ 0xe3, 0x1e, 0x2c, 0x61, 0x36, 0xbd, 0xa8, 0xc0, 0x53, 0xf0, 0xf4, 0x45, 0x91, 0x88, 0x0b, 0xa7, 0x86, 0x39, 0x35, 0xd8, 0xb3, 0xc9, 0x7a, 0x3c, 0xca, 0xdf, 0xdd, 0xc2, 0xb3, 0x68, 0x97, 0xe8, 0x6c, 0x17, 0xd7, 0x97, 0x60, 0x1a, 0x3c, 0xda, 0xa6, 0xb8, 0x24, 0xd3, 0xb9, 0xac, 0xe6, 0xec, 0xac, 0xac, 0x55, 0xfd, 0x49, 0xaf, 0x5d, 0xaa, 0x44, 0x4f, 0x8f, 0x3e, 0x7d, 0xf5, 0xf2, 0xa7 ]

que je xor par 0x15 afin d’obtenir le hash suivant :

f60b397423a8bdd546e5e150849d1eb2932c20cda6dc6f29dfcac8d7a67d82fd7902c282750f29cfb3ad31c6acb9f3f9b9b940e85cba48bf515a9a2b68e0e7b2

Nous savons que notre code PIN à une taille de 6 caractère et nous avons “0123456” comme set de caractère. On pourra donc très facilement casser ce mot de passe.

Pour ce faire j’ai utilisé hashcat :

.\hashcat.exe -m 1700 -a 3 f60b397423a8bdd546e5e150849d1eb2932c20cda6dc6f29dfcac8d7a67d82fd7902c282750f29cfb3ad31c6acb9f3f9b9b940e85cba48bf515a9a2b68e0e7b2 ?d?d?d?d?d?d

Et nous obtenons un code pin qui est 413372 !!

On peut donc maintenant l’essayer dans notre émulateur et obtenir le flag.

flag

Le flag est donc FCSC413372 !

Conclusion

C’était la première fois que je reversais un firmware et malgré beaucoup de difficultés au début j’ai apprécié ce challenge. Si je devais en retenir quelque chose ce serait prendre le temps de comprendre le firmware afin de configurer correctement son désassembleur. J’ai en effet l’habitude de reverse des binaires très communs tels que des ELF, PE, etc. qui sont très bien pris en charges par les désassembleurs. Ce fut un challenge très enrichissant, un grand merci aux créateurs.