[Из песочницы] Идем по приборам

n1qrihlhxz2c8eule5fph3ml60c.png

Много ли нужно, чтобы изменить пробег или залезть в память приборной панели?

Есть только один способ узнать — попробовать сделать это самому.


Постановка задачи

В данной статье мы покопаемся в механизмах диагностики нескольких приборных панелей и посмотрим, насколько далеко можно зайти без специализированного дилерского оборудования. Основными инструментами будут скрипты на Python и обычный CAN-USB адаптер. Все воздействия будут сводиться к сообщениям на CAN-шине. Так будет сложнее, но интереснее, так как все сделанное таким способом теоретически можно повторить на настоящем автомобиле через OBDII-разъем. В первую очередь нас будет интересовать доступ к памяти устройства где хранятся его конфигурация и пробег.

Немного дисклеймеров:
Работая с CAN-шиной реального автомобиля есть риск вывести его из строя. Ни в коем случае не беритесь за работу с CAN-шиной без должного опыта, инструментов и мер предосторожности.

Корректировка пробега с корыстной целью — занятие для редисок.


Поиск испытуемых

Чтобы разнообразить эксперимент, мы будем использовать три приборных панели от автомобилей разных производителей. С этой целью на Авито и авторазборках были закуплены три б/у панели согласно следующим критериям:


  • низкая цена детали (возможно с дефектом);
  • популярность автомобиля в прошедшем десятилетии;
  • доступность автомобиля на вторичном рынке;
  • в выборке панелей не должно быть родственных автопроизводителей (например, Hyundai и Kia, Mitsubishi и Citroen и т.п.)

Итак, в порядке закупки и повествования:

6nqyrwvhphubj4ownleltaegoim.jpeg

Приборная панель Hyundai Solaris 2013 г.в.
Партномер: 94003–4l715
Установлен сегментый экран с фиксированным количеством символов и обозначений. По сравнению с остальными просто набита разъемами, целых 4 штуки. Много сигналов поступает не по CAN-шине, а «аналоговым» методом по отдельным проводам.

sswfujlfvnrjsbld_mammc51iga.jpeg

Приборная панель Ford Focus 3 2012 г.в.
Партномер: BM5T-10849-BAE
Стоит монохромный экран малого разрешения. При подаче питания находится в состоянии спячки, чтобы увидеть экран и побаловаться со стрелками нужно периодически будить CAN-сообщениями.

ooapb3m92dwndhxoksqt_a9yzak.jpeg

Приборная панель Mitsubishi Lancer X 2008 г.в.
Партномер: 8100A117A

Оказалась достаточно распространенной и устанавливалась сразу на ряд моделей Mitsubishi, Citroen и Peugeout. Самая шумная из всех, при каждой подаче питания громко пищит и дает понять, что существовать отдельно от автомобиля ей не нравится.

Ни одним из данных автомобилей я не владел, поэтому в случае неточностей в описании прошу понять и простить.


Основы диагностики

Прежде чем идти дальше, сделаем короткую остановку и ознакомимся с используемыми диагностическими протоколами. Большинство систем диагностики строятся на двух схожих между собой протоколах: KWP2000 (Keyword Protocol 2000) и UDS (Unified Diagnostic Services). Первым появился KWP2000, затем на его базе был создан UDS, как более современная реализация. Применение этих протоколов предоставляет следующие возможности для автопроизводителей и автосервисов:


  • чтение кодов неисправностей;
  • запись/чтение прошивки;
  • запись/чтение конфигурации и отдельных настроек;
  • чтение данных с датчиков;
  • приведение в действие актуаторов.

Например, автопроизводитель может пользоваться этими механизмами, чтобы сконфигурировать ЭБУ под конкретную комплектацию собранного автомобиля, а в автосервисе могут использовать команду разжатия колодок при обслуживании тормозной системы.

В обоих протоколах основными участниками процесса являются две сущности: клиент и сервер. Клиент представляет интересы тестового оборудования, а сервер — интересы ЭБУ. Работа с каждым из ЭБУ ведется отдельно и для налаживания взаимодействия по CAN нужно знать адресную пару клиент-сервер. Упрощенно такую систему можно представить так:

cuoe2csjfpi8koozq0a5h_c5s74.png

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

Для защиты важных сервисов могут использоваться разные уровни доступа. Как правило, для заполучения нужного уровня доступа нужно запросить у сервера «семя» и подготовить корректный ответ с помощью заранее известного алгоритма.

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

Получается, чтобы составить беглое представление о неизвестном модуле и возможных операциях над ним, нужно совершить следующие шаги:


  1. Определить адреса клиента и сервера;
  2. Найти все доступные сессии (стандартные, расширенные, диагностические и т.д.);
  3. Для каждой из сессий найти все существующие уровни доступа (0×01, 0×03, 0×05 и т.д.);
  4. Для каждой из сессий найти все доступные сервисы (чтение/запись по адресу, управление рутинами и т.д.)


Инструментарий

Начнем с программной составляющей.
Для взаимодействия с устройством использовались Python-библиотеки:


  • python-udsoncan (диагностический клиент, предоставлет возможность пользоваться сервисами);
  • python-can-isotp (транспортный уровень ISO 15765–2, позволяет передавать данные длиной более 8 байт);
  • python-can (API для общения по CAN-шине).

