Lectura y escritura de una cremallera EPS de BMW por FlexRay
Un pequeño agente para el MPC5643L que corre dentro de la propia cremallera: lee y escribe la flash y la EEPROM de la cremallera por FlexRay, sin JTAG y sin abrir la carcasa.
Notas de banco, desde el lado de la reparación. Todo lo descrito se hizo sobre unidades desmontadas, en mi propio banco de pruebas, como trabajo de reconstrucción/diagnóstico, y todo salió del firmware: la imagen abierta en Ghidra y desensamblada, cotejada contra mis propios volcados y mucho texto plano conocido. Dejo fuera el acceso de seguridad (security access) del fabricante: no es mío para publicarlo y, de todos modos, no está en el agente.
En breve
La centralita de dirección en buena parte de la gama BMW Serie F (etiqueta interna del loader 000019EE, «19EE» de aquí en adelante) se construye en torno a un NXP MPC5643L — un PowerPC de doble núcleo en lockstep con el conjunto de instrucciones VLE — con la calibración repartida entre la flash C90FL en el chip y una SPI EEPROM M95640 externa. La herramienta de fábrica le habla por FlexRay.
Yo quería leer y escribir ambas memorias en el banco, por FlexRay, sin colgar una sonda JTAG en cada unidad. El camino fue un diminuto agente autónomo, cargado en la SRAM de la cremallera por FlexRay mediante una ruta de carga que extraje del firmware en Ghidra, que atiende las operaciones de memoria de vuelta hacia mi cable. Este artículo recorre el agente: la fontanería de FlexRay, los controladores de EEPROM y flash, y un truco de protocolo que arregló una lectura inestable.

el banco. La unidad MS561 para EPS (alimentación y diagnóstico), el cable FlexRay, una cremallera 19EE, la ThinkPad corriendo MS561 y el osciloscopio arriba.
El objetivo
El MPC5643L es un MCU orientado a la seguridad: dos núcleos e200z4 en lockstep, ECC por todas partes y un controlador FlexRay E-Ray de Freescale/NXP integrado en el die. Las cifras que importan para el resto del artículo:
| Qué | Dónde |
|---|---|
| Núcleo | e200z4, PowerPC VLE (longitud variable), big-endian |
| Flash de código | controlador C90FL en 0xC3F88000, flash mapeada en la base 0x0 |
| «block0» | 0x0000–0x3FFF (16 KB) — calibración + un CRC-32 de configuración |
| EEPROM | M95640 externa (64 Kbit SPI) en DSPI_B, en 0xFFF94000 |
| FlexRay | CC E-Ray de NXP en 0xFFFE0000, message RAM en la SRAM del sistema |
Una trampa de VLE de entrada: la mayoría de las compilaciones estándar de objdump no decodifican VLE — un binutils de 2005 le da basura. El trabajo estático lo hice en Ghidra con el lenguaje PowerPC:BE:64:VLE-32addr. Equivoque el modo y toda la imagen se decodifica en disparates.

el chip. El die principal de la cremallera — SPC5643LFMLQ1, el MPC5643L — con la pequeña SPI EEPROM M95640 de 8 patas a su izquierda.
Por qué un agente y no «JTAG a todo y ya»
Durante mucho tiempo la única vía a estos datos fue la cirugía a fuerza bruta: abrir la carcasa de la centralita, desoldar la EEPROM, leerla y reprogramarla en un programador externo barato y volver a soldarla. Funciona — muchos talleres aún lo hacen — pero es lento y delicado, y cada ida y vuelta es otra ocasión para levantar una pista o cocinar el chip. El JTAG es más limpio y es lo que usé en la fase de ingeniería inversa, pero sigue implicando abrir esa carcasa sellada para llegar a las almohadillas de depuración. Ninguna de las dos es algo que uno quiera repetir unidad tras unidad en el banco.

fase RE. Cómo la leía durante la ingeniería inversa: carcasa abierta, un programador JTAG P&E en el conector de depuración de la cremallera. Está bien para el laboratorio, pero no es algo que uno quiera hacer a cada unidad.
El agente evita todo eso. Entra por FlexRay — el bus que ya está en el conector — así que la unidad permanece cerrada: nada de abrir carcasa, nada de desoldar el chip, nada de levantar pistas.
Hurgando en el firmware en Ghidra encontré el mecanismo ya presente: una ruta en el loader que jala un pequeño programa a la SRAM libre de la cremallera por FlexRay y lo ejecuta. Así que escribí mi propio agente para esa ruta, cuya tarea es simplemente:
sondear un slot FlexRay en busca de comandos, hacer la operación de memoria y responder en otro slot.
Sin libc, sin RTOS, sin dependencias. Obtiene su puntero de pila y su punto de entrada del loader, emite una firma de «vida» para que yo confirme que es mi código el que corre, y luego se queda en un bucle de comandos.
Paso uno: desbloquear el modo ingeniería
Antes de que nada de esto funcione, hay que poner la cremallera en su modo ingeniería, que está protegido tras el acceso de seguridad UDS (security access). La secuencia es UDS corriente:
- DiagnosticSessionControl a la sesión extendida (
10 03), luego a la sesión de ingeniería (10 42). - SecurityAccess — el nivel «13/14»: solicitar el seed (
27 13) y la cremallera devuelve un seed de 8 bytes; calcular la respuesta; enviar la key (27 14). La key es un campo de longitud de 4 bytes seguido de una firma de 128 bytes. - RoutineControl Start (
31 01 03 0C) — esto es lo que arma el modo ingeniería. Ubiqué la rutina 030C en Ghidra; su dispatcher de RoutineControl está en0x635a0.
La única parte que me reservo es el paso del medio: cómo se produce esa firma y la key que hay detrás. Eso se queda en mi banco. ¿Cómo la cocina BMW? Vayan a preguntarles. 😉
Paso dos: meter el agente en el chip
En el firmware hay un loader que toma un trozo de código a la SRAM libre por FlexRay y lo ejecuta. Lo desarmé en Ghidra:
- Un comando SETUP (
00 03) pone la tarea de recepción del loader (una tarea RTOS; su dispatcher en0x80000, tabla de manejadores en0x40009098) en modo recepción; la cremallera responde con un ack (00 08 51). - El receptor de tramas en
0x8c664copia los datos entrantes a la SRAM solo si un gate de estado en0x7e40está armado a0xFE/0xFF. De lo contrario, cada trama se descarta en silencio. - Lo que arma ese gate es la propia entrada a ingeniería: la sesión UDS pone un byte de estado (
0x7944) en1, RoutineControl030Cejecuta su manejador, y la rutina de entrada en0x81010escribe0x7e40 = 0xFEy arranca la tarea.
La trampa que me costó tiempo: el SETUP se acepta, el ack vuelve, pero la ráfaga aun así no «aterriza». Un volcado de la RAM de la cremallera (ICDPPCNEXUS Hotsync, sin reset) mostró 0x7e40 = 0 — la tarea de recepción no estaba armada, así que cada trama se abortaba. Enviar más rápido no ayuda: el receptor debe estar armado y seguir armado cuando llega el flujo, así que el orden es: armar 030C, esperar a que el clúster FlexRay suba, y recién entonces enviar la ráfaga. Con el gate sostenido, el código aterriza en la SRAM, corre, y el agente se anuncia con su trama de vida (00 4C 8A A0 A1 … BF). Sin JTAG, carcasa cerrada.
FlexRay, tal como lo ve el E-Ray
Todo fluye por dos búferes de mensajes FlexRay (message buffers, MB): el agente escucha en uno y responde en otro. La message RAM del E-Ray es un arreglo de cabeceras más una región de datos, y la configuración de un búfer vive en registros en ERAY_BASE + 0x100 + idx*8.

