
GhostLock (CVE-2026-43499) für OPPO Find X5 Pro (PFEM10) — Reverse Engineering des OPlus-Watchdog- und Heap-Spray-Detektors
Englisch · 中文
GhostLock (CVE-2026-43499)-Port für das OPPO Find X5 Pro unter ColorOS 16. Erreicht einen Kindprozess mit uid=0 und ein geladenes kernelsu.ko; der Root-Prozess wird abgefangen.
CVE-2026-43499 — futex PI Use-after-free. remove_waiter() löscht current->pi_blocked_on, wenn current der Requester ist, auf dem -EDEADLK-Rollback-Pfad von rt_mutex_start_proxy_lock().
remove_waiter @ 0xffffffc0081ed254 — Zustand vor dem Fix.
| Gerät | 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 | gesperrt, grün |
| VA_BITS | 39 — KIMAGE_TEXT_BASE = 0xffffffc008000000 |
| Phase | |
|---|---|
Kompakter Waiter-Trigger (CMP_REQUEUE_PI → EDEADLK) | funktioniert |
task_struct-Leak (perf) | funktioniert |
PI-Schreibvorgang (8 Byte; Wert = 0 oder eine gültige Kernel-Adresse) | funktioniert |
task+0x778 oder task+0x780 allein → Uid=root | funktioniert — aber eine Landung auf einem einzelnen Feld lässt die Task divergieren, und das ist ein latentes hartes BUG_ON. Siehe die Divergenzgefahr |
| Beide Felder mit EINEM Wert beschrieben (ein konsistentes Paar) | ❌ nie mit einer gesprayten Seite erzeugt. Nur jemals mit dem globalen init_cred-Alias beobachtet (09-14, CONTROL=1). Der Runner erzwingt es jetzt (SAME_VALUE=1); nicht auf dem Gerät ausgeführt |
Credential-Laundering (setresgid + setresuid) | implementiert hinter V12_LAUNDER=1; nicht auf dem Gerät ausgeführt |
kernelsu.ko geladen | funktioniert |
| Root-Prozess überlebt | ⚠ nicht nachgewiesen — siehe unten |
| Reboot-Mechanismus | ❌ nicht nachgewiesen. Ein Kandidat (die Divergenz) ist jetzt ausgeschlossen; siehe unten |
probe_state als Landungskriterium | ❌ falsch — nicht verwenden. Drei Gegenbeispiele; siehe die Tabelle unten |
| pstore/ramoops-Panikkanal | ⚠ Instrument existiert; Kanal nie validiert (noch kein Nulltest) |
| „Das Opfer dreht sich im reinen Userspace" | ⚠ noch keine Messung — uid.stream zeichnet jetzt utime/stime/nvcsw auf, sodass es überprüft werden kann |
| pi-seitiger Dual-Write in einem Durchgang | ⚠ nicht nachgewiesen; pi.pc/pi.left sind in fdset_map.h fest auf 0 gesetzt |
Pfad A (UMH / modprobe_path) | STATIC_USERMODEHELPER_PATH="" |
Zu „Root-Prozess überlebt": Die Läufe in evidence/kill.log
erreichen uid=0 und laden kernelsu.ko, und in dem Lauf, der tatsächlich darauf
pollte, überlebte der KernelSU-Manager-Prozess 120 s, wobei kernelsu weiterhin
Live in /proc/modules war. In einem späteren Lauf hinterließ dieselbe Kette die
Android-Framework-Dienste unerreichbar (Can't find service: package/power/input/phone/wifi),
während das Modul noch Live war. Es wurde nie eine [ROOTCHECK-*]-Kernelzeile und
keine $$sys_call_number@@-Nutzlast erfasst, daher wird die Ursache für den Zustand
des späteren Laufs nicht zugeordnet. Siehe evidence/notes.md
§2.3, §2.4 und §7.
task_struct
| Feld | Offset |
|---|---|
real_cred / cred | 0x778 / 0x780 |
gecachte syscallno | 0xdf8 |
gecachte uid / euid / gid / egid | 0xe00 / 0xe08 / 0xe10 / 0xe18 |
thread_info
| Feld | Offset |
|---|---|
flags | 0x0 |
addr_limit | 0x8 |
ttbr0 | 0x10 |
preempt_count | 0x18 |
cred
| Feld | Offset | Feld | 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
**Stage 2 muss der Wert von Stage 1 explizit übergeben werden.** Stages 1 und 2 sind zwei
unabhängige Prozesse, jeder mit seinem eigenen Spray, daher ist „schreibe die Cred-Seite in beide
Slots" eine Falle: naiv gelesen erzeugt es `(pageA, pageB)`, und weil
`commit_creds` **Zeiger** vergleicht, ist dieses Paar divergent, selbst wenn beide Schreibvorgänge
erfolgreich sind. Das ist nicht hypothetisch — genau das taten die Läufe 3 und 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 startet daher Stufe 2 mit V12_W7_VALUE=<der in Stufe 1 beobachtete Wert> und weigert sich, sie überhaupt zu starten, wenn dieser Wert nicht wiederhergestellt werden kann. HOLD muss Stufe 2 überleben, sonst wird die Seite von Stufe 1 freigegeben und neu zugewiesen, und „derselbe Wert" wird zu einem Dangling Pointer. Siehe die Gleicher-Wert-Regel.
Eine Seite pro Boot wird repariert. Stufe 3 nullt V+8. Bei zwei verschiedenen Seiten würde das Nullsetzen beider den gid/suid-Stempel (unten) löschen und eine Divergenz wie Übereinstimmung aussehen lassen, daher repariert der Runner nur die Seite, die tatsächlich installiert wurde, und stoppt, wenn die beiden Werte nicht übereinstimmen.
Die Cred-Seite wird von payload.c erstellt: alle acht ID-Felder null, alle fünf Capability-Sets voll, und user / user_ns / group_info zeigen auf root_user / init_user_ns / init_groups. Stufe 3 existiert, weil der Seiteneffekt des Schreibens immer cred+8 (gid/suid) des jeweiligen Creds überschreibt, das installiert wird.
init_cred — eine explizite DichotomieZwei Abschnitte hier widersprachen sich früher gegenseitig („niemals das globale init_cred" vs. „CONTROL=1 reproduziert Zelle 2", und Zelle 2 ist init_cred). Beide Aussagen gelten für unterschiedliche Rollen:
init_cred-Zeigers lässt den Seiteneffekt init_cred+8 global beschädigen — init_cred wird von jedem Kernel-Thread geteilt, und Uid: 0 0 4294967176 0 ist genau diese Beschädigung. Der Code verweigert diesen Pfad, es sei denn V12_ALLOW_INIT_CRED=1 wird absichtlich gesetzt.0xffffff802a7e0be0) in beide Slots, sodass real_cred == cred konstruktionsbedingt gilt — deshalb überlebte sie bis execve. CONTROL=1 reproduziert dies. Es ist eine Kontrolle, keine Konfiguration, auf der aufgebaut werden sollte.perf leak: PERF_TYPE_SOFTWARE / PERF_COUNT_SW_CPU_CLOCK, PERF_SAMPLE_REGS_INTR, exclude_user=1.
Akzeptiere [0xffffff8400000000, 0xffffff90000000), Stimmen ≥ 15%.