Flash-ing Logs

Catégorie: Hardware Difficulté: hard Flag: HTB{n07h1n9_15_53cu23_w17h_phy51c41_4cc355!@}

Challenge

circle-info

Description


After deactivating the lasers, you approach the door to the server room. It seems there's a secondary flash memory inside, storing the log data of every entry. As the system is air-gapped, you must modify the logs directly on the chip to avoid detection. Be careful to alter only the user_id = 0x5244 so the registered logs point out to a different user. The rest of the logs stored in the memory must remain as is.

circle-exclamation

Analyse des fichiers

Ce challenge concerne une mémoire flash qui enregistres des données à chaque fois qu’une personne ouvre la porte et les sauvegarde sous la forme de logs (une liste d’événement). L’objectif est donc de réussir à communiquer avec cette mémoire flash pour, dans un premier temps, lire ces données et ensuite les modifier.

Nous avons 2 fichiers :

  • client.py qui permet de communiquer avec la mémoire flash

  • log_event.c qui décrit le fonctionnement des logs

Comme client.py ne sert qu’à parler avec la mémoire flash, ce fichier ne nous intéresse pas pour le moment. Comprenons plutôt comment fonctionnent le système de logs pour savoir ce que la mémoire à en elle

Structure des logs

La structure des logs se trouve dans log_event.c

// SmartLockEvent structure definition
typedef struct {
    uint32_t timestamp;   // Timestamp of the event
    uint8_t eventType;    // Numeric code for type of event // 0 to 255 (0xFF)
    uint16_t userId;      // Numeric user identifier // 0 t0 65535 (0xFFFF)
    uint8_t method;       // Numeric code for unlock method
    uint8_t status;       // Numeric code for status (success, failure)
} SmartLockEvent;

Cette structure se compose donc de :

  • uint32_t → 4 bytes pour le timestamp

  • uint8_t → 1 byte pour le type de l’événement

  • uint16_t → 2 bytes pour l’identifiant de l’utilisateur

  • uint8_t → 1 byte pour la méthode utilisée

  • uint8_t → 1 byte pour le status du résultat

On pourrait donc s’atteindre à ce que la structure fasse 4+1+2+1+1 = 9 bytes

Sauf que non, dû à des optimisations du compilateur pour accèder à la mémoire, la structure fait 12 bytes

On peut le voir si l’on compile la structure et qu’on la sérialise de la même façon que dans le code fournit :

On a un premier padding de 1 byte à la position 5, puis un padding de 2 bytes à la position 10/11. Il faudra donc prendre en compte cela quand on sérialisera et désérialisera nos données

Sauvegarde des logs

J’ai simplifié en ne gardant que ce qui intéresse ici, on voit qu’un checksum de type CRC32 (donc 4 bytes) est calculé et ajouté à la suite des données de l’event, ce qui nous fait un total de 12 + 4 = 16 bytes écrit en mémoire pour chaque événement

Mais si l’on va voir la fonction write_to_flash on s’aperçoit qu’avant d’être écrite en mémoire, les données sont chiffrées

Cette fonction encrypt_data va en fait récupérer une clé de 12 bytes à partir de l’adresse 0x52 du registre de sécurité n°1

Ces 12 bytes sont utilisés pour XOR les premiers 12 bytes des données, c’est à dire l’événement en lui même mais pas sont CRC32

Echange avec la mémoire flash

On sait donc comment sont enregistrées les données, il faut maintenant apprendre à discuter avec le W25Q128FV pour les récupérer, les modifier puis les réinjecter

Direction sa documentationarrow-up-right

Fonctionnement général

La mémoire est composée de 256 Blocs, chaque bloc est composé de 16 Secteurs et chaque secteur est composé de 16 Pages

  • Une page fait 256 bytes

  • Un secteur fait donc 256 * 16 = 4096 bytes (4KB)

  • Un bloc fait donc 4096 * 16 = 65536 bytes (64KB)

  • La mémoire est de 65536 * 256 = 16777216 (16MB)

