Odczyt i zapis przekładni kierowniczej BMW ze wspomaganiem elektrycznym (EPS) przez FlexRay
Niewielki agent dla MPC5643L działający wprost w przekładni: odczytuje i zapisuje flash oraz EEPROM przekładni przez FlexRay — bez JTAG i bez otwierania obudowy.
Notatki ze stanowiska, od strony warsztatu. Wszystko opisane wykonano na zdemontowanych podzespołach na moim własnym stanowisku — jako praca z zakresu regeneracji i diagnostyki, i wszystko to pochodzi z firmware’u: obraz otwarty w Ghidrze i zdezasemblowany, zweryfikowany względem moich własnych zrzutów i dużej ilości znanego tekstu jawnego. Fabrycznego dostępu zabezpieczonego (security access) OEM nie publikuję — to nie moje, a w agencie i tak go nie ma.
W skrócie
Sterownik układu kierowniczego w sporej części gamy BMW serii F (wewnętrzny znacznik bootloadera 000019EE, dalej „19EE”) zbudowano wokół NXP MPC5643L — dwurdzeniowego PowerPC w trybie lockstep z listą rozkazów VLE — a jego dane kalibracyjne są podzielone między wbudowany flash C90FL i zewnętrzny SPI EEPROM M95640. Fabryczne oprogramowanie komunikuje się z nim przez FlexRay.
Chciałem odczytywać i zapisywać obie pamięci na stanowisku, przez FlexRay, bez podłączania sondy JTAG do każdego pojedynczego egzemplarza. Drogą do tego był maleńki samodzielny agent, ładowany do SRAM przekładni przez FlexRay za pomocą mechanizmu ładowania, który wygrzebałem z firmware’u w Ghidrze; agent obsługuje operacje na pamięci, odsyłając dane z powrotem na mój kabel. Ten wpis omawia agenta: obsługę FlexRay, sterowniki EEPROM i flash oraz jedną sztuczkę protokołu, która naprawiła niestabilny odczyt.

stanowisko. Moduł MS561 do EPS (zasilanie i diagnostyka), kabel FlexRay, przekładnia 19EE, ThinkPad z uruchomionym MS561 oraz oscyloskop u góry.
Obiekt
MPC5643L to mikrokontroler nastawiony na bezpieczeństwo funkcjonalne: dwa rdzenie e200z4 w trybie lockstep, ECC wszędzie i wbudowany na krzemie kontroler FlexRay E-Ray firmy Freescale/NXP. Liczby istotne dla całej dalszej części:
| Co | Gdzie |
|---|---|
| Rdzeń | e200z4, PowerPC VLE (zmienna długość rozkazów), big-endian |
| Flash kodu | kontroler C90FL pod adresem 0xC3F88000, flash odwzorowany na adres bazowy 0x0 |
| „block0” | 0x0000–0x3FFF (16 KB) — kalibracja + konfiguracyjne CRC-32 |
| EEPROM | zewnętrzny M95640 (64 Kbit, SPI) na DSPI_B pod adresem 0xFFF94000 |
| FlexRay | kontroler komunikacyjny (CC) E-Ray firmy NXP pod adresem 0xFFFE0000, message RAM w systemowej SRAM |
Jeden haczyk z VLE na start: większość standardowych kompilacji objdump nie dekoduje VLE — binutils z 2005 roku daje śmieci. Analizę statyczną robiłem w Ghidrze z językiem PowerPC:BE:64:VLE-32addr. Pomyl tryb, a cały obraz dekoduje się w bezsens.

układ. Główny krzem przekładni — SPC5643LFMLQ1, czyli MPC5643L — a po jego lewej mały 8-nóżkowy SPI EEPROM M95640.
Dlaczego agent, a nie „po prostu JTAG do wszystkiego”
Przez długi czas jedyną drogą do tych danych była brutalna „chirurgia”: otworzyć obudowę sterownika, wylutować EEPROM, odczytać i przeprogramować go na tanim zewnętrznym programatorze, a potem wlutować z powrotem. Działa — wiele warsztatów wciąż tak robi — ale jest to wolne i żmudne, a każdy taki cykl to kolejna okazja, by zerwać pole lutownicze albo przegrzać układ. JTAG jest schludniejszy i to jego używałem na etapie inżynierii wstecznej, ale i tak oznacza rozcięcie szczelnej obudowy, by dostać się do pól debugowych. Żadnego z tych zabiegów nie chce się powtarzać egzemplarz po egzemplarzu na stanowisku.

