Читання та запис рульової рейки BMW з електропідсилювачем (EPS) по FlexRay
Невеликий агент під MPC5643L, що працює просто в рейці: читає й пише флеш та EEPROM рейки по FlexRay — без JTAG і без розкриття корпусу.
Нотатки зі стенда, з боку ремонту. Усе описане виконувалося на знятих з машини вузлах на моєму власному стенді — як робота з відновлення та діагностики, і все це отримано з прошивки: образ відкрито в Ghidra та дизасембльовано, звірено за моїми власними дампами й за великим обсягом відомого відкритого тексту. Заводський захищений доступ (security access) OEM я не публікую — це не моє, і в агенті його все одно немає.
Коротко
Блок керування рульовим керуванням у помітної частини модельного ряду BMW F-серії (внутрішній тег завантажувача 000019EE, далі «19EE») побудовано на NXP MPC5643L — двоядерному PowerPC з режимом lockstep і системою команд VLE, а його калібрувальні дані розділено між вбудованою флеш C90FL та зовнішньою SPI EEPROM M95640. Заводське ПЗ спілкується з ним по FlexRay.
Я хотів читати й писати обидві пам’яті на стенді, по FlexRay, не під’єднуючи JTAG-пробник до кожного окремого вузла. Шлях до цього — крихітний автономний агент, що завантажується в SRAM рейки по FlexRay через механізм завантаження, який я відкопав у прошивці за допомогою Ghidra; агент відповідає на операції з пам’яттю, надсилаючи дані назад на мій кабель. Цей матеріал розбирає агент: обв’язку FlexRay, драйвери EEPROM і флеш, а також один протокольний прийом, що вилікував нестабільне читання.

стенд. Блок MS561 для ЕПК (живлення та діагностика), кабель FlexRay, рейка 19EE, ThinkPad із запущеним MS561 та осцилограф згори.
Об’єкт
MPC5643L — це MCU з наголосом на функціональну безпеку: два ядра e200z4 в режимі lockstep, ECC повсюди та вбудований у кристал FlexRay-контролер E-Ray від Freescale/NXP. Цифри, важливі для всього подальшого:
| Що | Де |
|---|---|
| Ядро | e200z4, PowerPC VLE (змінна довжина команд), big-endian |
| Флеш коду | контролер C90FL за адресою 0xC3F88000, флеш відображено на базову адресу 0x0 |
| «block0» | 0x0000–0x3FFF (16 КБ) — калібрування + конфігураційна CRC-32 |
| EEPROM | зовнішня M95640 (64 Кбіт, SPI) на DSPI_B за адресою 0xFFF94000 |
| FlexRay | контролер зв’язку (CC) E-Ray від NXP за адресою 0xFFFE0000, message RAM у системній SRAM |
Один підступ із VLE одразу: більшість штатних збірок objdump не декодують VLE — binutils зразка 2005 року видає сміття. Статичний аналіз я робив у Ghidra з мовою PowerPC:BE:64:VLE-32addr. Помилитеся з режимом — і весь образ декодується в беззмістовність.

чіп. Основний кристал рейки — SPC5643LFMLQ1, той самий MPC5643L — а ліворуч від нього маленька 8-вивідна SPI EEPROM M95640.
Чому агент, а не «просто JTAG по всьому»
Довгий час єдиним способом дістатися цих даних була груба «хірургія»: розкрити корпус блоку керування, випаяти EEPROM, прочитати й перепрограмувати її на дешевому зовнішньому програматорі, а потім впаяти назад. Спосіб працює — чимало майстерень досі так і роблять, — але він повільний і вередливий, і кожен такий цикл — черговий шанс відірвати контактну площадку або перегріти мікросхему. JTAG акуратніший, і саме його я використовував на етапі реверс-інжинірингу, але він усе одно означає розкриття герметичного корпусу заради доступу до налагоджувальних площадок. Ані те, ані інше не хочеться повторювати з вузлом за вузлом на стенді.

етап RE. Як я читав її під час реверс-інжинірингу: корпус розкрито, JTAG-програматор P&E на налагоджувальному роз’ємі рейки. Для лабораторії — нормально, але повторювати з кожним вузлом не хочеться.
Агент позбавляє всього цього. Він заходить усередину по FlexRay — по шині, що й так виведена на роз’єм, — тож вузол лишається закритим: не потрібно розкривати корпус, випаювати мікросхему й відривати площадки.
Копирсаючись у прошивці в Ghidra, я виявив, що механізм уже вбудовано: у завантажувачі є шлях, який затягує невелику програму у вільну SRAM рейки по FlexRay і запускає її. Тож я написав власний агент під цей шлях, завдання якого просте:
опитувати слот FlexRay щодо команд, виконувати операцію з пам’яттю й відповідати в іншому слоті.
Ні libc, ні RTOS, ні залежностей. Вказівник стека й точку входу він отримує від завантажувача, видає сигнатуру «я живий», щоб я міг переконатися, що працює саме мій код, і потім крутиться в циклі обробки команд.
Крок перший: розблокування інженерного режиму
Перш ніж усе це запрацює, рейку треба перевести в її інженерний режим, а він закритий захищеним доступом UDS (security access). Послідовність — звичайний UDS:
- DiagnosticSessionControl у розширену сесію (
10 03), потім в інженерну сесію (10 42). - SecurityAccess — рівень «13/14»: запит seed (
27 13), рейка повертає 8-байтовий seed; обчислюємо відповідь; надсилаємо ключ (27 14). Ключ — це 4-байтове поле довжини, за яким іде 128-байтовий підпис. - RoutineControl Start (
31 01 03 0C) — саме це й зводить інженерний режим. Процедуру 030C я зафіксував у Ghidra; її диспетчер RoutineControl розташований за адресою0x635a0.
Єдине, що я лишаю при собі, — середній крок: як формується цей підпис і який за ним стоїть ключ. Це лишається на моєму стенді. Як її «варить» BMW? Ідіть питайте в них. 😉
Крок другий: як доставити агент у чіп
У прошивці є завантажувач, що приймає шматок коду у вільну SRAM по FlexRay і запускає його. Я розібрав його в Ghidra:
- Команда SETUP (
00 03) переводить приймальну задачу завантажувача (задача RTOS; її диспетчер за адресою0x80000, таблиця обробників за0x40009098) у режим прийому; рейка відповідає підтвердженням (00 08 51). - Приймач кадрів за адресою
0x8c664копіює вхідні дані в SRAM лише якщо ознака стану за адресою0x7e40зведена в0xFE/0xFF. Інакше кожен кадр мовчки відкидається. - Зводить цю ознаку сам вхід в інженерний режим: сесія UDS встановлює байт стану (
0x7944) у1, RoutineControl030Cвідпрацьовує свій обробник, а процедура входу за адресою0x81010записує0x7e40 = 0xFEі запускає задачу.
Підступ, що коштував мені часу: SETUP приймається, підтвердження повертається, але пакет усе одно не «долітає». Дамп RAM рейки (ICDPPCNEXUS Hotsync, без скидання) показав 0x7e40 = 0 — приймальну задачу не було зведено, тому кожен кадр відвалювався. Слати швидше не допомагає: приймач має бути зведений і лишатися зведеним до моменту приходу потоку, тож порядок такий: звести 030C, дочекатися підняття кластера FlexRay, потім видати пакет. Поки ознака утримується, код потрапляє в SRAM, запускається, і агент заявляє про себе своїм кадром «я живий» (00 4C 8A A0 A1 … BF). Без JTAG, корпус закритий.
FlexRay очима E-Ray
Усе проходить через два буфери повідомлень FlexRay (message buffers, MB): один агент слухає, в іншому відповідає. Message RAM контролера E-Ray — це масив заголовків плюс область даних, а конфігурація буфера лежить у регістрах за адресою ERAY_BASE + 0x100 + idx*8.

на лінії. Кабель знімає лінію FlexRay RX рейки, поки я розбирався з форматом кадрів — тут запис у реальному часі на ~580 тис. фронтів/с за частоти дискретизації 275 МГц.
eng_agent.c — карта буферів повідомлень 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 */
Щоб звернутися до буфера, його треба заблокувати (перемкнути LCKT і переконатися, що LCKS повернувся встановленим). Ось що мене тут вкусило:
Передавальний MB апаратно повторно передає свій вміст у кожному циклі FlexRay. Поки контролер копіює буфер на шину, блокування належить йому, і одна спроба «перемкнув і сподіваюся» програє гонку частіше, ніж хотілося б.
Наївне одноразове блокування мовчки повертає «не отримав», відповідь так і не йде, дальній кінець бачить, що блок замовк, і читання обривається або повзе, поки інша сторона знову й знову перепитує. Рішення — крутитися в циклі, доки контролер не віддасть буфер: вільне вікно є в кожному циклі. Повторювати безпечно: відхилений запис LCKT просто ігнорується (статус не змінюється), тож стан не зіпсуєш, і ти зупиняєшся тієї ж миті, коли LCKS зчитується встановленим, — тому ніколи не знімеш блокування, яке вже тримаєш.
mb_lock — крутимося, доки CC не віддасть буфер
/* 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); }
Тоді опитування команди має такий вигляд: заблокувати, перевірити DVAL (новий кадр?), прочитати заголовок, щоб дізнатися, де лежить корисне навантаження і якої воно довжини, скопіювати його, розблокувати. Надсилання відповіді — дзеркальне відображення.
rx_poll / tx_send — одна команда на вхід, одна відповідь на вихід
/* 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);
}
Прийом, що зробив читання надійним: ехо у відповіді
У поведінки «повторює кожен цикл» є й другий наслідок. Оскільки TX-буфер продовжує викладати свій останній вміст на шину, приймач може засклити застарілу копію попередньої відповіді — особливо при першому читанні після зміни стану: ти опиняєшся на один запит позаду, питаєш адресу N, а отримуєш байти для N-1, і виглядає це майже правильно — рівно до тієї миті, коли стає зовсім неправильно.
Затримки на встановлення та повтори це маскують, але це гонка, і це повільно. Детерміноване рішення — зробити кожну відповідь самоідентифікованою. Агент вставляє в кадр два додаткові слова: ехо-маркер (запитана адреса або фіксоване контрольне значення для запису) і магічну позначку відповіді. Після цього кабель відкидає будь-яку відповідь, ехо якої не збігається з щойно надісланим запитом. Жодного таймінгу, жодних здогадок — неправильний кадр відкидається структурно.
build_response — кадр відповіді + ехо-позначка
#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 */
З ехом на місці повні читання по 16 КБ щоразу повертаються цілком, а не обриваються на півдорозі. Якщо ви колись воювали зі статичними слотами FlexRay, ви розумієте, яку плавучу помилку це вбиває.

читання. Повне читання flash block0 у MS561: 16 КБ отримано, hex на екрані, і консоль завершується рядками flash block0 read OK (16384 bytes) та Flash integrity OK. Агент піднятий (Agent connected), і на вузлі немає JTAG.
Читання EEPROM — повертаємо до життя виводи SPI
M95640 висить на DSPI_B. Підступ: прошивка читає її один раз під час завантаження, а потім знімає мультиплексування з виводів, тож на момент запуску агента ці виводи — вже не SPI. Передусім їх треба повернути назад: призначити CS/SCK/SOUT як виходи, а SIN як вхід через регістри керування виводами SIUL, потім підняти DSPI_B як повільний, безпечний 8-бітний майстер. (Номери виводів і CTAR взято просто із власної завантажувальної процедури роботи з EEPROM у рейці.)
Класичне читання SPI-EEPROM — це код операції 0x03, 16-бітна адреса, потім холості (dummy) байти, що видаються тактами, аби зчитати дані назад. Єдина тонкість на цьому DSPI: сигнал вибору кристала (chip-select) має лишатися активним протягом усієї транзакції, тож кадри команди/адреси/холостий передаєш встик зі встановленим бітом продовження, знімаючи CS лише на останньому. Одне очікування на кожен байт призводить до спорожнення FIFO та зняття CS посеред передачі. Тому кожен байт даних — це свій акуратний пакет із 4 кадрів.
spi_ee_read — пакетне читання M95640 по 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 */
}
}
Запис EEPROM — лише ті байти, що реально змінилися
Запис M95640 — хрестоматійний танець: WREN (дозвіл запису, 0x06), потім WRITE (0x02) + адреса + дані, після чого опитуємо біт WIP регістра стану, доки не завершиться цикл запису (~5 мс). Знову весь блок cmd+addr+data іде одним пакетом із CS у низькому рівні.
spi_ee_write_byte — WREN / WRITE / опитування 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)) { }
}
Два свідомі рішення на користь безпеки з боку керування цим:
- Команда запису несе захисний байт. Запис виконується лише якщо у слові команди присутнє фіксоване захисне значення. Випадковий або спотворений кадр не може ненавмисно записати EEPROM — він просто відкидається.
- Хост пише різницю, а не цілий блок. На боці MS561 я тримаю останнє повне читання EEPROM як знімок; запис зачіпає лише ті байти, що відрізняються від нього (і ніколи — мінливі робочі поля), після чого зчитує вікно назад і перевіряє, що все лягло як слід. Якщо ви колись «цеглили» модуль, переписавши байт, який не збиралися чіпати, ви зрозумієте, навіщо це.
У майстерні це зазвичай та сама сторона даних під час ремонту з перескоком ременя. BMW реєструє це як код несправності (DTC) 0x482452 — «EPS steering angle sensor: belt jump detected» у нашому декодері — з апаратним парним кодом 0x4822D9 («…steering angle invalid») і лічильником перескоків ременя в DID 0xE33C. Попри назву, сам ремінь зазвичай цілий; просто збережені в модулі дані більше не відповідають рейці, і перепрошивкою чи кодуванням це не скинути. Привести дані до ладу дозволяє саме зворотний запис правильного вмісту EEPROM.