Они интересны по нескольким причнам:


  • подробная документация с множеством примеров;
  • широкая поддержка железа + не так сложно добавить новое;
  • их можно подключать по мере надобности, начиная с голого CAN и поднимаясь выше;
  • из них можно собрать полноценный стек для UDS-диагностики.

de3xbjcdrx33lizc3jkjsypndkm.png

Использованный набор библиотек отчасти и мотивировал меня написать данную статью, поскольку показался любопытным и полезным для быстрых экспериментов. Связка в виде скриптов на Python и адаптера CAN-USB дают гибкость и способность на лету прикидывать последовательности обмена данными. Можно автоматизированно подбирать пароли к уровням доступа, притворяться дилерским диагностическим оборудованием, просто слушать CAN-шину и многое другое.

Об отрицательном опыте использования: не всегда получалось добиться отклика в заданных временных рамках, например, скрипт мог не успеть отправить flow-control сообщение и тем самым обрывал пересылку большого куска данных. Также были случаи, когда CAN-сообщения отправлялись в неправильном порядке, но разбор полетов показал, что корень проблемы находился не в библиотеках. В конечном счете данные проблемы получилось обойти, главное всегда помнить, что настольный ПК это все-таки не real-time устройство.

Для некоторых задач, например подбора адресов клиента и сервера, нам пригодится утилита caringcaribou, которая тоже использует python-can.

Что касается аппаратной части и адаптеров CAN-USB, то в ход шло все, что удавалось раздобыть и одолжить, библиотеки это позволяют. В данном конкретном случае, работа была проделана с помощью Vector VN1610 и VN1611, а также адаптера на базе протокола Lawicel.
Что до последнего, то он сравнительно прост и предcтавляет из себя виртуальный COM-порт, который преобразует по определенным правилам строки в CAN-сообщения и наборот. При некотором желании похожий девайс можно сделать своими силами, на хабре есть интересная статья «CAN-USB адаптер из stm32vldiscovery» на эту тему.

Для работы с приборными панелями на столе достаточно иметь блок питания, способный выдать 12 В. В обзоре на каждую панель будут приведены краткие справки по железу, распиновке и настройке CAN.


Приборная панель Hyundai Solaris


Обзор

Железо:

q2db1uo5klkbs2bnmlntbubboaq.png

Распиновка панели и конфигурация CAN:

dy2sbnyajmcuqryqyjy_bpyw_y8.png
Замечание: у панели Solaris сзади 4 разъема, поэтому они обозначены слева направо как A, B, C, D.

Сессии и уровни доступа:

js1mqejtsqsltqzhinvpgjlmkro.png

Таблица сервисов:

i5wmp-w4srfrifve2wt9xkenpmm.png

Знаки вопросов указывают на предполагаемое имя сессии, поскольку точной информации нет.
Нумерация сессий почему-то перемешана, 0×03 взят из UDS, а 0×85 и 0×90 взяты из KWP2000.
Из результатов сканирования видно, что панель достаточно демократично относится к чтению и записи в память, эти операции доступны во всех сессиях, в том числе сразу после подачи питания. В таком случае, можно не терять время и сразу приступить к сканированию адресов и типов памяти.


В поисках памяти

Для чтения памяти и в KWP2000 и в UDS используется сервис ReadMemoryByAddress (0×23), есть только некоторые отличия по формату запроса. Для KWP2000 он будет выглядеть вот так:

Все просто, указывается ID сервиса, адрес чтения и сколько байт нужно прочитать. В некоторых системах старший байт может быть использован как memoryIdentifier, чтобы выбирать из какой именно памяти произодится чтение (EEPROM, flash), но при этом ширина адреса уменьшается до 16 бит.

Теперь про UDS:

Здесь ширина адреса и размера памяти может меняться, это настраивается байтом addressAndLengthFormatIdentifier, в котором старший ниббл определяет ширину memorySize, а младший — memoryAddress. Например, для 32-битного memoryAddress и 16-битного memorySize этот байт будет равен 24. Старший байт адреса так же может быть использован как memoryIdentifier.

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

То есть нечто промежуточное: поля шире, чем в KWP2000, но при этом addressAndLengthFormatIdentifier из UDS не применяется. Далее перебираем адреса и находим, что старший байт адреса выбирает тип памяти: 0×00 для флеш-памяти микроконтроллера (виден кусочек таблицы векторов прерываний):

TX: <7C6> (8)    07 23 00 00 00 00 00 04
RX: <7CE> (8)    05 63 80 07 00 07 00 00

и 0×04 для внешней EEPROM.

TX: <7C6> (8)    07 23 04 00 00 00 00 04
RX: <7CE> (8)    05 63 0d a8 00 00 00 00


Скрипт чтения флеш-памяти и EEPROM
import can
import isotp
import logging
import time
import argparse

def process_stack_receive(stack, timeout=1):
    t1 = time.time()
    while time.time() - t1 < timeout:
        stack.process()
        if stack.available():
            break
        time.sleep(stack.sleep_time())
    return stack.recv()