etap RE. Jak odczytywałem ją podczas inżynierii wstecznej: obudowa otwarta, programator JTAG P&E na złączu debugowym przekładni. Dobre do laboratorium — ale nie coś, co chce się robić z każdym egzemplarzem.
Agent eliminuje to wszystko. Wchodzi do środka przez FlexRay — magistralę, która i tak jest na złączu — więc egzemplarz pozostaje zamknięty: nie trzeba otwierać obudowy, wylutowywać układu ani zrywać pól.
Grzebiąc w firmwarze w Ghidrze, znalazłem, że mechanizm już tam jest: w bootloaderze istnieje ścieżka, która wciąga mały program do wolnej SRAM przekładni przez FlexRay i go uruchamia. Napisałem więc własnego agenta pod tę ścieżkę, którego zadaniem jest po prostu:
odpytywać slot FlexRay o polecenia, wykonać operację na pamięci i odpowiedzieć w innym slocie.
Bez libc, bez RTOS, bez zależności. Wskaźnik stosu i punkt wejścia dostaje od bootloadera, emituje sygnaturę „żyję”, bym mógł potwierdzić, że działa właśnie mój kod, po czym tkwi w pętli poleceń.
Krok pierwszy: odblokowanie trybu inżynierskiego
Zanim to wszystko zadziała, przekładnię trzeba wprowadzić w jej tryb inżynierski, a ten jest zamknięty za dostępem zabezpieczonym UDS (security access). Sekwencja to zwykły UDS:
- DiagnosticSessionControl do sesji rozszerzonej (
10 03), następnie do sesji inżynierskiej (10 42). - SecurityAccess — poziom „13/14”: żądanie seeda (
27 13), przekładnia zwraca 8-bajtowy seed; obliczamy odpowiedź; wysyłamy klucz (27 14). Klucz to 4-bajtowe pole długości, po którym następuje 128-bajtowy podpis. - RoutineControl Start (
31 01 03 0C) — to właśnie uzbraja tryb inżynierski. Procedurę 030C zlokalizowałem w Ghidrze; jej dyspozytor RoutineControl znajduje się pod adresem0x635a0.
Jedyną rzeczą, którą zachowuję dla siebie, jest środkowy krok — jak powstaje ten podpis i jaki klucz za nim stoi. To zostaje na moim stanowisku. Jak BMW to „warzy”? Idźcie spytać ich. 😉
Krok drugi: jak dostarczyć agenta do układu
W firmwarze jest bootloader, który przyjmuje kawałek kodu do wolnej SRAM przez FlexRay i go uruchamia. Rozłożyłem go na części w Ghidrze:
- Polecenie SETUP (
00 03) przełącza zadanie odbiorcze bootloadera (zadanie RTOS; jego dyspozytor pod0x80000, tablica obsługi pod0x40009098) w tryb odbioru; przekładnia odpowiada potwierdzeniem (00 08 51). - Odbiornik ramek pod adresem
0x8c664kopiuje przychodzące dane do SRAM tylko wtedy, gdy znacznik stanu pod0x7e40jest uzbrojony na0xFE/0xFF. W przeciwnym razie każda ramka jest po cichu odrzucana. - Ten znacznik uzbraja samo wejście w tryb inżynierski: sesja UDS ustawia bajt stanu (
0x7944) na1, RoutineControl030Curuchamia swój handler, a procedura wejścia pod adresem0x81010zapisuje0x7e40 = 0xFEi startuje zadanie.
Haczyk, który kosztował mnie czas: SETUP jest przyjmowany, potwierdzenie wraca, ale paczka i tak nie „ląduje”. Zrzut RAM przekładni (ICDPPCNEXUS Hotsync, bez resetu) pokazał 0x7e40 = 0 — zadanie odbiorcze nie było uzbrojone, więc każda ramka odpadała. Szybsze wysyłanie nie pomaga: odbiornik musi być uzbrojony i pozostać uzbrojony do chwili nadejścia strumienia, więc kolejność jest taka: uzbroić 030C, poczekać na podniesienie klastra FlexRay, dopiero potem wysłać paczkę. Gdy znacznik jest utrzymany, kod ląduje w SRAM, uruchamia się, a agent oznajmia się swoją ramką „żyję” (00 4C 8A A0 A1 … BF). Bez JTAG, obudowa zamknięta.
FlexRay oczami E-Ray
Wszystko płynie przez dwa bufory komunikatów FlexRay (message buffers, MB): na jednym agent nasłuchuje, w drugim odpowiada. Message RAM kontrolera E-Ray to tablica nagłówków plus obszar danych, a konfiguracja bufora znajduje się w rejestrach pod adresem ERAY_BASE + 0x100 + idx*8.

