
كتابة تقنية وإثبات مفهوم (PoC) لثغرة CVE-2024-6769، تجمع بين اختطاف DLL وتسميم ذاكرة التخزين المؤقت للتنشيط لتصعيد الصلاحيات من مستوى النزاهة المتوسط إلى العالي على أنظمة ويندوز.
يتعلق هذا المقال بثغريتين مترابطتين: المرحلة الأولى هي ثغرة اختطاف DLL ناتجة عن إعادة تعيين محرك الأقراص الجذر (ROOT drive)، والمرحلة الثانية هي ثغرة تسميم ذاكرة التخزين المؤقت للتفعيل (Activation Cache Poisoning) التي يديرها خادم CSRSS.
تم تقديم المرحلة الأولى بالتفصيل في مؤتمر Ekoparty 2023 في العرض التقديمي المسمى "I'm High" من قبل نيكولاس إيكونومو من BlueFrost Security. شرح كيفية استغلال الثغرة التي لم تكن قد صُححت بعد من قبل مايكروسوفت في ذلك الوقت. سمح ذلك لمستخدم بصلاحية سلامة متوسطة (MEDIUM INTEGRITY) بالارتقاء للحصول على صلاحيات سلامة عالية محدودة (limited HIGH PRIVILEGES)، ولكن دون الوصول الكامل ليكون مسؤولاً كاملاً.
لم تُعرض المرحلة الثانية في ذلك المؤتمر، على الرغم من اقتراح بعض الخطوات لبدء البحث فيها.
للبدء، سنستعرض المرحلة الأولى لتوفير سياق تمهيدي. من هناك، سنتعمق في بحثي حول المرحلة الثانية، ونتناول تفاصيل تحقيق الارتقاء الكامل من سلامة عالية محدودة (limited HIGH INTEGRITY) إلى مسؤول كامل (full Administrator). يتضمن ذلك برهان مفهوم (PoC) عملي كامل للمرحلتين لجميع إصدارات ويندوز، والذي تم اختباره بنجاح في ويندوز 10، ويندوز 11، ويندوز سيرفر 2022، ويندوز سيرفر 2019 مع جميع التحديثات المطبقة.

الشرط الوحيد لهذه المرحلة هو أن تبدأ العملية بمستوى سلامة متوسطة (MEDIUM INTEGRITY LEVEL) وأن ينتمي المستخدم إلى مجموعة المسؤولين.
يمكن تلخيص المرحلة الأولى من الاستغلال في الخطوات التالية:
مثال: إعادة تعيين القرص من
"C:\" إلى "C:\users\public"
سيؤدي هذا أيضاً إلى إعادة تعيين مجلد "system32" من
"C:\windows\system32" إلى "C:\users\public\windows\system32"
أحد البرامج المتأثرة هو CTFMON، والذي يعمل بمستوى سلامة عالية (HIGH INTEGRITY LEVEL) ولكن بدون صلاحيات المسؤول.
عادةً، يحاول تحميل الوحدة المسماة MsCtfMonitor.dll من مجلد system32 الحقيقي، ولكن نظراً لإعادة تعيين محرك الأقراص الجذر، فإنه يبحث عن MsCtfMonitor.dll في مجلد system32 المزيف الذي نتحكم به، حيث يمكننا إنشاء ووضع ملف DLL مصمم بنفس الاسم.
في هذه المرحلة، من خلال وضع نسختنا من MsCtfMonitor.dll في مجلد system32 المزيف، يتم استدعاء دالة DoMsCtfMonitor الخاصة بها وتنفيذ الكود الخاص بنا بمستوى سلامة عالية.




في الوقت نفسه، يمكننا التأكد من أن العملية، على الرغم من كونها بمستوى سلامة عالية، لا تملك صلاحيات المسؤول:


في عرضه التقديمي في Ekoparty، اقترح نيكولاس الخطوات التالية لإكمال الاستغلال:


بينما يبدو هذا بسيطاً، إلا أنه يتطلب الكثير من الوقت للهندسة العكسية والتصحيح.
عند التعمق قليلاً في قصة ناقل الهجوم هذا، أصبح من الواضح أن تسميم ذاكرة التخزين المؤقت لسياق التفعيل (Activation Context Cache) قد استُخدم في بعض الاستغلالات. وبالتالي، من المفيد تعلم كيفية الاستغلال سابقاً لتوفير سياق ورؤى إضافية. التفاصيل حول هذا الاستغلال متاحة من خلال مقال Zero Day Initiative، تسميم ذاكرة التخزين المؤقت لسياق التفعيل: استغلال CSRSS للارتقاء بالصلاحيات.
يحدث استخدام ذاكرة التخزين المؤقت للتفعيل عندما يكون البرنامج على وشك تحميل مكتبة تتطلب إصداراً محدداً.
على سبيل المثال، إذا كان تطبيق سيحمل C:\Windows\System32\comctl32.dll، فلا يوجد ضمان بأن comctl32.dll في ذلك الموقع هو الإصدار الذي يحتاجه التطبيق. هذا هو استخدام أساسي لذاكرة التخزين المؤقت لسياقات التفعيل (Activation Contexts Cache). يمكن للبرنامج إرسال طلب إلى خادم CSRSS لمعالجة إدخال سياق تفعيل جديد ليتم إدخاله في الذاكرة المؤقتة، بحيث يمكن لهذا البرنامج تحميل إصدار المكتبة المحددة التي يحتاجها.
لهذا الغرض، يتم استخدام ما يسمى بملف البيان (manifest)، وهو بتنسيق XML. عادةً ما يكون مضمنًا كمورد في ملف EXE أو DLL. بدلاً من ذلك، سيبحث ويندوز عن ملف بيان في نفس المجلد الذي يوجد فيه الملف التنفيذي للبرنامج.
يحتوي الرابط المذكور أعلاه على بعض الأمثلة لملفات البيان المستخدمة في الاستغلالات القديمة، مثل خداع النظام لتحميل مكتبة advapi32.dll من مجلد يتحكم به المهاجم تم الوصول إليه من خلال تقنية تجول المسار (PATH TRAVERSAL).

بالطبع، تم تصحيح بعض ناقلات الهجوم المستخدمة، وتم اكتشاف بعض التقنيات الجديدة. بالإضافة إلى ذلك، في تصحيح أكتوبر 2022 لويندوز 11 22H2، تمت إضافة فحص جديد.
بعد تطبيق هذا التصحيح، لا يمكن تجاوز الفحص عند تسجيل سياق التفعيل (ACTX) إلا إذا كانت العملية التي تضيف الإدخال الجديد في الذاكرة المؤقتة لها نفس RID أو أعلى من العملية التي ستستخدمه.
في winnt.h يمكننا رؤية قيم RID:

الاقتراح لتجاوز هذا الفحص هو إنشاء طلب باستخدام سياق تفعيل (Activation Context) من عملية CTFMON حيث يعمل ملف DLL المصمم. هذا الملف DLL المصمم له RID=0x3000 وبعد إضافة الإدخال إلى الذاكرة المؤقتة، سيقوم TCMSETUP الذي له RID=0x3000 بتحميل tapi32.dll.
أثناء محاولتي اتباع الخطوات، قمت بعمل جميع التركيبات الممكنة لتسجيل ACTX باستخدام CreateActCtx. أثبت ذلك أنه مستحيل نظراً لوجود فحص دائم يتجنبه.
من المهم ملاحظة أن هذه الدالة موجودة في مساحة المستخدم، ويتم تصديرها بواسطة kernel32.dll. يمكن تجنب الفحوصات عن طريق تصحيح ملف DLL في الذاكرة، وهو أمر غير أنيق، لكنه ممكن ويجب أن يعمل.

تشير شريحة العرض التقديمي من نيكولاس إلى استخدام مستوى منخفض (LOW LEVEL). ومع ذلك، مع ملاحظة الوجه المبتسم، كان من الواضح أن استخدام CreateActCtx ليس الخيار الأفضل عند استغلال هذه الثغرة بدون تصحيح.

