0
Виберіть свою мову
0
Виберіть свою мову

Читання та запис рульової рейки BMW з електропідсилювачем (EPS) по FlexRay

Читання та запис рульової рейки BMW з електропідсилювачем (EPS) по FlexRay
06.10.2026
Поділитися

Невеликий агент під 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 і флеш, а також один протокольний прийом, що вилікував нестабільне читання.

Стенд 19EE: блок MS561, кабель FlexRay, рейка та осцилограф

стенд. Блок 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) та EEPROM M95640

чіп. Основний кристал рейки — SPC5643LFMLQ1, той самий MPC5643L — а ліворуч від нього маленька 8-вивідна SPI EEPROM M95640.

Чому агент, а не «просто JTAG по всьому»

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

JTAG-програматор P&E на налагоджувальному роз’ємі рейки на етапі реверс-інжинірингу

етап RE. Як я читав її під час реверс-інжинірингу: корпус розкрито, JTAG-програматор P&E на налагоджувальному роз’ємі рейки. Для лабораторії — нормально, але повторювати з кожним вузлом не хочеться.

Агент позбавляє всього цього. Він заходить усередину по FlexRay — по шині, що й так виведена на роз’єм, — тож вузол лишається закритим: не потрібно розкривати корпус, випаювати мікросхему й відривати площадки.

Копирсаючись у прошивці в Ghidra, я виявив, що механізм уже вбудовано: у завантажувачі є шлях, який затягує невелику програму у вільну SRAM рейки по FlexRay і запускає її. Тож я написав власний агент під цей шлях, завдання якого просте:

опитувати слот FlexRay щодо команд, виконувати операцію з пам’яттю й відповідати в іншому слоті.

Ні libc, ні RTOS, ні залежностей. Вказівник стека й точку входу він отримує від завантажувача, видає сигнатуру «я живий», щоб я міг переконатися, що працює саме мій код, і потім крутиться в циклі обробки команд.

Крок перший: розблокування інженерного режиму

Перш ніж усе це запрацює, рейку треба перевести в її інженерний режим, а він закритий захищеним доступом UDS (security access). Послідовність — звичайний UDS:

  1. DiagnosticSessionControl у розширену сесію (10 03), потім в інженерну сесію (10 42).
  2. SecurityAccess — рівень «13/14»: запит seed (27 13), рейка повертає 8-байтовий seed; обчислюємо відповідь; надсилаємо ключ (27 14). Ключ — це 4-байтове поле довжини, за яким іде 128-байтовий підпис.
  3. 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, RoutineControl 030C відпрацьовує свій обробник, а процедура входу за адресою 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 рейки

на лінії. Кабель знімає лінію 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, ви розумієте, яку плавучу помилку це вбиває.

MS561 читає flash block0 — читання OK, цілісність OK

читання. Повне читання 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 у MS561 підтверджено зворотним зчитуванням

запис eeprom. Запис EEPROM і читання одразу назад для перевірки: EEPROM write verified — 1151 byte(s). Зачіпаються лише змінені байти, і зворотне зчитування підтверджує кожен із них.

Запис флеш — калібрування L/R, з розрахунком CRC просто в пристрої

У 19EE лівий і правий руль — не косметика: на праворульній рейці мотор підсилювача крутиться в інший бік, тому прапорець має відповідати машині. Усе зводиться до двох байтів-прапорців у block0, а block0 захищено CRC-32, яку перевіряє прошивка. Поміняєш прапорці, не виправивши CRC, — і блок керування відкине калібрування.

Рецепт я розкрив на стенді, порівнявши справжній дамп LEFT зі справжнім дампом RIGHT:

  • block0[0xE9] = 0x00 для LEFT, 0x01 для RIGHT
  • block0[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, зворотне зчитування збігається, оборотно.

Запис флеш у MS561 підтверджено — рульове переведено в RIGHT

запис 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.

Подяки

Особлива подяка нашим колегам із Бразилії та Мексики за допомогу в розробці цього рішення. Ми щиро цінуємо вашу підтримку та співпрацю!