
استغلال تصعيد صلاحيات محلي (LPE) لنظام أندرويد لثغرة CVE-2026-43499 يستهدف OPPO PMG110 (kernel 6.6). يستخدم ثغرة الاستخدام بعد التحرير (UAF) في futex PI للحصول على صلاحيات الجذر وتثبيت خفي su عبر LD_PRELOAD.
CVE-2026-43499 (futex PI rt_mutex_waiter use-after-free) تصعيد صلاحيات محلي، منقول إلى OPPO PMG110 / K15 Pro+ — MediaTek MT6991، ColorOS 16.
ملف واحد يُدفع، ويُشغَّل عبر LD_PRELOAD:
adb push out/preload-pmg110-16.0.9.400.so /data/local/tmp/preload.so
adb shell chmod 644 /data/local/tmp/preload.so
adb shell LD_PRELOAD=/data/local/tmp/preload.so /system/bin/true
عند النجاح، يُترك su دائم خلفك:
adb shell /data/local/tmp/su -c id # uid=0(root)
تم التحقق على الجهاز (2026-07-27): uid=0 خلال حوالي 35 ثانية من تشغيل بسيط دون أي تجاوز لمتغيرات البيئة، وكان su يستجيب بعد ذلك من adb shell عادي غير مميَّز:
$ adb shell "/data/local/tmp/su -c 'echo 0 > /proc/sys/kernel/kptr_restrict'"
$ adb shell "/data/local/tmp/su -c 'grep -w init_task /proc/kallsyms'"
ffffffe89033e780 D init_task
تم التحقق من الاستغلال وتثبيت su معًا على هذا الجهاز. وما قرأته تلك الصدفة المُخترَقة لاحقًا يؤكد أيضًا P0_KERNEL_PHYS_LOAD وإزاحات الرموز وKS_MTE_TAGGED=0 بشكل مستقل عن الاستغلال — انظر targets/pmg110-16.0.9.400/NOTES.md.
يشغّل Write 1 (SELinux permissive) وWrite 2 (cred → init_cred)، ويجعل عمليةً فرعية تحصل على uid=0، ومن هناك يثبّت برنامج su الخفي المضمّن.
su ليس أثرًا ثانيًا: su_daemon.c يُبنى كـ aarch64 PIE مستقل ويُضمَّن عبر .incbin داخل .rodata الخاصة بالمكتبة، فينتقل داخل preload.so ويُكتب مرة أخرى في وقت التشغيل. هذا نهج warhol-root دون تغيير.ksud، لا KernelSUيُثبَّت su في ثلاثة مواضع، لأن أحدها هو الذي ستصل إليه فعلًا:
يستمع البرنامج الخفي على /data/local/tmp/temp_su.sock ويسجّل في /data/local/tmp/su_daemon.log. صلاحيات root لا تدوم عبر إعادة التشغيل — أعد تشغيل سطر LD_PRELOAD بعد كل إقلاع.
لتثبيت KernelSU، استخدم مسار /data/local/tmp/a/e في ghostlock-oneplus بدلًا من ذلك.
كل شيء باستثناء نواة الاستغلال هو من warhol-root، مأخوذ لا مُعاد اختراعه:
targets/<device>/، تُنقل إلى source/src/ وقت البناء، بحيث لا يترك تبديل DEVICE ملفات رأس الجهاز السابق خلفه أبدًاsource/Makefile (NDK إن وُجد، أو clang المضيف مع NDK sysroot إن لم يوجد) وقاعدة التضمين ذات المرحلتين التي تُنتج build/embed/su_daemon_aarch64_pie قبل ربط ملف .sosu_daemon.c وsu_blob.S مطابقان بايتًا ببايت لملفات warhol-root، وsu_install.c هو مثبّت preload.c الخاص بـ warhol-rootنواة الاستغلال ليست من warhol-root. warhol-root هو popsicle، وهو مقيَّد بـ GKI 6.12 / android16 وتَرفض generate_target.py الخاصة به أي لافتة أخرى. PMG110 يعمل بنواة 6.6 / android15، لذا فالنواة هنا من شجرة ghostlock 6.6 — وهي نفسها سليلة لنفس الشيفرة (kernelsnitch/utils.h وtimeutils.h مطابقان بايتًا ببايت بين المستودعين)، مع مزيد من التطوير.
كل سطر في الاستغلال الفعلي — Write 1، وWrite 2، وKernelSnitch، ومسار pselect — هو نفس الشيفرة في الشجرتين.
هذا هو الاختلاف البنيوي الوحيد، وهو مفروض لأن الشجرتين تحصلان على root بشكل مختلف.
يمنح warhol-root الصلاحيات لعملية الاستغلال نفسها ولذلك يستدعي install_embedded_su() مباشرة من run_direct_root(). هنا تبدّل Write 2 مؤشر cred لـعملية فرعية مُفترَعة بينما يبقى الأب هو المستدعي غير المميَّز، لذا فالعملية الفرعية في child_main() هي السياق الوحيد القادر على التثبيت — وهناك يعمل.
تحمل الشجرتان نفس كعب install_embedded_su() الضعيف في util.c الذي يعيد ENOSYS؛ وتوفير التعريف القوي هو ما يُفعّل المسار. من الجدير معرفة ذلك لأن بناءً يُسقط su_install.c لسبب ما سيظل يُربط ويُشغَّل — لكنه سيُبلّغ su=0/38 ولن يثبّت شيئًا.
make # = make preload -> out/preload-<DEVICE>.so
make DEVICE=<name> # use targets/<name>/
make devices # list available DEVICE values
make info # show the selected target and the resolved toolchain
يتم اكتشاف سلسلة الأدوات تلقائيًا: ANDROID_NDK_HOME / ANDROID_NDK_ROOT أولًا، ثم مواقع تثبيت NDK المعتادة على Linux وmacOS، وإن فشلت جميعها، يُستخدم clang المضيف مستهدفًا NDK sysroot. اضبط ANDROID_NDK_HOME فقط لتجاوز البحث. يطبع make info ما اختاره.
يتكون البناء من مرحلتين، وهذا هو الجزء الجدير بمعرفته:
su_daemon.c → build/embed/su_daemon_aarch64_pie, وهو aarch64 PIE مستقلsu_blob.S يُضمّن ذلك الملف الثنائي في .rodata عبر .incbin، ويُربط كل ذلك في ملف preload.so الواحدإذن make clean وإعادة البناء هما الطريقة الوحيدة لتغيير su المضمّن — يكفي تعديل su_daemon.c وحده، فالاعتماد مُعلَن، لكن الكتلة (blob) أثر بناء وغير متتبَّعة.
تتم إعادة نقل targets/<device>/{target.h,device_offsets.h} إلى source/src/ في كل بناء، لذا لا يمكن أن يُلتقط ملف رأس قديم من جهاز آخر دون أن يُلاحَظ.
out/*.so غير متتبَّع (نفس الاتفاقية المتبعة في warhol-root) — استنسِخ ثم نفّذ make.
يُبنى ملف .so باستخدام -fvisibility=hidden ويُصدّر صفر رموز. رموز مكتبة LD_PRELOAD تفوز في البحث عن الرموز للعملية بأكملها، لذا فإن أي رمز تُصدّره قد يحجب رمزًا بنفس الاسم في الملف الثنائي المضيف أو في libc. هذا العلم يتحكم في توليد شيفرة C فقط، لذا يعلّم su_blob.S رمزَيه .hidden يدويًا — وبدون هذين السطرين كانت حدود الكتلة ستكون الشيء الوحيد الذي ما تزال المكتبة تُصدّره.
لم يكن أيٌّ من هذه ضروريًا في التشغيل المُتحقَّق منه.
[*] futex_hashsize 2048 (8 possible CPUs)
[*] ks collisions=3/3 baseline=8 threshold=10x (80) accepted=[1244..1597] slowest_rejected=N
[+] child uid = 0
[+] embedded su wrote 15304 bytes to /apex/com.android.virt/bin/su
[+] embedded su daemon ready pid=NNNN socket=/data/local/tmp/temp_su.sock daemon=/apex/com.android.virt/bin/su
[+] embedded su install ok=1 errno=0 daemon=NNNN
[+] su ready: /data/local/tmp/su and /apex/com.android.virt/bin/su
[+] ghostlock preload verdict: EXPLOIT OK
child uid = 0 تعني نجاح الاستغلال؛ وكل ما بعدها هو التثبيت. يُبلَّغ الاثنان منفصلين عمدًا، وكذلك الحكم:
الحالة الوسطى هي التمييز الجدير بالاهتمام: فهي تقول إن الإزاحات في target.h صحيحة لهذا البناء وإن المشكلة في مكان ما في التثبيت، وهو أمر مختلف تمامًا عند التنقيح. su=0/38 (ENOSYS) داخلها تعني تحديدًا أن الكعب الضعيف هو ما تم ربطه.
mm_struct leak failed متبوعًا بـ prepare_kernel_page retry N/24 ليس فشلًا. إنه تقدم الحلقة، والتشغيل الناجح يُظهره أيضًا. لم يفشل شيء حتى تُستنفد 24 محاولة ويظهر prepare_kernel_page timeout. وبالمثل، فإن probing cfi ... expected=9 مع child uid = 2000 هي جولة واحدة ناقصة من أصل عشر.
لا تحكم على تشغيل من سجل مبتور — هذا الخطأ كلّف جولة كاملة من التشخيص الخاطئ هنا.
التشغيل الفاشل بالكامل أمر طبيعي أيضًا. سباق pselect ليس مضمونًا 100%: يمكن أن يخسره تشغيل خمس مرات متتالية وينتهي بـ Write 1 failed، ثم يربحه التشغيل التالي من المحاولة الأولى مع ret=9. لوحظ هذا على هذا الجهاز. ret=4 expected=9 هو شكل خسارة السباق، وليس target.h خاطئًا — فشل واحد ليس سببًا لإعادة اشتقاق الإزاحات. أعد تشغيله.
لأغراض البحث الأمني المصرَّح به والتعليم فقط.
| الجهاز | OPPO PMG110 / K15 Pro+ / OP61E5L1 |
| SoC | MediaTek MT6991 (Dimensity 9500s) |
| النواة | 6.6.118-android15-8-g93e223c276e7-abogki500782043-4k (GKI, 4K pages) |
| الإصدار | ColorOS 16 / PMG110_16.0.9.400(CN01) — نفس بايتات النواة في 16.0.8.300 |
| الثغرة | CVE-2026-43499، غير مُصلَحة في هذه الصورة (كما يظهر من التفكيك، وليس من رقم الإصدار) |
| المسار | السبب |
|---|
/apex/com.android.virt/bin/su | على tmpfs مُثبَّت فوق هذا الدليل؛ وهو في PATH لصدفة root |
/data/local/tmp/su | يمكن الوصول إليه من adb shell عادي دون حيل PATH |
/apex/com.android.virt/bin/su في مساحة أسماء التركيب الخاصة بـ adbd | يُثبَّت عبر setns، لذا فسيراه أي adb shell جديد |
| الملف | العلاقة |
|---|
util.c slide.c fops.c pipe.c root.c miniadb.c common.h offset.h kernelsnitch/* | من ghostlock، مطابق بايتًا ببايت |
su_daemon.c su_blob.S | من warhol-root، مطابق بايتًا ببايت (يضيف su_blob.S سطرَي .hidden — انظر البناء) |
su_install.c | مثبّت preload.c الخاص بـ warhol-root، نُقل إلى ملف خاص به لأن preload.c في هذه الشجرة لديه مهمة أخرى أصلًا |
main.c | من ghostlock، مع استدعاء su في العملية الفرعية المُخترَقة وإبلاغ النتيجة |
preload.c | موجود هنا فقط — المُنشئ وسجل الوجهتين |
offsets.h | تعريف البنية فقط؛ الإدخال يُنقل من targets/<device>/device_offsets.h |
| المتغير | التأثير |
|---|
GHOSTLOCK_LOG | وجهة التسجيل (الافتراضي /data/local/tmp/.ghostlock.log)؛ الإخراج يذهب إلى stdout و الملف |
GHOSTLOCK_KS_VERBOSE=1 | طباعة عناوين التصادم ونطاقات المسح الخاصة بـ KernelSnitch |
GHOSTLOCK_KS_THRESHOLD=<n> | تجاوز مضاعِف عتبة التصادم |
GHOSTLOCK_MTE=1 | مسح وسوم مؤشرات النواة أيضًا (أبطأ 15 مرة) |
GHOSTLOCK_PHYS_LOAD=0x... | تجاوز عنوان التحميل الفيزيائي للنواة |
PSELECT_SHIFT=<n> | تجاوز إزاحة تراكب المكدس (يستبدل، ولا يضيف) |
| الحكم | المعنى |
|---|
EXPLOIT OK | root، وsu يستجيب |
EXPLOIT OK, SU INSTALL FAILED | نجح Write 1 وWrite 2؛ التثبيت فقط هو ما أخطأ |
EXPLOIT FAILED | لم تنجح عمليات الكتابة |
ABORTED | انتهى التشغيل قبل أن يتمكن من الإبلاغ — اقرأ آخر سطر [!] |
| المسار | المحتويات |
|---|
source/src/preload.c | المُنشئ: يشغّل الاستغلال، يُبلّغ، يتوقف |
source/src/main.c | الاستغلال نفسه (Write 1 / Write 2) |
source/src/su_daemon.c | ملف su الثنائي — يُبنى مستقلًا كـ aarch64 PIE، غير مرتبط داخل .so |
source/src/su_blob.S | يُضمّن ذلك الـ PIE عبر .incbin داخل .rodata الخاصة بملف .so |
source/src/su_install.c | يكتب الكتلة مرة أخرى، يبدأ البرنامج الخفي، يختبره |
source/src/target.h | وجهة النقل (مستثناة من تتبع git) |
targets/<device>/target.h | تخطيط زمن الترجمة: إزاحات البنيات، ثوابت physmap، أشكال slab وfutex |
targets/<device>/device_offsets.h | إزاحات الرموز العامة من kallsyms |
tools/extract_device.py | boot.img → الإزاحات، وحقول بنيات BTF، ونتيجة تراكب pselect |
tools/preloader_memlayout.py | MediaTek preloader → P0_KERNEL_PHYS_LOAD |
tools/qemu_verify.py | يُقلع النواة تحت QEMU: يقيس تراكب المكدس، ويتحقق من ثبات الخريطة الخطية |
tools/device_probe.sh | فحص مسبق من adb shell غير مميَّز |