en el cable. El cable husmeando la línea FlexRay RX de la cremallera mientras yo resolvía el entramado — aquí grabando en vivo a ~580,000 flancos/s con un reloj de muestreo de 275 MHz.
eng_agent.c — mapa de búferes de mensajes del 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 */
Para tocar un búfer se lo bloquea (conmutar LCKT, confirmar que LCKS volvió en alto). Lo que me mordió aquí:
El MB de transmisión retransmite por hardware su contenido en cada ciclo FlexRay. Mientras el controlador copia el búfer al bus posee el bloqueo, y un único intento de «conmuta y reza» pierde la carrera más seguido de lo que uno quisiera.
El bloqueo ingenuo de un solo disparo devuelve «no lo obtuve» en silencio, la respuesta nunca sale, el extremo lejano ve que el bloque se calla, y la lectura se trunca o se arrastra mientras la otra parte sigue repreguntando. El arreglo es girar en bucle hasta que el controlador ceda el búfer: hay una ventana libre en cada ciclo. Reintentar es seguro: una escritura LCKT rechazada simplemente se ignora (estado sin cambios), así que no puede corromper nada, y uno se detiene en el instante en que LCKS se relee en alto, de modo que nunca quita un bloqueo que ya tiene.
mb_lock — gira hasta que el CC ceda el búfer
/* 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); }
Sondear un comando es entonces: bloquear, revisar DVAL (¿trama nueva?), leer la cabecera para hallar dónde está la carga útil y de qué largo es, copiarla afuera, desbloquear. Enviar una respuesta es la imagen en espejo.
rx_poll / tx_send — un comando que entra, una respuesta que sale
/* 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);
}
El truco que hizo confiables las lecturas: eco en la respuesta
Ese comportamiento de «retransmite cada ciclo» tiene una segunda consecuencia. Como el búfer TX sigue poniendo su último contenido en el bus, el receptor puede enganchar una copia vieja de la respuesta anterior — sobre todo en la primera lectura tras un cambio de estado: uno queda una solicitud atrás, pide la dirección N y obtiene los bytes de N-1, y se ve casi bien hasta que claramente no lo está.
Retardos de asentamiento y reintentos lo disimulan, pero son una carrera y son lentos. El arreglo determinista es hacer que cada respuesta sea autoidentificable. El agente estampa dos palabras extra en la trama: un token de eco (la dirección solicitada, o un centinela fijo para una escritura) y un magic de respuesta. El cable rechaza entonces cualquier respuesta cuyo eco no coincida con la solicitud que acaba de enviar. Sin temporización, sin adivinanzas — la trama equivocada se rechaza de forma estructural.
build_response — trama de respuesta + sello de eco
#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 */
Con el eco en su lugar, las lecturas completas de 16 KB vuelven enteras cada vez, en vez de truncarse a mitad de camino. Si alguna vez peleó con los slots estáticos de FlexRay, sabe el tipo de falla intermitente que esto elimina.

lectura. Una lectura completa de flash block0 en MS561: 16 KB de vuelta, hex en pantalla, y la consola cerrando con flash block0 read OK (16384 bytes) e Flash integrity OK. El agente está activo (Agent connected) y no hay JTAG en la unidad.
Lectura de la EEPROM — revivir las patas SPI
La M95640 cuelga de DSPI_B. La trampa: el firmware la lee una vez al arranque y luego quita el multiplexado de los pads, así que para cuando el agente corre, esas patas ya no son SPI. El primer paso es devolverlas — multiplexar CS/SCK/SOUT como salidas y SIN como entrada por los registros de control de pad del SIUL, y luego levantar DSPI_B como un maestro de 8 bits lento y seguro. (Los números de pad y el CTAR salieron directo de la propia rutina de EEPROM de arranque de la cremallera.)
Una lectura clásica de SPI-EEPROM es el opcode 0x03, la dirección de 16 bits, y luego bytes de relleno (dummy) reloteados para leer los datos de vuelta. La única sutileza en este DSPI: el chip-select debe permanecer activo durante toda la transacción, así que uno empuja las tramas de comando/dirección/relleno una tras otra con el bit de continuación puesto, bajando CS solo en la última. Una espera por byte vacía la FIFO y baja CS a mitad de transferencia. Por eso cada byte de datos es su propia prolija ráfaga de 4 tramas.
spi_ee_read — lectura por ráfaga del M95640 en 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 */
}
}
Escritura de la EEPROM — solo los bytes que realmente cambiaron
Escribir la M95640 es el baile de manual: WREN (habilitación de escritura, 0x06), luego WRITE (0x02) + dirección + datos, y después sondear el bit WIP del registro de estado hasta que termine el ciclo de escritura de ~5 ms. De nuevo, todo el bloque cmd+addr+data viaja en una sola ráfaga con CS en bajo.
spi_ee_write_byte — WREN / WRITE / sondeo de 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)) { }
}
Dos decisiones de seguridad deliberadas en cómo se maneja esto:
- El comando de escritura lleva un byte de guarda. Una escritura se atiende solo si en la palabra de comando está presente un valor de guarda fijo. Una trama espuria o corrupta no puede programar la EEPROM por accidente — simplemente se descarta.
- El host escribe una diferencia, no un bloque entero. Del lado del MS561 guardo la última lectura completa de la EEPROM como instantánea; una escritura toca solo los bytes que difieren de ella (y nunca los campos volátiles de tiempo de ejecución), luego relee la ventana y verifica que quedó. Si alguna vez dejó inservible un módulo por reescribir un byte que no quería tocar, entenderá por qué.
En el taller esto suele ser el lado de datos de una reparación por salto de correa. BMW lo registra como DTC 0x482452 — «EPS steering angle sensor: belt jump detected» en nuestro decodificador — con 0x4822D9 («…steering angle invalid») como gemelo del lado de hardware, y un contador de saltos de correa en el DID 0xE33C. Pese al nombre, la correa suele estar bien; simplemente los datos guardados en el módulo ya no coinciden con la cremallera, y un flasheo o una codificación no lo borran. Es reescribir la EEPROM correcta lo que deja los datos en orden.

