
GhostLock (CVE-2026-43499) para OPPO Find X5 Pro (PFEM10) — ingeniería inversa del watchdog de OPlus y del detector de heap-spray
English · 中文
GhostLock (CVE-2026-43499) port para el OPPO Find X5 Pro en ColorOS 16. Alcanza un proceso hijo con uid=0 y un kernelsu.ko cargado; el proceso root es interceptado.
CVE-2026-43499 — use-after-free de futex PI. remove_waiter() limpia current->pi_blocked_on cuando current es el requeuer, en la ruta de rollback -EDEADLK de rt_mutex_start_proxy_lock().
remove_waiter @ 0xffffffc0081ed254 — forma previa al fix.
| Dispositivo | OPPO Find X5 Pro (PFEM10) |
| SoC | SM8450 / Adreno 730 |
| SO | ColorOS 16.0.3.520 (CN01) |
| Kernel | 5.10.236-android12-9-o-gaf2075ad2c06 |
| Bootloader | bloqueado, verde |
| VA_BITS | 39 — KIMAGE_TEXT_BASE = 0xffffffc008000000 |
| Etapa | |
|---|---|
Disparo de waiter compacto (CMP_REQUEUE_PI → EDEADLK) | funciona |
Fuga de task_struct (perf) | funciona |
Escritura PI (8 bytes; valor = 0 o una dirección de kernel válida) | funciona |
task+0x778 o task+0x780 por sí solos → Uid=root | funciona — pero un aterrizaje de un solo campo deja la tarea divergente, y eso es un BUG_ON duro latente. Ver el riesgo de divergencia |
| Ambos campos escritos con UN solo valor (un par consistente) | ❌ nunca producido con una página rociada. Solo observado con el alias global init_cred (09-14, CONTROL=1). El runner ahora lo impone (SAME_VALUE=1); no ejecutado en el dispositivo |
Blanqueo de credenciales (setresgid + setresuid) | implementado detrás de V12_LAUNDER=1; no ejecutado en el dispositivo |
kernelsu.ko cargado | funciona |
| El proceso root sobrevive | ⚠ no establecido — ver abajo |
| Mecanismo de reinicio | ❌ no establecido. Un candidato (la divergencia) ahora está excluido; ver abajo |
probe_state como criterio de aterrizaje | ❌ incorrecto — no usar. Tres contraejemplos; ver la tabla de abajo |
| Canal de pánico pstore/ramoops | ⚠ el instrumento existe; canal nunca validado (aún sin prueba nula) |
| "La víctima gira en espacio de usuario puro" | ⚠ aún sin lectura — uid.stream ahora registra utime/stime/nvcsw para poder comprobarlo |
| doble escritura de una sola pasada del lado pi | ⚠ no establecido; pi.pc/pi.left están codificados a 0 en fdset_map.h |
Ruta A (UMH / modprobe_path) | STATIC_USERMODEHELPER_PATH="" |
Sobre "el proceso root sobrevive": las ejecuciones en evidence/kill.log
alcanzan uid=0 y cargan kernelsu.ko, y en la ejecución que realmente lo sondeó
el proceso del gestor de KernelSU sobrevivió 120 s con kernelsu aún Live en
/proc/modules. En una ejecución posterior la misma cadena dejó los servicios del framework de Android inalcanzables (Can't find service: package/power/input/phone/wifi)
mientras el módulo seguía Live. Nunca se ha capturado ninguna línea de kernel [ROOTCHECK-*] ni ningún payload $$sys_call_number@@, por lo que la causa del estado de la ejecución posterior no se atribuye. Ver evidence/notes.md
§2.3, §2.4 y §7.
task_struct
| Campo | Offset |
|---|---|
real_cred / cred | 0x778 / 0x780 |
syscallno en caché | 0xdf8 |
uid / euid / gid / egid en caché | 0xe00 / 0xe08 / 0xe10 / 0xe18 |
thread_info
| Campo | Offset |
|---|---|
flags | 0x0 |
addr_limit | 0x8 |
ttbr0 | 0x10 |
preempt_count | 0x18 |
cred
| Campo | Offset | Campo | Offset |
|---|---|---|---|
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
**La etapa 2 debe recibir explícitamente el valor de la etapa 1.** Las etapas 1 y 2 son dos
procesos independientes, cada uno con su propio spray, por lo que "escribir la página de credenciales en ambas
ranuras" es una trampa: leído ingenuamente produce `(pageA, pageB)`, y como
`commit_creds` compara **punteros**, ese par es divergente incluso cuando ambas escrituras
se realizan. Esto no es hipotético — es exactamente lo que hicieron las ejecuciones 3 y 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 por lo tanto dispara la etapa 2 con V12_W7_VALUE=<valor observado de la etapa 1> y se niega a dispararla por completo si ese valor no puede recuperarse. HOLD debe sobrevivir a la etapa 2, o la página de la etapa 1 se libera y se reasigna y "el mismo valor" se convierte en un puntero colgante. Véase la regla del mismo valor.
Se repara una página por arranque. La etapa 3 pone a cero V+8. Con dos páginas diferentes, poner a cero ambas borraría el sello gid/suid (abajo) y haría que una divergencia pareciera un acuerdo, por lo que el ejecutor repara solo la página que realmente se instaló y se detiene si los dos valores no coinciden.
La página de credenciales es construida por payload.c: los ocho campos de id a cero, los cinco conjuntos de capacidades completos, y user / user_ns / group_info apuntando a root_user / init_user_ns / init_groups. La etapa 3 existe porque el efecto secundario de la escritura siempre sobrescribe cred+8 (gid/suid) de cualquier credencial que instale.
init_cred — una dicotomía explícitaDos secciones aquí solían contradecirse entre sí ("nunca el init_cred global" vs "CONTROL=1 reproduce la celda 2", y la celda 2 es init_cred). Ambas afirmaciones son ciertas para roles diferentes:
init_cred hace que el efecto secundario corrompa init_cred+8 globalmente — init_cred es compartido por cada hilo del kernel, y Uid: 0 0 4294967176 0 es precisamente esa corrupción. El código rechaza esta ruta a menos que V12_ALLOW_INIT_CRED=1 se establezca deliberadamente.0xffffff802a7e0be0) en ambas ranuras, por lo que real_cred == cred por construcción — es por eso que sobrevivió hasta execve. CONTROL=1 lo reproduce. Es un control, no una configuración sobre la que construir.fuga de perf: PERF_TYPE_SOFTWARE / PERF_COUNT_SW_CPU_CLOCK, PERF_SAMPLE_REGS_INTR, exclude_user=1.
Aceptar [0xffffff8400000000, 0xffffff90000000), votos ≥ 15%.