
GhostLock (CVE-2026-43499) OPPO Find X5 Pro (PFEM10) के लिए — OPlus watchdog और heap-spray detector रिवर्स इंजीनियरिंग
English · 中文
ColorOS 16 पर OPPO Find X5 Pro के लिए GhostLock (CVE-2026-43499) पोर्ट। एक uid=0 चाइल्ड प्रोसेस और एक लोडेड kernelsu.ko तक पहुँचता है; रूट प्रोसेस को इंटरसेप्ट किया जाता है।
CVE-2026-43499 — futex PI use-after-free। remove_waiter() current->pi_blocked_on को तब साफ़ करता है जब current requeuer हो, rt_mutex_start_proxy_lock() के -EDEADLK रोलबैक पथ पर।
remove_waiter @ 0xffffffc0081ed254 — pre-fix shape।
"root process survives" पर: evidence/kill.log में रन
uid=0 तक पहुँचते हैं और kernelsu.ko लोड करते हैं, और उस रन में जिसने वास्तव में इसके लिए पोल किया
KernelSU manager प्रोसेस 120 s तक जीवित रहा और kernelsu अभी भी /proc/modules में Live था। बाद के एक रन में वही श्रृंखला
Android framework services को अगम्य छोड़ गई (Can't find service: package/power/input/phone/wifi)
जबकि मॉड्यूल अभी भी Live था। कोई [ROOTCHECK-*] kernel लाइन और कोई $$sys_call_number@@ payload कभी कैप्चर नहीं किया गया, इसलिए बाद के रन की स्थिति का कारण श्रेय नहीं दिया गया। देखें evidence/notes.md
§2.3, §2.4 और §7।
task_struct
thread_info
| Field | Offset |
|---|---|
cred
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
**चरण 2 को चरण 1 का मान स्पष्ट रूप से दिया जाना चाहिए।** चरण 1 और 2 दो
स्वतंत्र प्रक्रियाएँ हैं, प्रत्येक का अपना स्प्रे है, इसलिए "क्रेड पेज को दोनों
स्लॉट में लिखें" एक जाल है: इसे भोलेपन से पढ़ने पर `(pageA, pageB)` उत्पन्न होता है, और चूँकि
`commit_creds` **पॉइंटर्स** की तुलना करता है, वह युग्म विचलित होता है भले ही दोनों लेखन
सफल हों। यह काल्पनिक नहीं है — यह ठीक वही है जो रन 3 और 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 इसलिए stage 2 को V12_W7_VALUE=<stage 1's observed value> के साथ चलाता है और यदि वह मान पुनर्प्राप्त नहीं किया जा सकता है तो उसे बिल्कुल भी चलाने से इनकार कर देता है। HOLD को stage 2 से अधिक समय तक जीवित रहना चाहिए, अन्यथा stage 1 का पृष्ठ मुक्त और पुनःआवंटित हो जाता है और "वही मान" एक डैंगलिंग पॉइंटर बन जाता है। देखें
the same-value rule।
प्रति बूट एक पृष्ठ की मरम्मत होती है। Stage 3 V+8 को शून्य करता है। दो भिन्न पृष्ठों के साथ, दोनों को शून्य करने से gid/suid स्टैम्प (नीचे) मिट जाएगा और एक विचलन को सहमति जैसा दिखाएगा, इसलिए रनर केवल उसी पृष्ठ की मरम्मत करता है जो वास्तव में इंस्टॉल किया गया था और यदि दोनों मान असहमत हों तो रुक जाता है।
cred पृष्ठ payload.c द्वारा बनाया जाता है: सभी आठ id फ़ील्ड शून्य, सभी पाँच capability सेट पूर्ण, और user / user_ns / group_info जो root_user / init_user_ns / init_groups की ओर इंगित हैं। Stage 3 इसलिए मौजूद है क्योंकि लेखन का साइड इफ़ेक्ट हमेशा जिस भी cred को इंस्टॉल करता है उसके cred+8 (gid/suid) को क्लॉबर कर देता है।
init_cred पर — एक स्पष्ट द्विभाजनयहाँ दो अनुभाग पहले एक-दूसरे का खंडन करते थे ("कभी भी ग्लोबल init_cred नहीं" बनाम "CONTROL=1 सेल 2 को पुनरुत्पादित करता है", और सेल 2 है init_cred)। दोनों कथन विभिन्न भूमिकाओं के लिए सत्य हैं:
init_cred पॉइंटर लिखने से साइड इफ़ेक्ट init_cred+8 को वैश्विक रूप से भ्रष्ट कर देता है — init_cred हर कर्नेल थ्रेड द्वारा साझा किया जाता है, और Uid: 0 0 4294967176 0 ठीक वही भ्रष्टता है। कोड इस पथ से इनकार करता है जब तक कि V12_ALLOW_INIT_CRED=1 जानबूझकर सेट न किया गया हो।0xffffff802a7e0be0) लिखा, इसलिए real_cred == cred निर्माण से ही — यही कारण है कि यह execve तक जीवित रहा। CONTROL=1 इसे पुनरुत्पादित करता है। यह एक नियंत्रण है, न कि निर्माण करने योग्य कोई कॉन्फ़िगरेशन।perf leak: PERF_TYPE_SOFTWARE / PERF_COUNT_SW_CPU_CLOCK, PERF_SAMPLE_REGS_INTR, exclude_user=1.
[0xffffff8400000000, 0xffffff90000000) स्वीकार करें, वोट ≥ 15%।
UAF को rb_erase_cached Case 1-left के माध्यम से चलाया जाता है। यह दो स्टोर देता है, एक नहीं:```
*(write_target) = write_value // the store you aim
*(write_value + 0x08) = write_target // unavoidable side effect
`write_value` को 8-बाइट aligned होना चाहिए और bit 0 clear होना चाहिए — यह या तो `0` है या एक
valid kernel address है। **यही कारण है कि `g_boot_state` को इस
primitive से सेट नहीं किया जा सकता**: जिस byte को आपको `1` बनाना है, उसका low bit
alignment requirement के कारण `0` पर forced हो जाता है, और `write_value` वही quantity है जो उस
address के समान है जहाँ side effect पड़ता है।
### Side effect जहाँ भी `write_value` point करता है, वहीं लिखता है
`write_value` दोनों है — *वह value जो store होती है* और *वह address जहाँ side effect
लिखता है* (`+8` पर)। इसे किसी global kernel object पर point करें और आप उस
object को corrupt कर देंगे।
**W7 पहले बिल्कुल यही करता था** — `write_value` को `init_cred` alias पर aim करते हुए —
और यह readback में दिखाई देता है। `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 init_cred alias था और write_target child_task+0x778 था।
init_cred+8 gid/suid है, इसलिए साइड इफेक्ट ने वहाँ 0xffffff8800cdd178
संग्रहीत किया: init_cred.gid = 0x00cdd178 और init_cred.suid = 0xffffff88 = 4294967176 — ठीक ऊपर दी गई Uid: लाइन का चौथा awk फ़ील्ड। init_cred+8 को
शून्य करने से इसे ठीक कर दिया (out/t5_repair.txt: Uid: 0 0 4294967176 0 →
), और "W7 stage 3" बस इतना ही था।
यह पथ अब कोड में अस्वीकृत है। V12_W7_INIT_CRED=1 एक स्पष्टीकरण के साथ
निरस्त हो जाता है जब तक कि V12_ALLOW_INIT_CRED=1 भी सेट न हो, और W2/W6/LTC
पथ अब निजी cred पेज के अनुपस्थित होने पर init_cred पर वापस नहीं गिरते — वे
इसके बजाय निरस्त हो जाते हैं। डिफ़ॉल्ट, और एकमात्र विवेकपूर्ण पथ, स्प्रे किया
गया cred पेज है।
साइड इफेक्ट स्वयं टाला नहीं जा सकता: write_value को ही cred पॉइंटर होना
चाहिए, इसलिए cred+8 हमेशा write target से अधिलेखित हो जाता है। केवल इसका
स्थान एक विकल्प है — और मरम्मत अब cred_page+8 का एक स्थानीय शून्यकरण
(stage 3) है, किसी वैश्विक ऑब्जेक्ट में लेखन नहीं।
एक कचरा
groups=रीडआउट एक अलग लक्षण है, यह वाला नहीं। यह एक ऐसे रन में देखा गया जहाँgidऔरegidसाफ़ पढ़े गए, इसलिए यहinit_cred+8साइड इफेक्ट से नहीं आ सकता; यह नकली cred के स्वयं केgroup_infoफ़ील्ड की ओर इंगित करता है। देखेंevidence/notes.md§10.6।
BUG_ON हैप्रिमिटिव प्रति पास ठीक एक पते पर संग्रहीत करता है। task+0x778
(real_cred) और task+0x780 (cred) दो अलग पते हैं, इसलिए कोई भी लैंड किया
गया केवल-0x778 या केवल-0x780 लेखन कार्य को cred != real_cred के साथ छोड़
देता है — एक विचलन अवस्था।
इस इमेज पर वह अवस्था एक कठोर पैनिक है, चेतावनी नहीं। commit_creds
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()
और kernel **`CONFIG_PANIC_ON_OOPS=y`** (`CONFIG_PANIC_ON_OOPS_VALUE=1`) के साथ build किया गया है।
`__put_cred @0xffffffc008185530` उसी परिवार के assertions रखता है
(`usage != 0` → BUG; `cred == current->cred` / `current->real_cred` → BUG)।
तो divergence *latent* है — जब तक victim बस spin करता है तब तक यह कुछ नहीं करता —
जब तक उस task पर **कोई भी** `commit_creds` नहीं होता: `setresuid` / `setresgid` /
`setuid` / `setgid` / `capset`, या **`install_exec_creds` के माध्यम से `execve`**।
> **⛔ वापस लिया गया (2026-09-18 देर से): यह reboot mechanism नहीं है।**
>
> इस section के एक पहले के revision ने divergence को "reboots के लिए प्रमुख mechanism
> candidate" कहा था और कहा था कि यह "shape split को समझाता है"। यह नहीं करता,
> और इसका कारण अब बहस के बजाय मापा गया है:
>
> * `commit_creds` अपना task **`current`** से लेता है — `0x1867a0 mrs x20, sp_el0`।
> इसका signature है `commit_creds(struct cred *new)`; कोई task argument नहीं है।
> तो divergence तभी मायने रखता है जब इसे **धारण करने वाला** task स्वयं
> `commit_creds` call करे।
> * Rebooting runs सभी `V12_NO_EXEC=1` थे (शब्दशः उद्धृत:
> `run3_0445.log:18`, `run9_0606.log:20`, `run10_0616.log:24`), इसलिए victim ने
> कभी `execve` जारी नहीं किया और कभी `commit_creds` तक पहुँचा ही नहीं।
> * Run 10 में कोई poke बिल्कुल नहीं था (`grep -c poke` = 0)।
> * `exit_creds` `put_cred` से पहले **दोनों** pointers को null करता है
> (`0x185cb8 str xzr,[x19,#0x778]`; `0x185d24 str xzr,[x19,#0x780]`), इसलिए
> victim का `_exit(0)` divergence को **मिटा** देता है, उस पर ट्रिप करने के बजाय।
>
> ⇒ उन runs में divergence **inert** था। `BUG_ON` वास्तविक है, लेकिन यह एक
> landmine है जो अभी फटा नहीं है। "Shape A never reboots" फिर से एक correlation
> बन जाता है। Landmine वास्तव में जिस चीज़ को constrain करता है वह है
> **laundering**, क्योंकि `setresgid`/`setresuid` स्वयं `commit_creds` call करते हैं।
>
> **वह मात्रा जो वास्तव में chains को अलग करती है वह pointer equality है, और इसका
> मतलब है कि दोनों shots को ONE value लिखनी चाहिए** — अगला subsection देखें।
### ★★★ दोनों shots को *समान* value लिखनी चाहिए — केवल दोनों का land होना पर्याप्त नहीं
`BUG_ON` **pointers** की तुलना करता है। दो pages जो दोनों `uid 0` रखते हैं, फिर भी
दो अलग objects हैं। Captures इस अंतर को ठोस बनाते हैं:
| chain | 0x778 shot | 0x780 shot | pointers |
|---|---|---|---|
| old (`t5loop.sh MODE=CRED`) | `in[0]=0xffffff802a7e0be0` | `in[0]=0xffffff802a7e0be0` | **equal** → ksud loaded, manager alive 120 s |
| new (`run_bootA.sh`) | `0xffffff88679bade0` (run 9) | `0xffffff8785d6ade0` | **differ** → divergent even with both landed |
`tools/t5loop.sh` **हर** offset पर **एक** `$ENVV` लागू करता है, इसलिए `MODE=CRED`
ने दोनों shots को *रचना के अनुसार* समान बना दिया। `run_bootA.sh` ने step 5 और
step 6 को खाली `$extra` के साथ fire किया, इसलिए प्रत्येक ने अपना **स्वयं का** page
spray किया।
⇒ आवश्यकता है **"दोनों shots समान value लिखें"**। `run_bootA.sh` अब इसे लागू करता
है (`SAME_VALUE=1`, default): step 6 step 5 के observed `write_value` को शब्दशः
reuse करता है, और यदि वह उस value को recover नहीं कर पाता तो **fire करने से ही
इनकार** कर देता है — क्योंकि fire करना एक divergent pair बना देगा।
⚠ `HOLD` को दूसरी shot से अधिक जीवित रहना चाहिए। यदि पहली shot का PIN child पहले
मर जाता है, तो page free और reallocate हो जाता है और "same value" एक dangling
pointer बन जाता है। Default `HOLD=20` **बहुत छोटा** है; `HOLD=600` उपयोग करें।
यह अब *default* है जब भी `SAME_VALUE=1` हो — पुराना unconditional 20 s का मतलब था
कि default configuration स्वयं ही trap थी — और `SAME_VALUE=1` के साथ एक explicit
छोटा `HOLD` अब चुपचाप dangling pointer बनाने के बजाय ज़ोर से चेतावनी देता है।
⚠ `CONTROL=1` पहले **केवल step 5** बदलता था, इसलिए यह
`(init_cred, fresh page)` — एक divergent pair — उत्पन्न करता था, जबकि यह file दावा
करती थी कि यह cell 2 को reproduce करता है। ठीक किया गया: अब यह दोनों shots को
`init_cred` पर सेट करता है। (Cell 2 की कीमत बरकरार है: side effect `init_cred+8`
को globally corrupt करता है, जो कि `Uid: 0 0 4294967176 0` है।)
**credential को launder करने वाली किसी भी चीज़ के लिए परिणाम**
(`setresgid` + `setresuid`, sprayed page को असली `struct cred` से बदलने के लिए):
- Mechanism वास्तविक और verified है — `commit_creds` `x21` को **दोनों**
`task+0x778` और `task+0x780` पर लिखता है (`0x186998` / `0x1869a0`), इसलिए एक
call split को स्थायी रूप से ठीक कर देता है; `prepare_creds @0xffffffc008186070`
है `kmem_cache_alloc(cred_jar)` + `memcpy(new, task->cred, 0xA8)` +
`security_prepare_creds(...)`, और 147/149 दोनों guard की exempt list पर हैं।
- **लेकिन इसकी precondition "0x778 shot छोड़ दो" के विपरीत है।** Launder स्वयं
`commit_creds` call करता है, इसलिए इसे केवल तब जारी किया जाना चाहिए जब **दोनों**
pointers पहले से समान value रखते हों।
- `V12_LAUNDER=1` **दो** चीज़ों पर gated है, और पहली कोई observation नहीं है:
1. **`V12_W7_SAME_VALUE=1`** — यह *provenance* तथ्य कि दोनों shots को समान value
दी गई थी। बिना read primitive के, pointer identity unobservable है, इसलिए इसे
बेहतर userspace check से नहीं बदला जा सकता; इसे declare करना ही होगा।
2. `consistent=1` — `0x780` view (`getuid()`) का `0x778` view
(`/proc/self/status` `Uid:`) से मेल खाना। **आवश्यक लेकिन अकेले पर्याप्त नहीं**:
दो अलग pages जो दोनों `uid 0` रखते हैं, बराबर पढ़े जाते हैं जबकि pointers अलग
होते हैं — जो वास्तव में वही case है जिसे runner पहले manufacture करता था।
(1) दिए जाने पर, यह पर्याप्त हो जाता है: agree + same value ⇒ दोनों एक ही page
पर land हुए।
कोई भी check fail हो ⇒ इनकार, और four-case table evidence में चला जाता है।
LT report line दोनों views (`uid=` / `real_uid=` / `consistent=`) के साथ
`same_value_declared=` भी print करती है ताकि state पढ़ी जाए, अनुमान न लगाया जाए।
### ★ Instrument 1 — side effect एक STAMP है जो target पर निशाना लगाता है
`*(write_value + 8) = write_target`, और `cred+8` / `cred+0xc` हैं `gid` / `suid`,
इसलिए एक 8-byte store दोनों पर land करता है:```
cred.gid = low32(write_target)
cred.suid = hi32(write_target)
यह एक माप है, कोई मॉडल नहीं। out/t5_w7_778.txt में
write_target = 0xffffff8800cdd178 और Uid: 0 0 4294967176 0 है, जहाँ
4294967176 = 0xffffff88 = hi32(write_target); notes.md §11 दूसरे आधे को दर्ज करता है, init_cred.gid = 0x00cdd178 = low32(write_target)।
दो उपयोग:
task+0x778 के लिए लैंडिंग ओरेकल है। /proc/<pid>/status real_cred = task+0x778 पढ़ता है — ठीक वही cred जो अभी इंस्टॉल हुआ है — इसलिए स्टैम्प सीधे userspace से पढ़ा जा सकता है। इसे stage 3 से पहले पढ़ें: मरम्मत cred+8 को शून्य कर देती है और इसे मिटा देती है (notes.md §11 का t5_repair.txt एक सफल मरम्मत से पहले 4294967176 पढ़ता है और बाद में 0)।0 पढ़ता है, इसलिए दो अलग-अलग पेज दोनों "consistent" रिपोर्ट करते हैं। लेकिन low32(T+0x778) और low32(T+0x780) ठीक 8 से भिन्न होते हैं, इसलिए दो पेजों के साथ getgid() ( से) और ( से) असहमत होते हैं — और uid के साथ-साथ gid की भी तुलना करता है।⇒ V12_W7_SAME_VALUE दूसरा gate है, केवल एकमात्र नहीं। यह अभी भी मायने रखता है: स्टैम्प केवल तभी विभेद करता है जब दोनों साइड इफेक्ट सक्रिय हुए हों, इसलिए same-value नियम उस शेष छेद को बंद कर देता है। और ध्यान दें कि gate क्या है — एक डिटेक्टर, न कि निवारक। यह केवल मना कर सकता है; यह कार्य को बाकी बूट के लिए विचलित छोड़ देता है। same-value नियम ही इस जोड़ी को सही बनाता है, जो पुरानी श्रृंखला में था और जो execve तक पहुँचने के लिए आवश्यक है।
probe_state लैंडिंग मानदंड नहीं हैयह इस परियोजना में तीन बार गलत रहा है: W1 ग्लोबल पर उतरा और R रिपोर्ट किया; run 12 का D एक cred के बजाय एक ग्लोबल पर लक्षित था; और run 11 का R एक तालिका में लिख दिया गया जैसे कि वह एक लैंडिंग हो (run11_w778r1_miss.txt और run 7 का w7_w7781.txt पंक्ति-दर-पंक्ति समरूप हैं — दोनों probe_state = R, probe_done = 0)। प्रति-लक्ष्य ओरेकल का उपयोग करें:
run_bootA.sh अब stage 1 के लिए स्टैम्प का उपयोग करता है — जो task+0x778 पर ROUNDS>1 पुनःप्रयासों को अर्थपूर्ण बनाता है, क्योंकि एक विफल राउंड अनुमानित होने के बजाय पठनीय होता है — और यह stage 2 को तब तक सक्रिय नहीं करेगा जब तक stage 1 उतर न जाए।
⛔ "awk फ़ील्ड" कहें, कभी "तीसरा फ़ील्ड" नहीं।
uid_lineलेबल भी प्रिंट करता है (Uid: 0 0 4294967176 0), इसलिए awk का$1"Uid:"है और चार id मान$2..$5हैं:$2=uid$3=euid$4=suid$5=fsuid। स्टैम्पcred+8पर बैठता है, यानीgid(low32) औरsuid(hi32) — इसलिए यहGid:$2औरUid:है। इसे "तीसरा फ़ील्ड" कहना (जो की गिनती करता है, और §11 इसे इसी तरह शब्दबद्ध करता है) कोड को पढ़ने के लिए आमंत्रित करता है, जो नकली cred पर = है और कभी भी के बराबर नहीं हो सकता। यह off-by-one यहाँ मौजूद था: ने एक उतरे हुए शॉट के लिए "no stamp" लौटाया, इसलिए stage 2 कभी सक्रिय नहीं हुआ और launder gate हमेशा के लिए मना करता रहा — , क्योंकि "no stamp" एक वास्तविक मिस का भी सामान्य परिणाम है।
एक मानदंड जिसका कभी किसी ज्ञात-सकारात्मक नमूने के विरुद्ध परीक्षण नहीं किया जाता, वह मानदंड नहीं, एक अनुमान है — और इस श्रेणी की विफलता (यह off-by-one, probe_state, dmesg -w, खाली klog.host, खाली readback) हमेशा "कुछ नहीं हुआ" के रूप में प्रस्तुत होती है, जो एक वैध प्रायोगिक परिणाम भी है। इसलिए जाँच अब दो बार रक्षित है:
stamp_selftest() preflight में चलता है और विफलता पर exit 9 करता है, वही निष्कर्षण फ़ंक्शन चलाते हुए जो gate उपयोग करता है, out/t5_w7_778.txt के मापे गए मानों के विरुद्ध (write_target = 0xffffff8800cdd178 → Uid $4 = 4294967176, Gid $2 = 13488504) साथ ही नकारात्मक और अपठनीय नमूने। एक स्व-परीक्षण जो जाँच को पुनः लागू करता है, कुछ सिद्ध नहीं करता, इसलिए फ़ील्ड निष्कर्षण को uid_suid_field / gid_gid_field में अलग किया गया है।tools/test_stamp_criterion.sh — एक स्टैंडअलोन रिग्रेशन टेस्ट के रूप में वही चीज़, जो वास्तविक फ़ंक्शनों को से निकालता है।stamp_ok() तीन स्थितियाँ लौटाता है, क्योंकि "पढ़ नहीं सकते" "no stamp" नहीं है (यही मिलावट run 13 को "no change" जैसा दिखाती है): 0 = मौजूद, 1 = पठनीय और कोई स्टैम्प नहीं, 2 = अपठनीय। और जब यह 1 लौटाता है जबकि probe_state = D, तो रनर "did not land" संदेश के बजाय ⛔ ORACLE INCONSISTENT — "जाओ मानदंड जाँचो" — प्रिंट करता है, जो ऑपरेटर को पूरी तरह से भिन्न स्थान पर भेजता है (एक नया बूट, या हिट-रेट शिकार)।
पूर्ण व्युत्पत्ति: evidence/2026-09-18-divergence-is-latent.md
और evidence/2026-09-18-cred-launder-verification.md
(बाद वाले का §2.3 यथास्थान वापस लिया गया है)। write-shape ओवरलैप स्व-जाँच ऑफ़लाइन बंद है: shape शब्द कर्नेल स्टैक पर fd_set ग्रिड में रहते हैं जबकि साइड इफेक्ट स्प्रे किए गए पेज के अंदर उतरता है, इसलिए दोनों किसी भी shape में ओवरलैप नहीं कर सकते।
तीन स्वतंत्र रिपोर्टर। कोई भी दूसरे के लिए फ़ॉलबैक नहीं है, और केवल पथ 1 कॉलिंग कार्य को मार सकता है।
पथ 2 execve पर सक्रिय होता है, credential परिवर्तन पर नहीं, और यह पथ 1 से एक अलग कोड पथ है। यह kevent_send_to_user के माध्यम से रिपोर्ट करता है, इसलिए आगे क्या होता है यह एक userspace डेमॉन का निर्णय है, कर्नेल का नहीं।
जाँच exec किए जा रहे इमेज के पथ पर है, इसलिए लोडर के payload को memfd-लोड करना पर्याप्त नहीं है: यदि लोडर स्वयं /data/local/tmp से exec किया जाता है, तो वह पहला execve पहले से ही रिपोर्ट कर देता है। memfd पर d_path() /memfd:… होता है, इसलिए लोडर को स्वयं एक memfd के माध्यम से exec किया जाना चाहिए — V12_EXEC_MEMFD अब ठीक इसी कारण से डिफ़ॉल्ट रूप से चालू है। पुराना व्यवहार RUN 4 में दिखाई देता है:```
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
पथ 2 के लिए ग्रेप-योग्य मार्कर:```
[ROOTCHECK-EXEC-INFO]:common %s result %s with "execve_report" / "execve_block"
%d,path@@%s kevent payload fragment
क्योंकि path 2 और path 3 केवल kevent के माध्यम से रिपोर्ट करते हैं, "kernel log में कोई [ROOTCHECK-*] नहीं" इस बात को बाहर नहीं करता कि उनमें से कोई भी सक्रिय हुआ हो। उस अनुमान के लिए userspace receiver की आवश्यकता है, जिसे हमने अभी तक नहीं खोजा है।
oplus_security_guard.kosys_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
`sys_exit` जाँच:```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)
कंट्रोल-फ़्लो नोट (एक्सप्लॉइट क्रम के लिए महत्वपूर्ण)। चार अवरोही-किनारा
तुलनाएँ सीधे 0x468 पर शाखा करती हैं, जो सिस्कॉल-नंबर डिस्पैच है — वे
addr_limit गेट से नहीं गुज़रतीं। 0x454–0x464 तक केवल तब पहुँचा जाता है जब
कोई भी id अवरोहित न हुआ हो। इसलिए डिस्पैच तब प्रविष्ट होता है जब या तो कोई id अवरोहित हुआ
या addr_limit == KERNEL_DS; यह addr_limit द्वारा गेटेड नहीं है।
परिणाम:
sys_enter अभी भी पुराना uid कैश किए हुए था। यदि सिस्कॉल प्रवेश के समय टास्क पहले से uid=0 है
(0x400 cbz), तो हुक लौट जाता है और तब से अंधा बना रहता है।delivery/外部建议评审_2026-09-18.md।g_boot_state — 1 बाइट .data..ro_after_init, मॉड्यूल इनिट पर verified_bootstate से
strstr के माध्यम से सेट होता है। is_unlocked() = LDRB + RET।
मॉड्यूल VA राइट फ़ॉल्ट करते हैं (CONFIG_STRICT_MODULE_RWX=y) — फ़िज़मैप उपनाम 0xffffff80… का उपयोग करें।
रिपोर्ट पेलोड: $$sys_call_number@@%d, $$set_id_flag@@%d, $$addr_limit@@%lx, $$enforce@@%d।
.rodata+0, इंडेक्स 143–214शेष 60 प्रविष्टियाँ → रिपोर्ट + किल।
किसी थ्रेड को sendmsg (211) पर ब्लॉक न करें जबकि उसके क्रेडेंशियल बदल रहे हों। यह
तालिका में नहीं है, इसलिए थ्रेड रिपोर्ट और किल किया जाता है। इस उद्देश्य के लिए
जिन सिस्कॉलों में ब्लॉक होना सुरक्षित है, वे केवल ऊपर दिए गए बारह हैं:
setregid, setgid, setreuid, setuid, setresuid, setresgid,
connect, getsockname, setsockopt, shutdown, readahead, brk.
सुधार (2026-09-18)। इस तालिका के एक पहले के संशोधन ने प्रत्येक प्रविष्टि को वास्तविक arm64 सिस्कॉल नंबर से एक कम लेबल किया था (
146कोsetresuidकहा गया था; यहsetuidहै —setresuid147है)। संख्याएँ हमेशा सही थीं; केवल नाम गलत थे। नाम अब इस डिवाइस की कर्नेल इमेज मेंsys_call_table@0xffffffc00a13d8c0से हल किए गए हैं। विशेष रूप सेsendmsg(211),munmap(215),getsockopt(209) औरgetpeername(205) छूट-प्राप्त नहीं हैं — इनमें से किसी में भी थ्रेड को ब्लॉक करना जबकि उसके क्रेडेंशियल बदल रहे हों, पास नहीं बल्कि किल है।tools/gen_exempt_table.pyसे पुनः जनरेट करें।
oplus_secure_harden.ko`entry_handler_setxattr` → `oplus_heapspray_check(7)`, `entry_handler_cpuinfo` → `(6)`, दोनों बिना शर्त।
`oplus_heapspray_check` — काउंटर `heapspary_ip4` / `ip6` / `xttr` / `cpuinfo`, `ktime_get_real_ts64` डेल्टा, 7 रिपोर्ट साइटें (`snprintf` + `printk` + `kevent_send_to_user`), `verified_bootstate` पर निर्भर।
### बचाव
| प्रिमिटिव | |
|---|---|
| `setsockopt` optname 46 `MCAST_JOIN_SOURCE_GROUP` | फ़िल्टर नहीं किया गया |
| `setsockopt` level `SOL_IPV6` (41) | फ़िल्टर नहीं किया गया यदि फ़िल्टर `level` पढ़ता है |
| `setxattr` | हमेशा गिना जाता है |
| `/proc/cpuinfo` | हमेशा गिना जाता है |
| `socket()` / `socketpair()` | हुक नहीं किया गया |
| `sendmsg`, `pipe`, `memfd`, `add_key`, `io_uring`, mmap | हुक नहीं किया गया |
## कॉन्फ़िग```
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
NDK r28c। -O1 / API 26 / -D__ARM=1 fixed हैं — ये reclaim stack-frame geometry (delta=0 calibration) को बनाए रखते हैं। इनमें से कोई भी बदलने पर डिवाइस पर फिर से calibration करना आवश्यक होगा।```bash
export ANDROID_NDK_HOME=/path/to/android-ndk-r28c
make # → exploit_guard
./build.sh # same, with NDK auto-detection
मैनुअल:```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 बिल्ड (.github/workflows/build.yml, Ubuntu + NDK r28c, आर्टिफैक्ट exploit_guard)।
adb push exploit_guard /data/local/tmp/e adb shell chmod 755 /data/local/tmp/e adb shell /data/local/tmp/e
## फ़ाइलें```
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
evidence/kill.log — चार रूट रनों की शब्दशः adb shell प्रतिलेख: पूरी टाइमलाइन, जिस क्षण लक्ष्य कार्य का uid 0 हो जाता है, kernelsu.ko का लोड, और उसके बाद की स्थिति। पहले हेडर ब्लॉक को पढ़ें: यह सूचीबद्ध करता है कि फ़ाइल में क्या नहीं है और क्यों।
evidence/notes.md — कर्नेल पक्ष। मॉड्यूल पते और कौन से /proc चैनल किस SELinux स्थिति में काम करते हैं; strstr कुंजी सहित पूरी g_boot_state व्युत्पत्ति; संशोधित exempt तालिका; वह कैप्चर विधि जो गायब कर्नेल आधे हिस्से को उत्पन्न करेगी; और अभी भी क्या खुला है इसकी सूची।
evidence/2026-09-18-bootA/ — वर्तमान डिज़ाइन का पहला डिवाइस रन, 13 बूट। रीबूट व्यवस्थित हैं (bootreason=reboot) और कोई panic लाइन कभी कैप्चर नहीं हुई — लेकिन इसे एक के नमूने के रूप में पढ़ें, तेरह के नहीं। जो चार रन रीबूट हुए, उनमें से दो ने खाली klog.host सहेजा, एक ने अगले बूट का लॉग सहेजा, और केवल एक विंडो ही अपने स्वयं के रीबूट को प्रशंसनीय रूप से ब्रैकेट कर सकती है। इसी तरह, एक रन का probe_state और victim readback दोनों खाली हैं (डिवाइस पहले ही चला गया था), इसलिए यह इस बारे में कोई जानकारी नहीं रखता कि उसका लेखन पहुँचा या नहीं — एक खाली फ़ील्ड "कोई परिवर्तन नहीं" नहीं है। यह निर्देशिका उस पद्धति संबंधी त्रुटि का भी दस्तावेज़ीकरण करती है जिसके बारे में जानना उचित है: dmesg -w इस डिवाइस पर एक नो-ऑप है (toybox एक बार डंप करता है और बाहर निकल जाता है), इसलिए एक पहले के रन के कर्नेल लॉग में केवल कैप्चर-पूर्व इतिहास था — "कोई [ROOTCHECK-*] नहीं" किसी चीज़ का साक्ष्य नहीं था। evidence/notes.md §6 में संशोधित poll-and-stream-to-host विधि है।
evidence/2026-09-18-cred-launder-verification.md
— इस इमेज के स्वयं के डिसअसेम्बली (सामान्य 5.10 स्रोत नहीं) के विरुद्ध क्रेडेंशियल-लॉन्डरिंग प्रस्ताव का सत्यापन: commit_creds का दोहरा स्टोर, prepare_creds आवंटन और मापा गया sizeof(struct cred) = 0xA8, ऊपर वर्णित BUG_ON(cred != real_cred) विचलन जोखिम, बंद write-shape ओवरलैप स्व-जाँच, और 13 बूटों का साक्ष्य-कवरेज ऑडिट।
§2.3 यथास्थान वापस लिया गया है — विचलन अव्यक्त है, रीबूट तंत्र नहीं।
evidence/2026-09-18-divergence-is-latent.md
— दूसरे दौर का सत्यापन। commit_creds अपना कार्य current से लेता है
(0x1867a0 mrs x20, sp_el0), exit_creds put_cred से पहले दोनों पॉइंटर को null करता है, और कच्चे कैप्चर दिखाते हैं कि पुरानी श्रृंखला ने दोनों स्लॉट में एक समान मान (0xffffff802a7e0be0) लिखा जबकि नई श्रृंखला ने दो अलग पेज लिखे। तो मानदंड पॉइंटर समानता है — "दोनों शॉट समान मान लिखते हैं", न कि "दोनों शॉट पहुँचते हैं"।
postreboot_forensics.sh — रीबूट फोरेंसिक्स जो पोलर पर निर्भर नहीं करता। मानदंड एक ही शर्त है:
CONFIG_PSTORE_CONSOLE=y panic() को kmsg_dump(KMSG_DUMP_PANIC) पर कंसोल टेल को ramoops में लिखने देता है — किसी भी रीसेट से पहले — इसलिए बॉक्स फिर रीबूट होता है या हैंग करता है, यह अप्रासंगिक है। /sys/fs/pstore/ खींचता है, kernel BUG / __put_cred / cred.c के लिए grep करता है, और boot-reason स्ट्रिंग प्रिंट करता है (इतिहास प्रविष्टियों में reboot,shell / bootloader / reboot,edl प्रत्यय रहे हैं, इसलिए कारण एक अभिनेता को अलग करता है जहाँ epoch नहीं करता)।
⚠ "स्वच्छ
bootreason=reboot" को "कोई panic नहीं" के रूप में न पढ़ें। QCOM पर एक SoC वॉचडॉग एसर्ट को PMIC PON ब्लॉक के माध्यम से रीसेट किया जाता है, इसलिएpanic → panic_timeout=-1 → hang → watchdog → PMIC reset → clean bootreasonएक स्व-संगत श्रृंखला है जो हमारे पास मौजूद साक्ष्य पर हार्डवेयर रीसेट से अप्रभेद्य है। इस रेपो का स्वयं काtotal_17_dump_0_pmic_17सभी 17 असामान्य रीबूटों कोpmicके लिए जिम्मेदार ठहराता है, जो वास्तव में वॉचडॉग का सामान्य आकार है, "कर्नेल नहीं" का साक्ष्य नहीं।bootreasonयहाँ कुछ भी संकीर्ण नहीं करता; ramoops ही एकमात्र मानदंड है।
दो पूर्वापेक्षाएँ, अन्यथा स्क्रिप्ट का निर्णय शून्य है (铁律 8 — एक नो-सिग्नल निष्कर्ष के लिए आवश्यक है कि चैनल पहले पहुँच योग्य सिद्ध हो):
/sys/fs/pstore/* केवल root के लिए है, इसलिए Enforcing के अंतर्गत adb pull और cat दोनों विफल होते हैं — और "पढ़ नहीं सकते" वही आउटपुट उत्पन्न करता है जो "पढ़ा और यह खाली था" करता है। एक द्वि-अवस्था स्क्रिप्ट उस चैनल से "pstore खाली है ⇒ panic असिद्ध" प्रिंट करती है जिसे उसने कभी खोला ही नहीं। इसलिए स्क्रिप्ट CHANNEL UNREACHABLE उत्सर्जित करती है (ls विफल, या सभी ज्ञात प्रविष्टियाँ मौजूद न होने के बजाय पढ़ने में विफल) और साथ में getenforce रिपोर्ट करती है।adb reboot के बाद तत्काल फ़ेच। यदि एक ज्ञात-अच्छा रीबूट कुछ भी पठनीय नहीं देता, तो चैनल सिद्ध नहीं है और बाद का प्रत्येक "खाली pstore" साक्ष्य नहीं है। क्रम मायने रखता है: डिवाइस बूट के कुछ ही समय बाद रिकॉर्ड को हटा देता है, इसलिए अनुक्रम है
reboot → Permissive प्राप्त करें (W1) → स्क्रिप्ट तुरंत चलाएँ।run_bootA.sh — उस एक बूट के लिए ऑर्केस्ट्रेशन, उस क्रम में जो मायने रखता है (0x778 → 0x780 उसी मान के साथ → वास्तव में इंस्टॉल किए गए cred की स्थानीय मरम्मत → पुष्टि करें → तभी poke करें)। ADB=/SER=/BIN_LOCAL= ओवरराइड करने योग्य; SAME_VALUE=1 (डिफ़ॉल्ट) समान-मान नियम लागू करता है, LAUNDER=1 gated launder सक्षम करता है, HOLD=600 समान-मान अनुक्रम के लिए आवश्यक है।
पुनःप्रयास प्रति चरण विभाजित हैं (R5/R6), क्योंकि दोनों चरणों के जोखिम प्रोफ़ाइल विपरीत हैं:
R5 डिफ़ॉल्ट रूप से ROUNDS है, R6 डिफ़ॉल्ट रूप से 1।
अनुशंसित launder रन — डिफ़ॉल्ट sprayed-page पथ, न कि CONTROL=1:```bash
LAUNDER=1 R5=3 R6=1 HOLD=600 CHAINWAIT=6000 NODRAIN=1 WATCH=180 ./run_bootA.sh
`CONTROL=1` भी एक सुसंगत युग्म उत्पन्न करेगा, लेकिन `init_cred`
पॉइंटर लिखकर, जिसका दुष्प्रभाव `init_cred+8` को **वैश्विक रूप से** दूषित करता है — और "फ्रेमवर्क मर जाता है" उन चीज़ों में से एक है जिन पर नज़र रखी जा रही है, इसलिए मापन की पृष्ठभूमि में डिवाइस-व्यापी दोष ले जाना ठीक उसी रीडिंग को धुंधला कर देता है जिसे लेने के लिए यह रन मौजूद है। स्प्रे की गई-पेज पथ की लागत केवल "दोनों शॉट्स का लगना आवश्यक" है, जिसके लिए `R5=3` है। `CONTROL=1` एकमात्र *प्रमाणित* सुसंगत युग्म और एक नियंत्रण के रूप में बना रहता है, अनुशंसित कॉन्फ़िगरेशन के रूप में नहीं।
**कौन सा पेज इंस्टॉल हुआ, यह बिना शर्त वाली पंक्ति से पढ़ा जाता है।** `run_w7` राइट वैल्यू को दो पंक्तियों पर प्रिंट करता है, और उनमें से केवल एक बिना शर्त है:```
L1793 W7[..] write value = private cred page 0x.. — spray path only
L1802 W7[..] write_value = 0x.. — after the if/else, ALL paths
The runner पहले वाले wording से match करता था, इसलिए CONTROL=1 पर extraction खाली वापस आया, if [ -n "$CRED" ] ने repair को skip कर दिया, और same-value conjunction का अपना [ -n "$CRED" ] term उसे 0 पर रोके रखा — launder gate हमेशा के लिए मना कर देता। दोनों मामलों में एक silent no-op, एक ऐसे regex से जो दो print sites में से एक को पहचानता था। अब यह और write_target extraction (stamp criterion का input) दोनों wv_from/wt_from से होकर जाते हैं, जिन्हें criterion के साथ-साथ regression test भी exercise करता है।
दोनों streams उस चीज़ से पहले शुरू होते हैं जिसे वे measure करते हैं। uid.stream stage 1 से चलता है; cred.stream poke पर शुरू होता है, watch के बाद नहीं — poke child को उसके NO_EXEC report loop में छोड़ देता है, जो 240 × 0.5 s = 120 s है और फिर _exit(0) (exploit.c: "LT child NO-EXEC mode done (120s)"), इसलिए t+~135 s पर पुरानी placement child के जाने के बाद sampling शुरू करती थी, ठीक उसी window में जिसके लिए instrument मौजूद है। Runner आगे बढ़ने से भी मना कर देता है अगर stamp_selftest() fail हो, और जब stamp और probe_state असहमत हों तो "did not land" के बजाय ORACLE INCONSISTENT print करता है।
artifacts/guard_post_handler.s — relocations भरे हुए kill chain। पुरानी listings में adrp x9, #0 .data..ro_after_init है; bl #0x4ac oplus_root_check_succ है। अपने ही device से vendor modules pull करने के बाद tools/gen_guard_disasm.py से regenerate करें।
tools/kdis_ko.py — RELA sh_info से match होता है; इन builds पर .text relocs .rela.text.<func> में रहते हैं, इसलिए name-based lookup कुछ नहीं लौटाता।
| Project | |
|---|---|
| JoinChang/ghostlock-oneplus | reference implementation; 5.10 compact waiter |
| NebuSec CyberMeowfia | original GhostLock research |
GPL-3.0 — देखें LICENSE।
| Device | 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 | locked, green |
| VA_BITS | 39 — KIMAGE_TEXT_BASE = 0xffffffc008000000 |
| Stage |
|---|
Compact waiter trigger (CMP_REQUEUE_PI → EDEADLK) | works |
task_struct leak (perf) | works |
PI write (8-byte; value = 0 या एक वैध kernel address) | works |
task+0x778 या task+0x780 अकेले → Uid=root | works — लेकिन एकल-फ़ील्ड लैंडिंग कार्य को विचलित छोड़ देती है, और वह एक latent hard BUG_ON है। देखें the divergence hazard |
| दोनों फ़ील्ड एक ही मान से लिखे गए (एक सुसंगत युग्म) | ❌ sprayed page के साथ कभी उत्पन्न नहीं हुआ। केवल वैश्विक init_cred alias (09-14, CONTROL=1) के साथ देखा गया। runner अब इसे लागू करता है (SAME_VALUE=1); डिवाइस पर नहीं चलाया गया |
Credential laundering (setresgid + setresuid) | V12_LAUNDER=1 के पीछे लागू; डिवाइस पर नहीं चलाया गया |
kernelsu.ko loaded | works |
| Root process survives | ⚠ स्थापित नहीं — नीचे देखें |
| Reboot mechanism | ❌ स्थापित नहीं। एक उम्मीदवार (the divergence) अब बहिष्कृत है; नीचे देखें |
probe_state as a landing criterion | ❌ गलत — उपयोग न करें। तीन प्रति-उदाहरण; नीचे तालिका देखें |
| pstore/ramoops panic channel | ⚠ instrument मौजूद है; channel कभी मान्य नहीं किया गया (अभी तक कोई null test नहीं) |
| "The victim spins in pure userspace" | ⚠ अभी तक कोई रीडिंग नहीं — uid.stream अब utime/stime/nvcsw रिकॉर्ड करता है ताकि इसे जाँचा जा सके |
| pi-side single-pass dual write | ⚠ स्थापित नहीं; pi.pc/pi.left fdset_map.h में hard-coded 0 हैं |
Path A (UMH / modprobe_path) | STATIC_USERMODEHELPER_PATH="" |
| Field | Offset |
|---|
real_cred / cred | 0x778 / 0x780 |
cached syscallno | 0xdf8 |
cached uid / euid / gid / egid | 0xe00 / 0xe08 / 0xe10 / 0xe18 |
flags0x0 |
addr_limit | 0x8 |
ttbr0 | 0x10 |
preempt_count | 0x18 |
| Field | Offset | Field | 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 |
Uid: 0 0 0 0credstatus_gidreal_credlt_cred_ids_agree()| लक्ष्य | लैंडिंग ओरेकल |
|---|
task+0x778 | Uid: चौथा awk फ़ील्ड = hi32(write_target) और Gid: दूसरा awk फ़ील्ड = low32(write_target) — ऊपर का स्टैम्प; stage 3 से पहले पढ़ें |
task+0x780 | पीड़ित का स्वयं का getuid() |
ग्लोबल selinux_enforcing | getenforce |
probe_state | ❌ मानदंड नहीं। अधिक से अधिक श्रृंखला के बारे में एक संकेत; कभी भी इस बात का प्रमाण नहीं कि कोई राइट उतरा |
$4notes.md$3euid0hi32(write_target)stamp_ok()run_bootA.sh| # | हुक | ट्रिगर | क्रिया |
|---|
| 1 | oplus_root_check_post_handler, sys_exit tracepoint | कोई id नीचे उतरा, या addr_limit == KERNEL_DS | oplus_root_killed → printk + do_exit(SIGKILL); और oplus_root_check_succ → kevent_send_to_user |
| 2 | oplus_exe_block_ret_handler, sys_exit लेकिन केवल execve (221) के लिए | d_path(mm->exe_file) /data, /data/local/tmp, /data/nativetest, /data/nativetest64 से शुरू होता है | oplus_RWO_root_check → printk + kevent_send_to_user (कोई do_exit नहीं) |
| 3 | oplus_secure_harden kretprobes | setsockopt optname ∈ {41,42,48}, setxattr, /proc/cpuinfo, SELinux policy reload | oplus_heapspray_check → kevent_send_to_user |
143 setregid | 144 setgid | 145 setreuid | 146 setuid |
|---|
147 setresuid | 149 setresgid | 203 connect | 204 getsockname |
208 setsockopt | 210 shutdown | 213 readahead | 214 brk |
| kretprobe | हुक्स | फ़िल्टर |
|---|
socket_kretprobe | ip_setsockopt | regs[1] ∈ {41, 42, 48} |
socket_ip6_kretprobe | do_ipv6_setsockopt | regs[1] ∈ {41, 42} |
cpuinfo_kretprobe | cpuinfo_open | — |
setxattr_kretprobe | setxattr | — |
sepolicy_reload_kretprobe | spolicy_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 |
| चरण | पुनःप्रयास सुरक्षा |
|---|
R5 | चरण 5, task+0x778 | सुरक्षित — एक मिस कुछ भी इंस्टॉल नहीं करता, और स्टैम्प मानदंड एक विफल राउंड को पठनीय बनाता है, इसलिए एक और शॉट बस एक और प्रयास है। R5=3 प्रति-शॉट हिट दर को ~p से ~1−(1−p)³ तक ले जाता है। |
R6 | चरण 6, task+0x780 | सुरक्षित नहीं, और आवश्यक भी नहीं — यह केवल चरण 5 के पहुँचने के बाद चलता है, इसलिए एक पुनःप्रयास पहले से ही विचलित कार्य पर शूट करता है: एक दूसरा, भिन्न पेज पहुँचाने का एक और मौका, बिना किसी लाभ के, क्योंकि एक के पहुँचने से जोड़ी पूरी हो जाती है। इसे 1 पर रखें। |