def process_stack_send(stack, timeout=1):
    t1 = time.time()
    while time.time() - t1 < timeout:
        stack.process()
        if not stack.transmitting():
            break
        time.sleep(stack.sleep_time())

def my_error_handler(error):
    logging.warning('IsoTp error happened : %s - %s' % (error.__class__.__name__, str(error)))

def print_frame(frame):
    if frame:
        print(''.join(format(x, '02x') for x in frame))
    else:
        print("None")

bus = can.interface.Bus(bustype='slcan', 
                        channel="COM3", 
                        ttyBaudrate=115200, 
                        bitrate=500000)
addr = isotp.Address(isotp.AddressingMode.Normal_11bits, rxid=0x7ce, txid=0x7c6)
stack = isotp.CanStack(bus, address=addr, error_handler=my_error_handler, params = {'tx_padding':0})

parser = argparse.ArgumentParser()
parser.add_argument("-e", "--eeprom", help="read EEPROM", action="store_true")
parser.add_argument("-f", "--flash", help="read Flash", action="store_true")
args = parser.parse_args()

if args.eeprom:
    for addr in range(0x00000000, 0x000007ff, 0x10):
        addr0 = (addr >>  0) & 0xff
        addr1 = (addr >>  8) & 0xff
        array = [0x23, 0x04, 0x00, addr1, addr0, 0x00, 0x10]

        stack.send(bytearray(array))
        process_stack_send(stack)
        print_frame(process_stack_receive(stack))

if args.flash:
    for addr in range(0x00000000, 0x0003ffff, 0x10):
        addr0 = (addr >>  0) & 0xff
        addr1 = (addr >>  8) & 0xff
        addr2 = (addr >> 16) & 0xff
        array = [0x23, 0x00, addr2, addr1, addr0, 0x00, 0x10]

        stack.send(bytearray(array))
        process_stack_send(stack)
        print_frame(process_stack_receive(stack))

bus.shutdown()

Раз уж получилось прочесть, почему бы не попробовать и записать что-нибудь? Берем сервис WriteMemoryByAddress (0×3d) и, наученные прошлым опытом, поменьше верим стандартам и побольше — собственным догадкам. Рабочим вариантом зароса оказывается:

то есть не нужно даже указывать memorySize, достаточно просто указать адрес и сами байты.

Теперь пишем один байт и тут же читаем назад для проверки. Запрос записи во флеш-память подтверждается успешным ответом 0×7d, но по факту память остается нетронутой:

TX: <7C6> (8)    06 3d 00 00 00 00 81 00
RX: <7CE> (8)    01 7d 00 00 00 00 00 00

TX: <7C6> (8)    07 23 00 00 00 00 00 04
RX: <7CE> (8)    05 63 80 07 00 07 00 00

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

А вот запись в EEPROM прошла успешно:

TX: <7C6> (8)    06 3d 04 00 00 00 0e 00
RX: <7CE> (8)    01 7d 00 00 00 00 00 00

TX: <7C6> (8)    07 23 04 00 00 00 00 04
RX: <7CE> (8)    05 63 0e a8 00 00 00 00

байт 0×0d поменял значение на 0×0e.

Этого достаточно, чтобы переходить к следующему этапу: поиску и изменению значений пробега.


Загадка со змейкой

Сейчас нам необходимо понять какие именно из байт EEPROM отвечают за хранение пробега. Для этого применяется очень простой подход: посылаем на панель сигнал скорости, пока она не увеличивает пробег на 1 км, снова читаем EEPROM и сравниваем с предыдущим дампом. Для симуляции сигнала скорости необходимо найти CAN-сообщение, отвечающее за скорость и отправлять его с определенным периодом. Впрочем, бывают и исключения, как в случае с данной панелью: она оказалась от автомобиля с МКПП, где скорость передается отдельным импульсным сигналом.

Итак, 1 км пройден, давайте сравнивать:

jxbvkkhk5tyoe8bqwy2xirajce4.png

Из всего EEPROM поменялся только один байт и его значение уменьшилось на 0×20? Явно надо идти глубже.

«Накатываем» еще 23 км:

mutk9e3n3c3ekjhyfd8tkw_-joa.gif

И начинаем наблюдать некоторые закономерности:


  1. для хранения используются 64 байта
  2. pост пробега = вычитание значений
  3. таблица проходится зигзагом в 2 отдельных прохода:
    lvqweioshw9dsry-_6ashutggzy.png
  4. вычитаемое определяется номером столбца:

Более подробный разбор спрятан под спойлер, так как он занимает много места.


Разгадка

Итак, начнем с исходных данных для пробега 64890 км, для удобства разбитых на четыре группы:

[13, F8, 27, F0, 4F, E0, 9F, C0, 3F, 81, 7F, 02, FE, 04, FC, 09]
[F8, 13, F0, 27, E0, 4F, C0, 9F, 81, 3F, 02, 9F, 05, 3E, 0A, 7C]
[13, F8, 27, F0, 4F, E0, 9F, C0, 3F, 81, 7F, 02, FE, 04, FC, 09]
[F8, 13, F0, 27, E0, 4F, C0, 9F, 81, 3F, 02, 9F, 05, 3E, 0A, 7C]

Далее для 0-ой и 2-ой строк попарно меняем значения в столбцах:

[F8, 13, F0, 27, E0, 4F, C0, 9F, 81, 3F, 02, 7F, 04, FE, 09, FC]
[F8, 13, F0, 27, E0, 4F, C0, 9F, 81, 3F, 02, 9F, 05, 3E, 0A, 7C]
[F8, 13, F0, 27, E0, 4F, C0, 9F, 81, 3F, 02, 7F, 04, FE, 09, FC]
[F8, 13, F0, 27, E0, 4F, C0, 9F, 81, 3F, 02, 9F, 05, 3E, 0A, 7C]

Вычитаем каждый байт из FF, чтобы заменить разности суммами:

[07, EC, 0F, D8, 1F, B0, 3F, 60, 7E, C0, FD, 80, FB, 01, F6, 03]
[07, EC, 0F, D8, 1F, B0, 3F, 60, 7E, C0, FD, 60, FA, C1, F5, 83]
[07, EC, 0F, D8, 1F, B0, 3F, 60, 7E, C0, FD, 80, FB, 01, F6, 03]
[07, EC, 0F, D8, 1F, B0, 3F, 60, 7E, C0, FD, 60, FA, C1, F5, 83]

Попарно объединяем значения в каждой строке, чтобы получить 2-ух байтные:

[07EC, 0FD8, 1FB0, 3F60, 7EC0, FD80, FB01, F603]
[07EC, 0FD8, 1FB0, 3F60, 7EC0, FD60, FAC1, F583]
[07EC, 0FD8, 1FB0, 3F60, 7EC0, FD80, FB01, F603]
[07EC, 0FD8, 1FB0, 3F60, 7EC0, FD60, FAC1, F583]

Для переполненных значений переносим младшие разряды в старшие:

[07EC, 0FD8, 1FB0, 3F60, 7EC0, FD80, 1FB00, 3F600]
[07EC, 0FD8, 1FB0, 3F60, 7EC0, FD60, 1FAC0, 3F580]
[07EC, 0FD8, 1FB0, 3F60, 7EC0, FD80, 1FB00, 3F600]
[07EC, 0FD8, 1FB0, 3F60, 7EC0, FD60, 1FAC0, 3F580]

Теперь нормализуем значения, чтобы получить набор простых счетчиков; не забываем, что делитель зависит от номера столбца:

[07EC, 07EC, 07EC, 07EC, 07EC, 07EC, 07EC, 07EC]
[07EC, 07EC, 07EC, 07EC, 07EC, 07EB, 07EB, 07EB]
[07EC, 07EC, 07EC, 07EC, 07EC, 07EC, 07EC, 07EC]
[07EC, 07EC, 07EC, 07EC, 07EC, 07EB, 07EB, 07EB]

Наконец, суммируем все счетчики и получаем ожидаемые FD7A (64890).

Осталось только преобразовать желаемый пробег в соответствующую таблицу, записать ее обратно в EEPROM и:

uviekdybnijirqfcaeutrgi4ac4.gif

Шалость удалась!


Приборная панель Ford Focus 3


Обзор

Железо:

0x6slgan3yauofamjtxs42awvcq.png

Распиновка панели и конфигурация CAN:

exxlimbb98iuoks0qr6yr93mssa.png
Замечание: к панели Focus подключаются 2 CAN-шины: MS (Medium Speed) и MM (Multimedia). Нам понадобится MS CAN.

Сессии и уровни доступа:

iatkqvaqfwchsofylfgyfxhrh7w.png

Таблица сервисов:

obeotiuy48gkuduzlix6oohty2k.png

По результатам сканирования видно, что реализация диагностического сервера придерживается стандарта UDS вплоть до нумерации сессий. Сессия 0×01 используется по умолчанию при каждой подаче питания, сессия 0×02 используется для изменения прошивки и сессия 0×03 применяется для изменения настроек. Прямого доступа к памяти посредством ReadMemoryByAddress/WriteMemoryByAddress нет ни в одной из сессий, поэтому начинаем с чтения всех возможных DID с помощью сервиса ReadDataByIdentifier.


Первая попытка

Перебирая DID, читаем значения одно за другим, пока на глаза не попадается DID 0×61BB размером в 3-байта, в котором содержится значение пробега:

RX: <720> (8)    03 22 61 bb 00 00 00 00
RX: <728> (8)    06 62 61 bb 00 e9 20 00

Просто так перезаписать это значение вызовом WriteDataByIdentifier не вышло, для этого необходимо перейти в расширенную диагностическую сессию и получить уровень доступа 0×03.

Краткое отступление по поводу алгоритма. При запросе нужного уровня доступа сервер вернет «семя» длиной 3 байта, для которого нужно подобрать корректный ответ той же длины. Подсказки как правильно это сделать, можно найти в публикациях Beneath the Bonnet: a Breakdown of Diagnostic Security и Adventures in Automotive Networks and Control Units.

Получив уровень доступа 0×03, пробуем произвести запись еще раз, увеличив значение на 1 км:

RX: <720> (8)    02 10 03 00 00 00 00 00
RX: <728> (8)    06 50 03 00 32 01 f4 00

RX: <720> (8)    02 27 03 00 00 00 00 00
RX: <728> (8)    05 67 03 XX XX XX 00 00
RX: <720> (8)    05 27 04 XX XX XX 00 00
RX: <728> (8)    02 67 04 00 00 00 00 00

RX: <720> (8)    06 2e 61 bb 00 e9 21 00
RX: <728> (8)    03 6e 61 bb 00 00 00 00

Тут же читаем обратно, чтобы проверить:

RX: <720> (8)    03 22 61 bb 00 00 00 00
RX: <728> (8)    06 62 61 bb 00 e9 21 00

Все получилось. А если уменьшить значение?… Пробуем:

RX: <720> (8)    02 10 03 00 00 00 00 00
RX: <728> (8)    06 50 03 00 32 01 f4 00

RX: <720> (8)    02 27 03 00 00 00 00 00
RX: <728> (8)    05 67 03 XX XX XX 00 00
RX: <720> (8)    05 27 04 XX XX XX 00 00
RX: <728> (8)    02 67 04 00 00 00 00 00

RX: <720> (8)    06 2e 61 bb 00 e9 20 00
RX: <728> (8)    03 7f 2e 31 00 00 00 00

Панель отвечает ошибкой 0×31 requestOutOfRange, то есть новое значение не проходит проверку. Что ж, значит нужно искать другой путь.


Вторичный загрузчик

Если вернуться к результатам сканирования, то можно найти в них reprogrammingSession. Обычно использование этой сессии подразумевает наличие вторичного загрузчика, и в случае с Ford его легко можно найти в открытом доступе. Поэтому мы попробуем его загрузить и посмотреть, что будет. Вдруг у него есть доступ к EEPROM?

Вторичный загрузчик (Secondary Bootloader) применяется для обновления прошивки ЭБУ следующим образом: SBL загружается в ОЗУ устройства, затем происходит передача управления SBL, после чего начинается чтение/запись флеш-памяти микроконтроллера. Упрощенно это можно представить так:

aqq2pakcfyszky-la_ze8tfmk50.png

Для работы с прошивками Ford испольует формат VBF или Volvo Binary Format. Такой файл состоит из двух частей: человекочитаемого заголовка с адресом загрузки, типом данных и контрольной суммой; и бинарными данными для отправки в ЭБУ.

Последовательность загрузки SBL состоит из следующих шагов:


  1. DiagnosticSessionControl: переход в сессию программирования 0×02;
  2. SecurityAccess: получениe уровня доступа 0×01;
  3. RequestDownload: адрес загрузки 0×03ff0000 (начало ОЗУ), размер передачи указывается как в VBF-файле;
  4. TransferData: непосредственный процесс передачи данных;
  5. RequestTransferExit: запрос завершения передачи данных;
  6. RoutineControl: запуск рутины 0×0301, которая передает управление SBL.

После этого можно приступать к работе с флеш-памятью микроконтроллера: читать ее с помощью RequestUpload и записывать с помощью RequestDownload.


Скрипт чтения флеш-памяти (работает с извлеченным из VBF бинарником)
import can
import isotp
import time
import binascii
from udsoncan.connections import PythonIsoTpConnection
from udsoncan.client import Client
from udsoncan import Response, MemoryLocation, DataFormatIdentifier
from ford_glfsr import calculate_key

download_payload_size = 0xc8
upload_payload_size = 0x20

def my_error_handler(error):
   logging.warning('IsoTp error happened : %s - %s' % (error.__class__.__name__, str(error)))

bus = can.interface.Bus(bustype='slcan', 
                        channel="COM3", 
                        ttyBaudrate=115200, 
                        bitrate=125000)
tp_addr = isotp.Address(isotp.AddressingMode.Normal_11bits, txid=0x720, rxid=0x728)
stack = isotp.CanStack(bus=bus, 
                       address=tp_addr, 
                       params = {'tx_padding':0}, 
                       error_handler=my_error_handler)
conn = PythonIsoTpConnection(stack)

# wake up the IPC
ping_msg = can.Message(arbitration_id=0x080, 
                       data=[0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], 
                       is_extended_id=False)
bus.send(ping_msg)
time.sleep(0.2)

