
استغلال إثبات المفهوم لثغرة CVE-2022-37969، وهي تصعيد صلاحيات محلي في برنامج تشغيل نظام ملفات السجل المشترك في ويندوز. يوضح رش الكومة، وسرقة الرموز المميزة، والكتابة التعسفية في النواة لتحقيق صلاحيات النظام.
المؤلفون: Ricardo Narvaja و Daniel Kazimirow (Solid)
لأغراض العرض التوضيحي فقط. يعمل الاستغلال الكامل على أنظمة Windows 11 21H2 الضعيفة.
إثبات مفهوم وظيفي مبني على المعلومات المنشورة سابقًا بواسطة Zscaler
اطلع على المقال فهم تصعيد الامتيازات المحلية في برنامج تشغيل Windows Common Log File System Driver المرتبط بـ CVE-2022-37969.
شرح خطوات الاستغلال:
السيناريو المستخدم هنا هو Windows 11 21H2 (OS Build 22000.918) و clfs.sys v10.0.22000.918
الخطوة الأولى هي إنشاء ملف باسم MyLog.blf في المجلد العام (%public%)، باستخدام الدالة CreateLogFile():



ثم يقوم بإنشاء عدة ملفات سجل بأسماء عشوائية باستخدام حلقة Loop.
وداخل الحلقة، يستدعي الدالة getBigPoolInfo() الخاصة بنا:

يستدعي NtQuerySystemInformation()، مع 0x42 (66 بالنظام العشري) كوسيط أول، وسيعيد في v5 المعلومات حول التخصيصات التي تمت في bigpool، والتي تكون بنيتها من النوع SYSTEM_BIGPOOL_INFORMATION.

علينا استدعاء هذه الدالة مرتين. الاستدعاء الأول سيعيد خطأً، لكنه سيعطينا الحجم الصحيح للمخزن المؤقت (buffer) لاستدعاء الدالة مرة ثانية للحصول على المعلومات المطلوبة.

ستستقبل v5 معلومات بنية SYSTEM_BIG_POOL_INFORMATION.

يُخزَّن عدد التخصيصات في bigpool في الحقل الأول المسمى Count, وفي الحقل الثاني توجد مصفوفة من البنى SYSTEM_BIGPOOL_ENTRY.

ثم سنبحث عبر جميع البنى عن وسم "Clfs" والحجم 0x7a00.

يخزّن في مصفوفة تسمى kernelAddrArray العنوان VirtualAddress الذي يمثل الحقل الأول لكل بنية تحمل وسم CLFS وحجم 0x7a00. من الآن فصاعدًا، ستُسمى التجمعات (pools) التي تستوفي الشرطين معًا: "التجمعات الصحيحة".

بالإضافة إلى تخزين كل تجمع صحيح في المصفوفة، فإنه يخزن آخر تجمع صحيح تم العثور عليه في محتوى المتغير a2، الذي يُستخدم كوسيط للدالة.

بهذه الطريقة، يشير a2 دائمًا إلى آخر تجمع صحيح تم إنشاؤه بوسم CLFS وحجم 0x7a00.
المتغير v26 يخزن دائمًا التجمع الصحيح السابق الذي تم العثور عليه لأنه يساوي v24 (v26=v24)، قبل استدعاء getBigPoolinfo()، لكن v24 يُحدَّث عند الخروج من هذا الاستدعاء بآخر تجمع صحيح تم العثور عليه، بينما يبقى v26 مع التجمع الصحيح السابق الذي تم العثور عليه.

ثم يطرح العنوانين من بعضهما، وفي حال كانت النتيجة سالبة، يعكس المعاملين بحيث تكون النتيجة موجبة دائمًا.

بهذه الطريقة، سيخزن v32 الفرق بين VirtualAddress لآخر تجمعين صحيحين تم العثور عليهما.
ثم يقوم بشيء مشابه، في هذه الحالة v23 يساوي صفرًا في البداية، لذلك يقوم بـ v23=v32 في المرة الأولى.

في المرة التالية داخل الحلقة، ما زال v23 يحمل القيمة نفسها وليست صفرًا، لذلك يخرج من الحلقة ويذهب إلى هنا.

يحمل V32 الفرق الأخير وv23 الفرق السابق، وإذا كانا متساويين، فإنه يخرج ويزيد العداد بمقدار واحد، لكنه يعيد تعيين العداد إلى الصفر.
الفكرة هي العثور على 6 مقارنات متتالية لوسوم CLFS وحجم 0x7a00 تكون فروقها متساوية، وسيكون هذا الفرق هو 0x11000. سنرى عند التنفيذ أنه عندما يجد 6 عناصر متتالية بمسافات متساوية (لأنه يبدأ من الصفر)، سيعطي قيمة الفرق بينها.


هناك نرى أنه وجد 6 عناصر متتالية وخرج من حلقة إنشاء ملفات السجل.
في المجلد "public" يمكننا رؤية الملفات التي تم إنشاؤها

تفتح الدالة craftFile() الخاصة بنا الملف الأصلي (MyLog.blf) وتعدّله لتفعيل الثغرة.

بعد تعديل الملف، من الضروري تغيير CRC32، وإلا فسنحصل على خطأ ملف تالف
تقع هذه القيمة عند الإزاحة 0x80C من الملف.

