
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
- Die ersten fünf Bytes
5A56001000
sind eine Art Header, der den Inhalt des Speicherauszugs nicht beeinflusst. - 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
- 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 ändern820EE13F→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
:x0xxx0xx
in dump ->x2xxx1xx
in filex0xxx4xx
in dump ->x2xxx0xx
in filexCxxx0xx
in dump ->x0xxx5xx
in file
- Für die Maske
00F000F0
:xx0xxx0x
in dump ->xx0xxx3x
in filexx0xxx5x
in dump ->xx9xxx8x
in filexx0xxx9x
in dump ->xxAxxxBx
in filexxFxxx0x
in dump ->xxExxxDx
in filexxFxxxFx
in dump ->xx8xxxDx
in file
- Für die Maske
000F000F
:xxx0xxx7
in dump ->xxxFxxxF
in filexxx7xxx0
in dump ->xxxExxxF
in filexxx7xxx1
in dump ->xxx9xxx8
in 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 ( 0x1000
Bytes).
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.
