
GhostLock (CVE-2026-43499) pour OPPO Find X5 Pro (PFEM10) — rétro-ingénierie du watchdog OPlus et du détecteur de heap-spray
English · 中文
Portage de GhostLock (CVE-2026-43499) pour l'OPPO Find X5 Pro sous ColorOS 16. Atteint un processus enfant avec uid=0 et un kernelsu.ko chargé ; le processus root est intercepté.
CVE-2026-43499 — use-after-free de futex PI. remove_waiter() efface current->pi_blocked_on lorsque current est le requeueur, sur le chemin de rollback -EDEADLK de rt_mutex_start_proxy_lock().
remove_waiter @ 0xffffffc0081ed254 — forme pré-correctif.
| Appareil | OPPO Find X5 Pro (PFEM10) |
| SoC | SM8450 / Adreno 730 |
| OS | ColorOS 16.0.3.520 (CN01) |
| Noyau | 5.10.236-android12-9-o-gaf2075ad2c06 |
| Bootloader | verrouillé, vert |
| VA_BITS | 39 — KIMAGE_TEXT_BASE = 0xffffffc008000000 |
| Étape | |
|---|---|
Déclenchement du waiter compact (CMP_REQUEUE_PI → EDEADLK) | fonctionne |
Fuite de task_struct (perf) | fonctionne |
Écriture PI (8 octets ; valeur = 0 ou une adresse noyau valide) | fonctionne |
task+0x778 ou task+0x780 seul → Uid=root | fonctionne — mais un atterrissage sur un seul champ laisse la tâche divergente, et c'est un BUG_ON dur latent. Voir le risque de divergence |
| Les deux champs écrits avec UNE seule valeur (une paire cohérente) | ❌ jamais produit avec une page pulvérisée. Observé uniquement avec l'alias global init_cred (09-14, CONTROL=1). Le runner l'impose désormais (SAME_VALUE=1) ; non exécuté sur l'appareil |
Blanchiment des identifiants (setresgid + setresuid) | implémenté derrière V12_LAUNDER=1 ; non exécuté sur l'appareil |
kernelsu.ko chargé | fonctionne |
| Le processus root survit | ⚠ non établi — voir ci-dessous |
| Mécanisme de redémarrage | ❌ non établi. Un candidat (la divergence) est désormais exclu ; voir ci-dessous |
probe_state comme critère d'atterrissage | ❌ incorrect — ne pas utiliser. Trois contre-exemples ; voir le tableau ci-dessous |
| Canal de panique pstore/ramoops | ⚠ l'instrument existe ; canal jamais validé (pas encore de test nul) |
| « La victime tourne en pur espace utilisateur » | ⚠ aucune lecture pour l'instant — uid.stream enregistre désormais utime/stime/nvcsw pour pouvoir le vérifier |
| double écriture en une passe côté pi | ⚠ non établi ; pi.pc/pi.left sont codés en dur à 0 dans fdset_map.h |
Chemin A (UMH / modprobe_path) | STATIC_USERMODEHELPER_PATH="" |
À propos de « le processus root survit » : les exécutions dans evidence/kill.log
atteignent uid=0 et chargent kernelsu.ko, et dans l'exécution qui l'a réellement sondé,
le processus du gestionnaire KernelSU a survécu 120 s avec kernelsu toujours Live dans
/proc/modules. Dans une exécution ultérieure, la même chaîne a laissé les services du framework Android
injoignables (Can't find service: package/power/input/phone/wifi)
alors que le module était toujours Live. Aucune ligne noyau [ROOTCHECK-*] et aucune
charge utile $$sys_call_number@@ n'a jamais été capturée, donc la cause de l'état de
l'exécution ultérieure n'est pas attribuée. Voir evidence/notes.md
§2.3, §2.4 et §7.
task_struct
| Champ | Décalage |
|---|---|
real_cred / cred | 0x778 / 0x780 |
syscallno mis en cache | 0xdf8 |
uid / euid / gid / egid mis en cache | 0xe00 / 0xe08 / 0xe10 / 0xe18 |
thread_info
| Champ | Décalage |
|---|---|
flags | 0x0 |
addr_limit | 0x8 |
ttbr0 | 0x10 |
preempt_count | 0x18 |
cred
| Champ | Décalage | Champ | Décalage |
|---|---|---|---|
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
**L'étape 2 doit recevoir explicitement la valeur de l'étape 1.** Les étapes 1 et 2 sont deux
processus indépendants, chacun avec son propre spray, donc « écrire la page de creds dans les deux
slots » est un piège : lu naïvement, cela produit `(pageA, pageB)`, et comme
`commit_creds` compare des **pointeurs**, cette paire est divergente même lorsque les deux écritures
aboutissent. Ce n'est pas hypothétique — c'est exactement ce que font les exécutions 3 et 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 déclenche donc l'étape 2 avec V12_W7_VALUE=<valeur observée à l'étape 1> et refuse de la déclencher du tout si cette valeur ne peut pas être récupérée.
HOLD doit survivre à l'étape 2, sinon la page de l'étape 1 est libérée et réallouée et « la
même valeur » devient un pointeur suspendu. Voir
la règle de la même valeur.
Une page par démarrage est réparée. L'étape 3 met à zéro V+8. Avec deux pages différentes,
mettre à zéro les deux effacerait l'estampille gid/suid (ci-dessous) et ferait
passer une divergence pour un accord, donc le runner ne répare que la page qui a été
effectivement installée et s'arrête si les deux valeurs divergent.
La page cred est construite par payload.c : les huit champs id à zéro, les cinq
ensembles de capacités complets, et user / user_ns / group_info pointant vers
root_user / init_user_ns / init_groups. L'étape 3 existe parce que l'effet de bord de l'écriture
écrase toujours cred+8 (gid/suid) du cred qu'il installe.
init_cred — une dichotomie expliciteDeux sections ici se contredisaient auparavant (« jamais le global init_cred »
vs « CONTROL=1 reproduit la cellule 2 », et la cellule 2 est init_cred). Les deux affirmations
sont vraies pour des rôles différents :
init_cred fait que l'effet de bord
corrompt init_cred+8 globalement — init_cred est partagé par chaque thread
du noyau, et Uid: 0 0 4294967176 0 est précisément cette corruption. Le code
refuse ce chemin sauf si V12_ALLOW_INIT_CRED=1 est défini délibérément.0xffffff802a7e0be0) dans les deux slots, donc
real_cred == cred par construction — c'est pourquoi elle a survécu jusqu'à execve.
CONTROL=1 la reproduit. C'est un contrôle, pas une configuration sur laquelle bâtir.fuite perf : PERF_TYPE_SOFTWARE / PERF_COUNT_SW_CPU_CLOCK, PERF_SAMPLE_REGS_INTR, exclude_user=1.
Accepter [0xffffff8400000000, 0xffffff90000000), votes ≥ 15 %.