بعد ذلك، يقوم بتنفيذ HeapSpray، مستخدمًا الدالة VirtualAlloc() لتخصيص الذاكرة، في العنوانين الاعتباطيين 0x10000 و 0x5000000 على التوالي، ويحفظ في التخصيص الثاني (0x10000) القيمة 0x5000000 كل 0x10 بايت.

يستخدم CreatePipe() لإنشاء أنبوب مجهول (anonymous pipe) واستدعاء NtFsControlFile() باستخدام 0x11003c كوسيط لإضافة سمة (attribute)، ويمكنك لاحقًا استدعاء الدالة نفسها مع الوسيط 0x110038 لقراءة تلك السمة.
يمكن العثور على مزيد من التفاصيل حول هذه الطريقة هنا

هناك نرى المخزن المؤقت للإدخال وهو السمة التي نضيفها، وإذا استدعينا NtFsControlFile() مرة أخرى مع الوسيط 0x11038، فسيعيد في الإخراج السمة نفسها.

ابحث في التجمع عن وسم السمة التي تم إنشاؤها (NpAt)


وعندما يعثر عليه، يحفظ في v30.Pointer العنوان VirtualAddress لهذا التجمع.
يشير V30.pointer+24 إلى AttributeValueSize في تجمع النواة ويحفظه في أحد عمليات HeapSpray التي قمنا بها سابقًا.

الفكرة هي الكتابة إلى عنوان النواة +8، لاستبدال AttributeValue.


تحتوي بنية PipeAttribute في حقلها الأول على LIST_ENTRY بحجم 16 بايت، ثم مؤشر إلى اسم السمة بحجم 8 بايت، ثم يأتي عند الإزاحة 0x18 (24 بالنظام العشري) الحقل AttributeValueSize، وهو الحقل الذي نخزنه في HeapSpray.
بعد ذلك، نقوم بتحميل CLFS.sys و ntoskrnl في وضع المستخدم (usermode)، وباستخدام GetProcAddress() نعثر على عناوين الدالتين ClfsEarlierLsn() و SeSetAccessStateGenericMapping().

ثم نستدعي الدالة FindKernelModulesBase() التي ستجد العنوان الأساسي للنواة لكلا الوحدتين نفسهما باستخدام NtquerySystemInformation() هذه المرة مع الوسيط SystemModuleInformation لإرجاع معلومات حول جميع الوحدات.

بهذه الطريقة، يمكننا حساب الإزاحة (offset) لكل دالة، ثم الحصول عليها في النواة

يتم استدعاء الدالة pipeArbitraryWrite() مرتين، وهناك علم (flag) يكون صفرًا في البداية للاستدعاء الأول، وعندما يكون في الاستدعاء الثاني قيمته 1، فإنه سيغير قيم HeapSpray.

في الاستدعاء الأول، توجد القيم التالية في عنوان الذاكرة 0x5000000

تذكر أن هذه القيمة، بالإضافة إلى تخصيصها في ذلك العنوان، تُخزَّن في HeapSpray الخاص بنا.

هذه هي حالة الذاكرة بعد الاستدعاء الأول، كما قلنا، في العنوان القريب من 0x5000000

وفي HeapSpray من الذاكرة 0x10000، سيخزن المؤشر إلى AttributeValueSize كل 0x10 بايت، بالإضافة إلى المؤشر إلى 0x5000000.

هذا التسلسل سيؤدي إلى تشغيل الثغرة:

يتم استدعاء CreateLogFile() مرة أخرى على الملف المعدل وعلى ملف آخر باسم عشوائي.
ثم يتم استدعاء AddLogContainer() باستخدام مقابض (handles) تلك الملفات.

يتم استدعاء NtSetinformationFile()، ثم تُغلق المقابض مما يؤدي إلى إفساد المؤشر (سيتم شرح ذلك لاحقًا)

يمنع HeapSpray حدوث شاشة زرقاء (BSOD) في هذه المرحلة:

عند تعيين نقطة توقف (breakpoint) هناك، يمكننا أن نرى أن المؤشر تالف ويشير إلى HeapSpray الخاص بنا، وبه يمكننا التحكم في استدعائي الدالتين التاليتين من جدول vtable.


يأخذ RAX القيمة 0x5000000 ويقفز أولاً إلى الدالة الموجودة عند 0x5000000+18، ثم إلى 0x5000000+8.


إذن، القفزة الأولى إلى fnClfsEarlierLsn() ثم إلى fnSeSetAccessStateGenericMapping().
نتتبع التنفيذ من نقطة التوقف ونرى أنه يصل إلى CLFS!ClfsEarlierLsn().

تُستدعى هذه الدالة حصريًا لأنه عند عودتها، تقوم بتعيين EDX إلى 0xFFFFFFFF

في العنوان 0xFFFFFFFF كنا قد خزّنا نتيجة SYSTEM _EPROCESS & 0xFFFFFFFFFFFFFFF000

كما ذكرنا، عند العودة من CLFS!ClfsEarlierLsn()، تكون قيمة RDX هي 0x00000000FFFFFFFF

نصل إلى الدالة الثانية nt!SeSetAccessStateGenericMapping()

هذه الدالة مفيدة، لأن RCX يشير إلى HeapSpray الخاص بنا، وقيمة RDX هي 0xFFFFFFFF، ونحن نتحكم في محتواها


