company_banner

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


    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 открыта и довольно проста, так что вместе с моим анализом ему удалось удовлетворить свое любопытство по поводу содержимого прошивок. Теперь он может вносить изменения в код — например, автозапуск двигателя при падении температуры ниже заданного уровня, чтобы использовать автомобиль в условиях суровой русской зимы.

    RUVDS.com
    VDS/VPS-хостинг. Скидка 10% по коду HABR

    Комментарии 33

      +6
      Странно, что прошивка не подписана производителем и позволяет вносить любые изменения. Сейчас даже в бытовые устройства левую прошивку не загрузить, а тут транспортное средство повышенной опасности.
        +5
        Обычная консервативность технологий в отрасли, где нет общественного внимания по компьютерной безопасности. Подобное еще должно наблюдаться в судоходстве и прочих подобных отраслях, вплоть до заводов.
          0
          Хвала небесам, на заводах это потихоньку привлекает внимание публики.
          Но вообще иранские друзья и участие в их жизни Stuxnet'а подтверждают, что проблема серьёзная ;)
          +3
          Может ещё и капот пломбировать, чтобы хозяин автомобиля не мог открутить какую-нибудь важную деталь?
            0
            Можно поступить как John Deere в США — запретить юридически.
            habr.com/ru/post/396259
            +5
            Так автопроизводителям такой кулхацкинг только на руку.

            • Если получится у юзера провернуть финт с прошивкой, то он еще 10 расскажет о том, «как он поимел систему и проапдейтил свою autoname занедорого». Что почти равняется призыву «покупайте autoname».
            • Если случиться что — то тут и автоматический слет с гарантии и снятие с себя ответственности в случае чего (не виноватые мы, это он сам прошивку накатил), да и для тех же страховых думаю это будет поводом для отказа.
              +1
              На сколько мне известно конкретно в тойотах нельзя считать прошивку. Только залить новую. Таким образом в отличие от большинства европейцев установить факт изменения прошивки затруднительно.
              И еще — нет такой процедуры «слет с гарантии». Есть отказ в гарантийном ремонте, для чего необходимо доказать, что неисправность возникла в результате вмешательства владельца.
                0
                >конкретно в тойотах нельзя считать прошивку

                Но вот же статья именно про анализ дампа прошивки с тойоты.
                Может, штатными средствами и нельзя, поэтому дампят напрямую с флешки программатором.
                  +1
                  Статья про анализ файла прошивки, распространяемого Тойотой. Ни о каком дампе речь не идёт.
                  Toyota распространяет свои прошивки в недокументированном формате. Мой заказчик, у которого автомобиль этой марки, показал мне файл прошивки,
                    +2
                    А страницей ниже:
                    >Конкретно для этой прошивки у него имелся дамп содержимого
                      0
                      my bad ))
                  0
                  И даже отказа в гарантийном ремонте не может быть, может быть отказ в бесплатном ремонте.
                    0
                    Можно. В некоторых прямо по CAN. В других через JTAG, иногда с отпайкой и переносом процессора.
                  0
                  Не надо давать глупые идеи производителям, там своих хватает
                    +1
                    В автомобиль тоже левую прошивку не загрузить. Та что была в блоке редактируется и обратно загружается. Про чиптюнтнг думаю слышали. И в бытовой технике такое возможно, только кому например в голову придет «тюнинговать» стиральную машинку или мультиварку. Поэтому и создается ощущение что там все тихо, а с автомобилями все крутят потому что там все доступно и не защищено.
                      +3

                      Лет 15 назад обращался ко мне знакомый из одного официального сервисного центра по ремонту авто. У них как-то глюкнул фирменный прошивочный софт, и в снегоход залилась программа от водного мотоцикла (там моторы одинаковые). Мотор не заводится, не хватает каких-то сигналов от водомета. А наоборот прошить никак — в водный мотоцикл прошивка от снегохода никак не льется.

                      0
                      Большинство программ флешеров сейчас считают новые контрольные суммы в правленной прошивке.
                        +1
                        Я в криптографии не силён, но контрольная сумма — это не подпись.
                          +1
                          Да, но при заливке нужна она.
                        0
                        ну можем рассмотреть тот же VAG на платформе MQB, можно кодингом менять некоторые вещи, не глобально но изменения получаются достаточно приятные для пользователя.
                        но возможно мое сравнение некорректно
                        +4
                        Интересная тема, расшифровка в ней не самое интересное, как насчет остального процесса?
                          +1
                          В остальном автор не силен. ) Как в общем-то и в расшифровке)
                          +2
                          А сама прошивка после расшифровки что из себя представляет? Бинарь какой-то, текстовый файл с неким набором параметров или что?
                            +1
                            Прошивка для контроллера на выходе, надо дизассемблить
                            +1

                            А прошивка из какого блока, моторный, АКП, бортовой электроники? Блоков в современном автомобиле может быть более десятка и в каждом процессор с прошивкой. В зависимости от блока и примененного процессора прошивка может быть программным кодом, а может быть просто набором калибровок. Если это был моторный блок и была идея сделать "чип тюнинг", то для этого существуют специализированные редакторы, где можно наглядно двигать параметры, а не ковыряться в байтах. Не понятна цель сего действа.

                              0
                              Подтверждаю на собственном отрицательном опыте — в моём уже не самом современном автомобиле от старости слетели настройки дроссельной заслонки (память бэдами покрылась там что ли?), самоадаптация приспособиться так и не смогла (обороты менялись немного в зависимости от педали и в основном от какого-то рандома), решилось перепрошивкой.
                                0
                                Для Тойоты редакторов по сути нет. Единственный нормальный продукт — HPTuners но он для ограниченного числа моделей американского рынка. Есть еще отечественный BitEdit но он мягко говоря сыроват (и ему таки требуется раскодированный файл прошивки). Кроме того, ни один редактор не позволит сделать изменения в программном коде.
                                  +1

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

                                    +2
                                    В современных тойотах прошивка ДВС и большая часть прошивки АКПП хранится в одном процессоре и соответственно в одном файле обновления приходит. Поскольку автор указывал что смог считать содержимое, речь явно идет о ДВС — только на него и выходят обновления, и есть инструменты для считывания. Может быть непрофессионально, но статей по теме мало, и мне было интересно
                                0
                                Как то у Toyota с этим сложно. У Митсу очень много и не очень сложно можно всего поменять. Я в свое время сам активировал круиз контроль, дилер за 500р залил новую прошивку в машину причём официально прайсу.
                                  +1

                                  Кроме всего прочего автор даже не знает полного алгоритма. )))) То что он описал — лишь первая часть ))

                                    +1
                                    формат называется CUW(calibration update wizard), маска шифрование находится в самом файле
                                      +1

                                      Хаб "реверс инжиниринг" надо бы ещё добавить.

                                      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                      Самое читаемое