na łączu. Kabel podsłuchuje linię FlexRay RX przekładni, gdy rozpracowywałem format ramek — tu zapis na żywo z ~580 tys. zboczy/s przy częstotliwości próbkowania 275 MHz.
eng_agent.c — mapa buforów komunikatów E-Ray
#define ERAY_BASE 0xFFFE0000u /* E-Ray CC; MVR @ +0 reads 0xA268 */
#define FR_MEMBASE 0x40002A00u /* message RAM base = SYMBADHR:SYMBADLR */
#define MBCCSR(i) (*(volatile u16 *)(ERAY_BASE + 0x100u + (u32)(i)*8u + 0u))
#define MBCCFR(i) (*(volatile u16 *)(ERAY_BASE + 0x100u + (u32)(i)*8u + 2u))
#define MBFIDR(i) (*(volatile u16 *)(ERAY_BASE + 0x100u + (u32)(i)*8u + 4u))
#define MBIDXR(i) (*(volatile u16 *)(ERAY_BASE + 0x100u + (u32)(i)*8u + 6u))
#define MEM16(off) (*(volatile u16 *)(FR_MEMBASE + (u32)(off)*2u))
/* MBCCSR bits */
#define MB_MTD 0x1000u /* transmit direction */
#define MB_CMT 0x0800u /* commit (TX) */
#define MB_LCKT 0x0200u /* lock toggle */
#define MB_DVAL 0x0008u /* data valid (RX) */
#define MB_LCKS 0x0002u /* locked status */
#define MB_MBIF 0x0001u /* interrupt flag */
#define RX_MB 2u /* I listen on slot 3 (frameID 3 = MB index 2) */
#define TX_MB 0u /* I answer on slot 1 (frameID 1 = MB index 0) */
#define KEEP 0xF900u /* config bits I must preserve on every CSR write */
Aby sięgnąć do bufora, trzeba go zablokować (przełączyć LCKT i potwierdzić, że LCKS wrócił ustawiony). To, co mnie tu ugryzło:
Bufor nadawczy (TX MB) sprzętowo retransmituje swoją zawartość w każdym cyklu FlexRay. Gdy kontroler kopiuje bufor na magistralę, blokada należy do niego, a pojedyncza próba „przełącz i miej nadzieję” przegrywa wyścig częściej, niżby się chciało.
Naiwna jednorazowa blokada po cichu zwraca „nie udało się”, odpowiedź nigdy nie wychodzi, drugi koniec widzi, że blok zamilkł, a odczyt urywa się lub pełznie, podczas gdy druga strona wciąż dopytuje. Rozwiązaniem jest kręcić się w pętli, aż kontroler odda bufor — wolne okno jest w każdym cyklu. Ponawianie jest bezpieczne: odrzucony zapis LCKT jest po prostu ignorowany (status bez zmian), więc stanu nie da się uszkodzić, a zatrzymujesz się w chwili, gdy LCKS odczytuje się jako ustawiony — nigdy więc nie zdejmiesz blokady, którą już trzymasz.
mb_lock — kręcimy się, aż CC odda bufor
/* Lock a MB. Return 1 if locked, 0 if the CC never yielded it.
The TX MB retransmits every cycle and the CC owns it while copying out,
so a single LCKT write often loses the race. Spin until a free window. */
static int mb_lock(u32 i)
{
u32 spin;
for (spin = 0; spin < 200000u; spin++) {
MBCCSR(i) = (u16)((MBCCSR(i) & KEEP) | MB_LCKT);
if (MBCCSR(i) & MB_LCKS) return 1;
}
return 0;
}
static void mb_unlock(u32 i) { MBCCSR(i) = (u16)((MBCCSR(i) & KEEP) | MB_LCKT); }
static void mb_clrflag(u32 i) { MBCCSR(i) = (u16)((MBCCSR(i) & KEEP) | MB_MBIF); }
Odpytanie polecenia wygląda wtedy tak: zablokować, sprawdzić DVAL (nowa ramka?), odczytać nagłówek, by ustalić, gdzie leży ładunek i jak jest długi, skopiować go, odblokować. Wysłanie odpowiedzi jest lustrzanym odbiciem.
rx_poll / tx_send — jedno polecenie na wejściu, jedna odpowiedź na wyjściu
/* Poll the RX slot. On a new frame copy up to maxw words into dst, return #words. */
static int rx_poll(u16 *dst, int maxw)
{
u16 idx, hdr, dataoff, len, p;
if (!mb_lock(RX_MB)) return 0;
if (!(MBCCSR(RX_MB) & MB_DVAL)) { mb_unlock(RX_MB); return 0; }
mb_clrflag(RX_MB);
idx = MBIDXR(RX_MB);
hdr = (u16)(idx * 5u); /* header = idx*5 halfwords */
dataoff = (u16)(MEM16(hdr + 3) / 2u); /* header[3] = data byte offset */
len = (u16)(MEM16(hdr + 1) & 0x7Fu); /* header[1] low 7 bits = words */
if (len > maxw) len = (u16)maxw;
for (p = 0; p < len; p++) dst[p] = MEM16(dataoff + p);
mb_unlock(RX_MB);
return (int)len;
}
/* Write the response words into the TX slot and commit. */
static void tx_send(const u16 *src, int words)
{
u16 idx, hdr, dataoff; int p;
if (!mb_lock(TX_MB)) return; /* CC busy: skip; the far side re-asks (robust) */
mb_clrflag(TX_MB);
idx = MBIDXR(TX_MB);
hdr = (u16)(idx * 5u);
dataoff = (u16)(MEM16(hdr + 3) / 2u);
for (p = 0; p < words; p++) MEM16(dataoff + p) = src[p];
MBCCSR(TX_MB) = (u16)((MBCCSR(TX_MB) & KEEP) | MB_CMT); /* commit */
mb_unlock(TX_MB);
mb_clrflag(TX_MB);
}
Sztuczka, która uczyniła odczyt niezawodnym: echo w odpowiedzi
Zachowanie „retransmituje co cykl” ma drugi skutek. Ponieważ bufor TX wciąż wystawia na magistralę swoją ostatnią zawartość, odbiornik może zatrzasnąć przeterminowaną kopię poprzedniej odpowiedzi — szczególnie przy pierwszym odczycie po zmianie stanu: zostajesz jeden request w tyle, pytasz o adres N, a dostajesz bajty dla N-1, i wygląda to niemal poprawnie — aż do chwili, gdy zdecydowanie takie nie jest.
Opóźnienia na ustabilizowanie i ponawiania to maskują, ale to wyścig i jest wolny. Deterministycznym rozwiązaniem jest uczynić każdą odpowiedź samoidentyfikującą się. Agent wstawia do ramki dwa dodatkowe słowa: znacznik echa (żądany adres albo stała wartość kontrolna dla zapisu) oraz magiczny znacznik odpowiedzi. Kabel odrzuca wtedy każdą odpowiedź, której echo nie zgadza się z właśnie wysłanym żądaniem. Żadnego timingu, żadnego zgadywania — błędna ramka jest odrzucana strukturalnie.
build_response — ramka odpowiedzi + znacznik echa
#define REPLY_MAGIC 0x4321u /* "this is a real data reply" tag */
#define WRPOS_ECHO 0x5A5Au /* sentinel for the destructive write reply */
/* Build "00 4C 8A <32 data bytes>" from 32 source bytes into the frame buffer. */
static void build_response(u16 *frame, const u8 *data32)
{
u8 b[36]; int i;
b[0] = 0x00; b[1] = 0x4C; b[2] = 0x8A; /* my fixed reply header */
for (i = 0; i < 32; i++) b[3 + i] = data32[i];
for (i = 0; i < 62; i++) frame[i] = 0;
/* pack byte pairs little-end-first into each MB halfword (verified on the bench) */
for (i = 0; i < 18; i++) frame[i] = (u16)((b[2*i + 1] << 8) | b[2*i]);
}
/* ...and at each call site, right before tx_send: */
frame[18] = (u16)(addr & 0xFFFFu); /* echo the requested address */
frame[19] = REPLY_MAGIC; /* tag it as a fresh data reply */
Z echem na miejscu pełne odczyty po 16 KB wracają za każdym razem w całości, zamiast urywać się w połowie. Jeśli walczyłeś kiedyś ze statycznymi slotami FlexRay, wiesz, jaki rodzaj kapryśnego błędu to zabija.