محتوى RCX+0x48 يحوي المؤشر إلى AttributeValueSize الذي تم تخزينه في v30.Pointer+24



تُنقل قيمة المؤشر تلك إلى RAX، ثم يقرأ محتويات العنوان 0xFFFFFFFF حيث كنا قد خزّنا عنوان SYSTEM _EPROCESS & 0xFFFFFFFFFFFFFFF000.

ثم يستبدل في RAX+8 الحقل التالي وهو AttributeValue()


بالطبع، كان AttributeValue يشير عادةً في النواة إلى السمة التي أضفناها.

والآن سنستبدله بمؤشر إلى نتيجة system _EPROCESS & 0xFFFFFFFFFFFFFFF00.
وهذا يعني أنه عندما نستدعي الدالة NtFsControlFile() مرة أخرى، هذه المرة مع الوسيط 0x110038 لقراءة السمة، فبدلاً من إرجاع الأحرف "A” التي كان يشير إليها مؤشر AttributeValue، سيقرأ الآن من _EPRROCESS & 0xFFFFFFFFFFFFFFFFF000 عدد البايتات المطلوب ويعيدها في المخزن المؤقت للإخراج، ومن ثم يمكننا في الاستدعاء الأول الحصول على قيمة SYSTEM TOKEN.

v9b هو عنوان بداية مخزن الإخراج المؤقت حيث تم نسخ محتوى نتيجة System EPROCESS & 0xFFFFFFFFFFFFFFF000.
إلى ذلك، يضيف v14 وهي آخر 3 بايتات من EPROCESS الخاص بالنظام، ثم يضيف 0x4b8 وهي إزاحة Token لهذا الإصدار من Windows 11، ثم يجد محتويات ذلك العنوان التي ستكون قد خزّنت قيمة System Token.



تذكر أن آخر 4 بتات تم تغييرها، وهذا غير مهم، لذا تظل القيمة متطابقة.
في الاستدعاء الثاني تكون قيمة Flag هي 1 لأنه تمت زيادتها في نهاية الاستدعاء الأول.

هناك نرى الترتيب الذي تُخزَّن به القيم

يحتوي العنوان 0xFFFFFFFF على القيمة التي وجدناها للتو لـ System Process Token.


وفي HeapSpray توجد قيمة عنوان Token الخاص بالعملية مطروحًا منها 8. ستُستخدم هذه القيمة مضافًا إليها ثمانية كهدف، وتذكر أنك كتبت على العنوان المشار إليه بواسطة RAX+8.


في عنوان الذاكرة الذي يبدأ عند 0x5000000

نرى أيضًا أنه يستخدم اسم حاوية أخرى، لأن الحاوية السابقة قيد الاستخدام من قبل عملية النظام ولا يمكن فتحها مرة أخرى أو حذفها.

ثم يتم تشغيل الثغرة للمرة الثانية بنفس الطريقة التي كانت عليها في المحاولة الأولى.

يصل مرة أخرى إلى CLFS!ClfsEarlierLsn().

ويقوم بتعيين RDX إلى 0xFFFFFFFF

ثم يصل إلى nt!SeSetAccessStateGenericMapping()

يقرأ عنوان Token الخاص بالعملية مطروحًا منه 8 حيث سيكتب

ثم يقرأ SYSTEM TOKEN

ويكتب في عنوان Token الخاص بالعملية (يضيف 8) قيمة System Token

وبهذه الطريقة، تمتلك العملية لدينا System Token

بمجرد كتابة الرمز، نبدأ عملية للتحقق من الصلاحيات، وفي هذه الحالة، نقوم بتشغيل Notepad.exe



تذكر أن إثبات المفهوم هذا يعمل فقط على Windows 11، أما على Windows 10 فسيؤدي إلى شاشة زرقاء (BSOD)، لذا يجب إجراء بعض التعديلات ليعمل بشكل صحيح، وهذا غير مشروح في هذه التدوينة.
تحليل البنى
لقد أخذنا البنى ومعظم التوثيق حول تنسيق ملفات CLFS من العمل الممتاز لـ IONESCU حول CLFS Internals.
يمكننا أن نرى أنه تمت إضافة فحص في الدالة ClfsBaseFilePersisted::LoadContainerQ

القيم التي تقوم بعملية الجمع تنتمي إلى بنية _CLFS_BASE_RECORD_HEADER.

لاحظ أن Base Block يبدأ عند الإزاحة 0x800 من الملف، وينتهي عند الإزاحة 0x71FF، حيث تقابل أول 0x70 بايت Log Block Header
كممارسة جيدة، يمكننا إضافة بنية _CLF_LOG_BLOCK_HEADER في IDA
struct _CLFS_LOG_BLOCK_HEADER
{
UCHAR MajorVersion;
UCHAR MinorVersion;
UCHAR Usn;
char ClientId;
USHORT TotalSectorCount;
USHORT ValidSectorCount;
ULONG Padding;
ULONG Checksum;
ULONG Flags;
CLFS_LSN CurrentLsn;
CLFS_LSN NextLsn;
ULONG RecordOffsets[16];
ULONG SignaturesOffset;
};
ثم لدينا رأس السجل الأساسي (_CLFS_BASE_RECORD_HEADER) الذي يبدأ عند الإزاحة 0x870 من بداية الملف ويبلغ طوله 0x1338 بايت.

