
منذ فبراير 2022 تم الإبلاغ عن برنامج فدية جديد يبدو أنه
يستخدم ثغرة يوم-صفر في ويندوز، وفقًا للبحث الذي أجرته
شركة تريند مايكرو.
مزيد من المعلومات حول برنامج الفدية هذا متاح على هذا
الرابط.
وفقًا لتحليل كاسبرسكي، استخدمت مجموعة فدية Nokoyawa
استغلالات أخرى تستهدف برنامج تشغيل نظام ملفات السجل المشترك (CLFS)
منذ يونيو 2022، ذات خصائص مشابهة لكنها متميزة، وكلها مرتبطة
بمطور استغلال واحد.
في أبريل 2023 عندما أصدرت مايكروسوفت التصحيح، تم تخصيص
CVE-2023-28252.
سابقًا، في عام 2022، قمنا بالبحث عن خطأ مشابه في المكوّن نفسه،
وتم توثيقه في هذه
التدوينة
لمواجهة التحليل، من الضروري معرفة تنسيق ملف .blf، الذي يعالجه برنامج تشغيل نظام ملفات السجل المشترك المعرّض للثغرة المسمى CLFS.sys والموجود في مجلد برامج التشغيل داخل system32.
مزيد من المعلومات حول هذا النوع من الملفات موجود في الروابط أدناه:
https://github.com/ionescu007/clfs-docs/blob/main/README.md
https://www.coresecurity.com/core-labs/articles/understanding-cve-2022-37969-windows-clfs-lpe
تم إجراء هذا التحليل على Windows 11 21H2 بإصدار clfs.sys 10.0.22000.1574، على الرغم من أنه يعمل أيضًا على Windows 10 21H2 وWindows 10 22H2 وWindows 11 22H2 وWindows server 2022.
في إصدارات ويندوز السابقة، من الضروري تعديل بعض القيم، وإلا فسوف نسبب شاشة زرقاء (BSOD).
تحديثات الثلاثاء من مايكروسوفت أبريل 2023.
يمكنك التحقق من إصدار برنامج التشغيل
كما هو موضح
عندما نُشرت الثغرة في أبريل 2023، بدأت مع إستيبان كازيميروف بفحص برنامج تشغيل CLFS.sys عكسيًا، على الرغم من أنه في هذه الحالة كان تحليل التصحيح وحده صعبًا جدًا لاستنتاج مكان الخطأ وكيفية استثارته، لأن الاستغلال معقد جدًا.
لاحقًا، صدرت تدوينة أظهر مؤلفها، من عينة من برنامج خبيث، بعض أجزاء الكود المفكك بواسطة HexRays وبعض المعلومات التي وجهت إلى أين يجب توجيه الاستغلال.
من الواضح أن المعلومات المقدمة لم تكن كاملة، ولكن بدون هذه المساعدة لم يكن من المرجح أن نتمكن من بناء الإثبات (PoC) ثم استغلالًا وظيفيًا.
لتسهيل الفهم، سنشرح أولًا كيفية بناء الإثبات (PoC) ثم سنقوم بتحليل الثغرة.
تحتوي هذه التدوينة على قسمين:
بناء الإثبات (PoC):
1-الحصول على عناوين النواة التي نحتاجها للاستغلال
2-تحضير المسار لإنشاء ملفات .blf:
3-إنشاء ملف "trigger blf" باستخدام دالة CreateLogFile()
4-صياغة ملف “trigger blf”
5-الحصول على عنوان النواة للكتلة الأساسية BASE BLOCK لملف trigger blf
6-استدعاء AddLogContainer بمقبض ملف trigger blf
7-تحضير ملفات spray blf
8-تحضير الذاكرة لتنفيذ الرش
9-استثارة الخطأ
التصحيح:
1-التحقق من رش الذاكرة
2-النظر إلى RecordOffset[12] في ملف trigger blf
3-النظر إلى قيمة iFlushBlock في ملف spray blf
4-لماذا يقرأ من BLOCK 1 SHADOW بدلاً من BLOCK 0 CONTROL؟
5-لماذا يكون المجموع الاختباري مساويًا صفرًا في ملفات blf spray؟
6-إنهاء الاستغلال.
7-التصحيح الحقيقي
سأنشئ دالة باسم InitEnvironment للحصول على بعض عناوين النواة الضرورية.
احصل على عنوان EPROCESS الخاص بعملي وأخزنه في المتغير g_EProcessAddress، ثم عنوان EPROCESS لعملية SYSTEM، وأخزنه في system_EPROCESS، ثم عنوان EHTREAD للـخيط الرئيسي في عملي، وأخزنه في g_EThreadAddress، وأخيرًا عنوان PREVIOUS MODE الذي لن يُستخدم في هذا الإصدار من الإثبات (PoC).

هذه الطريقة معروفة جيدًا، فدالة GetObjectKernelAddress تستدعي NtQuerySystemInformation مرتين مع الوسيط الأول SystemExtendedHandleInformation، وتُمرَّر المكالمة الأولى بحجم غير صحيح وتُرجع خطأً، ولكنها تُرجع أيضًا الحجم الصحيح الذي يُستخدم في المكالمة الثانية وتحصل على معلومات جميع المقابض، ثم تمر في حلقة على معلومات كل مقبض وفي الحقل Object من بنية handleinfo الصحيحة تحصل على العنوان المبحوث عنه في النواة.

أحتاج أيضًا إلى عناوين النواة للدوال التالية المُصدَّرة من CLFS.sys:
• ClfsEarlierLsn
• ClfsMgmtDeregisterManagedClient
والدوال المُصدَّرة من NTOSKRNL.exe
• RtlClearBit/PoFxProcessorNotification
• SeSetAccessStateGenericMapping
للحصول على هذه العناوين تُستخدم طريقة مشابهة لتلك المستخدمة للحصول على قاعدة النواة لكلا الوحدتين، عبر استدعاء NtQuerySystemInformation مرتين، لكن في هذه الحالة ستكون الوسيطة الأولى SYSTEM_INFORMATION_CLASS (في الإثبات PoC نستخدم دالة FindKernelModulesBase لهذا الغرض).
ثم يقوم بتحميل CLFS.sys
وNTOSKRNL.exe كوحدات عادية في وضع المستخدم عن طريق استدعاء
LoadLibrary، ويحصل على العناوين في وضع المستخدم باستخدام
GetProcAddress، ثم يطرح قاعدة الصورة من كل منهما، مما يحصل
على إزاحة الدالة، وأخيرًا يضيف كل إزاحة إلى قواعد النواة
المقابلة وبذلك يحصل على عناوين النواة لجميع الدوال الضرورية.

أنشئ دالة تسمى createInitialTriggerBlfFile ستقوم بتوليد وكتابة ملف .blf.
المسار المستخدم كوسيطة في CreateLogFile يختلف عن المسار العادي، على سبيل المثال لفتح الملف 1280.blf الموجود في مجلد C:\Users\Public، يجب تعيين المسار LOG:C:\Users\Public\1280. سيتم حفظ ذلك في المتغير stored_name_CreateLog.
أقوم بذلك باستخدام wsprintfW() لأن stored_env يخزن المسار C:\Users\Public، الذي تم الحصول عليه مسبقًا من متغيرات البيئة. إلى هذه السلسلة سأضيف في البداية السلسلة LOG: واسمًا عشوائيًا في النهاية، بدون امتداد .blf.

سيكون هذا هو المسار إلى ملفي الأولي الذي سأسميه "trigger blf". بالطبع، يجب أيضًا حفظ المسار العادي لنفس الملف بدون LOG: في المقدمة ومع امتداد BLF لفتحه وتعديله باستخدام CreateFile() وWriteFile() مثل أي ملف آخر، سيكون هذا المسار مثلًا: C:\Users\Public\1280.blf، وسيتم تخزينه في المتغير stored_name_fopen.

بالطبع، كلا المسارين يشيران إلى الملف نفسه، ويجب استخدام أحدهما أو الآخر وفقًا للسياق.
تقوم دالة CreateLogFile بوظيفة مشابهة جدًا لدالة CreateFile() (إنشاء ملفات جديدة أو فتح ملفات موجودة والحصول على مقبضها)، حتى أن بعض الوسائط متشابهة، لكن CreateLogFile() تعمل فقط مع ملفات blf.
بالإضافة إلى ذلك، عند فتح ملف موجود، تتحقق من أن التنسيق سليم، حتى أن كل كتلة تحتوي على مجموع اختباري وإذا لم يكن صحيحًا فستُرجع خطأً.
سأنشئ نوعين من ملفات BLF:
ملف Trigger blf
ملف Spray blf
كلاهما ملفان blf لكن تم تعديلهما بطريقة مختلفة.
بهذه الطريقة، يقوم الإثبات (PoC) أولاً بإنشاء ملف
"trigger blf" باستخدام CreateLogFile، مع المسار مثلًا:
LOG:C:\Users\Public\1280 الذي قمت بإعداده قبل،
وتم تخزينه في المتغير stored_name_CreateLog.
الوسيطة الخامسة fCreateDisposition، كما في CreateFileA()، يمكن أن تأخذ القيم التالية:

في هذه الحالة سأستخدم الوسيطة OPEN_ALWAYS، لذلك سيتم إنشاء الملف إذا لم يكن موجودًا، وإذا كان موجودًا فسيتم فتحه. وبما أن الملف غير موجود بعد، فسيتم إنشاؤه باسم عشوائي.
logFile = CreateLogFile(stored_name_CreateLog, GENERIC_READ | GENERIC_WRITE, 1, 0, 4, 0);
ستقوم CreateLogFile() بإنشاء ملف "trigger blf" الخاص بنا مع كتلته الست ومجاميعها الاختبارية المقابلة وستُرجع المقبض الذي سيتم تخزينه في المتغير logFile.

كل كتلة ستحتوي، بدءًا من الإزاحة الموضحة في العمود الأيسر، على ترويسة حجمها 0x70 بايت.
لذلك، على سبيل المثال، ترويسة CONTROL BLOCK تمتد من الإزاحة 0x0 إلى 0x70.

جميع ترويسات كل الكتل لها البنية نفسها المسماة _CLFS_LOG_BLOCK_HEADER.
هذه هي بنية الترويسة:

عند الإزاحة 0xC من الترويسة يمكنني العثور على المجموع الاختباري، وبما أن CONTROL BLOCK يبدأ عند الإزاحة 0، فسيكون المجموع الاختباري عند الإزاحة 0xC من الملف، وبالتالي سيكون لكل كتلة مجموعها الاختباري عند 0xC من بداية كتلتها.

لتعديل ملف trigger blf، يجب فتحه كملف عادي إما باستخدام CreateFileA أو fopen ثم تعديله باستخدام WriteFile أو fwrite على التوالي. أقوم بذلك في بداية دالة fun_prepare في الإثبات (PoC).
تذكر أن المسار العادي مخزّن في المتغير stored_name_fopen، لذلك أستخدمه لفتح الملف باستخدام wfopen_s (وهو متغير من fopen يدعم سلاسل Unicode).
يتم تعديل الملف في دالة craftTriggerBlfFile التي تُستدعى من fun_prepare.

ثم أستدعي fseek للإشارة إلى الإزاحة المطلوب تغييرها ثم باستخدام fwrite يتم تعديل الملف.
التعديلات التي يجب إجراؤها على ملف
"trigger blf" هي كما يلي:
بعد إجراء هذه التعديلات، يتم استدعاء FixCRCFile لحساب المجموع الاختباري الجديد وإصلاح المجاميع الاختبارية للكتل الأربع الأولى. الكتلتين التاليتين لا تحتويان على أي تغييرات، لذلك ليس من الضروري إعادة حساب مجاميعهما الاختبارية.

يقوم برنامج تشغيل CLFS.sys بقراءة الكتل الست للملف، ولتخزين محتواها يقوم بتخصيص ذاكرة في تجمع النواة (Kernel pool).

هناك بنية مهمة جدًا بحجم 0x90، وقد وجدت بعض الحقول فيها عبر الفحص العكسي في التدوينة السابقة حول CVE-2022-37969، وأطلقت عليها اسم pool_0x90. بعد قدر أكبر من الفحص العكسي، أعرف الآن أن اسمها الحقيقي هو m_rgBlocks، وبينما تقوم وحدة التحكم بتخصيص ذاكرة لنسخ محتويات كل كتلة من الملف، تحفظ هناك حجم كل كتلة وإزاحة البداية وعنوان النواة حيث تم تخزينها.

تحتوي على ست بنى CLFS_METADATA_BLOCK تتوافق مع كل كتلة برقمها.
طول كل بنية CLFS_METADATA_BLOCK هو 0x18 بايت. (0x18*6=0x90)
عند الإزاحة 0 يوجد اتحاد (union)، لكن على الأقل
في هذا الاستغلال يُستخدم الحقل pbImage فقط، لذا بتبسيطها ستكون:
يمكن أن يتم تخصيص تلك البنية من موضعين مختلفين في برنامج تشغيل CLFS.sys، اعتمادًا على إنشاء ملف جديد أو فتح ملف موجود. في حالة إنشاء ملف جديد، يقوم برنامج التشغيل بتخصيص 0x90 بايت من CClfsBaseFilePersisted::CreateImage+28A، بينما في حالة ملف موجود فإنه يخصص من CClfsBaseFilePersisted: ReadImage+6E.
بعد ذلك، سأحصل على عنوان بداية الكتلة 2 التي تتوافق مع ملف trigger blf، وتسمى BASE BLOCK وتبدأ عند الإزاحة 0x800 وطولها 0x7a00.

داخل دالة fun_prepare أدناه سيتم العثور على هذا العنوان في النواة باستخدام هذه القطعة من الكود.

أولًا، تقوم دالة getBigPoolInfo بالعثور على جميع التخصيصات في التجمع التي تحمل وسم "Clfs" وحجم 0x7a00، ثم تخزنها في مصفوفة.
بعد ذلك، يفتح مرة أخرى ملف trigger blf المعدّل سابقًا باستخدام CreateLogFile مع الوسيطة OPEN_EXISTING، فيفتح ملفًا موجودًا، وسيؤدي ذلك إلى تخصيص الكتلة الأساسية BASE BLOCK الخاصة به.
عند استدعاء getBigPoolInfo مرة أخرى، سيكون هناك تجمع “Clfs” جديد واحد بحجم 0x7a00، ويتم استرجاع عنوانه عبر استدعاء NtQuerySystemInformation مرتين.
يتم تخزين عنوان BASE BLOCK لملف trigger blf في المتغير CLFS_kernelAddrArray.

لاحظ أنه إذا لم يكن ملف trigger blf المعدّل يحتوي على المجموع الاختباري الصحيح، فستفشل دالة CreateLogFile().
الجزء الأخير من دالة fun_prepare يستدعي واجهة AddLogContainer باستخدام مقبض ملف trigger blf.

في آخر دالة من الإثبات (PoC) تسمى to_trigger، سيتم إنشاء نوع ثانٍ من ملفات blf،
سأسميه spray blf.
سيُستخدم هذا النوع من الملفات لملء مساحة ذاكرة (رش)،
نحتاج إلى 10 ملفات من هذا النوع، لكن يتم إنشاء ملف واحد فقط في البداية.

سيتم إنشاء ثلاث مصفوفات لتخزين الأسماء العشوائية لهذه الملفات:
stored_log_arrays: تخزن عشرة أسماء عشوائية جديدة لملفات .blf التي ستُستخدم مع CreateLogFile.
stored_container_arrays: تخزن أسماء عشوائية لإنشاء عشرة ملفات حاوية جديدة.
stored_fopen_arrays: تخزن أسماء ملفات السجل من المصفوفة الأولى (المتغير stored_log_arrays)، لكن مع مسارها العادي (بدون السلسلة “LOG:”) ومع امتداد .blf.

في كل تكرار يتم نسخ ملف blf باستخدام CopyFileW، ويتم تعيين الأسماء المخزنة في المصفوفات.
دالة fun_trigger تستدعي craftSprayBlfFile حيث يتم إجراء التعديلات على كل ملف، وسيقوم FixCRCFile بإصلاح مجاميع CRC.

باختصار، لقد أنشأت 10 ملفات متشابهة (spray blf) بأسماء عشوائية مع التعديلات التالية:

التعديل الأخير هو نسخ الكتلة 0 بالكامل (CONTROL BLOCK) إلى الكتلة 1 (CONTROL BLOCK SHADOW)

سيتم شرح تأثير هذه التعديلات، بالإضافة إلى تلك التي أُجريت على ملف trigger blf، لاحقًا في فصل التصحيح.
بعض هذه التعديلات هي التي تُحدث الثغرة، بينما البعض الآخر ضروري فقط لتجاوز فحوصات برنامج التشغيل.
عند هذه النقطة تكون الملفات قد أُنشئت وعُدّلت بالفعل، وجاهزة لتنفيذ الرش، فعند فتحها باستخدام CreateLogFile ستتوضع في منطقة الذاكرة التي نريدها، كما سيظهر لاحقًا.

في دالة to_trigger، يتم إنشاء مصفوفة من 12 عنصرًا، تحتوي على عنوان BASE BLOCK لملف trigger blf زائد 0x30.
ثم في دالة fun_pipeSpray، تمتلئ الذاكرة برش من الأنابيب، وفي داخلها حلقة تستدعي CreatePipe وتنشئ عدد الأنابيب الذي يُمرَّر كوسيطة أولى، والوسيطة الثانية هي مصفوفة ستخزن مقابض جميع الأنابيب التي تم إنشاؤها.

ضمن حلقة، يستدعي CreatePipe
لإنشاء أنابيب قراءة-كتابة.
بهذه الطريقة، سيتم أولاً إنشاء 0x5000 أنبوب
ثم يتم الاستدعاء مرة أخرى لإنشاء 0x4000 أنبوب إضافي.
ثم يستخدم WriteFile لكتابة المصفوفة التي تم إنشاؤها مؤخرًا إلى أول 5000 أنبوب، والتي تحتوي على عناوين BASE BLOCK + 0x30 لملف trigger blf.

الآن لديه كتلة متراصة أُنشئت في الذاكرة، سيحرر 0x667 أنبوبًا من الرقم 0x2000 وحتى 0x2667، ونظرًا لأن الأنابيب في الذاكرة ليست بالترتيب نفسه الذي أُنشئت به، فإن ما سيحدث هو ظهور مساحات فارغة في هذه الكتلة من الذاكرة.
لاحظ أن تخصيصات الأنابيب لها حجم مستخدم
يبلغ 0x90 بايت، لذلك عند تحريرها سيكون لدينا
إنه يحرر مساحات الذاكرة بحجم 0x90 بين الذاكرة الممتلئة
بالأنابيب.
ثم يكرر الاستدعاء لـCreateLogFile مع ملفات spray blf
العشرة.
عند استدعاء CreateLogFile لفتح ملفات موجودة، يتم تنفيذ
تخصيص 0x90 بايت لبنية m_rgBlocks واحدة لكل ملف
spray blf، لذلك ستشغل هذه التخصيصات الفجوات التي
تُركت عند تحرير الأنابيب لأنها بنفس الحجم.

ثم يكرر عملية الكتابة في آخر 0x4000 أنبوب للمصفوفة التي تحتوي على عنوان BASE BLOCK +0x30 لملف trigger blf.
كل هذه التلاعبات تُنشئ مساحة ذاكرة خاضعة للتحكم، وسأريك كيف تكون عند تصحيح الأخطاء، لكن الفكرة هي أن بنى m_rgBlocks لكل ملف spray blf تشغل فجوات الـ 0x90 بايت التي تم تحريرها.
ثم في الجزء الأخير، يتم استثارة الخطأ داخل حلقة while( 1 )
باستخدام استدعاء AddLogContainer لملفات spray blf.
داخل حلقة while هذه يتم تشغيل الثغرة:

ستخرج حلقة while هذه عندما تجد رمز System المميز (token)، باستخدام دالة NtFsControlFile التي ستقرأ خصائص الأنابيب (pipes).

ثم باستخدام CreateLogFile، يتم مرة أخرى استبدال رمز العملية الخاص بنا برمز System المميز الذي تم العثور عليه مؤخرًا، وبهذه الطريقة نحقق رفع الصلاحيات.

ثم نقوم باستعادة بعض القيم، وإغلاق مقابض (handles) الأنابيب وملفات blf، وتشغيل Notepad كـ System للتحقق من أننا رفعنا الصلاحيات بشكل صحيح.

لاحظ ملفات blf التي تم إنشاؤها في مجلد PUBLIC. تذكر أنه إذا أردت المحاولة مرة أخرى، يجب عليك أولاً حذف الملفات التي تم إنشاؤها. بعضها سيكون مقفلاً ولا يمكن حذفه، لكن الـ PoC سيظل يعمل.

