
Локальный эксплойт повышения привилегий, нацеленный на CVE-2026-43499, для устройства Xiaomi 17T Pro (warhol) под управлением Android 16 с SoC MediaTek MT6993.
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.
| Элемент | Значение | Источник |
|---|---|---|
| Кодовое имя | warhol | pre-device=warhol |
| Сборка | Xiaomi/warhol_global/warhol:16/BP2A.250605.031.A3/OS3.0.304.0.WPSJPXM:user/release-keys | post-build |
| Инкрементальная | OS3.0.304.0.WPSJPXM (глобальная JP) | post-build-incremental |
| Android | 16, SDK 36 | post-sdk-level=36 |
| Патч безопасности | 2026-05-01 | post-security-patch-level |
| Тип OTA | A/B (payload.bin, CrAU, 39 разделов) | ota-type=AB |
| SoC | MediaTek MT6993 (Dimensity 9500) | совместимые mediatek,mt6993-* в DTB из vendor_boot; cmdline bootopt=64S3,32N2,64N2 |
| Ядро | 6.12.38-android16-5-g1d46253471dd-ab15048002-4k, clang 19.0.1 | баннер, считанный из извлечённого boot.img |
| boot-образ | заголовок v4, ядро 18 898 125 Б, сжатие LZ4-legacy, без ramdisk | tools/bootinfo.py |
Здесь ядро важнее 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
отказывается загружать ядро, размещённое где-либо ещё.boot.img, сжатое в LZ4-legacy, а не в виде голого arm64
Image, поэтому генератор распаковывает его перед анализом.В WARHOL_PORT.md §2 приведён полный вывод.
MediaTek lk не загружает ядро по константе времени компиляции — он ищет резерв DRAM с именем
mb_kernel и проверяет, что ядро приземлилось точно на него. Этот резерв создаёт прелоадер,
именно поэтому сборка прелоадера, установленная на телефоне, здесь вообще является входным
параметром сборки: это единственное, что вне boot.img может сдвинуть
P0_KERNEL_PHYS_LOAD, а неверное значение означает неверный псевдоним линейной карты и
мёртвый телефон, а не неудачный эксплойт.
Отсюда два варианта, хранящиеся в отдельных целевых каталогах:
PRELOADER= | Прелоадер на телефоне | Целевой каталог | Статус |
|---|---|---|---|
retail (по умолчанию) | штатный preloader_<device>.bin, побайтно идентичный preloader_raw.img из fastboot-прошивки | targets/warhol-OS3.0.304.0.WPSJPXM/ | ✅ проверено на устройстве 2026-07-26 |
eng | инженерная сборка, preloader_<device>_eng.bin | targets/warhol-OS3.0.304.0.WPSJPXM-eng/ | ✅ проверено на устройстве 2026-07-26 |
make preload # PRELOADER=retail -> out/preload-<device>.so
make PRELOADER=eng preload # -> out/preload-<device>-eng.so
make both # оба примера выше, а также вывод их sha256
Оба артефакта хранятся рядом под разными именами. На этой прошивке они получаются побайтно идентичными — это результат приведённого ниже анализа, а не обходной путь, поэтому обе сборки всё равно выполняются и именуются раздельно.
Прочитайте таблицы раскладки памяти обоих прелоадеров самостоятельно:
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.
Заметки с запуска на устройстве: