
استغلال تصعيد صلاحيات محلي (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.
| الجهاز | 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، غير مُصلَحة في هذه الصورة (كما يظهر من التفكيك، وليس من رقم الإصدار) |
يشغّل 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 في ثلاثة مواضع، لأن أحدها هو الذي ستصل إليه فعلًا:
| المسار | السبب |
|---|---|
/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 جديد |
يستمع البرنامج الخفي على /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 مطابقان بايتًا ببايت بين المستودعين)، مع مزيد من التطوير.
| الملف | العلاقة |
|---|---|
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 |
كل سطر في الاستغلال الفعلي — 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 يدويًا — وبدون هذين السطرين كانت حدود الكتلة ستكون الشيء الوحيد الذي ما تزال المكتبة تُصدّره.
| المتغير | التأثير |
|---|---|
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> | تجاوز إزاحة تراكب المكدس (يستبدل، ولا يضيف) |
لم يكن أيٌّ من هذه ضروريًا في التشغيل المُتحقَّق منه.