with Client(conn) as client:
    # TesterPresent
    client.tester_present()

    # ChangeSession
    client.change_session(0x02)
    time.sleep(0.3)

    # SecurityAccess
    response = client.request_seed(0x01)
    seed_int = int.from_bytes(response.service_data.seed, "little")
    key_int = calculate_key(seed_int, 0x01)
    key_bytes = key_int.to_bytes(3, byteorder="big")
    client.send_key(0x01, key_bytes)

    # RequestDownload
    memloc = MemoryLocation(address=0x03ff0000, 
                            memorysize=0x54d4, 
                            address_format=32, 
                            memorysize_format=32)
    dfi = DataFormatIdentifier(compression=0, encryption=0)
    client.request_download(memory_location=memloc, dfi=dfi)

    # TransferData
    f = open("BM5T-14C025-AD.bin", "rb")
    ipc_sbl = f.read()
    f.close()

    end_block = 0x6d  # memorysize/upload_payload_size
    print("DOWNLOAD END BLOCK:", (hex(end_block)))
    # sequence should start with "0x01", not "0x00"
    for block_num in range(0x01, end_block + 0x01):
        print(hex(block_num))
        start_i = (block_num - 0x01) * download_payload_size
        end_i = start_i + download_payload_size
        client.transfer_data(block_num, ipc_sbl[start_i:end_i])

    # TransferExit
    client.request_transfer_exit()

    # StartRoutine
    client.start_routine(0x0301, data = b'\x03\xff\x00\x00')

    # RequestUpload
    memloc = MemoryLocation(address=0x00007000, 
                            memorysize=0x000f9000, 
                            address_format=32, 
                            memorysize_format=32)
    dfi = DataFormatIdentifier(compression=0, encryption=0)
    client.request_upload(memory_location=memloc, dfi=dfi)

    upload_result = bytearray()
    end_block = 0x7c80  # memorysize/upload_payload_size
    print("UPLOAD END BLOCK:", (hex(end_block)))
    # sequence should start with "0x01", not "0x00"
    for block_num in range(0x01, end_block + 0x01):
        print(hex(block_num))
        response = client.transfer_data(block_num % 0x100)
        upload_result.extend(response.service_data.parameter_records)

    f = open("flash_dump.bin", "wb")
    f.write(upload_result)
    f.close()

    # TransferExit
    try:
        client.request_transfer_exit()
    except:
        pass

    # ECUReset
    client.ecu_reset(1)

Тем не менее, оказалось, что у SBL нет доступа к внешней EEPROM. Было перепробовано несколько разных SBL, но все попытки закончились ничем. С положительной стороны — у нас теперь есть возможность читать и записывать прошивку микроконтроллера. Что мешает отредактировать ее и залить обратно?


Модификация прошивки

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

Перед этим был экспериментально получен предел увеличения километража: по какой-то странной причине максимально возможное для установки значение составляет 4294967 км.

Этот «волшебное» число пригодилось для поиска операций с граничными значениями, в пределах которых можно изменять пробег в меньшую сторону. Найдя нечто похожее, заменяем одну из проверок на «nop»:

ggqxgsobke1n0r9ynvg8q-4wgjq.png

затем заливаем прошивку обратно, повторяем эксперимент:

TX: <720> (8)    02 10 03 00 00 00 00 00
RX: <728> (8)    06 50 03 00 32 01 f4 00

TX: <720> (8)    02 27 03 00 00 00 00 00
RX: <728> (8)    05 67 03 XX XX XX 00 00
TX: <720> (8)    05 27 04 XX XX XX 00 00
RX: <728> (8)    02 67 04 00 00 00 00 00

TX: <720> (8)    03 22 61 bb 00 00 00 00
RX: <728> (8)    06 62 61 bb 01 e2 40 00

TX: <720> (8)    06 2e 61 bb 01 38 d5 00
RX: <728> (8)    03 6e 61 bb 00 00 00 00

TX: <720> (8)    03 22 61 bb 00 00 00 00
RX: <728> (8)    06 62 61 bb 01 38 d5 00

Теперь все работает и можно мотать пробег в меньшую сторону, как самый настоящий злодей:

zggcqxsifjmembgi5so4yk-fojo.gif

Скорее всего, коммерческие инструменты для изменения пробега работают другим способом, менее хлопотным.


Подмена битмапов

После того, как основная работа проделана, можно и с битмапами поиграться. Например, взять и поменять приветственное лого панели.

Начинается все снова с изучения дампа, но в этот раз нужно найти в нем графические данные. Для этого очень удобно визуализировать файл дампа целиком, пример такого анализа с помощью GIMP был взят отсюда.

Если вкратце, то нужно скормить GIMPу файл под видом raw-данных и, меняя цветовое представление и ширину поля, искать нечто похожее на изображения. Экран у выбранной панели монохромный (бинарный), поэтому тип изображения устанавливаем как «Ч/б 1 бит», а ширину подбираем экспериментально.

При ширине визуализации в 16 пикселей, обнаруживаются первые изображения (шрифт для 1, 2, 3, 4, 5, 6). Но при включении панели отображается логотип Ford, поэтому нужно продолжать поиски. Как оказалось, шрифты и маленькие иконки найти проще, чем крупное изображение, разбитое на куски. Поэтому логотип попался на глаза не с первой попытки:

evq4wy06p94vcj6hwnd_qgygocq.png

Видите эти загогулины? Это два верхних участка буквы F в логотипе Ford. Если собрать все части воедино и склеить в картинку, то мы увидим следующий результат:

dog1doytubrfozvuw1w5b7bkr3m.png


