
Исследование CVE-2026-43499 (GhostLock) на HUAWEI MatePad Pro 11 GOT-W29
Исследование повышения привилегий для CVE-2026-43499 (rtmutex/futex-PI UAF, «GhostLock») на GOT-W29 (HarmonyOS 4.0, ядро 4.19.157-perf+).
Основные выводы:
sysctl_bootid, выведен KASLR slide).| Параметр | Значение |
|---|---|
| Модель | HUAWEI MatePad Pro 11 GOT-W29 |
| SoC | Qualcomm kona (SM8250, Snapdragon 870) |
| Система | HarmonyOS 4.0 (104.0.0.136) |
| Ядро | 4.19.157-perf+ |
| VA | 39-бит, страницы 4K, KASLR включён |
remove_waiter() в kernel/locking/rtmutex.c в пути отката rt_mutex_start_proxy_lock() использует current вместо waiter->task для очистки, что приводит к висячему pi_blocked_on (UAF в стеке). Затрагивает 2.6.39 ~ 7.1 (данное ядро входит в диапазон). Исправление в апстриме — commit 3bfdc63936dd.
На данном устройстве подтверждено: исходный код rtmutex.c:1110-1112, декомпиляция boot.elf, срабатывание на реальном устройстве — всё проверено.
Создание PI-цикла, чтобы FUTEX_CMP_REQUEUE_PI вернул -EDEADLK; откат запускает баг remove_waiter, оставляя висячий pi_blocked_on (указывающий на rt_waiter в стеке ядра потока-waiter).
Старый триггер заставлял waiter самому удерживать целевой futex для requeue (self-own), что как раз попадало в раннюю проверку owner==task в task_blocks_on_rt_mutex данного ядра (boot.elf 0x3808-0x3868), возвращаясь до записи pi_blocked_on → висячий указатель никогда не создавался → размещение overlay было ошибочным диагнозом (без краха + boot_id не менялся).
PI-цикл: owner FUTEX_LOCK_PI(target) удерживает цель requeue; waiter удерживает chain futex; owner затем блокируется на chain (цикл: waiter→target→owner→chain→waiter). При requeue обход цепочки обнаруживает rt_mutex_owner(chain)==top_task → -EDEADLK → откат очищает pi_blocked_on не того потока → pi_blocked_on у waiter становится висячим. Owner должен понизить приоритет (nice=10), чтобы после boost его prio отличался от prio у owner_waiter->prio, иначе rt_mutex_waiter_equal выйдет раньше времени.
В shell (uid 2000) perf_event_paranoid=-1, perf_event_open(PERF_SAMPLE_IP, exclude_user=1) сэмплирует кластеры адресов текста ядра, выравнивая по известным смещениям символов для получения slide.
samples=27651 kernel_ips=1685 lo=0xffffff948728176c hi=0xffffff9488ebfc7c
KASLR slide=0x147f200000 (40/40 IP отображены в область текста ядра, проверено)
runtime _stext=0xffffff9487280800
Инструмент: tools/perf_kaslr.c. Предусловие запуска: shell (Shizuku rish), без перехвата seccomp.
[M] CMP_REQUEUE_PI ret=-1 errno=35 (EDEADLK!)
[W] WAIT_REQUEUE_PI ret=-1 errno=110 (ETIMEDOUT) ← waiter вернулся
[M] waiter_returned=1 ← остался висячий pi_blocked_on
Инструмент: tools/edeadlk_probe.c (вариант 8+2+1 = 11 или 27).
rt_mutex_adjust_prio_chain step[7] выполняет rb_erase для fake waiter (путь с одним левым потомком): *(tree_left) = tree_pc + инкрементальная запись __rb_change_child. Все смещения в target.h получены дизассемблированием boot.elf.
См. exploit/ghostlock-source/src/target.h. Ключевые моменты:
Самописный KPM (tools/kpm-debug/rtmutex-dbg.c, KernelPatch 0.13.5 inline-hook) при fake walk пересоздаёт overlay (tree/task/lock) и переписывает параметр next_lock на empty_zero_page (KernelPatch _transit8 вызывает исходную функцию с изменёнными fargs), чтобы rt_mutex_adjust_prio_chain [3] next_lock==waiter->lock прошёл, [5] trylock нулевой блокировки успешен, [6] ownerless, [7] rt_mutex_dequeue (rb_erase с одним левым потомком) выполнился —
sysctl_bootid перезаписан на &loggers[0][1], slide-kaslr-ok.
REPAIR3 waiter=0xffffff801ecdbc00 lock=0xffffffa7c4950000
slide boot_id_leaked_nfulnl_logger value=ffffffa7c4612320
slide-kaslr-ok base=ffffffa7c1280000 slide=00000027b9200000
Геометрия стека подтверждена размерами фреймов boot.elf: в __arm64_sys_futex(0x70) + do_futex(0x60+0x1a0) rt_waiter находится на sp+0xc0 → глубина 0x1b0; в пути pselect stack_fds[0] на глубине 0x210, разница 0x60 = 12 слов. Таким образом, word_i попадает в stack_fds[12+i]:
words 0-2 находятся в области ввода ex[2..4] (напрямую управляемы), words 6-7 (task/lock) — в res_in[3..4] (кодируются через in[3..4] + готовый POLLIN fd), words 3-5/8-10 остаются 0.
pselect немедленно возвращается из-за готового fd → waiter в пользовательском режиме занят ожиданием (сигналы запрещены, ноль syscall), пока consumer не завершит работу.
Слова task/lock overlay попадают в res_in[3]/[4] (кодируются готовностью fd). По факту res_in[4] (lock) детерминированно перезаписывается на пути возврата pselect (остатки фрейма rt_sigreturn) и никогда не равен fake_lock из payload; res_in[3] (task) иногда остаётся целым. Обход в пользовательском режиме (FP-операции + sched_yield) может лишь снизить частоту срабатывания do_notify_resume до ~21%, но перезапись lock почти неизбежна.
Ещё два связанных факта:
rt_mutex_adjust_pi() содержит if (!owner) return 0;, поэтому при отсутствии owner у fake_lock adjust_prio_chain не вызывается. В popsicle (6.12) этой проверки нет. Payload с owner перенесён (fake_lock owner=fake_task|1), но из-за неизбежной перезаписи слова lock его нельзя надёжно сработать.После дизассемблирования boot.elf __arm64_sys_ppoll / do_sys_poll подтверждено: pollfd — 16-байтная структура (fd 4B + events 4B + revents 4B + pad 4B), которая не может вместить 64-битные слова task/lock fake waiter — значение fd ограничено (должен быть реальный fd), events занимает всего 4 байта и не непрерывен, revents записывается ядром (неуправляем).
smt878u / popsicle можно полностью повысить привилегии: их геометрия стека позволяет словам попадать в управляемые пользователем in/out/ex (3 fd_set pselect). У GOT-W29 позиция waiter (bits+0x60 → task/lock в res_in[3]/[4]) не имеет такого окна. Это различие в геометрии стека ядра, а не дефект реализации.
В домене приложений (untrusted_app) нет готового канала KASLR (perf/kallsyms/pagemap/dmesg — всё отклонено); CMP_REQUEUE_PI возвращает 1 (requeue успешен) и не идёт по пути отката EDEADLK; major_only в cpuset без root жёстко закорачивает step[6] обхода цепочки. Единственная точка входа для изменения QOS — /dev/iaware_qos_ctrl — заблокирована SELinux. Поэтому обычные права приложения не позволяют сработать этой CVE.
Самописный модуль KernelPatch (rtmutex-dbg) для наблюдения на реальном устройстве за fake waiter и примитивом записи, собран на основе заголовков LyraVoid/KernelPatch 0.13.5 (того же исходника, что FolkPatch).
cd <KernelPatch>/kpms/rtmutex-dbg
make TARGET_COMPILE=aarch64-linux-android- \
CC=$PREFIX/bin/aarch64-linux-android-clang \
LD=$PREFIX/bin/aarch64-linux-android-ld
Результат — rtmutex_dbg.kpm. Флаги компиляции должны включать
-fno-pic -fno-pie -fno-asynchronous-unwind-tables -fno-unwind-tables
(уже встроены в Makefile): clang по умолчанию использует PIC, что даёт релокации GOT, и по умолчанию генерирует .eh_frame
(R_AARCH64_PREL32), а загрузчик KPM этого не поддерживает → ошибка загрузки -1.
Superkey у FolkPatch — su (не KernelPatch по умолчанию, как в APatch). Используйте инструмент sc_kpm_load (исходник sc_kpm_load.c):
adb shell /data/local/tmp/sc_kpm_load su /sdcard/Download/rtmutex_dbg.kpm # загрузка
adb shell /data/local/tmp/sc_kpm_load unload rtmutex-dbg su # выгрузка
adb shell /data/local/tmp/sc_kpm_load ctl rtmutex-dbg counts su # счётчики
run_rtmdbg_test.sh (на устройстве /data/local/tmp/ghostlock-test/): запуск GhostLock от имени shell
(реальный путь GOT_SLIDE_NO_RT=1), цикл синхронизации 0.5s для предотвращения потери логов,
dmesg -w в файл, автоматическое завершение через 90s (SIGSTOP для предотвращения перезагрузки из-за soft-lock). Перед тестом:
adb shell 'su -c "sh /sdcard/ghostlock-test/set_debug_no_reboot.sh"' # предотвращение перезагрузки
[RTMDBG])REPAIR3: при проверке примитива записи пересоздание overlay и перезапись параметра next_lockFAKEWALK skip: перекрытый fake walk пропущен (без записи и без краха)prio_chain[N] / prio_chain_ret: вызовы обхода и возвращаемые значения (0=пройден полностью;
4294967261=-EDEADLK)do_select n=320: res_in[3]/[4] на стороне ядраfutex op=13/14: CMP_REQUEUE_PI / WAIT_REQUEUE_PIПолные стеки oops/panic Huawei записываются в /data/log/bbox/history.log.
tools/ инструменты проверки (perf KASLR, EDEADLK-зонд, цепочка KPM)
target/ все проверенные смещения
exploit/ перенесённый slide.c (с изменениями для срабатывания EDEADLK)
| hook | назначение |
|---|
rt_mutex_adjust_pi | запись корректировок PI; при перекрытии overlay — skip (очистка pi_blocked_on) |
rt_mutex_adjust_prio_chain | безусловный skip при fake walk; полный дамп waiter |
__arm64_sys_pselect6 / __arm64_sys_ppoll | очистка _TIF_WORK_MASK на пути возврата; наблюдение fd_set |
do_select | чтение res_in[3]/[4] на стороне ядра |
__arm64_sys_futex | трассировка WAIT_REQUEUE_PI / CMP_REQUEUE_PI |
rt_mutex_dequeue | подтверждение выполнения примитива записи step[7] и формы дерева |