
استغلال ثغرة الاستخدام بعد التحرير (use-after-free) في win32k!xxxEnableWndSBArrows (CVE-2015-0057) على كل من الأنظمة 32-bit و64-bit (Aaron Adams من NCC)
المؤلف: Aaron Adams
الترجمة: 55-AA
ملاحظة المترجم: تمت ترجمة أجزاء من هذه المقالة بشكل تفسيري، في حالة وجود استفسارات، يرجى الرجوع إلى النص الأصلي.
المصطلحات:
في وقت سابق من هذا العام، صادفت ثغرة مثيرة للاهتمام في win32k.sys (CVE-2015-0057)، وتمكنت من تحقيق استغلال مستقر على أنظمة 32 بت و64 بت، والتي تغطي النطاق من XP إلى Windows 8.1 (مع بعض الاستثناءات). تصف هذه المقالة بالتفصيل كيف قمت بالاستغلال على هاتين المنصتين، وتتضمن أيضًا بعض الأمور الإضافية في النهاية. كما تصف كيفية تحقيق الاستغلال تحت صلاحيات منخفضة النزاهة على Windows 8.1 مع تمكين SMEP.
هذه المقالة طويلة، وقد بذلت قصارى جهدي لتوفير أكبر قدر ممكن من التفاصيل لإظهار تعقيد استغلال هذه الثغرة، بدلاً من إخفاء هذه التفاصيل، مع تجنب بعض التفاصيل أيضًا. آمل أن تكون هذه التفاصيل مفيدة للجميع.
في 10 فبراير 2015، أصدرت Microsoft تفاصيل حول MS15-010. تم اكتشاف هذا الخلل لأول مرة بواسطة Udi Yavo من enSilo. قدم Udi تحليلًا ممتازًا على مدونة breaking malware بعنوان "one bit rule-bypassing windows 10 protections using single bit". أوصي بقراءة هذه المقالة بعناية لفهم الخلل بشكل أعمق، على الرغم من أنني سأقدم أكبر قدر ممكن من التفاصيل في هذه المقالة، والتي تغطي بعض العقبات التي يجب التغلب عليها عند تشغيل الثغرة. استغلال هذه الثغرة مثير للاهتمام للغاية، والعديد من التفاصيل مستمدة من مدونة Udi، وإليك تصريحه:
إفشاء معقول: على الرغم من أن هذه المدونة تقنية، إلا أننا لن نكشف عن أي رمز أو تفاصيل كاملة، لمنع أي خبير تقني من إعادة إنتاج استغلال الثغرة.
كمكافأة إضافية لاستغلال هذه الثغرة، حصلنا على تطور البوكيمون: تكنيكيمون. أعتقد أنني يجب أن أعطي Udi بعض التقدير لاكتشافه هذه الثغرة وتقديم المعلومات ذات الصلة والتفاصيل حول استغلالها في مدونته، فقد كانت هذه الأشياء مفيدة للغاية.
في السابق، لم أستغل أي ثغرة في win32k.sys من قبل، ولم أكن على دراية بالاستدعاءات الخلفية لوضع المستخدم والعديد من واجهات برمجة التطبيقات ذات الصلة. لذلك، أشكر أيضًا بعض الباحثين الأمنيين المشهورين الذين قدموا موارد جديدة على الإنترنت، مثل Skywing وTarjei Mandt وAlex Ionescu وj00ru وغيرهم. هؤلاء الأشخاص يقدمون الكثير من المعلومات التقنية بشكل علني، ويستحقون الثناء جميعًا. ما رجعت إليه بكثرة هو مقالة Tarjei Mandt Win32k.sys exploitation paper.
عندما كنت أكتب هذا الاستغلال، قام مهندس عكسي ممتاز بتنفيذ استغلال ثابت لـ CVE-2015-1701، وكانت أمثلة التعليمات البرمجية الخاصة بالاستدعاءات الخلفية لوضع المستخدم مفيدة للغاية، وأشكر هذا المؤلف.
من الجدير بالذكر أن تحليلي أدناه تم على Windows 7، لأنه يبدو الإصدار الوحيد الذي تحتوي فيه جميع الهياكل في win32k.sys على رموز مقابلة. معظم هذه الرموز قابلة للاستخدام في هياكل إصدارات أخرى من Win32k.sys. لسبب غير معروف، أزالت Microsoft هذه الرموز من Windows 8.
أخيرًا، أود أن أقول إن طريقتي في الاستغلال معقدة للغاية. من الممكن تمامًا وجود طريقة أسهل لتحقيق ذلك، لكنني لم أجدها. أود أن أسمع عن استخدام طرق مختلفة من قبل الآخرين. على أي حال، آمل أن يكون كل هذا مفيدًا لدراسة ثغرات win32k.sys.
دعونا نلقي نظرة على الخلل في التفكيك الخاص بـ win32k!xxxEnableWndSBArrows، إنه خلل دقيق للغاية:
الحالة غير المصححة:
.text:FFFFF97FFF1B157D mov r8d, r13d
.text:FFFFF97FFF1B1580 mov rdx, r14
.text:FFFFF97FFF1B1583 call xxxDrawScrollBar ; تشغيل استدعاء خلفي لوضع المستخدم
.text:FFFFF97FFF1B1588 jmp short loc_FFFFF97FFF1B1519
[...]
.text:FFFFF97FFF1B1519 mov eax, [rbx] ; الرجوع إلى مؤشر tagSBINFO بدون التحقق
.text:FFFFF97FFF1B151B mov ebp, 0FFFFFFFBh
.text:FFFFF97FFF1B1520 xor eax, esi
في الكود أعلاه، يمكن لـ Win32K!xxxdrawscrollbar استدعاء وضع المستخدم في الظروف المناسبة، وفي كود وضع المستخدم، قد يتم تحرير مؤشر tagSBINFO بواسطة المهاجم. عند العودة إلى الكود أعلاه، سيشير الكود في 0xFFFFF97FFF1B1519 إلى مؤشر غير صالح.
الحالة المصححة:
.text:FFFFF97FFF1D69C3 xor r8d, r8d
.text:FFFFF97FFF1D69C6 mov rdx, rbp
.text:FFFFF97FFF1D69C9 call xxxDrawScrollBar ; تشغيل استدعاء خلفي لوضع المستخدم
.text:FFFFF97FFF1D69CE cmp rbx, [rdi+0B0h] ; التحقق من صحة مؤشر tagSBINFO
---.text:FFFFF97FFF1D69D5 jz short loc_FFFFF97FFF1D69E4; إذا كان صحيحًا، استمر في التدفق الأصلي
| .text:FFFFF97FFF1D69D7 mov rcx, rbp
| .text:FFFFF97FFF1D69DA call _ReleaseDC
| .text:FFFFF97FFF1D69DF jmp loc_FFFFF97FFF1D6958 ; القفز إلى خروج الدالة
|->.text:FFFFF97FFF1D69E4 mov eax, [rbx] ; استخدام مؤشر tagSBINFO الصحيح بأمان
.text:FFFFF97FFF1D69E6 xor eax, r14d
في الإصدار المصحح أعلاه، نرى التحقق من وجود مؤشر tagSBINFO قبل استخدامه. سيتم تقديم معلومات الهيكل ذات الصلة لاحقًا.
عند تنفيذ هذا الاستغلال، قمنا بعدة مراحل من الإفساد. وقمنا بتشغيل الثغرة في إحدى هذه المراحل.
الجذر التقني لهذا الخلل هو UAF (use-after-free) في كومة سطح المكتب (Desktop Heap). في البداية، كان هذا محيرًا بالنسبة لي، لأنني لم أكن على دراية بآلية الاستدعاء الخلفي لوضع المستخدم في win32k.sys، ولم أكن أعرف كيفية عملها. لذلك، اعتقدت أن هذا كان سباقًا (race condition) يؤدي إلى UAF. في الواقع، تم استخدام قفل الهيكل بشكل صحيح، وكان التدفق متوقعًا. باختصار، السبب الحقيقي للمشكلة هو:
هذا كل شيء، دون النظر إلى الاستدعاء الخلفي لوضع المستخدم، هذه المرحلة واضحة إلى حد ما.
ولكن، كيف نقوم بالإفساد، ولماذا؟ كما ذكر Udi في مدونته، يمكنك تعيين أو مسح بتين في موقع معين، يعتبره النظام كحقل WSBflags في هيكل tagSBINFO. هذه ليست فكرة استغلال UAF التقليدية، لكن المقالة أعطت تلميحًا حول كيفية القيام بذلك، سأشرح ذلك في الأقسام التالية. أولاً، دعونا نفهم كيفية التلاعب بهذه البتات.
هيكل tagSBINFO (متوافق بين 32 بت و64 بت):
kd> dt -b !tagSBINFO
win32k!tagSBINFO
+0x000 WSBflags : Int4B
+0x004 Horz : tagSBDATA
+0x000 posMin : Int4B
+0x004 posMax : Int4B
+0x008 page : Int4B
+0x00c pos : Int4B
+0x014 Vert : tagSBDATA
+0x000 posMin : Int4B
+0x004 posMax : Int4B
+0x008 page : Int4B
+0x00c pos : Int4B
توجد ثغرة UAF في دالة win32k!xxxEnableWndSBArrows()، والتي تستخدم لتمكين أو تعطيل أسهم عنصر تحكم شريط التمرير (واحد أو اثنين، أفقي أو عمودي). عنصر تحكم شريط التمرير هو نافذة خاصة تستخدم للتلاعب بشريط التمرير. يمكن إنشاؤه بواسطة دالة CreateWindow() باستخدام فئة النافذة المضمنة "SCROLLBAR".
النموذج الأولي لدالة win32k!xxxEnableWndSBArrows():
BOOL xxxEnableWndSBArrows(PWND wnd, UINT WSBflags, UINT wArrows);
معنى معامل WSBflags هو نفسه المحدد في WinUser.h، ويستخدم للإشارة إلى أي أشرطة التمرير سيتم التلاعب بها:
#define SB_HORZ 0
#define SB_VERT 1
#define SB_CTL 2
#define SB_BOTH 3
معامل wArrows يشير إلى حالة الأسهم، سواء كانت متاحة أو غير متاحة. القيمة المحددة تعني أن الأسهم غير متاحة، وإلا فهي متاحة. أقل بتين من wArrows يمثلان شريط التمرير الأفقي، والبتين التاليين يمثلان شريط التمرير العمودي، والبتات المتبقية غير مرتبطة بغرض هذا الاستغلال.
الكود التالي مأخوذ من دالة win32k!xxxEnableWndSBArrows()، إذا تم تعيين SB_HORZ أو SB_BOTH، فسيتم تعيين أو إلغاء البتات ذات الصلة بالسهم الأفقي:

يوجد الخلل عند تعيين علامات شريط التمرير الأفقي والعمودي. بعد تحديث شريط التمرير الأفقي، بمجرد أن تكون النافذة المقابلة لشريط التمرير مرئية على سطح المكتب، ستستدعي win32k!xxxEnableWndSBArrows() دالة win32k!xxxDrawScrollBar()، والتي ذكرت في المقالات السابقة أنها قد تؤدي إلى استدعاء خلفي محتمل لوضع المستخدم.
قبل مناقشة الاستدعاء الخلفي لوضع المستخدم، دعنا نستمر في مناقشة ما سيحدث بعد استدعاء win32k!xxxDrawScrollBar(). هذا في الواقع له نفس منطق شريط التمرير الأفقي، مع اختلاف بضع بتات فقط. إذا اخترنا تعطيل شريط التمرير العمودي، وافترضنا أننا تسببنا في UAF، فسيتم كتابة بتين في مكان ما من كتلة tagSBINFO. لذلك، إذا كانت القيمة الأصلية هي 0x2، فستصبح الآن 0xe. كما هو موضح في الشكل أدناه.

هذا التغيير البت يكفي لتحقيق تنفيذ الكود في النهاية. لم أتعمق في كيفية تحقيق الاستغلال من خلال مسح البتات، لكنه ممكن.
النقطة الأساسية المذكورة أعلاه هي أنه للتلاعب بكل من شريط التمرير الأفقي والعمودي، يجب إنشاء عنصر تحكم شريط تمرير يحتوي على هذين العنصرين بطريقة ما. يتم ذلك من خلال استدعاء CreateWindow() مع تعيين علامتي WS_HSCROLL و WS_VSCROLL. الكود كما يلي:
g_hSBCtl = CreateWindowEx(
0, // لا يوجد نمط موسع
"SCROLLBAR", // الفئة
NULL, // الاسم
SBS_HORZ | WS_HSCROLL | WS_VSCROLL, // عمودي + أفقي
10, // x
10, // y
100, // العرض
100, // الارتفاع
g_hSpray[UAFWND], // نافذة أب غير مشروطة
(HMENU)NULL,
NULL, // مالك النافذة
NULL // معاملات إضافية
);
يمكن ضمان رؤيتها من خلال الكود التالي (عادة ما يكون هذا افتراضيًا، نستدعيه هنا بشكل صريح):
result = ShowWindow(g_hSBCtl, SW_SHOW);
شريط التمرير مفعل افتراضيًا، عندما نكون مستعدين لمحاولة تشغيل كود الثغرة، يمكننا تعيين شريط التمرير على أنه غير متاح لإفساد البتات التي نحتاجها:
result = EnableScrollBar(g_hSBCtl, SB_CTL | SB_BOTH, ESB_DISABLE_BOTH);
على الرغم من أننا وصفنا تفاصيل الخلل أعلاه وكيفية تشغيل الكود ذي الصلة، إلا أننا ما زلنا نتجاهل الخطوة الأكثر أهمية، وهي اعتراض الاستدعاء الخلفي لوضع المستخدم الذي بدأته win32k!xxxDrawScrollBar()، حتى نتمكن من تغيير محتوى الكومة قبل أن تستمر win32k!xxxEnableWndSBArrows() في التنفيذ. نحتاج حقًا إلى تشغيل الثغرة، ولكن بدون معرفة أي شيء عن Win32k.sys وواجهات برمجة التطبيقات ذات الصلة، كما كنت في البداية، فهذه مغامرة بحد ذاتها.
توفر المقالات السابقة رسمًا تخطيطيًا جيدًا لاستدعاء المكدس، يوضح هذه العملية بعمق، من خلال win32k!xxxDrawScrollBar()، ثم يتم استدعاء ClientLoadLibrary()، ويتم توزيعها عبر KeUserModeCallback(). نحتاج حقًا إلى فهم استدعاء KeUserModeCallback()، حتى نتمكن من اعتراضه في عمليتنا الخاصة.
لقد وجدت بعض الأوراق الجيدة التي تذكر معلومات حول الاستدعاءات الخلفية لوضع المستخدم بشكل أو بآخر. الأجزاء المتعلقة بـ win32k مفيدة جدًا:
عادة، تحتوي كل عملية على جدول مؤشرات لدوال الاستدعاء الخلفي لوضع المستخدم، يشير PEB->KernelCallBackTable إلى هذا الجدول. عندما يريد kernel استدعاء دالة في وضع المستخدم، فإنه يمرر فهرس الدالة إلى KeUserModeCallBack(). في المثال أعلاه، يشير الفهرس إلى دالة __ClientLoadLibrary() في وضع المستخدم.
يبحث KeUserModeCallBack() في PEB->KernelCallBackTable بناءً على الفهرس عن الدالة المقابلة وينفذها، وينتهي باستدعاء KiUserModeCallbackDispatch() في وضع المستخدم.
لاعتراض نقطة الدخول المحددة، يجب العثور على فهرس __ClientLoadLibrary() في PEB->KernelCallBackTable، ثم استبداله بدالتنا الخاصة. من الجدير بالذكر أن هذا الفهرس يختلف حسب إصدار نظام التشغيل والمنصة.
إذا أردنا عرض PEB->KernelCallBackTable، يمكننا العثور على عنوان هذا الجدول عبر WinDbg. بمقارنة منصات 32 بت و64 بت، لا نجد اختلافات كبيرة.
kd> dt !_PEB @$peb
ntdll!_PEB
+0x000 InheritedAddressSpace : 0 ''
+0x001 ReadImageFileExecOptions : 0 ''
+0x002 BeingDebugged : 0 ''
+0x003 BitField : 0x8 ''
+0x003 ImageUsesLargePages : 0y0
[...]
+0x02c KernelCallbackTable : 0x76daf620 Void
kd> dds 0x76daf620
76daf620 76d96443 user32!__fnCOPYDATA
76daf624 76ddf0e4 user32!__fnCOPYGLOBALDATA
76daf628 76da736b user32!__fnDWORD
76daf62c 76d9d603 user32!__fnNCDESTROY
76daf630 76dc50f9 user32!__fnDWORDOPTINLPMSG
76daf634 76ddf1be user32!__fnINOUTDRAG
76daf638 76dc6cd0 user32!__fnGETTEXTLENGTHS
76daf63c 76ddf412 user32!__fnINCNTOUTSTRING
76daf640 76d9ce49 user32!__fnINCNTOUTSTRINGNULL
[...]
76daf724 76da3962 user32!__ClientLoadLibrary
kd> ?? (0x76daf724-0x76daf620)/4 int 0n65
في المثال أعلاه، نعلم أن فهرس __ClientLoadLibrary هو 65، وهذا هو المكان الذي سنقوم باعتراضه. بعد الاعتراض، اكتشفت أن __ClientLoadLibrary يتم استدعاؤها عدة مرات بواسطة كود win32k! أول شيء يجب فعله هو كيفية إخطار كود الاعتراض الخاص بنا قبل أن نصل إلى الاستدعاء الذي يهمنا، حتى نعرف أننا قمنا بالفعل باعتراض التعديل المطلوب. لذلك، استخدم كود الاعتراض متغيرًا عامًا للإشارة، وعند تعيين هذا المؤشر فقط يتم تنفيذ العمليات ذات الصلة.
الآن هناك عقبتان:
لذلك، تبدو دالة الاعتراض على النحو التالي:
void ClientLoadLibraryHook(void * p)
{
CHAR Buf[PGSZ];
memset(Buf, 0, sizeof(Buf));
if (g_PwnFlag)
{
dprintf("[+] __ClientLoadLibrary hook called\n");
if (++g_HookCount == 2)
{
g_PwnFlag = 0; // يتم تنفيذها مرة واحدة فقط..
ReplaceScrollBarChunk(NULL);
}
}
fpClientLoadLibrary(&Buf); // استدعاء الدالة الأصلية
}
بمجرد أن نحدد أننا قادمون من دالة win32k!xxxDrawScrollBar()، يمكننا محاولة تشغيل الثغرة. الآن لا نفكر إلا في التشغيل، نحتاج فقط إلى استدعاء DestroyWindow(g_hSBCtl). سيؤدي ذلك إلى تحرير هيكل tagSBINFO للنافذة، بينما لن يتم تحرير هيكل النافذة نفسه فورًا، لأن عداد المرجع الخاص به لا يزال مستخدمًا من قبل الاستدعاء الأصلي، لكن tagSBINFO ليس لديه آلية عداد مرجع، لذلك يتم تحريره فورًا.
عند هذه النقطة، قمنا بالفعل بتشغيل الثغرة. على الرغم من أننا لم نعيد تخصيص كتلة tagSBINFO، إلا أننا نستطيع كتابة بتين يشيران إلى التعطيل على الكومة المحررة. الخطوة التالية هي استبدال هذه الكتلة المحررة بما نريد، لذلك يمكننا القيام بأشياء أكثر إثارة من مجرد تعيين بضع بتات. لهذا، نحتاج إلى فهم بعض الخلفية حول كومة سطح المكتب.
يستخدم win32k.sys كومة سطح المكتب لتخزين كائنات واجهة المستخدم الرسومية المرتبطة بسطح مكتب معين. تتضمن كائنات النافذة وهياكلها ذات الصلة، مثل قوائم الخصائص ونص النافذة وأشرطة التمرير. تشير مقالة Tarjei إلى ذلك، ولكن من المهم ملاحظة أن كومة سطح المكتب هي في الواقع نسخة مبسطة من المخصص الخلفي لوضع المستخدم، وتستخدم أيضًا RtlAllocateHeap() و RtlHeapFree() لإجراء العمليات. تتم إدارة كومة سطح المكتب بواسطة هيكل _HEAP، ونظرًا لعدم وجود مخصص أمامي، فلا يوجد LFH (Low Fragmentation Heap) أو قوائم جانبية (lookaside lists) وما شابه.
كلما تم إنشاء سطح مكتب، يتم توفير كومة سطح مكتب مقابلة له. وهذا يعني أنه يمكننا تخصيص سطح مكتب جديد للحصول على كومة سطح مكتب "نظيفة"، حيث تكون عملياتنا أكثر قابلية للتنبؤ. ومع ذلك، هذا لا معنى له بالنسبة للعمليات ذات الصلاحيات المنخفضة النزاهة، لأن هذه العمليات لا يُسمح لها بإنشاء سطح مكتب جديد.
المشكلة الرئيسية الآن هي تتبع عملية التخصيص (سيتم تغطية المزيد من التفاصيل حول البيانات الوصفية وما إلى ذلك لاحقًا).
لتحرير تخصيص كومة سطح المكتب وتحريرها، أستخدم عادةً نصوص WinDbg:
مراقبة كومة 64 بت
ba e 1 nt!RtlFreeHeap ".printf\"RtlFreeHeap(%p, 0x%x, %p)\", @rcx, @edx, @r8; .echo ; gc";
ba e 1 nt!RtlAllocateHeap "r @$t2 = @r8; r @$t3 = @rcx; gu; .printf \"RtlAllocateHeap(%p, 0x%x):\", @$t3, @$t2; r @rax; gc";
مراقبة كومة 32 بت
ba e 1 nt!RtlAllocateHeap "r @$t2 = poi(@esp+c); r @$t3 = poi(@esp+4); gu; .printf \"RtlAllocateHeap(%p, 0x%x):\", @$t3, @$t2; r @eax; gc";
ba e 1 nt!RtlFreeHeap ".printf\"RtlFreeHeap(%p, 0x%x, %p)\", poi(@esp+4), poi(@esp+8), poi(@esp+c); .echo ; gc"
بالإضافة إلى هذه النصوص، نظرًا لأن كومة سطح المكتب هي مجرد نسخة مبسطة من المخصص الخلفي لوضع المستخدم، يمكننا أيضًا استخدام الأمر المدمج في WinDbg !heap.
لاستغلال هذه الثغرة، نحتاج إلى استبدال كتلة tagSBINFO المحررة مؤخرًا، ونعرف أيضًا كيفية استخدام هذه الثغرات النموذجية، وهي إفساد البيانات المجاورة. يمنحنا هذا المطلب الأساسي تخصيص بعض الكتل بشكل مسبق بالقرب من الهيكل المراد إفساده. للتنبؤ بمكان تخصيص كتلة، يجب التحكم في تخطيط الكومة بأكمله (أو بقدر الإمكان). لتحقيق ذلك، الطريقة الممكنة هي تخصيص أكبر عدد ممكن من الكتل لملء الكتل المحررة، بحيث تكون الكتل المخصصة حديثًا متصلة. عندما نحتاج إلى فجوة، يمكننا حفر واحدة في مكان يمكن التنبؤ به (عن طريق تحرير الكتل المخصصة).
هذا الجزء هو فهم بسيط للعوامل التي تؤثر على التخصيص، تساعدنا نصوص WinDbg أعلاه. ذكر Tarjei في عرضه التقديمي حول win32k الكائنات الرئيسية المخصصة على كومة سطح المكتب، وهو ما يتوافق تمامًا مع ما رأيته. هذه هي:
كومة سطح المكتب مثيرة للاهتمام إلى حد ما، معظم التخصيصات مرتبطة مباشرة بكائنات النافذة وتتم إدارتها من خلال هيكل tagWND، مما يعني أننا إذا أردنا تخصيص كتلة بحجم عشوائي (ما يسمى بالكتل الصغيرة لملء الفجوات الصغيرة)، يجب علينا أولاً تخصيص نافذة مرتبطة بها. يمكن القول إن هيكل النافذة هو واجهة تخصيص الكومة. شيء آخر مثير للاهتمام هو أن العديد من الكتل المخصصة من خلال عمليات النافذة لا يمكن تحريرها فورًا، إلا إذا تم تدمير النافذة نفسها، وهذا يؤثر بوضوح على الكومة. أخيرًا، دعنا نفترض أننا خصصنا كتلة بحجم N من خلال نافذة، كما فعلنا في المثال أعلاه، هل سنخصص العديد من الكتل بحجم N؟ من المؤكد أن هيكل النافذة المخصص لا يتم تخزينه في قائمة مرتبطة بغض النظر عن حجمه. لذلك، كل نافذة يمكنها التحكم في تخصيص كتلة بحجم N. وهذا يعني أنه إذا كنت بحاجة إلى تخصيص عدد كبير من الكتل بحجم N، فيجب عليك أولاً إنشاء عدد كبير من النوافذ، واستخدام النوافذ للمساعدة في تخصيص الكتل.
هناك أيضًا ثلاثة أنواع بيانات مهمة يتم تخصيصها على كومة سطح المكتب، يمكننا استخدامها بشكل غير مباشر من خلال كائنات النافذة للتحكم في البيانات على الكومة. نستخدم هذه الأنواع بشكل كبير لتحقيق الاستغلال وبناء هندسة الكومة. هذه الأنواع الثلاثة هي:
يوضح الشكل 2 العلاقات بين هذه الأنواع:

