
Эксплойт Android LPE для CVE-2026-43499, нацеленный на OPPO PMG110 (ядро 6.6). Использует futex PI UAF для получения root-прав и установки su-демона через LD_PRELOAD.
Локальное повышение привилегий через CVE-2026-43499 (use-after-free в futex PI rt_mutex_waiter), портированное на OPPO PMG110 / K15 Pro+ — MediaTek MT6991, ColorOS 16.
Загружается один файл, запуск через LD_PRELOAD:
adb push out/preload-pmg110-16.0.9.400.so /data/local/tmp/preload.so
adb shell chmod 644 /data/local/tmp/preload.so
adb shell LD_PRELOAD=/data/local/tmp/preload.so /system/bin/true
В случае успеха на устройстве остаётся постоянный su:
adb shell /data/local/tmp/su -c id # uid=0(root)
Проверено на устройстве (2026-07-27): uid=0 примерно за 35 секунд при «чистом» запуске без переопределений окружения, и su затем отвечает из обычного непривилегированного adb shell:
$ adb shell "/data/local/tmp/su -c 'echo 0 > /proc/sys/kernel/kptr_restrict'"
$ adb shell "/data/local/tmp/su -c 'grep -w init_task /proc/kallsyms'"
ffffffe89033e780 D init_task
Эксплойт и установка su проверены на этом устройстве. То, что затем считала рутированная оболочка, также подтверждает P0_KERNEL_PHYS_LOAD, смещения символов и KS_MTE_TAGGED=0 независимо от эксплойта — см. targets/pmg110-16.0.9.400/NOTES.md.
| Устройство | OPPO PMG110 / K15 Pro+ / OP61E5L1 |
| SoC | MediaTek MT6991 (Dimensity 9500s) |
| Ядро | 6.6.118-android15-8-g93e223c276e7-abogki500782043-4k (GKI, страницы 4K) |
| Прошивка | ColorOS 16 / PMG110_16.0.9.400(CN01) — те же байты ядра, что и в 16.0.8.300 |
| Ошибка | CVE-2026-43499, не исправлена в этом образе (показано дизассемблированием, а не версией) |
Он выполняет Write 1 (SELinux permissive) и Write 2 (cred → init_cred), переводит дочерний процесс в uid=0 и оттуда устанавливает встроенный демон su.
su — не второй артефакт: su_daemon.c собирается как отдельный aarch64 PIE и через .incbin встраивается в .rodata библиотеки, поэтому он находится внутри preload.so и записывается обратно во время выполнения. Путь warhol-root, без изменений.ksud, нет KernelSUsu устанавливается в трёх местах, потому что одно из них — то, до которого вы действительно доберётесь:
| Путь | Зачем |
|---|---|
/apex/com.android.virt/bin/su | на tmpfs, смонтированной поверх этого каталога; в PATH для root-оболочки |
/data/local/tmp/su | доступен из обычного adb shell без игр с PATH |
/apex/com.android.virt/bin/su в mount namespace adbd | устанавливается через setns, поэтому новый adb shell его видит |
Демон слушает /data/local/tmp/temp_su.sock и пишет лог в /data/local/tmp/su_daemon.log. Root не сохраняется после перезагрузки — повторно запускайте команду с LD_PRELOAD после каждой загрузки.
Для установки KernelSU используйте вместо этого маршрут /data/local/tmp/a/e в ghostlock-oneplus.
Всё, кроме ядра эксплойта, принадлежит warhol-root — взято, а не изобретено заново:
targets/<device>/, размещаемые в source/src/ во время сборки, так что переключение DEVICE никогда не оставит позади заголовки предыдущего устройстваsource/Makefile (NDK, если он есть, иначе хост-clang против sysroot NDK) и двухэтапное правило встраивания, которое создаёт build/embed/su_daemon_aarch64_pie перед линковкой .sosu_daemon.c и su_blob.S побайтно идентичны warhol-root, а su_install.c — это его установщик из preload.cЯдро эксплойта — не warhol-root. warhol-root — это popsicle, привязанный к GKI 6.12 / android16, и его generate_target.py отвергает любой другой баннер. У PMG110 — 6.6 / android15, поэтому ядро здесь — это дерево ghostlock 6.6, само являющееся потомком того же кода (kernelsnitch/utils.h и timeutils.h побайтно идентичны между двумя репозиториями) и получившее дальнейшее развитие.
| Файл | Происхождение |
|---|---|
util.c slide.c fops.c pipe.c root.c miniadb.c common.h offset.h kernelsnitch/* | ghostlock, побайтно идентично |
su_daemon.c su_blob.S | warhol-root, побайтно идентично (su_blob.S добавляет две строки .hidden — см. «Сборка») |
su_install.c | установщик warhol-root из preload.c, вынесен в отдельный файл, потому что preload.c в этом дереве уже выполняет другую работу |
main.c | ghostlock, плюс вызов su в рутированном дочернем процессе и отчёт о результате |
preload.c | только здесь — конструктор и лог с двумя приёмниками |
offsets.h | только определение структуры; запись размещается из targets/<device>/device_offsets.h |
Каждая строка собственно эксплойта — Write 1, Write 2, KernelSnitch, маршрут pselect — это один и тот же код в обоих деревьях.
Это единственное структурное различие, и оно обусловлено тем, что два дерева получают root по-разному.
warhol-root рутит сам процесс эксплойта и потому вызывает install_embedded_su() напрямую из run_direct_root(). Здесь Write 2 подменяет указатель cred у форкнутого дочернего процесса, а родитель остаётся непривилегированным вызывающим, поэтому дочерний процесс в child_main() — единственный контекст, который может выполнить установку, — именно там он и запускается.
Оба дерева несут одну и ту же слабую заглушку install_embedded_su() в util.c, возвращающую ENOSYS; именно сильное определение включает этот маршрут. Это стоит знать, потому что сборка, в которой по какой-то причине теряется su_install.c, по-прежнему линкуется и запускается — просто сообщает su=0/38 и ничего не устанавливает.
make # = make preload -> out/preload-<DEVICE>.so
make DEVICE=<name> # use targets/<name>/
make devices # list available DEVICE values
make info # show the selected target and the resolved toolchain
Тулчейн находится автоматически: сначала ANDROID_NDK_HOME / ANDROID_NDK_ROOT, затем обычные места установки NDK для Linux и macOS, а если ничего из этого не нашлось — хост-clang, нацеленный на sysroot NDK. Устанавливайте ANDROID_NDK_HOME только для переопределения поиска. make info выводит, что было выбрано.
Сборка двухэтапная — вот что стоит знать:
su_daemon.c → build/embed/su_daemon_aarch64_pie — отдельный aarch64 PIEsu_blob.S через .incbin встраивает этот бинарник в .rodata, и всё это линкуется в единый preload.soПоэтому make clean и пересборка — единственный способ изменить встроенный su: достаточно отредактировать один su_daemon.c, зависимость объявлена, но блоб — артефакт сборки и не отслеживается.
targets/<device>/{target.h,device_offsets.h} заново размещаются в source/src/ при каждой сборке, так что устаревший заголовок от другого устройства не может быть молча подхвачен.
out/*.so не отслеживается (та же договорённость, что и в warhol-root) — клонируйте и запускайте make.
.so собирается с флагом -fvisibility=hidden и экспортирует ноль символов. Библиотека LD_PRELOAD выигрывает поиск символов для всего процесса, поэтому всё, что она экспортирует, может затенять одноимённый символ в хост-бинарнике или в libc. Этот флаг управляет только генерацией кода C, поэтому su_blob.S помечает свои два символа .hidden вручную — без этих строк границы блоба были бы единственным, что библиотека по-прежнему экспортировала.