SaeLog #2: Komunikacja z pamięcią EEPROM
• tech • 2959 słów • 14 minut czytania
Ta notatka jest częścią serii Saleae Logic Hack. Zapoznaj się z pozostałymi wpisami.
Jedną z pierwszych rzeczy niezbędnych do rozwiązania problemu, a może tylko do poznania mechanizmu działania komunikacji programu z pamięcią EEPROM, jest analiza funkcji, które za to odpowiadają. Obiekt LogicAnalyzerDevice
udostępnia dwie metody do służące do tego celu, są to ReadEeprom
i WriteEeprom
. Wspominałem już o nich w poprzedniej części, ale dopiero niedawno udało mi się im bliżej przyjrzeć i wyłonić obraz ich działania. Dodatkowo w czasie analizy, pojawiło się kilka ciekawych, związanych z tymi funkcjami aspektów. Mowa tutaj o implementacji buforów używanych przez program oraz szyfrowaniu transmisji z urządzeniem, o którym wcześniej nie miałem pojęcia.
Tytuł mówi o komunikacji z pamięcią EEPROM, co może być trochę mylące, bo nie będę mówił o kwestiach hardware-owych ani low-level-owych, ale o komunikacji z tym związanej, z punktu widzenia aplikacji. Czyli bardziej o samej komunikacji aplikacji z urządzeniem w celach związanych z operacjami na pamięci EEPROM. O samym firmware i hardware w kontekście działania z fizycznym układem pamięci podpiętym pod szynę I2C, pewnie będzie w niedalekiej przyszłości.
Bufory
Analizując kod aplikacji można trafić na kilka ciekawych implementacji buforów, używanych w różnych miejscach, w tym także w funkcjach związanych z zapisem i odczytem pamięci EEPROM. Warto się nad nimi trochę zastanowić i je zbadać, na pewno pomoże to w dalszej analizie kodu.
Wydaje mi się, że bufory te bazują na typowym vector
-ze z standardowej biblioteki C++ lub na czymś bardzo podobnym. Przemawiać za tym może ich budowa i implementacja, a także fakt, że gdzieś trafiłem na fragment kodu, który IDA w komentarzach dorzuciła wzmiankę o vectorze.
Jako przykład do analizy może posłużyć kod zaczerpnięty z ReadEeprom
. Najistotniejszy fragment zaczyna się pod adresem 511FA0
:
00511FA0 lea eax, [ebp+var_28] ; vector
00511FA3 push eax
00511FA4 call sub_47F060 ; vector.ctor()
00511FA9 mov eax, 5
00511FAE lea ecx, [ebp+var_28]
00511FB1 mov [ebp+var_4], 2
00511FB8 call sub_503190 ; vector.reserve/resize(5)
00511FBD mov ecx, [ebp+var_18]
00511FC0 sub ecx, [ebp+var_1C] ; length = end - begin
Przydatny może być również fragment stosu:
00511F10 var_28 = dword ptr -28h
00511F10 var_1C = dword ptr -1Ch
00511F10 var_18 = dword ptr -18h
00511F10 var_14 = dword ptr -14h
Komentarzami oznaczyłem wydedukowane fragmenty i przeznaczenie poszczególnych zmiennych oraz instrukcji. Na początek warto zajrzeć do sub_47F060
, który wydaje się być konstruktorem vectora.
0047F060 sub_47F060 proc near
[...]
0047F082 mov esi, [esp+18h+arg_0] ; esi = &var_28
0047F086 push 4
0047F088 call ??2@YAPAXI@Z ; eax = operator new(4)
0047F08D xor ecx, ecx ; ecx = 0
0047F08F add esp, 4
0047F092 cmp eax, ecx ; if eax == nullptr
0047F094 jz short loc_47F09A ; then loc_47F09A
0047F096 mov [eax], esi ; *eax = var_28
0047F098 jmp short loc_47F09C
0047F09A
0047F09A loc_47F09A:
0047F09A xor eax, eax ; eax = 0
0047F09C
0047F09C loc_47F09C:
0047F09C mov [esi], eax ; *var_28 = eax
0047F09E mov [esi+0Ch], ecx ; *var_1C = 0
0047F0A1 mov [esi+10h], ecx ; *var_18 = 0
0047F0A4 mov [esi+14h], ecx ; *var_14 = 0
0047F0A7 mov eax, esi ; eax = esi
[...]
0047F0B6 add esp, 10h
0047F0B9 retn 4
0047F0B9 sub_47F060 endp
Działanie tego kodu jest proste, opisałem je w komentarzach. Po alokacji 4 bajtów pamięci na wskaźnik do nowego obiektu, który de facto wskazuje sam na siebie, i zapisany zostaje w 4 z 12 bajtów zmiennej var_28
. Następnie inicjalizowane są 3 wskaźniki, które są kolejnymi zmiennymi ze stosu znajdującymi się za var_28
…
Inicjalizacja tych wskaźników dokładnie odzwierciedla to co robi cześć kodu inicjująca vector i podstawowe byty w implementacji tego typu w wykonaniu Microsoftu. Jest do bardzo podobne do fragmentu kodu z nagłówka vector
dołączonego STL-a do VC, gdzie znaleźć można template-ową klasę _Vector_val
, zawierającą poniższy fragment:
_Vector_val() { // initialize values
_Myfirst = pointer();
_Mylast = pointer();
_Myend = pointer();
}
pointer _Myfirst; // pointer to beginning of array
pointer _Mylast; // pointer to current end of sequence
pointer _Myend; // pointer to end of array
Kod ten jest bardzo analogiczny do tego fragmentu przedstawionego wyżej. Tak się składa, że _Vector_val
jest jednym z bazowych elementów implementujących typ kontenera vector
. Dodatkowo 12 bajtowy var_28
może być w całości lub częścią obiektu alokatora.
Jako uzupełnienie konstruktora fajnie sprawdzić działanie destruktora. Kompilator bardzo ładnie optymalizuje kod wynikowy, często również rozwija funkcje w miejscu wywołania, gdy uzna to za stosowne. Takie rzeczy mogą trochę utrudniać analizę kodu. Kod destruktora jest rozwijany, dlatego dla przykładu możemy znów posłużyć się fragmentem z ReadEeprom
:
005120B1 ; var_buffer dtor begin
005120B1 mov eax, [ebp+var_buffer.begin]
005120B4 cmp eax, edi ; edi = 0 -> xor edi, edi at 512072
005120B6 jz short loc_5120C1 ; jmp if var_buffer.begin == nullptr
005120B8 push eax
005120B9 call ??3@YAXPAX@Z ; delete(var_buffer.begin)
005120BE add esp, 4
005120C1 loc_5120C1:
005120C1 mov ecx, [ebp+var_buffer.ptr]
005120C4 push ecx
005120C5 mov [ebp+var_buffer.begin], edi ; var_buffer.begin = 0
005120C8 mov [ebp+var_buffer.end], edi ; var_buffer.end = 0
005120CB mov [ebp+var_buffer.max], edi ; var_buffer.max = 0
005120CE call ??3@YAXPAX@Z ; delete(var_buffer.ptr)
005120D3 add esp, 4
005120D3 ; var_buffer dtor end
Kod standardowy, gdy vector posiada zalakowany bufor na dane, to zostaje on zwolniony operatorem delete
, a wszystkie wskaźniki, o których pisałem wyżej zostaną wyzerowane. Na koniec zwolnienie pamięci obiektu i koniec, po vectorze nie ma śladu.
Kolejna funkcja - sub_503190
, występująca w parze z konstruktorem odzwierciedla działanie typowej metody vectora służacej do rezerwacji miejsca - reserve
. Choć bardziej prawdopodobne wydaję się to, że jest to resize
, gdyż późniejsze operacje bezpośrednio przeprowadzane są na buforze, a nie ma nigdzie alokacji i relokacji (przynajmniej w tych miejscach, które mnie interesowały w czasie analizy, pod kątem głównego problemu).
Nie będę tutaj za bardzo przedstawiał poszczególnych fragmentów kodu, ponieważ funkcja ta może nie jest długa i skomplikowana, ale wywołuje masę innych funkcji, więc zajęłoby zbyt dużo miejsca i czasu. Jeśli rzucimy okiem na ich kod to większość sprawdza różne warunki, m. in. długość bufora, alokacji i wykonuje operacje na pamięci. W głównej mierze za pomocą standardowych memmove_s
, memset
oraz operatorów new
i delete
. Co uświadamia mnie, że istnieją małe szanse, co do możliwej mojej pomyłki odnośnie przeznaczenia tejże funkcji.
To właśnie gdzieś w drzewie wywołań tych funkcji trafiłem na wspomniany fragment ze stringiem mogącym zdradzać typ obiektu, o którym mówiłem na początku. W funkcji sub_4F6CF0
trafimy na wykorzystanie stringa:
.rdata:00BF856C aVectorTTooLong db 'vector<T> too long',0
na krótko przed rzuceniem wyjątku:
call __CxxThrowException@8 ; _CxxThrowException(x,x)
Moje przewidywania zostały udowodnione. Teraz wystarczy zmienić nazwy poszczególnych funkcji oraz dodać do definicji strukturę w postaci podobnej do:
struct VectorBuf {
void* ptr; // obj ptr?
int unk[2]; // alocator?
char* begin; // ptr to to beginning of buffer array
char* end; // ptr to end of used buffer array
char* max; // ptr to end of allocated buffer array
};
aby IDA ładnie podpowiadała w kodzie o wystąpieniach i użyciach vectora w roli bufora.
Czytanie i zapisywanie
Pora przejść do meritum. Znając działanie używanych buforów, analiza kodu oraz działania odczytu i zapisu do EEPROM powinna być dużo łatwiejsza. W prezentowanych fragmentach kodu będę pomijał wszelkie walidacje i nieistotne części, dorzucając komentarze i zmieniając nazwy niektórym zmiennym/adresom, aby odzwierciedlały ich przeznaczenie, bazując na moich odkryciach…
Prototyp funkcji ReadEeprom
po krótkiej analizie można przedstawić następująco:
void __stdcall LogicAnalyzerDevice::ReadEeprom(void* obj, int address, char* buffer, int length);
Tutaj taka mała uwaga, prócz zmiennej buffer
przekazywanej w argumencie, który jest zwykłym kawałkiem pamięci, w kodzie używana jest zmienna lokalna, leżąca sobie na stosie, o nazwie var_buffer
, która jest poznanym już buforem:
00511F10 var_buffer = VectorBuf ptr -28h
Jest to jedyna istotna zmienna we fragmentach interesującego kodu. A skoro jest to vectorowy bufor, to pierwszy istotny kod odpowiada za jego inicjalizację i alokację 5 bajtów, dokładnie tak jak opisałem to wyżej:
00511FA9 mov eax, 5
00511FAE lea ecx, [ebp+var_buffer]
[...]
00511FB8 call VectorBuf__Resize ; var_buffer.resize(5)
Najbardziej ciekawy fragment to budowa pakietu danych (żądania) wysyłanego do urządzenia, który wypełnia te 5 zaalokowanych wcześniej bajtów pamięci.
00511FCA mov edx, [ebp+var_buffer.begin]
00511FCD mov byte ptr [edx], 7 ; buffer[0] = 0x07
[...]
00511FE0 mov ecx, [ebp+var_buffer.begin]
00511FE3 mov byte ptr [ecx+1], 33h ; buffer[1] = 0x33
[...]
00511FF7 mov eax, [ebp+var_buffer.begin]
00511FFA mov byte ptr [eax+2], 81h ; buffer[2] = 0x81
[...]
0051200E mov dl, byte ptr [ebp+address]
00512011 mov eax, [ebp+var_buffer.begin]
00512014 mov [eax+3], dl ; buffer[3] = (char)address
[...]
00512027 mov dl, byte ptr [ebp+length]
0051202A mov eax, [ebp+var_buffer.begin]
0051202D mov [eax+4], dl ; buffer[4] = (char)length
A później już wypchnięcie tego do urządzenia przez port USB i oczekiwanie na odpowiedź, która ładowana jest do bufora przekazanego w parametrze.
0051204D mov ebx, [ebp+var_buffer.end]
00512050 sub ebx, [ebp+var_buffer.begin]
[...]
0051205D mov eax, [ebp+var_buffer.begin]
00512060 mov edx, [edi]
00512062 mov edx, [edx+8]
00512065 push ebx ; length
00512066 push eax ; buffer
00512067 lea ecx, [esi+0C0h]
0051206D push ecx ; pipeID
0051206E mov ecx, edi ; WindowsUsbDevice object ptr
00512070 call edx ; WindowsUsbDevice::Write
00512072 xor edi, edi
00512074 mov [ebp+address], edi
[...]
00512097 movzx edx, byte ptr [ebp+length]
0051209B mov ecx, [esi+4] ; WindowsUsbDevice object ptr
0051209E mov eax, [ecx]
005120A0 mov eax, [eax+0Ch]
005120A3 push edx ; length
005120A4 mov edx, [ebp+buffer]
005120A7 push edx ; buffer
005120A8 add esi, 0C4h
005120AE push esi ; pipeID
005120AF call eax ; WindowsUsbDevice::Read
Żadnej magii tutaj nie ma, trochę więcej może dziać się w bliźniaczej funkcji służącej do zapisu.
Prototyp WriteEeprom
jest następujący:
void __stdcall LogicAnalyzerDevice::WriteEeprom(void* obj, VectorBuf* buffer);
WriteEeprom
w roli bufora z danymi do zapisu przyjmuje vector. Podobnie jak przy ReadEeprom
, tutaj również istotną dla nas zmienną jest jedynie var_buffer
leżący na stosie:
00511C50 var_buffer = VectorBuf ptr -24h
który alokowany jest do rozmiaru zdolnego pomieścić zapisywane dane i 5 bajtowy nagłówek pakietu:
00511D00 mov eax, [ebp+10h] ; ebp = buffer
00511D03 sub eax, [ebp+0Ch] ; len = buffer.end - buffer.begin
00511D06 lea ecx, [esp+44h+var_buffer]
00511D0A add eax, 5 ; len += 5
00511D0D call VectorBuf__Resize ; var_buffer.resize(len)
Budowa pakietu podobna jest jak przy odczycie, tylko z innymi wartościami identyfikującymi żądanie.
00511D21 mov eax, [esp+44h+var_buffer.begin]
00511D25 mov byte ptr [eax], 6 ; buffer[0] = 0x06
[...]
00511D3A mov edx, [esp+44h+var_buffer.begin]
00511D3E mov byte ptr [edx+1], 42h ; buffer[1] = 0x42
[...]
00511D54 mov ecx, [esp+44h+var_buffer.begin]
00511D58 mov byte ptr [ecx+2], 55h ; buffer[2] = 0x55
[...]
00511D6E mov eax, [esp+44h+var_buffer.begin]
00511D72 mov byte ptr [eax+3], 8 ; buffer[3] = 0x08
[...]
00511D7A mov ebx, [ebp+10h] ; ebp = buffer
[...]
00511D81 sub ebx, [ebp+0Ch] ; length = buffer.end - buffer.begin
[...]
00511D8E mov edx, [esp+44h+var_buffer.begin]
00511D92 mov [edx+4], bl ; buffer[4] = (char)length
Tutaj mała uwagą, w parametrze nie jest przekazywany do funkcji ani adres ani offset, mówiący w którym miejscu pamięci EEPROM mają zostać zapisane dane. Za to 3 bajt pakietu wskazuje, że wykonane to ma być za 8 bajtem. Na podstawie tej informacji, mogę wnioskować, że zapisać można jedynie całość dodatkowych danych ulokowanych za identyfikatorami urządzenia w EEPROM. Ciekawa informacja.
Dalej, jak można się spodziewać, następuje dodanie do buforu, zaraz za nagłówkiem, danych jakie zostaną zapisane w EEPROM-ie. Dzieje się to w funkcji sub_512FC0
, ale nim to nastąpi dosyć dużo różnej maści instrukcji warunkowych weryfikujących bufory, rozmiary i tym podobne sprawy. Według mnie trochę za dużo i za bardzo pokomplikowane jest to. Podobne rzeczy dzieją się w w/w funkcji. Nie chcę tego tutaj przedstawiać, bo to trochę dużo jest tego kodu, w gruncie nie tak bardzo istotnego, bo ważne jest finalne działanie. Ciekawi mnie jaki był oryginalny kod źródłowy, który wygenerował tyle tych instrukcji… A może to kompilator…
Pomijając te kwestie, finalnie w kodzie sub_512FC0
, użyta zostaje funkcja memmove_s
, do przeniesienia danych z buffer
na koniec var_ buffer
, co odwzorowując w pseudokodzie można przedstawić jako:
memmove_s(var_buffer.begin + 5, buffer.end - buffer.begin, buffer.begin, buffer.end - buffer.begin);
Na koniec zapis danych do portu USB, a tym samym do urządzenia, gdzie odpowiedni kod firmware zajmie się resztą.
00511E99 mov edi, [esp+44h+var_buffer.end]
00511E9D sub edi, [esp+44h+var_buffer.begin]
[...]
00511EAB mov eax, [esp+44h+var_buffer.begin]
00511EAF mov edx, [esi]
00511EB1 mov edx, [edx+8]
00511EB4 push edi ; length
00511EB5 push eax ; buffer
00511EB6 add ebx, 0C0h
00511EBC push ebx ; pipeID
00511EBD mov ecx, esi ; WindowsUsbDevice object ptr
00511EBF call edx ; WindowsUsbDevice::Write
Bazując na informacjach, jakie uzyskałem analizując działanie opisanych wyżej funkcji, całość można skrócić do prostego zapisu formatu danych pakietu (żądania), jaki wysyłany jest do urządzenia w celu operowania na pamięci EEPROM.
read: 07 33 81 ADR LEN
write: 06 42 55 ADR LEN [data]
Pola ADR
i LEN
to 1 bajtowe wartości określające adres, bądź offset względem początku pamięci oraz długość danych na jakich będzie operować dana instrukcja. Przy odczycie długość określa ilość danych do odczytania, a przy zapisie długość danych zawartych w pakiecie, jakie należy zapisać w pamięci.
Szyfrowanie
Przesyłane dane są szyfrowane. Może to za mocne słowo, lepiej tutaj pasuje “zaciemnianie”. Jeśli porównamy zawartość bufora wychodzącą z wyżej opisanych funkcji, z danymi jakie udało się uzyskać z podsłuchania transmisji (chociażby ze wzmianki zawartej w SaeLog #1) to bez trudu dostrzec można, że dane te są różne. Dane wchodzące i wychodzące z funkcji Read
i Write
w WindowsUsbDevice
są inne niż te wysyłane i odbierane na porcie USB.
Początkowo sam się zdziwiłem, gdy to odkryłem. Dopiero wraz z debugerem udało mi się zauważyć, że to właśnie w implementacji WindowsUsbDevice
przed wysłaniem i po odebraniu, dane trafiają najpierw do odpowiednich funkcji. Są nimi sub_4FDBA0
i sub_4FDBE0
, którym można przypisać bardziej adekwatne nazwy, jak EncryptData
i DecryptData
. Parametry do nich przekazywane są poprzez rejestry esi
i edi
, kolejno jako bufor i jego długość. A dane przekształcane są bezpośrednio w buforze źródłowym.
Algorytm szyfrowania danych przy zapisie do portu, zaszyty w kodzie sub_4FDBA0
przedstawia się następująco:
004FDBA0 sub_4FDBA0 proc near
004FDBA0
004FDBA0 var_1 = byte ptr -1
004FDBA0
004FDBA0 push ecx
004FDBA1 xor ecx, ecx
004FDBA3 push ebx
004FDBA4 mov [esp+8+var_1], 9Bh
004FDBA9 mov bl, 54h
004FDBAB test edi, edi
004FDBAD jbe short loc_4FDBDA
004FDBAF nop
004FDBB0
004FDBB0 loc_4FDBB0:
004FDBB0 mov dl, [ecx+esi]
004FDBB3 mov al, dl
004FDBB5 xor al, bl
004FDBB7 xor al, 2Bh
004FDBB9 sub al, 5
004FDBBB xor al, 35h
004FDBBD sub al, 39h
004FDBBF xor al, [esp+8+var_1]
004FDBC3 inc ecx
004FDBC4 xor al, 5Ah
004FDBC6 add al, 50h
004FDBC8 xor al, 38h
004FDBCA sub al, 45h
004FDBCC mov [ecx+esi-1], al
004FDBD0 mov bl, al
004FDBD2 mov [esp+8+var_1], dl
004FDBD6 cmp ecx, edi
004FDBD8 jb short loc_4FDBB0
004FDBDA
004FDBDA loc_4FDBDA:
004FDBDA pop ebx
004FDBDB pop ecx
004FDBDC retn
004FDBDC sub_4FDBA0 endp
Odpowiednik tego kodu zapisany w języku C++ można przedstawić tak:
void EncryptData(char* buffer /*esi*/, size_t len /*edi*/) {
size_t count = 0;
char p = 0x9B;
char h = 0x54;
if (len == 0)
return;
do {
char x;
char c = buffer[count];
x = (((c ^ h ^ 0x2B) - 0x05) ^ 0x35) - 0x39;
x = (((x ^ p ^ 0x5A) + 0x50) ^ 0x38) - 0x45;
buffer[count] = x;
h = x;
p = c;
count++;
} while (count < len);
}
Odwrotność tych operacji można znaleźć w kodzie sub_4FDBE0
służącym do deszyfracji przychodzących danych z urządzenia.
Oczywiście w urządzeniu zaimplementowano ten sam algorytm, w postaci odpowiedników tych funkcji. Dla przykładu kod deszyfrujący z firmware, znajdujący się w funkcji code_10F5
:
000010F5 code_10F5:
000010F5 mov RAM_2F, R3
000010F7 mov RAM_30, R2
000010F9 mov RAM_31, R1
000010FB mov RAM_32, R5
000010FD mov RAM_34, #0x9B
00001100 mov RAM_35, #0x54
00001103 mov RAM_37, #0
00001106
00001106 code_1106:
00001106 mov A, RAM_37
00001108 clr C
00001109 subb A, RAM_32
0000110B jnc code_116D
0000110D mov R3, RAM_2F
0000110F mov R2, RAM_30
00001111 mov R1, RAM_31
00001113 mov R7, RAM_37
00001115 mov DPL0, R7 ; DPTR0 Low Byte
00001117 mov DPH0, #0 ; DPTR0 High Byte
0000111A lcall code_FD1
0000111D mov R7, A
0000111E mov RAM_36, R7
00001120 mov RAM_38, RAM_36
00001123 mov A, #0x45
00001125 add A, RAM_38
00001127 mov RAM_38, A
00001129 xrl RAM_38, #0x38
0000112C mov A, #0xB0
0000112E add A, RAM_38
00001130 mov RAM_38, A
00001132 xrl RAM_38, #0x5A
00001135 mov A, RAM_34
00001137 xrl RAM_38, A
00001139 mov A, #0x39
0000113B add A, RAM_38
0000113D mov RAM_38, A
0000113F xrl RAM_38, #0x35
00001142 mov A, #5
00001144 add A, RAM_38
00001146 mov RAM_38, A
00001148 xrl RAM_38, #0x2B
0000114B mov A, RAM_38
0000114D xrl A, RAM_35
0000114F mov RAM_33, A
00001151 mov R3, RAM_2F
00001153 mov R2, RAM_30
00001155 mov R1, RAM_31
00001157 mov R7, RAM_37
00001159 mov DPL0, R7 ; DPTR0 Low Byte
0000115B mov DPH0, #0 ; DPTR0 High Byte
0000115E mov A, RAM_33
00001160 lcall code_1010
00001163 mov RAM_35, RAM_36
00001166 mov RAM_34, RAM_33
00001169 inc RAM_37
0000116B sjmp code_1106
0000116D
0000116D code_116D:
0000116D ret
0000116D ; End of function code_10F5
Co podobnie, w zapisie bardziej ludzkim, dokładnie odpowiada operacji odwrotnej do szyfrowania:
void DecryptData(char* buffer /*esi*/, size_t len /*edi*/) {
size_t count = 0;
char p = 0x9B;
char h = 0x54;
if (len) {
do {
char x;
char c = buffer[count];
x = (((c + 0x45) ^ 0x38) - 0x50) ^ 0x5A ^ p;
x = (((x + 0x39) ^ 0x35) + 0x5) ^ 0x2B ^ h;
buffer[count] = x;
h = c;
p = x;
count++;
printf("%x ", x);
} while (count < len);
}
}
O firmware, sposobach jego wydobycia i analizy pewnie napiszę w którymś z następnych wpisów.
Znając już sposób szyfrowania, bez problemu możemy potwierdzić, że działa to tak jak należy. Jeśli weźmiemy dane wysyłane do urządzenia przy pobieraniu identyfikatora (funkcja GetIdFromDevice
) i przepuścimy przez te obliczenia, to w wyniku otrzymamy dokładnie to co wyleci z USB:
input: 07 33 81 08 08
output: DF C6 B3 51 60
Na podstawie załączonych kodów wyraźnie widać, że z prawdziwym szyfrowaniem nie ma to wiele wspólnego, ot proste operacje matematyczne i logiczne.
…
Znając już techniczne niuanse związane z komunikacją programu z urządzeniem, jeśli chodzi o pamięć EEPROM, to można spróbować stworzyć mapę pamięci odzwierciedlającą jej zawartość i znaczenie poszczególnych danych w niej ulokowanych. Może to być ciekawe, choć na razie nie jest mi to potrzebne.
Teraz można przejść do zabaw z firmware, skoro mamy pojęcie jak to działa w aplikacji i wiemy, że to właśnie tam mieści się cała magia. Na tym pewnie będę się koncentrował w najbliższym czasie.
Komentarze (0)