استدعاء الإجراء المحلي المتقدم (ALPC) هو آلية اتصال بين العمليات تُستخدم لإرسال الرسائل بسرعة عالية داخل نظام ويندوز. على عكس واجهة برمجة تطبيقات ويندوز القياسية، فإن ALPC غير متاح مباشرة للتطبيقات. بدلاً من ذلك، هي آلية داخلية لا يمكن الوصول إليها إلا من خلال مكونات نظام ويندوز. (ونحن أيضاً.)
عند إجراء المزيد من البحث، لاحظت أن بعض استغلالات تسميم الذاكرة المؤقتة القديمة استخدمت ALPC للتواصل مباشرة مع الخادم. يمكن رؤية مثال في مقال فيليب تسوكرمان، سياقات التفعيل — قصة حب.
تقوم دالة CsrClientCallServer بتطبيق واجهة ALPC بين عمليات Win32 وعملية CSRSS.
لذا، يجب محاولة إجراء استدعاء لعملية CSRSS التي تعمل كخادم باستخدام CsrClientCallServer.
أثناء البحث عن أمثلة في الاستغلالات القديمة، وجدت صفحة على Packet Storm حول مشكلة تجاوز سعة المخزن المؤقت في الكومة ذات الصلة.
عند استدعاء خادم CSRSS بالحزمة الصحيحة، يتم استلامها في دالة BaseSrvSxsCreateActivationContextFromMessage، التي تنتمي إلى الوحدة sxssrv.dll.
تحتوي الدالة على وسيطة واحدة فقط: المؤشر إلى الحزمة المستلمة. لعكس هندستها، قمت بإنشاء هيكل TotalMessage مخصص.
يحتوي هيكل الحزمة TotalMessage على أول 0x40 بايت من الرأس (HEADER)، متبوعة بـ رسالة سياق التفعيل المضمنة (Activation Context Message)، التي هيكلها هو _BASE_SXS_CREATE_ACTIVATION_CONTEXT_MSG.
يمكن رؤية هيكل TotalMessage أدناه:

وهذا هو هيكل _BASE_SXS_CREATE_ACTIVATION_CONTEXT_MSG:

داخل هذا الهيكل توجد ستة سلاسل UNICODE (UNICODE_STRINGS) تتوافق مع اللغة أو CultureFallbacks، AssemblyDirectory، TextualAssemblyIdentity، AssemblyName، وهيكلين _BASE_MSG_SXS_STREAM يحتوي كل منهما على سلسلة UNICODE واحدة بالداخل.
أدناه هو هيكل _BASE_MSG_SXS_STREAM:

نظراً لصعوبة إنشاء حزمة صالحة يقبلها الخادم، يجدر تفصيل كيفية القيام بذلك.
قيمة حقل Flags داخل _BASE_SXS_CREATE_ACTIVATION_CONTEXT_MSG مهمة جداً نظراً لوجود العديد من التركيبات. بدون قيمة العلم الصحيحة، لا يمكن استغلال الثغرة.
على سبيل المثال، خذ كود MsCtfMonitor.dll الخاص بي. بعد العديد من المحاولات، استنتجت أن قيمة flags الصحيحة الوحيدة لاستغلال هذه الثغرة هي 0x41:

قد يؤدي الجمع بين القيم المختلفة إلى قيمة علم مسار خاطئة:

سيكون لهيكل TotalMessage نفسه رأس بحجم = 0x40 بايت. الـ 0x1f8 بايت المتبقية محجوزة لهيكل _BASE_SXS_CREATE_ACTIVATION_CONTEXT_MSG:
struct TotalMessage
{
signed __int64 pad[8];
_BASE_SXS_CREATE_ACTIVATION_CONTEXT_MSG message;
};
الحجم للتخصيص هو 0x40+0x1f8:

ثم قمت بتجميع السلاسل وقمت بتنفيذ سياق ذاكرة تخزين مؤقت للتفعيل لـ tapi32.dll. هذا DLL نادر الاستخدام يتم تحميله بواسطة عملية تسمى TCMSETUP. لها مستوى سلامة عالية الصلاحيات (HIGH PRIVILEGES INTEGRITY LEVEL) (RID=0x3000) مع نفس صلاحيات المسؤول.

في كود DLL الخاص بي، يتم استدعاء دالة CaptureUnicodestring. ينتهي هذا باستدعاء CsrCaptureMessageString:
NTSTATUS CaptureUnicodeString(LPVOID CaptureBuffer, PSTR OutputString,
PCWSTR String, ULONG Length = 0) {
if (Length == 0) {
Length = lstrlenW(String);
}
return CsrCaptureMessageString(CaptureBuffer, (PCSTR)String, Length * 2,
Length * 2 + 2, OutputString);
}
هذه الخطوة ضرورية لتحضير الحزمة بشكل صحيح، مما يسمح للنظام بنسخ السلاسل من حزمتي إلى عملية CSRSS. هذا يحافظ على صحة السلاسل ويستبدل مؤشراتي بمؤشرات صالحة في سياقها.
أضفت أيضاً ملف بيان XML مضمن، مع اللغة "Tasks" فيه. هذه لغة غير موجودة، لكنها ستكون مفتاح الاستغلال (Kudos لنيكو على هذا):

تفصيل مهم آخر في كودي هو عند إنشاء CaptureBuffer. الدالة CsrAllocateCaptureBuffer لها وسيطة تحدد عدد سلاسل UNICODE التي يجب أن تديرها وتنسخها إلى الخادم.
في حالتي استخدمت "4" سلاسل:

الوسيطة بالقيمة "4" موضحة أدناه:

للوصول إلى خادم التفعيل، سترسل دالة CsrClientCallServer حزمتي من MsCtfMonitor.dll الخاص بي بنفس ApiNumber 0x1001001E مثل الاستغلالات القديمة المذكورة أعلاه.
مدونة Geoff Chappell توفر المزيد من التفاصيل حول CsrClientCallServer:

هذا هو الاستدعاء لـ CsrClientCallServer:

وهذه هي الحزمة المراد إرسالها، التي تم بناؤها في DLL الخاص بي:

قيمة Manifest.Offset تشير إلى ملف البيان XML المضمن الخاص بي:

أمر مثير للاهتمام لتسجيل عملية التفعيل هو sxstrace، الذي يُستخدم في وحدة تحكم المسؤول داخل الهدف.
هذا الأمر يمكن التتبع وحفظ نتائج السجل في sxstrace.etl. (اضغط ENTER لإنهاء التتبع.)
sxstrace trace -logfile:sxstrace.etl
يمكن بعد ذلك تحويل ملف sxstrace.etl الخام إلى تنسيق قابل للقراءة:
sxstrace parse -logfile:sxstrace.etl -outfile:sxstrace.txt
إذا كانت الحزمة صحيحة، يجب أن تصل إلى دالة BaseSrvSxsCreateActivationContextFromMessage في وحدة sxssrv من عملية csrss. لذلك عند تصحيح النواة عن بعد، يجب تبديل السياق إلى هذه العملية. بعد ذلك، يجب إعادة تحميل رموز وضع المستخدم لوضع نقطة توقف عليها:

استخدمت IDA PRO لتصحيح النواة مع ملحق Windbg:

بمجرد أن تتوقف عند BaseSrvSxsCreateActivationContextFromMessage، سيشير RCX إلى هيكل TotalMessage:

بعد الـ 0x40 بايت الأولى من الرأس (HEADER) (يملؤها النظام ببعض القيم مثل PID لعملية العميل، إلخ...)، يمكن رؤية رسالة التفعيل الخاصة بي التي تنتمي إلى هيكل _BASE_SXS_CREATE_ACTIVATION_CONTEXT_MSG:

لاحظ أن المؤشرات إلى السلاسل ليس لها نفس القيمة عندما أرسلتها:

لكنها أشارت بشكل صحيح إلى السلاسل:

عندما تم إرسال الحزمة من العميل إلى الخادم، قام النظام بنسخ السلاسل من عمليتي إلى عملية CSRSS وغير المؤشرات في حزمتي لتكون صالحة في سياقها.
بعد ذلك، تتحقق الدالة BaseSrvSxsCreateActivationContextFromMessage من صحة السلاسل.

في حلقة، تتحقق من ست سلاسل، لكنها تجتاز الفحص تماماً. في حالتي، مررت أربع سلاسل فقط والاثنتان الأخريان صفر.
بعد فحوصات ثانوية أخرى، تستدعي BaseSrvSxsCreateActivationContextFromStructEx، وهي الدالة الأكثر أهمية في عملية التفعيل:

بمجرد الوصول إلى BaseSrvSxsCreateActivationContextFromStructEx، سيشير r8 إلى _BASE_SXS_CREATE_ACTIVATION_CONTEXT_MSG، وهي رسالة التفعيل:

تقيّم قيمة الأعلام. في حالتي، كانت القيمة 0x41 مقابل 0xD:

يمكن تجاوز دالة test باستخدام خيار العلم الذي يتوافق مع التحقق من بنية المعالج (1).

بعد ذلك، تحصل على RID لعملية الاستدعاء وتخزينه للمقارنة لاحقاً. في هذه الحالة، RID هو 0x3000 لأن CTFMON لديها مستوى سلامة عالية.

الجزء الأكثر أهمية في هذه الدالة هو الاستدعاء لـ BaseSrvActivationContextCacheLookupEntry:

تبحث في ذاكرة التخزين المؤقت لسياق التفعيل (Activation Context Cache) لتحديد ما إذا كان هناك أي إدخال لـ tapi32.dll.
تستدعي دالة اسمها BaseSrvActivationContextCacheCompareEntries، التي تقارن أجزاء معينة من إدخال رسالة التفعيل مقابل جميع الإدخالات الموجودة في الذاكرة المؤقتة:

تقارن قيمة LastWriteTime المرسلة في حزمتي مع نفس القيمة في جميع الإدخالات.
قمت بحساب هذه القيمة مسبقاً باستخدام GetFileTime في tapi32.dll وأرسلتها داخل حزمة التفعيل الخاصة بي:

نظراً لعدم وجود إدخال لـ tapi32.dll، لن تتطابق المقارنات. كما هو متوقع، ترجع خطأ 0xC0000225. بعد ذلك ستتحقق من ACTX الخاص بي لمعرفة ما إذا كان مناسباً للإضافة إلى الذاكرة المؤقتة:

يحتاج الخادم إلى قراءة ملف البيان XML المضمن الخاص بي، وعنوان Manifest.Offset الذي أشار إليه. ومع ذلك، في هذا السياق الجديد، ليس مؤشراً صالحاً بعد. يجدر وضع نقطة توقف على هذه القيمة لرؤية كيف ومتى يتم قراءة ملف البيان XML المضمن الخاص بي باستخدام هذه القيمة.
للتحقق من أين يقرأ CSRSS ملف البيان XML المضمن الخاص بي الذي تم إرساله في طلب ACTX الخاص بي، يجب وضع نقاط توقف في Manifest.Offset. بالإضافة إلى ذلك، يجب إضافة نقاط توقف في كل مرة يتوقف فيها، إذا نسخ إلى عنوان آخر.

تتوقف عند نقطة التوقف عند قراءة عنوان قيمة Manifest.Offset.
ستستخدم هذا العنوان لقراءة ملف البيان XML المضمن الخاص بي من عملية CTFMON باستخدام NtReadVirtualMemory، نظراً لأن العنوان الموجود في حقل Manifest.Offset ينتمي إلى ذلك السياق:

تتم قراءة ملف البيان XML المضمن الخاص بي ونسخه إلى المخزن المؤقت الوجهة:

قم بالتبديل إلى سياق عملية CTFMON وتحقق من أن ملف البيان XML المضمن الخاص بي موجود في عنوان Manifest.Offset الذي أرسلته سابقاً. في حالتي، كان 0x7ff93a261470.

يتم استدعاء ملف البيان XML المضمن المقروء من SxSGenerateActivationContext. نظراً لعدم العثور عليه في أي إدخال صالح في الذاكرة المؤقتة، يحاول "توليده" باستخدام ملف البيان المضمن:

من هناك، يبدأ في تحليل ملف البيان XML المضمن الخاص بي.
بالنظر إلى مكدس الاستدعاءات الأخير، قررت وضع نقطة توقف في الاستدعاء لـ RtlReadOutOfProcessMemoryStream للتوقف عندما يتم ملء المخزن المؤقت بالكامل.

الآن يمكن وضع نقطة توقف على الوصول إلى السلسلة "Tasks" للتوقف عندما تتم قراءتها أو معالجتها بواسطة الخادم.

هذه هي السلسلة tasks داخل ملف البيان XML المضمن:

تتوقف عدة مرات أثناء القراءة والنسخ:

تتوقف في CharEncoder::wideCharFromUtf8 عندما تقوم بتحويل السلسلة “tasks” إلى حرف عريض:

ثم تتوقف في محلل XML:

تستمر في تحليل سمات XML، كما يشير اسم الدالة parseAttributes.

ثم تتوقف في memcpy المستدعاة من ValidateElementAttributes:

يمكن وضع نقطة توقف أخرى حيث تنسخ:

تتحقق من سمة اللغة، كما يشير اسم الدالة SxspValidateLanguageAttribute:

تتوقف في memcpy مرة أخرى ولكن يتم استدعاؤها من SxspCreateAssemblyIdentityfromIdentityElement:

مرة أخرى، تتوقف في memcpy، هذه المرة تم استدعاؤها من SxsInsertAssemblyIdentityAttribute+0xc48:
ثم يتوقف عند SxsInsertAssemblyIdentityAttribute:

يستدعي memcpy مرة أخيرة، في هذه الحالة من BufferedStream::prepairForInput:

ثم يقرأ السلسلة النصية tasks هنا:

ثم يقرأها من هنا:


يستمر القراءة من هنا:


أسماء هذه الدوال لفتت انتباهي. في الاسم ProbingCandidate، يتضمن نفس الكلمات (probing manifests) المستخدمة في ملف سجل SXS النصي.

يتوقف مرة أخرى هنا:

بعد ذلك، يستخدم GetFileAttributesExW للتحقق مما إذا كان الملف الأول المذكور في ملف سجل SXS النصي موجودًا. وبما أنه غير موجود، فإنه يُرجع صفرًا.

يمكن رؤية ترتيب التحقق من الملف في ملف السجل:

الملف الثاني غير موجود لأنه المسار في مجلد tasks إلى tapi32.dll:

من هناك، يبدو أنه "probing" (بحث استقصائي) عن tapi32.manifest في tasks:

ثم يصل إلى CProbedAssemblyInformation::ProbeManifestExistence:


يتحقق مما إذا كان ملف البيان الخاص بي موجودًا في مجلد tasks. وبما أنه موجود، فإنه يُرجع بدون خطأ:

حسنًا، تم العثور على tapi32.manifest في مجلد "tasks".
تم إجبار الخادم على البحث عن ملف بيان في المجلد الفرعي "tasks" داخل system32 بواسطة البيان المضمن الخاص بي (embedded XML manifest) مع قيمة اللغة "tasks" داخله:


إذا تم وضع نقاط توقف (breakpoints) لمتابعة المكان الذي يستخدم فيه المسار، فإنه يتوقف عند EncodingStream::Read حيث تتم قراءة محتوى ملف tapi32.manifest.

بعد ذلك، سيقوم بتحليل محتوى ملف TAPI32.manifest. إذا حدث خطأ، فسيظهره في سجل SXS TRACE، مما يسهل تصحيحه.

إذا تم تحليل ملف TAPI32.manifest الخاص بي بشكل صحيح، فسيعود إلى BaseSrvSxsCreateActivationContextFromStructEx بدون خطأ. هذا يتجنب طباعة رسالة تحتوي على السلسلة النصية FAILED.
في حالتي، كان إنشاء سياق التنشيط (Activation Context Generation) ناجحًا، باستخدام ملف TAPI32.manifest الخاص بي.


ثم وصلت إلى الاستدعاء حيث سيتم إدخال الإدخال الخاص بي في الذاكرة المؤقتة (cache).
يتم تمريره دون أي مشكلة، مع إرجاع صفر. هذه هي القيمة الصحيحة ويتم إدراج الإدخال الذي يحتوي على TAPI32.manifest المصمم بنجاح.

يتم تضمين الإدخال الخاص بي في ذاكرة التخزين المؤقت للتنشيط (Activation cache) ويعيد الخادم استجابة OK إلى الاستدعاء من DLL من CTFMON.
يظهر ملف السجل النصي العملية الكاملة.
يقرأ البيان المضمن (embedded XML manifest). نظرًا لأن لغته هي "Tasks"، فإنه يبحث عن ملف بيان جديد في المجلد الفرعي "Tasks" داخل system32، تمامًا كما سيبحث عن بيان في المجلد الفرعي داخل system32 المسمى "en-us" إذا كانت اللغة مضبوطة على "en-us."

يظهر ملف سجل SXS النصي الرسالة "Activation Context generation succeeded"!

بعد إضافة إدخال ACTX الخاص بي إلى الذاكرة المؤقتة، إذا تم تشغيل tcmsetup.exe، فسيقوم بتحميل tapi32.dll، ويجب أن يستخدم ملف البيان الخاص بي لتحميل imm32.dll.
ومع ذلك، الأمر ليس بهذه البساطة، لأنه لا يمكن تحميل imm32.dll لأن هناك بعض الفحوصات التي يمكن أن تمنع تحميله.
يتم إجراء الفحوصات في استدعاء لاحق لنفس الدالة BaseSrvSxsCreateActivationContextFromStructEx، لذا قم بإزالة جميع نقاط التوقف واترك نقطة توقف واحدة عليها فقط.
من هناك يمكننا تشغيل TCMSETUP.EXE من وحدة التحكم، على الرغم من أن PoC الخاص بي ينفذ TCMSETUP من MsCtfMonitor.dll بعد الانتهاء من تسميم ذاكرة التخزين المؤقت للتنشيط (Activation Cache poisoning):

يتوقف عند نقطة التوقف عدة مرات. في كل مرة يتوقف، انظر إلى البنية المشار إليها بواسطة r8 لترى ما إذا كانت تتوافق مع طلب متعلق بـ tapi32.dll.

بعد عدة توقفات لوحدات أخرى، يظهر طلب لـ TCMSETUP.exe:

نرى في مكدس الاستدعاء أنه يأتي من لحظة إنشاء العملية. يتم استدعاؤه للتحقق مما إذا كان لديه أي إدخال في ذاكرة التخزين المؤقت للتنشيط لـ TCMSETUP.
استمر في تشغيله حتى يصل الاستدعاء لـ TAPI32.dll. قبل حدوث ذلك، سيكون هناك العديد من الاستدعاءات لـ TCMSETUP.