قبل أن أبدأ بتأثير التغييرات على ملفات trigger blf و spray blf لتنفيذ الاستغلال، يجب أن أتحقق من أن m_rgBlocks لملفات spray blf files موجودة في الفجوات التي تحدث في توزيع الذاكرة، بعد تنفيذ حقن الأنابيب (pipe spray) والإطلاق اللاحق لعدد ثابت من الأنابيب.
عندما ينتهي هذا الإجراء، يجب أن يكون هناك أنبوب موجود أسفل بايتات الـ 0x90 الخاصة بـ m_rgBlocks، لذلك عند استخدام m_rgBlocks، سيحدث OUT OF BOUNDS وسيقرأ من ذلك الأنبوب الموجود بالأسفل.
يحتوي الـ PoC على نقطة مثالية لوضع نقطة توقف (breakpoint):

عند هذه النقطة، يكون فتح ملفات spray blf قد اكتمل ولم يتم استدعاء دالة AddLogContainer بعد.
لتصحيح الأخطاء في وضع المستخدم، سأستخدم x64dbg، وفي وضع النواة، IDA مع إضافة Windbg.

عند هذه النقطة يجب أن تكون الذاكرة جاهزة بالفعل، ويمكنني رؤية التوزيع.
سأوقف IDA مؤقتًا للعثور على نقطة مثيرة للاهتمام لوضع نقطة توقف.
سأضع نقطة توقف عند CClfsBaseFilePersisted::AddContainer، والتي يتم استدعاؤها من AddLogContainer وفي البداية، يشير سجل RCX إلى بنية CClfsBaseFilePersisted وعند الإزاحة 0x30 يوجد مؤشر إلى m_rgBlocks.

عند الوصول إلى نقطة التوقف، أتحقق في مكدس الاستدعاءات (call stack) من أن AddLogContainer يتم استدعاؤها من الـ PoC الخاص بي.

سجل RCX يشير إلى:

الحقل الأول هو المؤشر إلى vtable (CLFS! CClfsBaseFilePersisted::'vftable') وعند الإزاحة 0x30 يوجد المؤشر إلى m_rgBlocks.

الكتل 0 و1 و4 و5 لم تقم بحفظ pbImage بعد، بينما الكتلتان 2 (BASE BLOCK) و3 (SHADOW BLOCK) قامتا بذلك.
كل كتلة في جدول m_rgBlocks لها cbOffset وهو الإزاحة التي تبدأ عندها الكتلة في الملف، وcbImage هو حجم الكتلة، وeBlockType هو نوع الكتلة.
إذا كان الحقن صحيحًا، فيجب أن يكون هناك أنبوب أسفل m_rgBlocks وبداخله المؤشرات إلى BASE BLOCK + 0x30 الخاص بـ trigger blf.

يعرض أمر "!pool" في windbg توزيع الذاكرة:

كل m_rgBlocks له وسم "Clfs” وحجمه 0xa0 لأنه حجم المستخدم 0x90 بالإضافة إلى ترويسة 0x10، وتحته يوجد أنبوب بوسم "NpFr" له نفس حجم المستخدم 0x90
نظرًا لأن التوزيع ليس علمًا دقيقًا، فقد تم وضع بعض "Clfs” بشكل متواصل، وهو أمر غير مرغوب فيه، لكن الذي أعمل عليه موضوع بشكل صحيح ويتبعه أنبوب.
من أول التغييرات المؤثرة هو التغيير الذي تم في ملف trigger blf عند الإزاحة 0x858، حيث يتم تخزين القيمة 0x369.

تبدأ BASE BLOCK عند الإزاحة 0x800 في الملف.

داخل _CLFS_LOG_BLOCK_HEADER عند الإزاحة 0x800+0x58 (0x58 من بداية ترويسة BASE BLOCK).

عند الإزاحة 0x28 تبدأ مصفوفة RecordOffsets (DWORD).
بالتحرك 0x30 بايت إلى الأمام، عند الإزاحة 0x58 (0x828+0x30=0x858 من البداية)، يوجد الحقل 12 من RecordOffsets.

أقوم بتشغيل الـ PoC حتى CreateLogFile كما هو موضح في الصورة أدناه:


قبل أن أدخل إلى CreateLogFile سأضع نقطة توقف في مكان لم يتم استخدام القيمة 0x369 فيه بعد.
في حالة قيام CreateLogFile بفتح ملف موجود، يتم تخصيص بنية m_rgBlocks هنا:
CClfsBaseFilePersisted::ReadImage+6E
لذا، سأضع نقطة توقف في IDA هنا تمامًا:

عند تفعيل نقطة التوقف**:**

في m_rgBlocks لا تزال هناك بعض البيانات العشوائية (garbage) لأنه غير مهيأ بعد، ولكن بمجرد تخصيص pbImage للكتلة 2، سيتم حفظ العنوان في الإزاحة 0x30 من البداية، نظرًا لأن الحقل الأول داخل كل CLFS_METADATA_BLOCK هو pbImage.


الآن أضع نقطة توقف عتادية (hardware breakpoint) على الكتابة: ba w1 ffffd003'7f5bea30
بعد التهيئة إلى صفر، يتوقف عندما يحفظ pbImage.

يقول التحليل أنه يتوافق مع block0، لأنه لا يأخذ في الاعتبار الثابت r14*8 الذي يساوي 0x30 بعد ذلك، ونتيجة لذلك فهو في الحقيقة يكتب pbImage الخاص بـ block 2.

لاحظ أن CClfsBaseFilePersisted::ReadMetadataBlock يُستخدم لتخصيص أي من الكتل، باستخدام الحجم الذي تم تمريره كوسيط.