إذا كنت تريد استيرادها إلى IDA، يجب عليك أولاً إضافة الأنواع والبنى المفقودة التالية
typedef GUID CLFS_LOG_ID;
typedef UCHAR CLFS_LOG_STATE;
struct _CLFS_METADATA_RECORD_HEADER
{
ULONGLONG ullDumpCount;
};
الآن أصبح جاهزًا للإضافة:
typedef struct _CLFS_BASE_RECORD_HEADER
{
CLFS_METADATA_RECORD_HEADER hdrBaseRecord;
CLFS_LOG_ID cidLog;
ULONGLONG rgClientSymTbl[0x0b];
ULONGLONG rgContainerSymTbl[0x0b];
ULONGLONG rgSecuritySymTbl[0x0b];
ULONG cNextContainer;
CLFS_CLIENT_ID cNextClient;
ULONG cFreeContainers;
ULONG cActiveContainers;
ULONG cbFreeContainers;
ULONG cbBusyContainers;
ULONG rgClients[0x7c];
ULONG rgContainers[0x400];
ULONG cbSymbolZone;
ULONG cbSector;
USHORT bUnused;
CLFS_LOG_STATE eLogState;
UCHAR cUsn;
UCHAR cClients;
} CLFS_BASE_RECORD_HEADER, *PCLFS_BASE_RECORD_HEADER;

بعد تضمين البنى، نلاحظ أنه يتم إجراء جمع بين cbSymbolZone والعنوان الذي ينتهي عنده _CLFS_BASE_RECORD_HEADER. (البداية + 1338h)
تذكر أن cbSymbolZone تم تعديله في ملف السجل المعدّ من 0x000000F8 إلى 0x0001114B.
(الإزاحة 0x1b98 من الملف)
0x800(offset of the start of the Base Block) + 0x70 (logBlockHeader) + 0x1328 (cbsymbolZone)
0x800+0x70+0x1328 = 0x1b98
ملف MyLog.blf مع cbsymbolZone المخصص:


بما أن التصحيح موجود في دالة CClfsBaseFilePersisted::LoadContainerQ، يجب أن نلقي نظرة على الكائن CClfsBaseFilePersisted.
اضبط نقطة توقف في CLFS!CClfsBaseFilePersisted::LoadContainerQ، وعند استدعاء CreateLogFile بمقبض الملف المعدّ ستتوقف.

استدعِ الدالة CClfsBaseFile::GetBaseLogRecord للحصول على عنوان Base Log Record (_CLFS_BASE_RECORD_HEADER)

سيشير RAX إلى عنوان _CLFS_BASE_RECORD_HEADER

لاحظ بنية _CLFS_BASE_RECORD_HEADER في الذاكرة وحقل cbsymbolZone الذي يبعد 0x1328 بايت إلى الأمام


يخزّن r14 البنية المقابلة لـ “this”، وهي CClfsBaseFilePersisted لأنها this الخاصة بالدالة CClfsBaseFilePersisted::LoadContainerQ.

بنية CClfsBaseFilePersisted في الذاكرة:

إذن، لننشئ بنية بطول 0x21c0 لإكمال حقولها أثناء تفكيكها (إنها بنية غير موثقة)، وسنسميها struct_CClfsBaseFilePersisted

داخل الدالة CClfsBaseFile::GetBaseLogRecord() يتم الحصول على المؤشر إلى _CLFS_BASE_RECORD_HEADER. ونعلم أن "this" في تلك الدالة هو البنية: struct_CClfsBaseFilePersisted.

اقرأ حقلين (الإزاحة 0x28 و 0x30)

الحقل 0x28 هو كلمة (word) وقيمته 6، لذا نغيّر النوع إلى word في البنية.



في الوقت الحالي، نعيد تسميته إلى الثابت 6 (const_6)


وفقًا للتوثيق، الرقم 6 قد يكون عدد الكتل CLFS_METADATA_BLOCK_COUNT. قد يشير هذا الحقل إلى هذه القيمة.
وهذا المؤشر يقع عند الإزاحة 0x30.

لاحظ أن الحجم الموضّح هناك يشمل الترويسة بطول 0x10


عند استدعاء الدالة ExAllocatePoolWithTag، يتم طلب عدد قليل من البايتات، لكن الترويسة غير مضمنة، لذلك سيتم طلب 0x90 بايت (0xa0 - 0x10) في الاستدعاء.
عند البحث عن النص +30h]، وهي التعليمات التي تكتب عند الإزاحة 0x30، وجدنا قائمة طويلة، لكن تصفية القائمة حسب نوع الكائن CClfsBaseFilePersisted تُبقي لنا نتائج قليلة، ونجد فورًا مكان تخصيص هذا الحجم، وبنفس الوسم. (نصيحة: أسماء الدوال Create و Initialize هي دائمًا أول ما يجب النظر إليه).


بما أننا لا نعرف الاسم بعد، سنسميه pool_0x90، وهو بنية أخرى غير موثقة، وسننشئ بنية بهذا الحجم.


البنية pool_0x90 في الذاكرة تحتوي على مؤشر آخر عند الإزاحة الخاصة بها 0x30.