أخيرًا، يجب أن تكون الحزمة التي تصل مشابهة جدًا لتلك التي تم إجراؤها سابقًا من DLL الخاص بي عندما قمت بإدراج الإدخال في الذاكرة المؤقتة. ومع ذلك، فإنه يتوقف الآن عندما يحاول TCMSETUP تحميل TAPI32.dll.

في هذه المرحلة، لاحظت بعض القيم المهمة في هذه الحزمة.

بالتمديد من بداية الهيكل _BASE_SXS_CREATE_ACTIVATION_CONTEXT_MSG بمقدار 0x40 بايت للأعلى، قم بتعيين هيكل TotalMessage. معرّف العملية (PID) التي تقوم بالطلب لـ TAPI32.dll هو TCMSETUP لأنه يريد تحميل DLL.

بتغيير السياق إلى عملية TCMSETUP، يمكن رؤية قيمة Manifest.Offset مشيرة إلى بيان مضمن (embedded XML manifest).


افتح tapi32.dll في NOTEPAD لترى أن البيان المضمن المستلم (XML) هو نفس الموجود في الملف.
يقرأ TCMSETUP مورد الملف (resource) سابقًا لقراءة البيان ووضعه في الحزمة كـ البيان المضمن (embedded XML manifest).
بعد ذلك، يتم إجراء المقارنة مرة أخرى بواسطة الدالة BaseSrvActivationContextCacheCompareEntries، والتي يتم استدعاؤها من BaseSrvSxsCreateActivationContextFromStructEx. الآن أصبح إدخالي لـ tapi32.dll موجودًا أيضًا في الذاكرة المؤقتة.

يتم استدعاء BaseSrvActivationContextCacheCompareEntries داخل حلقة لمقارنة الطلب الفعلي مع كل إدخال في ذاكرة التخزين المؤقت لسياق التنشيط (Activation Context Cache) (مثل إدخالي).
في البداية، يقارن كلا قيمتي LastWriteTime، وبما أنهما متساويتان، يستمر في مقارنة المزيد من القيم.
قيمة LastWriteTime هذه حاسمة. إذا كانت هناك قيم مختلفة، فسيتجاهل الإدخال المخزن مؤقتًا الخاص بي ولن يتم تحميل imm32.dll الخاص بي.
يمضي قدمًا ويتوقف عند الفحص التالي.

الآن، يتحقق من قيمة ResourceName والتي يجب أن تكون 0x7c في كليهما.

ثم يقارن لغة حزمة ACTX الفعلية، وهي "en-us"، مع لغة الإدخال المخزن مؤقتًا الخاص بي. لغة الإدخال المخزن مؤقتًا الخاص بي هي أيضًا "en-us".

الحزمة الخاصة بي لها نفس قيمة اللغة:

ثم، يقارن بنية المعالج (processor architecture)، والتي في هذه الحالة ستكون 9 في كلتا الحالتين:


ثم، يقارن كلا Manifest.path.

قمت ببناء نفس المسار بدون ترميز ثابت (hardcoding) باستخدام قيمة دليل النظام (System Directory):

ثم يقارن AssemblyDirectory، وهو أيضًا نفسه:


إذا كانت جميع المقارنات صحيحة، فإنها تُرجع صفرًا. هذا يعني أنه تم العثور على إدخالي في ذاكرة التخزين المؤقت للتنشيط (activation cache) وسيتم استخدامه.
تذكر أنه عندما أرسلت طلبي لأول مرة لإضافة الإدخال، أعادت المقارنة خطأً لأنه لم يكن هناك إدخال في الذاكرة المؤقتة لـ TAPI32.dll. نظرًا لأنه تمت إضافة إدخالي مسبقًا، فإنه الآن يُرجع صفرًا.
بعد ذلك، يقارن معرفات RID الخاصة بـ TCMSETUP مع CTFMON، وبما أن كلاهما لهما RID = 0x3000، تستمر العملية.
شرح كامل لتصحيح RID متاح في مدونة من Zero Day Initiative.

هذا هو كود هذا التصحيح:

R15 يحتوي على RID للمتصل TCMSETUP = 0x3000 و buffer يخزن RID=0x3000 لعملية CTFMON.
كما ذكر سابقًا، أضافت Microsoft تصحيح RID check هذا في أكتوبر 2022.
بعد تطبيق هذا التصحيح، إذا حاولت إضافة إدخال tapi32.dll إلى الذاكرة المؤقتة باستخدام نفس MsCtfMonitor.dll من عملية بمستوى تكامل متوسط (MEDIUM INTEGRITY LEVEL PROCESS) (0x2000)، سيتم إضافة الإدخال إلى الذاكرة المؤقتة، لكنه سيفشل. وذلك لأن RID لعملية المتصل 0x2000 يتم تخزينه، وعندما تحاول تشغيل TCMSETUP بـ RID=0x3000 لتحميل imm32، يتم مقارنة RID وإزالة الإدخال.
في تلك الحالة الافتراضية، سيكون R15 يحتوي على RID=0x3000 لعملية TCMSETUP التي طلبت تحميل tapi32.dll، وسيكون المتغير "buffer" قد خزّن RID=0x2000 للعملية التي أضافت الإدخال إلى الذاكرة المؤقتة والتي لديها مستوى تكامل متوسط (MEDIUM INTEGRITY LEVEL).

في أحدث إصدارات Windows، لن يعمل تسميم الذاكرة المؤقتة (cache poisoning) إذا كانت العملية التي تطلب إضافة الإدخال أقل من المنفذ (executor) وتم إزالة الإدخال. الإصدارات السابقة التي تم إصدارها قبل هذا التصحيح ستعمل دون مشاكل مع أي RID.

بالعودة إلى هذه الحالة، تم تجاوز فحص RID وكلا العمليتين لهما نفس RID=0x3000. وبالتالي، لا يتم حذف الإدخال وتستمر العملية دون أي أخطاء.
يعيد الخادم الاستجابة إلى TCMSETUP. عند تحميل tapi32.dll، سيستخدم إدخالي مع ملف tapi32.manifest، والذي سيقوم بتحميل imm32.dll من مجلد tasks.
هذه هي الطريقة الكاملة من LoadLibrary إلى حيث يقوم TCMSETUP بتقديم الطلب إلى ذاكرة التخزين المؤقت للتنشيط
عند تحميل tapi32.dll.

BasepCreateActCtx هو الذي يقدم طلبه إلى خادم CSRSS. يجب محاولة رؤية متى ينتهي الأمر بتحميل وحدة IMM32.dll.
بالنظر إلى kernel32.dll، فإنه يستدعي CsrBasepCreateActCtxCommon. في الداخل، هناك استدعاء خادم مشابه لذلك الذي تم إجراؤه من DLL الخاص بي لإدراج إدخال الذاكرة المؤقتة الخاص بي.

يستخدم نفس ApiNumber الخاص بي.
عند تنفيذ TCMSETUP، يمكن وضع نقطة توقف هناك عند عودته من الخادم، بعد قبول ملف tapi32.manifest الخاص بي.

هذا هو مكدس الاستدعاء بالكامل حتى يتم إنتاج استدعاء الخادم في CsrBasepCreateActCtxCommon.

يتم وضع نقاط التوقف على عودة بعض دوال مكدس الاستدعاء.

عند التوقف، يمكن ملاحظة أنه تم تحميل imm32.dll من مجلد "tasks":

يمكن التحقق باستخدام PROCESS MONITOR من أن TCMSETUP يقوم بتحميل IMM32.dll من مجلد "tasks".

عملية CMD التي تم تنفيذها للتو لديها صلاحيات HIGH.

أيضًا، لديها نفس صلاحيات Administrator.
بهذه الصلاحيات، يمكننا الآن تثبيت أي برنامج يحتاج إلى رفع إلى مسؤول (elevation to administrator) والكتابة في أي مجلد. على سبيل المثال، الكتابة إلى SYSTEM32 أو أي مجلد تثبيت برنامج، كما يمكن رؤيته في فيديو العرض التوضيحي أدناه.
إليك الصلاحيات قبل الاستغلال (مستوى التكامل Medium وليس Administrator):

وهنا الصلاحيات بعد الاستغلال (مستوى التكامل High FULL Administrator):

في هذه المرحلة، إنها فرصة جيدة للارتقاء بسهولة إلى صلاحيات SYSTEM، عن طريق إسقاط بعض ملفات DLL المصممة في مجلد النظام.


شاهد الفيديو هنا و إثبات المفهوم الوظيفي هنا
tapi32.manifest الذي أنشأته، وقبله، مما سمح له بتحميل IMM32.dll الخارجي من نفس مجلد "tasks".شكرًا لنيكولاس إيكونومو حيث أن عرضه التقديمي كان نقطة البداية لأبحاثي ونشر هذه التدوينة.
ريكاردو نارفايا