So entschlüsseln Sie Auto-Firmware in unbekanntem Format



Toyota vertreibt seine Firmware in einem undokumentierten Format. Mein Kunde, der ein Auto dieser Marke hat, hat mir die Firmware-Datei gezeigt, die so beginnt: Dann gibt es Zeilen mit 32 hexadezimalen Ziffern. Der Besitzer und andere Handwerker möchten vor der Installation der Firmware überprüfen können, was sich darin befindet: Legen Sie sie in den Disassembler und sehen Sie, was sie bewirkt.



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

...










Speziell für diese Firmware hatte er einen Content Dump:



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
...


Wie Sie sehen können, befindet sich in der Firmware-Datei nichts in der Nähe der hexadezimalen Ziffernfolgen. Es stellt sich die Frage: In welchem ​​Format wird die Firmware verteilt und wie wird sie entschlüsselt? Der Besitzer des Autos hat mich mit dieser Aufgabe betraut.



Fragmente wiederholen



Schauen wir uns diese hexadezimalen Zeilen genauer an: Wir sehen acht Wiederholungen einer Folge von drei Zeilen, die den ersten acht Zeilen eines Speicherauszugs sehr ähnlich sind und mit 12 Null-Bytes enden. Drei Schlussfolgerungen können sofort gezogen werden:



5A56001000820EE13FE2030133E20301

33E2030133C20EF13FE2030133E20301

33E2030133E2030133E2030133E20301

33E2030133C20EF13FE2030133E20301

33E2030133C20EF13FE2030133E20301

33E2030133C20EF13FE2030133E20301

33E2030133E2030133E2030133E20301

33E2030133C20EF13FE2030133E20301

33E2030133E20911381959FAB0EE9000

81C9E03ADE35CEEEEFC5CF8DE9AC0910

38C2E031DE35CEEEEFC8CF87E95C0920

...





E2030133



  1. Die ersten fünf Bytes 5A56001000sind eine Art Header, der den Inhalt des Speicherauszugs nicht beeinflusst.
  2. Weiterer Inhalt wird in Blöcken von 4 Bytes verschlüsselt, und dieselben Bytes im Speicherauszug entsprechen denselben Bytes in der Datei:
    • E2030133 → 00000000
    • 820EE13F → 80078000
    • C20EF13F → 80070000
    • E2091138 → E0076001
    • 1959FAB0 → 2A0600FF
    • EE900081 → 00000A58
    • C9E03ADE → EAFF2000
  3. Es ist ersichtlich, dass dies keine XOR-Verschlüsselung ist, sondern etwas Komplexeres. Ähnliche Dump-Blöcke entsprechen jedoch ähnlichen Blöcken in der Datei. Wenn Sie beispielsweise ein Bit 80078000→80070000ändern, müssen Sie ein Bit ändern 820EE13F→C20EF13F.


Korrespondenz zwischen Blöcken



Lassen Sie uns eine Liste aller Paare (Dateiblock, Dump-Block) abrufen und nach Mustern darin suchen:



$ 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'), ...]


So sehen die ersten Paare in der sortierten Liste aus:



00beb5ff → 4c07a010
02057139 → 0000f00f
03ef5ed0 → 50ff710f \ Änderung in Bit 24 im Speicherauszug ändert die Bits 8, 10, 24-27 in der Datei
04ef5bd0 → 51ff710f < 
0408ed38 → 14002d06 \
05f92ed7 → ffffd087 |
0a5d22bb → f602dffe> Durch Ändern von Bit 25 im Speicherauszug werden die Bits 11, 25-27 in der Datei geändert
0a62f9a9 → e10f5761 |
0acdc6e4 → a25d2c06 /
0aef53d0 → 53ff710f <
0aef5cd0 -> 52ff710f / Änderung in Bit 24 im Speicherauszug ändert die Bits 8-11 in der Datei
0bdebd6f → 4c57a410
0d0c7fec → 0064ffff
0d0fe57f → 18402c57
0d8fa4d0 → bfff88ff
0ee882d7 → eafd7f00
1001c5c6 → 6c570042 \
1008d238 -> 42003e06> Änderung von Bit 1 im Speicherauszug ändert die Bits 0, 3, 16-19 in der Datei
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> Änderung auf Bit 3 im Speicherauszug ändert die Bits 2 und 19 in der Datei
12aed1bf -> 40073090 /> Änderung in Bit 10 im Speicherauszug ändert Bit 8 in der Datei
12c3f1ea → 20560001 \
12c9f1ea -> 20560002 / Änderungen an den Bits 0 und 1 im Speicherauszug ändern die Bits 17 und 19 in der Datei
...


In der Tat sind die Muster sichtbar:



  • Änderungen an den Bits 0-3 im Speicherauszug ändern die Bits 0-3 und 16-19 in der Datei (Maske 000F000F)
  • Änderungen an den Bits 24-25 im Speicherauszug ändern die Bits 8-11 und 24-27 in der Datei (Maske 0F000F00)


Die Hypothese ist, dass alle 4 Bits in einem Dump die gleichen 4 Bits in jeder 16-Bit-Hälfte eines 32-Bit-Blocks beeinflussen.



Um dies zu überprüfen, "schneiden" wir die höchstwertigen 4 Bits in jedem Halbblock ab und sehen, welche Paare wir erhalten:



>>> 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'), ...]


Nach dem Vertauschen von 4-Bit-Unterblöcken im Sortierschlüssel werden die Entsprechungen zwischen Paaren von Unterblöcken noch deutlicher:



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 /
...


Entsprechungen zwischen Unterblöcken



Die obige Liste zeigt die folgenden Übereinstimmungen:



  • Für die Maske 0F000F00:
    • x0xxx0xxin dump -> x2xxx1xxin file
    • x0xxx4xxin dump -> x2xxx0xxin file
    • xCxxx0xxin dump -> x0xxx5xxin file
  • Für die Maske 00F000F0:
    • xx0xxx0xin dump -> xx0xxx3xin file
    • xx0xxx5xin dump -> xx9xxx8xin file
    • xx0xxx9xin dump -> xxAxxxBxin file
    • xxFxxx0xin dump -> xxExxxDxin file
    • xxFxxxFxin dump -> xx8xxxDxin file
  • Für die Maske 000F000F:
    • xxx0xxx7in dump -> xxxFxxxFin file
    • xxx7xxx0in dump -> xxxExxxFin file
    • xxx7xxx1in dump -> xxx9xxx8in file


Wir können daraus schließen, dass jeder 32-Bit-Block im Speicherauszug in vier 8-Bit-Werte aufgeteilt ist und diese Werte mithilfe einiger Nachschlagetabellen für jede Maske ersetzt werden. Der Inhalt dieser vier Tabellen scheint relativ zufällig zu sein, aber versuchen wir, alle aus unserer Datei zu extrahieren:



>>> 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, ...}]


Wenn die Nachschlagetabellen fertig sind, ist der Entschlüsselungscode ganz einfach:



>>> 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'


Firmware-Header



Ganz am Anfang stand vor den verschlüsselten Daten ein Fünf-Byte-Header 5A56001000. Die ersten beiden Bytes - die Signatur 'ZV'- zeigen an, dass das LZF-Format verwendet wird . weiter angegeben die Komprimierungsmethode ( 0x00- keine Komprimierung) und Länge ( 0x1000Bytes).



Der Besitzer des Autos, der mir die Dateien zur Analyse gegeben hat, bestätigte, dass LZF-komprimierte Daten auch in der Firmware enthalten sind. Glücklicherweise ist die Implementierung von LZF Open Source und ziemlich einfach. Zusammen mit meiner Analyse gelang es ihm, seine Neugier auf den Inhalt der Firmware zu befriedigen. Jetzt kann er Änderungen am Code vornehmen - zum Beispiel den Motor automatisch starten, wenn die Temperatur unter ein vorbestimmtes Niveau fällt, um das Auto im harten russischen Winter zu benutzen.






All Articles