Битмап Ford
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 30 00 FC 00 FE 00 FF 80 E3 80 C1 C0 C0 40 C0 60 E0 60 E0 20 70 30 18 30 00 30 00 30 00 30 80 30 C0 20 F0 60 38 60 1C 60 0C 60 06 60 03 C0 C1 C0 20 C0 20 C0 C0 C0 00 C0 01 C0 01 80 01 80 01 80 03 80 03 80 03 80 07 80 07 80 07 80 07 80 0F 80 0F 80 0F 80 07 80 07 80 07 80 03 80 03 C0 01 C0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 80 00 C0 00 C0 00 C0 00 40 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 18 00 7E 00 FF 80 FF C0 E1 E0 C0 70 C0 30 80 10 80 00 80 01 C0 01 C0 03 E0 03 F0 01 F0 11 78 08 3C 04 1F 84 0F C4 07 E4 01 FE 80 3F C0 0F E0 07 78 03 FC 02 FF 82 CF C2 03 C2 01 E2 00 63 00 66 00 29 80 00 80 40 E0 60 F8 C0 7F C0 1F 00 06 00 07 80 83 C0 E1 E0 F0 60 FC 70 3E F0 1F E0 07 80 0F 80 39 C0 70 E0 70 F0 31 F0 31 E0 19 00 FC 00 FE 00 FF 80 1F 80 07 C0 01 E0 00 E0 00 70 80 70 C0 E0 E0 E0 FF 80 7F 80 3F E0 0F F0 07 FC 80 7E C0 1F E0 0F 60 03 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 01 00 03 00 03 00 07 00 07 00 07 00 07 00 03 00 03 00 01 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 01 00 00 00 00 00 01 00 03 00 07 00 07 00 07 00 07 00 07 00 03 00 03 00 03 00 01 00 00 00 00 00 00 00 04 00 07 00 07 00 07 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 03 00 07 00 0F 00 0E 00 0E 00 0E 00 06 00 07 00 03 00 07 00 0F 00 0F 00 0C 00 0C 00 06 00 07 00 03 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

Получается, что размер лого составляет 114×48 пикселей, оно состоит из 6 фрагментов, а адрес можно приблизительно понять с помощью сетки координат GIMP. Нам остается только отредактировать этот участок прошивки, загрузить его обратно:

fpwjnsn70w8spytqgek_54jxzbu.png
и получить новое стартовое лого.


Битмап Habr
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 FC 00 FC 00 FC 00 FC 00 FC 00 FC 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 FC 00 FC 00 FC 00 FC 00 FC 00 FC 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 FF FF FF FF FF FF FF FF FF FF FF FF F0 01 F8 00 7C 00 7C 00 FC 00 FC 01 FC FF FC FF F8 FF F0 FF E0 FF C0 FF 00 00 00 00 00 00 00 3C 80 FF C0 FF E0 FF F0 FF F8 FF F8 C3 F8 81 F8 00 F8 00 F8 00 F8 00 F0 81 E0 C3 F8 FF F8 FF F8 FF F8 FF F8 FF 00 00 00 00 00 00 00 00 FF FF FF FF FF FF FF FF FF FF FF FF E0 C3 F0 81 F8 00 F8 00 F8 00 F8 00 F8 81 F8 C3 F8 FF F0 FF E0 FF C0 FF 80 FF 00 3C 00 00 00 00 00 00 F8 FF F8 FF F8 FF F8 FF F8 FF F8 FF E0 03 F0 01 F8 00 78 00 7C 00 FC 00 F8 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 1F 00 1F 00 1F 00 1F 00 1F 00 1F 00 00 00 00 00 00 00 00 00 00 00 00 00 1F 00 1F 00 1F 00 1F 00 1F 00 1F 00 00 00 00 00 00 00 00 00 01 00 03 00 07 00 0F 00 1F 00 1F 00 1F 00 1F 00 1F 00 1F 00 1F 00 0F 00 07 00 1F 00 1F 00 1F 00 1F 00 1F 00 00 00 00 00 00 00 00 00 1F 00 1F 00 1F 00 1F 00 1F 00 1F 00 07 00 0F 00 1F 00 1F 00 1F 00 1F 00 1F 00 1F 00 1F 00 0F 00 07 00 03 00 01 00 00 00 00 00 00 00 00 00 1F 00 1F 00 1F 00 1F 00 1F 00 1F 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00


Приборная панель Mitsubishi Lancer X


Обзор

Железо:

_wqebxz6u_xjj8aftkzlhrhua_4.png
Замечание: точно определить модель микроконтроллера не получилось, т.к. маркировка не соответствует модели.

Распиновка панели и конфигурация CAN:

u_h18uizkjattba8axx5e8k8gxu.png

Сессии и уровни доступа:

xh5oke_snyv_hogoh5loqzitwdu.png

Таблица сервисов:

vwnpt1x4anldboo3lys_pk_okl4.png

Сканирование показало, что доступны только две сессии: сессия по умолчанию и расширенная диагностическая. Установить точные наименования сессий и внести некоторую ясность помогла удачная находка. На просторах интернета можно найти обрывки документации начала 2000-х, в которой описаны основы применения KWP2000 для диагностики автомобилей альянса DaimlerChrysler-Mitsubishi, а именно марок Dodge, Chrysler, Jeep, Mitsubishi, Mercedes-Benz и Smart. Этот альянс уже давно распался, что добавляет нашим экспериментам немного автомобильной археологии.

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


Не все коту масленица