escritura eeprom. Escribir la EEPROM y leerla de inmediato para comprobar: EEPROM write verified — 1151 byte(s). Solo se tocan los bytes cambiados, y la relectura confirma cada uno.
Escritura de flash — calibración L/R, con el CRC hecho en el propio chip
En el 19EE, volante a la izquierda frente a la derecha no es cosmético: en una cremallera con volante a la derecha el motor de asistencia gira al revés, así que el flag debe coincidir con el auto. Se reduce a dos bytes de flag en block0, y block0 está protegido por un CRC-32 que el firmware verifica. Cambie los flags sin arreglar el CRC y la centralita rechaza la calibración.
La receta la descifré en el banco comparando un volcado LEFT auténtico contra un volcado RIGHT auténtico:
block0[0xE9]=0x00para LEFT,0x01para RIGHTblock0[0x4E9]=0xFFpara LEFT,0xFEpara RIGHT- un CRC-32 zlib/PKZIP estándar (polinomio reflejado
0xEDB88320) sobreblock0[0 .. 0x3E44), almacenado en big-endian en0x3FCC
Convertir una cremallera toca exactamente esos seis bytes — nada más. Y los bytes de flag son dos, no uno, porque forman un par redundante: 0x4E9 contiene el complemento a uno (one's-complement) de 0x0E9 (0x00/0xFF, 0x01/0xFE), exactamente 0x400 más allá, y la parte alta de block0 está reflejada — como NOT bit a bit — en ese mismo desplazamiento +0x400. Es el almacenamiento «valor más su inverso» que usan las centralitas de seguridad, y ambos bytes caen dentro de la región del CRC-32 — así que una edición válida significa fijar el par de forma coherente y recalcular el CRC.
El CRC coincidió exactamente con ambos volcados de referencia — así supe que tenía el tramo correcto y el algoritmo correcto. Por eso el agente lleva su propio CRC-32 bit a bit diminuto (sin tabla, para que el agente siga siendo chico) y recalcula la suma de verificación en la cremallera tras editar los flags, de modo que la imagen que programa queda consistente consigo misma.
crc32_zlib — en el chip, sin tabla
/* 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;
}
La programación en sí es el controlador C90FL: desbloquear el bloque, borrarlo, y luego reprogramar doubleword por doubleword, saltando las doublewords en blanco (0xFF). Nada exótico, pero cada paso es «arma, alto voltaje, espera DONE, revisa PEG (program/erase good)», y un error aquí es una cremallera muerta, así que conviene ser cuidadoso y rebloquear al final.
flash_write_block0 / flash_set_position — borrar, programar, 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();
}
Un borrado de flash es lo más peligroso que el agente puede hacer, así que el comando que lo dispara está tras una triple guarda: un opcode dedicado, un byte centinela y una magic word deben alinearse todos en la misma trama antes de que se llame a flash_set_position. Un solo bit malo en cualquier lado y la solicitud se ignora. La respuesta lleva un centinela de eco distinto (no una dirección) y el código de retorno del controlador en la cola, así el host sabe que esa trama es la confirmación posterior a la escritura y verifica el resultado por relectura.
Convertí una cremallera de prueba LEFT→RIGHT→LEFT, verifiqué los volcados en cada sentido y la dejé en su configuración LEFT original. ret=0, la relectura coincide, reversible.

escritura L/R. El cambio L/R en flash, releído para confirmar: Write verified: steering = RIGHT. Safe to switch off the power. La fila cambiada aparece resaltada en el volcado, y el CRC se recalculó en la cremallera.
El bucle de comandos, todo junto
Cuatro comandos — leer flash, leer EEPROM, escribir EEPROM, escribir L/R — cada uno respondido en el slot TX con el sello de eco. El punto de entrada escribe una firma de vida (00 4C 8A A0 A1 A2 … BF) antes de entrar al bucle, así puedo confirmar que mi código está residente y corriendo puramente desde el bus, sin ningún JTAG conectado.
agent_main — el dispatcher
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);
}
}
}
El «cifrado», en breve — y por qué nunca necesité el secreto
La memoria no vuelve en claro; la ruta de lectura en el firmware la pone en XOR con un keystream. A menudo se lo trata como un cifrado fuerte; no lo es. Tras reunir texto plano conocido (regiones borradas a 0xFF, más la región de código casi idéntica entre unidades) el keystream resulta ser una función por dirección, lineal sobre GF(2) — un LFSR/CRC de la dirección, no un cifrado de bloque real. Hay además un indicio estadístico: el MSB de cada palabra de keystream de 32 bits está sesgado, que es lo que filtra un generador lineal barato.
Al ser lineal, el keystream se recupera por completo a partir de texto plano conocido: se resuelve un pequeño sistema sobre un bloque conocido, se extiende al resto con una palabra conocida por bloque, y se puede descifrar/cifrar cualquier unidad sin extraer jamás un seed ni una key secreta. De modo que no hay secreto que ocultarle al agente — no lleva ninguno. Entrega los bytes crudos y deja las cuentas al lado del host. La secuencia de security access la recorrí antes; lo único que me guardo es cómo se produce la key, y eso se queda en mi software MS561.
Describiré la forma de la debilidad — a una ofuscación débil hay que llamarla débil — pero no las constantes por unidad ni el handshake de acceso. Esa parte no es mía para repartirla.
Una nota sobre originalidad
He oído la afirmación de que mis herramientas son «copias 1:1» de las de cierto proveedor, así que una nota al respecto.
Todo en esta página salió de la ingeniería inversa del firmware en Ghidra: los mapas de registros, la carrera por el bloqueo, la temporización SPI, la receta del CRC a partir de mis propios volcados left/right, el fuente del agente de arriba. El método está aquí completo, con fuente incluido.
El protocolo en sí es mío: el eco de respuesta 0x4321, la cabecera de respuesta 00 4C 8A, el bloqueo «gira hasta que el CC ceda», el borrado de triple guarda. Si eso aparece textual en otro binario, saque usted mismo la conclusión de hacia qué lado fue.
Merhaba. Que disfrute la lectura. 👋
Resultados
| Operación | Transporte | Estado |
|---|---|---|
| Lectura de flash (bloques de 16 KB, imagen completa) | FlexRay, sin JTAG | diff-0 frente a la referencia, validada repetidamente (~27 s / 16 KB) |
| Lectura de EEPROM (todo el M95640) | FlexRay, sin JTAG | byte a byte exacta, validada (~14 s / 8 KB) |
| Escritura de EEPROM (solo bytes cambiados) | FlexRay, sin JTAG | relectura COINCIDE, reversible |
| Escritura de flash (flags L/R + CRC) | FlexRay, sin JTAG | ret=0, verificada por relectura, reversible |
Ni sonda JTAG en la unidad, ni secreto horneado en el agente — solo un pequeño programa en la SRAM de la cremallera respondiendo preguntas por FlexRay, y un banco que ahora puede leer y escribir una cremallera 19EE de principio a fin.
El resto del agente — el controlador completo del lado del host y el juego de comandos — se queda en mi banco, pero las partes de arriba eran las difíciles.
Referencias
- NXP, MPC5643L Microcontroller Reference Manual — Flash Memory Array and Control (C90FL): los registros de bloqueo LML/SLL (offsets
0x04/0x0C) y sus contraseñas de edición de bloqueo,0xA1A11111y0xC3C33333. - Comunidad NXP, programación de registros de bloqueo C90FL en piezas MPC5xxx afines: ejemplo MPC5644A, bloque FLASH MPC5744P.
Agradecimientos
Un agradecimiento especial a nuestros colegas de Brasil y México por su ayuda en el desarrollo de esta solución. ¡Valoramos mucho su apoyo y colaboración!