الآن أضع نقطة توقف قراءة/كتابة عند 0x58 من الكتلة الأساسية (base block)، لمعرفة متى يستخدم القيمة 0x369.
ba r1 FFFF978A'16ECF000+0x58

عند الوصول إلى نقطة التوقف، يقرأ القيمة 0x369 الموجودة في RecordOffset[12]، ويضيفها إلى مؤشر غريب على r14 ويزيد محتويات RAX+r14.
بضعة أسطر أعلاه في الكود، ESI لديه القيمة 0x13 ويضربها في 0x18، وهو حجم كل كتلة في m_rgBlocks.
WINDBG>? 0x18*0x13
Evaluate expression: 456 = 00000000'000001c8
إذا أضفت قيمة r8= 0x1c8 التي هي أكبر من 0x90، إلى العنوان الأولي لـ m_rgBlocks، فسيكون يقرأ OUT OF BOUNDS.


أسفل m_rgBlocks، يوجد الأنبوب الذي يحمل المؤشر إلى BASE BLOCK + 0x30، ويقرأ هذا المؤشر الذي تم وضعه بشكل استراتيجي داخل الأنبوب.
الموضع الحالي في
الكود تم استدعاؤه من عبارة while(1) الخاصة بالوحدة الرئيسية.
داخل ملف spray blf قمت بوضع القيمة 0x13 بشكل استراتيجي عند الإزاحة 0x48a (iFlushBlock).

عند الإزاحة 0x8a من ملف spray blf، يوجد iFlushBlock الخاص بـ BLOCK 0، وقيمته 4، بينما الإزاحة 0x48a تخص iFlushBlock الخاص بـ BLOCK 1، وقيمته 0x13

الآن يجب أن أعرف لماذا يقرأ iFlushBlock = 0x13 من BLOCK 1 بدلاً من iFlushBlock = 4 من BLOCK 0.
إذا نظرت إلى الوراء لمعرفة من أين جاءت 0x13، أرى في مكدس الاستدعاءات أن WriteMetadataBlock يتم استدعاؤها من CClfsBaseFilePersisted::ExtendMetadataBlock+416، وهناك الوسيط الثاني iFlushBlock هو EDX=0x13، والذي يأتي من r9w.


قبل سطرين،
تم استدعاء CClfsBaseFile::GetControlRecord لاسترجاع عنوان
BLOCK 0، ربما تكون المشكلة هنا، لذا سأعيد التشغيل وأضع
نقطة توقف عليه.
تستدعي GetControlRecord الدالة CClfsBaseFile::AcquireMetadataBlock التي يجب أن تملأ جدول m_rgBlocks بعنوان block 0، وعندما أتخطى هذه الدالة تحصل على عنوان block 1، لذا، تحدث المشكلة داخل CClfsBaseFile::AcquireMetadataBlock.
بإضافة 0x8A إلى العنوان المسترجع، يمكنني تأكيد أن القيمة 0x13 التي تخص BLOCK 1 موجودة.

سأعيد التشغيل وأضع نقطة توقف هناك:

CClfsBaseFile::GetControlRecord+27 call CClfsBaseFile::AcquireMetadataBlock

الوسيط الثاني الذي تم تمريره إلى AcquireMetadataBlock هو صفر، وهو
يتوافق مع block 0، وسيقوم بالنسخ من الملف وتخزين
عنوانه في m_rgBlocks
.
في تعداد نوع الكتلة _CLFS_METADATA_BLOCK_TYPE، لديهم أسماء مختلفة عن تلك التي استخدمتها، لكنها نفس الكتل الست.

بعد التحقق من أن نوع الكتلة أقل من الحد الأقصى m_cBlocks=6، يقوم بحفظ قيمة مرجعية (reference) لتجنب قراءة نفس الكتلة مرتين.

يتم استدعاء ReadMetadataBlock، مشكلة قراءة block 1 بدلاً من block 0 ستكون داخل هذه الدالة.

إذا كان كل شيء على ما يرام، فإنه يخصص باستخدام cbImage كحجم ويخزن العنوان في الحقل block 0-> pbImage في m_rgBlocks.

يعرض أمر !pool الوسم (tag) والحجم المخصص.

لذا، لدي بالفعل عنوان
pbImage الخاص بـ block 0 مخزن في m_rgBlocks، لذلك
أحتاج إلى معرفة لماذا ينسخ بايتات block 1 هناك بدلاً من
بايتات block 0.
أصل إلى استدعاء CClfsContainer::ReadSector حيث يتم تمرير مؤشر إلى متغير يحتوي على pbImage، لكتابة البايتات.

لاحظ التغييرات التي حدثت في محتوى pbimage عند تخطي ReadSector.

بإضافة 0x8a إلى pbImage يمكنني العثور على القيمة 4 وهي القيمة الصحيحة، بدلاً من 0x13، لذا يجب أن تحدث المشكلة لاحقًا.
بعد استدعاء ClfsDecodeBlock فإنها ترجع خطأ 0x0C01A000A.
CClfsBaseFilePersisted::ReadMetadataBlock+153 يستدعي ClfsDecodeBlock
بعد هذا الخطأ، يضيف 1 إلى النوع ويستدعي CClfsBaseFilePersisted::ReadMetadataBlock مرة أخرى ولكن بالنوع 1 لقراءة block 1.

