[Из песочницы] Идем по приборам
Много ли нужно, чтобы изменить пробег или залезть в память приборной панели?
Есть только один способ узнать — попробовать сделать это самому.
Постановка задачи
В данной статье мы покопаемся в механизмах диагностики нескольких приборных панелей и посмотрим, насколько далеко можно зайти без специализированного дилерского оборудования. Основными инструментами будут скрипты на Python и обычный CAN-USB адаптер. Все воздействия будут сводиться к сообщениям на CAN-шине. Так будет сложнее, но интереснее, так как все сделанное таким способом теоретически можно повторить на настоящем автомобиле через OBDII-разъем. В первую очередь нас будет интересовать доступ к памяти устройства где хранятся его конфигурация и пробег.
Немного дисклеймеров:
Работая с CAN-шиной реального автомобиля есть риск вывести его из строя. Ни в коем случае не беритесь за работу с CAN-шиной без должного опыта, инструментов и мер предосторожности.
Корректировка пробега с корыстной целью — занятие для редисок.
Поиск испытуемых
Чтобы разнообразить эксперимент, мы будем использовать три приборных панели от автомобилей разных производителей. С этой целью на Авито и авторазборках были закуплены три б/у панели согласно следующим критериям:
- низкая цена детали (возможно с дефектом);
- популярность автомобиля в прошедшем десятилетии;
- доступность автомобиля на вторичном рынке;
- в выборке панелей не должно быть родственных автопроизводителей (например, Hyundai и Kia, Mitsubishi и Citroen и т.п.)
Итак, в порядке закупки и повествования:
Приборная панель Hyundai Solaris 2013 г.в.
Партномер: 94003–4l715
Установлен сегментый экран с фиксированным количеством символов и обозначений. По сравнению с остальными просто набита разъемами, целых 4 штуки. Много сигналов поступает не по CAN-шине, а «аналоговым» методом по отдельным проводам.
Приборная панель Ford Focus 3 2012 г.в.
Партномер: BM5T-10849-BAE
Стоит монохромный экран малого разрешения. При подаче питания находится в состоянии спячки, чтобы увидеть экран и побаловаться со стрелками нужно периодически будить CAN-сообщениями.
Приборная панель Mitsubishi Lancer X 2008 г.в.
Партномер: 8100A117A
Оказалась достаточно распространенной и устанавливалась сразу на ряд моделей Mitsubishi, Citroen и Peugeout. Самая шумная из всех, при каждой подаче питания громко пищит и дает понять, что существовать отдельно от автомобиля ей не нравится.
Ни одним из данных автомобилей я не владел, поэтому в случае неточностей в описании прошу понять и простить.
Основы диагностики
Прежде чем идти дальше, сделаем короткую остановку и ознакомимся с используемыми диагностическими протоколами. Большинство систем диагностики строятся на двух схожих между собой протоколах: KWP2000 (Keyword Protocol 2000) и UDS (Unified Diagnostic Services). Первым появился KWP2000, затем на его базе был создан UDS, как более современная реализация. Применение этих протоколов предоставляет следующие возможности для автопроизводителей и автосервисов:
- чтение кодов неисправностей;
- запись/чтение прошивки;
- запись/чтение конфигурации и отдельных настроек;
- чтение данных с датчиков;
- приведение в действие актуаторов.
Например, автопроизводитель может пользоваться этими механизмами, чтобы сконфигурировать ЭБУ под конкретную комплектацию собранного автомобиля, а в автосервисе могут использовать команду разжатия колодок при обслуживании тормозной системы.
В обоих протоколах основными участниками процесса являются две сущности: клиент и сервер. Клиент представляет интересы тестового оборудования, а сервер — интересы ЭБУ. Работа с каждым из ЭБУ ведется отдельно и для налаживания взаимодействия по CAN нужно знать адресную пару клиент-сервер. Упрощенно такую систему можно представить так:
Сессией определяется текущее состояние сервера и набор доступных сервисов. То есть при выборе стандартной сессии вряд ли будут доступны перезагрузка модуля или перепрошивка, так как это не предусмотрено в обычном состоянии. В то же время при переходе в сессию пререпрошивки модуль перестанет принимать/рассылать рабочий трафик и будет ждать передачи данных.
Для защиты важных сервисов могут использоваться разные уровни доступа. Как правило, для заполучения нужного уровня доступа нужно запросить у сервера «семя» и подготовить корректный ответ с помощью заранее известного алгоритма.
Каждый отдельный сервис выполняет только одну определенную функцию, например чтение памяти по адресу или сброс ошибок.
Получается, чтобы составить беглое представление о неизвестном модуле и возможных операциях над ним, нужно совершить следующие шаги:
- Определить адреса клиента и сервера;
- Найти все доступные сессии (стандартные, расширенные, диагностические и т.д.);
- Для каждой из сессий найти все существующие уровни доступа (0×01, 0×03, 0×05 и т.д.);
- Для каждой из сессий найти все доступные сервисы (чтение/запись по адресу, управление рутинами и т.д.)
Инструментарий
Начнем с программной составляющей.
Для взаимодействия с устройством использовались Python-библиотеки:
- python-udsoncan (диагностический клиент, предоставлет возможность пользоваться сервисами);
- python-can-isotp (транспортный уровень ISO 15765–2, позволяет передавать данные длиной более 8 байт);
- python-can (API для общения по CAN-шине).
Они интересны по нескольким причнам:
- подробная документация с множеством примеров;
- широкая поддержка железа + не так сложно добавить новое;
- их можно подключать по мере надобности, начиная с голого CAN и поднимаясь выше;
- из них можно собрать полноценный стек для UDS-диагностики.
Использованный набор библиотек отчасти и мотивировал меня написать данную статью, поскольку показался любопытным и полезным для быстрых экспериментов. Связка в виде скриптов на 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
Обзор
Железо:
Распиновка панели и конфигурация CAN:
Замечание: у панели Solaris сзади 4 разъема, поэтому они обозначены слева направо как A, B, C, D.
Сессии и уровни доступа:
Таблица сервисов:
Знаки вопросов указывают на предполагаемое имя сессии, поскольку точной информации нет.
Нумерация сессий почему-то перемешана, 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
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 км пройден, давайте сравнивать:
Из всего EEPROM поменялся только один байт и его значение уменьшилось на 0×20? Явно надо идти глубже.
«Накатываем» еще 23 км:
И начинаем наблюдать некоторые закономерности:
- для хранения используются 64 байта
- pост пробега = вычитание значений
- таблица проходится зигзагом в 2 отдельных прохода:
- вычитаемое определяется номером столбца:
Более подробный разбор спрятан под спойлер, так как он занимает много места.
Итак, начнем с исходных данных для пробега 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 и:
Шалость удалась!
Приборная панель Ford Focus 3
Обзор
Железо:
Распиновка панели и конфигурация CAN:
Замечание: к панели Focus подключаются 2 CAN-шины: MS (Medium Speed) и MM (Multimedia). Нам понадобится MS CAN.
Сессии и уровни доступа:
Таблица сервисов:
По результатам сканирования видно, что реализация диагностического сервера придерживается стандарта 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, после чего начинается чтение/запись флеш-памяти микроконтроллера. Упрощенно это можно представить так:
Для работы с прошивками Ford испольует формат VBF или Volvo Binary Format. Такой файл состоит из двух частей: человекочитаемого заголовка с адресом загрузки, типом данных и контрольной суммой; и бинарными данными для отправки в ЭБУ.
Последовательность загрузки SBL состоит из следующих шагов:
DiagnosticSessionControl
: переход в сессию программирования 0×02;SecurityAccess
: получениe уровня доступа 0×01;RequestDownload
: адрес загрузки 0×03ff0000 (начало ОЗУ), размер передачи указывается как в VBF-файле;TransferData
: непосредственный процесс передачи данных;RequestTransferExit
: запрос завершения передачи данных;RoutineControl
: запуск рутины 0×0301, которая передает управление SBL.
После этого можно приступать к работе с флеш-памятью микроконтроллера: читать ее с помощью RequestUpload
и записывать с помощью RequestDownload
.
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»:
затем заливаем прошивку обратно, повторяем эксперимент:
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
Теперь все работает и можно мотать пробег в меньшую сторону, как самый настоящий злодей:
Скорее всего, коммерческие инструменты для изменения пробега работают другим способом, менее хлопотным.
Подмена битмапов
После того, как основная работа проделана, можно и с битмапами поиграться. Например, взять и поменять приветственное лого панели.
Начинается все снова с изучения дампа, но в этот раз нужно найти в нем графические данные. Для этого очень удобно визуализировать файл дампа целиком, пример такого анализа с помощью GIMP был взят отсюда.
Если вкратце, то нужно скормить GIMPу файл под видом raw-данных и, меняя цветовое представление и ширину поля, искать нечто похожее на изображения. Экран у выбранной панели монохромный (бинарный), поэтому тип изображения устанавливаем как «Ч/б 1 бит», а ширину подбираем экспериментально.
При ширине визуализации в 16 пикселей, обнаруживаются первые изображения (шрифт для 1, 2, 3, 4, 5, 6). Но при включении панели отображается логотип Ford, поэтому нужно продолжать поиски. Как оказалось, шрифты и маленькие иконки найти проще, чем крупное изображение, разбитое на куски. Поэтому логотип попался на глаза не с первой попытки:
Видите эти загогулины? Это два верхних участка буквы F в логотипе 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. Нам остается только отредактировать этот участок прошивки, загрузить его обратно:
и получить новое стартовое лого.
00 00 00 00 00 00 00 00 00 00 00 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
Обзор
Железо:
Замечание: точно определить модель микроконтроллера не получилось, т.к. маркировка не соответствует модели.
Распиновка панели и конфигурация CAN:
Сессии и уровни доступа:
Таблица сервисов:
Сканирование показало, что доступны только две сессии: сессия по умолчанию и расширенная диагностическая. Установить точные наименования сессий и внести некоторую ясность помогла удачная находка. На просторах интернета можно найти обрывки документации начала 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 поближе, а также составить сравнительную таблицу:
Для сравнения были использованы подмножество 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×