Sploitus

Exploit for CVE-2026-43499-warhol-root

kitploit · 2026-09-06

Exploit Code

MARKDOWN177 lines
## https://sploitus.com/exploit?id=KITPLOIT:TOOLS-GITHUB-SORALIS0912-CVE-2026-43499-WARHOL-ROOT
# warhol-root

CVE-2026-43499 (IonStack) локальное повышение привилегий, портированный на **Xiaomi 17T Pro (`warhol`)** — MediaTek MT6993, Android 16.

> **Только GKI 6.12 / android16.** Оверлей ожидания (waiter overlay), раскладка стека `pselect` и все смещения структур привязаны к этой ветке, поэтому ничего из этого не переносится на 6.6, 6.1 или 5.10 — для них нужна база, собранная отдельно. `generate_target.py` отказывается от любого другого баннера, а не генерирует заголовок, который вызвал бы отказ на устройстве.

> **Статус: работает.** Проверено на устройстве 2026-07-26 с чистой загрузки, в **обоих** вариантах прелоадера — `uid=0(root) context=u:r:kernel:s0`, SELinux permissive, `su` установлен. Root не постоянный: повторно выполняйте строку `LD_PRELOAD` после каждой загрузки. Гонка pselect не даёт 100% результата: неудачный запуск может вызвать панику телефона, а повторная попытка после перезагрузки — это нормально. Полный журнал см. в `WARHOL_PORT.md`.

> **Этому устройству требуется исправление kernel-MTE.** Стоковый апстрим-попсикл не может получить на нём root — он вечно крутится в `kernel page retry N/12`. См. Kernel MTE.

Сборки выпускаются в двух вариантах прелоадера: `PRELOADER=retail` (по умолчанию) и `PRELOADER=eng`. Они выбирают разные целевые каталоги, чтобы ни один не мог перезаписать адреса другого — см. Вариант прелоадера.

* * *

## Целевое устройство

Приведённые ниже факты считаны из розничного полного OTA-пакета — метаданных, манифеста A/B-нагрузки и образов `boot`/`vendor_boot`/`dtbo`, извлечённых из `payload.bin`.

Здесь ядро важнее SoC: `warhol` находится в **той же ветке GKI, что и апстрим-попсикл** (`android16-5`, страницы 4K), на расстоянии в один patchlevel — `6.12.38` против проверенного `6.12.23` у попсикла. Смещения структур всё равно должны перегенерироваться для каждой сборки; переносится только форма эксплойта. Для другой сборки `OS3.0.x` нужен свой `target.h`.

* * *

## Почему эта база

Цепочка эксплойта жёстко привязана к **ветке GKI** , а не к SoC. Раскладка ожидания rt_mutex, оверлей стека `pselect` и смещения структуры `pipe_buffer` отслеживают ядро, поэтому база 6.12/android16 лучше, чем база того же вендора на более старой ветке.

`x-spy/CVE-2026-43499-popsicle` — единственная публичная реализация, проверенная на линии GKI 6.12/android16 (Xiaomi 17 / Pro / Ultra, ядро `6.12.23-android16-5`), поэтому `source/` взят из неё и максимально приближен к апстриму — **за исключением двух строк** (см. Kernel MTE, который этому устройству нужен вообще для работы).

Бо́льшая часть отличий MediaTek сосредоточена в **генерации целевых данных на этапе сборки** , в двух частях:

  * апстрим читает `p0_phys_offset` и `p0_kernel_phys_load` из раздела Qualcomm `xbl_config`, которого у `warhol` нет. Вместо этого `generate_target.py --dtb` читает базовый адрес DRAM из узла `/memory` FDT из `vendor_boot`, округляя его вниз так же, как это делает `arm64_memblock_init`. Физический адрес загрузки ядра считывается из резерва `mb_kernel` прелоадера через `--preloader`; для этого устройства дельта получается **0** , и lk отказывается загружать ядро, размещённое где-либо ещё.
  * MediaTek поставляет ядро в `boot.img`, сжатое в LZ4-legacy, а не в виде голого arm64 `Image`, поэтому генератор распаковывает его перед анализом.



В `WARHOL_PORT.md` §2 приведён полный вывод.

* * *

## Вариант прелоадера

