Близкий родственник эльфа – программер
Олеся Ахметшина. Шедеврум.
Оглавление
Заголовок файла
Начинаются отличия
Стартуем с конца. Убираем цепочку сертификатов.
Проверяем хеш программных разделов
В шапке остались заголовок и пользовательские данные
Разбираем пользовательский раздел
Увидимся
P.S.
Многие знакомы с ELF-файлами и их структурой. На Хабре достаточно статей про это. Поговорим о программерах. Программер — это файл в формате ELF (расширение может быть BIN, MBN или ELF), который предназначен для работы с памятью смартфонов на Android с процессорами от Qualcomm в режиме аварийной загрузки (EDL mode — emergency download, 9008). Также его некоторые называют «пожарный шланг» (от английского firehose) или просто «шланг». Файл представляет из себя контейнер с набором команд для базовой работы с памятью, которые подписаны цепочкой сертификатов. Иногда возникает необходимость подобрать для своего устройства подходящий программер. Очень мало программ, способных решить такой вопрос. Большинство из такого рода софта просто позволяет загрузить имеющийся программер в устройство, а вот выбрать из большого массива, скачанного из интернета или подтянуть из базы данных — крайне мало. Наиболее известный — edl (https://github.com/bkerler/edl). Другой удачный проект, к сожалению, с завершившейся поддержкой базы данных — Emmcdl_Gui (https://4pda.to/forum/index.php? showtopic=1020752). Программы для работы с устройствами в аварийном режиме, но без баз программеров — QFIL и emmdl от Qualcomm (устарели, теперь PCAT, но для неё требуется лицензия), ещё есть несколько программ от вендоров, которые специализируются на конкретных моделях. Можно просто опросить устройство программой QLM CPU Info (https://4pda.to/forum/index.php? showtopic=643084&st=8820#entry90626224) и по этим данным подбирать программер. Есть ещё платные варианты, но на них я, пожалуй, ссылаться не буду. Поэтому в 2020 я решил создать проект «Firehose Finder». В нём я попытался автоматизировать получение данных от устройства и пакетно обрабатывать массив программеров, которые раньше приходилось анализировать практически вручную, по одному. Также у пользователя есть возможность поделиться рабочим программером или идентификаторами своего устройства. Предложение о добавлении в базу данных успешно использованного программера или данных о новом, подключённом устройстве отправляется ботом в общий канал Телеграм и доступно сразу всем подписчикам. Основные моменты, как происходит подбор программера под требуемые параметры, я и хочу показать в этой статье. Ссылку на свой проект на GitHub оставлю в конце статьи.
Заголовок файла
Поиск подходящего программера начинается с подключения устройства. Если оно уже находится в аварийном режиме, то необходимо запросить идентификаторы, если устройство подключено в обычном режиме, то можно попробовать его перегрузить в аварийный средствами ADB (не все устройства поддерживают перезагрузку в аварийный режим средствами ADB). Если программно перегрузиться в аварийный режим не получилось, то остаётся вариант использовать тест-поинты.
Получение идентификаторов устройства происходит по протоколу Sahara, без использования программера. Обращение идёт напрямую к процессору и при успешном старте сессии (процессор должен отправить «Hello» при открытии порта) пользователь увидит идентификаторы, которые будут использованы для проверки корректности выбранного программера.
Основной идентификатор — OEM_PK_HASH — контрольная сумма (хеш) корневого сертификата вендора. Не менее важные:
● HW_ID — hardware id — идентификатор железа, который состоит из трёх частей:
— JTAG_ID — код процессора,
— OEM_ID — код вендора,
— MODEL_ID — код модели устройства;
● ANTI_ROLLBACK_VERSION — номер версии программного обеспечения, ниже которого процессор не примет код.
Получив все эти идентификаторы можно начинать поиск подходящего программера. Взял, для примера, программер от Xiaomi для phoenix (Redmi K30). Как и все файлы формата ELF он начинается с magic number »7F45 4C46».
Признак ELF и заголовки
Дальше идут данные стандартного заголовка. На его разборе в рамках данной статьи останавливаться не будем. Обращу внимание только на адреса 0×34, 0×36 и 0×38. Значения по этим адресам рассказывают нам о том, что в файле собрано 0×12 (18) программных заголовков размером 0×38 (56) байт и начинаются они с адреса 0×40.
Начинаются отличия
Если использовать инструмент readelf в Linux с параметрами –l -W, то в ответе можно заметить, что у первых двух программных заголовков не отображаются флаги.
Пропущенные флаги
В хекс-редакторе адреса этих флагов 0×47–44 и 0×7F-7C, соответственно 0×07 и 0×022 без последних нулей.
Достаём флаги из кода
Дальше приходится смотреть документы, описывающие специфические типы флагов[1]:
0×07000000: Access Type 0, Segment Type 7. Non Paged Segment.
0×02200000: Access Type 1, Segment Type 2. Paged segment. Type Hash Table Segmentsh Table
Как мы знаем, первый сегмент, который начинается с нулевого адреса и размером 0×430 (1072) байт — это наш заголовок 64 байта + 18 программных заголовков по 56 байт.
Самое интересное начинается с адреса 0×1000. Это второй программный сегмент, который используется для проверки неизменности кода — таблица хешей. Начиная с 6-й версии данные об устройстве, для которого программер предназначен (поля OU), перенесли в шапку раздела таблицы хешей. Получить их из сертификата стало невозможно, так что попробуем разобрать её элементы. Пользовательские данные должны лежать где-то между заголовком таблицы хешей и самой таблицей (согласно схеме).
Стандартная схема программера
Стартуем с конца. Убираем цепочку сертификатов.
Для удобства определения пользовательских полей в заголовке таблицы хешей начинаем двигаться от конца раздела таблицы к началу. Как нам известно, стартует раздел с адреса 0×1000 и имеет размер 0×1D08. Смотрим в хекс-редакторе адрес 0×2D08.
Завершение таблицы хешей в коде
Видно, что заполнение условного пустого места байтами 0хFF по этому адресу закончилось и дальше пошли нули. Следующий раздел с ненулевыми данными стартует с адреса 0×3000.
Условное пустое место, которое заполнено 0хFF, начинается с адреса 0×2124. Проверяем окончание цепочки сертификатов. Удобно воспользоваться инструментом в Linux binwalk.
Поиск по маске известных частей кода
Адрес начала третьего (корневого) сертификата 0×1D31 + шапка 4 байта + размер 1007 = 0×2124 — конец совпал с началом 0хFF. Проверяем второй и первый сертификаты…
● 0×1913 + 4 + 1050 = 0×1D31 — Ok!
● 0×1508 + 4 + 1031 = 0×1913 — Ok!
Таким образом, получаем адреса самой таблицы хешей: начало 0×1000, конец 0×1507 = 1287 байт. Хешей у нас должно быть по количеству разделов, т.е. 18 штук + 1 общий хеш на всю эту таблицу. Чтобы понять каким методом проводился расчёт хеша, необходимо посмотреть алгоритм цифровой подписи любого из сертификатов. Статья по порядку кодирования ASN1-DER есть на Хабре в свободном доступе, так что просто опишу последовательность байт для поиска на нашем примере: (06092A864886F70D01010A = 1.2.840.113549.1.1.10):
● 06 — OBJECT IDENTIFIER;
● 09 — длина блока;
● 2A — 0×2A = 42. Первые два числа 1.2. (формула х1×40+х2);
● 8648 — 840. Первые 0×80 в уме. 6×128 = 768–840=72(0×48);
● 86F70D — 113549. 80 в уме. 6×128*128=98304. 0хF7–0×80=0×77(119)*128=15232. 98304+15232+0×0D (13)=113549;
● 01 — 1
● 01 — 1
● 0A — 10
В сети можно легко найти кодировки алгоритма цифровой подписи и в зависимости от полученного значения использовать расчёт хеша. В нашем случае это SHA384, что соответствует 384 бит/8=48 байтам (96 знаков). К сожалению, для версии 6 я так и не смог найти документы, которые детально описывают алгоритм формирования заголовка таблицы хешей и пользовательские данные, поэтому будем пытаться их вычислить самостоятельно.
Проверяем хеш программных разделов
Зная, что в таблице хешей у нас должны лежать хеши 18 разделов в кодировке SHA384 (48 байт на один хеш) + один общий в конце, смотрим на любую подходящую последовательность байт в промежутке между 0×1000 и 0×1507. По программным заголовкам, где размер файла равен нулю, считаем, что хеш тоже будет равен нулю. Получаем такую вот последовательность, которую потом наложим на данные хекс-редактора. Сразу укажу в ней стартовый адрес и длину, чтоб удобно было вычислить хеш и потом сравнить с эталоном в таблице.
№ | Стартовый адрес | Длина | Описание (комментарий) | Хеш (SHA384 — 48 байт — 96 знаков) |
0 | 0×0 | 0×430 | Заголовок файла | 691D5E0F0BCA527007C57DDD6DCD7734E3DC198C26C9BD956ABC26E718F035FEE3C4FADA7EF2EC0D70EE59E7F16AFA3E |
1 | 0×1000 | 0×1D08 | Хеш таблица с заголовком (не считаем) | 0×0 (96) |
2 | 0×07BC90 | 0×0 | Пока не считаем | 0×0 (96) |
3 | 0×058660 | 0×001DCC | Тут посчитали | B2D14CBC4449E38862046FF82AAC0F1F4C62F5E11C94E07B8211AEA7A525E24F62E61E7E91480FAADB0BAA3F8BDFA71F |
4 | 0×05A430 | 0×0 | Пока не считаем | 0×0 (96) |
5 | 0×07BC90 | 0×0 | Опять не считаем | 0×0 (96) |
6 | 0×003000 | 0×04E46C | Тут посчитали | 173d1921f234b84ba3a4b886e974ddcc 0f1f967ef171ad373e63dd519f5aa191 45d708d0791f085cd1e636a68cbdfda9 (хеш от Тембласт, отличается от эталона) |
7 | 0×058660 | 0×0 | Уже не считаем | 0×0 (96) |
8 | 0×05A430 | 0×021858 | Теперь считаем | 2E3B26DAAFE4DF2BA05FEC5BAA07D1074D48FE9C867A939E9AA42336DBF02587FC88B4F47AAEDE99FE718A997E4905EA |
9 | 0×051470 | 0×0 | Пока не считаем | 0×0 (96) |
10 | 0×051470 | 0×0071F0 | Теперь считаем | 7285c2f867a3886e7941b9e8577ebb38 8a7f7b8ad00f31019c0d18d53783be72 aedee8efc0e06b84f1fe5d54e475bcb4 (хеш от Тембласт, отличается от эталона) |
11 | 0×07BC90 | 0×0000AC | Теперь считаем | 820DF4D64077328D99347FF924CB417FC228858A8AF4C9EE9B69D00E53F8B6C3CF50E59FBF0AB1F48ECEBC84CFA52BCC |
12 | 0×07BD3C | 0×00A890 | 32650B7F67130693C843B1C5278481EEDB99E158FE876E637B9400430BE17FFB9DD6C78BBE220ADE63E114183B62E452 | |
13 | 0×0865CC | 0×003404 | B22357C1F29ABAF04E07B985DC8D70C8AE4A404771CECF2A5113925AA50F1178CE52B3BA101617E47F6C5CDAD95EE7F5 | |
14 | 0×0899D0 | 0×003404 | BD6589D4C242106C4A9D0A2F24A07C4FE21AAD67EF8A0533FFC6839BBBC883D1496C4BDC55CD1211905E129179573064 | |
15 | 0×08CDD4 | 0×000010 | AE40659DA1193CDEC8DF474B5E36416A82473B83D32DBBE1DD6DF8EC9499D24902CA08C334876BC8E69E818BEECC046A | |
16 | 0×08CDE4 | 0×017000 | 417F5424745EC880A0E102939214FAF9B1D2A6BB0CB347B996BBE47533B1F633D7123050281C250A364D3F0BF66C8237 | |
17 | 0×058660 | 0×0 | И тут уже не считаем | 0×0 (96) |
0×10A8 | 0×35F | Общий | ??? |
Итак, сравнили таблицу с данными хекс-редактора и увидели, что первый хеш (запись 0) стартует с адреса 0×10A8, а последний хеш (запись17) заканчивается 0×1407. Для подписи самой таблицы остаётся места 0×1507–0×1407=0×100 (256 байт — 2048 бит). Как проверить этот общий хеш мне не понятно, так что пока оставим. Продолжим разбираться с шапкой.
В шапке остались заголовок и пользовательские данные
На заголовок таблицы хешей и пользовательские данные у нас осталось 0×10A7–0×1000=0xA8(168 байт). Вариантов разделения этой части кода несколько:
● Разделы могут быть фиксированного размера на основании протокола. Тогда данных о начальном адресе и/или длине записано не будет;
● Разделы могут быть «плавающего» размера. Тогда в коде должны будут присутствовать либо начальный адрес и размер, либо только размер, если разделы идут последовательно.
● Может быть комбо из первого и второго вариантов.
Имеет смысл проверить все ненулевые, подходящие данные этой части кода, значения которых лежат в диапазоне от 0×1000 до 0×10A7 (начальные адреса) и от 0×4 (0+4) до 0хA4 (0xA8–4) (размер сектора — минимум 4 байта).
Номер версии и размер пользовательских данных
Чтоб долго вас не мучить, отмечу сразу. Ищем 4 байта с адреса 0×102С-2F = 0×00000078 (120 байт) — вот наш клиент! С адреса 0×1030 до 0×10A8 как раз и есть 0×78 (120 байт). Теперь мы точно знаем, что заголовок таблицы хешей у нас фиксированный, 48 байт (0×30), с адреса 0×1000 до 0×102F. Пользовательские данные — область плавающего размера, который определяется 4 байтами по адресу 0×102С. В заголовке нас интересует ещё один параметр — по адресу 0×1004 одним байтом (может двумя) описывается номер версии программера = 0×06. Как я писал выше, для версии программера 5 и ниже пользовательские данные лежали в полях OU первого сертификата, для версии 7 данных пока нет.
Разбираем пользовательский раздел
Теперь напишу то, что удалось определить опытным путём, анализируя данные пользовательского раздела программеров от разных вендоров и для разных процессоров на примере выбранного программера:
● Идентификатор образа (SW_ID) — адрес 0×1038, 1 байт — 0×03 — firehose. По официальной документации — 4 байта;
● Идентификатор процессора (JTAG_ID) — адрес 0×103C, 4 байта — 0×00000000;
● Код вендора (OEM_ID) — адрес 0×1040, 2 байта — 0×0072 — Xiaomi. По официальной документации — 4 байта;
● Идентификатор модели (MODEL_ID) — адрес 0×1044, 2 байта — 0×0000;
● Версия образа (ANTI_ROLLBACK_VERSION) — адрес 0×10A4, 1 байт — 0×02
Вот такую информацию даёт официальная документация. С учётом данных из примера получается:
● SW_ID — 32 бита — 4 байта — 8 знаков.
● HW_ID (JTAG_ID) — 32 бита — 4 байта — 8 знаков.
● OEM_ID — 32 бита — 4 байта — 8 знаков.
● MODEL_ID — не указано. По расчёту, из-за флагов, остаётся 5 байт на это поле, что очень странно.
● FLAGS_ID — 5 байт — 10 знаков — 7 флагов (1–1–1–1–2–2–2). В зависимости от флагов процессор выбирает какие данные получать. В примере все значения флагов 0, кроме 02 и 01. К каким флагам относятся эти значения я так и не разобрался.
● SOCS_VERS — может содержать до 12 значений при установленном флаге 1. В примере указано только одно значение — 0×600С0000. Могу предположить, что используется либо только оно одно, либо свободное место до следующего поля заполнено нулями и при работе игнорируется.
● MULTI_SERIAL_NUMBERS — может содержать до 8 серийных номеров, если хотя бы один из трёх последних флагов имеет значение 2. В примере последний флаг, скорее всего, имеет значение 0×02 — debug. В коде размещена какая-то последовательность ненулевых данных. Вполне вероятно, список каких-нибудь серийных номеров. По размеру 8 групп по 4 байта (размер одной группы соответствует правилу записи серийного номера).
● MRC_INDEX — не указано. Считаем 4 байта.
● ANTI_ROLLBACK_VERSION — не указано. Считаем 4 байта. В примере 0×00000002.
Увидимся
На этом пользовательские поля заканчиваются, и начинается таблица хешей. Некоторые данные из заголовков я так и не смог идентифицировать, так что, если есть желание и возможности дополнить этот разбор, то пишите свои предложения в комментариях на Хабре, в дискуссиях или предложениях на странице проекта на GitHub. С исходным кодом программы подбора программера под устройство на процессоре от Qualcomm — «Firehose Finder» — можно ознакомиться на GitHub (https://github.com/hoplik/Firehose-Finder). Там же лежат и релизы для установки на Windows х64.
P.S.
Пока рецензировал проект статьи, получил несколько вопросов.
1. Вопрос: Для процессоров Snapdragon 8+ Gen1 используются программеры версии 7. Будет ли их поддержка в FhF?
Ответ: С уверенностью ответить не могу. По мере возможности я пытаюсь разобраться в алгоритмах новой версии, но дело осложняется тем, что кроме изменения алгоритмов программера новой версии, вендоры стали применять и новую версию протокола Sahara (v. 3). Старый подход — получение данных процессора командами Sahara и сравнение их с парсингом программера, для новых процессоров не может быть применён. Некоторые необходимые команды протокола Sahara новый процессор отклоняет как неизвестные.
2. Вопрос: Для программера из приведённого примера расчётные данные хешей проектом от Temblast (https://www.temblast.com/qcomview.htm) не совпадают с данными, записанными в таблицу хешей для 6 и 10 записи. Это говорит о том, что код программера был изменён после подписания таблицы цепочкой сертификатов?
Ответ: Скорее всего, нет. Полностью нельзя исключать такую возможность, но для данного примера верным будет изменённый алгоритм проверки хешей. В официальной документации есть упоминание о том, что кроме единичной подписи (описано выше) может присутствовать двойная подпись. И это, как раз, тот самый случай. При разборе файла командой binwalk было видно, что у файла два заголовка ELF и две цепочки сертификатов. По адресу начала второго заголовка видно, что он совпадает с адресом начала последней (16-й) записи. Таким образом, мы имеем как бы «виртуальную матрёшку». Один эльф внутри сектора другого эльфа. Второй эльф разбирается по похожим алгоритмам, но готового решения у меня пока нет. Поэтому просто приведу перевод части официальной документации.[2] Кстати, именно во второй части пользовательских данных лежит идентификатор процессора, для которого этот программер и предназначен, то есть JTAG_ID 0×00000000 надо изменить на 0×00E060E1.
Схема двойной подписи
«Производители оборудования и QTI могут дважды подписывать образ. Образы с подписью QTI имеют другую таблицу метаданных с SW_ID и хэшем. Некоторые поля маскируются при проверке подлинности хэша сегмента во время проверки QTI. Если хэш в хэш-таблице не совпадает с хэш-сегментом, проверьте, подписано ли изображение, и проверьте с помощью устаревших методов.
Шаги проверки подлинности для изображения с двойной подписью следующие:
1. Аутентифицируйте таблицу метаданных 1 с помощью OEM-ключа.
2. Аутентифицируйте таблицу метаданных 2 с помощью ключа QTI.
3. Проверьте соответствие хэша конкретного сегмента хэша и таблицы метаданных 1 и 2.
4. Проверьте соответствие хэша конкретного сегмента ELF и хэш в сегменте хэша.»
[1] 80-NL239–45 Secure Boot Enablement User Guide. November 11, 2019
[2] 80-PG596–42 Secure Boot Enablement User Guide. July 29, 2019.