
تحليل السبب الجذري لثغرة CVE-2026-54107: استخدام بعد التحرير (use-after-free) في win32kfull.sys بنظام Windows، مع تصحيح أخطاء حالة السباق، والتحليل الساكن، ورؤى فرز MSRC، وبحث عملي في استغلال النواة.
ValidateHwnd بوابة: التحليل الجذري لـ CVE-2026-54107استخدام بعد التحرير (use-after-free) في إدارة دورة حياة النوافذ في
win32kfull.sys— كيف وجدته، وكيف أقنعت نفسي بأنه حقيقي، وكيف بدت عملية MSRC فعليًا من منظور الباحث.
كانت الساعة تقترب من الثانية صباحًا عندما توقفت الآلة الافتراضية المستهدفة عن الاستجابة لنبض المُنقِّح (debugger) وسقطت في نقطة توقف عند التعليمات التي قضيت أسابيع أُثبت أنها قابلة للوصول بالضبط. لم يكن تأكيدًا، ولا توقفًا بسبب تلف تجمّع — بل انتهاك وصول بسيط في مسار توزيع الرسائل، أثناء فك إشارة كائن كان قد هُدم بالفعل بواسطة خيط آخر.
تلك النقطة أصبحت CVE-2026-54107، الحالة 11xxxxx لدى MSRC، وتم إصلاحها في تحديث الأمان لشهر يوليو 2026 عبر 27 منتجًا من منتجات Windows.
هذا المنشور هو النصف غير الخاضع لقيود النشر من القصة: السبب الجذري، ولماذا هذه الفئة من الثغرات على ما هي عليه، والمنطق الذي أوصلني إليها. تبقى تفاصيل الاستغلال خارج النطاق.
Win32k هو النصف الذي يعمل في وضع النواة من النظام الفرعي الرسومي في Windows. إنه قديم، وضخم، والأهم من ذلك — أنه يمكن الوصول إليه من سياقات من المفترض أن تكون غير موثوقة. هذه الخاصية الأخيرة هي السبب في بقائه هدفًا دائمًا للبحث على الرغم من عشرين عامًا من أعمال التحصين والتصفية وتقييد استدعاءات النظام.
داخل win32k، يكون كائن tagWND (PWND) مثيرًا للاهتمام بشكل غير عادي لأن عمره يُدار بأكثر من آلية واحدة في الوقت نفسه. النافذة هي:
ValidateHwnd،أي كائن له عدة مسارات إسناد مستقلة ومسار تدمير مشترك واحد يستحق القراءة بتمعن. هذا ليس ادعاءً بوجود ثغرة — بل هو استدلال إرشادي حول أين تستثمر وقتك.
ما جعلني أتوقف عند هذا المكوّن هو سطح الاستيراد. يستورد win32kfull.sys ثلاث بدائيات مرجعية كائنات مميزة من ntoskrnl:```c
NTSTATUS ObReferenceObjectByPointer(
void *Object,
uint32_t DesiredAccess,
POBJECT_TYPE ObjectType,
char AccessMode);
NTSTATUS ObReferenceObjectByHandle( HANDLE Handle, uint32_t DesiredAccess, POBJECT_TYPE ObjectType, char AccessMode, void **Object, OBJECT_HANDLE_INFORMATION *HandleInformation);
NTSTATUS ObReferenceObjectByName(/* ... */);
ثلاث طرق للدخول، ومسار واحد للخروج عبر `ObfDereferenceObject`.
هذا لا يعني أن الكود خاطئ. إنه يعني أن **الخاصية الثابتة (invariant) موزّعة** — لا توجد دالة واحدة تملك *"هذا الكائن حي الآن،"* لذا تعتمد الصحة على اتفاق كل مستدعي على أي مرجع يحملونه وإلى متى يظل صالحًا.
الثوابت الموزّعة هي المكان الذي تعيش فيه حالات السباق، لأن السباق ليس أبدًا خللًا منطقيًا يمكنك رؤيته في دالة واحدة. بل هو خلل في افتراض مشترك عبر دالتين.
لذلك السؤال الذي بدأت أطرحه على كل دالة تلامس `PWND` لم يكن *"هل هذا الكود صحيح؟"* بل:
> **إذا نُفِّذ جسم هذه الدالة نفسه على خيطين بفارق بضع تعليمات، فأيُّهما يكون على خطأ؟**
## 3. السبب الجذري
الخلل هو **فجوة وقت-الفحص / وقت-الاستخدام (time-of-check / time-of-use) التي تقع بين تحرير المرجع وتفكيك الكائن** في مسار تدمير النافذة، دون مزامنة كافية مع مستهلك متزامن يتحقق من صحة المقبض.
عند تجريده إلى شكله الأساسي:```c
/* Thread A — teardown */
NtUserDestroyWindow(HWND hwnd)
{
PWND pWnd = ValidateHwnd(hwnd);
if (pWnd) {
ObfDereferenceObject(pWnd); /* reference released */
/* <-- race window: object may become reclaimable here */
FreeWindowObject(pWnd); /* teardown proceeds on a pointer
no longer guaranteed live */
}
}
لم يتم توفير أي نص في الإدخال (INPUT) لترجمته. يرجى إرسال المحتوى المطلوب ترجمته.```c /* Thread B — consumer, concurrent / NtUserMessageCall(HWND hwnd, UINT msg, ...) { PWND pWnd = ValidateHwnd(hwnd); / may resolve a handle whose object is mid-teardown */
if (pWnd->fnid == FNID_BUTTON) /* use-after-free */
...
}
يجب أن يتحقق أمران حتى يكون لهذا أهمية، وقد تحقق كلاهما:
**(أ) النافذة حقيقية.** `ValidateHwnd` هي البوابة التي يُفترض أن تجعل الوصول المعتمد على المقابض آمنًا. إذا كان التحقق يمكن أن ينجح ضد كائن بدأ تفكيكه بالفعل، فهذه البوابة ليست بوابة — بل اقتراح.
**(ب) الذاكرة المحررة قابلة للتأثير من قِبل المهاجم.** الحقول التي تُقرأ مباشرة بعد التحقق تتضمن `fnid`، الذي يوجّه توزيع الرسائل. قرار التوزيع المُتخذ من ذاكرة مستعادة هو الفرق بين *"انهيار غير موثوق"* و*"انتهاك لحدود الأمان"*. هذا التمييز هو السبب الكامل لكون هذا CWE-362 مع تأثير تصعيد الصلاحيات (EoP) وليس خلل استقرار.
> إن **الفساد** الملاحظ هو استخدام بعد التحرير (use-after-free)؛ أما **السبب** فهو CWE-362، وهو تنفيذ متزامن يستخدم موردًا مشتركًا دون مزامنة صحيحة. هاتان عبارتان مختلفتان وMSRC تهتم بالثانية. **أبلغ عن السبب، وليس العرض فقط.**
### لماذا سباقات win32k أصعب هيكليًا مما تبدو
إذا كنت قد سبقت خللًا في أنظمة فرعية أخرى، فسوف يُحبِطك win32k، لأن البنية تقاومك بثلاث طرق محددة.
**النوافذ لها تقارب مع الخيوط (thread affinity).** تنتمي النافذة إلى الخيط الذي أنشأها. جزء كبير من النظام الفرعي مبني على افتراض أن الخيط المالك هو من يلمس الكائن، مما يعني أن النهج الساذج "أدر خيطين يستدعيان نفس الواجهة البرمجية" غالبًا لا يُحدث أي تداخل — أنت لا تخلق سباقًا، بل تخلق طابور انتظار. جعل مسارين يصطدمان فعليًا على نفس الكائن يتطلب فهم العمليات التي تنفذ فعليًا على خيط المتصل مقابل تلك التي تُمرَّر إلى خيط المالك.
**توزيع الرسائل يفرض تسلسلًا جزئيًا عليك.** الإرساليات (Sends) والمنشورات (Posts) تتصرف بشكل مختلف، والتوزيع عبر الخيوط يختلف عن التوزيع داخل نفس الخيط مجددًا. بعض ما يبدو فرصة للتوازي يُحوَّل بصمت إلى عملية مرتبة قبل أن يصل حتى إلى الكود الذي يهمك. إذا كنت لا تعرف أي فئة يقع فيها محفزك، فستستنتج أن السباق الحقيقي غير قابل للوصول — وهي نتيجة سلبية كاذبة تبدو مطابقة لـ"لا خلل هنا".
**القسم الحرج مختبئ في المتصل.** يعمل جزء كبير من النظام الفرعي تحت قفل خشن (coarse lock) يُكتسب أعلى بكثير من الدالة التي تنظر إليها. هذا هو أكبر مصدر وحيد لإهدار الوقت في تدقيق win32k: دالة لا تحتوي على أي مزامنة مرئية لكنها آمنة تمامًا لأن كل مسار إليها مُسلسَل بالفعل. **تغطية القفل خاصية عابرة للإجراءات.** يجب أن تصعد في مخطط الاستدعاءات، لا أن تقرأ الدالة فحسب.
تلك النقطة الثالثة هي السبب في أن *"لا يوجد قفل في هذه الدالة"* لا تساوي شيئًا تقريبًا كإشارة، والسبب في أن معظم العمل في هذا البحث أُنفق على قابلية الوصول بدلاً من العيب نفسه.
## 4. لماذا تقييم الأثر هو ما هو عليه