
GhostLock (CVE-2026-43499) para OPPO Find X5 Pro (PFEM10) — engenharia reversa do watchdog OPlus e do detector de heap-spray
English · 中文
Port do GhostLock (CVE-2026-43499) para o OPPO Find X5 Pro no ColorOS 16. Alcança um processo filho com uid=0 e um kernelsu.ko carregado; o processo root é interceptado.
CVE-2026-43499 — use-after-free no futex PI. remove_waiter() limpa current->pi_blocked_on quando current é o requeuer, no caminho de rollback -EDEADLK de rt_mutex_start_proxy_lock().
remove_waiter @ 0xffffffc0081ed254 — forma pré-correção.
| 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 do waiter compacto (CMP_REQUEUE_PI → EDEADLK) | funciona |
Vazamento de task_struct (perf) | funciona |
Escrita PI (8 bytes; valor = 0 ou um endereço de kernel válido) | funciona |
task+0x778 ou task+0x780 sozinho → Uid=root | funciona — mas um pouso de campo único deixa a task divergente, e isso é um BUG_ON rígido latente. Ver o risco de divergência |
| Ambos os campos escritos com UM valor (um par consistente) | ❌ nunca produzido com uma página pulverizada. Só foi observado com o alias global init_cred (09-14, CONTROL=1). O runner agora impõe isso (SAME_VALUE=1); não executado no dispositivo |
Lavagem de credenciais (setresgid + setresuid) | implementado atrás de V12_LAUNDER=1; não executado no dispositivo |
kernelsu.ko carregado | funciona |
| Processo root sobrevive | ⚠ não estabelecido — ver abaixo |
| Mecanismo de reboot | ❌ não estabelecido. Um candidato (a divergência) agora está excluído; ver abaixo |
probe_state como critério de pouso | ❌ errado — não usar. Três contraexemplos; ver a tabela abaixo |
| Canal de pânico pstore/ramoops | ⚠ instrumento existe; canal nunca validado (ainda sem teste nulo) |
| "A vítima gira em userspace puro" | ⚠ ainda sem leitura — uid.stream agora registra utime/stime/nvcsw para que possa ser verificado |
| escrita dupla de passagem única do lado pi | ⚠ não estabelecido; pi.pc/pi.left estão fixados em 0 em fdset_map.h |
Caminho A (UMH / modprobe_path) | STATIC_USERMODEHELPER_PATH="" |
Sobre "processo root sobrevive": as execuções em evidence/kill.log
alcançam uid=0 e carregam kernelsu.ko, e na execução que de fato fez polling
o processo do gerenciador KernelSU sobreviveu 120 s com kernelsu ainda Live em
/proc/modules. Em uma execução posterior, a mesma cadeia deixou os serviços do framework Android
inacessíveis (Can't find service: package/power/input/phone/wifi)
enquanto o módulo ainda estava Live. Nenhuma linha de kernel [ROOTCHECK-*] e nenhum
payload $$sys_call_number@@ jamais foi capturado, então a causa do estado da
execução posterior não é atribuída. Ver evidence/notes.md
§2.3, §2.4 e §7.
task_struct
| Campo | Offset |
|---|---|
real_cred / cred | 0x778 / 0x780 |
syscallno em cache | 0xdf8 |
uid / euid / gid / egid em cache | 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
**O Estágio 2 deve receber explicitamente o valor do estágio 1.** Os Estágios 1 e 2 são dois
processos independentes, cada um com seu próprio spray, então "escrever a página de credenciais em ambos
os slots" é uma armadilha: lida ingenuamente, produz `(pageA, pageB)`, e porque
`commit_creds` compara **ponteiros**, esse par é divergente mesmo quando ambas as escritas
ocorrem. Isso não é hipotético — é exatamente o que as execuções 3 e 9 fizeram:```
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 portanto dispara o estágio 2 com V12_W7_VALUE=<valor observado pelo estágio 1> e recusa-se a dispará-lo de todo se esse valor não puder ser recuperado.
HOLD deve sobreviver ao estágio 2, ou a página do estágio 1 é liberada e realocada e "o
mesmo valor" torna-se um ponteiro pendente. Veja
a regra do mesmo valor.
Uma página por boot é reparada. O estágio 3 zera V+8. Com duas páginas
diferentes, zerar ambas apagaria o carimbo gid/suid (abaixo) e faria uma divergência
parecer concordância, então o runner repara apenas a página que foi realmente
instalada e para se os dois valores discordarem.
A página de cred é construída por payload.c: todos os oito campos de id zerados, todos os cinco
conjuntos de capacidades completos, e user / user_ns / group_info apontando para
root_user / init_user_ns / init_groups. O estágio 3 existe porque o efeito colateral da escrita sempre
sobrescreve cred+8 (gid/suid) de qualquer cred que ele instale.
init_cred — uma dicotomia explícitaDuas seções aqui costumavam se contradizer ("nunca o init_cred global"
vs "CONTROL=1 reproduz a célula 2", e a célula 2 é init_cred). Ambas as afirmações
são verdadeiras para papéis diferentes:
init_cred faz o efeito colateral
corromper init_cred+8 globalmente — init_cred é compartilhado por toda thread
do kernel, e Uid: 0 0 4294967176 0 é precisamente essa corrupção. O código
recusa esse caminho a menos que V12_ALLOW_INIT_CRED=1 seja definido deliberadamente.0xffffff802a7e0be0) em ambos os slots, então
real_cred == cred por construção — é por isso que sobreviveu até execve.
CONTROL=1 o reproduz. É um controle, não uma configuração para se basear.vazamento de perf: PERF_TYPE_SOFTWARE / PERF_COUNT_SW_CPU_CLOCK, PERF_SAMPLE_REGS_INTR, exclude_user=1.
Aceitar [0xffffff8400000000, 0xffffff90000000), votos ≥ 15%.