يشير هذا المؤشر الآخر إلى الكتلة الأساسية في الملف (تبدأ الكتلة الأساسية عند الإزاحة 0x800)


صورة مأخوذة من منشور مدونة Zscaler:

التخصيص ضخم لأنه يحتوي على الكتلة الأساسية بأكملها.



إذن، سننشئ بنية جديدة بحجم 0x7a00 ونسميها BASE_BLOCK

البايتات السبعون الأولى، وكما نعلم بالفعل، تتوافق مع _CLFS_LOG_BLOCK_HEADER، والـ 0x1338 التالية مع _CLFS_BASE_RECORD_HEADER.

إذن، بجمع بداية الكتلة الأساسية مع الإزاحة إلى السجل التالي (وهي 0x70)، نحصل على _CLFS_BASE_RECORD_HEADER

البنية _CLFS_BASE_RECORD_HEADER في الذاكرة.

بالنظر إلى دوال أخرى لنفس الكائن CClfsBaseFilePersisted، في الدالة CClfsBaseFilePersisted::AddContainer تحصل أيضًا عبر CClfsBaseFile::GetBaseLogRecord على عنوان _CLFS_BASE_RECORD_HEADER.

بعد ذلك، استدعِ CClfsBaseFile::OffsetToAddr باستخدام cbOffset، إذ يحصل على عنوان _CLFS_CONTAINER_CONTEXT، ويخزّن cboffset في مصفوفة rgbcontainers التي تقع عند الإزاحة 0x328 من _CLFS_BASE_RECORD_HEADER.

تُستخدم الدالة CClfsBaseFile::OffsetToAddr للعثور على عناوين البنى من الإزاحة

عند هذه النقطة، لا تزال إزاحة الحاوية التي سيتم تخزينها عند 0x328 تساوي 0، لأننا لم نضف حاوية بعد.

يستدعي الـ PoC الدالة CreateLogFile مرتين، المرة الأولى مع الملف التالف MyLog.blf والمرة الثانية مع الملف العادي MyLogxxx.blf، لذا يجب أن نوقف التصحيح مرتين في جميع الأماكن المذكورة أعلاه وندوّن في المفكرة عناوين البنى المذكورة أعلاه لكلا الملفين.

لنتقدم سريعًا قليلًا إلى CLFS!CClfsLogFcbPhysical::AllocContainer عن طريق تعيين نقطة توقف عليه وتشغيله هناك.
عند الوصول إلى AddLogContainer() في الـ PoC، نتوقف عند نقطة التوقف.

لنقم أيضًا بتعيين نقطة توقف على CClfsBaseFilePersisted::AddContainer+176 حيث رأينا سابقًا أنها ستجد الإزاحة والمؤشر إلى بنية _CLFS_CONTAINER_CONTEXT.


عند توقف المصحح، يمكننا رؤية أن الإزاحة هي 0x1468.

سيُرجع RAX عنوان بنية _CLFS_CONTAINER_CONTEXT.

البنية ما تزال فارغة لأنه لم تتم إضافة الحاوية بعد.

لاحظ أن قيمة SignatureOffset=0x50 التي كتبناها عند الإزاحة 0x868 في الملف التالف، عند طرح 0x800 من بداية الكتلة الأساسية، ستكون في بنية _CLFS_LOG_BLOCK_HEADER عند الإزاحة 0x68.


عندما يستدعي الـ PoC الدالة AddLogContainer() باستخدام الملف التالف، تكون القيمة عند الإزاحة 0x68 من _CLFS_LOG_BLOCK_HEADER, بدلًا من القيمة 0x50 التي كتبناها هناك، هي حاليًا 0xFFFF0050 في الذاكرة.

في وقت ما، تم تغيير تلك القيمة بواسطة البرنامج؛ ولمعرفة متى حدث ذلك، في التنفيذ التالي، سنضع نقطة توقف ذاكرة عند الكتابة.
يتم تخزين الإزاحة عند r15 + 0x328 (r15 يشير إلى بنية _CLFS_BASE_RECORD_HEADER)


يخزّن RBX الإزاحة 0x1468.

إذن، في عنوان الكتلة الأساسية + 0x70 + الإزاحة 0x1468 التي اكتشفناها، سيكون عنوان حاوية CLFS_CONTAINER_CONTEXT.

في بنية CLFS_CONTAINER_CONTEXT، عند الإزاحة 0x18 سيكون مؤشر pContainer الذي سيُخزَّن هناك، ويمكننا تعيين نقطة توقف عند الكتابة لمعرفة متى يُكتب.


هذا هو المؤشر الذي يجب علينا إفساده؛ لأنه في الدالة التي توجد بها الثغرة، تقوم أولاً بقراءة CLFS_CONTAINER_CONTEXT، ثم تنقله إلى r15، ثم تقرأ قيمة r15+18، وهو هذا المؤشر الذي وضعنا عليه نقطة توقف الكتابة للتو.


يخزّن pContainer عند الإزاحة 0x1c0 من بنية struct_CClfsBaseFilePersisted.

بعد عدة توقفات، نصل إلى اللحظة التي يحدث فيها الإفساد. الجزء العلوي من عنوان المؤشر تغيّر من FFs إلى صفر.

يحدث هذا عند استدعاء AddLogContainer() الثاني للملف التالف، فيتم إفساد مؤشر MyLogxxx السابق.
تكمن المشكلة في أن SignaturesOffset، التي يجب أن تكون 0x50، أصبحت الآن 0xFFFF0050، مما يسمح بالكتابة خارج الحدود في memset التالية.


ستقوم دالة memset() بإفساد بنية _CLFS_CONTAINER_CONTEXT الموجودة بالأسفل، وهذه البنية تتوافق مع ملف MyLogxxx، حيث أنها عند الإنشاء وضعتها على بُعد 0x11000 بايت من بعضها البعض.
بهذه الطريقة، تحسب بالضبط أين تكتب في البنية التالية وتصفّر الجزء العلوي من المؤشر، بحيث يشير إلى كومة المستخدم حيث تم إنشاء HeapSpray.
بنية الكتلة الأساسية للملف التالف تسبق بنية ملف MyLogxxx بمقدار 0x11000 تمامًا.
الملف التالف:

MyLogxxx


RCX أصغر من RDX لأنه تمت إضافة 0xFFFF0050 إليه، بدلًا من 0x50 كما ينبغي.

ووصلنا إلى دالة memset()، لضبط مقدار 0xb0 بايت على أصفار، مع توجيه RCX إلى بنية CLFS_CONTAINER_CONTEXT الخاصة بملف MyLogxxx، وتحديدًا إلى البايتات الخمسة العليا من pContainer.

سيتم إفساد هذا المؤشر عن طريق الكتابة فوق البايتات الأولى:

فيبقى يشير إلى عنوان ذاكرة تحكمنا به مسبقًا عبر HeapSpray


بعد ذلك، سيتم إغلاق مقبض ملف MyLogxxx، ويتم الوصول إلى CClfsBaseFilePersisted::RemoveContainer، وعندها تُستغل الثغرة أخيرًا.

الآن بعد أن أصبح لدينا مزيد من المعلومات، نلاحظ أنه هنا يقرأ Base_Block.LOG_BLOCK_HEADER.SignaturesOffset ويقرأ the Base_Block. .LOG_BLOCK_HEADER.TotalSectorCount
في الجزء الأول من التصحيح، يجب ألا تكون SignaturesOffset أكبر من 0x7a00؛ في ملفنا كانت قيمتها في الأصل 0x50، وإذا وصلت بقيمة أكبر من 0x7a00 فسيُخرجنا من الدالة.

عند تشغيل الـ PoC على الجهاز المحدّث، يقارن 0x50 مع 0x7a00، وبما أنها أصغر فإنه يستمر.

في الكتلة التالية، تتم إضافة cbSymbolZone التالف إلى قيمة العنوان النهائي لـ _CLFS_BASE_RECORD_HEADER، ويُخزَّن هذا المجموع في result_1.

ثم يُضاف عنوان Base_Block إلى قيمة SignatureOffset، والتي تكون في الملف العادي 0x7980.

العنوان الأقصى لـ base_block هو 0x7a00، والآن يُسمح لـ SymbolZone حتى 0x80 قبل الحد.
سيخزّنها في result_2، أي أن هذا سيكون الحد الأقصى لـ SymbolZone داخل الكتلة الأساسية، ثم يقارن النتيجتين؛ إذا كانت الأولى أكبر من الثانية، فهذا يعني أنها تجاوزت الحدود.


من الواضح أن العضو الأول سيكون أكبر من الثاني ولن يستمر، لأن المجموع الأول لـ cbSymbolZone + العنوان النهائي لـ _CLFS_BASE_RECORD_HEADER يتجاوز الحد (وهو result_2)، مما يؤدي إلى “خروج عن الحدود”.

آخر ما علينا اكتشافه هو أين تتحول قيمة SignatureOffset من 0x50 إلى 0xFFFF0050.إذن، لنبدأ من جديد، وأعد التشغيل وتوقف عند CLFS!CClfsBaseFilePersisted::LoadContainerQ حيث لم تتغير القيمة بعد في الذاكرة ولا تزال 0x50.
عيّن نقطة توقف وصول (access breakpoint) عند الإزاحة 0x68 في SignatureOffset.

وبعد عدة توقفات، نكتشف اللحظة المناسبة التي يعدّل فيها القيمة، داخل ClfsEncodeBlockPrivate.

هذه الدالة غير مُرقّعة (patched)، لذا قد يكون هذا سلوكًا ناتجًا عن انخفاض قيمة 0x50 وبقية القيم التي تم التلاعب بها.
من بين القيم المصنوعة (crafted)، يمكننا رؤية قيمة ccoffsetArray التي اسمها في بنية _CLFS_BASE_RECORD_HEADER هو rgClients وتمثل مصفوفة الإزاحات التي تشير إلى كائن Client Context.
حقل rgClients يقع عند الإزاحة 0x138 (0x9a8-0x800-0x70) من بنية _CLFS_BASE_RECORD_HEADER.


في الـ PoC، تكون هذه القيمة مشوّهة لتشير إلى كائن سياق عميل مزيف، يُسمى FakeClientContext


