## 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 пропатчиваются в памяти, поэтому это нужно повторять после каждой перезагрузки. Разблокировка загрузчика не требуется.
* * *
## Отказ от ответственности
Для исследований на оборудовании, которым вы владеете. Запуск может привести к циклу загрузки или «кирпичу» и аннулирует гарантию. Никаких гарантий любого рода.
Апстрим-проекты лицензированы их авторами; этот порт несёт их условия.