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
- Los primeros cinco bytes
5A56001000
son algún tipo de encabezado que no afecta el contenido del volcado; - 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
- 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→80070000
corresponde a cambiar un bit820EE13F→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
:x0xxx0xx
en volcado ->x2xxx1xx
en archivox0xxx4xx
en volcado ->x2xxx0xx
en archivoxCxxx0xx
en volcado ->x0xxx5xx
en archivo
- Para la mascarilla
00F000F0
:xx0xxx0x
en volcado ->xx0xxx3x
en archivoxx0xxx5x
en volcado ->xx9xxx8x
en archivoxx0xxx9x
en volcado ->xxAxxxBx
en archivoxxFxxx0x
en volcado ->xxExxxDx
en archivoxxFxxxFx
en volcado ->xx8xxxDx
en archivo
- Para la mascarilla
000F000F
:xxx0xxx7
en volcado ->xxxFxxxF
en archivoxxx7xxx0
en volcado ->xxxExxxF
en archivoxxx7xxx1
en volcado ->xxx9xxx8
en 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 ( 0x1000
bytes).
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.