هذه هي بنية Client Context _CLFS_CLIENT_CONTEXT
struct _CLFS_CLIENT_CONTEXT
{
CLFS_NODE_ID cidNode;
CLFS_CLIENT_ID cidClient;
USHORT fAttributes;
ULONG cbFlushThreshold;
ULONG cShadowSectors;
ULONGLONG cbUndoCommitment;
LARGE_INTEGER llCreateTime;
LARGE_INTEGER llAccessTime;
LARGE_INTEGER llWriteTime;
CLFS_LSN lsnOwnerPage;
CLFS_LSN lsnArchiveTail;
CLFS_LSN lsnBase;
CLFS_LSN lsnLast;
CLFS_LSN lsnRestart;
CLFS_LSN lsnPhysicalBase;
CLFS_LSN lsnUnused1;
CLFS_LSN lsnUnused2;
CLFS_LOG_STATE eState;
union
{
HANDLE hSecurityContext;
ULONGLONG ullAlignment;
};
};
قيمة eState موجودة عند الإزاحة 0x78 من بداية البنية، وفي الملف المشوّه عند 0x23a0+0x78.


توضح هذه القيمة حالة السجل (log).
typedef UCHAR CLFS_LOG_STATE, *PCLFS_LOG_STATE;
const CLFS_LOG_STATE CLFS_LOG_UNINITIALIZED = 0x01;
const CLFS_LOG_STATE CLFS_LOG_INITIALIZED = 0x02;
const CLFS_LOG_STATE CLFS_LOG_ACTIVE = 0x04;
const CLFS_LOG_STATE CLFS_LOG_PENDING_DELETE = 0x08;
const CLFS_LOG_STATE CLFS_LOG_PENDING_ARCHIVE = 0x10;
const CLFS_LOG_STATE CLFS_LOG_SHUTDOWN = 0x20;
const CLFS_LOG_STATE CLFS_LOG_MULTIPLEXED = 0x40;
const CLFS_LOG_STATE CLFS_LOG_SECURE = 0x80;
هذه القيمة مضبوطة على CLFS_LOG_STATE CLFS_LOG_SHUTDOWN =0x20
القيمة المشوّهة الأخرى هي fAttributes والتي تقابل مجموعة علامات FILE_ATTRIBUTE المرتبطة بملف السجل الأساسي (مثل System و Hidden).


بما أن الحقل يبدأ قبل ذلك ببايت واحد عند 0xa ويمتد على بايتين، فإن قيمة fAttributes هي 0x100.


أخيرًا، هناك قيمة blocknameoffset التي تشير إلى الإزاحة 0x1bb8، أي بإضافة 0x78 و 0x800 تشير إلى الإزاحة 0x2428 من الملف.


لاحظ أن الإزاحة إلى Client Context هي 0x1b30

إذن، Client Context موجود عند الإزاحة 0x23a0.


وقبلها مباشرة بـ 0x10، توجد القيمة المقابلة لـ blocknameoffset.


والتي تشير إلى السلسلة النصية التي تحمل الاسم.
آخر قيمة هي blockattributeoffset التي تقع قبل Client Context بمقدار 0xC عند 0x2394.

هاتان القيمتان الأخيرتان تنتميان إلى بنية تسبق Client Context بطول 0x30 بايت، تُسمى _CLFSHASHSYM
typedef struct _CLFSHASHSYM
{
CLFS_NODE_ID cidNode;
ULONG ulHash;
ULONG cbHash;
ULONGLONG ulBelow;
ULONGLONG ulAbove;
LONG cbSymName;
LONG cbOffset;
BOOLEAN fDeleted;
} CLFSHASHSYM, *PCLFSHASHSYM;


تقعان عند 0x20 و 0x24 بايت من بداية بنية _CLFSHASHSYM، إذن في بنية _CLFSHASHSYM، القيمة المسماة blockNameOffset في الـ PoC هي الحقل cbSymName، و blockAttributteoffset هو الحقل cbOffset.


تلك هي القيم المشوّهة، الآن نحتاج إلى معرفة كيف تؤثر على تغيير SignaturesOffset من القيمة 0x50 إلى 0xFFFF0050.
لنلقِ نظرة على الدالة CClfsBaseFile::AcquireClientContext()، التي يجب أن تعيد سياق العميل (client context).

إنها تستدعي CClfsBaseFile::GetSymbol مع الوسيطة الرابعة التي ستكون من النوع _CLFS_CLIENT_CONTEXT ** حيث سيُخزَّن المؤشر إلى Client Context.

داخل الدالة CClfsBaseFile::GetSymbol نمرر إزاحة ccoffsetArray المشوّهة إلى CClfsBaseFile::OffsetToAddr ونحصل على عنوان سياق العميل، لنضع نقطة توقف هناك بحيث تتوقف عند استدعاء الملف الذي أُنشئ باستخدام CreatelogFile.

هناك تتوقف عند الوسيطة المصنوعة ccoffsetArray.


الدالة CClfsBaseFile::OffsetToAddr تعيد سياق العميل المزيف.

وتتحقق من أن قيمة cbOffset ليست صفرًا، حيث يوجد 0xC قبل بنية _CLFS_CLIENT_CONTEXT الموجودة في RAX.


ثم تقارن cbOffset مع ccoffsetArray (الموجودة في RSI)، يجب أن تكونا متساويتين، وإلا سنحصل على خطأ.

وتتحقق أيضًا من أن cbSymName تساوي cbOffset+0x88، وإلا سنحصل على خطأ أيضًا.

وأخيرًا، تقارن بايت cidClient مع الصفر.