في CClfsBaseFilePersisted::ReadMetadataBlock يقوم بتخصيص وتخزين pbImage جديد في m_rgBlocks لـ block 1.

الكتلتان 0 و1 لهما عناوين مختلفة، الآن إذا أضفت 0x8a إلى عنوان block 1 فإن قيمته تكون 0x13.
ربما بما أن block 0 أرجع خطأً، فإنه يستخدم block 1 ويعيده إلى GetControlRecord ككتلة تحكم (Control Block).
كما هو موضح سابقًا، عندما يستخدم القيمة 0x13 بدلاً من 4، فإنه يخرج خارج حدود m_rgBlocks ويقرأ قيم حقن الأنابيب (pipe spray) التي أتحكم بها.
ثم يحرر pbImage من block
0 وينسخ المؤشر من block 1 إلى block 0.

سيكون من الضروري العثور على القيمة التي تسبب الخطأ 0x0C01A000A داخل ClfsDecodeBlock.
داخل ClfsDecodeBlock، المجموع الاختباري (checksum) للكتلة الأولى هو صفر، وهذا هو الخطأ 0xC01A000A.

قبل استدعاء AddLogContainer، عند فتح أي ملف spray blf بمحرر سداسي عشري، تم تغيير المجموع الاختباري إلى صفر.

كان يجب أن يتغير من قبل عندما تم فتحه باستخدام CreateLogFile.

لسبب ما، تنتهي ملفات spray blf بعد الخروج من CreateLogFile بمجموع اختباري للـ block 0 يساوي 0 وتُرجع مقبضًا (handle) صالحًا، دعنا نرى لماذا يحدث هذا.
أتوقف عند CreateLogFile قبل فتح بعض ملفات spray blf.

لاحظ أنه قبل استدعاء CreateLogFile، تحتوي ملفات spray على المجموع الاختباري الصحيح في block 0 وبعد اكتمال الدالة، تتغير قيمة المجموع الاختباري إلى صفر.

لذا، أضع نقطة توقف على CClfsBaseFile::GetControlRecord، للنظر إلى الداخل.
بعد تجاوز
CClfsContainer::ReadSector، المجموع الاختباري ليس صفرًا.
قبل الدخول لحساب CRC32، يقوم بوضع حقل المجموع الاختباري على صفر في الذاكرة لحساب CRC، والنتيجة صحيحة.


ثم يتحقق من قيمة eExtendState =2 ويذهب إلى WriteMetadataBlock.

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

يتحقق من بعض القيم التي تمت هندستها في ملف blf spray للوصول إلى CClfsBaseFilePersisted::ExtendMetadataBlock.

بعد حلقة لقراءة الكتل التي لم تتم قراءتها بعد، يستمر block 0 بـ checksum = 0.

الوصول إلى WriteMetadataBlock.

بما أنني أقوم بالتشغيل قبل أن يستبدل block 0 بـ 1، فإن قيمة iFlushBlock لملف blf spray لا تزال 4 القيمة الصحيحة.

الآن يعمل مع block 4، وسيكتب block 4 في الملف، هنا ليست المشكلة بعد.
ثم يصل إلى CClfsBaseFilePersisted::FlushControlRecord

بداخله يصل إلى WriteMetadataBlock، ولكن مع الوسيط 0، لكتابة block 0 إلى الملف**.

ثم تُرجع ClfsEncodeBlock الخطأ 0xC01A000A، على الرغم من أنها ستكتب الملف بـ block 0 السيئ في CClfsContainer::WriteSector، في الأسفل مباشرة.

يقوم المتغير var_54 بتخزين قيمة الخطأ 0xC01A000A وسيتم التحقق منه قبل الخروج من الدالة.

ولكن بعد استدعاء CClfsContainer::WriteSector التي لا تُرجع أي خطأ، يتم استبدال محتوى var_54 بصفر.
لذا، تُرجع الدالة صفرًا بدون خطأ وتستمر في العمل نظرًا لأن CreateLogFile سيعيد مقبضًا (handle) بدلاً من قيمة خطأ.

القيمة 0x13 في iFlushBlock تجعله يخرج عن الحدود (out of bounds) و سيقرأ المؤشر الموجود في الأنابيب الذي يشير إلى Base Block +30 الخاص بـ trigger blf.
ثم يضيف 0x28 إلى ذلك المؤشر، (0x58 من بداية الكتلة الأساسية لـ
trigger blf) التي لها القيمة 0x369.


تعليمة INC ستزيد القيمة 0x14 بمقدار 1 وتتكرر 4 مرات، لذا تنتهي 0x14 لتصبح 0x18.
WINDBG>db r14+369
ffffcb82'091e7397 14 00 00 00
بعد ذلك، يتم استدعاء CreateLogFile، ويقرأ القيمة 0x1858.

تتحقق GetSymbol مما إذا كانت الكتلة المزيفة
المُنشأة سابقًا في trigger blf، والمشار إليها بالإزاحة 0x1858،
تحتوي على القيم الصحيحة.

لو لم تتم زيادة المؤشر عدة مرات، لكانت له القيمة الأصلية 0x1458 وسيشير إلى الكتلة الصحيحة.
بعد الخروج من GetSymbol، سيستخدم تلك الكتلة المزيفة هنا.

