Русский
Русский
English
Статистика
Реклама

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


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

CALIBRATIONXi
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 Nim5A56001000820EE13FE2030133E20301
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-шифрование, а нечто более сложное; но при этом похожим блокам дампа соответствуют похожие блоки в файле например, изменению одного бита 8007800080070000 соответствует изменение одного бита 820EE13FC20EF13F.

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


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

$ 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  4c07a01002057139  0000f00f03ef5ed0  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  4c57a4100d0c7fec  0064ffff0d0fe57f  18402c570d8fa4d0  bfff88ff0ee882d7  eafd7f001001c5c6  6c570042 \1008d238  42003e06  > изменение в бите 1 в дампе меняет биты 0, 3, 16-19 в файле100ec5cf  6c570040 /109ec58f  6c07005010e1ebdf  62ff600810ec4cdd  dafd4c07119f0f8f  08006d5711c0feee  2c5f0500120ff07e  20420452125ef13e  20f600c8125fc14e  60420032126f02af  02006d671281d09f  400f34881281d19f  400f308812a6d0bb  4007349812a6d1bb  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  0f63ff020388  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  52e8831c98ff  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 открыта и довольно проста, так что вместе с моим анализом ему удалось удовлетворить свое любопытство по поводу содержимого прошивок. Теперь он может вносить изменения в код например, автозапуск двигателя при падении температуры ниже заданного уровня, чтобы использовать автомобиль в условиях суровой русской зимы.

Источник: habr.com
К списку статей
Опубликовано: 30.07.2020 12:15:48
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Блог компании ruvds.com

Python

Автомобильные гаджеты

Лайфхаки для гиков

Ruvds_статьи

Автомобили

Прошивка

Категории

Последние комментарии

© 2006-2021, personeltest.ru