إذا نجحت كل هذه الفحوصات، سيتم حفظ client context.

مخرج الدالة r14 يشير إلى Client Context

عند الخروج من CClfsLogFcbPhysical::Initialize سيكون لدينا عنوان CLFS_CLIENT_CONTEXT.

الآن تقرأ قيمة fAttributes (0x100)

هذه الدالة تنتمي إلى الفئة CClfsLogFcbPhysical


والتي تم تخصيصها هنا، وحجمها 0x15d0 ووسمها “ClfC”

لننشئ بنية لتخزين ما نقوم بفهمه عكسيًا (reversing)، وسنسميها: struct_CClfsLogFcbPhysical.

لاحظ أنه عند 0x2b0 يحفظ عنوان بنية CClfsBaseFilePersisted.

بعد حفظ العديد من القيم في البنية، ينتقل إلى جزء مهم، حيث يختبر eState مع 0x20.


بما أن القيمة المشوّهة كانت 0x20، فإن الاختبار سيعيد 1.


نرى أنه في المُنشئ (constructor) داخل vtable يوجد:

سيتحقق مما إذا كان الملف multiplexed.

لذا، يسير عبر المسار المطلوب، ليصل إلى CClfsLogFcbPhysical::ResetLog.


يتم تهيئة عدة حقول إلى الصفر باستثناء حقل واحد يُهيأ إلى 0xFFFFFFFF00000000.

هنا يسترجع Client Context

يخزن القيمة 0xFFFFFFFF00000000.



يكتب 0xFFFFFFFF عند الإزاحة 0x5c وهي الجزء العلوي من CLFS_LSN lsnRestart.ullOffset



الآن ننفذ الدالة ClfsEncodeBlockPrivate()، وهي المسؤولة عن الكتابة فوق 0x50 بالقيمة 0xFFFF0050 كما رأينا سابقًا.
هناك يقرأ قيمة SignatureOffset = 0x50 التي لا تزال كما وضعناها في الملف المشوّه ويضيفها إلى بداية CLFS_LOG_BLOCK_HEADER.

هذه حلقة تكتب بايتين، ونظرًا لأن SignatureOffset بدلًا من أن يشير إلى قيمة صحيحة، وهي في الملف العادي قيمة مرتفعة مثل 0x3f8 تجعله يكتب في موضع أبعد، فإنه هنا سيكتب داخل نفس CLFS_LOG_BLOCK_HEADER
الفكرة هي تغيير وجهة الكتابة لمحاولة إفساد قيمة SignatureOffset.
الملف العادي

عند هذه النقطة، سيبدأ في التكرار وكتابة بايتين.

يجب أن يصل العداد إلى القيمة 0x3d للخروج من الحلقة.

RCX يتزايد من 0x200، نحن الآن في الدورة الثالثة، وقيمته 0x600

في التكرار 0xe، تكون RCX هي 0x1a00


هذا هو المكان الذي كتب فيه 0xFFFFFFFF000000.


إنه يقرأ آخر بايتين FFFF

ثم سينسخهما إلى R8


كما رأينا، هذه القيمة حرجة لأنها تسمح بتجاوز الفحص والكتابة خارج الحدود لإفساد مؤشر pContainer للملف الذي يلي memset() وكتابة أصفار في الأعلى وتركه يشير إلى الذاكرة التي نتحكم بها (HeapSpray).
في CClfsBaseFilePersisted::AllocSymbol، نفس المجموع الذي سيحصل على وجهة الـ memset وهي cbSymbolZone + العنوان النهائي لـ CLFS_BASE_RECORD_HEADER يقارنه مسبقًا مع Base_block + 0xFFFF0050، إذن لدينا قيم مشوّهة على جانبي المعادلة.
CbSymbolZone= 0x1114B
إنها القيمة المشوّهة التي عند إضافتها إلى العنوان النهائي لـ CLFS_BASE_RECORD_HEADER ستجعلها تكتب خارج الحدود، والعضو الآخر في المقارنة الذي يفترض أن يكون عنوان Base Block + SignatureOffset يظل SignatureOffset =0xFFFF0050 مما يسمح باجتياز هذا الفحص والكتابة خارج الحدود في memset() وتصفير الجزء العلوي من المؤشر الذي سيظل يشير إلى HeapSpray الخاص بنا.

بما أن RCX أصغر من RDX.

كما رأينا سابقًا. (قد تختلف القيم لأنها تعود إلى تنفيذ سابق)
سيُفسد المؤشر، مع ضبط البايتات الأعلى على 0

تاركًا إياه يشير إلى منطقة ذاكرة نتحكم فيها عبر HeapSpray


إذن، عند تشغيل الثغرة، نصل إلى CClfsBaseFilePersisted::RemoveContainer

سيكون هناك المؤشر الفاسد بالفعل ويمكن استغلاله كما رأينا سابقًا.

عند هذه النقطة نكون قد استغللنا الثغرة بنجاح، مما يؤدي إلى التحكم في الدوال التي تسمح بقراءة رمز SYSTEM والكتابة في عمليتنا الخاصة لتحقيق تصعيد الامتيازات المحلية.
نأمل أن تجدوه مفيدًا، إذا كان لديكم أي استفسار يمكنكم التواصل معنا على [email protected] و [email protected]
استمتعوا!