
Extracteur de firmware pour microprocesseurs CH55x
L'extracteur de firmware CH55x est utilisé pour lire le firmware des circuits intégrés CH55x. Il n'existe pas de support intégré dans les appareils pour lire directement le firmware via le bootloader. Cependant, il existe une fonctionnalité permettant de vérifier le contenu du firmware 8 octets à la fois par rapport à des données fournies. Cette fonctionnalité est vulnérable à une attaque temporelle classique et c'est ce qui est exploité ici pour permettre l'extraction du firmware de ces appareils. Le bootloader peut être accessible via USB ou UART. Cet extracteur de firmware ne fonctionne que via UART, car la latence plus élevée de l'USB rendrait cette attaque difficile. Les puces testées sont CH552 et CH554, et les versions de bootloader 2.4 et 2.5. La solution matérielle pour l'extracteur de firmware est basée sur un STM32 Blue Pill, car ceux-ci sont facilement disponibles, bon marché et offrent les performances nécessaires.
Le bootloader a déjà été lu depuis l'appareil et son protocole de communication a été rétro-conçu. Ci-dessous se trouve une commande de vérification approximativement correcte et la fonction de vérification utilisée dans le bootloader. Certaines protections sont en place, qui exigent que la longueur soit un multiple de 8, que l'adresse soit alignée sur une limite de 8 octets, que l'adresse soit inférieure à 0x3800 et qu'il n'y ait pas d'erreurs de vérification antérieures. La dernière protection signifie qu'un redémarrage du CH55x est nécessaire après chaque vérification infructueuse.
Nous pouvons voir que la fonction de vérification retourne immédiatement lorsqu'un octet de la vérification échoue. Cela signifie que plus le nombre d'octets corrects est élevé, plus la fonction de vérification prendra de temps. C'est l'exemple classique d'une attaque temporelle qui peut être exploitée.
unsigned char verifycmd[] = {
// 0x57, 0xab, // UART magic not included to verify function
0xa6, // Verify command
5 + len, // Constant 5 plus length of data to verify
0, // Unused
addr_low, // Low byte of address
addr_high, // High byte of address
0, 0, 0, // Unused
0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, // Data to verify against
checksum
}
unsigned char verify(unsignec char *cmdbuffer)
{
static char prev_verify_error;
unsigned char len = cmdbuffer[1]-5
unsigned short addr = cmdbuffer[3] + cmdbuffer[4] << 8;
if (len & 0x07 || addr & 0x07 || addr > 0x3800 || prev_verify_error) {
return 0xfe;
}
for (int i=0; i < len; i++) {
// Key can be set through bootloader, and CBYTE[] means code memory
if(key[i & 0x07] ^ cmdbuffer[8+i] ^ CBYTE[addr+i]) {
prev_verify_error = 1;
return 0xf5;
}
}
return 0;
}
Par essais et erreurs, nous avons constaté que chaque octet correct prolonge le temps d'exécution de la fonction de vérification d'environ 4,2 µs. Le débit en bauds utilisé par le bootloader pour la communication UART est de 57600 (indépendamment de ce que vous lisez ailleurs), ce qui signifie que la transmission d'un bit prend environ 17,4 µs. La relation entre ces deux temps est importante, car il semble que la réponse soit envoyée avec une gigue d'environ 17 µs également. Nous essayons donc de distinguer des réponses qui diffèrent de 4,2 µs dans le temps de réponse de la fonction de vérification, mais qui peuvent différer jusqu'à 17 µs en raison de la gigue UART (temporisation de l'horloge). Cela semble être une tâche difficile, mais elle est possible par des moyens statistiques.
Nous pouvons déterminer si un octet était correct ou non en essayant de le vérifier plusieurs fois et en enregistrant les résultats. Supposons par exemple que le temps le plus court possible pour recevoir une réponse si le premier octet est incorrect est de 30 µs. Alors, avec la gigue UART, nous pouvons nous attendre à un temps de réponse maximal de 30 µs + 17,4 µs = 47,4 µs pour un premier octet invalide. En même temps, un premier octet valide et un second octet invalide pousseraient cette "plage" de 34,2 µs à 51,6 µs. En ajoutant quelques marges, nous pouvons conclure que le premier caractère était invalide si le temps de réponse est inférieur à environ 33 µs. De même, nous pouvons dire que le premier caractère était valide si le temps de réponse était supérieur à 48 µs. C'est la base de l'attaque temporelle utilisée pour extraire le firmware.
Ce n'est pas un outil entièrement automatique pour l'extraction de firmware - une modification du code source et une recompilation seront nécessaires (en utilisant VS Code avec PlatformIO). La raison principale est que les caractéristiques de synchronisation exactes pour chaque octet diffèrent entre les configurations et devront être ajustées. Le processus d'ajustement pourrait être automatisé, mais ce n'était pas l'objectif de ce projet. Le réglage principal se fait via la variable prober_limits. Par exemple, prober_limits[0] contient les limites pour l'octet 0 parmi les 8 octets à vérifier. Si le temps de réponse est inférieur à .invalid_under_time, nous savons que l'octet était invalide. S'il est supérieur à .valid_over_time, nous savons que l'octet était correct. Il y a aussi un .min_delta qui permet de passer à l'octet suivant avant de savoir avec certitude si l'octet est correct ou non.
struct ProberByteLimits prober_limits[8] = {
{
// Byte 0
.invalid_under_time = 33,
.valid_over_time = 50,
.min_delta = 30
}, // ...
Pour trouver les bonnes valeurs à utiliser, il est recommandé de mettre .invalid_under_time à 0, .valid_over_time à par exemple 100, et .min_delta peut être conservé à 30. Cela signifie que le sondeur échouera à trouver le premier octet correct, mais la progression sera imprimée sur l'UART du PC hôte. Vous verrez quelque chose comme :
[0x0000]=0x01? min=31 max=47 tries=63
min=31 max=48 tries=127
min=31 max=48 tries=191
min=30 max=48 tries=255
min=30 max=48 tries=319
À condition que le premier caractère testé soit invalide, nous pouvons utiliser ces valeurs min/max avec un décalage de 1 ou 2 pour déterminer les limites appropriées à utiliser. Par exemple ci-dessus, nous pourrions mettre .invalid_under_time à 33 et .valid_over_time à 50. Le sondeur devrait alors essayer différentes valeurs jusqu'à en trouver une valide, ce qui ressemblerait à ceci :
...
[0x0000]=0x7d? min=31 max=39 tries= 7
[0x0000]=0x02? min=40 max=56 tries= 7
[0x0001]=0x01? min=35 max=52 tries=34
Nous voyons que la dernière tentative à l'adresse 0x0000 avait un temps de réponse maximal de 56 µs, ce qui signifie que c'était l'octet correct. Le sondeur passe ensuite à l'octet suivant et continue le processus. Notez que toutes les limites des 8 octets doivent être réglées séparément, mais une fois fait, cela fonctionnera pour toute la mémoire. Lorsque 8 octets corrects ont été trouvés, il les imprimera au format ihex :
:0800000002002932ffffffff9f
Enregistrer la sortie UART dans un fichier texte permet de faire un grep pour ^: afin d'extraire le contexte mémoire complet au format ihex.
Un exemple de circuit pour l'extraction de firmware est montré ci-dessous. Deux transistors permettent de couper l'alimentation du CH55x depuis la Blue Pill. Ceci est recommandé plutôt que d'utiliser uniquement une réinitialisation logicielle, car la réinitialisation logicielle ne fonctionne que lorsque le CH55x est en mode bootloader (et le bootloader a un délai après lequel il lance le code applicatif). Les résistances sur l'UART vers le CH55x sont incluses car je soupçonne que les pull-ups internes du CH55x pourraient alimenter le CH55x depuis l'UART même lorsque l'alimentation est coupée. La résistance de 10k entre V33 et P3.6 est nécessaire pour mettre le CH55x en mode bootloader. Sur la Blue Pill, PA11 est reliée à la broche RX UART pour pouvoir chronométrer la réponse à l'aide du timer 1.

En ajustant et en utilisant cet outil, le firmware des appareils CH55X peut être extrait. Le processus d'extraction n'est pas rapide, mais extraira les 14 ko en un ou deux jours. Cela dépend un peu du réglage et de la qualité de la table de fréquences utilisée par rapport à l'assembleur réel du firmware. Malheureusement, le code source est un peu désordonné. J'ai créé cet outil parce que j'avais besoin du firmware d'un appareil CH554, et maintenant que je l'ai obtenu, il n'y a pas de raison réelle de travailler sur l'outil lui-même. Néanmoins, il devrait s'avérer utile si quelqu'un a besoin d'extraire le firmware d'appareils CH55x.