ثم سيقرأ قيمة الإزاحة 0x18
من الكتلة المزيفة حيث أضع 0x05000000 ويقفز إلى محتوى ما
هو موجود هناك.
WINDBG>dps 0x5000000
00000000'05000000 00000000'05001000

يقرأ محتوى 0x05000000 ومحتوى 0x05001000 وهناك يوجد ClfsEarlierLsn.

تُستخدم هذه الدالة لإرجاع القيمة 0xFFFFFFFF في RDX على الرغم من أن هذه القيمة لا تُستخدم في المرة الأولى.
المكالمة الثانية تحدث هنا، حيث تستدعي PoFxProcessorNotification التي كانت على 0x501000 +8

WINDBG>dps 00000000**'05001000**
00000000'05001000 fffff805'7ab13220 CLFS! ClfsEarlierLsn
00000000'05001008 fffff805'769dc3b0 nt! PoFxProcessorNotification

في هذه الدالة RCX = 0x05000000، تتحقق من أن البايتات بعد 0x40 يجب أن تكون غير صفرية
WINDBG>dps rcx+40
00000000'05000040 00000000'05000000
عنوان القفز سيكون بعد 0x68.
WINDBG>dps rcx+68
00000000'05000068 fffff805'7ab2bfb0 CLFS! ClfsMgmtDeregisterManagedClient
وستكون الوسيطة بعد 0x48 بايت.
WINDBG>dps rcx+48
00000000'05000048 00000000'05000400
ClfsMgmtDeregisterManagedClient هي دالة ملائمة، لأنني أستطيع التحكم في الوسيطة، ولدي أيضًا قفزتان إلى دوال أتحكم فيها.

المكالمة الأولى هي مرة أخرى إلى ClfsEarlierLsn التي أعادت في RDX=0xFFFFFFFF.


سيأخذ مصدر الكتابة من محتوى RDX=0xFFFFFFFF.
WINDBG>dps rdx
00000000'ffffffff ffff8005'3a4ee000
في العنوان 0xFFFFFFFF كنت قد خزنت system_EPROCESS & 0xfffffffffffff000.

الوجهة هي المؤشر الموجود عند 0x5000400 +0x48
*(UINT64*)0x5000448 = para_PipeAttributeobjInkernel + 0x18;

مؤشر PipeAttribute في النواة الذي يشير إلى مخزن مؤقت مملوء بالحرف “A” سيتم استبداله بالجزء العلوي من مؤشر SYSTEM EPROCESS.
تم إنشاء هذا المؤشر عندما قمت سابقًا باستدعاء _NtFsControlFile مع مخزن مؤقت مليء بالحرف “A”.

يمكن قراءة محتوى تلك السمة باستخدام NtFsControlFile.

الآن سمة الأنبوب لم تعد تشير إلى المخزن المؤقت الذي يحتوي على “A” بل إلى system_EPROCESS & 0xffffffffffffff000.

سيتم تكرار هذا الكود حتى يتم استرداد رمز النظام.


في ويندوز 11، يكون رمز النظام عند الإزاحة 0x4b8 من بنية EPROCESS التي تمت قراءتها مؤخرًا.

أحتاج فقط إلى كتابة رمز النظام هذا في عمليتي عن طريق استدعاء CreateLogFile.

للقيام بهذه المهمة، فقط كرر الخطوة المستخدمة لقراءة رمز النظام.

في المكالمة المزدوجة، يستدعي أولاً ClfsEarlierLsn لإرجاع 0xFFFFFFFF في RDX ثم يستدعي nt_SeSetAccessStateGenericMapping.

أتحقق من أن القيمة المشار إليها بواسطة RDX هي رمز النظام.

رمز عمليتي هو:

سوف يقوم بالكتابة هناك.
WINDBG>dps rax+8
ffff9b8b'fc446578 ffffc402'f601c06c
WINDBG>dps rax+8
ffff9b8b'fc446578 ffffc402'ef841919
الآن عمليتي هي System، يمكنني تشغيل Notepad للتحقق.



يُظهر BINDIFF الكثير من الدوال المتغيرة.

الدالة الهشّة موجودة هنا:


الأساسي هو الإصدار المصحح، والثانوي هو الإصدار الهش.
يختبر التصحيح القيمة المرجعة من CflsEncodeBlock، وهي 0xC01A000A، ويخزنها في المتغير var_54، وبما أنها سالبة، يفحصها ويتجنب WriteSector.
التصحيح، بالإضافة إلى عدم كتابة الملف، تُرجع الدالة القيمة 0xc01a000a بشكل صحيح، وبهذا لا يُرجع CreateLogFile أي مقبض ولا يمكن أن يستمر الاستغلال.


فقط إذا لم تكن ClfsDecodeBlock سالبة، فإنها تنتقل إلى WriteSector لكنها تُبقي على إرجاع القيمة السالبة 0xC01A000A.
هذا هو التصحيح الفعلي الذي
يمنع الاستغلال حقًا باستخدام الـ PoC الذي أرفقته للتو.
عند هذه النقطة شرحنا كيفية استغلال الثغرة، فهي تؤدي إلى التحكم في الدوال التي تسمح لنا بقراءة رمز SYSTEM و كتابته في عمليتنا الخاصة لتحقيق تصعيد الامتيازات المحلي. يمكنك العثور على الـ PoC الوظيفي في GitHub الخاص بـ Fortra.
نأمل أن تجده مفيدًا، إذا كان لديك أي استفسار يمكنك التواصل معنا: