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 нулевых байт. Сразу же можно
сделать три вывода:- Пять первых байт
5A56001000
это некий заголовок, не влияющий на содержимое дампа; - Дальнейшее содержимое зашифровано блоками по 4 байта, причем
одинаковым байтам дампа соответствуют одинаковые байты в файле:
E2030133 00000000
820EE13F 80078000
C20EF13F 80070000
E2091138 E0076001
1959FAB0 2A0600FF
EE900081 00000A58
C9E03ADE EAFF2000
- Видно, что это не 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 открыта и довольно проста, так что вместе с моим анализом ему удалось удовлетворить свое любопытство по поводу содержимого прошивок. Теперь он может вносить изменения в код например, автозапуск двигателя при падении температуры ниже заданного уровня, чтобы использовать автомобиль в условиях суровой русской зимы.