Cómo descifrar el firmware del coche en formato desconocido



Toyota distribuye su firmware en formato indocumentado. Mi cliente, que tiene un auto de esta marca, me mostró el archivo de firmware, que comienza así: Luego hay líneas de 32 dígitos hexadecimales. El propietario y otros artesanos quisieran poder verificar lo que hay adentro antes de instalar el firmware: colóquelo en el desensamblador y vea qué hace.



CALIBRATIONêXi º

attach.att

ÓÏ[Format]

Version=4



[Vehicle]

Number=0

DateOfIssue=2019-08-26

VehicleType=GUN1**

EngineType=1GD-FTV,2GD-FTV

VehicleName=IMV

ModelYear=15-

ContactType=CAN

KindOfECU=0

NumberOfCalibration=1



[CPU01]

CPUImageName=3F0S7300.xxz

FlashCodeName=

NewCID=3F0S7300

LocationID=0002000100070720

CPUType=87

NumberOfTargets=3

01_TargetCalibration=3F0S7200

01_TargetData=3531464734383B3A

02_TargetCalibration=3F0S7100

02_TargetData=3747354537494A39

03_TargetCalibration=3F0S7000

03_TargetData=3732463737463B4A



3F0S7300forIMV.txt ¸Ni¶m5A56001000820EE13FE2030133E20301

33E2030133C20EF13FE2030133E20301

33E2030133E2030133E2030133E20301

33E2030133C20EF13FE2030133E20301

33E2030133C20EF13FE2030133E20301

33E2030133C20EF13FE2030133E20301

33E2030133E2030133E2030133E20301

33E2030133C20EF13FE2030133E20301

33E2030133E20911381959FAB0EE9000

81C9E03ADE35CEEEEFC5CF8DE9AC0910

38C2E031DE35CEEEEFC8CF87E95C0920

...










Específicamente para este firmware, tuvo un volcado de contenido:



0000: 80 07 80 00 00 00 00 00 │ 00 00 00 00 00 00 00 00
0010: 80 07 00 00 00 00 00 00 │ 00 00 00 00 00 00 00 00
0020: 00 00 00 00 00 00 00 00 │ 00 00 00 00 00 00 00 00
0030: 80 07 00 00 00 00 00 00 │ 00 00 00 00 00 00 00 00
0040: 80 07 00 00 00 00 00 00 │ 00 00 00 00 00 00 00 00
0050: 80 07 00 00 00 00 00 00 │ 00 00 00 00 00 00 00 00
0060: 00 00 00 00 00 00 00 00 │ 00 00 00 00 00 00 00 00
0070: 80 07 00 00 00 00 00 00 │ 00 00 00 00 00 00 00 00
0080: E0 07 60 01 2A 06 00 FF │ 00 00 0A 58 EA FF 20 00
0090: FF 57 40 00 EB 51 B2 05 │ 80 07 48 01 E0 FF 20 00
...


Como puede ver, no hay nada parecido a las cadenas de dígitos hexadecimales en el archivo de firmware. Surge la pregunta: ¿en qué formato se distribuye el firmware y cómo descifrarlo? El dueño del coche me confió esta tarea.



Fragmentos repetidos



Echemos un vistazo más de cerca a esas líneas hexadecimales: vemos ocho repeticiones de una secuencia de tres , que son muy similares a las primeras ocho líneas de un volcado, que terminan en 12 bytes cero. Se pueden sacar tres conclusiones de inmediato:



5A56001000820EE13FE2030133E20301

33E2030133C20EF13FE2030133E20301

33E2030133E2030133E2030133E20301

33E2030133C20EF13FE2030133E20301

33E2030133C20EF13FE2030133E20301

33E2030133C20EF13FE2030133E20301

33E2030133E2030133E2030133E20301

33E2030133C20EF13FE2030133E20301

33E2030133E20911381959FAB0EE9000

81C9E03ADE35CEEEEFC5CF8DE9AC0910

38C2E031DE35CEEEEFC8CF87E95C0920

...





E2030133



  1. Los primeros cinco bytes 5A56001000son algún tipo de encabezado que no afecta el contenido del volcado;
  2. El contenido adicional se cifra en bloques de 4 bytes, con los mismos bytes de volcado correspondientes a los mismos bytes en el archivo:
    • E2030133 → 00000000
    • 820EE13F → 80078000
    • C20EF13F → 80070000
    • E2091138 → E0076001
    • 1959FAB0 → 2A0600FF
    • EE900081 → 00000A58
    • C9E03ADE → EAFF2000
  3. Se puede ver que esto no es un cifrado XOR, sino algo más complejo; pero, al mismo tiempo, bloques de volcado similares corresponden a bloques similares en el archivo; por ejemplo, cambiar un bit 80078000→80070000corresponde a cambiar un bit 820EE13F→C20EF13F.


Correspondencias entre bloques



Obtengamos una lista de todos los pares (bloque de archivo, bloque de volcado) y busquemos patrones en ella:



$ xxd -r -p firmware.txt decoded

$ python
>>> f = open('decoded','rb')
>>> data=f.read()
>>> words=[data[i:i+4] for i in range(0,4096,4)]
>>> f = open('dump','rb')
>>> data=f.read()[:4096]
>>> reference=[data[i:i+4] for i in range(0,4096,4)]
>>> list(zip(words,reference))[:3]
[(b'\x82\x0e\xe1?', b'\x80\x07\x80\x00'), (b'\xe2\x03\x013', b'\x00\x00\x00\x00'), (b'\xe2\x03\x013', b'\x00\x00\x00\x00')]
>>> dict(zip(words,reference))
{b'\x82\x0e\xe1?': b'\x80\x07\x80\x00', b'\xe2\x03\x013': b'\x00\x00\x00\x00', b'\xc2\x0e\xf1?': b'\x80\x07\x00\x00', ...}
>>> decode=dict(zip((w.hex() for w in words), (r.hex() for r in reference)))
>>> decode
{'820ee13f': '80078000', 'e2030133': '00000000', 'c20ef13f': '80070000', ...}
>>> sorted(decode.items())
[('00beb5ff', '4c07a010'), ('02057139', '0000f00f'), ('03ef5ed0', '50ff710f'), ...]


Así es como se ven los primeros pares en la lista ordenada:



00beb5ff → 4c07a010
02057139 → 0000f00f
03ef5ed0 → 50ff710f \ cambio en el bit 24 en el volcado cambia los bits 8, 10, 24-27 en el archivo
04ef5bd0 → 51ff710f < 
0408ed38 → 14002d06 \
05f92ed7 → ffffd087 |
0a5d22bb → f602dffe> cambiar el bit 25 en el volcado cambia los bits 11, 25-27 en el archivo
0a62f9a9 → e10f5761 |
0acdc6e4 → a25d2c06 /
0aef53d0 → 53ff710f <
0aef5cd0 -> 52ff710f / cambio en el bit 24 en el volcado cambia los bits 8-11 en el archivo
0bdebd6f → 4c57a410
0d0c7fec → 0064ffff
0d0fe57f → 18402c57
0d8fa4d0 → bfff88ff
0ee882d7 → eafd7f00
1001c5c6 → 6c570042 \
1008d238 -> 42003e06> cambio en el bit 1 en el volcado cambia los bits 0, 3, 16-19 en el archivo
100ec5cf → 6c570040 /
109ec58f → 6c070050
10e1ebdf → 62ff6008
10ec4cdd → dafd4c07
119f0f8f → 08006d57
11c0feee → 2c5f0500
120ff07e → 20420452
125ef13e → 20f600c8
125fc14e → 60420032
126f02af → 02006d67
1281d09f → 400f3488
1281d19f → 400f3088
12a6d0bb → 40073498
12a6d1bb → 40073098 \
12aed0bf -> 40073490> cambiar al bit 3 en el volcado cambia los bits 2 y 19 en el archivo
12aed1bf -> 40073090 /> cambio en el bit 10 en el volcado cambia el bit 8 en el archivo
12c3f1ea → 20560001 \
12c9f1ea -> 20560002 / cambia a los bits 0 y 1 en el volcado cambia los bits 17 y 19 en el archivo
...


De hecho, los siguientes patrones son visibles:



  • Los cambios en los bits 0-3 en el volcado cambian los bits 0-3 y 16-19 en el archivo (máscara 000F000F)
  • Los cambios en los bits 24-25 en el volcado cambian los bits 8-11 y 24-27 en el archivo (máscara 0F000F00)


La hipótesis es que cada 4 bits en un volcado afecta a los mismos 4 bits en cada mitad de 16 bits de un bloque de 32 bits.



Para comprobarlo, "cortemos" los 4 bits más significativos de cada medio bloque y veamos qué pares obtenemos:



>>> ints=[int.from_bytes(w, 'big') for w in words]
>>> [hex(i) for i in ints][:3]
['0x820ee13f', '0xe2030133', '0xe2030133']
>>> scrambled=[((i & 0xf000f000) >> 12, (i & 0x0f000f00) >> 8, (i & 0x00f000f0) >> 4, (i & 0x000f000f)) for i in ints]
>>> scrambled=[tuple(((i >> 16) << 4) | (i & 15) for i in q) for q in scrambled]
>>> scrambled[:3]
[(142, 33, 3, 239), (224, 33, 3, 51), (224, 33, 3, 51)]
>>> [tuple(hex(i) for i in q) for q in scrambled][:3]
[('0x8e', '0x21', '0x3', '0xef'), ('0xe0', '0x21', '0x3', '0x33'), ('0xe0', '0x21', '0x3', '0x33')]
>>> [b''.join(bytes([i]) for i in q) for q in scrambled][:3]
[b'\x8e!\x03\xef', b'\xe0!\x033', b'\xe0!\x033']
>>> decode=dict(zip((b''.join(bytes([i]) for i in q).hex() for q in scrambled), (r.hex() for r in reference)))
>>> sorted(decode.items())
[('025efd97', 'ffffd087'), ('02a25bdb', 'f602dffe'), ('053eedf0', '50ff710f'), ...]
>>> decode=dict(zip((b''.join(bytes([i]) for i in q[1:]).hex() for q in scrambled), (r.hex()[1:4]+r.hex()[5:8] for r in reference)))
>>> sorted(decode.items())
[('018d90', '0f63ff'), ('020388', '200e06'), ('050309', 'c03000'), ...]


Después de reorganizar los subbloques de 4 bits en la clave de clasificación, las correspondencias entre pares de subbloques se vuelven aún más explícitas:



018d90 → 0f63ff
020388 → 200e06    \
050309 → c03000 \   |  xx0xxx0x     xx0xxx3x  
05030e → c0f000  |  |
05036e → c06000  | /
050c16 → c57042  |
050cef → c57040  |
05971e → c88007   >  xCxxx0xx     x0xxx5xx  
0598ef → c07050  |
05bfef → c07010  |
05db59 → c9000f  |
05ed0e → cff000 <
060ecc → 264fff  |
065ba7 → 205fff  |
0bed1f → 2ff008 <|
0bfd15 → 2ff086  |
0cedcd → afdc07 <|
10f2e7 → e06a7e   >  xxFxxx0x     xxExxxDx  
118d5a → 9fdfff  | \
13032b → 40010a  |  >  xxFxxxFx     xx8xxxDx  
148d3d → fff6fc  | /
16b333 → f00e30  |
16ed15 → fffe06 /
1b63e6 → 52e883
1c98ff → 400b57 \
1d4d97 → aff1b7  |  xx00xx57     xx9Fxx8F  
1ece0e → c5f500  |
1f98ff → 800d57 /
20032f → 00e400 \
200398 → 007401  |
2007fe → 042452  |
2020ef → 057490  |
206284 → 067463   >  x0xxx4xx     x2xxx0xx  
20891f → 00f488  |
20ab6b → 007498  | \
20abef → 007490  | /  xx0xxx9x     xxAxxxBx  
20ed1d → 0ff404  |
20fb6e → 0064c0 /
21030e → 00f000 \
21032a → 00b008  |
210333 → 000000  |
210349 → 00c008  |
21034b → 003007  |
210359 → 00000f  |
210388 → 000006   >  x00xx00x     x20xx13x  
21038b → 00300b  |
210398 → 007001  |
2103c6 → 007004  |
2103d2 → 008000  |
2103e1 → 008009  |
2103ef → 007000 /
...


Correspondencias entre subbloques



La lista anterior muestra las siguientes coincidencias:



  • Para la mascarilla 0F000F00:
    • x0xxx0xxen volcado -> x2xxx1xxen archivo
    • x0xxx4xxen volcado -> x2xxx0xxen archivo
    • xCxxx0xxen volcado -> x0xxx5xxen archivo
  • Para la mascarilla 00F000F0:
    • xx0xxx0xen volcado -> xx0xxx3xen archivo
    • xx0xxx5xen volcado -> xx9xxx8xen archivo
    • xx0xxx9xen volcado -> xxAxxxBxen archivo
    • xxFxxx0xen volcado -> xxExxxDxen archivo
    • xxFxxxFxen volcado -> xx8xxxDxen archivo
  • Para la mascarilla 000F000F:
    • xxx0xxx7en volcado -> xxxFxxxFen archivo
    • xxx7xxx0en volcado -> xxxExxxFen archivo
    • xxx7xxx1en volcado -> xxx9xxx8en archivo