MediaTek lk не загружает ядро по константе времени компиляции — он ищет резерв DRAM с именем `mb_kernel` и проверяет, что ядро приземлилось точно на него. Этот резерв создаёт **прелоадер** , именно поэтому сборка прелоадера, установленная на телефоне, здесь вообще является входным параметром сборки: это единственное, что вне `boot.img` может сдвинуть `P0_KERNEL_PHYS_LOAD`, а неверное значение означает неверный псевдоним линейной карты и мёртвый телефон, а не неудачный эксплойт.

Отсюда два варианта, хранящиеся в отдельных целевых каталогах:

root@kitploit:~
    
    
    make preload                  # PRELOADER=retail -> out/preload-<device>.so
    make PRELOADER=eng preload    #                  -> out/preload-<device>-eng.so
    make both                     # оба примера выше, а также вывод их sha256
    

Оба артефакта хранятся рядом под разными именами. На этой прошивке они получаются побайтно идентичными — это _результат_ приведённого ниже анализа, а не обходной путь, поэтому обе сборки всё равно выполняются и именуются раздельно.

Прочитайте таблицы раскладки памяти обоих прелоадеров самостоятельно:

root@kitploit:~
    
    
    python3 tools/preloader_memlayout.py <preloader.bin> --diff <preloader_eng.bin>
    

**Для этой прошивки обе таблицы идентичны** , `mb_kernel.start = 0x80000000` в обеих, поэтому eng-вариант `target.h` получается побайтно идентичным retail-варианту, и обе сборки дают один и тот же `preload.so`.

До эксплойта доходит разность `P0_KERNEL_PHYS_LOAD − P0_PHYS_OFFSET`, и два её слагаемых опираются на неодинаково надёжные данные:

  * `P0_KERNEL_PHYS_LOAD` — **измерено** по eng-образу, как указанный выше `mb_kernel.start`.
  * `P0_PHYS_OFFSET` — **обосновано, а не измерено** , поскольку это `memstart_addr`, который ядро берёт из узла `/memory`, который lk формирует во время выполнения из данных, сообщаемых прелоадером после инициализации DRAM, а не из статичного FDT, который читает генератор. Аргументация была такой: обе сборки разделяют весь путь DRAM — те же исходники `dramc`/`emi`/`mblock`/`memory_layout`, и среди eng-only строк нет кода раскладки памяти — а единственный дополнительный резерв eng-прелоадера, динамический `security_fe_rsv` объёмом 3 МиБ с `mapping=1`, не может ни сдвинуть запись с фиксированным адресом, ни вырезать дыру внизу DRAM. **Запуск на устройстве решил вопрос** : псевдонимы линейной карты приземлились под eng, значит, слагаемое верно.



Полный вывод в `targets/warhol-OS3.0.304.0.WPSJPXM-eng/NOTES.md`.

Заметки с запуска на устройстве:

  * **BootROM принимает этот eng-прелоадер** — он нормально загрузился в Android, с `ro.boot.verifiedbootstate=green` и неизменным `flash.locked=1`. Это оставалось открытым вопросом, пока не было проверено на практике; решает его efuse-хэш корневого ключа, а эта сборка подписана теми же ключами, что и retail.
  * Eng-прелоадер сохраняет **живым режим META / заводской загрузки** : `%s META DIS` — строка только retail, тогда как eng несёт целую реализацию META, которой у retail нет (`Enable fastmeta.`, `FAST META GPIO: %d`, `META_COM PORT: %d`, `read_meta_proinfo`, …). Здесь он загрузился сразу в Android, но если телефон с eng-прошивкой когда-нибудь окажется в другом состоянии, вероятная причина именно в этом, и ни одно значение `target.h` здесь ни при чём.
  * Если более поздняя прошивка сдвинет `mb_kernel`, две цели разойдутся — хранение их в отдельных каталогах как раз не даёт этому произойти молча.



* * *

## Kernel MTE

Это ядро работает с **KASAN_HW_TAGS** — в `/proc/cmdline` есть `kasan.stack_ring_size=524288` — поэтому указатели slab несут тег выделения в битах 59:56. Апстрим-попсикл предполагает указатели без тегов и потому **вообще не может получить root на этом устройстве** : он бесконечно крутится в `[-] kernel page retry N/12 mode=1`, и в логе нет ни слова о причине.

Две строки в `source/` исправляют это, и обе находятся в `warhol-mte-fix.patch`:

Снимают тег только _проверки_. Сами указатели остаются тегированными, потому что тег корректен для памяти, на которую они указывают, — а именно это и нужно при разыменовании их ядром.

