Comment décrypter le firmware de la voiture dans un format inconnu



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



  1. Les cinq premiers octets 5A56001000sont une sorte d'en-tĂŞte qui n'affecte pas le contenu du vidage;
  2. 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
  3. 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→80070000correspond à changer un bit 820EE13F→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:
    • x0xxx0xxdans le vidage -> x2xxx1xxdans le fichier
    • x0xxx4xxdans le vidage -> x2xxx0xxdans le fichier
    • xCxxx0xxdans le vidage -> x0xxx5xxdans le fichier
  • Pour le masque 00F000F0:
    • xx0xxx0xdans le vidage -> xx0xxx3xdans le fichier
    • xx0xxx5xdans le vidage -> xx9xxx8xdans le fichier
    • xx0xxx9xdans le vidage -> xxAxxxBxdans le fichier
    • xxFxxx0xdans le vidage -> xxExxxDxdans le fichier
    • xxFxxxFxdans le vidage -> xx8xxxDxdans le fichier
  • Pour le masque 000F000F:
    • xxx0xxx7dans le vidage -> xxxFxxxFdans le fichier
    • xxx7xxx0dans le vidage -> xxxExxxFdans le fichier
    • xxx7xxx1dans le vidage -> xxx9xxx8dans 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 ( 0x1000octets).



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.






All Articles