
تحليل تقني مفصل واستغلال إثبات المفهوم لـ CVE-2024-30051، وهو تجاوز سعة المخزن المؤقت في الكومة في مكتبة DWM Core لويندوز مما يتيح تصعيد الامتيازات المحلية إلى مستوى Integrity System.
في هذه التدوينة، سأشرح ثغرة في مكتبة DWM الأساسية لنظام Microsoft Windows قمت بتحليلها أثناء تطوير استغلال لـ Core Impact. تسمح للمهاجم غير المصرح له بتنفيذ تعليمات برمجية كمستخدم DWM بصلاحيات نظام التكامل (Integrity System) (CVE-2024-30051).
نظرًا لعدم توفر معلومات عامة كافية في ذلك الوقت لتطوير الاستغلال، اضطررت إلى إجراء هندسة عكسية كثيرة، لذا سأعرض هنا كيفية إجراء الهندسة العكسية للتصحيح KB5037771 لنظام Windows 23H2 باستخدام IDA PRO. سأستخدم BINDIFF لإجراء مقارنة ثنائية بين dwmcore.dll الإصدار 10.0.22621.3447 والإصدار 10.0.22621.3593، وسأشرح كيفية حدوث تجاوز سعة الكومة (heap overflow)، ثم سأستغله لرفع الامتيازات، وأخيرًا سأُنشئ PoC عملي.
الفهرس:
[Windows DWM Core Library Elevation of Privilege Vulnerability (CVE-2024-30051) 1](#windows-dwm-core-library-elevation-of-privilege-vulnerability-cve-2024-30051)
[تفاصيل الثغرة: 2](#تفاصيل-الثغرة)
[المقارنة لاكتشاف الخلل: 3](#المقارنة-لاكتشاف-الخلل)
[تحليل PoC الذي يستغل CVE-2024-30051: 8](#تحليل-poc-الذي-يستغل-cve-2024-30051)
[1) التهيئة 8](#التهيئة)
[2) الحوكة (Hooking) 8](#الحوكة-hooking)
[3) إنشاء النافذة 16](#إنشاء-النافذة)
[4) إنشاء الجهاز 16](#إنشاء-الجهاز)
[5) إنشاء المصنع 22](#إنشاء-المصنع)
[6) إنشاء سياق الجهاز 28](#إنشاء-سياق-الجهاز)
[7) إنشاء جهاز التركيب 29](#إنشاء-جهاز-التركيب)
[8) استدعاء دالة hook3 31](#استدعاء-دالة-dcompositioncreatedevice)
[9) إنشاء هدف لـ HWND 32](#إنشاء-هدف-لمقبض-hwnd)
[10) إنشاء سطح 33](#إنشاء-سطح)
[11) استدعاء BeginDraw و EndDraw و CreateVisual 34](#استدعاء-begindraw-enddraw-و-createvisual)
[11) استدعاء Visual SetContent 36](#استدعاء-visual-setcontent)
[12) تحرير الكائنات 38](#تحرير-الكائنات)
[13) التزام جهاز التركيب 38](#التزام-جهاز-التركيب)
[14) استدعاء hook2 39](#استدعاء-hook2)
[15) استدعاء hook 39](#تذكر-أن-الدالة-القابلة-للاختراق-يمكن-الوصول-إليها-باستخدام-بعض-طرق-الفئة-cprimitivegroup.-في-هذه-النقطة-تنشئ-كومة-ثم-يقوم-hook2-بالتقاط-وحفظ-heaphandle-المناظر.)
[16) استدعاء hook4 41](#استدعاء-الدالة-hook4)
[17) تنفيذ رش الكومة (Heap Spray) 49](#تنفيذ-رش-الكومة-heap-spray)
[18) تعديل القطعة الأساسية قبل الإرسال 51](#تعديل-القطعة-الأساسية-قبل-الإرسال)
[19) تصحيح عملية DWM 52](#تصحيح-عملية-dwm)
[20) رفع الامتيازات إلى مستوى نظام التكامل 62](#رفع-الامتيازات-إلى-مستوى-نظام-التكامل)
ثغرة رفع الامتيازات في مكتبة DWM الأساسية لنظام Windows CVE-2024-30051
تاريخ الإصدار: 14 مايو 2024
CNA المخصص: Microsoft CVE-2024-30051
التأثير: رفع الامتيازات
الحد الأقصى للخطورة: مهم
الضعف:
CWE-122: تجاوز سعة المخزن المؤقت على الكومة (Heap-based Buffer Overflow)
CVSS: 3.1 7.8 / 7.2
توجد الثغرة بسبب خطأ في حساب الحجم في عملية قسمة عدد صحيح داخل مكتبة DWM الرئيسية لنظام Windows المسماة dwmcore.dll. يمكن لمستخدم محلي التسبب في تجاوز سعة المخزن المؤقت على الكومة في طريقة CCommandBuffer::Initialize في dwmcore.dll وتنفيذ تعليمات برمجية عشوائية كمستخدم DWM بصلاحيات نظام التكامل (Integrity System Privileges). سيقوم الاستغلال بتنفيذ رش الكومة (Heap Spray) في عملية DWM لتحضير الذاكرة وأخيرًا ينتج تجاوز سعة الكومة في dwmcore.dll والذي سيتم تشغيله عن طريق تحرير أجزاء معينة من رش الكومة.
بمجرد نجاح الاستغلال، ستقوم عملية DWM بتحميل DLL خاص بنا الذي ينفذ تعليماتنا البرمجية أو ملفنا التنفيذي (في حالتنا CMD) كمستخدم DWM الذي يمتلك صلاحيات نظام التكامل.

دعنا نستعرض هذه الثغرة ونرى كيف تسمح لنا بالعمل كمستخدم DWM بمستوى تكامل SYSTEM. لاحظ أنه نظرًا لأن هذا ليس مستخدمًا ينتمي إلى مجموعة المسؤولين، فإن لديه بعض القيود على الصلاحيات.
يمكن تنزيل التصحيح لنظام Windows 11 23H2 من:
https://www.catalog.update.microsoft.com/Search.aspx?q=KB5037771
windows11.0-kb5037771-x64_19a3f100fb8437d059d7ee2b879fe8e48a1bae42.msu
الإصدار الضعيف من dwmcore.dll هو: 10.0.22621.3447
الإصدار المصحح من dwmcore.dll هو: 10.0.22621.3593
من خلال تحليل الدوال المتغيرة، من الواضح أن الإصدار المصحح من CCommandBuffer::Initialize يحتوي على العديد من الكتل المضافة، مما يجعله يبدو مختلفًا تمامًا عن الإصدار غير المصحح.

بعد إجراء الهندسة العكسية الثابتة لتلك الدالة، هناك استدعاءان لـ CD2DSharedBuffer::GetBufferSize.
الاستدعاء الأول يحصل على الحجم الذي سيتم تخصيصه في new والاستدعاء الثاني يحصل على نفس الحجم لـ memcpy.

يبدو كل شيء صحيحًا في البداية. ومع ذلك، قبل التخصيص، يقوم بإجراء بعض العمليات على الحجم.

يحصل على buffer_size و buffer_size2 عن طريق استدعاء نفس الدالة CD2DSharedBuffer::GetBufferSize، ويعيد كلاهما نفس القيمة. لكن في new يقوم بعملية مسبقة، وهي قسمة عدد صحيح لـ buffer_size على 0x90 ثم ضرب الناتج في 0x90، بينما في memcpy يستخدم القيمة المعادة buffer_size2 دون إجراء أي عملية عليها.
بهذه العمليات، وجدت أن الحجم المستخدم في النهاية في new وفي memcpy يمكن أن يكون مختلفًا.
buffer_size = buffer_size2 (الأحجام المعادة)
size_new= buffer_size/0x90 x 0x90
size_memcpy=buffer_size2
على سبيل المثال، إذا كان buffer_size يساوي 0x91
buffer_size = buffer_size2=0x91
size_new= buffer_\ size/0x90 x 0x90 =0x90
size_memcpy= buffer_size2= 0x91
هذا المثال يثبت وجود تجاوز سعة الكومة. إنه ينسخ بايتات أكثر من المخصص، والحجم قابل للتحكم.
على سبيل المثال، إذا كان buffer_size يساوي 0x23f كما استخدم في POC.
buffer_size = buffer_size2=0x23F
size_new= buffer_size/0x90 x 0x90 =0x1b0
size_memcpy== buffer_size2=0x23f
مع تحليل الدالة القابلة للاختراق، أردت أن أرى كيفية الوصول إلى الدالة القابلة للاختراق CCommandBuffer::Initialize. هنا تبدأ الأمور في التعقيد.
بالنظر إلى المراجع لهذه الدالة، يبدو أنه يتم الوصول إليها من طرق الفئة CPrimitiveGroup:

يمكن الوصول إلى هذه الطرق من جدول الدوال الافتراضية (vftable) لكائنات CPrimitiveGroup:

لها منشئ (constructor):

ويتم الوصول إليها بهذه الطريقة:

بما أنني مررت بهذه العملية في البداية، استغرقت وقتًا لقراءة PDF "العالم المفقود لـ DirectComposition: صيد أخطاء مدير نوافذ سطح المكتب في Windows" وتعمقت في عالم Direct Composition. ساعدني هذا في إنشاء أول PoC لي.
أيضًا، كنت بحاجة إلى إجراء هندسة عكسية لـ win32ksys وحاولت إرسال الحزم من خلال الدوال:
NtDCompositionCreateChannel
NtDCompositionProcessChannelBatchBuffer
NtDCompositionCommitChannel

وصل أول PoC لي إلى منشئ CPrimitiveGroup. ومع ذلك، بعد الكثير من الهندسة العكسية، لم أجد طريقة للتعامل مع استدعاءات طرق vftable للوصول إلى الدالة القابلة للاختراق مباشرة عبر استدعاءات ALPC باستخدام هذه الدوال.
قضيت الكثير من الوقت في إجراء هندسة عكسية معقدة. خلال هذه العملية، عثرت على عينة من البرامج الضارة التي استغلت الثغرة، والتي كانت مفيدة للغاية لأن طريقة الاستغلال أكثر تعقيدًا مما كنت أعتقد في البداية. كما أنها تتضمن عدة حوكات (hookings) لواجهات برمجة تطبيقات النظام وتستخدم طرقًا ربما تكون مشكوكًا فيها بعض الشيء. ولكن كل شيء مباح في الحرب والاستغلالات، لذا بدأت في تحليل البرامج الضارة ومن هذا التحليل أنشأت PoC النهائي الخاص بي الذي يستغل الثغرة في النهاية، وسأشرحه أدناه.
أولاً، أود أن أوضح أن البرنامج الضار لا يستغل فقط ثغرة CVE-2024-30051 التي ترفع عمليتنا إلى مستوى نظام التكامل (Integrity System Level)، بل يقوم أيضًا بجزء ثانٍ ينتهي منه برفع مستخدم SYSTEM بكل الصلاحيات، وهو ما يتجاوز ما تشرحه CVE بالفعل.
بالإضافة إلى ذلك، من المهم ملاحظة أن البرنامج الضار أكثر تعقيدًا بكثير من PoC الخاص بي الذي يحاول تقليل الكود. يقوم البرنامج الضار بالعديد من الفحوصات الإضافية لضمان الموثوقية، ولهذا يعمل من المحاولة الأولى. لقد تجاهلت كل هذه الفحوصات للتبسيط وكرست نفسي للاستغلال الخالص، حتى لو اضطررت لتشغيل PoC مرتين أو ثلاث مرات لتحقيق الاستغلال.
الرابط إلى PoC القابل للتنفيذ هو https://github.com/fortra/CVE-2024-30051
أولاً، يقوم PoC باستدعاء GetVersion للحصول على إصدار نظام التشغيل الذي يعمل عليه، ووفقًا لذلك يقوم بتهيئة مختلفة لبعض المتغيرات العامة. تم اختبار PoC الخاص بي على Windows 11 23H2 و Windows 11 22h2. الأنظمة الأخرى ضعيفة أيضًا وتمت إضافة القيم لاستغلالها.
يقوم بحوكة أربع دوال نظام وبدون حوكتها لا يمكن تحقيق الاستغلال. هذه الدوال هي: RtlAllocateHeap، RtlCreateHeap، NtDCompositionCreateChannel و NtDCompositionCommitChannel.

في هذه الدوال، سيقوم بتصحيح أول 5 بايتات لجعلها تقفز إلى الكود الخاص به. بالطبع، لا يمكن أن يكون الكود بعيدًا جدًا لأن قفزة من 5 بايتات لا تغطي كل الذاكرة ويجب أن تكون قريبة.
لتحقيق ذلك، يستخدم البرنامج الضار كودًا طويلًا جدًا، يحلل خريطة الذاكرة ليقرر أين يمكنه تخصيص الكود الخاص به. نظرًا لأن الكود معقد، ركزت على جعله سطرين بسيطين:
base_ntdll = GetModuleHandleW(L"ntdll.dll");
global4_ = (char *)VirtualAlloc((LPVOID)(base_ntdll-0x2000), 0x1000uLL, 0x3000u, 0x40u);
قمت بطرح 0x2000 من قاعدة ntdll ومررت هذا العنوان إلى VirtualAlloc للتخصيص هناك.
يتم تعيين مكتبات DLL ذات 64 بت بشكل منفصل تمامًا في الذاكرة عن بعضها البعض مع مسافات فارغة بينها.
دعنا نرى كيف تعمل الحوكات:

تقوم باستدعاء دالة hooking، وهي المسؤولة عن حوكة واجهة برمجة التطبيقات RtlAllocateHeap، والتي تحتوي على ثلاث وسائط، الأولى هي عنوان واجهة برمجة التطبيقات المراد تصحيحها، وتسمى sym_RtlAllocateHeap.
قبل التصحيح، يشير إلى بداية واجهة برمجة التطبيقات:

هنا دالة RtlAllocateHeap:

الوسيطة الثانية هي الروتين المسمى hook الذي سيتم تنفيذه عند اكتمال تصحيح واجهة برمجة التطبيقات:


دالة hook تستدعي my_RtlAllocateHeap.
ستقوم دالة hooking بتصحيح أول 5 بايتات من واجهة برمجة التطبيقات بحيث تقفز إلى hook.
ستستدعي الكود في المنطقة المخصصة حيث ستنفذ أول تعليمة واجهة برمجة التطبيقات التي تم تجاوزها بالـ 5 بايتات ثم تقفز إلى RtlAllocateHeap+5 بعد البايتات المصححة مباشرة:

هذا هو شكل واجهة برمجة التطبيقات بعد الحوكة. تم تغيير أول 5 بايتات لتقفز إلى hook. سوف تستدعي my_RtlAllocateHeap الكود الموجود أعلى والذي سيعود إلى المنطقة المحددة باللون الأرجواني لمواصلة تنفيذ واجهة برمجة التطبيقات:

عندما تنتهي واجهة برمجة التطبيقات من التنفيذ، ستعود إلى hook. من هناك ستقارن المتغير العام heap_base (الذي يساوي صفرًا في البداية) مع الوسيطة الأولى التي تم تمريرها إلى RtlAllocateHeap:

بعد ذلك، ينتظر الكود تخصيصًا خاصًا معينًا، له HeapHandle محدد. في البداية يكون هذا المتغير صفرًا وطالما هو صفر، سيتخطى ويعمل مثل RtlAllocateheap عادي:

يتم الحصول على المعامل HeapHandle داخل RtlCreateHeap والتي بالصدفة هي واجهة برمجة التطبيقات الثانية التي تم حوكتها.
بالنظر إلى مراجع المتغير العام heap_base، فإنه يغير قيمته فقط في دالة hook2، وهي التي يتم تنفيذها بعد حوكة RtlCreateHeap:


إذًا، الفكرة هي التقاط HeapHandle معين وحفظه في heap_base. نظرًا لأنه الآن غير صفري، ستبدأ دالة hook في مقارنة كل تخصيص. لذا، سيقوم PoC بحفظ عنوان الذاكرة الذي له نفس HeapHandle مثل المخزون سابقًا.
عندما يكون هذا هو الحال، سيقوم بحفظ اتجاه التخصيص إلى المتغير المسمى base:

هاتان الحوبتان الأوليتان أصبحتا الآن متسلسلتين. عندما تقوم hook2 بحفظ القيمة المتوقعة لـ HeapHandle، فإنها تُفعِّل دالة hook التي ستحفظ عنوان التخصيص الذي يستخدم نفس HeapHandle.
الحوكة الثالثة موجهة إلى NtDCompositionCreateChannel. في المرة الأولى التي يتم استدعاؤها، ستحفظ MappedAddress، وهو محتوى الوسيطة الثالثة. من هناك ستغير hooked_flag إلى 1 بحيث لا تحفظ بعد ذلك وستعمل بشكل طبيعي.


العنوان المحفوظ في المتغير base سيُقرأ لاحقًا ثلاث مرات. اثنتان منهما ستحدثان في الحوكة الأخيرة، المسماة hook4:

سيتم تحليل دالة hook4 لـ NtDCompositionCommitChannel لاحقًا لأنها معقدة جدًا ومهمة جدًا.
بعد اكتمال الحوكات الأربعة، يعود إلى الدالة الرئيسية لبدء إنشاء نافذة. يتم ذلك عن طريق استدعاء RegisterClassExW. ومع ذلك، لتسجيل فئة نافذة للاستخدام لاحقًا، يجب استدعاء دالة CreateWindowExW.

هذا يهيئ مكتبة COM عن طريق استدعاء CoInitializeEx لاستخدامها بواسطة الخيط المستدعي:

يقوم بحساب الحجم المطلوب لمستطيل النافذة، بناءً على الحجم المطلوب:

يتم استدعاء الدالة CreateWindowExW لإنشاء نافذة سيتم رسمها:

من هناك، يستدعي D3D11CreateDevice لإنشاء جهاز أو جهاز DirectX يمثل محول العرض:


في PoC الخاص بي، ppDevice يُسمى d3dDevice و ppInmediateContext يُسمى d3dContext:

يجب تعيين الوسيطة flags إلى 0x20:

ثم يستدعي AddRef:

هذا يزيد عداد المرجع لواجهة مؤشر لكائن COM:


يتم طرح القيمة 0x10 من THIS:


في الإزاحة 0xf8 من ID3D11Device-0x10 يوجد مؤشر إلى TComObject:



سيكون هذا هو THIS الجديد وينتهي بالقفز إلى TComObject::AddRef:

وينتهي بإضافة واحد إلى عداد الكائن الموجود في الإزاحة 8 من TComObject:

ثم، AddRef سيزيد عداد نوع الكائن الآخر الذي تم إنشاؤه في D3D11CreateDevice، وهو نوع ID3D11DeviceContext:

في هذه الحالة، لإيجاد THIS الجديد، يطرح 0x108:


يقفز إلى هنا حيث في الإزاحة 0x98 يوجد THIS الجديد:

هذا هو العداد. في هذا المثال، هو QWORD:

يقوم PoC باستدعاء D2D1CreateFactory لاستخدام Direct2D، ولإنشاء واجهة ID2D1Factory المستخدمة لإنشاء موارد Direct2D الأخرى التي يمكن استخدامها للرسم أو وصف الأشكال:

الوسيطة riid هي تلك المقترحة من صفحة Microsoft:
https://learn.microsoft.com/en-us/windows/win32/api/d2d1/nf-d2d1-d2d1createfactory

هذه هي التي يستخدمها البرنامج الضار:

يمكن العثور على القيمة الصحيحة لـ ID2D1Factory هنا:
https://github.com/apitrace/dxsdk/blob/master/Include/d2d1_1.h

نظرًا لأنني لست خبيرًا في Direct Composition، فقد استخدمت نفس الخطوات التي استخدمها البرنامج الضار:
المصنع الجديد الذي يعيده لا يقدم أي نوع مفصل. يذكر void *، مما يعني أنه غير موثق رسميًا:

نظرًا لأنني لا أعرف نوع كائن كما في هذه الحالة، قمت بتطوير ملف قابل للتنفيذ يستخدمه لرؤيته في الذاكرة بسهولة:

أضف نقاط توقف في دوال hook الأربعة. في هذه الحالة، نقطة توقف في hook2 ستظهر متى التقطت HeapHandle:

يجب إيقاف hook عند التقاط القطعة (chunk) المطلوبة:

ضع نقاط توقف في hookين الآخرين:

ثم يستمر في استدعاء QueryInterface:

https://help.solidworks.com/2020/english/api/sldworksapi/queryinterface_example_cplusplus_com.htm
https://github.com/tpn/winsdk-10/blob/master/Include/10.0.16299.0/shared/dxgi.idl

يحاول تنفيذ نوع من الصب الديناميكي. إذا كان الكائن من نوع ID3D11Device يمكنه قبول الواجهة (استخدام الطرق، إلخ) لـ IDXGIDevice، فإنه ينشئ نسخة من الكائن الأصلي تقبل النوع الجديد، وبعد ذلك يعيد المؤشر إليه. في هذه الحالة، سيكون المتغير d3dContext1 من نوع IDXGIDevice:

كلا الكائنين يرثان من CLayeredObject<Cdevice>
الكائن الأصلي ID3D11Device هو:

مثل الذي يعيد المؤشر.

ثم ينشئ كائن ID2D1Device باستخدام الدالة CreateDevice:

في value2 يعيد كائنًا من نوع ID2D1Device.
https://learn.microsoft.com/en-us/windows/win32/api/d2d1_1/nf-d2d1_1-id2d1device-createdevicecontext

هنا يتم تنفيذ ذلك في الإثبات:


ثم يتم استدعاء DCompositionCreateDevice
https://learn.microsoft.com/en-us/windows/win32/api/dcomp/nf-dcomp-dcompositioncreatedevice


ينتمي IID إلى _IDCompositionDevice


في نفس اللحظة التي يتم فيها تتبع الدالة DCompositionCreateDevice، تتوقف عند hook3، عندما تستدعي NtDCompositionCreateChannel:

بهذه الطريقة يتم التقاط MappedAddress الذي يستخدمه النظام داخليًا عند استدعاء DCompositionCreateDevice:
هذا هو مكدس الاستدعاء حتى هذه النقطة:

هذه هي النقطة التي تستدعي فيها وحدة dcomp الدالة NtDCompositionCreateChannel:

بعد العودة من الخطوة السابقة، يتم حفظ MappedAddress. باستخدام ALPC، سيتصل بعملية DWM ثم يستدعي CreateTargetForHwnd

يستخدم مقبض HWND للنافذة التي تم إنشاؤها. يتعلق هذا بالجهاز الذي قمت بإنشائه للتو، وهو THIS لهذه الطريقة:

ثم استدعاء CreateSurface
https://learn.microsoft.com/en-us/windows/win32/api/dcomp/nf-dcomp-idcompositiondevice-createsurface

ثم استدعاء BeginDraw و EndDraw والوصول إلى CreateVisual.

يستدعي BeginDraw
https://learn.microsoft.com/en-us/windows/win32/api/dcomp/nf-dcomp-idcompositionsurface-begindraw
يستخدم هذا IID _IDXGISurface:


ثم يستخدم EndDraw:
https://learn.microsoft.com/en-us/windows/win32/api/dcomp/nf-dcomp-idcompositionsurface-enddraw


أخيرًا، يستدعي CreateVisual:
https://learn.microsoft.com/en-us/windows/win32/api/dcomp/nf-dcomp-idcompositiondevice-createvisual

بعد ذلك، يستدعي IDCompositionVisual::SetContent:
https://learn.microsoft.com/en-us/windows/win32/api/dcomp/nf-dcomp-idcompositionvisual-setcontent


ويستدعي SetRoot:

إن updateObject الذي يتم استلامه في BeginDraw لا يحدد نوعه في التوثيق.

بعد ذلك، يقوم بتحرير الكائنات التي تم إنشاؤها سابقًا:

والآن باستخدام نفس كائن dcompDevice من النوع IDCompositionDevice، يستدعي طريقة Commit:


استدعاء طريقة Commit تلك يتوقف عند hook2 الذي يلتقط HeapHandle المطلوب:

هذا هو مكدس الاستدعاء الآن:

قبل العودة إلى الوظيفة الرئيسية، يقوم أيضًا بإنشاء كتلة (chunk) باستخدام RtlAllocateHeap. يتم التقاطها وتخزينها في المتغير base داخل دالة hook:


يتم تنفيذ استدعاءات Create و Allocate واحدًا تلو الآخر:

كلاهما (Allocate و Create) يتم استدعاؤهما من DirectComposition::Cdevice::Commit:

بعد ذلك، عند استدعاء NtDCompositionCommitChannel، يتوقف عند hook4:

NtDCompositionCommitChannel يتم استدعاؤه من هنا:

يتم استدعاؤه أيضًا من DirectComposition::Cdevice::Commit

تجدر الإشارة إلى أن النظام قد قام بالفعل بتجميع الأوامر (batch) لإرسالها عبر ALPC إلى DWM. بعد ذلك، يقوم بإرسال الأوامر باستخدام NtDCompositionCommitChannel.
تتداخل دالة hook4 مع استدعاءات NtDCompositionCommitChannel وفي هذه المرحلة ستتم إضافة المزيد من الأوامر إلى الدفعة (batch).
دعنا نرى ما تفعله hook4:
يتم تنفيذ حلقة خلال الكتلة المشار إليها بواسطة base.
تخرج الحلقة عندما تجد القيمة 0x120 داخل الكتلة:

تقوم بتخزين العنوان والإزاحة حيث تم تحديد موقع القيمة 0x120:

تقوم باستبدال القيمة 0x120 بقيمة value4، والتي تساوي 0x1b0 + **0x8f = 0x23f. هذا هو الحجم الذي سيتم استخدامه في memcpy عند حدوث تجاوز السعة (overflow):



تضيف 0xbc + 0x90 إلى مؤشر العنوان حيث كان موقع 0x120:


تذكر أنه عند الإزاحة 0x48 من base كان الحجم 0x120. تم استبداله بـ 0x23f، لذلك يجب أن يكون حجم الكتلة الأصلية 0x120:
المصدر هو عنوان المؤشر لـ 0x23f + 0x2c:

في البداية أضافت 0x90 لكنها الآن تطرح 0x90 مرة أخرى.
الوجهة ستكون عنوان المؤشر إلى 0x120 + 0xbc:

ستكتب على هذا:

جميع الكتابات ستكون داخل الكتلة:

ستكرر الحلقة 3 مرات، وهو نتيجة القسمة الكاملة لـ 0x1b0/0x90:

بعد ذلك، نظرًا لأن قناة ArgChannelHandle هي نفس القناة التي تم استخدامها عند التقاط MappedAddress، فإن الإثبات سيضيف أوامر إلى الدفعة (batch) باستخدام NtDCompositionProcessChannelBatchBuffer. سيتم معالجتها مع تلك التي أضافها النظام بالفعل. تقوم الدفعة بجمعها ثم يتم إرسال الأوامر جميعًا معًا باستخدام NtDCompositionCommitChannel:


الأمر المرسل له القيمة 8، والتي تتوافق مع SetResourceIntegerProperty لأربعة متتبعات مختلفة (1، 2، 3، و 4).
عند عودة الإثبات إلى الوظيفة الرئيسية، يقوم بإنشاء قناة مختلفة لتنفيذ HeapSpray.
يقوم بتجميع 0x10000 أمر، والتي يتم إرسالها مع _NtDCompositionCommitChannel:

يستخدم هذا القيمة CreateResource=1 والنوع الذي يتوافق مع CHolographicInteropTextureMarshaler = 0x50:

يتم تنفيذ التخصيصات في الكود أدناه. حجم الكائنات التي تم إنشاؤها لعمل الرش هو 0x1b0:

ثم يقوم بتنفيذ حلقة لتحرير الكائنات التي تم إنشاؤها في الخطوة السابقة ويصنع الآن فجوات في توزيع الذاكرة.
يبدأ المتغير counter2 عند 0x3000 ويضيف خطوات بمقدار 0x20 طالما كان أقل من 0x7000:

يكتب قيم 0x41 من اتجاه الكتلة التي كانت في base + 0x48 + 44 + 0x1b0
أي أنه يكتب قيمًا سيتم استخدامها لاحقًا - عند حدوث تجاوز السعة للكتلة المجاورة:
يقع pvalue7 على العنوان 0x224 من base:

ثم يذهب إلى الدالة "escribe":

يكتب pKernelCallbacktable زائد 0x388، وعنوان LoadLibraryA ومسار الـ DLL الذي سيتم تحميله. في هذه الحالة، أسميته s11.dll.

الآن، هناك حاجة إلى مصحح أخطاء Kernel للتوقف عند الدالة الضعيفة عند حدوث تجاوز سعة الكومة.这是因为 عملية DWM لا يمكن تصحيحها باستخدام مصحح أخطاء وضع المستخدم (user mode).
باستخدام IDA PRO لتصحيح الأخطاء عن بُعد للهدف، قم بتعيين نقطة توقف شرطية بحيث تتوقف عندما يكون الحجم مساويًا لـ 0x1b0:
print ("VALUE1 %x" % ((cpu.rax)))
return cpu.rax==0x1b0.
نظرًا لأنه يتم تصحيح برنامج وضع المستخدم من kernel، يلزم التبديل إلى سياق عملية DWM لوضع نقطة التوقف. أعد تحميل رموز المستخدم باستخدام:
. reload /user
أعد تحميل رموز kernel باستخدام:
. reload /f

سيتوقف عند تجاوز ShowWindow:

يقوم بالتخصيص بحجم 0x1b0 والنسخ بحجم 0x23f، مما ينتج عنه تجاوز سعة الكومة:

في هذه المرحلة، يبدو مكدس الاستدعاء كما يلي:
لإنشاء التجاوز، يتلقى DWM القيم في الكود أدناه:
يتم قراءة القيم المصنوعة في base المرسلة من الإثبات الخاص بي باستخدام MapViewofFile من عملية DWM في الوحدة dwmcore.dll:

يتم استدعاء الدالة السابقة من:


عندما يتم إرسالها باستخدام ALPC من hook4 باستخدام destination_copy (NtDCompositionCommitChannel)، تتوقف:

تذكر أنه في أوامر hook4، تمت إضافة أوامر إلى الدفعة. ومع ذلك، كان النظام قد أضاف بالفعل بعض الأوامر إلى الدفعة، بما في ذلك base والبيانات المصنوعة:



في هذه الحالة، يشارك مساحة ذاكرة تبدأ عند 000001cd'178d0000. عند استخدامها كمصدر لتنفيذ memcpy، ستكون 0x794 بايت لاحقًا في نفس مساحة الذاكرة.
حجم مساحة الذاكرة المشتركة هو 0x4000:

ستتوقف عندما يكون الحجم المراد تخصيصه هو 0x1b0، وتصل إلى memcpy لنسخ 0x23f بايت:

ما وراء 0x1b0 في الذاكرة هو الكود الذي سيتجاوز الكتابة فوق الكتلة المجاورة:
عند تحرير الكتل من الإثبات، ينتهي الأمر بالقفز إلى LoadLibraryA، الذي يقوم بتحميل المكتبة المصنوعة:

يأتي ذلك من هنا:


تم إجراء الرش على الكومة باستخدام كائنات بحجم 0x1b0 من النوع CHolographicInteropTexture.
نظرًا لأنني صنعت فجوات في توزيع الذاكرة، فإن ذلك يحرر بعض الكائنات. نظرًا لأن الكتلة التي ستفيض لها حجم 0x1b0 أيضًا، فإن لديها احتمالية عالية لتكون موجودة في الفجوات في رش الكومة.
في وجهة memcpy، توجد الكتل كل 0x1b0 بايت:

يتم استبدال المؤشر إلى vftable بالمؤشر إلى LoadLibrary:
قبل الكتابة فوق:

بعد الكتابة فوق:

تذكر أنها انتهت بالقفز إلى [R11+50]، وهو المؤشر إلى LoadLibraryA.
بتشغيل الإثبات، انسخ الـ DLL في نفس المسار المذكور في الإثبات:
بعد تشغيل الإثبات، يتم تنفيذ عملية CMD مع امتيازات مستوى تكامل النظام لمستخدم DWM:

المراجع:
الإثبات على GitHub الخاص بـ Fortra: https://github.com/fortra/CVE-2024-30051
https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2024-30051
https://msrc.microsoft.com/update-guide/en-US/advisory/CVE-2024-30051
هذا يكمل الإثبات. تذكر أنه إذا قمت بتنفيذه عدة مرات، ستبقى الكومة في حالة غير مستقرة، لذلك قد تحتاج إلى إعادة تشغيل الجهاز لجعله يعمل مرة أخرى. أيضًا، على الرغم من أنه قد لا يعمل دائمًا من المحاولة الأولى، إلا أنه سيعمل بشكل صحيح عادةً خلال المحاولة الثانية أو الثالثة. كما ترى، يمكن أن تكون الهندسة العكسية صعبة، لذا إذا كان لديك أي أسئلة، يمكنك استشارتي.
البريد الإلكتروني: [email protected]
X: @ricnar456