
إثبات المفهوم لـ cve-2019-1458
في ديسمبر نشرت كاسبرسكي تدوينة حول [استغلال 0day المستخدم في البرية][1]. أثار ذلك اهتمامي لأنه على الرغم من وصفهم لكيفية عمل الاستغلال، لم يقدموا أي POC في تحليلهم.
لهذا قررت محاولة كتابة POC لهذه الثغرة بالاعتماد على تدوينة كاسبرسكي وتحليل التصحيح.
تصف هذه التدوينة رحلتي في فعل ذلك.
أول شيء كان جمع أكبر قدر ممكن من المعلومات حول هذه الثغرة. بقراءة التدوينة المذكورة استخرجت المعلومات التالية:
NtUserMessageCallwin32k!DrawSwitchWndHiliteبالإضافة إلى ذلك، هناك لقطة شاشة جميلة للكود المُفكك تُظهر بعض الأشياء المذكورة سابقًا.
على وجه الدقة، تُظهر: إنشاء نافذة التبديل، استدعاء دالة باسم toggle_alt_key واستدعاءات متعددة لـ NtUserMessageCall.
[مصدر الصورة][1]
الكثير من المعلومات المفيدة، لكنها لا تزال لا تصف كيفية عمل هذه الثغرة بالضبط وكيفية تفعيلها.
[الوحدة المتأثرة كانت win32k.sys][2]. قمت بتحميل النسختين المصححة وغير المصححة من هذه الوحدة.
بالنسبة لـ Win7 x64 كانت هاتان:
يمكن تنزيلهما من [Microsoft Update Catalog][3]
إليك نتيجة bindiff لمقارنة النسختين

بعد استبعاد الدوال المرتبطة بوظيفة DebugHook، كل ما يتبقى لنا حقًا هو هذه الدالة المتغيرة قليلًا InitFunctionTables()

بالتأكيد ليس أكبر تصحيح في العالم.
لن يساعد هذا في تحديد السبب الجذري لهذه الثغرة فورًا. لكن من الجدير بالملاحظة أنه تمت إضافة بعض القيم الأولية للمتغيرات عند
*(gpsi+0x14E), *(gpsi+0x154), *(gpsi+0x180). لذلك قد يكون هذا خطأ متعلقًا بمتغير غير مهيأ.
في هذا القسم سأعرض كيف بنيت POC الذي يفعّل هذه الثغرة تدريجيًا، مع اكتشاف ماهية الثغرة في الوقت نفسه.
لم تقدم مقارنة التصحيحات الكثير من المعلومات المفيدة في البداية، لذلك اعتمدت بشكل أساسي على تدوينة كاسبرسكي في المرحلة الأولى من التطوير.
للحصول على بيئة اختبار جيدة، أعددت جهازًا افتراضيًا (VM) بنظام Win7 SP1 x64 يعمل بأحدث نسخة معرضة للثغرة من win32k. بالإضافة إلى ذلك، ربطت Windbg بهذا الجهاز الافتراضي للقيام بتصحيح أخطاء النواة، وأثناء ذلك قمت أيضًا بإعداد مسار خادم الرموز.
بدأت تحقيقي بالنظر إلى win32k!DrawSwitchWndHilite التي ذُكرت في التدوينة. يتم استدعاؤها من مكانين: xxxMoveSwitchWndHilite و xxxPaintSwitchWindow، والأخيرة لفتت انتباهي فورًا بسبب استدعاءات GetKeyState/GetAsyncKeyState المحيطة بها والتي ذُكرت في التقرير الأصلي. والأكثر من ذلك أن هذه الاستدعاءات تتحقق من كون مفتاح ALT مضغوطًا.

استدعاء DrawSwitchWndHilite من xxxPaintSwitchWindow
بمتابعة مراجع الاستدعاء المتقاطعة (xxxWrapSwitchWndProc->xxxSwitchWndProc->xxxPaintSwitchWindow->DrawSwitchWndHilite) وجدت أن العنصر الأول في تلك السلسلة مُشار إليه في InitFunctionTables، الدالة التي تم إصلاحها في التصحيح.
بعد ذلك نظرت في NtUserMessageCall من لقطة شاشة الكود المُفكك.
إليك تعريف هذه الدالة```cpp
NtUserMessageCall(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam, ULONG_PTR ResultInfo, DWORD dwType, BOOLEAN bAnsi)
يستدعيها الاستغلال مع `msg = 0x14` و `dwType = 0xE0`. لنرَ ما الذي يفعله.```cpp
HINSTANCE hInstance = GetModuleHandle(NULL);
WNDCLASSEX wcx;
ZeroMemory(&wcx, sizeof(wcx));
wcx.hInstance = hInstance;
wcx.cbSize = sizeof(wcx);
wcx.lpszClassName = L"SploitWnd";
wcx.lpfnWndProc = DefWindowProc;
printf("[*] Registering window\n");
ATOM wndAtom = RegisterClassEx(&wcx);
if (wndAtom == INVALID_ATOM) {
printf("[-] Failed registering SploitWnd window class\n");
exit(-1);
}
printf("[*] Creating instance of this window\n");
HWND sploitWnd = CreateWindowEx(0, L"SploitWnd", L"", 0, 0, 0, 0, 0, NULL, NULL, hInstance, NULL);
if (sploitWnd == INVALID_HANDLE_VALUE) {
printf("[-] Failed to create SploitWnd window\n");
exit(-1);
}
NtUserMessageCall(sploitWnd, WM_ERASEBKGND, 0, 0, 0, 0xE0, 1);
هنا قمت بتسجيل فئة نافذة بسيطة وأنشأت نافذة من تلك الفئة. ثم استدعيت NtUserMessageCall بنفس معاملات الاستغلال. لمعرفة ما يحدث داخل الكود، قمت بتعيين نقطة توقف kd> ba e 1 win32k!NtUserMessageCall وتشغيل الكود.
يتم إجراء عدد غير قليل من الاستدعاءات لهذه الدالة، لذا كان عليّ التقاط الاستدعاء الصحيح، لكن الأمر لم يكن بهذه الصعوبة؛ كان الاستدعاء ذا مكدس استدعاء قصير جدًا.

NtUserMessageCall
عند التنقل في الكود تبيّن أنها تستدعي دالة من مصفوفة gapfnMessageCall؛ يُحسب الفهرس بناءً على قيمة msg ويساوي 0، لذلك يتم الاستدعاء إلى NtUserfnDWORD.

NtUserfnDWORD
الاستدعاء التالي يتم باستخدام قيمة dwType، والآن إزاحة gpsi تساوي 0x40، ويؤدي الاستدعاء إلى xxxWrapSwitchWndProc (هذه الدالة ظهرت بالفعل عندما كنت أفحص سلسلة استدعاءات DrawSwitchWndHilite).
xxxWrapSwitchWndProc تستدعي ببساطة xxxSwitchWndProc.

xxxSwitchWndProc
وهذه هي النهاية؛ يفشل الكود هنا ولا يتقدم أكثر إلى xxxPaintSwitchWindow، وهو المكان الذي نريد الوصول إليه بناءً على قيمة msg (0x14). دعنا نتحقق من السبب.
يفشل الكود في هذه المرحلة لأنه، كما هو موضح في الصورة السابقة، قيمة fnid لنافذتنا لا تساوي 0x2A0 (FNID_SWITCH) والرسالة التي نرسلها لا تساوي 1، وبالتالي ننتهي في xxxDefWindowProc. لتجنب هذا السيناريو، يجب علينا استدعاء xxxSwitchWndProc مع ضبط قيمة fnid إلى FNID_SWITCH، حتى ننتقل مباشرة إلى جملة switch ثم إلى xxxPaintSwitchWindow.
كيف نضبط قيمة fnid الصحيحة؟ في الواقع، الدالة نفسها تقوم بذلك في كتلة if الأولى؛ كل ما علينا فعله هو إفشال جميع الفحوصات داخلها للوصول إلى التعليمة التي تضبط fnid.
إليك الشروط التي يجب تحقيقها لإفشال فحوصات if الثلاثة:
fnid == 0 و cbwndExtra + 0x128 >= *(gpsi + 0x154)0 لكل نافذة مستخدم منشأة حديثًا.
*(gpsi+0x154) تساوي 0 في win32k! غير المُرقّع. ولكن حتى لو تم تعيينها إلى 0x130، كما في الإصدار المُرقّع، يمكننا ضبط cbwndExtra إلى 8 أو أكثر وما زال بإمكاننا تجاوز الفحص الأول.msg == 1NtUserMessageCall. ومع ذلك، مع ضبط msg إلى 1، يمر تدفق التحكم عبر NtUserfnINLPCREATESTRUCT بدلاً من NtUserfnDWORD، لكنه لا يزال ينتهي في xxxSwitchWndProc.extraData == 0cbwndExtra المذكور. يتم إلحاق ExtraData مباشرة بعد بنية tagWND (أضفت هذا الحقل إلى بنية tagWND في IDA كـ QWORD عند الإزاحة sizeof(tagWND)، لجعل الكود المُفكك أفضل قليلًا). يمكن تعيين قيمته عبر استدعاء SetWindowLongPtr.إذا تحققت جميع هذه الشروط، سيتم تعيين قيمة fnid للنافذة إلى FNID_SWITCH.
إذن نحتاج الآن إلى استدعاء NtUserMessageCall مرتين: الأولى بقيمة msg تساوي 1 لتعيين fnid المطلوب، والثانية للوصول إلى xxxPaintSwitchWindow.```cpp
HINSTANCE hInstance = GetModuleHandle(NULL);
WNDCLASSEX wcx;
ZeroMemory(&wcx, sizeof(wcx));
wcx.hInstance = hInstance;
wcx.cbSize = sizeof(wcx);
wcx.lpszClassName = L"SploitWnd";
wcx.lpfnWndProc = DefWindowProc;
wcx.cbWndExtra = 8; //to pass check in xxxSwitchWndProc