
GhostLock (CVE-2026-43499) для OPPO Find X5 Pro (PFEM10) — реверс-инжиниринг watchdog и детектора heap-spray от OPlus
English · 中文
Порт GhostLock (CVE-2026-43499) для OPPO Find X5 Pro на ColorOS 16. Достигает дочернего процесса с uid=0 и загруженного kernelsu.ko; корневой процесс перехватывается.
CVE-2026-43499 — use-after-free в futex PI. remove_waiter() очищает current->pi_blocked_on, когда current является requeuer, на пути отката -EDEADLK в rt_mutex_start_proxy_lock().
remove_waiter @ 0xffffffc0081ed254 — форма до исправления.
| Устройство | OPPO Find X5 Pro (PFEM10) |
| SoC | SM8450 / Adreno 730 |
| ОС | ColorOS 16.0.3.520 (CN01) |
| Ядро | 5.10.236-android12-9-o-gaf2075ad2c06 |
| Загрузчик | заблокирован, зелёный |
| VA_BITS | 39 — KIMAGE_TEXT_BASE = 0xffffffc008000000 |
| Этап | |
|---|---|
Триггер компактного waiter (CMP_REQUEUE_PI → EDEADLK) | работает |
Утечка task_struct (perf) | работает |
Запись PI (8 байт; значение = 0 или валидный адрес ядра) | работает |
task+0x778 или task+0x780 по отдельности → Uid=root | работает — но приземление на одно поле оставляет задачу расходящейся, и это скрытый жёсткий BUG_ON. См. опасность расхождения |
| Оба поля записаны ОДНИМ значением (согласованная пара) | ❌ никогда не получено с распылённой страницей. Наблюдалось только с глобальным псевдонимом init_cred (09-14, CONTROL=1). Раннер теперь это принудительно обеспечивает (SAME_VALUE=1); на устройстве не запускалось |
Отмывание учётных данных (setresgid + setresuid) | реализовано за V12_LAUNDER=1; на устройстве не запускалось |
kernelsu.ko загружен | работает |
| Корневой процесс выживает | ⚠ не установлено — см. ниже |
| Механизм перезагрузки | ❌ не установлен. Один кандидат (расхождение) теперь исключён; см. ниже |
probe_state как критерий приземления | ❌ неверно — не использовать. Три контрпримера; см. таблицу ниже |
| Канал паники pstore/ramoops | ⚠ инструмент существует; канал никогда не проверялся (нулевого теста ещё нет) |
| «Жертва крутится в чистом userspace» | ⚠ показаний пока нет — uid.stream теперь записывает utime/stime/nvcsw, так что это можно проверить |
| Двойная запись за один проход со стороны pi | ⚠ не установлено; pi.pc/pi.left жёстко заданы как 0 в fdset_map.h |
Путь A (UMH / modprobe_path) | STATIC_USERMODEHELPER_PATH="" |
О «корневой процесс выживает»: запуски в evidence/kill.log
достигают uid=0 и загружают kernelsu.ko, и в запуске, который действительно опрашивал это,
процесс менеджера KernelSU выжил 120 с, при этом kernelsu оставался Live в
/proc/modules. В более позднем запуске та же цепочка оставила службы Android framework
недоступными (Can't find service: package/power/input/phone/wifi),
пока модуль всё ещё был Live. Ни одной строки ядра [ROOTCHECK-*] и ни одной
полезной нагрузки $$sys_call_number@@ никогда не было захвачено, поэтому причина
состояния более позднего запуска не атрибутирована. См. evidence/notes.md
§2.3, §2.4 и §7.
task_struct
| Поле | Смещение |
|---|---|
real_cred / cred | 0x778 / 0x780 |
кэшированный syscallno | 0xdf8 |
кэшированные uid / euid / gid / egid | 0xe00 / 0xe08 / 0xe10 / 0xe18 |
thread_info
| Поле | Смещение |
|---|---|
flags | 0x0 |
addr_limit | 0x8 |
ttbr0 | 0x10 |
preempt_count | 0x18 |
cred
| Поле | Смещение | Поле | Смещение |
|---|---|---|---|
uid | 0x4 | cap_inheritable | 0x28 |
gid | 0x8 | cap_permitted | 0x30 |
suid | 0xc | cap_effective | 0x38 |
sgid | 0x10 | cap_bset | 0x40 |
euid | 0x14 | cap_ambient | 0x48 |
egid | 0x18 | ||
fsuid / fsgid | 0x1c / 0x20 |
LT perf leak target task_struct → file W7 stage 1 task+0x778 = V (real_cred → private sprayed page; V observed) W7 stage 2 task+0x780 = V (cred) ★ V12_W7_VALUE=V — THE SAME VALUE, not a new page W7 stage 3 V+8 = 0 (LOCAL repair of the page that was installed, ZERO shape) LT child fexecve(memfd of loader) — no execve of a /data path loader ksud late-load → kernelsu ... Live
**Этап 2 должен явно получить значение этапа 1.** Этапы 1 и 2 — это два
независимых процесса, каждый со своим spray, поэтому «записать страницу учётных данных в оба
слота» — это ловушка: при наивном чтении это даёт `(pageA, pageB)`, и поскольку
`commit_creds` сравнивает **указатели**, эта пара является расходящейся, даже когда обе записи
происходят. Это не гипотетическая ситуация — именно это и произошло в запусках 3 и 9:```
run 9 0x778 shot write value = 0xffffff88679bade0
0x780 shot write value = 0xffffff8785d6ade0 <- a different page
run 3 0x778 shot write value = 0xffffff8787b5ade0
0x780 shot write value = 0xffffff881bad2de0 <- a different page
run_bootA.sh поэтому запускает stage 2 с V12_W7_VALUE=<значение, наблюдённое на stage 1> и отказывается запускать его вообще, если это значение не удаётся восстановить. HOLD должен пережить stage 2, иначе страница stage 1 освобождается и перераспределяется, и «то же самое значение» превращается в висячий указатель. См. правило одного и того же значения.
За одну загрузку восстанавливается одна страница. Stage 3 обнуляет V+8. При двух разных страницах обнуление обеих стёрло бы отпечаток gid/suid (см. ниже) и заставило бы расхождение выглядеть как согласие, поэтому раннер восстанавливает только ту страницу, которая была фактически установлена, и останавливается, если два значения расходятся.
Страница cred строится в payload.c: все восемь полей id равны нулю, все пять наборов capability заполнены, а user / user_ns / group_info указывают на root_user / init_user_ns / init_groups. Stage 3 существует потому, что побочный эффект записи всегда затирает cred+8 (gid/suid) того cred, который он устанавливает.
init_cred — явная дихотомияДва раздела здесь раньше противоречили друг другу («никогда глобальный init_cred» против «CONTROL=1 воспроизводит cell 2», а cell 2 и есть init_cred). Оба утверждения верны для разных ролей:
init_cred заставляет побочный эффект повредить init_cred+8 глобально — init_cred разделяется всеми потоками ядра, и Uid: 0 0 4294967176 0 — это в точности то повреждение. Код отказывается идти по этому пути, если только V12_ALLOW_INIT_CRED=1 не задан намеренно.0xffffff802a7e0be0) в оба слота, так что real_cred == cred по построению — именно поэтому она дожила до execve. CONTROL=1 воспроизводит это. Это контроль, а не конфигурация, на которой следует строить.утечка perf: PERF_TYPE_SOFTWARE / PERF_COUNT_SW_CPU_CLOCK, PERF_SAMPLE_REGS_INTR, exclude_user=1.
Принимать [0xffffff8400000000, 0xffffff90000000), голосов ≥ 15%.