Титул
Титул

Предыдущая статья 24 года на Хабре (https://habr.com/ru/articles/839936/) закончилась на вопросах, одним из которых был: «Для процессоров Snapdragon 8+ Gen1 используются программеры версии 7. Будет ли их поддержка в FhF?». Только сейчас я готов рассказать, что именно изменилось в алгоритмах обработки команд по протоколу Sahara v3, и вернуться к разбору кода программеров, теперь уже для версии 7. Поддержка уже реализована в новых версиях Firehose-Finder (https://github.com/hoplik/Firehose-Finder).  Код открытый, можно ознакомиться с любой его частью, кроме идентификатора телеграмм-бота. Так как официальной документации по работе с протоколом и программерами не нашлось (библиотека знаний Qualcomm стала не доступна), пришлось весь этот алгоритм расписывать самостоятельно. В связи с этим возможны неточности или частичная некорректная работа (не так, как предусматривал производитель оборудования). Идею об изменении команды запроса идентификаторов процессора для Sahara v3 я подсмотрел в открытом репозитарии наших китайских друзей (https://github.com/xiriovo). За что им огромное спасибо!

Представим ситуацию. У нас на руках есть устройство на базе SoC (чипсет) от Qualcomm в аварийном режиме – emergency download, edl, USB\VID_05C6&PID_9008 (для корректного определения в Диспетчере устройств соответствующие драйвера должны быть установлены). Для доступа к памяти устройства, чтобы провести его восстановление, требуется программер. В сети их много и большинство называется просто «prog_firehose_ddr.elf» без указания модели устройства, для которого их разрабатывали. Можно пробовать загружать по одному и проверять подходит или нет, но это очень долго. Куда быстрее и проще проанализировать список из нескольких программеров и проверить на соответствие только несколько, наиболее подходящих из них.

Связка устройство-компьютер представляет собой систему клиент-сервер. Основная специфика такой системы – без запроса от клиента сервер почти ничего не может. Программное обеспечение в такой системе тоже разделено на клиентскую часть и серверную. На устройство в процессор (в PBL, Primary Bootloader) записана клиентская часть протокола Sahara. На компьютере запускается её серверная часть. Это может быть библиотека или часть кода какого-то самописного софта, или исполнительный файл qsaharaserver.exe. В этой части статьи проанализируем работу с файлом от Qualcomm.

Первое, что происходит после подключения устройства к компьютеру и после определения его vid и pid, – это открытие со стороны компьютера com-порта. Сразу после открытия порта от устройства приходит информация, которая в терминологии Qualcomm называется «Hello». Выглядит это как строка из 48 байт. Для нас имеет значение только номер версии Sahara, на которой будет работать протокол.

Это данные с тестового устройства. Тут Sahara v2
Это данные с тестового устройства. Тут Sahara v2

Пришло 48 байт (96 знаков) в little-endian, это 12 групп по 4 байта. Разбираем начало строки «Hello». Первая группа – 0х00000001 – идентификатор команды, следующая – 0х(без первых нулей)30 – длина в байтах – 48 байт в десятичной системе исчисления, третья 0х02 – версия Sahara, следующая 0х01 – сопоставимо с версией – Sahara v1.

В зависимости от версии Sahara команды для получения идентификаторов процессора отличаются. Также отличается и алгоритм обработки полученных результатов даже в случае совпадения команд. Сравнение отправки и обработки команд приведу для удобства в таблице:

Sahara v2

Sahara v3

0x01 – запрос серийного номера чипа

0x01 – запрос серийного номера чипа

Ответ 4 байта. Совпадает с номером чипа, записанным в памяти в десятичном формате.

Ответ 8 байт. Последние 4 байта совпадают с номером чипа, записанным в памяти. Первые 4 байта я использую, как признак работы по протоколу Sahara v3.

0x02 – запрос идентификаторов чипа

0xА – запрос идентификаторов чипа

Три одинаковые группы идентификаторов HW_ID (JTAG_ID, OEM_ID, MODEL_ID). Выбираем одну и разбиваем на отдельные, соответствующие идентификаторы.

Получаем 10 байт. Jtag_id – 4 байта, Oem_id – 2 байта, Model_id – 2 байта.

Предположительно! ARB – 2 байта.

0x03 – запрос хеша корневого сертификата

0x03 – запрос хеша корневого сертификата

Три хеша. Либо они одинаковы, либо с версии Sahara v2.4. это хеши APPS, MBA, и MSS. Просто всегда берём первого.

Один хеш.

0x07 – запрос версии загрузчика (antiroll back)

Нет отдельной команды. Команда 0х07 выдаёт ошибку, как неизвестная.

Читаем всё, что пришло в ответе.

Данные получаю из ответа команды 0xА. Предполагаю, что данные из этого поля соответствуют понятию «версия ARB (antiroll back) загрузчика».

Прошу заметить, что тут речь идёт о версии ARB загрузчика в PBL в чипе, а не версии ARB для операционной системы Android в памяти устройства (HLOS, high level operation system). До загрузки операционной системы отсюда ещё очень далеко. То, что эти данные относятся к ARB пока не точно!

После успешного получения данных и их обработки в программе FhF (Firehose-Finder) по идентификаторам и открытой базе данных проверяется, подключалось ли такое же устройство ранее. Если устройство новое, с разрешения пользователя данные отправляются в публичный канал Телеграмм для последующего добавления или корректировки базы данных. Сообщение с идентификаторами выглядит примерно таким вот образом: 

Сообщение в канал
Сообщение в канал

По полученным идентификаторам устройства теперь можем подбирать программер. Как я уже писал ранее, для работы с устройствами на протоколе Sahara v3 п��именяются программеры версии 7. От версии 6 и более ранних отличаются тем, что вместо одного файла типа «elf» (или двух, где внутри одного elf может быть ещё один, вложенный) контейнер имеет 5 elf. Таким образом, признак «более двух типов «elf» в файле» может быть использован для определения начала обработки мультиэльф файла.

Исполняемые файлы типа «elf» имеют сертификаты (электронно-цифровая подпись) для проверки неизменности кода. При этом хеш корневого сертификата производителя записан в чипе устройства. Сертификатов в мультиэльфе обычно 18 штук и распределяются они по пяти «elf» следующим образом:

  1. Первый «elf» не подписан.

  2. Второй и третий «elf» подписан Qualcomm и OEM (по шесть сертификатов).

  3. Четвёртый и пятый «elf» подписан только OEM (по три сертификата).

Можно посчитать и сравнить хеш корневого сертификата OEM из любого «elf», кроме первого. Я же предпочитаю брать последний, пятый, потому что в нём обычно производитель хранит дерево устройств, для которых предназначен данный программер. Дерево устройств мы легко можем найти по 4 байтам «D00D FEED» и по адресу определить, к какому эльфу оно относится. С этого же эльфа получаем корневой сертификат производителя, считаем его хеш и добавляем в переменную для последующего сравнения с данными из камня. Корневой сертификат производителя находится по маске «3082(.{4})3082» как последнее вхождение для выбранного эльфа.

команда binwalk
команда binwalk

Итак, у нас из пяти требуемых для сравнения параметров один теперь уже есть – OEM_PK_Hash. Второй – JTAG_ID. Хранение кода процессора в дереве устройств и в чипе различаются. Раньше, до 7-ой версии программера, использовались одинаковые цифровые коды. Начиная с 7-ой версии программера, в дерево устройств записывается строковое наименование, а в чипе остался цифровой код. Так что теперь для корректного сопоставления требуется таблица соответствия. Вероятно, это одна из существенных проблем для открытого ПО, т.к. такого соответствия в открытых источниках мне найти не удалось и пришлось создавать вручную, сбором данных с различных сайтов. На текущий момент это выглядит следующим образом:

Таблица соответствия цифрового и буквенного кода процессора
Таблица соответствия цифрового и буквенного кода процессора

Получаем из дерева устройств наименование процессора. Адрес вхождения у нас есть (на примере другого файла будет 0х163BF4), дальше обрабатываем шапку DTB (device tree blob). По спецификации DTB (https://github.com/devicetree-org/devicetree-specification/releases/tag/v0.4) шапка у нас состоит из 10 значений по 4 байта каждое. Все записи в big-endian. Третья группа – сдвиг начала структуры, т.е. пропускаем 8 байт от начала дерева и считываем адрес, с которого располагаются данные (выделено красным). 0х163BF4 + 0x38 = 0x163C2C. Стартовая нода имеет код 0x0000 0001 и обычно её имя просто заполняется нулями (выделено жёлтым). Свойства ноды имеет код 0x0000 0003, потом идёт длина (на примере 0xD без первых нулей = 13 байт), которая записывается кратно 4 байтам, т.е. занимает 16 байт (добивается нулями) и потом уже имя с кодом 0x0000 0000. Запись сопоставимого процессора рекомендуется оформлять как «производитель, модель». Так же записывается и подмодель процессора следующей строкой с кодом 0x0000 000B (выделено зелёным).

Шапка дерева устройств
Шапка дерева устройств

Теперь можем сопоставить модель чипа из дерева устройств программера с JTAG_ID чипа устройства, используя таблицу соответствия. Осталось найти код производителя (OEM_ID), модель самого устройства (MODEL_ID) и версию ARB загрузчика (SBL SW Version). Для этого нам придётся разобрать шапку эльфа (в данном случае пятого эльфа внутри мультиэльфа). По данным из этой шапки определить, где располагаются необходимые нам данные. Адрес начала нужного нам эльфа можно найти по маске «7F45 4C46» или просто открыть архиватором 7Z. 1 405 260 = 0x0015714C (выделено красным).

Работа 7Z с мультиэльф
Работа 7Z с мультиэльф

Из данных шапки понятно, что в нашем примере эльф содержит 7 (0x0007) программных заголовков по 56 байт (0x0038) каждый, и начинаются они после шапки размером 64 байта (0x0040) (выделено красным). Переходя по этим адресам, для каждого программного заголовка мы видим его тип и флаг (выделено жёлтым). После них идёт адрес сдвига относительно начала файла (выделено зелёным). Тип заголовка 0x0000 0000 соответствует NULL. Первый NULL – это шапка эльфа (адрес сдвига 0). Мы её пропускаем, как и остальные программные заголовки с типом 0x0000 0001, которые участвуют в загрузке.

Разбор шапки эльфа
Разбор шапки эльфа

Находим следующий тип NULL c флагом 0x0200 0000 (выше 20 флаги командой readelf не комментируются) по адресу 0x1572DC. Тут адрес сдвига 0x0000 0000 0002 2000. То, что нам нужно!

Обрабатываем программные заголовки
Обрабатываем программные заголовки

Можем проверить корректность поисков, прочитав данные нашего эльфа командой readelf –a.

Проверяем корректность алгоритма
Проверяем корректность алгоритма

Последний программный заголовок идёт с типом «NULL», т.е. раздел не загружается как исполняемый компонент, а просто содержит данные, которые нам нужны для анализа. У седьмой версии программеров адреса для данных почти совпадают с версией 6, но есть отличия. По адресу 0x15714C + 0x22000 = 0x17914C смотрим данные программера:

Таблица пользовательских данных
Таблица пользовательских данных
  • Сдвиг 0x4 (2 байта) – 0007 – седьмая версия программера (как и в версии 6).

  • Сдвиг 0x38 (2 байта) – 0003 – тип ПО – Firehose programmer (как и в версии 6).

  1. ARB (SBL SW Version). Сдвиг 0x50 (2 байта) – A013 – ARB, версия программера для загрузчика. Пока не точно, что это про ARB!

  2. OEM_ID. Сдвиг 0xC8 (2 байта) – 0051 – код OEM – BBK.

  3. MODEL_ID. Сдвиг 0xCA (2 байта) – 0000 – код модели. Пока не точно! По аналогии с версией 6.

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