Skip to content
KitploitKITPLOIT
ToolsBlog
Einreichen
ToolsBlog
Einreichen

Hacking-, PenTest- und Cybersicherheits-Tools für Ihr Sicherheitsarsenal!

Kitploit ist ein Verzeichnis von Hacking-, Cybersicherheits- und Pentesting-Tools. Entdecken Sie die neuesten Projekt-Updates, um Schwachstellen zu finden, Systeme zu analysieren, Tests zu automatisieren und Ihre Sicherheit zu stärken.

··Feeds·Kontakt·Datenschutz·© 2026 Kitploit

Tool-Verzeichnis

Kategorien

Alle Kategorien anzeigen
Loading categories
ghostlock-pfem10 — GhostLock (CVE-2026-43499) für OPPO Find X5 Pro (PFEM10) — Reverse Engineering des OPlus-Watchdog- und Heap-Spray-Detektors | Kitploit
Tools/GitHubGitHub/imeiplus/ghostlock-pfem10
Android-SicherheitPrivilege EscalationSpeicherforensikSchwachstellenanalyseExploitationReverse EngineeringMobile SicherheitPayload-EntwicklungBinary-Exploitation
GitHubimeiplus/ghostlock-pfem10

ghostlock-pfem10

GhostLock (CVE-2026-43499) für OPPO Find X5 Pro (PFEM10) — Reverse Engineering des OPlus-Watchdog- und Heap-Spray-Detektors

vor 2 TagenNoch nicht geprüft

Beliebteste

Alle anzeigen →

Entdecken Sie die meistgenutzten Tools unserer Community.

Alle Tools erkunden

Durchsuchen Sie unsere Tool-Sammlung

Alle Tools anzeigen →
Teilen
Repository anzeigen

GhostLock — OPPO Find X5 Pro (PFEM10)

Englisch · 中文

build

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.

Schwachstelle

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

Status

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.

Offsets

task_struct

thread_info

FeldOffset

cred

Exploit-Ablauf```

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

root@kitploit:~
**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.

Über init_cred — eine explizite Dichotomie

Zwei 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:

  • Als Ziel verboten. Das Schreiben des 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.
  • Als einziges BEWIESENES konsistentes Paar beibehalten. Die 09-14-Kette, die ksud erreichte, schrieb eine feste Adresse (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%.

Das Schreib-Primitiv und sein Seiteneffekt

Die UAF wird über rb_erase_cached Case 1-left angetrieben. Das ergibt zwei Stores, nicht einen:``` *(write_target) = write_value // the store you aim *(write_value + 0x08) = write_target // unavoidable side effect

root@kitploit:~
`write_value` muss 8-Byte-ausgerichtet sein, wobei Bit 0 gelöscht ist — es ist entweder `0` oder eine gültige Kernel-Adresse. **Deshalb kann `g_boot_state` mit diesem Primitiv nicht gesetzt werden**: Das Byte, das zu `1` werden muss, hat sein niederwertigstes Bit durch die Ausrichtungsanforderung auf `0` gezwungen, und `write_value` ist dieselbe Größe wie die Adresse, an der der Seiteneffekt landet.

### Der Seiteneffekt schreibt in das, worauf `write_value` zeigt

`write_value` ist sowohl *der gespeicherte Wert* als auch *die Adresse, an die der Seiteneffekt schreibt* (bei `+8`). Richte es auf ein globales Kernel-Objekt und du korrumpierst dieses Objekt.

