Skip to content
KitploitKITPLOIT
StrumentiExploitsBlog
Log in
Invia
StrumentiExploitsBlog
Invia

Strumenti di Hacking, PenTest e Cybersecurity per il tuo Arsenale di Sicurezza!

Kitploit è una directory di strumenti di hacking, cybersecurity e pentesting. Scopri gli ultimi aggiornamenti dei progetti per trovare vulnerabilità, analizzare sistemi, automatizzare i test e rafforzare la tua sicurezza.

··Feed·Contatto·Privacy·© 2026 Kitploit

Directory degli strumenti

Categorie

Vedi tutte le categorie
Loading categories
8cryptdo — Documentazione e strumenti di reverse engineering per la crittografia del firmware 8BitDo | Kitploit
Strumenti/GitHubGitHub/aeromodes/8cryptdo
Sicurezza Sistemi EmbeddedStrumenti di Crittografia/DecrittografiaSicurezza IoTReverse EngineeringCrittografiaSicurezza Hardware e IoTAnalisi di BinariPaper e RicercaAnalisi del Firmware
GitHubaeromodes/8cryptdo

8cryptdo

Documentazione e strumenti di reverse engineering per la crittografia del firmware 8BitDo

291 giorno faNon ancora revisionato

Più Popolari

Vedi tutti →

Scopri gli strumenti più utilizzati dalla nostra community.

Esplora tutti gli strumenti

Sfoglia la nostra collezione di strumenti

Vedi tutti gli strumenti →
Condividi
Vedi Repository

8CryptDo

Documentazione e strumenti ottenuti tramite reverse engineering per la cifratura del firmware utilizzata da 8BitDo per diversi prodotti basati su GD32.

Vedere 8cryptdo.py per uno strumento di decifratura e ri-cifratura che applica l'algoritmo descritto in questo documento.

Lo strumento potenzialmente abilita la possibilità di creare firmware personalizzati. Tuttavia, questo repository non include alcuno dei dati firmware ufficiali. Uno script per scaricare i file firmware più recenti può essere trovato nel repository fwupd/8bitdo-firmware, con alcuni binari firmware più vecchi archiviati.

Header

Osservando superficialmente un file firmware .dat, è presente un header in chiaro di 28 byte.

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

Con i file firmware recenti c'è un valore a 32 bit sconosciuto posizionato dopo questi. Il resto dell'header è zero.

Livello di concatenamento senza chiave

Su alcuni file firmware compare un pattern: c'è un livello che mescola ogni parola nella successiva.

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))

Questo concatenamento si azzera a ogni confine di blocco di 128 parole. La prima parola di un blocco (pos = 0) non ha termine predecessore, e pos = 1..127 si concatena dalla parola precedente.

Un livello senza chiave non aggiunge sicurezza. Rimuoverlo rivela un intermedio più pulito (dechained), dove l'unica cosa che nasconde ancora il plaintext è il keystream.

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

Fattorizzazione del keystream

Il repository fwupd/8bitdo-firmware ha un catalogo di firmware più vecchi. Questi firmware sembravano tutti avere il livello di concatenamento.

Facendo lo XOR di alcuni di essi tra loro dopo aver rimosso il livello di concatenamento si rivelano lunghe sequenze di zeri e struttura leggibile.

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

Se lo stesso keystream è usato in entrambi...

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

il keystream si annulla.

Questo è un classico many-time pad. Un tale keystream è sicuro solo se usato una volta. Per qualche motivo, 8BitDo ha deciso di usare non solo un keystream per prodotto, ma un singolo keystream su diversi prodotti. Questo ha indebolito significativamente la sicurezza del loro cifrario e ha reso possibile il resto dell'analisi qui presentata.

Leggere il keystream

Una volta che sai che due plaintext sono XOR-ed insieme, puoi indovinare parte di uno e sottrarre la tua ipotesi per leggere l'altro. Questo recupera il keystream in quel punto.

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

Sarebbe più semplice con le stringhe, ma il trucco più produttivo qui è stata la rilocazione cross-version delle istruzioni ARM. Una funzione può apparire in due versioni firmware in posizioni spostate. Dove il keystream è noto in una versione, puoi allineare il codice condiviso in un'altra e raccogliere nuovi valori di keystream.

Sfruttare questo ha portato le parole di keystream note da ~1.500 a diverse migliaia, circa il 18% del firmware leggibile.

Regola del keystream

Con un campione più ampio del keystream, i pattern sono diventati visibili. Aveva posizioni distanti 16 parole che avanzavano di una quantità quasi costante, e ogni byte si spostava di una dimensione prevedibile.

Attraverso tentativi ed errori, è stato scoperto che ogni parola di keystream in un blocco segue una formula:

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

All'interno di un blocco, si percorre un semplice contatore (block_base(block) + pos*STEP), e lo si fa XOR con una maschera che parte da block_key e ruota di 18 bit a ogni passo. L'intero blocco di 128 parole è determinato da un singolo numero a 32 bit, block_key.

Una parola nota sblocca un intero blocco. Riordinando la formula si ottiene il block_key da qualsiasi parola di keystream nota:

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)

Tabella dei seed

Da dove veniva la chiave di ogni blocco? Si fattorizza in due metà a 16 bit prese da una tabella di seed di 256 voci:

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

La tabella dei seed stessa sembra non avere una formula specifica, probabilmente sono 512 byte costanti memorizzati da qualche parte nel bootloader di ogni dispositivo. Tuttavia, lo XOR con 0xF9 fornisce un bellissimo effetto collaterale: una legge speculare. Un blocco e il suo partner block ^ 0xF9 sono costruiti dalle stesse due voci della tabella scambiate.

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

Questo ha decifrato regioni precedentemente sconosciute. Invece di indovinare un intero block_key a 32 bit alla cieca, puoi indovinare due metà a 16 bit e valutare quale scelta trasforma entrambi i blocchi in stringhe o istruzioni ARM credibili.

Con questo, è stata ottenuta una copertura del 100% di tutti i dati firmware che utilizzano questo specifico cifrario.

Lavoro rimanente

Questo cifrario sembra coprire la maggior parte dei prodotti 8BitDo intorno al 2020, e i prodotti attuali che usano ancora SoC GD32 (incluso l'SN30 Pro).

Alcuni dispositivi come l'M30 e lo Zero 2 sembrano essere multi-sezione, con alcuni dati firmware che usano questo cifrario e un altro firmware con uno schema di cifratura diverso.

I dispositivi più recenti, almeno quelli con un SoC diverso, sembrano avere uno schema di cifratura del firmware più robusto. Un'analisi ingenua mostra che potrebbe potenzialmente essere condiviso, ma non è certo. Bisogna indagare ulteriormente.

Scarica lo strumento