
GhostLock (CVE-2026-43499) لـ OPPO Find X5 Pro (PFEM10) — هندسة عكسية لكاشف OPlus watchdog و heap-spray
English · 中文
منفذ GhostLock (CVE-2026-43499) لجهاز OPPO Find X5 Pro على ColorOS 16. يصل إلى عملية فرعية uid=0 ووحدة kernelsu.ko محمّلة؛ يتم اعتراض عملية الجذر.
CVE-2026-43499 — استخدام-بعد-التحرير في futex PI. تقوم remove_waiter() بمسح current->pi_blocked_on عندما يكون current هو المُعيد للطلب، على مسار التراجع -EDEADLK الخاص بـ rt_mutex_start_proxy_lock().
remove_waiter @ 0xffffffc0081ed254 — الشكل قبل الإصلاح.
بخصوص "بقاء عملية الجذر": التشغيلات في evidence/kill.log
تصل إلى uid=0 وتحمّل kernelsu.ko، وفي التشغيل الذي استطلعها فعلاً
نجت عملية مدير KernelSU لمدة 120 ثانية مع بقاء kernelsu بحالة Live في
/proc/modules. في تشغيل لاحق، تركت نفس السلسلة خدمات إطار عمل Android غير قابلة للوصول (Can't find service: package/power/input/phone/wifi)
بينما كانت الوحدة لا تزال Live. لم يُلتقط أبداً أي سطر نواة [ROOTCHECK-*] ولا أي حمولة $$sys_call_number@@، لذا لا يُنسب سبب حالة التشغيل اللاحق. انظر evidence/notes.md
§2.3 و§2.4 و§7.
task_struct
thread_info
| الحقل | الإزاحة |
|---|---|
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 لذلك يُشغّل المرحلة 2 مع V12_W7_VALUE=<القيمة المرصودة من المرحلة 1> ويرفض تشغيلها إطلاقًا إذا تعذّر استرجاع تلك القيمة. يجب أن يبقى HOLD حيًّا بعد المرحلة 2، وإلا فسيتم تحرير صفحة المرحلة 1 وإعادة تخصيصها وتصبح "القيمة نفسها" مؤشرًا معلّقًا. انظر
قاعدة القيمة نفسها.
تُصلَّح صفحة واحدة لكل إقلاع. المرحلة 3 تُصفّر V+8. مع صفحتين مختلفتين، فإن تصفير كلتيهما سيمحو ختم gid/suid (أدناه) ويجعل التباعد يبدو كتوافق، لذلك يُصلح المشغّل فقط الصفحة التي ثُبّتت فعليًا ويتوقف إذا اختلفت القيمتان.
تُبنى صفحة cred بواسطة payload.c: جميع حقول المعرّفات الثمانية صفر، وجميع مجموعات القدرات الخمس ممتلئة، وuser / user_ns / group_info تشير إلى root_user / init_user_ns / init_groups. المرحلة 3 موجودة لأن الأثر الجانبي للكتابة يُفسد دائمًا cred+8 (gid/suid) لأي cred يُثبّته.
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: PERF_TYPE_SOFTWARE / PERF_COUNT_SW_CPU_CLOCK، PERF_SAMPLE_REGS_INTR، exclude_user=1.
اقبل [0xffffff8400000000, 0xffffff90000000)، أصوات ≥ 15%.
يُقاد الـ UAF عبر rb_erase_cached الحالة 1-يسار. يمنحنا ذلك مخزنين، لا واحدًا:```
*(write_target) = write_value // the store you aim
*(write_value + 0x08) = write_target // unavoidable side effect
يجب أن يكون `write_value` محاذيًا لـ 8 بايت مع بت 0 صافٍ — فهو إما `0` أو عنوان kernel صالح. **لهذا السبب لا يمكن تعيين `g_boot_state` باستخدام هذه البدائية**: البايت الذي تحتاجه ليصبح `1` يتم فرض بتّه المنخفض إلى `0` بواسطة متطلب المحاذاة، و`write_value` هو نفس الكمية كالعنوان الذي يهبط فيه التأثير الجانبي.
### التأثير الجانبي يكتب في أي شيء يشير إليه `write_value`
`write_value` هو في آنٍ واحد *القيمة المخزّنة* و*العنوان الذي يكتب إليه التأثير الجانبي* (عند `+8`). وجّهه إلى كائن kernel عام وستُفسد ذلك الكائن.
**W7 كان يفعل هذا بالضبط** — حيث كان يوجّه `write_value` إلى الاسم المستعار `init_cred` — وهذا مرئي في القراءة المرتجعة. من `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 وكان write_target هو child_task+0x778.
init_cred+8 هو gid/suid، لذا خزّن الأثر الجانبي 0xffffff8800cdd178
هناك: init_cred.gid = 0x00cdd178 و init_cred.suid = 0xffffff88 = 4294967176 — وهو تحديداً الحقل الرابع في awk من سطر Uid: أعلاه. تصفير
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
ترتد إلى init_cred عند فقدان صفحة cred الخاصة — بل تُجهض بدلاً من ذلك.
الافتراضي، والمسار الوحيد المعقول، هو صفحة cred المرشوشة.
لا يمكن تجنب الأثر الجانبي نفسه: يجب أن يكون write_value هو مؤشر cred،
لذا يُطمس cred+8 دائماً بهدف الكتابة. موقعه فقط هو الخيار — والإصلاح الآن
هو تصفير محلي لـ cred_page+8 (stage 3)، وليس كتابة في كائن عام.
قراءة
groups=غير الصحيحة هي عرض منفصل، وليست هذه. لقد شوهدت في تشغيل حيث قُرئgidوegidبشكل سليم، لذا لا يمكن أن تأتي من الأثر الجانبي لـinit_cred+8؛ إنها تشير إلى حقلgroup_infoالخاص بـ cred المزيف. انظرevidence/notes.md§10.6.
BUG_ON صارمتخزّن البدائية في عنوان واحد بالضبط لكل تمريرة. task+0x778
(real_cred) و task+0x780 (cred) عنوانان منفصلان، لذا أي كتابة
هبطت على 0x778 فقط أو 0x780 فقط تترك المهمة بـ
cred != real_cred — وهي حالة تباعد.
على هذه الصورة تلك الحالة هي panic صارم، وليست تحذيراً. يفتتح 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()
وتم بناء النواة باستخدام **`CONFIG_PANIC_ON_OOPS=y`** (`CONFIG_PANIC_ON_OOPS_VALUE=1`).
`__put_cred @0xffffffc008185530` يحمل نفس عائلة التأكيدات
(`usage != 0` → BUG؛ `cred == current->cred` / `current->real_cred` → BUG).
إذن التباعد *كامن* — لا يفعل شيئًا بينما الضحية تدور فقط —
حتى يحدث **أي** `commit_creds` على تلك المهمة: `setresuid` / `setresgid` /
`setuid` / `setgid` / `capset`، أو **`execve` عبر `install_exec_creds`**.
> **⛔ مسحوب (2026-09-18 متأخرًا): هذه ليست آلية إعادة التشغيل.**
>
> نسخة سابقة من هذا القسم وصفت التباعد بأنه "المرشح الآلية الرائدة لإعادات
> التشغيل" وقالت إنه "يفسر انقسام الشكل". إنه لا يفعل ذلك، والسبب الآن مقيس
> بدلًا من أن يكون محل جدال:
>
> * `commit_creds` يأخذ مهمته من **`current`** — `0x1867a0 mrs x20, sp_el0`.
> توقيعه هو `commit_creds(struct cred *new)`؛ لا توجد وسيطة مهمة.
> لذا فإن التباعد يهم فقط إذا كانت المهمة **الحاملة** له تستدعي `commit_creds`
> بنفسها.
> * كانت جميع تشغيلات إعادة التشغيل `V12_NO_EXEC=1` (مذكورة حرفيًا في
> `run3_0445.log:18`، `run9_0606.log:20`، `run10_0616.log:24`)، لذا لم تصدر
> الضحية `execve` أبدًا ولم تصل إلى `commit_creds` إطلاقًا.
> * التشغيل 10 لم يكن فيه أي poke على الإطلاق (`grep -c poke` = 0).
> * `exit_creds` يصفّر **كلا** المؤشرين قبل `put_cred`
> (`0x185cb8 str xzr,[x19,#0x778]`؛ `0x185d24 str xzr,[x19,#0x780]`)، لذا فإن
> `_exit(0)` للضحية **يمحو** التباعد بدلًا من أن يتعثر به.
>
> ⇒ في تلك التشغيلات كان التباعد **خاملًا**. إن `BUG_ON` حقيقي، لكنه لغم أرضي
> لم ينفجر. "الشكل A لا يعيد التشغيل أبدًا" يعود ليكون ارتباطًا. ما يقيّده اللغم
> فعليًا هو **غسيل الاعتماد**، لأن `setresgid`/`setresuid` تستدعي `commit_creds`
> بنفسها.
>
> **الكمية التي تفصل السلاسل فعليًا هي تساوي المؤشرات، وهذا يعني أن الطلقتين
> يجب أن تكتبا قيمة واحدة** — انظر القسم الفرعي التالي.
### ★★★ الطلقتان يجب أن تكتبا القيمة *نفسها* — لا مجرد أن تنجحا كلتاهما
`BUG_ON` يقارن **المؤشرات**. صفحتان تحملان كلتاهما `uid 0` هما مع ذلك كائنان
مختلفان. اللقطات تجعل التمييز ملموسًا:
| السلسلة | طلقة 0x778 | طلقة 0x780 | المؤشرات |
|---|---|---|---|
| القديمة (`t5loop.sh MODE=CRED`) | `in[0]=0xffffff802a7e0be0` | `in[0]=0xffffff802a7e0be0` | **متساوية** → تم تحميل ksud، والمدير بقي حيًا 120 ث |
| الجديدة (`run_bootA.sh`) | `0xffffff88679bade0` (تشغيل 9) | `0xffffff8785d6ade0` | **مختلفة** → متباعدة حتى مع نجاح كلتيهما |
`tools/t5loop.sh` يطبّق **`$ENVV` واحدة** على **كل** إزاحة، لذا جعل `MODE=CRED`
الطلقتين متطابقتين *بحكم البناء*. أما `run_bootA.sh` فقد أطلق الخطوة 5 والخطوة 6
كلًا بـ `$extra` فارغة، لذا رشت كل واحدة **صفحتها الخاصة**.
⇒ الشرط هو **"أن تكتب الطلقتان القيمة نفسها"**. `run_bootA.sh` يفرض ذلك الآن
(`SAME_VALUE=1`، وهو الافتراضي): الخطوة 6 تعيد استخدام `write_value` المرصودة
من الخطوة 5 حرفيًا، و**ترفض الإطلاق إطلاقًا** إذا لم تستطع استعادة تلك القيمة —
لأن الإطلاق سيبني زوجًا متباعدًا.
⚠ يجب أن يعيش `HOLD` أطول من الطلقة الثانية. إذا مات طفل PIN الخاص بالطلقة
الأولى أولًا، تُحرَّر الصفحة وتُعاد تخصيصها ويصبح "القيمة نفسها" مؤشرًا معلقًا.
الافتراضي `HOLD=20` **قصير جدًا**؛ استخدم `HOLD=600`. هذا هو الآن *الافتراضي*
كلما كان `SAME_VALUE=1` — فالقيمة غير المشروطة القديمة البالغة 20 ث كانت تعني أن
الإعداد الافتراضي هو نفسه الفخ — و`HOLD` قصير صريح مع `SAME_VALUE=1` يحذّر الآن
بصوت عالٍ بدلًا من إنتاج مؤشر معلق بصمت.
⚠ كان `CONTROL=1` يغيّر **الخطوة 5 فقط**، لذا كان ينتج
`(init_cred, fresh page)` — زوجًا متباعدًا — بينما ادّعى هذا الملف أنه يعيد إنتاج
الخلية 2. تم الإصلاح: الآن يضبط الطلقتين على `init_cred`. (تكلفة الخلية 2 قائمة:
الأثر الجانبي يفسد `init_cred+8` عالميًا، وهو ما يمثله
`Uid: 0 0 4294967176 0`.)
**التبعات على أي شيء يريد غسل الاعتماد**
(`setresgid` + `setresuid`، لتبديل الصفحة المرشوشة بـ `struct cred` حقيقي):
- الآلية حقيقية ومتحقق منها — `commit_creds` يكتب `x21` في **كلا**
`task+0x778` و`task+0x780` (`0x186998` / `0x1869a0`)، لذا استدعاء واحد يصلح
الانقسام نهائيًا؛ `prepare_creds @0xffffffc008186070` هو
`kmem_cache_alloc(cred_jar)` + `memcpy(new, task->cred, 0xA8)` +
`security_prepare_creds(...)`، و147/149 كلاهما على قائمة إعفاء الحارس.
- **لكن شرطه المسبق هو عكس "تخطَّ طلقة 0x778".** الغسل نفسه يستدعي
`commit_creds`، لذا يجب ألا يُصدر إلا عندما يكون **كلا** المؤشرين يحملان
القيمة نفسها بالفعل.
- `V12_LAUNDER=1` مشروط بـ**شيئين**، والأول ليس ملاحظة:
1. **`V12_W7_SAME_VALUE=1`** — حقيقة *المصدر* بأن الطلقتين أُعطيتا القيمة
نفسها. بدون بدائية قراءة، تكون هوية المؤشر غير قابلة للملاحظة، لذا لا يمكن
استبدال هذا بفحص أفضل في مساحة المستخدم؛ يجب التصريح به.
2. `consistent=1` — توافق رؤية `0x780` (`getuid()`) مع رؤية `0x778`
(`/proc/self/status` `Uid:`). **ضروري لكنه غير كافٍ بمفرده**: صفحتان
متميزتان تحملان كلتاهما `uid 0` تُقرآن متساويتين بينما المؤشرات مختلفة
— وهي بالضبط الحالة التي كان المشغّل يصنعها. بالنظر إلى (1)، يصبح كافيًا:
توافق + القيمة نفسها ⇒ نجحت كلتاهما على الصفحة نفسها.
فشل أي من الفحصين ⇒ ارفض، وجدول الحالات الأربع يدخل في الأدلة.
سطر تقرير LT يطبع كلا الرؤيتين (`uid=` / `real_uid=` / `consistent=`) بالإضافة
إلى `same_value_declared=` بحيث تُقرأ الحالة، لا تُستنتج.
### ★ الأداة 1 — الأثر الجانبي هو ختم موجّه نحو الهدف
`*(write_value + 8) = write_target`، و`cred+8` / `cred+0xc` هما `gid` / `suid`،
لذا مخزن واحد بحجم 8 بايت يهبط عبر كليهما:```
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 المُثبَّت للتو — لذا فإن
الطابع قابل للقراءة مباشرة من مساحة المستخدم. اقرأه قبل المرحلة 3: فالإصلاح
يصفّر cred+8 ويمحوه (يقرأ t5_repair.txt في notes.md §11
4294967176 قبل إصلاح ناجح و0 بعده).0، لذا تُبلّغ
صفحتان متمايزتان كلتاهما عن "التوافق". لكن low32(T+0x778) و
low32(T+0x780) يختلفان بمقدار 8 بالضبط، لذا مع صفحتين يتعارض getgid() (من
) و (من ) — و
يقارن gid فضلًا عن uid.⇒ V12_W7_SAME_VALUE هو البوابة الثانية، وليس الوحيدة. وهو لا يزال مهمًا:
فالطابع لا يميّز إلا إذا أُطلقت الآثار الجانبية كليهما، لذا تسدّ قاعدة القيمة نفسها
تلك الثغرة المتبقية. ولاحظ ما هي البوابة بالضبط — إنها كاشف، وليست مانعًا. لا
يمكنها إلا أن ترفض؛ فهي تترك المهمة متباعدة لبقية الإقلاع. وقاعدة القيمة نفسها هي
ما يجعل الزوج صحيحًا، وهو ما كانت السلسلة القديمة تملكه وما يلزم للوصول إلى
execve أصلًا.
probe_state ليس معيار هبوطلقد كان خاطئًا ثلاث مرات في هذا المشروع: هبط W1 على العام وأبلغ R؛ وكان D في
التشغيل 12 موجّهًا إلى عام بدلًا من cred؛ وكُتب R في التشغيل 11 في جدول كما لو
كان هبوطًا (run11_w778r1_miss.txt وw7_w7781.txt في التشغيل 7 متماثلان سطرًا
بسطر — كلاهما probe_state = R، probe_done = 0). استخدم أوراكل لكل هدف:
يستخدم run_bootA.sh الآن الطابع للمرحلة 1 — وهذا ما يجعل إعادات المحاولة
ROUNDS>1 على task+0x778 ذات معنى، لأن الجولة الفاشلة قابلة للقراءة بدلًا من
أن تُستنتج — ولن يطلق المرحلة 2 ما لم تهبط المرحلة 1.
⛔ قل "حقل awk"، ولا تقل أبدًا "الحقل الثالث". يطبع
uid_lineالتسمية أيضًا (Uid: 0 0 4294967176 0)، لذا فإن$1في awk هو"Uid:"وقيم المعرّفات الأربع هي$2..$5:$2=uid$3=euid$4=suid$5=fsuid. يقع الطابع عندcred+8، أيgid(low32) وsuid(hi32) — لذا فهوGid:$2وUid:. وتسميته "الحقل الثالث" (وهو ما يعدّ ، وهكذا تصوغه §11) يغري الكود بقراءة ، وهو = على الـ cred المزيف ولا يمكن أن يساوي أبدًا. كان هذا الخطأ بمقدار واحد موجودًا هنا: أعاد "لا طابع" لطلقة هبطت، فلم تُطلق المرحلة 2 أبدًا ورفضت بوابة الغسيل إلى الأبد — ، لأن "لا طابع" هو أيضًا النتيجة الطبيعية لخطأ حقيقي.
المعيار الذي لا يُختبر أبدًا مقابل عيّنة موجبة معروفة ليس معيارًا، بل تخمين —
وهذه الفئة من الإخفاقات (هذا الخطأ بمقدار واحد، probe_state، dmesg -w،
klog.host الفارغ، القراءة المرتجعة الفارغة) تظهر دائمًا كـ "لم يحدث شيء"، وهي
أيضًا نتيجة تجريبية مشروعة. لذا أصبح الفحص الآن مدفوعًا مرتين:
stamp_selftest() يعمل في الفحص المسبق ويُخرج exit 9 عند الفشل، مشغّلًا
نفس دوال الاستخراج التي تستخدمها البوابة مقابل القيم المقيسة من
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() ثلاث حالات، لأن "لا يمكن القراءة" ليس "لا طابع" (هذا الخلط
هو ما جعل التشغيل 13 يبدو كـ "لا تغيير"): 0 = موجود، 1 = قابل للقراءة ولا
طابع، 2 = غير قابل للقراءة. وعندما يعيد 1 بينما probe_state = D، يطبع
المشغّل ⛔ ORACLE INCONSISTENT — "اذهب وافحص المعيار" — بدلًا من رسالة "لم
يهبط"، التي ترسل المشغّل إلى مكان مختلف تمامًا (إقلاع جديد، أو البحث عن معدل
الإصابة).
الاشتقاق الكامل: evidence/2026-09-18-divergence-is-latent.md
وevidence/2026-09-18-cred-launder-verification.md
(§2.3 في الأخير مسحوب في مكانه). الفحص الذاتي لتداخل شكل الكتابة مغلق دون اتصال:
تعيش كلمات الشكل في شبكة fd_set على مكدّس النواة بينما يهبط الأثر الجانبي داخل
الصفحة المرشوشة، لذا لا يمكن للاثنين أن يتداخلا في أي من الشكلين.
ثلاثة مُبلّغين مستقلين. لا أحد منهم بديل عن الآخر، والمسار 1 وحده يمكنه قتل المهمة المستدعية.
يُطلق المسار 2 عند execve، وليس عند تغيّر بيانات الاعتماد، وهو مسار كود
منفصل عن المسار 1. يبلّغ عبر kevent_send_to_user، لذا ما يحدث بعد ذلك قرار
خفيّة في مساحة المستخدم، وليس قرار النواة.
الفحص على مسار الصورة التي يُنفَّذ عليها exec، لذا لا يكفي تحميل الحمولة
الخاصة بالمحمّل عبر memfd: إذا نُفِّذ المحمّل نفسه من /data/local/tmp، فإن أول
execve يبلّغ بالفعل. d_path() على memfd هو /memfd:…، لذا يجب تنفيذ
المحمّل نفسه عبر memfd — 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
لأن المسار 2 والمسار 3 يُبلّغان فقط عبر kevent، فإن "عدم وجود [ROOTCHECK-*] في سجل النواة" لا يستبعد أن يكون أيٌّ منهما قد أُطلق. هذا الاستنتاج يحتاج إلى مستقبِل مساحة المستخدم، والذي لم نتمكن من تحديد موقعه.
oplus_security_guard.koذاكرة sys_enter المؤقتة:```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` check:```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 إلا عندما لا ينحدر أي معرّف. لذا يتم الدخول إلى الإرسال عندما إما ينحدر بعض المعرّفات أو addr_limit == KERNEL_DS؛ وهو ليس محكومًا بـ addr_limit.
النتائج المترتبة:
sys_enter الخاص به لا يزال يخزّن مؤقتًا uid القديم. إذا كانت المهمة بالفعل uid=0 عند دخول استدعاء نظام (0x400 cbz)، فإن الخطاف يعود ويبقى أعمى من ذلك الحين فصاعدًا.delivery/外部建议评审_2026-09-18.md.g_boot_state — بايت واحد .data..ro_after_init، يُضبط عند تهيئة الوحدة من verified_bootstate عبر strstr. is_unlocked() = LDRB + RET.
كتابات VA للوحدة تسبب خطأ (CONFIG_STRICT_MODULE_RWX=y) — استخدم الاسم المستعار physmap 0xffffff80….
حمولة التقرير: $$sys_call_number@@%d، $$set_id_flag@@%d، $$addr_limit@@%lx، $$enforce@@%d.
.rodata+0، الفهارس 143–214الإدخالات الستون المتبقية → تقرير + قتل.
لا تحجب خيطًا على sendmsg (211) أثناء تغيّر بيانات اعتماده. فهو ليس في الجدول، لذا يتم الإبلاغ عن الخيط وقتله. استدعاءات النظام الوحيدة الآمنة للحجب فيها لهذا الغرض هي الاثنا عشر أعلاه:
setregid، setgid، setreuid، setuid، setresuid، setresgid،
connect، getsockname، setsockopt، shutdown، readahead، brk.
تصحيح (2026-09-18). نسخة سابقة من هذا الجدول كانت تُسمّي كل إدخال أقل بواحد من رقم استدعاء النظام الحقيقي في arm64 (
146كان يُسمىsetresuid؛ وهوsetuid— أماsetresuidفهو147). كانت الأرقام دائمًا صحيحة؛ الأسماء فقط كانت خاطئة. تُستخرج الأسماء الآن من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 ثابتة — فهي تحافظ على هندسة إطار المكدس الخاص بالاسترجاع (معايرة delta=0). تغيير أيٍّ منها يتطلب إعادة المعايرة على الجهاز.```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؛ واشتقاق g_boot_state
الكامل بما في ذلك مفتاح strstr؛ وجدول الإعفاء المصحَّح؛ ووصفة الالتقاط
التي ستُنتج النصف الناقص من النواة؛ وقائمة بما لا يزال
مفتوحاً.
evidence/2026-09-18-bootA/ — أول
تشغيل للجهاز بالتصميم الحالي، 13 إقلاعاً. عمليات إعادة التشغيل منتظمة
(bootreason=reboot) ولم يُلتقط أي سطر panic على الإطلاق — لكن اقرأ ذلك
كعيّنة من واحدة، لا ثلاث عشرة. من بين عمليات التشغيل الأربع التي أعادت التشغيل، حفظت اثنتان
klog.host فارغاً، وحفظت واحدة سجل الإقلاع التالي، ونافذة واحدة فقط يمكن
أن تُحتمل أنها تؤطّر إعادة تشغيلها الخاصة. وبالمثل، فإن probe_state وقراءة الضحية
في إحدى عمليات التشغيل كلاهما فارغ (كان الجهاز قد اختفى بالفعل)، لذا لا يحمل
أي معلومة حول ما إذا كانت كتابته قد استقرّت — الحقل الفارغ ليس "لا تغيير".
كما يوثّق الدليل خطأ المنهجية الجدير بالمعرفة:
dmesg -w عملية لا أثر لها على هذا الجهاز (toybox يُفرّغ مرة واحدة ويخرج)، لذا
احتوى سجل نواة عملية تشغيل سابقة على تاريخ ما قبل الالتقاط فقط — "لا [ROOTCHECK-*]" لم يكن
دليلاً على أي شيء. يحمل evidence/notes.md §6 وصفة
الاستقصاء والبث إلى المضيف المصحَّحة.
evidence/2026-09-18-cred-launder-verification.md
— التحقق من مقترح غسل بيانات الاعتماد مقابل تفكيك هذه الصورة نفسها (وليس
مصدر 5.10 العام): المخزن المزدوج في commit_creds، وتخصيص prepare_creds
وsizeof(struct cred) = 0xA8 المقيس، وخطر التباعد
BUG_ON(cred != real_cred) أعلاه، والفحص الذاتي المغلق لتداخل شكل الكتابة،
وتدقيق تغطية الأدلة لعمليات الإقلاع الـ13.
§2.3 مسحوبة في مكانها — التباعد كامن، وليس آلية
إعادة التشغيل.
evidence/2026-09-18-divergence-is-latent.md
— تحقق الجولة الثانية. يأخذ commit_creds مهمته من current
(0x1867a0 mrs x20, sp_el0)، ويُصفّر exit_creds كلا المؤشرين قبل put_cred،
وتُظهر اللقطات الخام أن السلسلة القديمة كتبت قيمة واحدة متطابقة
(0xffffff802a7e0be0) في كلا الفتحتين بينما كتبت السلسلة الجديدة صفحتين مختلفتين.
إذن المعيار هو تساوي المؤشرات — "كلا الطلقتين تكتبان القيمة نفسها"،
وليس "كلا الطلقتين تستقرّان".
postreboot_forensics.sh — تحقيق ما بعد إعادة التشغيل الذي
لا يعتمد على المستقصي. المعيار شرط واحد:
CONFIG_PSTORE_CONSOLE=y يجعل panic() يكتب ذيل الطرفية في ramoops عند
kmsg_dump(KMSG_DUMP_PANIC) — قبل أي إعادة ضبط — لذا لا يهم ما إذا كان الصندوق
بعدها يعيد التشغيل أو يتعلّق. يسحب /sys/fs/pstore/، ويبحث عن kernel BUG /
__put_cred / cred.c، ويطبع سلسلة سبب الإقلاع (حملت مدخلات السجل لواحق
reboot,shell / bootloader / reboot,edl، لذا يميّز السبب
فاعلاً حيث لا تميّزه الحقبة).
⚠ لا تقرأ "
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/* للمستخدم الجذر فقط، لذا تحت Enforcing
يفشل كل من adb pull وcat — و"تعذّر القراءة" يُنتج المخرجات نفسها
التي يُنتجها "قرأته وكان فارغاً". سكربت بحالتين يطبع "pstore فارغ ⇒
panic مدحوض" من قناة لم يفتحها قط. لذا يُصدر السكربت
CHANNEL UNREACHABLE (فشل ls، أو فشلت جميع المدخلات المعروفة في القراءة
بدلاً من عدم وجودها) ويبلّغ عن getenforce إلى جانب ذلك.adb reboot يتبعها جلب فوري. إذا
لم يُنتج إقلاع سليم معروف شيئاً قابلاً للقراءة، فالقناة غير مُثبتة وكل
"pstore فارغ" لاحق ليس دليلاً. الترتيب مهم: ينقل الجهاز
السجل ويفصل رابطه بعد الإقلاع بقليل، لذا فإن التسلسل هو
إعادة التشغيل → الحصول على Permissive (W1) → تشغيل السكربت فوراً.run_bootA.sh — تنسيق ذلك الإقلاع الواحد، بالترتيب
المهم (0x778 → 0x780 بالقيمة نفسها → إصلاح محلي لبيانات الاعتماد
التي ثُبّتت فعلاً → التأكيد → وعندها فقط النبش). ADB=/SER=/
BIN_LOCAL= قابلة للتجاوز؛ SAME_VALUE=1 (افتراضي) يفرض قاعدة القيمة نفسها،
وLAUNDER=1 يفعّل الغسل المُبوّب، وHOLD=600 مطلوب لتسلسل
القيمة نفسها.
إعادات المحاولة مقسّمة لكل مرحلة (R5/R6)، لأن المرحلتين لهما
ملفّا مخاطر متعاكسان:
القيمة الافتراضية لـR5 هي ROUNDS، ولـR6 هي 1.
تشغيل الغسل الموصى به — مسار الصفحة المرشوشة الافتراضي، وليس
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
المشغّل كان يطابق الصياغة الأولى، لذا عند CONTROL=1 عاد الاستخراج
فارغًا، فتخطّى if [ -n "$CRED" ] الإصلاح، وأبقى حدّ [ -n "$CRED" ] الخاص
بالاقتران ذي القيمة نفسها عند 0 — وكانت بوابة الغسل سترفض إلى الأبد. عملية
لا-فعل صامتة على كلا الجانبين، من تعبير نمطي تعرّف على واحد من موقعَي طباعة.
كلاهما الآن، واستخراج write_target (وهو مدخل معيار الطابع)، يمرّان عبر
wv_from/wt_from، وهما مُختبَران بواسطة اختبار الانحدار إلى جانب المعيار نفسه.
كلا التدفقين يبدآن قبل الشيء الذي يقيسانه. يعمل uid.stream من المرحلة 1؛
ويبدأ cred.stream عند النكزة (poke)، لا بعد المراقبة — فالنكزة تُطلق
التابع إلى حلقة تقرير NO_EXEC الخاصة به، وهي 240 × 0.5 ث = 120 ث ثم
_exit(0) (exploit.c: "LT child NO-EXEC mode done (120s)")، لذا فإن الموضع
القديم عند t+~135 ث بدأ أخذ العينات بعد أن كان التابع قد ذهب بالفعل، في
النافذة بالضبط التي توجد الأداة من أجلها. كما يرفض المشغّل المتابعة إذا فشل
stamp_selftest()، ويطبع ORACLE INCONSISTENT بدلًا من "did not land" عندما
يختلف الطابع وprobe_state.
artifacts/guard_post_handler.s — سلسلة
القتل مع تعبئة الإزاحات (relocations). إن adrp x9, #0 في القوائم الأقدم هو
.data..ro_after_init؛ وbl #0x4ac هو oplus_root_check_succ. أعد التوليد
بـ tools/gen_guard_disasm.py بعد سحب وحدات البائع (vendor modules) من جهازك.
tools/kdis_ko.py — RELA مطابق بواسطة sh_info؛ في هذه البِنى توجد إزاحات
.text في .rela.text.<func>، لذا فإن البحث بالاسم لا يعيد شيئًا.
| Project | |
|---|---|
| JoinChang/ghostlock-oneplus | reference implementation; 5.10 compact waiter |
| NebuSec CyberMeowfia | original GhostLock research |
GPL-3.0 — see LICENSE.
| الجهاز | OPPO Find X5 Pro (PFEM10) |
| SoC | SM8450 / Adreno 730 |
| نظام التشغيل | ColorOS 16.0.3.520 (CN01) |
| النواة | 5.10.236-android12-9-o-gaf2075ad2c06 |
| محمّل الإقلاع | مقفل، أخضر |
| VA_BITS | 39 — KIMAGE_TEXT_BASE = 0xffffffc008000000 |
| المرحلة |
|---|
مُشغّل waiter المضغوط (CMP_REQUEUE_PI → EDEADLK) | يعمل |
تسريب task_struct (perf) | يعمل |
كتابة PI (8 بايت؛ القيمة = 0 أو عنوان نواة صالح) | يعمل |
task+0x778 أو task+0x780 وحده → Uid=root | يعمل — لكن الهبوط بحقل واحد يترك المهمة متباعدة، وهذا BUG_ON صلب كامن. انظر خطر التباعد |
| كتابة كلا الحقلين بقيمة واحدة (زوج متسق) | ❌ لم يتحقق أبداً بصفحة مرشوشة. لوحظ فقط مع الاسم المستعار العام init_cred (09-14، CONTROL=1). المُشغّل الآن يفرضه (SAME_VALUE=1)؛ لم يُشغَّل على الجهاز |
غسل بيانات الاعتماد (setresgid + setresuid) | مُنفَّذ خلف V12_LAUNDER=1؛ لم يُشغَّل على الجهاز |
تحميل kernelsu.ko | يعمل |
| بقاء عملية الجذر | ⚠ غير مُثبت — انظر أدناه |
| آلية إعادة التشغيل | ❌ غير مُثبتة. أحد المرشحين (التباعد) أصبح الآن مستبعداً؛ انظر أدناه |
probe_state كمعيار للهبوط | ❌ خاطئ — لا تستخدمه. ثلاثة أمثلة مضادة؛ انظر الجدول أدناه |
| قناة panic الخاصة بـ pstore/ramoops | ⚠ الأداة موجودة؛ القناة لم تُتحقق منها أبداً (لا اختبار فارغ بعد) |
| "الضحية تدور في فضاء المستخدم الخالص" | ⚠ لا قراءة بعد — uid.stream الآن يسجّل utime/stime/nvcsw حتى يمكن التحقق |
| الكتابة المزدوجة أحادية التمرير من جهة pi | ⚠ غير مُثبتة؛ pi.pc/pi.left مُثبَّتان على 0 في fdset_map.h |
المسار A (UMH / modprobe_path) | STATIC_USERMODEHELPER_PATH="" |
| الحقل | الإزاحة |
|---|
real_cred / cred | 0x778 / 0x780 |
syscallno المخزّن مؤقتاً | 0xdf8 |
uid / euid / gid / egid المخزّنة مؤقتاً | 0xe00 / 0xe08 / 0xe10 / 0xe18 |
flags0x0 |
addr_limit | 0x8 |
ttbr0 | 0x10 |
preempt_count | 0x18 |
| الحقل | الإزاحة | الحقل | الإزاحة |
|---|
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) — الطابع أعلاه؛ يُقرأ قبل المرحلة 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 | نزل بعض المعرّفات، أو 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 | kretprobes الخاصة بـ oplus_secure_harden | setsockopt optname ∈ {41,42,48}، setxattr، /proc/cpuinfo، إعادة تحميل سياسة SELinux | 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. |