Как расшифровать прошивку автомобиля в неизвестном формате

0nofy-hmj4td1hcqegkohx34a6a.jpeg


Toyota распространяет свои прошивки в недокументированном формате. Мой заказчик, у которого автомобиль этой марки, показал мне файл прошивки, который начинается так:

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

Дальше идут строки по 32 шестнадцатеричные цифры.

Хозяину и прочим умельцам хотелось бы перед установкой прошивки иметь возможность проверить, что там внутри: засунуть ее в дизассемблер и посмотреть, что она делает.

Конкретно для этой прошивки у него имелся дамп содержимого:

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

Как видно, нет ничего даже близко похожего на строчки шестнадцатеричных цифр в файле прошивки. Встает вопрос: в каком формате распространяется прошивка, и как ее расшифровать? Эту задачу хозяин автомобиля поручил мне.

Повторяющиеся фрагменты


Посмотрим внимательно на те шестнадцатеричные строчки:

5A56001000820EE13FE2030133E20301
33E2030133C20EF13FE2030133E20301
33E2030133E2030133E2030133E20301
33E2030133C20EF13FE2030133E20301
33E2030133C20EF13FE2030133E20301
33E2030133C20EF13FE2030133E20301
33E2030133E2030133E2030133E20301
33E2030133C20EF13FE2030133E20301
33E2030133E20911381959FAB0EE9000
81C9E03ADE35CEEEEFC5CF8DE9AC0910
38C2E031DE35CEEEEFC8CF87E95C0920
...

Видим восемь повторений последовательности из трехкратного E2030133, которые весьма напоминают восемь первых строчек дампа, заканчивающиеся на 12 нулевых байт. Сразу же можно сделать три вывода:

  1. Пять первых байт 5A56001000 — это некий заголовок, не влияющий на содержимое дампа;
  2. Дальнейшее содержимое зашифровано блоками по 4 байта, причем одинаковым байтам дампа соответствуют одинаковые байты в файле:
    • E2030133 → 00000000
    • 820EE13F → 80078000
    • C20EF13F → 80070000
    • E2091138 → E0076001
    • 1959FAB0 → 2A0600FF
    • EE900081 → 00000A58
    • C9E03ADE → EAFF2000
  3. Видно, что это не XOR-шифрование, а нечто более сложное;, но при этом похожим блокам дампа соответствуют похожие блоки в файле — например, изменению одного бита 80078000→80070000 соответствует изменение одного бита 820EE13F→C20EF13F.


Соответствия между блоками


Получим список всех пар (блок файла, блок дампа), и поищем в нем закономерности:

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


Вот как выглядят первые пары в отсортированном списке:

00beb5ff → 4c07a010
02057139 → 0000f00f
03ef5ed0 → 50ff710f \ изменение в бите 24 в дампе меняет биты 8, 10, 24-27 в файле
04ef5bd0 → 51ff710f < 
0408ed38 → 14002d06  \
05f92ed7 → ffffd087   |
0a5d22bb → f602dffe    > изменение в бите 25 в дампе меняет биты 11, 25-27 в файле
0a62f9a9 → e10f5761   |
0acdc6e4 → a25d2c06  /
0aef53d0 → 53ff710f <
0aef5cd0 → 52ff710f / изменение в бите 24 в дампе меняет биты 8-11 в файле
0bdebd6f → 4c57a410
0d0c7fec → 0064ffff
0d0fe57f → 18402c57
0d8fa4d0 → bfff88ff
0ee882d7 → eafd7f00
1001c5c6 → 6c570042 \
1008d238 → 42003e06  > изменение в бите 1 в дампе меняет биты 0, 3, 16-19 в файле
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  > изменение в бите 3 в дампе меняет биты 2 и 19 в файле
12aed1bf → 40073090 /> изменение в бите 10 в дампе меняет бит 8 в файле
12c3f1ea → 20560001 \
12c9f1ea → 20560002 /  изменения в битах 0 и 1 в дампе меняет биты 17 и 19 в файле
...


Действительно, видны закономерности:

  • Изменения в битах 0–3 в дампе меняют биты 0–3 и 16–19 в файле (маска 000F000F)
  • Изменения в битах 24–25 в дампе меняют биты 8–11 и 24–27 в файле (маска 0F000F00)


Напрашивается гипотеза, что каждые 4 бита в дампе влияют на те же самые 4 бита в каждой 16-битной половине 32-битного блока.

Для проверки — «отрежем» старшие 4 бита в каждом полублоке, и посмотрим, какие пары получатся:

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


После перестановки подблоков по 4 бита в ключе сортировки, соответствия между парами подблоков становятся еще более явными:

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


Соответствия между подблоками


В вышеприведенном списке видны такие соответствия:

  • Для маски 0F000F00:
    • x0xxx0xx в дампе → x2xxx1xx в файле
    • x0xxx4xx в дампе → x2xxx0xx в файле
    • xCxxx0xx в дампе → x0xxx5xx в файле
  • Для маски 00F000F0:
    • xx0xxx0x в дампе → xx0xxx3x в файле
    • xx0xxx5x в дампе → xx9xxx8x в файле
    • xx0xxx9x в дампе → xxAxxxBx в файле
    • xxFxxx0x в дампе → xxExxxDx в файле
    • xxFxxxFx в дампе → xx8xxxDx в файле
  • Для маски 000F000F:
    • xxx0xxx7 в дампе → xxxFxxxF в файле
    • xxx7xxx0 в дампе → xxxExxxF в файле
    • xxx7xxx1 в дампе → xxx9xxx8 в файле


Можно сделать вывод, что каждый 32-битный блок в дампе разбивается на четыре восьмибитных значения, и эти значения заменяются при помощи неких таблиц подстановки, для каждой маски своей. Содержимое этих четырех таблиц кажется относительно случайным, но попробуем выделить из нашего файла их все:

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


Когда таблицы соответствия готовы, код расшифровки получается совсем простой:

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


Заголовок прошивки


В самом начале перед зашифрованными данными был пятибайтный заголовок 5A56001000. Первые два байта — сигнатура 'ZV' — подсказывают, что используется формат LZF; дальше обозначены метод сжатия (0x00 — без сжатия) и длина (0x1000 байт).

Хозяин автомобиля, передавший мне файлы для анализа, подтвердил, что в прошивках встречаются и сжатые LZF данные. К счастью, реализация LZF открыта и довольно проста, так что вместе с моим анализом ему удалось удовлетворить свое любопытство по поводу содержимого прошивок. Теперь он может вносить изменения в код — например, автозапуск двигателя при падении температуры ниже заданного уровня, чтобы использовать автомобиль в условиях суровой русской зимы.

de0yl-6ppopvisr_a80b4yuhjj8.png

© Habrahabr.ru