Toyota distribue son firmware dans un format non documenté. Mon client, qui possède une voiture de cette marque, m'a montré le fichier du firmware, qui commence ainsi: Ensuite, il y a des lignes de 32 chiffres hexadécimaux. Le propriétaire et d'autres artisans aimeraient pouvoir vérifier ce qu'il y a à l'intérieur avant d'installer le firmware: mettez-le dans le désassembleur et voyez ce qu'il fait.
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
...
Spécifiquement pour ce firmware, il avait un vidage de contenu:
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
...
Comme vous pouvez le voir, il n'y a rien, même proche des lignes de chiffres hexadécimaux dans le fichier du firmware. La question se pose: dans quel format le firmware est-il distribué et comment le décrypter? Le propriétaire de la voiture m'a confié cette tâche.
Répéter des fragments
Examinons de plus près ces lignes hexadécimales: Nous voyons huit répétitions d'une séquence de trois , qui sont très similaires aux huit premières lignes d'un vidage, se terminant par 12 octets zéro. Trois conclusions peuvent être tirées immédiatement:
5A56001000820EE13FE2030133E20301
33E2030133C20EF13FE2030133E20301
33E2030133E2030133E2030133E20301
33E2030133C20EF13FE2030133E20301
33E2030133C20EF13FE2030133E20301
33E2030133C20EF13FE2030133E20301
33E2030133E2030133E2030133E20301
33E2030133C20EF13FE2030133E20301
33E2030133E20911381959FAB0EE9000
81C9E03ADE35CEEEEFC5CF8DE9AC0910
38C2E031DE35CEEEEFC8CF87E95C0920
...
E2030133
- Les cinq premiers octets
5A56001000
sont une sorte d'en-tête qui n'affecte pas le contenu du vidage; - Le contenu supplémentaire est chiffré par blocs de 4 octets, les mêmes octets de vidage correspondant aux mêmes octets dans le fichier:
E2030133 → 00000000
820EE13F → 80078000
C20EF13F → 80070000
E2091138 → E0076001
1959FAB0 → 2A0600FF
EE900081 → 00000A58
C9E03ADE → EAFF2000
- On peut voir que ce n'est pas un cryptage XOR, mais quelque chose de plus complexe; mais en mĂŞme temps, des blocs de vidage similaires correspondent Ă des blocs similaires dans le fichier - par exemple, changer un bit
80078000→80070000
correspond à changer un bit820EE13F→C20EF13F
.
Correspondances entre les blocs
Obtenons une liste de toutes les paires (bloc de fichiers, bloc de vidage) et cherchons les modèles à l'intérieur:
$ 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'), ...]
Voici à quoi ressemblent les premières paires dans la liste triée:
00beb5ff → 4c07a010 02057139 → 0000f00f 03ef5ed0 → 50ff710f \ changement du bit 24 dans le vidage change les bits 8, 10, 24-27 dans le fichier 04ef5bd0 → 51ff710f < 0408ed38 → 14002d06 \ 05f92ed7 → ffffd087 | 0a5d22bb → f602dffe> changer le bit 25 dans le vidage change les bits 11, 25-27 dans le fichier 0a62f9a9 → e10f5761 | 0acdc6e4 → a25d2c06 / 0aef53d0 → 53ff710f < 0aef5cd0 -> 52ff710f / changement du bit 24 dans le vidage change les bits 8-11 dans le fichier 0bdebd6f → 4c57a410 0d0c7fec → 0064ffff 0d0fe57f → 18402c57 0d8fa4d0 → bfff88ff 0ee882d7 → eafd7f00 1001c5c6 → 6c570042 \ 1008d238 -> 42003e06> changement du bit 1 dans le vidage change les bits 0, 3, 16-19 dans le fichier 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> passer au bit 3 dans le vidage change les bits 2 et 19 dans le fichier 12aed1bf -> 40073090 /> changement du bit 10 dans le vidage change le bit 8 dans le fichier 12c3f1ea → 20560001 \ 12c9f1ea -> 20560002 / les changements aux bits 0 et 1 dans le vidage changent les bits 17 et 19 dans le fichier ...
En effet, les motifs suivants sont visibles:
- Les changements aux bits 0-3 dans le vidage changent les bits 0-3 et 16-19 dans le fichier (masque
000F000F
) - Changements aux bits 24-25 dans le vidage changer les bits 8-11 et 24-27 dans le fichier (masque
0F000F00
)
L'hypothèse est que tous les 4 bits dans un vidage affectent les mêmes 4 bits dans chaque moitié de 16 bits d'un bloc de 32 bits.
Pour vérifier, "coupons" les 4 bits les plus significatifs de chaque demi-bloc, et voyons quelles paires nous obtenons:
>>> 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'), ...]
Après avoir réorganisé les sous-blocs de 4 bits dans la clé de tri, les correspondances entre paires de sous-blocs deviennent encore plus explicites:
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 /
...
Correspondances entre sous-blocs
La liste ci-dessus montre les correspondances suivantes:
- Pour le masque
0F000F00
:x0xxx0xx
dans le vidage ->x2xxx1xx
dans le fichierx0xxx4xx
dans le vidage ->x2xxx0xx
dans le fichierxCxxx0xx
dans le vidage ->x0xxx5xx
dans le fichier
- Pour le masque
00F000F0
:xx0xxx0x
dans le vidage ->xx0xxx3x
dans le fichierxx0xxx5x
dans le vidage ->xx9xxx8x
dans le fichierxx0xxx9x
dans le vidage ->xxAxxxBx
dans le fichierxxFxxx0x
dans le vidage ->xxExxxDx
dans le fichierxxFxxxFx
dans le vidage ->xx8xxxDx
dans le fichier
- Pour le masque
000F000F
:xxx0xxx7
dans le vidage ->xxxFxxxF
dans le fichierxxx7xxx0
dans le vidage ->xxxExxxF
dans le fichierxxx7xxx1
dans le vidage ->xxx9xxx8
dans le fichier
Nous pouvons conclure que chaque bloc de 32 bits dans le vidage est divisé en quatre valeurs de 8 bits, et ces valeurs sont remplacées à l'aide de certaines tables de recherche, pour chaque masque. Le contenu de ces quatre tableaux semble relativement aléatoire, mais essayons de les extraire tous de notre fichier:
>>> 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, ...}]
Lorsque les tables de recherche sont prêtes, le code de décryptage est assez 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'
En-tĂŞte du micrologiciel
Au tout début, il y avait un en-tête de cinq octets avant les données chiffrées
5A56001000
. Les deux premiers octets - la signature 'ZV'
- indiquent que le format LZF est utilisé ; indiqué en outre la méthode de compression ( 0x00
- pas de compression) et la longueur ( 0x1000
octets).
Le propriétaire de la voiture, qui m'a donné les fichiers pour analyse, a confirmé que les données compressées LZF se trouvaient également dans le firmware. Heureusement, l'implémentation de LZF est open source et assez simple, donc avec mon analyse, il a réussi à satisfaire sa curiosité pour le contenu du firmware. Maintenant, il peut apporter des modifications au code - par exemple, démarrer automatiquement le moteur lorsque la température descend en dessous d'un niveau prédéterminé, afin d'utiliser la voiture dans le rude hiver russe.