В этот раз перебор значений закончился гораздо быстрее, т.к. сервис ReadDataByLocalIdentifier может адресовать только 0xFF уникальных идентификаторов, в отличии от ReadDataByIdentifier где их может быть до 0xFFFF.

Значения пробега были обнаружены под DID 0xAD в виде трех байт:

TX: <6A0> (8)    02 21 ad 00 00 00 00 00
RX: <514> (8)    05 61 ad d0 18 03 00 00

Если попробуем использовать сервис WriteDataByLocalIdentifier и записать значение на 1 км больше, то сервер ответит отказом:

TX: <6A0> (8)    05 3b ad d1 18 03 00 00
RX: <514> (8)    03 7f 3b 12 00 00 00 00

Код ошибки 0×12 в ответном сообщении означает «Sub Function Not Supported» — то есть сервис записи по идентификатору работает корректно, но запись для него невозможна.

У нас осталось еще несколько непроверенных уровней доступа: вдруг если мы их получим, то операция станет возможной или откроются другие способы корректировки? Но для этого надо знать, как правильно это сделать. Поиск информации привел к очень полезной статье на хабре «Что можно сделать через разъем OBD в автомобиле», где автор смог извлечь алгоритм из более свежей цветной панели Mitsubishi с микроконтроллером другой архитектуры внутри. Оказалось, что алгоритм работает и для панели постарше, с помощью этой информации удалось подобрать и получить все три уровня доступа (0×01, 0×07 и 0×09). Самое время для повторной попытки:

TX: <6A0> (8)    02 10 92 00 00 00 00 00
RX: <514> (8)    02 50 92 d0 18 03 1e 00

TX: <6A0> (8)    02 27 01 00 00 00 00 00
RX: <514> (8)    06 67 01 XX XX XX XX 00
TX: <6A0> (8)    06 27 02 XX XX XX XX 00
RX: <514> (8)    03 67 02 34 1c ad 4f 00

TX: <6A0> (8)    02 27 07 00 00 00 00 00
RX: <514> (8)    06 67 07 XX XX XX XX 00
TX: <6A0> (8)    06 27 08 XX XX XX XX 00
RX: <514> (8)    03 67 08 34 36 74 26 00

TX: <6A0> (8)    02 27 09 00 00 00 00 00
RX: <514> (8)    06 67 09 XX XX XX XX 00
TX: <6A0> (8)    06 27 0a XX XX XX XX 00
RX: <514> (8)    03 67 0a 34 9f 1f f5 00

TX: <6A0> (8)    05 3b ad d1 18 03 00 00
RX: <514> (8)    03 7f 3b 12 9f 1f f5 00

которая снова возвращает ошибку. Кстати, забавный момент, по какой-то причине в ответах сервера застревают байты от предыдущих сообщений. Кто-то из разработчиков забыл про очистку буфера?

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

Из упомянутой ранее документации можно узнать, что теоретически должна существовать еще одна сессия: ECUFlashReprogrammingSession (0×85) в которой открываются возможности для загрузки прошивки, но найти способ попасть в нее так и не удалось.

На этом этапе можно было бы взять программатор EEPROM, разобрать панель и просто перезаписать значения, но это отходит от диагностической темы данной статьи.

Впрочем, вполне вероятно, что-то было упущено и истина была где-то рядом.


Извлеченный урок

Тем не менее, время потраченное на изучение панели Mitsubishi не пропало зря, поскольку в процессе получилось изучить отличия KWP2000 и UDS поближе, а также составить сравнительную таблицу:

cmm_otseoziukxkasgpfy0aychg.png

Для сравнения были использованы подмножество KWP2000 DaimlerChrysler-Mitsubishi, которое немного отличается от «чистого» KWP2000 и стандарт UDS 2006 года.

Сравнение интересно тем, что можно увидеть эволюцию, которую прошли протоколы диагностики. Например, избыточные сочетания сервисов были заменены одним сервисом, но с выбором подфункций: DisableNormalMessageTransmission / EnableNormalMessageTransmission заменены на CommunicationControl. То же касается и тройки StartRoutineByLocalIdentifier / StopRoutineByLocalIdentifier / RequestRoutineResultsByLocalIdentifier, которая превратилась в RoutineControl.

Если присмотреться внимательнее, то можно заметить, что в KWP2000 не хватает пары для StartDiagnosticSession, которая называлась бы StopDiagnosticSession и сбрасывала бы сервер в сессию по умолчанию. На самом деле, такой сервис может существовать, но он не входит в стандарт ISO 14230–3, и легко заменятся обычным вызовом StartDiagnosticSession с указанием перейти в сессию 0×81.

Поменялась логика чтения кодов неисправностей, добавлен сервис AccessTimingParameter для настройки продолжительности устанавливаемого соединения, SecuredDataTransmission для защиты сессии от прослушивания, LinkControl для настройки скорости соединения во время сессии.

Ну и самое заметное отличие — это упразднение ReadECUIdentification, который использовался для чтения идентификационных параметров модуля: VIN, серийный номер и т.д. В UDS вся эта информация читается обычным ReadDataByIdentifier, а для стандартных DID добавляется префикс 0xF1. Например для чтения VIN KWP2000 использует команду 0×1A90, а UDS — 0×

© Habrahabr.ru