Не полагайтесь на `arm64.memtag.bootctl`: это свойство управляет MTE в **userspace** и ничего не говорит о том, тегирует ли ядро собственные выделения.

* * *

## Апстрим / ссылки

Этот репозиторий — порт. Авторство цепочки эксплойта принадлежит апстриму.

* * *

## Структура

root@kitploit:~
    
    
    source/          ядро эксплойта (апстрим-попсикл + исправление kernel-MTE; см. warhol-mte-fix.patch)
      src/           main.c slide.c fops.c pipe.c util.c preload.c su_daemon.c + kernelsnitch/
    targets/         сгенерированный target.h для каждой сборки, по одному каталогу на fingerprint x вариант прелоадера
      warhol-OS3.0.304.0.WPSJPXM/       PRELOADER=retail
      warhol-OS3.0.304.0.WPSJPXM-eng/   PRELOADER=eng
    tools/
      payload_dump.py          извлечение разделов из A/B payload.bin (проверяет размер и sha256)
      kernel_banner.py         чтение баннера Linux из boot.img
      bootinfo.py              вывод полей заголовков boot / vendor_boot
      generate_target.py       генератор target.h; апстримный, только для MediaTek, плюс распаковка ядра
      preloader_memlayout.py   вывод/сравнение таблицы статических резервов DRAM прелоадера MediaTek
      lz4legacy.py             декомпрессор кадров LZ4 legacy (образы ядра)
    out/             артефакты сборки (в gitignore)
    

* * *

## Сборка

Ничего не собирается, пока не существует `target.h` — Makefile громко падает, а не выдаёт бинарник под неверное ядро.

root@kitploit:~
    
    
    # 1. извлеките boot.img прямо из OTA-зипа (payload.bin хранится без сжатия,
    #    поэтому --base ищет внутри зипа; распаковывать 7.6 ГБ заранее не нужно)
    python3 tools/payload_dump.py <ota.zip> --base 5081 -p boot,vendor_boot,dtbo -o <dir>
    
    # 2. подтвердите баннер ядра — на него завязано каждое смещение ниже
    python3 tools/kernel_banner.py <dir>/boot.img
    python3 tools/bootinfo.py <dir>/boot.img
    
    # 3. сгенерируйте заголовок цели. --preloader читает физический адрес загрузки ядра
    #    из резерва mb_kernel прелоадера; передавайте прелоадер, который запущен на телефоне.
    python3 tools/generate_target.py \
      --boot <dir>/boot.img --dtb <dir>/vendor_boot.img --preloader <preloader.bin> \
      -o targets/warhol-OS3.0.304.0.WPSJPXM/target.h
    
    # 4. сборка
    make preload            # DEVICE=warhol-OS3.0.304.0.WPSJPXM, PRELOADER=retail по умолчанию
    

Если образа прелоадера под рукой нет, `--kernel-phys-delta 0` — более старая форма и для этой прошивки даёт тот же заголовок, — но это утверждение, а не считывание, поэтому предпочтителен `--preloader`. Передача обоих заставляет генератор выполнить перекрёстную проверку и упасть при расхождении.

`--base 5081` — это смещение локального заголовка `payload.bin` для этого конкретного OTA; для любого другого пакета возьмите его из `ota-property-files` в `META-INF/com/android/metadata`.

Требуются Android NDK (`NDK_ROOT` / `ANDROID_NDK_HOME`) и `llvm-objdump`.

## Запуск

root@kitploit:~
    
    
    # <device> — имя цели: <fingerprint> для PRELOADER=retail, <fingerprint>-eng для eng
    adb push out/preload-<device>.so /data/local/tmp/preload.so
    adb shell chmod 0644 /data/local/tmp/preload.so
    adb shell "LD_PRELOAD=/data/local/tmp/preload.so /system/bin/true"
    adb shell "/data/local/tmp/su -c id"
    

`cred` и состояние SELinux пропатчиваются в памяти, поэтому это нужно повторять после каждой перезагрузки. Разблокировка загрузчика не требуется.

* * *

## Отказ от ответственности

Для исследований на оборудовании, которым вы владеете. Запуск может привести к циклу загрузки или «кирпичу» и аннулирует гарантию. Никаких гарантий любого рода.

Апстрим-проекты лицензированы их авторами; этот порт несёт их условия.