**W7 tat genau dies früher** — indem es `write_value` auf den `init_cred`-Alias richtete — und es ist im Readback sichtbar. Aus `out/t5_w7_778.txt`:```
shape shift=0 wps=5: in[0]=0xffffff802a7e0be0 (write_value) in[2]=0xffffff8800cdd178 (write_target)
W7[W7] write_target= 0xffffff8800cdd178
Uid:	0	0	4294967176	0

write_value war der init_cred-Alias und write_target war child_task+0x778. init_cred+8 ist gid/suid, also speicherte der Seiteneffekt 0xffffff8800cdd178 dort: init_cred.gid = 0x00cdd178 und init_cred.suid = 0xffffff88 = 4294967176 — genau das 4. awk-Feld der Uid:-Zeile oben. Das Nullsetzen von init_cred+8 reparierte es (out/t5_repair.txt: Uid: 0 0 4294967176 0 → ), was alles war, was „W7 stage 3" jemals war.

Dieser Pfad wird nun im Code verweigert. V12_W7_INIT_CRED=1 bricht mit einer Erklärung ab, es sei denn V12_ALLOW_INIT_CRED=1 ist ebenfalls gesetzt, und die W2/W6/LTC-Pfade fallen nicht mehr auf init_cred zurück, wenn die private cred-Seite fehlt — sie brechen stattdessen ab. Der Standard, und der einzig vernünftige Pfad, ist die gesprayte cred-Seite.

Der Seiteneffekt selbst kann nicht vermieden werden: write_value muss der cred- Zeiger sein, also wird cred+8 immer mit dem Schreibziel überschrieben. Nur seine Position ist eine Wahl — und die Reparatur ist nun ein lokales Nullsetzen von cred_page+8 (stage 3), kein Schreiben in ein globales Objekt.

Eine fehlerhafte groups=-Ausgabe ist ein separates Symptom, nicht dieses. Es wurde in einem Lauf beobachtet, in dem gid und egid sauber zurückgelesen wurden, also kann es nicht vom init_cred+8-Seiteneffekt stammen; es deutet auf das eigene group_info- Feld des gefälschten cred hin. Siehe evidence/notes.md §10.6.

★ Eine Single-Field-Landung hinterlässt die Task divergent — und das ist ein hartes BUG_ON

Das Primitiv schreibt auf genau eine Adresse pro Durchlauf. task+0x778 (real_cred) und task+0x780 (cred) sind zwei separate Adressen, also hinterlässt jeder gelandete 0x778-only- oder 0x780-only-Schreibvorgang die Task mit cred != real_cred — ein Divergenzzustand.

Auf diesem Image ist dieser Zustand ein harter Panic, keine Warnung. commit_creds beginnt mit BUG_ON(task->cred != task->real_cred):``` commit_creds @0xffffffc008186784 0x1867a4 ldr x19, [x20, #0x778] ; old = task->real_cred 0x1867a8 ldr x8, [x20, #0x780] ; task->cred 0x1867ac cmp x8, x19 0x1867b0 b.ne #0xffffffc008186b68 0x186b68 brk #0x800 ; == BUG()

root@kitploit:~
und der Kernel wird mit **`CONFIG_PANIC_ON_OOPS=y`** gebaut (`CONFIG_PANIC_ON_OOPS_VALUE=1`).
`__put_cred @0xffffffc008185530` trägt dieselbe Familie von Assertions
(`usage != 0` → BUG; `cred == current->cred` / `current->real_cred` → BUG).

Die Divergenz ist also *latent* — sie tut nichts, solange das Opfer nur spinnt —
bis **irgendein** `commit_creds` auf dieser Task geschieht: `setresuid` / `setresgid` /
`setuid` / `setgid` / `capset`, oder **`execve` via `install_exec_creds`**.

> **⛔ Zurückgezogen (2026-09-18 spät): dies ist NICHT der Reboot-Mechanismus.**
>
> Eine frühere Revision dieses Abschnitts nannte die Divergenz „den führenden Mechanismus-
> kandidaten für die Reboots" und sagte, sie „erklärt die Shape-Aufteilung". Das tut sie nicht,
> und der Grund ist jetzt gemessen statt argumentiert:
>
> * `commit_creds` nimmt seine Task von **`current`** — `0x1867a0 mrs x20, sp_el0`.
>   Seine Signatur ist `commit_creds(struct cred *new)`; es gibt kein Task-Argument.
>   Eine Divergenz ist also nur dann von Bedeutung, wenn die Task, die sie **hält**, selbst
>   `commit_creds` aufruft.
> * Die rebootenden Läufe waren alle `V12_NO_EXEC=1` (wörtlich angegeben bei
>   `run3_0445.log:18`, `run9_0606.log:20`, `run10_0616.log:24`), sodass das Opfer
>   nie `execve` ausführte und `commit_creds` überhaupt nie erreichte.
> * Lauf 10 hatte überhaupt keinen Poke (`grep -c poke` = 0).
> * `exit_creds` nullt **beide** Zeiger vor `put_cred`
>   (`0x185cb8 str xzr,[x19,#0x778]`; `0x185d24 str xzr,[x19,#0x780]`), sodass das
>   `_exit(0)` des Opfers die Divergenz **auslöscht**, statt auf sie zu treten.
>
> ⇒ In jenen Läufen war die Divergenz **inert**. Das `BUG_ON` ist real, aber es ist eine
> Landmine, die nicht hochgegangen ist. „Shape A rebootet nie" ist wieder eine
> Korrelation. Was die Landmine tatsächlich einschränkt, ist **Laundering**, weil
> `setresgid`/`setresuid` selbst `commit_creds` aufrufen.
>
> **Die Größe, die die Chains tatsächlich trennt, ist Zeiger-Gleichheit, und das
> bedeutet, die beiden Schüsse müssen EINEN Wert schreiben** — siehe den nächsten Unterabschnitt.

### ★★★ Die beiden Schüsse müssen den *selben* Wert schreiben — nicht bloß beide landen

`BUG_ON` vergleicht **Zeiger**. Zwei Seiten, die beide `uid 0` tragen, sind immer noch zwei
verschiedene Objekte. Die Captures machen den Unterschied konkret:

| chain | 0x778-Schuss | 0x780-Schuss | Zeiger |
|---|---|---|---|
| alt (`t5loop.sh MODE=CRED`) | `in[0]=0xffffff802a7e0be0` | `in[0]=0xffffff802a7e0be0` | **gleich** → ksud geladen, Manager 120 s am Leben |
| neu (`run_bootA.sh`) | `0xffffff88679bade0` (Lauf 9) | `0xffffff8785d6ade0` | **verschieden** → divergent selbst wenn beide gelandet sind |

`tools/t5loop.sh` wendet **ein** `$ENVV` auf **jeden** Offset an, sodass `MODE=CRED`
beide Schüsse *konstruktionsbedingt* identisch machte. `run_bootA.sh` feuerte Schritt 5 und
Schritt 6 jeweils mit leerem `$extra`, sodass jeder seine **eigene** Seite sprayte.

⇒ Die Anforderung ist **„beide Schüsse schreiben denselben Wert"**. `run_bootA.sh` erzwingt
dies jetzt (`SAME_VALUE=1`, der Standard): Schritt 6 verwendet den in Schritt 5 beobachteten
`write_value` wörtlich wieder und **weigert sich überhaupt zu feuern**, wenn er diesen
Wert nicht wiederherstellen kann — denn Feuern würde ein divergentes Paar erzeugen.

⚠ `HOLD` muss den zweiten Schuss überleben. Wenn das PIN-Kind des ersten Schusses zuerst stirbt,
wird die Seite freigegeben und neu alloziert, und „selber Wert" wird zu einem Dangling Pointer.
Der Standard `HOLD=20` ist **zu kurz**; verwende `HOLD=600`. Dies ist jetzt der *Standard*,
wann immer `SAME_VALUE=1` — die alte unbedingte 20 s bedeutete, dass die Standard-
konfiguration selbst die Falle war — und ein explizit kurzes `HOLD` mit
`SAME_VALUE=1` warnt jetzt laut, statt still einen Dangling Pointer zu erzeugen.

⚠ `CONTROL=1` änderte früher **nur Schritt 5**, sodass es
`(init_cred, frische Seite)` erzeugte — ein divergentes Paar — während diese Datei behauptete, es
reproduziere Zelle 2. Behoben: es setzt jetzt beide Schüsse auf `init_cred`. (Die Kosten von Zelle 2
bleiben bestehen: der Seiteneffekt korrumpiert `init_cred+8` global, was
`Uid: 0 0 4294967176 0` ist.)

**Konsequenzen für alles, was die Credential launderen will**
(`setresgid` + `setresuid`, um die gesprayte Seite gegen ein echtes `struct cred` zu tauschen):

- Der Mechanismus ist real und verifiziert — `commit_creds` schreibt `x21` in **beide**
  `task+0x778` und `task+0x780` (`0x186998` / `0x1869a0`), sodass ein Aufruf die
  Aufteilung dauerhaft repariert; `prepare_creds @0xffffffc008186070` ist
  `kmem_cache_alloc(cred_jar)` + `memcpy(new, task->cred, 0xA8)` +
  `security_prepare_creds(...)`, und 147/149 stehen beide auf der Ausnahmeliste des Guards.
- **Aber seine Vorbedingung ist das Gegenteil von „den 0x778-Schuss überspringen".** Das Launder
  selbst ruft `commit_creds` auf, also darf es nur ausgelöst werden, wenn **beide** Zeiger
  bereits denselben Wert halten.
- `V12_LAUNDER=1` ist an **zwei** Dinge geknüpft, und das erste ist keine Beobachtung:
  1. **`V12_W7_SAME_VALUE=1`** — die *Provenienz*-Tatsache, dass beiden Schüssen
     derselbe Wert gegeben wurde. Ohne Read-Primitive ist Zeiger-Identität unbeobachtbar, also
     kann dies nicht durch eine bessere Userspace-Prüfung ersetzt werden; es muss deklariert werden.
  2. `consistent=1` — die `0x780`-Sicht (`getuid()`) stimmt mit der `0x778`-Sicht
     (`/proc/self/status` `Uid:`) überein. **Notwendig, aber für sich allein nicht ausreichend**:
     zwei verschiedene Seiten, die beide `uid 0` tragen, lesen sich gleich, während die Zeiger
     verschieden sind — was genau der Fall ist, den der Runner früher erzeugte. Gegeben (1) wird
     es ausreichend: übereinstimmend + selber Wert ⇒ beide landeten auf derselben Seite.
  Schlägt eine der Prüfungen fehl ⇒ verweigern, und die Vier-Fälle-Tabelle geht in die Evidenz ein.
  Die LT-Report-Zeile gibt beide Sichten aus (`uid=` / `real_uid=` / `consistent=`) plus
  `same_value_declared=`, sodass der Zustand gelesen und nicht abgeleitet wird.

### ★ Instrument 1 — der Seiteneffekt ist ein STAMP, der auf das Ziel gerichtet ist

`*(write_value + 8) = write_target`, und `cred+8` / `cred+0xc` sind `gid` / `suid`,
sodass ein 8-Byte-Store über beide landet:```
cred.gid  = low32(write_target)
cred.suid = hi32(write_target)

Das ist eine Messung, kein Modell. out/t5_w7_778.txt enthält write_target = 0xffffff8800cdd178 und Uid: 0 0 4294967176 0, wobei 4294967176 = 0xffffff88 = hi32(write_target); notes.md §11 hält die andere Hälfte fest, init_cred.gid = 0x00cdd178 = low32(write_target).

Zwei Verwendungen:

  1. Es ist das Landing-Orakel für task+0x778. /proc/<pid>/status liest real_cred = task+0x778 — genau das gerade installierte Cred —, sodass der Stempel direkt aus dem Userspace lesbar ist. Lies ihn vor Stufe 3: Die Reparatur nullt cred+8 und löscht ihn (notes.md §11s t5_repair.txt liest 4294967176 vor einer erfolgreichen Reparatur und 0 danach).
  2. Es ist ein zweiter, unabhängiger Grund, warum das Launder-Gate zwei verschiedene Seiten fangen kann. Die UID-Hälfte allein kann das nicht: Jede Seite mit uid 0 liest 0, also melden zwei verschiedene Seiten beide „konsistent". Aber low32(T+0x778) und low32(T+0x780) unterscheiden sich um genau 8, sodass bei zwei Seiten getgid() (aus ) und (aus ) nicht übereinstimmen — und vergleicht gid ebenso wie uid.

⇒ V12_W7_SAME_VALUE ist das zweite Gate, nicht das einzige. Es ist weiterhin wichtig: Der Stempel unterscheidet nur, wenn beide Seiteneffekte ausgelöst haben, also schließt die Same-Value-Regel dieses verbleibende Loch. Und beachte, was ein Gate ist — ein Detektor, kein Verhinderer. Es kann nur ablehnen; es lässt die Task für den Rest des Boots divergent. Die Same-Value-Regel ist das, was das Paar korrekt macht, was die alte Kette hatte und was nötig ist, um execve überhaupt zu erreichen.

★ Instrument 2 — probe_state ist KEIN Landing-Kriterium

Es lag in diesem Projekt dreimal falsch: W1 landete auf dem Global und meldete R; das D von Lauf 12 zielte auf ein Global statt auf ein Cred; und das R von Lauf 11 wurde in eine Tabelle geschrieben, als wäre es ein Landing (run11_w778r1_miss.txt und w7_w7781.txt aus Lauf 7 sind Zeile für Zeile isomorph — beide probe_state = R, probe_done = 0). Verwende ein zielspezifisches Orakel:

run_bootA.sh verwendet jetzt den Stempel für Stufe 1 — was ROUNDS>1-Wiederholungen auf task+0x778 sinnvoll macht, da eine fehlgeschlagene Runde lesbar statt abgeleitet ist — und es wird Stufe 2 nicht auslösen, wenn Stufe 1 nicht gelandet ist.

⛔ Sage „awk-Feld", niemals „3. Feld". uid_line gibt auch das Label aus (Uid: 0 0 4294967176 0), also ist $1 von awk "Uid:" und die vier ID-Werte sind $2..$5: $2=uid $3=euid $4=suid $5=fsuid. Der Stempel sitzt bei cred+8, d. h. gid (low32) und suid (hi32) — also ist es Gid: $2 und Uid: . Ihn „das dritte Feld" zu nennen (was zählt und wie §11 es formuliert) verleitet den Code dazu, zu lesen, was = auf dem Fake-Cred ist und niemals gleich sein kann. Dieser Off-by-one war hier vorhanden: gab „kein Stempel" für einen Schuss zurück, der gelandet war, sodass Stufe 2 nie auslöste und das Launder-Gate für immer ablehnte — , weil „kein Stempel" auch das normale Ergebnis eines echten Fehlschlags ist.

Ein Kriterium, das nie gegen eine bekannte positive Probe getestet wird, ist kein Kriterium, sondern eine Vermutung — und diese Fehlerklasse (dieser Off-by-one, probe_state, dmesg -w, der leere klog.host, das leere Readback) zeigt sich immer als „nichts ist passiert", was ebenfalls ein legitimes experimentelles Ergebnis ist. Daher ist die Prüfung jetzt doppelt abgesichert:

  • stamp_selftest() läuft im Preflight und beendet mit exit 9 bei Fehler, wobei die gleichen Extraktionsfunktionen, die das Gate verwendet, gegen die gemessenen Werte aus out/t5_w7_778.txt laufen (write_target = 0xffffff8800cdd178 → Uid $4 = 4294967176, Gid $2 = 13488504) plus negative und unlesbare Proben. Ein Selbsttest, der die Prüfung neu implementiert, beweist nichts, daher ist die Feldextraktion in uid_suid_field / gid_gid_field herausgezogen.
  • tools/test_stamp_criterion.sh — dasselbe als eigenständiger Regressionstest, der die echten Funktionen aus extrahiert.

stamp_ok() gibt drei Zustände zurück, weil „kann nicht lesen" nicht „kein Stempel" ist (diese Vermischung ließ Lauf 13 wie „keine Änderung" aussehen): 0 = vorhanden, 1 = lesbar und kein Stempel, 2 = UNLESBAR. Und wenn es 1 zurückgibt, während probe_state = D, gibt der Runner ⛔ ORACLE INCONSISTENT aus — „geh das Kriterium prüfen" — statt der Meldung „nicht gelandet", was den Operator an eine völlig andere Stelle schickt (einen frischen Boot oder eine Hit-Rate-Jagd).

Vollständige Herleitung: evidence/2026-09-18-divergence-is-latent.md und evidence/2026-09-18-cred-launder-verification.md (letzteres §2.3 ist in situ zurückgezogen). Der Selbstcheck zur Überlappung der Write-Shape ist offline geschlossen: Die Shape-Wörter liegen im fd_set-Grid auf dem Kernel-Stack, während der Seiteneffekt innerhalb der gesprayten Seite landet, sodass sich die beiden in keiner der beiden Shapes überlappen können.

Erkennungspfade

Drei unabhängige Melder. Keiner ist ein Fallback für einen anderen, und nur Pfad 1 kann die aufrufende Task töten.

Pfad 2 löst bei execve aus, nicht bei Credential-Änderung, und er ist ein separater Codepfad von Pfad 1. Er meldet über kevent_send_to_user, also ist, was als Nächstes passiert, die Entscheidung eines Userspace-Daemons, nicht des Kernels.

Die Prüfung erfolgt auf dem Pfad des gerade exec'ten Images, daher reicht es nicht, die Payload des Loaders per memfd zu laden: Wenn der Loader selbst aus /data/local/tmp exec't wird, meldet bereits dieses erste execve. d_path() auf einem memfd ist /memfd:…, also muss der Loader selbst durch einen memfd exec't werden — V12_EXEC_MEMFD ist jetzt genau aus diesem Grund standardmäßig aktiv. Das alte Verhalten ist in RUN 4 sichtbar:``` LT child exec /data/local/tmp/glx12 (4 args) <- execve of a /data path while uid=0 LT child memfd loaded 5014624 bytes (fd=5) <- memfd only protected the second image

root@kitploit:~
Greppbare Marker für Pfad 2:```
[ROOTCHECK-EXEC-INFO]:common %s result %s      with  "execve_report" / "execve_block"
%d,path@@%s                                    kevent payload fragment

Da Pfad 2 und Pfad 3 nur über kevent berichten, schließt „kein [ROOTCHECK-*] im Kernel-Log" nicht aus, dass einer von ihnen ausgelöst wurde. Diese Schlussfolgerung erfordert den Userspace-Empfänger, den wir nicht lokalisiert haben.

Watchdog — oplus_security_guard.ko

sys_enter-Cache:```asm ldrsw x8, [x1, #0x118] ; regs->syscallno mrs x9, sp_el0 ; current ldr x10, [x9, #0x780] ; cred str x8, [x9, #0xdf8] ldr w8, [x10, #4] → str x8, [x9, #0xe00] ; uid ldr w8, [x10, #0x14] → str x8, [x9, #0xe08] ; euid ldr w8, [x10, #8] → str x8, [x9, #0xe10] ; gid ldr w8, [x10, #0x18] → str x8, [x9, #0xe18] ; egid

root@kitploit:~
`sys_exit`-Prüfung:```asm
ldr   x0,  [x8, #0xe00]         ; cached uid
cbz   x0, #0x48c                ; cached uid == 0 → return
adrp  x9, #0 ; ldrb w9, [x9]    ; g_boot_state
tbnz  w9, #0, #0x48c            ; is_unlocked → return
ldr   x9,  [x8, #0x780]         ; cred
ldr   w3,  [x8, #0xdf8]         ; cached syscallno
cmp   x0, w10 ; b.hi #0x468     ; uid descending → kill path
                                ; euid / gid / egid, same shape
ldr   x9,  [x8, #8]             ; addr_limit
cmp   x9,  #0x8000000001
b.lo  #0x48c                    ; addr_limit != KERNEL_DS → return
sub   w9,  w3, #0x8f            ; syscallno - 143
cmp   w9,  #0x47
b.hi  #0x4a0                    ; outside 143..214 → kill
ldrsw x12, [x10, x9, lsl #2]    ; jmp table @ .rodata+0
br    x11
0x48c: ret
0x4a0: bl oplus_root_check_succ ; printk + kevent_send_to_user
       bl oplus_root_killed     ; printk + do_exit(SIGKILL)

Hinweis zur Kontrollfluss-Reihenfolge (wichtig für die Exploit-Reihenfolge). Die vier Vergleiche der absteigenden Kante verzweigen direkt zu 0x468, dem Dispatch der Syscall-Nummer — sie fallen nicht durch das addr_limit-Gate. 0x454–0x464 wird nur erreicht, wenn keine id abgestiegen ist. Der Dispatch wird also betreten, wenn entweder eine id abgestiegen ist oder addr_limit == KERNEL_DS; er ist nicht durch addr_limit gegated.

Konsequenzen:

  • Der Killer feuert auf den Syscall während dessen sich die Credentials geändert haben — denjenigen, dessen sys_enter noch die alte uid gecacht hat. Wenn die Task bereits uid=0 ist, wenn ein Syscall eintritt (0x400 cbz), kehrt der Hook zurück und bleibt von da an blind.
  • Daher ist eine Credential-Änderung ohne Berührung des Moduls überlebbar: Lass eine andere Task den Schreibvorgang ausführen, während das Opfer im User-Space spinnt, oder leite die Änderung durch einen der 12 ausgenommenen Syscalls. Siehe delivery/外部建议评审_2026-09-18.md.

g_boot_state — 1 Byte .data..ro_after_init, gesetzt beim Modul-Init aus verified_bootstate via strstr. is_unlocked() = LDRB + RET.

Modul-VA-Schreibzugriffe schlagen fehl (CONFIG_STRICT_MODULE_RWX=y) — verwende den Physmap-Alias 0xffffff80….

Report-Payload: $$sys_call_number@@%d, $$set_id_flag@@%d, $$addr_limit@@%lx, $$enforce@@%d.

Ausgenommene Syscalls — .rodata+0, Indizes 143–214

Verbleibende 60 Einträge → Report + Kill.

Blockiere keinen Thread auf sendmsg (211), während sich seine Credentials ändern. Es ist nicht in der Tabelle, also wird der Thread reported und gekillt. Die einzigen Syscalls, in denen es sicher ist, zu diesem Zweck blockiert zu sein, sind die zwölf oben: setregid, setgid, setreuid, setuid, setresuid, setresgid, connect, getsockname, setsockopt, shutdown, readahead, brk.

Korrektur (2026-09-18). Eine frühere Revision dieser Tabelle beschriftete jeden Eintrag um eins niedriger als die echte arm64-Syscall-Nummer (146 wurde setresuid genannt; es ist setuid — setresuid ist 147). Die Nummern waren immer richtig; nur die Namen waren falsch. Namen werden jetzt aus sys_call_table @ 0xffffffc00a13d8c0 im Kernel-Image dieses Geräts aufgelöst. Insbesondere sendmsg (211), munmap (215), getsockopt (209) und getpeername (205) sind nicht ausgenommen — einen Thread in einem dieser Syscalls zu blockieren, während sich seine Credentials ändern, ist ein Kill, kein Pass. Neu generieren mit tools/gen_exempt_table.py.

Heap-Spray-Detektor — oplus_secure_harden.ko

root@kitploit:~
`entry_handler_setxattr` → `oplus_heapspray_check(7)`, `entry_handler_cpuinfo` → `(6)`, beide bedingungslos.

`oplus_heapspray_check` — Zähler `heapspary_ip4` / `ip6` / `xttr` / `cpuinfo`, `ktime_get_real_ts64`-Delta, 7 Report-Stellen (`snprintf` + `printk` + `kevent_send_to_user`), abhängig von `verified_bootstate`.

### Umgehung

| Primitive | |
|---|---|
| `setsockopt` optname 46 `MCAST_JOIN_SOURCE_GROUP` | nicht gefiltert |
| `setsockopt` level `SOL_IPV6` (41) | nicht gefiltert, wenn der Filter `level` liest |
| `setxattr` | wird immer gezählt |
| `/proc/cpuinfo` | wird immer gezählt |
| `socket()` / `socketpair()` | nicht gehookt |
| `sendmsg`, `pipe`, `memfd`, `add_key`, `io_uring`, mmap | nicht gehookt |

## Konfiguration```
CONFIG_CFI_CLANG=y
CONFIG_PTR_AUTH=y
CONFIG_SHADOW_CALL_STACK=y
CONFIG_STRICT_MODULE_RWX=y
CONFIG_STATIC_USERMODEHELPER=y
CONFIG_STATIC_USERMODEHELPER_PATH=""
CONFIG_SET_FS=y
CONFIG_RANDOMIZE_BASE=y
CONFIG_RANDOMIZE_MODULE_REGION_FULL=n
CONFIG_UNMAP_KERNEL_AT_EL0=y
CONFIG_ARM64_VA_BITS=39
CONFIG_ARM64_SW_TTBR0_PAN=y
CONFIG_SLAB_FREELIST_RANDOM=y
CONFIG_SLAB_FREELIST_HARDENED=y
CONFIG_INIT_ON_ALLOC_DEFAULT_ON=y
CONFIG_RANDOM_KMALLOC_CACHES=n
CONFIG_USER_NS=n
CONFIG_NF_TABLES=n
CONFIG_SYSVIPC=n
CONFIG_ANDROID_BINDER_IPC=y
CONFIG_KASAN=y

perf_event_paranoid = -1

Build

NDK r28c. -O1 / API 26 / -D__ARM=1 sind fest vorgegeben — sie erhalten die Reclaim-Stackframe-Geometrie (delta=0-Kalibrierung). Jede Änderung daran erfordert eine erneute Kalibrierung auf dem Gerät.```bash export ANDROID_NDK_HOME=/path/to/android-ndk-r28c make # → exploit_guard ./build.sh # same, with NDK auto-detection

root@kitploit:~
Manuell:```bash
"$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android26-clang" \
  -D__ARM=1 -O1 -Wall -Wextra -pthread -Isrc/core -Isrc/devices/pfem10 \
  -o exploit_guard src/core/exploit.c

CI-Builds bei jedem Push (.github/workflows/build.yml, Ubuntu + NDK r28c, Artefakt exploit_guard).

Einrichtung```bash

adb push exploit_guard /data/local/tmp/e adb shell chmod 755 /data/local/tmp/e adb shell /data/local/tmp/e

root@kitploit:~
## Dateien```
src/core/                 exploit.c  payload.c  payload.h  fdset_map.h
src/lib/                  KernelSnitch — kernelsnitch.h  futex_hash.h  timeutils.h  utils.h
src/devices/pfem10/       pfem10_target.h
model/                    model.c — host-side rtmutex chain-walk model
tools/                    kdis.py  kdis_ko.py  kdis_ko_reloc.py  gen_guard_disasm.py
                          gen_exempt_table.py  mod_layout.py  sct_dump.py
                          find_task_off.py  slide_resolve.py
                          test_stamp_criterion.sh   regression test for the 0x778
                                                    landing criterion (run it after
                                                    touching uid_line/gid_line)
artifacts/                guard_post_handler.s   kill chain, relocations resolved
                          guard_relocs.txt       raw .text relocation dump
                          guard_disasm.txt       guard + heap-spray detector
                          guard_exempt_table.txt 72-slot jump table, real names
evidence/                 kill.log  notes.md — device captures and their limits
Makefile  build.sh        exploit build (-O1, API 26, NDK r28c)
run.sh                    device-side run orchestration (retry across reboots)
.github/workflows/        build.yml — cloud build + artifact

Beweise

evidence/kill.log — wörtliche adb shell-Transkripte von vier Root-Läufen: die vollständige Zeitachse, der Moment, in dem die uid der Ziel-Task 0 wird, das Laden von kernelsu.ko und der Zustand danach. Zuerst den Header-Block lesen: Er listet auf, was die Datei nicht enthält und warum.

evidence/notes.md — die Kernel-Seite. Moduladressen und welche /proc-Kanäle in welchem SELinux-Zustand funktionieren; die vollständige g_boot_state- Herleitung einschließlich des strstr-Schlüssels; die korrigierte Exempt-Tabelle; das Capture- Rezept, das die fehlende Kernel-Hälfte erzeugen würde; und eine Liste dessen, was noch offen ist.

evidence/2026-09-18-bootA/ — der erste Gerätelauf des aktuellen Designs, 13 Boots. Die Reboots sind geordnet (bootreason=reboot) und es wurde noch nie eine Panic-Zeile erfasst — aber das ist als Stichprobe von eins zu lesen, nicht von dreizehn. Von den vier Läufen, die rebooteten, speicherten zwei ein leeres klog.host, einer speicherte das Log des nächsten Boots, und nur ein Fenster kann plausibel seinen eigenen Reboot eingrenzen. Ebenso sind bei einem Lauf probe_state und Victim- Readback beide leer (das Gerät war bereits weg), sodass er keine Information darüber trägt, ob sein Write gelandet ist — ein leeres Feld ist keine „keine Änderung". Das Verzeichnis dokumentiert außerdem den Methodikfehler, den es zu kennen gilt: dmesg -w ist auf diesem Gerät ein No-op (toybox gibt einmal aus und beendet sich), sodass ein früherer Lauf im Kernel-Log nur die Vor-Capture-Historie enthielt — „kein [ROOTCHECK-*]" war kein Beweis für irgendetwas. evidence/notes.md §6 enthält das korrigierte Poll-and-Stream-to-Host-Rezept.

evidence/2026-09-18-cred-launder-verification.md — Verifikation des Credential-Laundering-Vorschlags gegen die eigene Disassembly dieses Images (nicht gegen generischen 5.10-Quellcode): der commit_creds-Doppel-Store, die prepare_creds-Allokation und das gemessene sizeof(struct cred) = 0xA8, die oben genannte BUG_ON(cred != real_cred)-Divergenzgefahr, der geschlossene Write-Shape-Overlap-Selbstcheck und das Evidenzabdeckungs-Audit der 13 Boots. §2.3 ist in place widerrufen — die Divergenz ist latent, nicht der Reboot- Mechanismus.

evidence/2026-09-18-divergence-is-latent.md — die Verifikation der zweiten Runde. commit_creds nimmt seine Task von current (0x1867a0 mrs x20, sp_el0), exit_creds nullt beide Pointer vor put_cred, und die Roh-Captures zeigen, dass die alte Kette einen identischen Wert (0xffffff802a7e0be0) in beide Slots schrieb, während die neue Kette zwei verschiedene Seiten schrieb. Das Kriterium ist also Pointer-Gleichheit — „beide Schüsse schreiben denselben Wert", nicht „beide Schüsse landen".

postreboot_forensics.sh — Reboot-Forensik, die nicht vom Poller abhängt. Das Kriterium ist eine einzige Bedingung: CONFIG_PSTORE_CONSOLE=y lässt panic() den Konsolen-Tail in ramoops bei kmsg_dump(KMSG_DUMP_PANIC) schreiben — vor jedem Reset —, sodass es irrelevant ist, ob die Box danach rebootet oder hängt. Zieht /sys/fs/pstore/, grept nach kernel BUG / __put_cred / cred.c und gibt den Boot-Reason-String aus (History-Einträge haben reboot,shell / bootloader / reboot,edl-Suffixe getragen, sodass der Reason einen Akteur unterscheidet, wo die Epoche es nicht tut).

⚠ „Sauberes bootreason=reboot" nicht als „keine Panic" lesen. Auf QCOM wird ein SoC- Watchdog-Assert über den PMIC-PON-Block zurückgesetzt, sodass panic → panic_timeout=-1 → hang → watchdog → PMIC reset → clean bootreason eine selbstkonsistente Kette ist, die auf der uns vorliegenden Evidenz nicht unterscheidbar von einem Hardware-Reset ist. Das eigene total_17_dump_0_pmic_17 dieses Repos schreibt alle 17 abnormalen Reboots pmic zu, was genau die normale Form des Watchdogs ist, kein Beweis für „nicht der Kernel". bootreason grenzt hier nichts ein; ramoops ist das einzige Kriterium.

Zwei Vorbedingungen, sonst ist das Urteil des Skripts ungültig (铁律 8 — eine No-Signal- Schlussfolgerung erfordert, dass der Kanal zuerst als erreichbar nachgewiesen wird):

  • Dritter Zustand erforderlich. /sys/fs/pstore/* ist root-only, also schlagen unter Enforcing sowohl adb pull als auch cat fehl — und „kann nicht lesen" erzeugt die gleiche Ausgabe wie „gelesen und es war leer". Ein Zwei-Zustands-Skript gibt „pstore ist LEER ⇒ Panic widerlegt" aus einem Kanal aus, den es nie geöffnet hat. Das Skript gibt daher CHANNEL UNREACHABLE aus (ls fehlgeschlagen, oder alle bekannten Einträge konnten nicht gelesen werden, statt nicht zu existieren) und meldet getenforce daneben.
  • Zuerst Null-Test. Ein sauberes adb reboot, gefolgt von einem sofortigen Fetch. Wenn ein bekanntermaßen guter Reboot nichts Lesbares liefert, ist der Kanal nicht bewiesen und jedes spätere „leere pstore" ist kein Beweis. Die Reihenfolge ist wichtig: Das Gerät verschiebt und unlöscht den Datensatz kurz nach dem Boot, also ist die Sequenz reboot → Permissive holen (W1) → das Skript sofort ausführen.

run_bootA.sh — Orchestrierung für diesen einen Boot, in der Reihenfolge, die zählt (0x778 → 0x780 mit demselben Wert → lokale Reparatur des Cred, das tatsächlich installiert wurde → bestätigen → erst dann poke). ADB=/SER=/ BIN_LOCAL= überschreibbar; SAME_VALUE=1 (Standard) erzwingt die Same-Value-Regel, LAUNDER=1 aktiviert das gated Launder, HOLD=600 ist für die Same-Value- Sequenz erforderlich.

Retries sind pro Stage aufgeteilt (R5/R6), weil die beiden Stages entgegengesetzte Risikoprofile haben:

R5 ist standardmäßig ROUNDS, R6 standardmäßig 1.

Der empfohlene Launder-Lauf — der Standard-Sprayed-Page-Pfad, nicht CONTROL=1:```bash LAUNDER=1 R5=3 R6=1 HOLD=600 CHAINWAIT=6000 NODRAIN=1 WATCH=180 ./run_bootA.sh

root@kitploit:~
`CONTROL=1` würde ebenfalls ein konsistentes Paar erzeugen, aber durch das Schreiben des `init_cred`-Zeigers, dessen Nebenwirkung `init_cred+8` **global** korrumpiert — und „das Framework stirbt" ist eines der Dinge, die überwacht werden, sodass das Übertragen eines geräteweiten Fehlers in den Hintergrund der Messung genau die Ablesung verfälscht, für die dieser Lauf existiert. Der Pfad über die gesprayte Seite kostet nur „beide Schüsse müssen treffen", wofür `R5=3` da ist. `CONTROL=1` bleibt das einzige *bewiesene* konsistente Paar und dient als Kontrolle, nicht als empfohlene Konfiguration.

**Welche Seite installiert wurde, wird aus der unbedingten Zeile ausgelesen.** `run_w7` gibt den Schreibwert auf zwei Zeilen aus, und nur eine davon ist unbedingt:```
L1793  W7[..] write value = private cred page 0x..   — spray path only
L1802  W7[..] write_value = 0x..                     — after the if/else, ALL paths

Der Runner passte früher auf die erste Formulierung, sodass bei CONTROL=1 die Extraktion leer zurückkam, if [ -n "$CRED" ] die Reparatur übersprang und der eigene [ -n "$CRED" ]-Term der Gleichwertigkeits-Konjunktion ihn bei 0 hielt — das Launder-Gate hätte für immer verweigert. Ein stiller No-Op auf beiden Seiten, von einer Regex, die eine von zwei Ausgabestellen erkannte. Sowohl sie als auch die write_target-Extraktion (die Eingabe des Stamp-Kriteriums) laufen jetzt über wv_from/wt_from, die zusammen mit dem Kriterium selbst durch den Regressionstest abgedeckt werden.

Beide Streams starten vor dem, was sie messen. uid.stream läuft ab Stufe 1; cred.stream beginnt beim Poke, nicht nach dem Watch — der Poke entlässt das Kind in seine NO_EXEC-Report-Schleife, die 240 × 0,5 s = 120 s dauert und dann _exit(0) aufruft (exploit.c: "LT child NO-EXEC mode done (120s)"), sodass die alte Platzierung bei t+~135 s mit der Abtastung begann, nachdem das Kind bereits verschwunden war, genau in dem Fenster, für das das Instrument existiert. Der Runner weigert sich außerdem fortzufahren, wenn stamp_selftest() fehlschlägt, und gibt ORACLE INCONSISTENT aus statt "did not land", wenn Stamp und probe_state nicht übereinstimmen.

artifacts/guard_post_handler.s — die Kill-Chain mit ausgefüllten Relocations. adrp x9, #0 in den älteren Listings ist .data..ro_after_init; bl #0x4ac ist oplus_root_check_succ. Nach dem Abrufen der Vendor-Module von Ihrem eigenen Gerät mit tools/gen_guard_disasm.py neu generieren.

tools/kdis_ko.py — RELA über sh_info abgeglichen; bei diesen Builds liegen .text-Relocs in .rela.text.<func>, sodass namensbasierte Suche nichts zurückgibt.

Related

Project
JoinChang/ghostlock-oneplusReferenzimplementierung; 5.10 compact waiter
NebuSec CyberMeowfiaursprüngliche GhostLock-Forschung

License

GPL-3.0 — siehe LICENSE.

Tool herunterladen
GerätOPPO Find X5 Pro (PFEM10)
SoCSM8450 / Adreno 730
OSColorOS 16.0.3.520 (CN01)
Kernel5.10.236-android12-9-o-gaf2075ad2c06
Bootloadergesperrt, grün
VA_BITS39 — 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=rootfunktioniert — 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 geladenfunktioniert
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=""
FeldOffset
real_cred / cred0x778 / 0x780
gecachte syscallno0xdf8
gecachte uid / euid / gid / egid0xe00 / 0xe08 / 0xe10 / 0xe18
flags
0x0
addr_limit0x8
ttbr00x10
preempt_count0x18
FeldOffsetFeldOffset
uid0x4cap_inheritable0x28
gid0x8cap_permitted0x30
suid0xccap_effective0x38
sgid0x10cap_bset0x40
euid0x14cap_ambient0x48
egid0x18
fsuid / fsgid0x1c / 0x20
Uid: 0 0 0 0
cred
status_gid
real_cred
lt_cred_ids_agree()
ZielLanding-Orakel
task+0x778Uid: 4. awk-Feld = hi32(write_target) und Gid: 2. awk-Feld = low32(write_target) — der obige Stempel; vor Stufe 3 lesen
task+0x780das eigene getuid() des Opfers
global selinux_enforcinggetenforce
probe_state❌ kein Kriterium. Höchstens ein Hinweis auf die Kette; niemals ein Beleg, dass ein Write gelandet ist
$4
Werte
notes.md
$3
euid
0
hi32(write_target)
stamp_ok()
ohne Fehler irgendwo
run_bootA.sh
#HookAuslöserAktion
1oplus_root_check_post_handler, sys_exit-Tracepointirgendeine ID ist abgestiegen, oder addr_limit == KERNEL_DSoplus_root_killed → printk + do_exit(SIGKILL); und oplus_root_check_succ → kevent_send_to_user
2oplus_exe_block_ret_handler, sys_exit aber nur für execve (221)d_path(mm->exe_file) beginnt mit /data, /data/local/tmp, /data/nativetest, /data/nativetest64oplus_RWO_root_check → printk + kevent_send_to_user (kein do_exit)
3oplus_secure_harden kretprobessetsockopt optname ∈ {41,42,48}, setxattr, /proc/cpuinfo, SELinux-Policy-Reloadoplus_heapspray_check → kevent_send_to_user
143 setregid144 setgid145 setreuid146 setuid
147 setresuid149 setresgid203 connect204 getsockname
208 setsockopt210 shutdown213 readahead214 brk
kretprobeHooksFilter
socket_kretprobeip_setsockoptregs[1] ∈ {41, 42, 48}
socket_ip6_kretprobedo_ipv6_setsockoptregs[1] ∈ {41, 42}
cpuinfo_kretprobecpuinfo_open—
setxattr_kretprobesetxattr—
sepolicy_reload_kretprobespolicy_reload—
ldr w8, [x1, #8] ; regs[1]
cmp w8, #0x29 ; 41 IP_MSFILTER
b.eq #0xd58
cmp w8, #0x30 ; 48 MCAST_MSFILTER
b.eq #0xd60
cmp w8, #0x2a ; 42 MCAST_JOIN_GROUP
b.ne #0xd68 ; else → return, no call
bl oplus_heapspray_check
StageRetry-Sicherheit
R5Schritt 5, task+0x778sicher — ein Fehlschlag installiert nichts, und das Stamp-Kriterium macht eine fehlgeschlagene Runde lesbar, sodass ein weiterer Schuss nur ein weiterer Versuch ist. R5=3 hebt die Trefferrate pro Schuss von ~p auf ~1−(1−p)³.
R6Schritt 6, task+0x780nicht sicher und nicht nötig — es feuert nur nachdem Schritt 5 gelandet ist, sodass ein Retry auf eine bereits divergente Task schießt: eine weitere Chance, eine zweite, andere Seite zu landen, ohne Nutzen, da ein Landen das Paar vervollständigt. Bei 1 belassen.