odczyt. Pełny odczyt flash block0 w MS561: 16 KB z powrotem, hex na ekranie, a konsola kończy się liniami flash block0 read OK (16384 bytes) i Flash integrity OK. Agent jest aktywny (Agent connected), a na egzemplarzu nie ma JTAG.
Odczyt EEPROM — przywracanie wyprowadzeń SPI do życia
M95640 wisi na DSPI_B. Haczyk: firmware odczytuje go raz przy starcie, a następnie zdejmuje multipleksowanie z wyprowadzeń, więc gdy agent rusza, te nóżki nie są już SPI. Pierwszy krok to przywrócić je z powrotem — zmultipleksować CS/SCK/SOUT jako wyjścia, a SIN jako wejście przez rejestry sterowania wyprowadzeniami SIUL, a następnie podnieść DSPI_B jako wolnego, bezpiecznego 8-bitowego mastera. (Numery wyprowadzeń i CTAR wzięte wprost z własnej, startowej procedury obsługi EEPROM w przekładni.)
Klasyczny odczyt SPI-EEPROM to kod operacji 0x03, 16-bitowy adres, a potem bajty jałowe (dummy) wytaktowane, by odczytać dane z powrotem. Jedyny niuans na tym DSPI: sygnał wyboru układu (chip-select) musi pozostać aktywny przez całą transakcję, więc ramki polecenia/adresu/jałowe wypychasz jedna za drugą z ustawionym bitem kontynuacji, zdejmując CS dopiero na ostatniej. Jedno oczekiwanie na każdy bajt powoduje opróżnienie FIFO i zdjęcie CS w trakcie transferu. Dlatego każdy bajt danych to własna, schludna seria 4 ramek.
spi_ee_read — seryjny odczyt M95640 na DSPI_B
#define DSPI_B 0xFFF94000u
#define DSPI_MCR (*(volatile u32 *)(DSPI_B + 0x00u))
#define DSPI_CTAR0 (*(volatile u32 *)(DSPI_B + 0x0Cu))
#define DSPI_SR (*(volatile u32 *)(DSPI_B + 0x2Cu))
#define DSPI_PUSHR (*(volatile u32 *)(DSPI_B + 0x34u))
#define DSPI_POPR (*(volatile u32 *)(DSPI_B + 0x38u))
static void spi_ee_init(void)
{
*(volatile u16 *)0xC3F9004Au = 0x0600u; /* PCR5 CS0 out */
*(volatile u16 *)0xC3F9004Cu = 0x0600u; /* PCR6 SCK out */
*(volatile u16 *)0xC3F9004Eu = 0x0600u; /* PCR7 SOUT out */
*(volatile u16 *)0xC3F90050u = 0x0100u; /* PCR8 SIN in */
DSPI_MCR = 0x80010C00u; /* master, PCS0 idle-high, flush FIFOs, running */
DSPI_CTAR0 = 0x38004448u; /* 8-bit frame, mode0, slow baud (safe) */
}
/* READ (0x03): each byte is a 4-frame burst [cmd, addrHi, addrLo(CONT), dummy].
The 4th RX byte is the data; CS stays low for the whole burst. */
static void spi_ee_read(u16 addr, u8 *dst, int len)
{
int i;
spi_ee_init();
for (i = 0; i < len; i++) {
u16 a = (u16)(addr + i);
volatile u32 to = 0;
DSPI_MCR = 0x80010C00u; /* flush FIFOs */
DSPI_SR = 0xFFFF0000u; /* clear status */
DSPI_PUSHR = 0x80010000u | 0x03u; /* READ (CONT) */
DSPI_PUSHR = 0x80010000u | (u32)(a >> 8); /* addr hi(CONT) */
DSPI_PUSHR = 0x80010000u | (u32)(a & 0xFFu); /* addr lo(CONT) */
DSPI_PUSHR = 0x00010000u | 0xFFu; /* dummy, drop CS */
while (((DSPI_SR >> 4) & 0xFu) < 4u) if (++to > 200000u) break;
(void)DSPI_POPR; (void)DSPI_POPR; (void)DSPI_POPR; /* cmd/addr phases */
dst[i] = (u8)(DSPI_POPR & 0xFFu); /* 4th frame = data */
}
}
Zapis EEPROM — tylko te bajty, które faktycznie się zmieniły
Zapis M95640 to podręcznikowy taniec: WREN (zezwolenie na zapis, 0x06), potem WRITE (0x02) + adres + dane, a następnie odpytywanie bitu WIP rejestru statusu, aż zakończy się cykl zapisu (~5 ms). Ponownie cały blok cmd+addr+data idzie jedną serią z CS w stanie niskim.
spi_ee_write_byte — WREN / WRITE / odpytanie WIP
static void spi_ee_write_byte(u16 addr, u8 val)
{
volatile u32 to = 0;
spi_ee_init();
/* WREN */
DSPI_SR = 0xFFFF0000u;
DSPI_PUSHR = 0x00010000u | 0x06u;
to = 0; while (((DSPI_SR >> 4) & 0xFu) < 1u) if (++to > 200000u) break;
(void)DSPI_POPR;
/* WRITE 0x02 + addrHi + addrLo + data (CS held, drop after data) */
DSPI_MCR = 0x80010C00u; DSPI_SR = 0xFFFF0000u;
DSPI_PUSHR = 0x80010000u | 0x02u;
DSPI_PUSHR = 0x80010000u | (u32)(addr >> 8);
DSPI_PUSHR = 0x80010000u | (u32)(addr & 0xFFu);
DSPI_PUSHR = 0x00010000u | (u32)val;
to = 0; while (((DSPI_SR >> 4) & 0xFu) < 4u) if (++to > 200000u) break;
(void)DSPI_POPR; (void)DSPI_POPR; (void)DSPI_POPR; (void)DSPI_POPR;
/* poll WIP until the write cycle completes (~5 ms) */
to = 0; while ((spi_ee_rdsr() & 0x01u) && (++to < 100000u)) { }
}
Dwie świadome decyzje na rzecz bezpieczeństwa po stronie sterowania tym:
- Polecenie zapisu niesie bajt zabezpieczający. Zapis jest honorowany tylko wtedy, gdy w słowie polecenia obecna jest stała wartość zabezpieczająca. Błędna lub uszkodzona ramka nie może przypadkiem zaprogramować EEPROM — jest po prostu odrzucana.
- Host zapisuje różnicę, a nie cały blok. Po stronie MS561 trzymam ostatni pełny odczyt EEPROM jako migawkę; zapis dotyka tylko bajtów różniących się od niej (i nigdy ulotnych pól czasu pracy), po czym odczytuje okno z powrotem i weryfikuje, że wszystko się zapisało. Jeśli kiedyś „zacegliłeś” moduł, nadpisując bajt, którego nie zamierzałeś ruszać, zrozumiesz, po co to.
W warsztacie to zwykle strona danych naprawy po przeskoku paska. BMW zapisuje to jako kod usterki (DTC) 0x482452 — „EPS steering angle sensor: belt jump detected” w naszym dekoderze — ze sprzętowym bliźniaczym kodem 0x4822D9 („…steering angle invalid”) oraz licznikiem przeskoków paska w DID 0xE33C. Wbrew nazwie sam pasek jest zwykle sprawny; to po prostu dane zapisane w module przestały pasować do przekładni, a przeprogramowanie czy kodowanie tego nie skasuje. Dopiero ponowny zapis poprawnej zawartości EEPROM przywraca dane do porządku.