Podemos concluir que cada bloque de 32 bits en el volcado se divide en cuatro valores de 8 bits, y estos valores se reemplazan usando algunas tablas de búsqueda, para cada máscara. El contenido de estas cuatro tablas parece ser relativamente aleatorio, pero intentemos extraerlos todos de nuestro archivo:



>>> ref_ints=[int.from_bytes(w, 'big') for w in reference]
>>> ref_scrambled=[((i & 0xf000f000) >> 12, (i & 0x0f000f00) >> 8, (i & 0x00f000f0) >> 4, (i & 0x000f000f)) for i in ref_ints]
>>> ref_scrambled=[tuple(((i >> 16) << 4) | (i & 15) for i in q) for q in ref_scrambled]
>>> decode=dict(zip((b''.join(bytes([i]) for i in q).hex() for q in scrambled), (b''.join(bytes([i]) for i in q).hex() for q in ref_scrambled)))
>>> sorted(decode.items())
[('025efd97', 'fdf0f8f7'), ('02a25bdb', 'fd6f0f2e'), ('053eedf0', '5701f0ff'), ...]
>>> decode=[dict(zip((bytes([q[byte]]).hex() for q in scrambled), (bytes([q[byte]]).hex() for q in ref_scrambled))) for byte in range(4)]
>>> decode
[{'8e': '88', 'e0': '00', 'cf': '80', 'e1': 'e6', '1f': '20', 'c3': 'e2', ...}, {'03': '00', '5b': '0f', '98': '05', 'ed': 'f0', 'ce': '50', 'd6': '51', ...}, {'21': '00', '9a': 'a0', 'e0': '0a', '5e': 'f0', '5d': 'b2', 'c0': '08', ...}, {'ef': '70', '33': '00', '98': '71', '90': '6f', '01': '08', '0e': 'f0', ...}]
>>> decode=[dict(zip((q[byte] for q in scrambled), (q[byte] for q in ref_scrambled))) for byte in range(4)]
>>> decode
[{142: 136, 224: 0, 207: 128, 225: 230, 31: 32, 195: 226, 62: 244, 200: 235, ...}, {3: 0, 91: 15, 152: 5, 237: 240, 206: 80, 214: 81, 113: 16, 185: 2, 179: 3, ...}, {33: 0, 154: 160, 224: 10, 94: 240, 93: 178, 192: 8, 135: 2, 62: 1, 120: 26, ...}, {239: 112, 51: 0, 152: 113, 144: 111, 1: 8, 14: 240, 249: 21, 110: 96, 241: 47, ...}]


Cuando las tablas de búsqueda están listas, el código de descifrado es bastante simple:



>>> def _decode(x):
...   scrambled = ((x & 0xf000f000) >> 12, (x & 0x0f000f00) >> 8, (x & 0x00f000f0) >> 4, (x & 0x000f000f))
...   decoded = tuple(decode[i][((v >> 16) << 4) | (v & 15)] for i, v in enumerate(scrambled))
...   unscrambled = tuple(((i >> 4) << 16) | (i & 15) for i in decoded)
...   return (unscrambled[0] << 12) | (unscrambled[1] << 8) | (unscrambled[2] << 4) | (unscrambled[3])
...
>>> hex(_decode(0x00beb5ff))
'0x4c07a010'
>>> hex(_decode(0x12aed1bf))
'0x40073090'


Encabezado de firmware



Al principio, había un encabezado de cinco bytes antes de los datos cifrados 5A56001000. Los primeros dos bytes, la firma 'ZV', indican que se está utilizando el formato LZF ; indicó además el método de compresión ( 0x00- sin compresión) y la longitud ( 0x1000bytes).



El propietario del automóvil, que me entregó los archivos para el análisis, confirmó que los datos comprimidos de LZF también se encuentran en el firmware. Afortunadamente, la implementación de LZF es de código abierto y bastante simple, por lo que junto con mi análisis, logró satisfacer su curiosidad sobre el contenido del firmware. Ahora puede realizar cambios en el código, por ejemplo, arrancar automáticamente el motor cuando la temperatura desciende por debajo de un nivel predeterminado, para poder usar el automóvil en el duro invierno ruso.






All Articles