
GhostLock (CVE-2026-43499) per OPPO Find X5 Pro (PFEM10) — reverse engineering del watchdog OPlus e del rilevatore di heap-spray
English · 中文
Port di GhostLock (CVE-2026-43499) per OPPO Find X5 Pro su ColorOS 16. Raggiunge un processo figlio con uid=0 e un kernelsu.ko caricato; il processo root viene intercettato.
CVE-2026-43499 — futex PI use-after-free. remove_waiter() azzera current->pi_blocked_on quando current è il requeuer, sul percorso di rollback -EDEADLK di rt_mutex_start_proxy_lock().
remove_waiter @ 0xffffffc0081ed254 — forma pre-fix.
| Dispositivo | OPPO Find X5 Pro (PFEM10) |
| SoC | SM8450 / Adreno 730 |
| OS | ColorOS 16.0.3.520 (CN01) |
| Kernel | 5.10.236-android12-9-o-gaf2075ad2c06 |
| Bootloader | bloccato, verde |
| VA_BITS | 39 — KIMAGE_TEXT_BASE = 0xffffffc008000000 |
| Fase | |
|---|---|
Trigger compact waiter (CMP_REQUEUE_PI → EDEADLK) | funziona |
Leak di task_struct (perf) | funziona |
Scrittura PI (8 byte; valore = 0 o un indirizzo kernel valido) | funziona |
task+0x778 oppure task+0x780 da solo → Uid=root | funziona — ma un atterraggio su un singolo campo lascia il task divergente, e questo è un latente hard BUG_ON. Vedi il rischio di divergenza |
| Entrambi i campi scritti con UN SOLO valore (una coppia coerente) | ❌ mai prodotto con una pagina sprayata. Osservato solo con l'alias globale init_cred (09-14, CONTROL=1). Il runner ora lo impone (SAME_VALUE=1); non eseguito sul dispositivo |
Laundering delle credenziali (setresgid + setresuid) | implementato dietro V12_LAUNDER=1; non eseguito sul dispositivo |
kernelsu.ko caricato | funziona |
| Il processo root sopravvive | ⚠ non stabilito — vedi sotto |
| Meccanismo di reboot | ❌ non stabilito. Un candidato (la divergenza) è ora escluso; vedi sotto |
probe_state come criterio di atterraggio | ❌ errato — non usare. Tre controesempi; vedi la tabella sotto |
| Canale di panic pstore/ramoops | ⚠ lo strumento esiste; canale mai validato (nessun null test ancora) |
| "La vittima gira in puro userspace" | ⚠ nessuna lettura ancora — uid.stream ora registra utime/stime/nvcsw così può essere verificato |
| doppia scrittura single-pass lato pi | ⚠ non stabilito; pi.pc/pi.left sono hard-coded a 0 in fdset_map.h |
Path A (UMH / modprobe_path) | STATIC_USERMODEHELPER_PATH="" |
Su "il processo root sopravvive": le esecuzioni in evidence/kill.log
raggiungono uid=0 e caricano kernelsu.ko, e nell'esecuzione che effettivamente lo ha interrogato
il processo del manager KernelSU è sopravvissuto 120 s con kernelsu ancora Live in
/proc/modules. In un'esecuzione successiva la stessa catena ha reso irraggiungibili i servizi
del framework Android (Can't find service: package/power/input/phone/wifi)
mentre il modulo era ancora Live. Nessuna riga kernel [ROOTCHECK-*] e nessun
payload $$sys_call_number@@ è mai stato catturato, quindi la causa dello stato
dell'esecuzione successiva non è attribuita. Vedi evidence/notes.md
§2.3, §2.4 e §7.
task_struct
| Campo | Offset |
|---|---|
real_cred / cred | 0x778 / 0x780 |
syscallno in cache | 0xdf8 |
uid / euid / gid / egid in 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
**Lo stage 2 deve ricevere esplicitamente il valore dello stage 1.** Gli stage 1 e 2 sono due processi indipendenti, ciascuno con il proprio spray, quindi "scrivi la pagina delle credenziali in entrambi gli slot" è una trappola: letto ingenuamente produce `(pageA, pageB)`, e poiché `commit_creds` confronta i **puntatori**, quella coppia è divergente anche quando entrambe le scritture vanno a buon fine. Questo non è ipotetico — è esattamente ciò che hanno fatto i run 3 e 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 attiva quindi lo stage 2 con V12_W7_VALUE=<valore osservato dallo stage 1> e rifiuta di attivarlo del tutto se quel valore non può essere recuperato.
HOLD deve sopravvivere allo stage 2, altrimenti la pagina dello stage 1 viene liberata e riallocata e "lo stesso valore" diventa un puntatore pendente. Vedi
la regola dello stesso valore.
Una pagina per boot viene riparata. Lo stage 3 azzera V+8. Con due pagine diverse, azzerare entrambe cancellerebbe il timbro gid/suid (sotto) e farebbe sembrare una divergenza un accordo, quindi il runner ripara solo la pagina che è stata effettivamente installata e si ferma se i due valori non concordano.
La pagina cred è costruita da payload.c: tutti gli otto campi id a zero, tutti e cinque i set di capability pieni, e user / user_ns / group_info puntati a
root_user / init_user_ns / init_groups. Lo stage 3 esiste perché l'effetto collaterale della scrittura sovrascrive sempre cred+8 (gid/suid) di qualunque cred venga installato.
init_cred — una dicotomia esplicitaDue sezioni qui si contraddicevano a vicenda ("mai il globale init_cred"
contro "CONTROL=1 riproduce la cella 2", e la cella 2 è init_cred). Entrambe le affermazioni
sono vere per ruoli diversi:
init_cred fa sì che l'effetto collaterale
corrompa init_cred+8 globalmente — init_cred è condiviso da ogni thread
del kernel, e Uid: 0 0 4294967176 0 è precisamente quella corruzione. Il codice
rifiuta questo percorso a meno che V12_ALLOW_INIT_CRED=1 non sia impostato deliberatamente.0xffffff802a7e0be0) in entrambi gli slot, quindi
real_cred == cred per costruzione — ecco perché è sopravvissuta fino a execve.
CONTROL=1 la riproduce. È un controllo, non una configurazione su cui costruire.perf leak: PERF_TYPE_SOFTWARE / PERF_COUNT_SW_CPU_CLOCK, PERF_SAMPLE_REGS_INTR, exclude_user=1.
Accetta [0xffffff8400000000, 0xffffff90000000), voti ≥ 15%.