zapis eeprom. Zapis EEPROM i odczyt od razu z powrotem w celu sprawdzenia: EEPROM write verified — 1151 byte(s). Dotykane są tylko zmienione bajty, a odczyt kontrolny potwierdza każdy z nich.
Zapis flash — kalibracja L/R, z CRC liczonym na układzie
W 19EE kierownica po lewej kontra po prawej to nie kosmetyka: w przekładni z kierownicą po prawej silnik wspomagania obraca się w drugą stronę, więc flaga musi pasować do auta. Sprowadza się to do dwóch bajtów flag w block0, a block0 jest chroniony przez CRC-32, które sprawdza firmware. Przełącz flagi bez poprawienia CRC, a sterownik odrzuci kalibrację.
Receptę rozpracowałem na stanowisku, porównując autentyczny zrzut LEFT z autentycznym zrzutem RIGHT:
block0[0xE9]=0x00dla LEFT,0x01dla RIGHTblock0[0x4E9]=0xFFdla LEFT,0xFEdla RIGHT- standardowe CRC-32 zlib/PKZIP (odbity wielomian
0xEDB88320) poblock0[0 .. 0x3E44), zapisane w formacie big-endian pod adresem0x3FCC
Przełączenie jednej przekładni dotyka dokładnie tych sześciu bajtów — niczego więcej. Bajtów flag są dwa, nie jeden, bo tworzą parę nadmiarową: 0x4E9 zawiera dopełnienie do jedynki (one's-complement) 0x0E9 (0x00/0xFF, 0x01/0xFE), dokładnie o 0x400 dalej, a początek block0 jest odbity — jako bitowe NOT — pod tym samym przesunięciem +0x400. To ten schemat przechowywania „wartość plus jej odwrotność”, którego używają sterowniki bezpieczeństwa, a oba bajty mieszczą się w obszarze CRC-32 — więc poprawna edycja oznacza spójne ustawienie pary i ponowne policzenie CRC.
CRC zgodziło się dokładnie z obydwoma zrzutami odniesienia — i tak właśnie wiedziałem, że mam właściwy zakres i właściwy algorytm. Dlatego agent niesie własne, maleńkie, bitowe CRC-32 (bez tablicy — by agent pozostał mały) i przelicza sumę kontrolną na przekładni po edycji flag, tak że programowany obraz jest spójny.
crc32_zlib — na układzie, bez tablicy
/* CRC-32 (zlib/PKZIP: reflected poly 0xEDB88320, init/xorout 0xFFFFFFFF), bit-serial. */
static u32 crc32_zlib(const u8 *p, u32 n)
{
u32 c = 0xFFFFFFFFu, i; int k;
for (i = 0; i < n; i++) {
c ^= p[i];
for (k = 0; k < 8; k++) c = (c & 1u) ? ((c >> 1) ^ 0xEDB88320u) : (c >> 1);
}
return c ^ 0xFFFFFFFFu;
}
Samo programowanie to kontroler C90FL: odblokować blok, skasować go, a następnie przeprogramować podwójne słowo po podwójnym słowie, pomijając puste (0xFF) podwójne słowa. Nic egzotycznego, ale każdy krok to „uzbrój, wysokie napięcie, czekaj na DONE, sprawdź PEG (program/erase good)”, a pomyłka tutaj to martwa przekładnia, więc warto być ostrożnym i ponownie zablokować blok po wszystkim.
flash_write_block0 / flash_set_position — kasowanie, programowanie, CRC
#define CFLASH_BASE 0xC3F88000u
#define CF_MCR (*(volatile u32 *)(CFLASH_BASE + 0x00u))
#define CF_LML (*(volatile u32 *)(CFLASH_BASE + 0x04u))
#define CF_SLL (*(volatile u32 *)(CFLASH_BASE + 0x0Cu))
#define CF_LMS (*(volatile u32 *)(CFLASH_BASE + 0x10u))
#define MCR_PGM 0x10u
#define MCR_ERS 0x04u
#define MCR_EHV 0x01u
#define MCR_DONE 0x400u
#define MCR_PEG 0x200u
#define LML_PW 0xA1A11111u
#define SLL_PW 0xC3C33333u
static u8 blk[0x4000];
static int flash_write_block0(void)
{
int i; volatile u32 to;
if (!(CF_MCR & MCR_DONE)) return -1;
CF_LML = LML_PW; CF_LML = 0x001303FEu; /* unlock block0 */
CF_SLL = SLL_PW; CF_SLL = 0x001303FEu;
/* erase block0 */
CF_MCR = MCR_ERS;
CF_LMS = 0x00000001u; /* select block0 */
*(volatile u32 *)0x0 = 0xFFFFFFFFu; /* interlock write */
CF_MCR = MCR_ERS | MCR_EHV;
to = 0; while (!(CF_MCR & MCR_DONE)) if (++to > 40000000u) break;
if (!(CF_MCR & MCR_PEG)) { CF_MCR = MCR_ERS; CF_MCR = 0u; CF_LMS = 0u; return -2; }
CF_MCR = MCR_ERS; CF_MCR = 0u; CF_LMS = 0u;
/* reprogram non-blank doublewords from the SRAM snapshot */
for (i = 0; i < 0x4000; i += 8) {
u32 w0 = ((u32)blk[i] << 24) | ((u32)blk[i+1] << 16) | ((u32)blk[i+2] << 8) | blk[i+3];
u32 w1 = ((u32)blk[i+4] << 24) | ((u32)blk[i+5] << 16) | ((u32)blk[i+6] << 8) | blk[i+7];
if (w0 == 0xFFFFFFFFu && w1 == 0xFFFFFFFFu) continue;
CF_MCR = MCR_PGM;
*(volatile u32 *)(u32)i = w0;
*(volatile u32 *)(u32)(i + 4) = w1;
CF_MCR = MCR_PGM | MCR_EHV;
to = 0; while (!(CF_MCR & MCR_DONE)) if (++to > 4000000u) break;
if (!(CF_MCR & MCR_PEG)) { /* re-lock and bail -> restore from backup */ return -3; }
CF_MCR = MCR_PGM; CF_MCR = 0u;
}
CF_LML = LML_PW; CF_LML = 0x001303FFu; /* re-lock */
CF_SLL = SLL_PW; CF_SLL = 0x001303FFu;
return 0;
}
/* snapshot block0, flip L/R flags, recompute CRC, commit */
static int flash_set_position(int right)
{
int i; u32 crc;
if (!(CF_MCR & MCR_DONE)) return -1;
for (i = 0; i < 0x4000; i++) blk[i] = *(const volatile u8 *)(u32)i;
if (right) { blk[0x0E9] = 0x01u; blk[0x4E9] = 0xFEu; }
else { blk[0x0E9] = 0x00u; blk[0x4E9] = 0xFFu; }
crc = crc32_zlib(blk, 0x3E44u);
blk[0x3FCC] = (u8)(crc >> 24); blk[0x3FCD] = (u8)(crc >> 16); /* big-endian */
blk[0x3FCE] = (u8)(crc >> 8); blk[0x3FCF] = (u8)(crc);
return flash_write_block0();
}
Skasowanie flash to najgroźniejsza rzecz, jaką agent może zrobić, dlatego polecenie, które je wyzwala, jest ukryte za potrójnym zabezpieczeniem: dedykowany kod operacji, bajt kontrolny i magiczne słowo — wszystkie muszą się zgrać w jednej ramce, zanim zostanie wywołane flash_set_position. Jeden zły bit gdziekolwiek i żądanie jest ignorowane. Odpowiedź niesie odrębną wartość kontrolną echa (nie adres) oraz kod powrotu kontrolera w ogonie, więc host może rozpoznać, że ta ramka to potwierdzenie po zapisie, i zweryfikować wynik odczytem kontrolnym.
Przełączyłem testową przekładnię LEFT→RIGHT→LEFT, zweryfikowałem zrzuty w każdą stronę i zostawiłem ją w pierwotnej konfiguracji LEFT. ret=0, odczyt kontrolny się zgadza, odwracalnie.

zapis L/R. Przełączenie L/R we flashu, odczytane z powrotem w celu potwierdzenia: Write verified: steering = RIGHT. Safe to switch off the power. Zmieniony wiersz jest podświetlony w zrzucie, a CRC przeliczono na przekładni.
Pętla poleceń w całości
Cztery polecenia — odczyt flash, odczyt EEPROM, zapis EEPROM, zapis L/R — na każde odpowiedź idzie w slocie TX ze znacznikiem echa. Punkt wejścia zapisuje sygnaturę „żyję” (00 4C 8A A0 A1 A2 … BF) przed wejściem w pętlę, więc mogę potwierdzić, że mój kod jest rezydentny i działa wyłącznie z magistrali, bez podłączonego JTAG.
agent_main — dyspozytor
void agent_main(void)
{
static u16 frame[64];
static u8 buf[32];
u16 cmd[20]; int n, i, ret; u32 addr;
for (;;) {
n = rx_poll(cmd, 20);
if (n < 3) continue;
if (/* READ flash */ cmd[0] == 0x0017u && (cmd[1] & 0xFF00u) == 0x6300u) {
addr = ((u32)(cmd[1] & 0xFFu) << 16) | cmd[2];
for (i = 0; i < 32; i++) buf[i] = *(const volatile u8 *)(addr + i);
build_response(frame, buf);
frame[18] = (u16)(addr & 0xFFFFu); frame[19] = REPLY_MAGIC;
tx_send(frame, 62);
}
else if (/* READ eeprom */ cmd[0] == 0x0025u && (cmd[1] & 0xFF00u) == 0x6300u) {
addr = ((u32)(cmd[1] & 0xFFu) << 16) | cmd[2];
spi_ee_read((u16)addr, buf, 32);
build_response(frame, buf);
frame[18] = (u16)(addr & 0xFFFFu); frame[19] = REPLY_MAGIC;
tx_send(frame, 62);
}
else if (/* WRITE eeprom (guarded) */ cmd[0] == 0x00B7u /* + guard check */) {
/* ... write the diff bytes, read the window back, reply with read-back ... */
}
else if (/* WRITE L/R (triple-guarded, destructive) */ cmd[0] == 0x00B9u /* + guards */) {
ret = flash_set_position((cmd[1] >> 8) & 0x1);
for (i = 0; i < 30; i++) buf[i] = *(const volatile u8 *)(u32)i;
buf[30] = (u8)(ret & 0xFF); buf[31] = (u8)((ret >> 8) & 0xFF);
build_response(frame, buf);
frame[18] = 0x5A5Au; frame[19] = REPLY_MAGIC;
tx_send(frame, 62);
}
}
}
„Szyfrowanie”, pokrótce — i dlaczego sekret nigdy nie był mi potrzebny
Pamięć nie wraca w postaci jawnej; ścieżka odczytu w firmwarze XOR-uje ją ze strumieniem klucza (keystream). Często traktuje się to jak silny szyfr; nim nie jest. Po zebraniu znanego tekstu jawnego (skasowane obszary 0xFF plus obszar kodu, niemal identyczny między egzemplarzami) strumień klucza okazuje się funkcją liniową nad GF(2), zależną od adresu — LFSR/CRC adresu, a nie prawdziwym szyfrem blokowym. Jest też statystyczny trop: najstarszy bit (MSB) każdego 32-bitowego słowa strumienia jest obciążony — a to właśnie zdradza tani generator liniowy.
Ponieważ jest liniowy, strumień klucza daje się w pełni odtworzyć ze znanego tekstu jawnego: rozwiązujesz mały układ na znanym bloku, rozszerzasz na resztę po jednym znanym słowie na blok — i możesz odszyfrować/zaszyfrować dowolny egzemplarz, nie wydobywając nigdy ani seeda, ani tajnego klucza. Nie ma więc sekretu do ukrycia przed agentem — on żadnego nie niesie. Oddaje surowe bajty i pozwala liczyć stronie hosta. Sekwencję dostępu zabezpieczonego omówiłem wyżej; jedyne, co zatrzymuję, to sposób wytwarzania klucza, a to zostaje w moim oprogramowaniu MS561.
Opiszę kształt słabości — słabą obfuskację należy nazywać słabą — ale nie podam ani stałych per egzemplarz, ani handshake’u dostępu. Ta część nie jest moja, by ją rozdawać.
Słowo o oryginalności
Słyszałem twierdzenie, że moje narzędzia to „kopie 1:1” narzędzi pewnego dostawcy, więc jedno słowo w tej sprawie.
Wszystko na tej stronie wyszło z inżynierii wstecznej firmware’u w Ghidrze: mapy rejestrów, wyścig blokady, timing SPI, receptura CRC z moich własnych zrzutów left/right, źródło agenta powyżej. Metoda jest tu w całości, z kodem źródłowym.
Sam protokół jest mój: echo odpowiedzi 0x4321, nagłówek odpowiedzi 00 4C 8A, blokada „kręć się, aż CC odda”, potrójnie zabezpieczone kasowanie. Jeśli to pojawi się dosłownie w cudzym binarnym, sam wyciągnij wniosek, w którą stronę to szło.
Merhaba. Miłej lektury. 👋
Wyniki
| Operacja | Transport | Status |
|---|---|---|
| Odczyt flash (bloki 16 KB, pełny obraz) | FlexRay, bez JTAG | diff-0 względem odniesienia, wielokrotnie zweryfikowane (~27 s / 16 KB) |
| Odczyt EEPROM (cały M95640) | FlexRay, bez JTAG | co do bajtu, zweryfikowane (~14 s / 8 KB) |
| Zapis EEPROM (tylko zmienione bajty) | FlexRay, bez JTAG | odczyt kontrolny — ZGODNY, odwracalnie |
| Zapis flash (flagi L/R + CRC) | FlexRay, bez JTAG | ret=0, zweryfikowane odczytem kontrolnym, odwracalnie |
Ani sondy JTAG na egzemplarzu, ani sekretu wpieczonego w agenta — jedynie mały program w SRAM przekładni, odpowiadający na pytania przez FlexRay, i stanowisko, które potrafi teraz odczytać i zapisać przekładnię 19EE od początku do końca.
Reszta agenta — pełny sterownik po stronie hosta i zestaw poleceń — zostaje na moim stanowisku, ale to, co powyżej, było tą trudną częścią.
Źródła
- NXP, MPC5643L Microcontroller Reference Manual — Flash Memory Array and Control (C90FL): rejestry blokady LML/SLL (przesunięcia
0x04/0x0C) oraz ich hasła do edycji blokady,0xA1A11111i0xC3C33333. - Społeczność NXP, programowanie rejestrów blokady C90FL na pokrewnych układach MPC5xxx: przykład MPC5644A, blok FLASH MPC5744P.
Podziękowania
Szczególne podziękowania dla naszych kolegów z Brazylii i Meksyku za pomoc w opracowaniu tego rozwiązania. Bardzo cenimy Wasze wsparcie i współpracę!