Sploitus

Exploit for CVE-2026-43499-pmg110-root

kitploit · 2026-09-06

Exploit Code

MARKDOWN136 lines
## https://sploitus.com/exploit?id=KITPLOIT:TOOLS-GITHUB-SORALIS0912-CVE-2026-43499-PMG110-ROOT
# pmg110-root

Локальное повышение привилегий через CVE-2026-43499 (use-after-free в futex PI `rt_mutex_waiter`), портированное на **OPPO PMG110 / K15 Pro+** — MediaTek MT6991, ColorOS 16.

Загружается один файл, запуск через `LD_PRELOAD`:

root@kitploit:~
    
    
    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`:

root@kitploit:~
    
    
    adb shell /data/local/tmp/su -c id      # uid=0(root)
    

**Проверено на устройстве** (2026-07-27): uid=0 примерно за 35 секунд при «чистом» запуске без переопределений окружения, и `su` затем отвечает из обычного непривилегированного `adb shell`:

root@kitploit:~
    
    
    $ 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`.

## Что делает и чего не делает

Он выполняет Write 1 (SELinux permissive) и Write 2 (`cred` → `init_cred`), переводит дочерний процесс в uid=0 и оттуда устанавливает встроенный демон `su`.

  * **по-прежнему загружается один файл.** `su` — не второй артефакт: `su_daemon.c` собирается как отдельный aarch64 PIE и через `.incbin` встраивается в `.rodata` библиотеки, поэтому он находится внутри `preload.so` и записывается обратно во время выполнения. Путь warhol-root, без изменений.
  * нет root-скрипта, нет `ksud`, нет KernelSU
  * вызывающий процесс остаётся непривилегированным — он получает root, _обращаясь к демону_ , и то же самое вы будете делать из оболочки потом
  * SELinux **остаётся в режиме permissive** , как это делает warhol-root: демон должен обслуживать непривилегированных клиентов через свой сокет. Перезагрузка вернёт enforcing.



`su` устанавливается в трёх местах, потому что одно из них — то, до которого вы действительно доберётесь:

Демон слушает `/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

Всё, кроме ядра эксплойта, принадлежит warhol-root — взято, а не изобретено заново:

  * **структура** — заголовки для каждого устройства в `targets/<device>/`, размещаемые в `source/src/` во время сборки, так что переключение `DEVICE` никогда не оставит позади заголовки предыдущего устройства
  * **сборка** — выбор тулчейна в `source/Makefile` (NDK, если он есть, иначе хост-`clang` против sysroot NDK) и двухэтапное правило встраивания, которое создаёт `build/embed/su_daemon_aarch64_pie` перед линковкой `.so`
  * **путь su** — `su_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` побайтно идентичны между двумя репозиториями) и получившее дальнейшее развитие.

Каждая строка собственно эксплойта — Write 1, Write 2, KernelSnitch, маршрут pselect — это один и тот же код в обоих деревьях.

### Откуда вызывается установка su

Это единственное структурное различие, и оно обусловлено тем, что два дерева получают 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` и ничего не устанавливает.

## Сборка

root@kitploit:~
    
    
    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` выводит, что было выбрано.

Сборка двухэтапная — вот что стоит знать:

  1. `su_daemon.c` → `build/embed/su_daemon_aarch64_pie` — отдельный aarch64 PIE
  2. `su_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` вручную — без этих строк границы блоба были бы единственным, что библиотека по-прежнему экспортировала.

## Переменные окружения

При проверенном запуске не понадобилась ни одна из них.

## Чтение лога

root@kitploit:~
    
    
    [*] futex_hashsize 2048 (8 possible CPUs)
    [*] ks collisions=3/3 baseline=8 threshold=10x (80) accepted=[1244..1597] slowest_rejected=N
    [+] child uid = 0
    [+] embedded su wrote 15304 bytes to /apex/com.android.virt/bin/su
    [+] embedded su daemon ready pid=NNNN socket=/data/local/tmp/temp_su.sock daemon=/apex/com.android.virt/bin/su
    [+] embedded su install ok=1 errno=0 daemon=NNNN
    [+] su ready: /data/local/tmp/su and /apex/com.android.virt/bin/su
    [+] ghostlock preload verdict: EXPLOIT OK
    

`child uid = 0` — это успех эксплойта; всё, что после него, — установка. Эти два события намеренно сообщаются раздельно, как и вердикт:

Средний вариант — то различие, которое стоит иметь в виду: он говорит, что смещения в `target.h` верны для этой сборки, а проблема где-то в установке, а это совершенно другая вещь для отладки. `su=0/38` (`ENOSYS`) внутри него конкретно означает, что слинковалась именно слабая заглушка.

**`mm_struct leak failed` с последующим `prepare_kernel_page retry N/24` — не сбой.** Это прогресс цикла, и успешный запуск тоже показывает его. Ничего не является сбоем, пока не исчерпаны 24 попытки и не появится `prepare_kernel_page timeout`. Аналогично, `probing cfi ... expected=9` с `child uid = 2000` — это один недобранный раунд из десяти.

Не судите о запуске по усечённому логу — эта ошибка стоила здесь целого раунда неверной диагностики.

**Полностью неудачный запуск — тоже норма.** Гонка `pselect` не срабатывает в 100 % случаев: запуск может проиграть её пять раз подряд и закончиться `Write 1 failed`, а следующий запуск выигрывает её с первой попытки с `ret=9`. Наблюдалось на этом устройстве. `ret=4 expected=9` — так выглядит проигрыш гонки, а не неправильный `target.h` — один сбой не повод заново выводить смещения. Перезапустите.

## Файлы

## Лицензия

Только для авторизованных исследований в области безопасности и образовательных целей.