Skip to content
KitploitKITPLOIT
OutilsExploitsBlog
Log in
Soumettre
OutilsExploitsBlog
Soumettre

Outils de Hacking, PenTest et Cybersécurité pour votre Arsenal de Sécurité !

Kitploit est un répertoire d'outils de hacking, de cybersécurité et de pentesting. Découvrez les dernières mises à jour des projets pour trouver des vulnérabilités, analyser des systèmes, automatiser les tests et renforcer votre sécurité.

··Flux·Contact·Confidentialité·© 2026 Kitploit

Répertoire d'outils

Catégories

Voir toutes les catégories
Loading categories
8cryptdo — Documentation et outils d'ingénierie inverse pour le chiffrement du firmware 8BitDo | Kitploit
Outils/GitHubGitHub/aeromodes/8cryptdo
Sécurité des Systèmes EmbarquésOutils de Chiffrement/DéchiffrementSécurité IoTRétro-ingénierieCryptographieSécurité Matériel et IoTAnalyse de BinairesArticles et RechercheAnalyse de Micrologiciel
GitHubaeromodes/8cryptdo

8cryptdo

Documentation et outils d'ingénierie inverse pour le chiffrement du firmware 8BitDo

29il y a 1 jourPas encore vérifié

Populaires

Voir tout →

Découvrez les outils les plus utilisés par notre communauté.

Explorer tous les outils

Parcourez notre collection d'outils

Voir tous les outils →
Partager
Voir le dépôt

8CryptDo

Documentation et outils issus de l'ingénierie inverse du chiffrement du firmware utilisé par 8BitDo pour plusieurs produits basés sur GD32.

Voir 8cryptdo.py pour un outil de déchiffrement et de rechiffrement appliquant l'algorithme décrit dans ce document.

L'outil permet potentiellement de créer des firmwares personnalisés. Cependant, ce dépôt n'inclut aucune des données de firmware officielles. Un script pour télécharger les derniers fichiers de firmware se trouve dans le dépôt fwupd/8bitdo-firmware, avec quelques anciens binaires de firmware archivés.

En-tête

En observant un fichier de firmware .dat en surface, un en-tête en clair de 28 octets est présent.

root@kitploit:~
import struct

raw = open("sn30-v2_07.dat", "rb").read()
version, addr, payload_len = struct.unpack("<III", raw[:12])
# version     = 207        -> firmware == v2.07
# addr        = 0x08003400 -> destination (probably)
# payload_len = 99328      -> section payload length in bytes

Avec les fichiers de firmware récents, une valeur 32 bits inconnue est placée après celles-ci. Le reste de l'en-tête est à zéro.

Couche de chaînage sans clé

Sur certains fichiers de firmware, un motif apparaît : il existe une couche qui mélange chaque mot dans le suivant.

root@kitploit:~
def rotr(x, r):
    return ((x >> r) | (x << (32 - r))) & 0xFFFFFFFF

dechained = [words[0]]
for i in range(1, len(words)):
    dechained.append(words[i] ^ rotr(words[i - 1], 3))

Ce chaînage se réinitialise à chaque frontière de bloc de 128 mots. Le premier mot d'un bloc (pos = 0) n'a pas de terme prédécesseur, et pos = 1..127 se chaînent depuis le mot précédent.

Une couche sans clé n'ajoute aucune sécurité. La retirer révèle un intermédiaire plus propre (dechained), où la seule chose qui cache encore le texte en clair est le keystream.

root@kitploit:~
P[i] = dechained[i] ^ keystream[i]

Factorisation du keystream

Le dépôt fwupd/8bitdo-firmware contient un catalogue d'anciens firmwares. Ces firmwares semblaient tous avoir la couche de chaînage.

Un XOR de certains d'entre eux après suppression de la couche de chaînage révèle de longues séquences de zéros et une structure lisible.

root@kitploit:~
plaintext_diff = [a ^ b for a, b in zip(dechained_a, dechained_b)]

Si le même keystream est utilisé dans les deux...

root@kitploit:~
a = dechained_a[i] ^ dechained_b[i]
b = (P_a[i] ^ keystream[i]) ^ (P_b[i] ^ keystream[i])
c = P_a[i] ^ P_b[i]
assert a == b == c

le keystream s'annule.

Il s'agit d'un classique many-time pad. Un tel keystream n'est sûr que s'il est utilisé une seule fois. Pour une raison quelconque, 8BitDo a décidé d'utiliser non pas un keystream par produit, mais un seul keystream pour plusieurs produits. Cela a considérablement affaibli la sécurité de leur chiffrement et rendu possible le reste de l'analyse présentée ici.

Lecture du keystream

Une fois que vous savez que deux textes en clair sont XORés ensemble, vous pouvez deviner une partie de l'un et soustraire votre supposition pour lire l'autre. Cela permet de récupérer le keystream à cet endroit.

root@kitploit:~
keystream[i] = dechained[i] ^ P[i]
for fw in all_fw:
    P[fw][i] = dechained[fw][i] ^ keystream[i]

Cela serait plus simple avec des chaînes de caractères, mais l'astuce la plus productive ici a été la relocalisation inter-versions d'instructions ARM. Une fonction peut apparaître dans deux versions de firmware à des positions décalées. Là où le keystream est connu dans une version, vous pouvez aligner le code partagé dans une autre et récolter de nouvelles valeurs de keystream.

Exploiter cela a fait passer les mots de keystream connus d'environ 1 500 à plusieurs milliers, soit environ 18 % du firmware lisible.

Règle du keystream

Avec un échantillon plus large du keystream, des motifs sont devenus visibles. Il avait des positions espacées de 16 mots qui progressaient d'une quantité presque constante, et chaque octet se déplaçait d'une taille prévisible.

Par tâtonnements, il a été découvert que chaque mot de keystream dans un bloc suit une formule :

root@kitploit:~
STEP  = 0x92A753FA
A_MUL = 0x80000301
ROT   = 18
UINT32_MAX  = 0xFFFFFFFF

def rotr(x, r):
    r &= 31
    return ((x >> r) | (x << (32 - r))) & UINT32_MAX if r else x

def block_base(block):
    return (block * A_MUL + block // 2) & UINT32_MAX

def keystream(block, pos, block_key):
    counter = (block_base(block) + pos * STEP) & UINT32_MAX
    mask = rotr(block_key, ROT * pos)
    return counter ^ mask

Au sein d'un bloc, on parcourt un simple compteur (block_base(block) + pos*STEP), et on le XOR avec un masque qui commence à block_key et tourne de 18 bits à chaque étape. Le bloc entier de 128 mots est déterminé par un seul nombre 32 bits, block_key.

Un mot connu déverrouille tout un bloc. En réarrangeant la formule, on obtient le block_key à partir de n'importe quel mot de keystream connu :

root@kitploit:~
def block_key_from_known(block, pos, known_keystream):
    counter = (block_base(block) + pos * STEP) & UINT32_MAX
    return rotr(known_keystream ^ counter, -ROT * pos & 31)

Table de graines

D'où venait la clé de chaque bloc ? Elle se factorise en deux moitiés de 16 bits tirées d'une table de graines de 256 entrées :

root@kitploit:~
def block_key_of(block, seed_table):
    hi = seed_table[block]
    lo = seed_table[block ^ 0xF9]
    return (hi << 16) | lo

La table de graines elle-même ne semble suivre aucune formule spécifique ; il s'agit probablement de 512 octets constants stockés quelque part dans le bootloader de chaque appareil. Cependant, le XOR avec 0xF9 offre un magnifique effet secondaire : une loi miroir. Un bloc et son partenaire block ^ 0xF9 sont construits à partir des deux mêmes entrées de table échangées.

root@kitploit:~
mirror = block_key_of(block ^ 0xF9, seed_table)
assert mirror == rotr(block_key_of(block, seed_table), 16)

Cela a permis de déchiffrer des régions jusqu'alors inconnues. Au lieu de deviner un block_key complet de 32 bits à l'aveugle, vous pouvez deviner deux moitiés de 16 bits et évaluer quel choix transforme les deux blocs en chaînes de caractères ou en instructions ARM plausibles.

Avec cela, une couverture à 100 % de toutes les données de firmware utilisant ce chiffrement spécifique a été obtenue.

Travaux restants

Ce chiffrement semble couvrir la plupart des produits 8BitDo vers 2020, ainsi que les produits actuels qui utilisent encore des SoC GD32 (y compris le SN30 Pro).

Certains appareils comme le M30 et le Zero 2 semblent être multi-sections, avec certaines données de firmware utilisant ce chiffrement et un autre firmware avec un schéma de chiffrement différent.

Les appareils plus récents, du moins ceux avec un SoC différent, semblent avoir un schéma de chiffrement de firmware plus robuste. Une analyse naïve montre qu'il pourrait potentiellement être partagé, mais ce n'est pas certain. Il faut approfondir la recherche.

Télécharger l’outil