لتهيئة الكومة، قمت بإنشاء عدد كبير من هياكل tagWND (من خلال إنشاء كائنات النافذة). يمكن أن يملأ هذا العديد من الفجوات الكبيرة في الكومة، ويوفر لنا واجهة لتخصيص الكتل الأخرى التي نحتاجها. على Windows 8 وWindows 8.1، يؤدي تخصيص نافذة جديدة إلى تخصيص هيكل tagPROPLIST تلقائيًا (يمكن ملاحظته من خلال نصوص WinDbg المذكورة سابقًا). على Windows 7 والإصدارات الأقدم، نقوم نحن بتخصيص tagPROPLIST جديد لملء الفجوات الصغيرة.
هنا، جميع كائنات النافذة التي قمنا برشها لا تحتوي على سلاسل نص النافذة، ومع ذلك، إذا لزم الأمر، يمكننا استخدامها لتخصيص أو تحرير كتل بأي حجم. بمجرد الإنشاء، لا يمكن إزالة قائمة الخصائص الموجودة (property list) إلا إذا تم تدمير النافذة، ولكن يمكننا التحكم في إعادة تخصيص هذه القائمة لاستيعاب خصائص جديدة، ويمكن استخدام هذه الآلية لحفر فجوة في المكان السابق. كل ما تحتاجه هو تعيين خاصية جديدة غير موجودة في القائمة السابقة (مميزة بـ atomkey).
من المثير للاهتمام أن كومة سطح المكتب تم تعيينها إلى مساحة المستخدم، على الرغم من أنها للقراءة فقط. وهذا يعني أنه يمكننا التحقق من تخطيط الكومة الذي بنيناه والتأكد من أنه يعمل بشكل صحيح. أولاً، نحتاج إلى تحديد المكان الذي تم تعيين كومة سطح المكتب إليه في وضع المستخدم. ذكر Tarjei ذلك في ورقته حول win32k. هناك هيكل غير معلن Win32ClientInfo في TEB يتعلق بهذا، تعريفه التقريبي كما يلي:
typedef struct _CLIENTINFO {
ULONG_PTR CI_flags;
ULONG_PTR cSpins;
DWORD dwExpWinVer;
DWORD dwCompatFlags;
DWORD dwCompatFlags2;
DWORD dwTIFlags;
PDESKTOPINFO pDeskInfo;
ULONG_PTR ulClientDelta; // غير مكتمل. راجع reactos
} CLIENTINFO, *PCLIENTINFO;
حيث يتم تعريف PDESKTOPINFO على النحو التالي:
typedef struct _DESKTOPINFO {
PVOID pvDesktopBase;
PVOID pvDesktopLimit; // غير مكتمل. راجع reactos
} DESKTOPINFO, *PDESKTOPINFO;
الحقل الأول pvDesktopBase يشير إلى عنوان كومة سطح المكتب في وضع kernel، نضعه في الاعتبار. حقل ulClientDelta في Win32ClientInfo هو الفرق بين عنوان وضع kernel وعنوان وضع المستخدم، ومن خلال هذه المعلومات يمكننا الحصول على ما نريد.
ومع ذلك، نحن لا نريد تحليل هيكل الكومة بأنفسنا، بل نريد الحصول على مقبض user32، مثل قيمة HWND، والتي يمكن تحويلها إلى عنوان تعيين وضع المستخدم، وبالتالي يمكننا تحديد ما إذا كان مرتبطًا بتخصيص كومة آخر. للعثور على هذا المقبض، نحتاج إلى العثور على هيكل يسمى gShared، والذي يقع عادةً في uer32.dll، وفي Windows 7 والإصدارات اللاحقة يتم تصديره، لذلك يمكن العثور عليه بسهولة.
في معظم الأنظمة، يكون هذا الهيكل على النحو التالي:
kd> dt !tagSHAREDINFO
win32k!tagSHAREDINFO
+0x000 psi : Ptr32 tagSERVERINFO
+0x004 aheList : Ptr32 _HANDLEENTRY
+0x008 HeEntrySize : Uint4B
+0x00c pDispInfo : Ptr32 tagDISPLAYINFO
+0x010 ulSharedDelta : Uint4B
+0x014 awmControl : [31] _WNDMSG
+0x10c DefWindowMsgs : _WNDMSG
+0x114 DefWindowSpecMsgs : _WNDMSG
في الهيكل أعلاه، يشير aheList إلى مصفوفة _HANDLEENTRY، كل _HANDLEENTRY يحتوي على مقبض يشير إلى عنوان وضع kernel. يمكننا الحصول على عنوان وضع مستخدم قابل للاستخدام من خلال "الفرق بين عنوان وضع kernel وعنوان وضع المستخدم". لسوء الحظ، هذا غير ممكن على الإصدارات الأقدم من Windows 7، لأن gSharedInfo غير مصدر. ذكرت مقالة Tarjei أن الدالة غير المعلنة CsrClientConnectToServer يمكن استخدامها للحصول على نسخة من gSharedInfo، لكنني لم أجد مثالًا عمليًا. المزعج هو أن هذه الدالة تتطلب هيكلًا يختلف طوله حسب النظام، لذلك، وفقًا لتجربتي، لا يمكنك الوثوق تمامًا بما تراه في ReactOS.
بمجرد حساب موقع التعيين، يمكننا بناء دالة تخبرنا بموضع كائن النافذة على كومة سطح المكتب. بعد ذلك، إذا أردنا معرفة أين تم تخصيص كتلة قائمة الخصائص أو سلسلة النص المقابلة، نحتاج فقط إلى تحليل الهيكل في وضع المستخدم.
الآن، نقترب أخيرًا من استغلال هذه الثغرة. لدينا طريقة للتحكم في الكتلة، وطريقة للتحقق من صحة موقع الكتلة، ويمكننا تشغيل الثغرة، لذلك الآن يمكننا أخيرًا استبدال كتلة tagSBINFO المحررة بكتلة tagPROPLIST المختارة. لاحظ أنه نظرًا لأن tagPROPLIST هو مجرد رأس لقائمة كبيرة، يمكننا جعل حجم القائمة يتطابق مع حجم كتلة شريط التمرير. الجزء الخلفي من tagPROPLIST هو أساسًا مصفوفة من هيكل tagPROP، أو قائمة خصائص؛ لذلك، لن أميز بين المصطلحين. هيكل tagPROPLIST على أنظمة 64 بت هو كالتالي:
kd> dt -b !tagPROPLIST
win32k!tagPROPLIST
+0x000 cEntries : Uint4B
+0x004 iFirstFree : Uint4B
+0x008 aprop : tagPROP
+0x000 hData : Ptr64
+0x008 atomKey : Uint2B
+0x00a fs : Uint2B
كما ذكرنا سابقًا، كائن النافذة لديه قائمة خصائص مرتبطة به. يتم إنشاء هذه القائمة من خلال دالة SetProp(). تُستخدم للبحث عن الخصائص الموجودة من خلال مطابقة autoKey، إذا لم تكن الخاصية موجودة، يتم إنشاء عنصر خاصية جديد في القائمة. إذا لم تكن هناك قائمة خصائص على الإطلاق، يتم إنشاء قائمة خصائص وربطها بهيكل tagWND.
إذا قمنا بالفعل برش tagWND به ثغرة وإنشاء عناصر tagPROPLIST المرتبطة، فسيكون التخطيط النهائي كما في الشكل 3:

بمجرد إعداد ذلك، يمكننا تخصيص عنصر تحكم شريط التمرير الذي نريد استغلاله. سيؤدي ذلك إلى النتيجة الموضحة في الشكل 4:

ثم نقوم بالتلاعب بشريط التمرير، لتشغيل اعتراض الاستدعاء الخلفي لوضع المستخدم، في دالة الاعتراض، نحاول تدمير النافذة لتحرير هيكل tagSBINFO. يؤدي هذا إلى الحالة في الشكل 5:

على 64 بت، حجم هيكل tagSBINFO هو 0x28 بايت، وحجم عنصر مصفوفة tagPROPLIST هو 0x18 بايت، منها 0x10 بايت هي tagPROP الافتراضية. لذلك، قائمة خصائص تحتوي على عنصرين تكون بحجم 0x28 بايت (0x8 + 0x10 + 0x10)، وهذا توافق إلهي. لنفترض أننا قمنا برش الذاكرة مسبقًا لملء الفجوة. نحتاج فقط إلى نافذة تحتوي على قائمة خصائص، وبعد تحرير هيكل tagSBINFO (كما هو موضح في الشكل السابق)، نضيف على الفور عنصر قائمة خصائص جديد. هذه العملية تبدأ بتحرير كتلة tagPROPLIST السابقة بحجم 0x18 بايت، نظرًا لأن الكومة قد تم رشها، فلا توجد كتل فارغة قريبة، لذلك لن يحدث دمج للكتل، وبالتالي لا توجد مساحة كافية لاستيعاب الكتلة الجديدة بحجم 0x28 بايت. بهذه الطريقة، يتم استخدام موقع tagSBINFO المحرر حديثًا (حجمه بالضبط 0x28 بايت)، وهذه الحالة موضحة في الشكل 6:

بعد العودة من دالة الاعتراض الخاصة بنا، سيتم تشغيل UAF، وسيتم كتابة بضع بتات في حقل cEntries الخاص بـ tagPROPLIST. كانت قيمة cEntries الأصلية 0x2، مما يشير إلى أننا أنشأنا عنصرين في قائمة الخصائص. بعد الفائض، أصبحت 0xe، وتم تعيين البتتين الثالثة والرابعة (بدءًا من 1) إلى 1.
عند هذه النقطة، أكملنا فائض الكومة الجديدة، وقمنا بزيادة عدد عناصر قائمة الخصائص إلى عدد أكبر من 0xc. بعد ذلك، سنقوم بفيضان الكتلة المجاورة، وهو ما نسميه مرحلة الإفساد 2.
في مدونة Udi، تم شرح هذا فقط. قبل ذلك، كان هذا يُسمى "فائض كومة نموذجي"، ومع ذلك، وفقًا لتجربتي، من الصعب تحقيق قراءة/كتابة عنوان عشوائي أو تنفيذ كود من هذه النقطة. دعنا ننظر مرة أخرى إلى هيكل tagPROPLIST على 64 بت:
kd> dt -b !tagPROPLIST
win32k!tagPROPLIST
+0x000 cEntries : Uint4B
+0x004 iFirstFree : Uint4B
+0x008 aprop : tagPROP
+0x000 hData : Ptr64
+0x008 atomKey : Uint2B
+0x00a fs : Uint2B
مرحلة الإفساد 1 أعطتنا مصفوفة tagPROPLIST فاسدة، مما يسمح لنا بزيادة عناصر المصفوفة tagPROP. يحتوي tagPROPLIST على حقلين فقط:
عند إدراج عنصر جديد في القائمة، يتم استدعاء دالة لفحص كل عنصر أولاً حتى يتم العثور على قيمة iFirstFree المناسبة. إذا لم يتم العثور عليها، يتم التحقق مما إذا كانت قيمة iFirstFree أكبر من cEntries. إذا لم يكن العنصر المطابق لـ atomKey موجودًا في القائمة، يتم التحقق مما إذا كان iFirstFree != cEntries. إذا لم يكونا متساويين، يتم إدراج عنصر في فهرس iFirstFree، وإذا كانا متساويين، يتم تخصيص قائمة خصائص جديدة يمكنها استيعاب العنصر المدرج، ويتم نسخ العناصر القديمة، وإدراج العنصر الجديد.
حقل atomKey يتوافق مع LPCTSTR lpString. كما هو مذكور في وثائق MSDN الخاصة بـ SetProp()، يمكن للمتصل تمرير مؤشر سلسلة أو قيمة atom ذات 16 بت. عند تمرير مؤشر سلسلة، يتم تحويله تلقائيًا إلى قيمة atom قبل تخزينها في قائمة الخصائص. نظرًا لأننا يمكننا تمرير أي قيمة atom إلى دالة SetProp()، فإن هذا يمنحنا القدرة على التحكم في هذين البايتين، ولكن هناك بعض القيود. وهي أن بيانات atomKey التي نعيثها فسادًا لا يمكن أن تتكرر، وإلا عند تعيين عنصر خاصية جديد، فإنه سيحل محل العنصر الحالي الذي له نفس قيمة atom. بالإضافة إلى ذلك، لا يمكننا التحكم في حقل fs، وقيمته 0 تشير إلى أن قيمة atomKey < 0xBFFF، وهو يتوافق مع قيمة atom عدد صحيح. قيمة fs تساوي 2 تشير إلى أن قيمة atomKey >= 0xC000.
شيء آخر يجب ملاحظته هو أن tagPROP له حجم 0xc بايت فقط. هذا الهيكل على أنظمة 64 بت يتم محاذاته إلى 0x10 بايت، لذلك عند إدراج عنصر tagPROP، هناك 4 بايتات إضافية لا يمكن إفسادها. آخر نقطة مهمة هي أن أول 8 بايتات من كتلة tagPROPLIST تحدد حجم عناصر القائمة، مما يعني أن كل عنصر tagPROP مضاف حديثًا سيتم كتابته دائمًا في موقع محاذي إلى 8 بايت.
بالنسبة لكل tagPROP مضاف على أنظمة 64 بت، يكون الوضع كما يلي:
* الإزاحة 0x0: 8 بايت من البيانات القابلة للتحكم بالكامل (hData)
* الإزاحة 0x8: 2 بايت يمكن التحكم بها في الغالب (atomKey)
* الإزاحة 0xa: 2 بايت لا يمكن التحكم بها (fs)
* الإزاحة 0xc: 4 بايت لا يمكن تعديلها (حشو)
هذا الوضع أفضل بكثير من بتين، لكنه لا يزال غير مثالي. ما لم نتمكن من الكتابة فوق شيء باستخدام أول 8 بايت، والتي تأتي من حقل hData القابل للتحكم بالكامل، وإلا فسيكون محدودًا للغاية. إذا احتجنا إلى كتابة حقول أعمق في الهيكل المجاور، فلا يمكننا تجنب الإفساد غير القابل للتحكم لبعض القيم. قضيت بعض الوقت في البحث عن كائنات مختلفة على كومة سطح المكتب، وبالنظر إلى قيود الإفساد السابقة، فإن الطريقة الوحيدة التي تمكنت من التفكير بها لتجاوز هذا القيد وتحقيق قراءة/كتابة عنوان عشوائي هي إفساد حقل strName في tagWND، وهو هيكل _LARGE_UNICODE_STRING:
kd> dt !_LARGE_UNICODE_STRING
win32k!_LARGE_UNICODE_STRING
+0x000 Length : Uint4B
+0x004 MaximumLength : Pos 0, 31 Bits
+0x004 bAnsi : Pos 31, 1 Bit
+0x008 Buffer : Ptr64 Uint2B
إذا تمكنا من إفساد حقل Buffer في هذا الهيكل، يمكننا من خلال التلاعب بنص النافذة قراءة أو كتابة MaximumLength بايت من عنوان معين. هذا ما سأفعله. ربما لاحظت هذا الهيكل في الأقسام السابقة، حول كيفية إنشاء كتلة بحجم وقيمة عشوائيين على كومة سطح المكتب، لذلك يمكن تطبيق نفس الوضع هنا.
الآن، عرفنا كيف نستخدم عناصر قائمة tagPROPLIST لإفساد البيانات، ونعرف الأجزاء التي يمكننا التحكم بها، والأهم من ذلك، نعرف القيود التي سنواجهها، والتي تختلف بين 32 بت و64 بت. ما فعلناه سابقًا على 64 بت لا يعمل على 32 بت. قريبًا سننتقل من مرحلة الإفساد 2 (أي الكتابة من خلال هيكل tagPROP) إلى عملية إفساد أخرى "بدائية" يمكننا من خلالها كتابة بيانات قابلة للتحكم بالكامل، وهو ما أسميه مرحلة الإفساد 3.
الهدف هو إفساد حقل strName في tagWND المجاور. نعلم بالفعل أنه هيكل LARGE_UNICODE_STRING، ولكن دعونا نلقي نظرة على تفاصيل أكثر لهيكل tagWND، يبدو كالتالي:```ar kd> dt !tagWND win32k!tagWND +0x000 head : THRDESKHEAD +0x028 state : Uint4B +0x028 bHasMeun : Pos 0, 1 Bit +0x028 bHasVerticalScrollbar : Pos 1, 1 Bit +0x028 bHasHorizontalScrollbar : Pos 2, 1 Bit [أعلام مقتطعة] +0x028 bDestroyed : Pos 31, 1 Bit +0x02c state2 : Uint4B [أعلام مقتطعة] +0x02c bWMCreateMsgProcessed : Pos 31, 1 Bit +0x030 ExStyle : Uint4B +0x030 bWS_EX_DLGMODALFRAME : Pos 0, 1 Bit +0x030 bUnused1 : Pos 1, 1 Bit +0x030 bWS_EX_NOPARENTNOTIFY : Pos 2, 1 Bit [أعلام مقتطعة] +0x030 bUIStateFocusRectHidden : Pos 31, 1 Bit +0x034 style : Uint4B +0x034 bReserved1 : Pos 0, 16 Bits [أعلام مقتطعة] +0x034 bWS_POPUP : Pos 31, 1 Bit +0x038 hModule : Ptr64 Void +0x040 hMod16 : Uint2B +0x042 fnid : Uint2B +0x048 spwndNext : Ptr64 tagWND +0x050 spwndPrev : Ptr64 tagWND +0x058 spwndParent : Ptr64 tagWND +0x060 spwndChild : Ptr64 tagWND +0x068 spwndOwner : Ptr64 tagWND +0x070 rcWindow : tagRECT +0x080 rcClient : tagRECT +0x090 lpfnWndProc : Ptr64 int64 +0x098 pcls : Ptr64 tagCLS +0x0a0 hrgnUpdate : Ptr64 HRGN +0x0a8 ppropList : Ptr64 tagPROPLIST +0x0b0 pSBInfo : Ptr64 tagSBINFO +0x0b8 spmenuSys : Ptr64 tagMENU +0x0c0 spmenu : Ptr64 tagMENU +0x0c8 hrgnClip : Ptr64 HRGN__ +0x0d0 hrgnNewFrame : Ptr64 HRGN__ +0x0d8 strName : LARGE_UNICODE_STRING +0x0e8 cbwndExtra : Int4B +0x0f0 spwndLastActive : Ptr64 tagWND +0x0f8 hImc : Ptr64 HIMC_ +0x100 dwUserData : Uint8B +0x108 pActCtx : Ptr64 _ACTIVATION_CONTEXT +0x110 pTransform : Ptr64 _D3DMATRIX +0x118 spwndClipboardListenerNext : Ptr64 tagWND +0x120 ExStyle2 : Uint4B +0x120 bClipboardListener : Pos 0, 1 Bit [أعلام مقتطعة] +0x120 bChildNoActivate : Pos 11, 1 Bit
ما سبق هو هيكل 64 بت، حيث يمكننا ملاحظة أن إزاحة بنية _LARGE_UNICODE_STRING التي نريد الكتابة فوقها هي 0xd8. ستلاحظ أيضًا حقلًا مهمًا في بداية هذه البنية. كنت آمل في البداية أن أتمكن من التلاعب به بحرية، لكن وجود العديد من المؤشرات في _THRDESKHEAD يتطلب منا الحذر، وللأسف، لا يمكننا التحكم في المكان الذي نكتب فيه، والقيود ناقشناها سابقًا.
تعريف بنية _THRDESKHEAD:
kd> dt !_THRDESKHEAD
win32k!_THRDESKHEAD
+0x000 h : Ptr64 Void
+0x008 cLockObj : Uint4B
+0x010 pti : Ptr64 tagTHREADINFO
+0x018 rpdesk : Ptr64 tagDESKTOP
+0x020 pSelf : Ptr64 UChar
مشكلة _THRDESKHEAD لا تثير الحيرة فقط، بل تجعلنا نعيد النظر في قيود المحاذاة. بغض النظر عن إزاحة عنصر قائمة tagPROP الجديد، فإن عملية الكتابة لدينا ستكتب مباشرة فوق بداية _LARGE_UNICODE_STRING:
win32k!_LARGE_UNICODE_STRING
+0x000 Length <-- hData (قابل للتحكم الكامل) يكتب هنا
+0x004 MaximumLength <-- وهنا
+0x004 bAnsi <-- وهنا
+0x008 Buffer <-- atomKey و fs (قابل للتحكم الجزئي) يكتب هنا
من الواضح أننا نريد الكتابة فوق مؤشر Buffer للوصول إلى ذاكرة في عنوان عشوائي، ولكن حتى لو تمكنا من مهاجمة الحقول الأخرى لهذه البنية بأمان، فإننا لا نستطيع التحكم في المؤشر المطلوب.
لا يمكننا إفساد بيانات عشوائية، ولم يعد حل هذه المشكلة يتعلق بإفساد tagPROPLIST، بل أصبح آلية إفساد مختلفة تمامًا.
في الإصدارات التي تلي Windows XP، يتم تخزين رأس كتلة الكومة (أي _HEAP_ENTRY) للمخصص الخلفي لوضع المستخدم (مثل كومة سطح المكتب kernel) على الكومة، ويقع قبل المحتوى الفعلي للكتلة. تدير كومة سطح المكتب نفسها من خلال بنية _HEAP، مما يمنحنا بعض الحرية عند استغلال هذه الكتلة.
تعريف بنية _HEAP_ENTRY:
kd> dt !_HEAP_ENTRY
ntdll!_HEAP_ENTRY
+0x000 PreviousBlockPrivateData : Ptr64 Void
+0x008 Size : Uint2B
+0x00a Flags : UChar
+0x00b SmallTagIndex : UChar
+0x00c PreviousSize : Uint2B
+0x00e SegmentOffset : UChar
+0x00f UnusedBytes : UChar
يبلغ إجمالي رأس الكتلة 0x10 بايت، أول 8 بايت هي PreviousBlockPrivateData، والتي تُستخدم لاستيعاب بيانات الكتلة الفعلية السابقة عندما يتجاوز الحجم المطلوب 0x10 العادي (يتم محاذاة أقل من 8 بايت إلى 8 بايت). تم وصف هذا بإيجاز في إدخال مدونة Leviathan، بالإضافة إلى المقالات السابقة حول كومة وضع المستخدم. يشير Size و PreviousSize إلى حجم الكتلة الحالية وحجم الكتلة السابقة، بوحدات 0x10 بايت. تشير Flags إلى ما إذا كانت الكتلة خالية أم لا. إذا تم تمكين الوضع الآمن _HEAP_ENTRY في _HEAP، فسيحتوي SmallTagIndex على مجموع اختباري XOR للبيانات الموجودة في الكتلة.
على الرغم من أن قيود المحاذاة تعمل ضدنا، إلا أنها موجودة بالفعل. إذا قمت باستدعاء tagPROPLIST، فسيكون دائمًا على الأقل 0x18 بايت، ثم يزيد بمقدار 0x10 بايت لكل tagPROP. بالنسبة لـ tagPROPLIST بحجم 0x28 بايت مع عنصري قائمة، سيتم وضعها في كتلة كومة بحجم 0x20 بايت، وستستخدم البايتات الإضافية التي تمثلها PreviousBlockPrivateData الكتل المجاورة. هذا يعني أنه عندما نضيف عنصر قائمة ثالثًا، يتم إفساد الكتلة المجاورة، وستكتب hData القابلة للتحكم (8 بايت) فوق الجزء العلوي من _HEAP_ENTRY.
ما نريد فعله هو استغلال هذا لتمكين كتابة بيانات عشوائية إلى الموضع العلوي من Buffer. أولاً، نقوم بتعديل تخطيط الكومة بحيث نقترب من كتلة tagPROPLIST المفسدة، وأثناء عملية التحكم، لدينا كتلة كومة صغيرة تحتوي على سلسلة نصية متعلقة بالنافذة، نسميها "كتلة التغطية". بجوار "كتلة التغطية"، نضع tagWND حتى نتمكن من إفساد tagWND. يوضح الشكل 7 أدناه هذه العملية. لاحظ أننا بدأنا بحذف الكتل المتناثرة مسبقًا لتوفير المساحة، لذا يجب اعتبارها الآن ضمنية.

بعد ذلك، نقوم بإدخال tagPROP ثالث في قائمة tagPROPLIST، والذي سيكتب فوق آخر 8 بايت من _HEAP_ENTRY وأول 8 بايت من "كتلة التغطية". بهذه الطريقة نتمكن من تعديل _HEAP_ENTRY لـ "كتلة التغطية" لجعل حجمها أكبر من حجمها الفعلي، بحيث يمكنها استيعاب بنية tagWND المجاورة.
الآن نقوم بتحرير "كتلة التغطية" المفسدة، بحيث يضعها مدير الكومة في قائمة الكتل الحرة المقابلة لحجم الكتلة (كل قائمة حرة تتوافق مع حجم ثابت) أكبر من الحجم الفعلي لـ "كتلة التغطية". ثم نعيد استخدام هذه الكتلة عن طريق تعديل نص النافذة (الذي يمكننا التحكم فيه بالكامل). ومع ذلك، هناك مشكلة صغيرة نحتاج إلى حلها. عندما يتم تحرير "كتلة التغطية"، يحاول مدير الكومة العثور على الكتلة السابقة المجاورة، ويعتمد ذلك على حقل Size المفسد. يتحقق مدير الكومة مما إذا كانت هذه الكتلة المجاورة خالية لدمجها. بغض النظر عن كيفية الإشارة إليها، نريد التحكم فيها وتعيين علم "قيد الاستخدام". يمكننا تحقيق ذلك عن طريق تعديل تخطيط الكومة قليلاً. في هذه المرحلة، نضع كتلة كومة زائفة برأس كومة تم تعيين علم "قيد الاستخدام" عليها، ويتم تعيين قيمة PreviousSize على قيمة Size المفسدة، بحيث يمكننا تحقيق ذلك ببساطة عن طريق تخصيص نص نافذة لنافذة أخرى. يظهر تخطيط الكومة الجديد أدناه (الشكل 8):

الآن يمكننا تحرير "كتلة التغطية" المفسدة عن طريق تحديث سلسلة النص المرتبطة بنافذتها لجعل طولها أكبر من 0x10 بايت الأصلي. بهذه الطريقة، يتم تحرير "كتلة التغطية" المفسدة أولاً ووضعها في القائمة الحرة، ولكن حجمها مفسد، والحجم المعلن أكبر من الحجم الفعلي. يمكن تعديل هذا الحجم وفقًا لاحتياجاتنا الفعلية. بهذه الطريقة، يتم كتابة بيانات السلسلة الخاصة بنا إلى "كتلة التغطية"، ثم نستخدم "كتلة التغطية" هذه لإفساد tagWND المجاورة ببيانات عشوائية. كما هو موضح أدناه (الشكل 9):

هذا هو إفساد المرحلة 3. الآن يمكننا الكتابة فوق مؤشر strName.Buffer بأي بيانات نريدها. ومع ذلك، لا يزال هناك بعض المتاعب في إفساد البيانات الأخرى لـ tagWND، لكن هذه ليست مشكلة لأن كومة سطح المكتب تم تعيينها في مساحة المستخدم! لذلك قبل إفساد كل شيء، نقرأ جميع محتويات tagWND، ونعدل محتوى بنية strName إلى ما نريد، ونرسل جميع البيانات عن طريق تعديل نص النافذة.
من خلال strName، لا نحصل فقط على "بدائية" القراءة والكتابة العشوائية، بل يمكننا أيضًا تعديل strName بشكل متكرر، وهو ما تسمح به آلية تعديل نص النافذة. طالما أن طول السلسلة المكتوبة لا يتجاوز قيمة MaximumLength، يمكننا الاستمرار في استخدام نفس الكتلة. لذلك، في كل مرة نريد فيها تعديل عنوان strName لقراءة قيمة من مكان ما، نستخدم سلسلة جديدة ونضيف بياناتنا لتحديث "كتلة التغطية". يوضح الشكل 10 هذا الاستخدام المتكرر. لاحظ أنني قمت بتكبير دقة الرسم البياني مرة أخرى لعرض كل إفساد بالتفصيل.

هذا يعني أننا في النهاية نحتاج فقط إلى إفساد شيئين إضافيين (بالإضافة إلى عنصر قائمة tagPROPLIST الأصلي):
الآن، إذا أردنا قراءة بضع بايتات من مكان ما في الذاكرة، فإننا نستخدم دالة InternalGetWindowText() للاستعلام عن نص النافذة، حيث يوجد إدخال strName المفسد. يمكننا قراءة عدد البايتات المعلن عنها في حقل Length. وبالمثل، إذا أردنا الكتابة إلى موقع عشوائي في الذاكرة، فإننا نستخدم دالة NtUserDefSetText() لتحديث نص النافذة المفسد، ولكن يجب ألا يتجاوز مقدار الكتابة القيمة المعلنة في حقل MaximumLength (وهي قيمة يمكننا تعيينها أيضًا). بهذه الطريقة، يتم إعادة استخدام المخزن المؤقت الموجود ويشير إلى عنوان الذاكرة الذي نريد.
على الرغم من أن المخصص الخلفي لوضع المستخدم يستخدم ترميز الكومة منذ Windows Vista، إلا أن كومة سطح المكتب لم يتم تمكينها مطلقًا حتى Windows 8. لذلك، على الأنظمة التي تعمل بنظام Windows 8 وما بعده، هناك عقبة عند إجراء عملية التغطية لـ "كتلة التغطية". ومع ذلك، فإن بنية _HEAP التي تحتوي على الكومة تحتوي على ملف تعريف الارتباط (cookie) هذا وتستخدمه لترميز رأس الكومة بالكامل، لذلك يمكننا قراءة ملف تعريف الارتباط هذا من كومة سطح المكتب المعينة في مساحة المستخدم، ثم استخدامه لترميز رأس "كتلة التغطية" عن طريق عكس كود المخصص وتقليد عملياته، حيث يمكن للمخصص قبول هذه العملية.
أول ما يجب ملاحظته هو أن بنية tagPROP في أنظمة 32 بت هي 8 بايت، وليس 0xc بايت كما في أنظمة 64 بت، وحقل hData الذي نتحكم فيه هو 4 بايت فقط، وليس 8 بايت كما في أنظمة 64 بت. لا توجد بايتات حشو إضافية أيضًا، حيث توجد 8 بايتات حشو في أنظمة 64 بت، لذا فإن الهيكل بأكمله هو بالضبط 8 بايت. هذا يعني أننا لا نستطيع إفساد رأس الكتلة المجاورة بالكامل إذا تمكنا فقط من التحكم جزئيًا في البيانات. في بعض إصدارات Windows، يكون ذلك ممكنًا لأننا نستطيع التحكم في أهم الحقول، ولكن في Windows 8 و 8.1، يكون رأس الكومة مشفرًا، وفي النهاية يمكننا الكتابة بشكل غير آمن فوق جزء من رأس الكومة من خلال حقل fs. يبدو رأس _HEAP_ENTRY لأنظمة 32 بت مشابهًا، لكنه يفتقر إلى حقل PreviousBlockPrivateData.
ما زلنا لا نستطيع إفساد جميع أجزاء tagWND بسبب عدم القدرة على تجنب المؤشرات المقتطعة. ولم أجد كائنًا يلبي هذا، نظرًا لأن _LARGE_UNICODE_STRING يعمل بشكل جيد على أنظمة 64 بت، أردت استخدامه أيضًا على أنظمة 32 بت.
فكرتي هي أنه إذا تمكنا من إفساد حقل iFirstFree لبنية tagPROPLIST (فهرس أول خاصية تم تحريرها في قائمة الخصائص) عن طريق زيادة قيمة الفهرس، فيمكننا جعلها تشير إلى موقع أبعد على الكومة. على سبيل المثال، يمكننا جعلها تشير إلى الجزء العلوي من tagWND.strName. يوضح الشكل 11 هذه الفكرة:

لتوضيح العملية، سنستخدم الآن بنيتين من tagPROPLIST، هما "قائمة الخصائص A" المستخدمة لـ UAF، و"قائمة الخصائص B" أخرى. نحتاج إلى معرفة بالضبط أي أجزاء من tagPROP التي تم إدراجها في "قائمة الخصائص A" ستكتب فوق حقل iFirstFree لـ "قائمة الخصائص B". يجب أن نتذكر أيضًا أنه يمكننا الكتابة فقط 8 بايت في المرة الواحدة، لذلك يجب علينا إدراج tagPROP إضافي واحد على الأقل في "قائمة الخصائص A"، حيث يقوم الأول بإفساد رأس الكومة المجاورة، والثاني يصيب حقل tagPROPLIST لـ "قائمة الخصائص B". قد تختلف هذه وفقًا لأنظمة التشغيل المختلفة وأحجام الكومة المختلفة، ويجب أن يتكيف استغلالي مع تخطيطات الكومة المختلفة. يوضح الشكل 12 كيفية إفسادنا. لاحظ أن أول tagPROPLIST في الشكل لم يتم تقسيمه إلى حقول منفصلة، لذا فإن tagPROP[0] ضمني. ومع ذلك، في tagPROPLIST الثاني، تم تقسيم أعضائه الداخليين لإظهار عملية الإفساد لدينا. هذا هو السبب في ظهور tagPROP[0]:

أولاً نلاحظ أنه إذا كتبنا 8 بايت لكل tagPROP، فهذا يعني أننا نستطيع التحكم جزئيًا فقط في الكتابة فوق iFirstFree (لأنها تأتي من حقلي atomKey و fs)، وهذا هو ما يهمنا أكثر. نظرًا لأننا نستطيع التحكم الكامل في بايتين رئيسيين على الأقل من خلال قيمة atomKey، فعندما تكون هذه القيمة صغيرة بما يكفي، سيصبح حقل fs 0. لذلك نستخدم قيمة hData لكتابة قيمة معقولة لـ cEntries، ونستخدم atomKey لجعل iFirstFree يشير إلى tagWND، حيث يوجد مؤشر strName.Buffer الذي نريد الكتابة فوقه. إذا لم نتمكن من الكتابة مباشرة فوق قيم Length و MaximumLength، فيمكننا تخصيص سلسلة مسبقًا للنافذة الهدف لضمان تعيين طولها بالفعل إلى قيمة معينة.
دعنا نلقي نظرة على بنية tagWND لـ 32 بت لمعرفة ما يمكننا الحصول عليه. لاحظ أنني استخدمت المعلمة -b هذه المرة لتسهيل حساب إزاحة Buffer في strName.
kd> dt -b !tagWND
win32k!tagWND
+0x000 head : _THRDESKHEAD
+0x000 h : Ptr32
+0x004 cLockObj : Uint4B
+0x008 pti : Ptr32
+0x00c rpdesk : Ptr32
+0x010 pSelf : Ptr32
+0x014 state : Uint4B
+0x014 bHasMeun : Pos 0, 1 Bit
[أعلام مقتطعة]
+0x014 bDestroyed : Pos 31, 1 Bit
+0x018 state2 : Uint4B
[أعلام مقتطعة]
+0x018 bWMCreateMsgProcessed : Pos 31, 1 Bit
+0x01c ExStyle : Uint4B
+0x01c bWS_EX_DLGMODALFRAME : Pos 0, 1 Bit
[أعلام مقتطعة]
+0x01c bUIStateFocusRectHidden : Pos 31, 1 Bit
+0x020 style : Uint4B
+0x020 bReserved1 : Pos 0, 16 Bits
[أعلام مقتطعة]
+0x020 bWS_POPUP : Pos 31, 1 Bit
+0x024 hModule : Ptr32
+0x028 hMod16 : Uint2B
+0x02a fnid : Uint2B
+0x02c spwndNext : Ptr32
+0x030 spwndPrev : Ptr32
+0x034 spwndParent : Ptr32
+0x038 spwndChild : Ptr32
+0x03c spwndOwner : Ptr32
+0x040 rcWindow : tagRECT
+0x000 left : Int4B
+0x004 top : Int4B
+0x008 right : Int4B
+0x00c bottom : Int4B
+0x050 rcClient : tagRECT
+0x000 left : Int4B
+0x004 top : Int4B
+0x008 right : Int4B
+0x00c bottom : Int4B
+0x060 lpfnWndProc : Ptr32
+0x064 pcls : Ptr32
+0x068 hrgnUpdate : Ptr32
+0x06c ppropList : Ptr32
+0x070 pSBInfo : Ptr32
+0x074 spmenuSys : Ptr32
+0x078 spmenu : Ptr32
+0x07c hrgnClip : Ptr32
+0x080 hrgnNewFrame : Ptr32
+0x084 strName : _LARGE_UNICODE_STRING
+0x000 Length : Uint4B
+0x004 MaximumLength : Pos 0, 31 Bits
+0x004 bAnsi : Pos 31, 1 Bit
+0x008 Buffer : Ptr32
+0x090 cbwndExtra : Int4B
+0x094 spwndLastActive : Ptr32
+0x098 hImc : Ptr32
+0x09c dwUserData : Uint4B
+0x0a0 pActCtx : Ptr32
+0x0a4 pTransform : Ptr32
+0x0a8 spwndClipboardListenerNext : Ptr32
+0x0ac ExStyle2 : Uint4B
+0x0ac bClipboardListener : Pos 0, 1 Bit
[أعلام مقتطعة]
+0x0ac bChildNoActivate : Pos 11, 1 Bit
إزاحة strName هي 0x84، وإزاحة Buffer هي 0x8c. نعلم أن لدينا فهرس عنصر قائمة tagPROP، ونعلم أيضًا أنه يمكننا كتابة 8 بايت. لذلك يمكننا بسهولة معرفة ما إذا كان iFirstFree يشير إلى إزاحة 0x88 من النافذة، وهو ما يشير إليه MaximumLength. نظرًا لأننا نستطيع التحكم فقط في بايتين من Buffer، فإن عملية الكتابة غير ممكنة، وبالنظر إلى أن هدفنا هو جعل هذا "بدائية" القراءة والكتابة العشوائية، فإن هذه النتيجة غير مقبولة. إذا كتبنا الفهرس التالي ليشير إلى 0x90، فسنكتب فوق cbwndExtra، وهذا ليس ما نبحث عنه.
بالعودة إلى ما يمكننا التحكم فيه عند القيام بهندسة الكومة السابقة، ثم النظر في tagWND لمعرفة ما إذا كان هناك إزاحة مثيرة للاهتمام يمكننا التحكم فيها. في tagWND، عند الإزاحة 0x70 يوجد حقل pSBInfo. هذه الإزاحة قابلة للقسمة على 8، لذلك يمكننا استخدام جزء من بيانات hData من tagPROP الزائف لكتابة هذا المؤشر.
هل يمكننا كتابة pSBInfo ليشير مباشرة إلى strName في نفس بنية tagWND؟ ربما يمكننا استخدام واجهة برمجة تطبيقات شريط التمرير لإفساد strName لتحقيق هدفنا.
يشير pSBInfo إلى بنية tagSBINFO، والتي تم ذكرها في عملية UAF الأولية.
kd> dt -b !tagSBINFO
win32k!tagSBINFO
+0x000 WSBflags : Int4B
+0x004 Horz : tagSBDATA
+0x000 posMin : Int4B
+0x004 posMax : Int4B
+0x008 page : Int4B
+0x00c pos : Int4B
+0x014 Vert : tagSBDATA
+0x000 posMin : Int4B
+0x004 posMax : Int4B
+0x008 page : Int4B
+0x00c pos : Int4B
نتذكر أن WSBflags لا يمنحنا سيطرة أكبر، لكننا نعرف على الأقل أنه يتم تعيينه على 1 عند تمكين شريط التمرير، ويتم تعيينه على 0 عند تعطيله. لا يمكن تعيين حقل العلم هذا بشكل عشوائي، ومن خلال عكس الوظائف ذات الصلة، نجد أنه إذا لم يتم تغيير حالة شريط التمرير، فإن حقل العلم هذا يظل كما هو. تبدو القيم في بنية tagSBDATA أكثر إثارة للاهتمام. إذا قرأنا وثائق SetScrollInfo()، يمكننا فهم معنى هذه القيم جيدًا. يبدو أنه يمكننا تمرير المعلمات إلى SetScrollInfo() من خلال بنية SCROLLINFO. طالما أن هناك عنصر تحكم شريط تمرير بالقرب من النافذة التي نريد إفسادها، يمكننا التعامل مباشرة مع مؤشر pSBInfo (حيث سيرسل رسالة نافذة خاصة إلى عنصر تحكم النافذة المرتبط). من الواضح أنه يمكننا التحكم غير المشروط في قيم posMin و posMax. هناك بعض المتاعب مع حقلي page و pop، حيث أنهما مقيدان بنطاق معين، ونحن الآن نحاول تجنب ذلك. قم بتعيين علامة SIF_RANGE لبنية SCROLLINFO للإعلان عن المكان الذي نريد تعيين القيم القصوى والدنيا فيه.
نريد الكتابة فوق Buffer ببيانات عشوائية، وهذا يعني أننا نريد أن يكتب posMin فوقه، لذلك يمكننا كتابة pSBInfo ليشير إلى strName.MaximumLength. طالما أننا لا نقوم بتمكين أو تعطيل شريط التمرير، فلن يتم تعديل حقل WSBflags، مما يضمن سلامة strName.MaximumLength. هذا يعني أنه بغض النظر عن كيفية تعيين posMin (من خلال nMin في SCROLLINFO)، فسيتم كتابته فوق Buffer، وسيتم كتابة posMax فوق cbwndExtra. هذه ليست مشكلة كبيرة؛ في أنظمة 64 بت، يمكننا قراءة هذه القيمة مسبقًا واستعادتها لاحقًا. يظهر الفكرة العامة للتجاوز في الشكل 13:

الآن نشرح عملية الهجوم على أنظمة 32 بت في الشكل 14. قبل إفساد أي عنوان بعيد عن UAF، نتراجع أولاً لننظر في الكتل ذات الصلة وتخطيط الكومة في الشكل. الآن بعد أن عرفنا المزيد من التفاصيل، أصبحت الخطوات التالية واضحة.

بعد ذلك، نقوم بإدراج عنصري خاصية في "قائمة الخصائص A"، مما سيفسد البيانات القريبة من "قائمة الخصائص A"، وذلك بفضل إفساد UAF السابق، وفي نفس الوقت يجعل iFirstFree لـ "قائمة الخصائص A" يشير إلى pSBInfo. لاحظ أن هذا سيفسد أيضًا قيمة pSBInfo القريبة، ولكن يمكننا قراءتها مسبقًا لاستعادتها بعد الإفساد.

نقوم بإدراج tagPROP جديد في "قائمة الخصائص B"، مع معرف atom مختلف عن الموجود بالفعل في القائمة، بحيث يتم إدراج tagPROP هذا في فهرس العنصر الحر التالي. مما يؤدي إلى إفساد pSBInfo ليشير إلى strName.MaximumLength في نفس tagWND.

أخيرًا، نقوم بتحديث شريط التمرير لإفساد حقل strName.Buffer (كما هو موضح في الشكل 17):

اعلم أنه على عكس حالة 64 بت، لا يمكننا إفساد قيم طول strName. يمكننا تخصيص سلسلة نص نافذة بطول مناسب مسبقًا بحيث تكون قيمتها قيد الاستخدام بالفعل. بعد ذلك، سواء أردنا قراءة أو كتابة بعض البيانات من عنوان kernel، نحتاج فقط إلى استدعاء SetScrollInfo() لتشغيل النافذة الهدف لتحديث قيمة Buffer، ثم استخدام واجهة برمجة تطبيقات نص النافذة للعمل.
الآن لدينا "بدائية" قابلة لإعادة الاستخدام للقراءة والكتابة العشوائية على أنظمة 32 بت!
كل شيء من الآن فصاعدًا يفترض أن لدينا "بدائية" قراءة/كتابة عشوائية. لذلك عندما أقول تسريب/قراءة قيمة معينة أو تعديل قيمة أخرى، فهذا يعني تنفيذ هذه "البدائية" التي تم إنشاؤها في مرحلة الإفساد السابقة. هذه "البدائية" هي نفسها تقريبًا على كلا النظامين الأساسيين. كل ما تبقى هو تعديل مؤشر دالة وجعله يشير إلى حمولة شيل كود في مكان ما. الطريقة الشائعة هي الكتابة فوق العنصر الثاني في nt!HalDispatchTable، والذي يتوافق مع دالة HalQuerySystemInformation(). ثم في وضع المستخدم، استدعاء دالة NtQueryInternalProfile() لتشغيلها.
نحتاج إلى معرفة عنوان تحميل وحدة kernel الأساسي لحساب عنوان kernel لـ nt!HalDispatchTable. للقيام بذلك، يمكننا استدعاء NtQuerySystemInformation() في وضع المستخدم للحصول على معلومات الوحدة، والتي تتضمن عنوان الوحدة الأساسي.
// القيمة المعدودة 11 تمثل SystemModuleInformation، وهي غير موثقة...
rc = NtQuerySystemInformation((SYSTEM_INFORMATION_CLASS)11, pModuleInfo, 0x100000, NULL);
بعد ذلك، نقوم بتحميل ntoskrnl.exe في وضع المستخدم للعثور على إزاحة nt!HalDispatchTable، مما يتيح لنا الحصول على عنوان مساحة kernel الخاص به. ثم نستخدم "بدائية القراءة" لقراءة عنوان kernel لـ HaliQuerySystemInformation() (الوظيفة غير المصدرة) لتعديله، ثم نستخدم "بدائية الكتابة" لإفساد مؤشر هذه الدالة ليشير إلى عنوان shellcode (يمكن أن يكون في مساحة عنوان kernel أو مساحة المستخدم، كما هو موضح لاحقًا). عدد البايتات المقروءة والمكتوبة هو نفسه على أنظمة 32 بت و 64 بت.
قدم Windows 8 و 8.1 دعمًا لـ SMEP، ويمكن أيضًا تمكينه بواسطة بعض منتجات الأمان على Windows 7، لذلك نفترض أنه موجود بالتأكيد. يمنعنا SMEP من تنفيذ الكود في مساحة المستخدم بصلاحيات kernel، مما يجعل تعديل عناصر nt!HalDispatchTable ليشير إلى عناوين مساحة المستخدم غير قابل للاستخدام. لذلك، نريد أن يشير إلى موقع قابل للتحكم في مساحة kernel، حيث يمكن للكود الموجود تعديل قيمة سجل cr4 لتعطيل SMEP، ثم نتمكن من القفز إلى مساحة عنوان المستخدم. تقدم مقالة MWR تقنية مثيرة للاهتمام على أنظمة 64 بت، من خلال تعيين إدخالات جدول الصفحات الخاصة بنا، والحصول على عنوان فعال في مساحة kernel لأي عنوان افتراضي. ثم نستخدم "بدائية الكتابة" لتعديل إدخالات جدول الصفحات مباشرة وتعديل بتات القناع. قمت بنقل هذه التقنية إلى أنظمة 32 بت، ولكن هناك بعض الاختلافات بين الأنظمة التي تم تمكين PAE فيها وتلك التي لم يتم تمكينها.
لتحقيق ذلك، الطريقة الواضحة هي تعيين عنوان مساحة المستخدم إلى مساحة kernel، ثم استخدام "بدائية الكتابة" لجعل إدخالات جدول الصفحات ذات صلاحيات النظام بدلاً من صلاحيات المستخدم. هذا هو أول شيء نفعله. عندما قمت بتنفيذ ذلك على Windows 8، واجهت مشكلة مثيرة للاهتمام. يقوم مدير سطح المكتب (dwm.exe) في Windows 8 وما بعده بمسح النوافذ على سطح المكتب بشكل دوري والاستعلام عن أسمائها، لأي سبب لم أتحقق منه. لا ترسل هذه العملية رسالة إلى النافذة، ولكن لديها وظيفة معالجة نافذة مقابلة تستدعي GetInternalWindowText(). لذلك، المشكلة هي أن استخدام حقول strName للنافذة لتعديل إدخالات جدول الصفحات التي تحتوي على shellcode، والتي تنتمي إلى مساحة العملية الخاصة بنا. عندما يحصل dwm.exe على اسم النافذة من kernel، تتسبب إدخالات جدول الصفحات المعدلة في قيام kernel بالتحقق مما إذا كان strName.Buffer فارغًا، مما يؤدي إلى الإشارة غير المباشرة إلى هذا العنوان، وإذا كان العنوان غير صالح، فسيؤدي ذلك إلى تعطل النظام.
لتلبية استعلامات dwm.exe، استخدمت عنوان kernel كحمولة. بهذه الطريقة، بغض النظر عن كيفية تحميل أي شيء بواسطة العملية الحالية، فإن إدخالات جدول الصفحات المرتبطة بهذا العنوان ستظل صالحة دائمًا. اخترت وضعها على كومة سطح المكتب، لأننا نستطيع حساب عنوان kernel الخاص بها من خلال الطريقة المذكورة سابقًا. ما زلنا نستخدم تقنية تعيين إدخالات جدول الصفحات الخاصة بنا، حيث تم وضع علامة على إدخالات جدول الصفحات على أنها ذات صلاحيات عالية، ولكن لم يتم وضع علامة عليها على أنها قابلة للتنفيذ. لذلك كل ما علينا فعله هو تعيين بت التنفيذ.
الخطوات كالتالي:
في Windows 8.1، هناك مشكلة أخرى وهي أن NtQuerySystemInformation() تتحقق من قيمة SID للصلاحية المنخفضة، مما يعني أنه فقط الصلاحية المتوسطة أو الأعلى هي التي يمكنها الحصول على عنوان kernel الأساسي. يمكن تحقيق ذلك بسهولة من خلال تقنية sidt المعروفة. نقوم بحفظ عنوان IDT في وضع المستخدم (لا يتطلب هذا التحقق من الصلاحية)، ثم نستخدم "بدائية القراءة" لقراءة فهرس IDT المطلوب، والذي يشير غالبًا إلى مساحة عنوان kernel، لذلك يمكننا تسريب عنوان kernel لمعالج المقاطعة، ثم البحث عن الإزاحة في ملف PE المقابل لوحدة kernel.
بمجرد الحصول على عنوان تحميل kernel الأساسي، يمكننا حساب عنوان nt!HalDispatchTable.
الطريقة المعتادة هي تحميل ملف ntoskrnl.exe وتفسير إزاحة الرموز الخاصة به، ثم إضافة عنوان kernel الأساسي المسرب. ومع ذلك، هذا لا يعمل مع صندوق الحماية المحسن، لأن هناك قيودًا على نظام الملفات نفسه، ولا يمكنك قراءة C:\windows\system32\ntoskrnl.exe. لتجاوز هذا القيد، نستخدم "بدائية التسريب" الخاصة بنا لتحليل إزاحة الرمز المطلوب من صورة PE الخاصة بـ kernel في الذاكرة.
هذه هي جميع المواد. شكرًا لقراءتك. باستخدام التقنيات المقدمة في هذه المقالة، تمكنت من تحقيق استغلال مستقر لجميع أنظمة 32 بت و 64 بت، بما في ذلك XP و Vista و 7 و 8 و 8.1 و Server 2012. في Windows 2003 و 2008، لا يعمل بشكل افتراضي لأنه لا يمكن اعتراض استدعاءات وضع المستخدم مرة أخرى، لذلك لا يمكن مهاجمة هذين النظامين ما لم يتم استيفاء الشروط المطلوبة. عملية الاستغلال معقدة للغاية، وهناك العديد من العقبات التي يجب التغلب عليها، لكن هذا يوفر أيضًا الكثير من المتعة والأشياء التي تستحق التعلم، وقد تم ذكر العديد من الأساليب القابلة للتطبيق ونتائج الأبحاث المستخدمة في هذه المقالة في مقالات باحثين آخرين. على حد علمي، هناك إجراء تخفيف واحد فقط يمكنه منع استغلال win32k.sys، وهو صندوق حماية Google Chrome، الذي يمنع بشكل فعال استدعاءات نظام kernel لـ win32k في وقت التشغيل. آمل في أي تحسينات أو ملاحظات، إذا كان هناك أي نقص في بعض التقنيات التي اقترحتها، فأخبرني بذلك وسأقوم بتحديث هذه الوثيقة. يمكنك الاتصال بي عبر تويتر @fidgetingbits أو عبر البريد الإلكتروني [email protected].