Autre point, on peut écrire 256 bytes à la fois et écraser les données par secteur, par demi-bloc, par bloc ou tout

Et enfin, il existe 3 registres de sécurité de 256 bytes chacun


Lecture des données

Pour lire la mémoire, il faut envoyer 0x03 puis l’adresse de 24 bits à lire

circle-info

Exemple :

Pour lire le début de la mémoire, on envoit 0x03 0x00 0x00 0x00

Mettons à jour notre client.py avec cette nouvelle fonction


Lecture des registres de sécurité

La commande est, dans l’ordre :

  • 0x48

  • 0x00

  • 4 bits contenant le numéro du registre

  • 4 bits null

  • 1 octets pour dire à partir d’où lire le registre

circle-info

Exemples :

Pour lire le registre de sécurité n°1 à partir de l’octet 0x52 : 0x48 0x00 0x10 0x52 Pour lire le registre de sécurité n°2 à partir de l’octet 0x18 : 0x48 0x00 0x20 0x18 Pour lire le registre de sécurité n°3 à partir de l’octet 0x42 : 0x48 0x00 0x30 0x42

Mettons à jour notre client.py avec cette nouvelle fonction


Ecrire dans la mémoire

Pour modifier les données, on va utiliser la commande 0x02 qui demande les conditions suivantes :

  • La mémoire à l’endroit que l’on veut programmer doit être écrasée avant

  • On doit envoyer l’instruction Write Enable avant

  • Le commande de Page Program se compose dans cette ordre :

    • 0x02

    • Adresse de 24bits où l’on souhaite écrite

    • Au moins 1 octets de données à écrire (et maximum 256 octets)

Mettons à jour notre client.py avec cette nouvelle fonction

Pour Write Enable, c’est la commande 0x06

Mettons à jour notre client.py avec cette nouvelle fonction

Pour écraser un endroit avant d’écrire dedans, c’est la commande Sector Erase 0x20 suivis de l’adresse 24 bits à écraser. Cette commande doit également être précédée de Write Enable. Elle permet d’écraser un secteur complet donc 4MB.

Mettons à jour notre client.py avec cette nouvelle fonction

circle-info

Exemple :

On cherche à écrire 0xdeadbeef à l’adresse 0x000018 L’adresse fait partie du première secteur, donc le secteur à l’adresse 0x000000 On active Write Enable, on écrase le secteur, on active encore Write Enable et enfin on écrit, ce qui donne les commandes :

0x06 (Write Enable) 0x20 0x00 0x00 0x00 (Sector Erase) 0x06 (Write Enable) 0x02 0x00 0x00 0x18 0xde 0xad 0xbe 0xef (Program page)

A noter que si d’autres données existaient dans le secteur, elles ont étaient effacées. Pour les concerver, il aurait fallu les lire avant, modifier l’adresse 0x000018 de notre côté puis renvoyer l’ensemble modifié par groupe de 256 bytes (puisque c’est le maximum que l’on peut écrire d’un coup)


Workflow

  1. Récupérer la clé de chiffrement à l’adresse 0x52 du registre n°1 de sécurité

  2. Lire des données dans la mémoire

  3. Déchiffrer les données

  4. Désérialiser les données déchiffrées pour trouver les logs avec le userID 0x5244 (21060)

  5. Modifier les logs concernés (ne pas oublier de recalculant le CRC32 si modification)

  6. Sérialiser les nouveaux logs

  7. Chiffrer les nouvelles données

  8. Ecraser le(s) secteur(s) où se situe les données que l’on souhaite modifier

  9. Ecrire dedans par paquet de 256 bytes


Script de résolution

Commençons par le fichier model.py dans lequel nous créons une classe qui permet de sérialiser et désérialiser des logs mais aussi de calculer leur CRC32 à partir de leur données

Ensuite on a notre client.py avec les nouvelles fonctions que nous avons ajouté

Et enfin, notre main.py qui contient la logique à faire pour résoudre notre challenge

Mis à jour