запис eeprom. Запис EEPROM і читання одразу назад для перевірки: EEPROM write verified — 1151 byte(s). Зачіпаються лише змінені байти, і зворотне зчитування підтверджує кожен із них.
Запис флеш — калібрування L/R, з розрахунком CRC просто в пристрої
У 19EE лівий і правий руль — не косметика: на праворульній рейці мотор підсилювача крутиться в інший бік, тому прапорець має відповідати машині. Усе зводиться до двох байтів-прапорців у block0, а block0 захищено CRC-32, яку перевіряє прошивка. Поміняєш прапорці, не виправивши CRC, — і блок керування відкине калібрування.
Рецепт я розкрив на стенді, порівнявши справжній дамп LEFT зі справжнім дампом RIGHT:
block0[0xE9]=0x00для LEFT,0x01для RIGHTblock0[0x4E9]=0xFFдля LEFT,0xFEдля RIGHT- стандартна CRC-32 zlib/PKZIP (відображений поліном
0xEDB88320) поblock0[0 .. 0x3E44), збережена у форматі big-endian за адресою0x3FCC
Перемикання однієї рейки зачіпає рівно ці шість байтів — і більше нічого. Байтів-прапорців два, а не один, бо це надлишкова пара: 0x4E9 містить доповнення до одиниці (one's-complement) від 0x0E9 (0x00/0xFF, 0x01/0xFE), рівно на 0x400 далі, і початок block0 дзеркально продубльовано — як побітове НЕ — за тим самим зміщенням +0x400. Це та сама захисна схема зберігання «значення плюс його інверсія», яку використовують блоки керування, і обидва байти потрапляють в область CRC-32 — тож коректна зміна означає узгоджене встановлення пари та перерахунок CRC.
CRC точно збіглася з обома еталонними дампами — саме так я й зрозумів, що взяв правильний діапазон і правильний алгоритм. Тому агент несе власну крихітну побітову CRC-32 (без таблиці — щоб агент лишався компактним) і перераховує контрольну суму просто в рейці після правки прапорців, тож прошиваний образ виходить самоузгодженим.
crc32_zlib — просто в пристрої, без таблиці
/* 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;
}
Саме програмування — це контролер C90FL: розблокувати блок, стерти його, потім перепрограмувати подвійне слово за подвійним словом, пропускаючи чисті (0xFF) подвійні слова. Нічого екзотичного, але кожен крок — це «звести, подати високу напругу, дочекатися DONE, перевірити PEG (program/erase good)», а помилка тут — це мертва рейка, тож варто бути акуратним і повторно заблокувати блок після.
flash_write_block0 / flash_set_position — стирання, програмування, 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();
}
Стирання флеш — найнебезпечніше, що може зробити агент, тому команда, що його запускає, схована за потрійним захистом: виділений код операції, контрольний байт і магічне слово — усі мають збігтися в одному кадрі, перш ніж буде викликано flash_set_position. Один хибний біт будь-де — і запит ігнорується. Відповідь несе окреме ехо-контрольне значення (не адресу) і код повернення контролера в хвості, тож хост може зрозуміти, що цей кадр — підтвердження після запису, і перевірити результат зворотним зчитуванням.
Я перемкнув тестову рейку LEFT→RIGHT→LEFT, звірив дампи в кожен бік і лишив її у вихідній конфігурації LEFT. ret=0, зворотне зчитування збігається, оборотно.

запис L/R. Перемикання L/R у флеш, зчитане назад для підтвердження: Write verified: steering = RIGHT. Safe to switch off the power. Змінений рядок підсвічено в дампі, а CRC перераховано просто в рейці.
Цикл обробки команд цілком
Чотири команди — читання флеш, читання EEPROM, запис EEPROM, запис L/R — на кожну відповідь іде в TX-слоті з ехо-позначкою. Точка входу записує сигнатуру «я живий» (00 4C 8A A0 A1 A2 … BF) перед входом у цикл, тож я можу підтвердити, що мій код завантажено й він працює виключно з шини, без під’єднаного JTAG.
agent_main — диспетчер
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);
}
}
}
«Шифрування», стисло — і чому секрет мені так і не знадобився
Пам’ять повертається не у відкритому вигляді; шлях читання в прошивці XOR-ить її з гамою (keystream). Її часто сприймають за стійкий шифр — це не так. Після збору відомого відкритого тексту (стерті області 0xFF плюс область коду, майже однакова в різних вузлів) гама виявляється лінійною над GF(2) функцією від адреси — це LFSR/CRC за адресою, а не справжній блоковий шифр. Є й статистична ознака: старший біт (MSB) кожного 32-бітного слова гами зміщений — саме це й видає дешевий лінійний генератор.
Оскільки вона лінійна, гама повністю відновлюється з відомого відкритого тексту: розв’язуєш невелику систему на відомому блоці, поширюєш на решту по одному відомому слову на блок — і можеш розшифрувати/зашифрувати будь-який вузол, так і не видобуваючи ні seed, ні секретний ключ. Тож жодного секрету, який треба ховати від агента, немає — він його й не несе. Він віддає сирі байти, а обчислення виконує хост. Послідовність захищеного доступу я розібрав вище; єдине, що я притримую, — як формується ключ, і це лишається в моєму ПЗ MS561.
Форму вразливості я опишу — слабку обфускацію треба називати слабкою, — але не наведу ні повузлові константи, ні рукостискання доступу. Ця частина — не моя, щоб її роздавати.
Зауваження щодо оригінальності
Я чув твердження, що мої інструменти — «точні копії 1:1» інструментів одного вендора, тож одне зауваження щодо цього.
Усе на цій сторінці отримано з реверс-інжинірингу прошивки в Ghidra: карти регістрів, гонка з блокуванням, таймінги SPI, рецепт CRC з моїх власних дампів left/right, вихідник агента вище. Метод тут наведено повністю, з вихідним кодом.
Сам протокол — мій власний: ехо-відповідь 0x4321, заголовок відповіді 00 4C 8A, блокування «крутися, доки CC не віддасть», потрійний захист стирання. Якщо це дослівно спливе в чужому бінарнику — робіть висновок самі, у який бік усе йшло.
Merhaba. Приємного читання. 👋
Результати
| Операція | Транспорт | Статус |
|---|---|---|
| Читання флеш (блоки по 16 КБ, повний образ) | FlexRay, без JTAG | diff-0 відносно еталона, багаторазово перевірено (~27 с / 16 КБ) |
| Читання EEPROM (весь M95640) | FlexRay, без JTAG | побайтово точно, перевірено (~14 с / 8 КБ) |
| Запис EEPROM (лише змінені байти) | FlexRay, без JTAG | зворотне зчитування — ЗБІГ, оборотно |
| Запис флеш (прапорці L/R + CRC) | FlexRay, без JTAG | ret=0, перевірено зворотним зчитуванням, оборотно |
Ні JTAG-пробника на вузлі, ні секрету, зашитого в агент, — лише невелика програма в SRAM рейки, що відповідає на запити по FlexRay, і стенд, який тепер уміє читати й писати рейку 19EE від початку до кінця.
Решта агента — повний хостовий драйвер і набір команд — лишається на моєму стенді, але описане вище було найскладнішим.
Джерела
- NXP, MPC5643L Microcontroller Reference Manual — Flash Memory Array and Control (C90FL): регістри блокування LML/SLL (зміщення
0x04/0x0C) та їхні паролі для зміни блокування,0xA1A11111і0xC3C33333. - Спільнота NXP, програмування регістрів блокування C90FL на споріднених кристалах MPC5xxx: приклад MPC5644A, FLASH-блок MPC5744P.
Подяки
Особлива подяка нашим колегам із Бразилії та Мексики за допомогу в розробці цього рішення. Ми щиро цінуємо вашу підтримку та співпрацю!