
درس تعليمي حول CVE-2022-37969 مع التركيز على منهجية استغلال النواة، وليس على الأسباب الداخلية لـ CVE
تم إنشاء هذا لتوضيح الجوانب العامة المتعلقة باستغلال ثغرات Windows. يشرح المفاهيم الأساسية المطبقة على CVE-2022-37969. النتيجة النهائية هي PoC عامل. لا يوضح كل جانب من جوانب الثغرة، لكنه يوفر قطعًا قابلة لإعادة الاستخدام من الكود ويشرح الآليات التي يمكن العثور عليها في العديد من الاستغلالات العامة.
المستخدم المستهدف هو مهندس هندسة عكسية مبتدئ، أو مطور استغلالات يبحث عن كود مصدري لبرهان مفهوم عامل لاختباره وفهم أساسيات Windows الداخلية. يوفر نقطة مرجعية لمزيد من التعلم.
المتطلبات: تصحيح أخطاء أساسي للنواة، هندسة عكسية أساسية، أساسيات Windows الداخلية، مهارات برمجة c/c++
البرنامج هو قطعة من الكود تعمل على جهاز. عمومًا، يستقبل البرنامج بيانات (مدخلات) ويجري حسابات باستخدام المدخلات ويولّد بيانات (مخرجات). معظم البرامج كتبها بشر، وبالتالي تحتوي على أخطاء. ينشأ الخطأ عن كود مصدري لم يُكتب بشكل صحيح (أراد المبرمج فعل شيء ما بالمدخلات، لكن الكود الناتج كان مختلفًا عن النتيجة المقصودة). تُصحح معظم الأخطاء قبل إطلاق المنتج، لكن يبقى بعضها. يحدث هذا لأن هناك أنواعًا مختلفة من الأخطاء، بعضها يصعب اكتشافه أكثر من البعض الآخر.
Windows هو برنامج كمبيوتر، كتبه بشر، وبالتالي يحتوي على أخطاء. لماذا هذا مهم؟ لأن أنظمة Windows يمكنها تشغيل برامج تتعامل مع بيانات حساسة مثل الحسابات المصرفية وقواعد بيانات الرعاية الصحية وغيرها. يمكن استخدام بعض الأخطاء للوصول بشكل غير قانوني إلى بيانات مقيدة (هذه حالة استخدام جيدة للاستغلال).
هناك أنواع متعددة من الأخطاء، بعضها مفيد وبعضها ليس كذلك. عمومًا، تنشأ الأخطاء من مدخلات تُعطى للبرنامج، وبالتزامن مع سطور الكود التي كُتبت بشكل خاطئ، تولّد مخرجات أو سلوكًا غير سليم للبرنامج. إيجاد تلك المدخلات هو وظيفة أخصائي الأمن (أو الهاكر). الخطوة التالية هي تقييم المخرجات/السلوك غير السليم الناتج والإجابة على السؤال: «هل يمكن استخدامه بطريقة مفيدة؟». هنا تُصنف الأخطاء إلى فئات مختلفة. على سبيل المثال، قد يولّد خطأ سلوكًا يفسد بعض هياكل البيانات ويؤدي إلى إعادة تشغيل الكمبيوتر المستهدف. فائدته محدودة. قد يتسبب خطأ ما في كتابة المدخلات في منطقة ذاكرة تتحكم في أذونات الوصول إلى الملفات المقيدة. هذا النوع من الأخطاء أكثر فائدة.
إذن، من مجموعة كل الأخطاء الممكنة، يبحث الهاكر عن المجموعة الفرعية الأكثر فائدة لغرضه. بشكل عام، المشكلة هي: «هل يمكنني إعطاء البرنامج المستهدف مدخلات مصممة خصيصًا بحيث لا أكسر النظام ولكن يمكنني رفع مستوى وصولي والاستفادة؟»
بعد هذه المقدمة غير التقنية، يمكن صياغة نطاق البرنامج التعليمي: هل يمكننا العثور على برنامج Windows يقبل مدخلات غير سليمة، ونتيجة لرمز مطور خاطئ، يمكنه رفع أذوناتنا بشكل غير قانوني من مستخدم عادي إلى مسؤول؟
البرنامج المستهدف: Windows CLFS (Common Log File System Driver)
اسم الاستغلال: CVE-2022-37969
النوع: رفع امتيازات محلي (Local Privilege Escalation)
تحميل ISO القابل للاستغلال: حمّله من هنا
مساحة عناوين Windows مقسمة تقريبًا بين مساحة المستخدم (تشغيل البرامج العامة) ومساحة النواة (تشغيل نظام التشغيل نفسه وبرامج مكونات العتاد --> برامج التشغيل). لا يجب على المستخدم العادي الوصول إلى مساحة النواة، لكن هناك آليات يمكن من خلالها لبرامج المستخدم العادي الوصول إلى أجزاء من كود النواة (استدعاءات النظام، إجراءات برامج التشغيل). لماذا نحتاج إلى الوصول؟ للتفاعل مع نظام التشغيل بطريقة آمنة ومنضبطة، وهذا ما يوفره مصممو نظام التشغيل.
تستخدم بعض برامج التشغيل مدخلات بيانات يقدمها المستخدم للعمل على هياكل بيانات في مساحة النواة. إذا ولّد الإدخال خطأً، فقد تتلف النواة. إحدى الحالات هي Common Log File System Driver. باستخدام بعض المدخلات الخاصة، يمكننا إجبار برنامج التشغيل على تغيير هياكل بيانات النواة التي تحتوي على مستوى وصول الامتيازات للمستخدم، والكتابة فوق المستخدم العادي بقيمة المسؤول.
ما الذي يجب تعديله لرفع الامتياز إلى صلاحيات المسؤول؟
نبدأ بالهدف النهائي في أذهاننا. يخزن Windows داخل هيكل بيانات في النواة يُسمى _EPROCESS معلومات لكل عملية تعمل على النظام. مثال على _Eprocess
من الحقول المهمة struct _EX_FAST_REF Token. هذا هيكل بيانات آخر يشير بدوره إلى بيانات تتعلق بمستوى امتياز تلك العملية. في الصورة التالية، تمتلك عملية System رمز نظام (system token)، وعملية Explorer تمتلك رمز مستخدم عادي.

إذن، لرفع امتياز Explorer.exe نحتاج إلى نسخ القيمة من System's _EPROCESS-->Token إلى Explorer's _EPROCESS-->Token. سنحقق شيئًا مشابهًا عن طريق نسخ Token النظام إلى Token الخاص ببرنامجنا وتشغيل موجه أوامر من العملية المرفوعة الامتيازات (العمليات الفرعية ترث Token العملية الأب).
لإكمال هذه الإجراءات، نحتاج إلى آليات من أجل:
مقدمة: طبيعة Windows عبر السنوات: مع اكتشاف ثغرات جديدة، احتاج Windows إلى تصحيحات للتخفيف من حدتها. ومع ظهور تقنيات جديدة، احتاج Windows إلى تحديثات ليبقى قادرًا على المنافسة. كان أحد المتطلبات الحاسمة هو التوافق مع الإصدارات السابقة. وأحيانًا كان يتم تحقيق الأمن عبر الغموض (obscurity). أُزيلت هياكل البيانات وتعريفات الدوال من الأدلة، لكن الوظائف بقيت. من خلال الهندسة العكسية، تمكن الباحثون من استخدام تلك الوظائف لأغراض متنوعة.
للعثور على عنوان _EPROCESS في النواة، سنستخدم دالة غير موثقة: NtQuerySystemInformation (انظر الرابط للاطلاع على المعاملات). باستخدام معامل SystemInformationClass يمكننا تحديد نوع المعلومات التي نريد استرجاعها. سنسترجع معلومات عامة عن العمليات بتحديد قيمة SystemExtendedHandleInformation (#define SystemExtendedHandleInformation 0x40).
من المحاذير في استخدام NtQuerySystemInformation أننا لا نعرف مسبقًا طول البيانات التي سيتم إرجاعها، لكن NtQuerySystemInformation لديها آلية تساعد في ذلك. إذا تم استدعاؤها بمصفوفة ذات حجم خاطئ للبيانات المطلوبة، فإنها تُرجع ERROR والحجم الصحيح للبيانات الذي كان يجب طلبه. يمكن استخدام هذا لقراءة معلومات العملية بشكل صحيح بالطريقة التالية:
NtQuerySystemInformation بمعامل SystemInformationLength وهميReturnLength المُعادNtQuerySystemInformation مرة أخرى بقيمة SystemInformationLength الصحيحة التي تم إرجاعها سابقًابنية البيانات المُرجعة هي من النوع PSYSTEM_HANDLE_INFORMATION_EX. هذا هيكل بيانات غير موثق (انظر الرابط) ويقود إلى هيكل بيانات SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX، الذي يحمل في الحقل Object عنوان النواة لهيكل بيانات _Eprocess للعملية المقابلة.
إذن المنطق سيكون: التكرار عبر جميع عناصر PSYSTEM_HANDLE_INFORMATION_EX، ومقارنة حقل UniqueProcessId في SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX مع PID الخاص بالعملية المطلوبة، واختيار حقل Object المقابل للعثور على عنوان _Eprocess الخاص بها في النواة.
مقتطف من الكود:

يُعلَن NtQuerySystemInformation كمؤشر إلى دالة، ويتم الحصول على عنوانه ديناميكيًا في وقت التشغيل عبر loadlibrary و getprocaddress.
typedef NTSTATUS(WINAPI* ptr_NtQuerySystemInformation)(int, PVOID, ULONG, PULONG);ntdll=LoadLibrary(L"ntdll.dll");MyNtQuerySystemInformation = (ptr_NtQuerySystemInformation)GetProcAddress(ntdll, "NtQuerySystemInformation");لقراءة وكتابة Token، سيتعين علينا الاعتماد على ثغرات داخل clfsw32.sys و NamedPipes.
هذا يشبه إلى حد ما صندوقًا أسود، ومفصّل في مقالات أخرى، لكن سيتم شرح الحد الأدنى من المعرفة اللازمة لهذه الوظيفة من أجل اكتساب فهم أساسي لعملية الاستغلال. الأنابيب هي آليات اتصال بين العمليات. يمكن للعمليات تمرير المعلومات لبعضها البعض باستخدام الأنابيب. تُمثَّل الأنابيب كهياكل بيانات في النواة تحتوي على بعض الحقول التي يمكن ملؤها من مساحة المستخدم. مثال على ذلك سمات الأنابيب (Pipe Attributes).
(لاحقًا سنربط هذا بثغرة CLSF للحصول على قراءة/كتابة عشوائية من مساحة المستخدم إلى مساحة النواة)
لمزيد من القراءة، يرجى الاطلاع على fengshui-spraying-big-kids-pool. شرح مبسط للآلية سيكون:
لدى النواة طريقتان لتخصيص الذاكرة فيما يتعلق بالحجم الذي نريد تخصيصه: تجمع صغير (small-pool) للكائنات <4KB+header وتجمع كبير (big-pool) للكائنات >4KB+header. صفحات التجمع الكبير مهمة لأن يمكن تعدادها من مساحة المستخدم. هذا يعني أن المستخدم العادي يمكنه العثور على جميع عناوين بداية النواة التي تحتوي على صفحة تجمع كبير.
كيف؟ تحتوي كل صفحة تجمع كبير على حقل يُسمى Tag (يمكن استخدامه للحصول على معلومات حول نوع البيانات المخزنة هناك). يمكن تعداد جميع صفحات التجمع الكبير في النظام باستخدام NtQuerySystemInformation مع SystemBigPoolInformation كقيمة لمعامل SystemInformationClass. ثم من بين جميع الصفحات يمكننا التصفية حسب Tag والحصول على عنوان صفحات التجمع الكبير التي نهتم بها. على سبيل المثال، يستخدم CLFS صفحات تجمع كبير بعلامة 'Clfs'. يمكننا الحصول على عنوان جميع الصفحات في النواة حيث يتم تخصيص كائنات CLFS.

بالعودة إلى الأنابيب، يمكننا العمل بنفس الطريقة: تخصيص أنبوب كبير بما يكفي لاستخدام آلية التجمع الكبير، وتعداد صفحات التجمع الكبير، والبحث عن Tag الخاص بالأنبوب وتصفية تلك الصفحات.
وبالتالي يمكننا تسريب إلى مساحة المستخدم المكان الذي خصصت فيه النواة أنابيبنا. ماذا عن التحكم في البيانات التي تدخل إلى النواة وقراءتها؟
لهذا نعتمد على سمات الأنابيب (Pipe Attributes). تمامًا مثل علامة التجمع الكبير، فإن سمة الأنبوب هي مصفوفة يمكن أن تحتوي معلومات تصف الأنبوب (يملؤها المستخدم). باستخدام الدالة غير الموثقة NtFsControlFile يمكننا القراءة والكتابة بشكل عشوائي على هيكل بيانات سمة الأنبوب. ولأنها غير موثقة، يتم توفير برهان مفهوم فقط يعيّن متجه PipeAttribute ثم يقرؤه. التعديل الوحيد المسموح به على بدائيات القراءة والكتابة هو التحكم في محتوى المخزن المؤقت للإدخال/الإخراج وحجمهما.
هنا نخصص أنبوبًا كبيرًا بما يكفي لاستخدام صفحات التجمع الكبير (0x2000)، ونضبط مخزن إدخال ومخزن إخراج بقيمة خاضعة للتحكم. تحذير: يجب أن تكون أول 2 بايت من قيمة الإدخال 0x5a 0x00 حتى يعمل.

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

والنتيجة:

فيما يلي محتويات صفحة التجمع الكبير للأنبوب في النواة: من خلال الاستعلام عن صفحات التجمع الكبير وجدنا بداية هيكل بيانات الأنبوب في النواة. عند العنوان pipe_begin+0x20 نجد مؤشرًا إلى input buffer+0x2 الخاص بنا.

عندما نستدعي دالة قراءة سمة الأنبوب، سيقوم نظام التشغيل بما يلي:
لماذا هو مفيد؟ تخيل لو استطعنا استبدال المؤشر عند pipe_begin+0x20 بموقع رمز الأمان لعملية System واستدعينا قراءة سمة الأنبوب. سنقوم بتسريب قيمته إلى مساحة المستخدم. يتم الاستبدال عبر ثغرة CLFS.SYS.
فهم هذه التقنية أمر حاسم لفهم الاستغلال.
معظم الاستغلالات ليست حتمية بطبيعتها، بل احتمالية. حتى إذا كان الكود الذي يستغل الثغرة صحيحًا، فقد لا يعمل الاستغلال. من خلال وضع البرنامج المستهدف في حالة غير مستقرة، يجب على مطور الاستغلال التأكد من أن النظام لن ينهار بعد تنفيذ الاستغلال. تخيل برنامجًا عند استغلاله يمنح القدرة على القراءة من عنوان يعتمد على قيمة متغير داخل البرنامج.
مثال:
لنقل أن target=0x1000000+ var_1&0xff+ var_2&0xff00.
لا يمكن للمهاجم التحكم في target أو var_1 أو var_2.
لكن الاستغلال يمنح القدرة على القراءة من العنوان target.
يمكننا القراءة من أي مكان بين 0x1000000 و 0x100FFFF، وهو ما قد يكون مفيدًا في بعض الحالات أو لا.
مثال 2:
لنقل أن استغلالًا ما يمنح القدرة على قراءة QWORD من عنوان يطابق نمطًا معينًا ووضع المحتويات في قيمة رمز الأمان الخاص به. لنقل أننا حصلنا مسبقًا على قيمة رمز الأمان لـ System.exe. كيف يمكننا الاستفادة من الاستغلال لرفع الامتيازات؟
read_addr=0x1000000+alfa&0xFFFF00
لا يمكننا التحكم في معامل alfa.
خارج الموضوع لكنه مهم جدًا: يمكن للنواة الوصول إلى مساحة المستخدم المقابلة للعملية التي تشغّل كود النواة في تلك اللحظة.
من أي عنوان يمكننا القراءة؟ حسنًا، 0x1000000، 0x1000100 (alfa=1)، 0x1000200 (alfa=2)، ....، 0x1FFFF00 (alfa=ffff00).
للتأكد من نجاح الاستغلال، يجب على المبرمج القيام بما يلي:
memory=virtualalloc(dest=0x1000000,size=0x1000000,....)for (i=0x1000000;i<0x2000000;i+=0x100) ((QWORD*)i)[0]=system_token_value;هذا في الواقع هو رش الذاكرة. معالجة الذاكرة بنمط محدد يطابق جميع القيم الممكنة لتعبير احتمالي ناتج عن استغلال.
أحد المتطلبات لكي يعمل هذا في حالتنا هو أن الذاكرة يمكن تخصيصها عند العنوان 0x1000000. هذا قيد يجب أن نتعامل معه أيضًا في حالة الاستغلال الحقيقي.
بعد توضيح بعض الجوانب التقنية، نحتاج الآن إلى فهم أساسي حول CLFS. رابط Microsoft. يُستخدم CLFS لسجلات التطبيقات وسجلات قواعد البيانات والمعاملات وما إلى ذلك.
تتطلب الاستغلالات عمومًا تخطيطًا معينًا للذاكرة لكي تعمل. شكل التخطيط تمليه قيم المتغيرات داخل البرنامج في اللحظة الدقيقة للاستغلال.
لن يشرح هذا البرنامج التعليمي بالتفصيل الكود القابل للاستغلال، أو تنسيق نظام ملفات السجل. سوف يقدم فهمًا أساسيًا للعمليات التي تولّد الاستغلال.
ملفات السجل هي نوع خاص من الملفات لها تنسيق معين ويمكن التفاعل معها عبر برنامج تشغيل CLSF وواجهات برمجة التطبيقات (APIs) الخاصة بـ dll. ضمن هذا النظام يوجد أيضًا مفهوم حاويات السجل (log containers). حاويات السجل هي أيضًا ملفات سجل، لكنها مرتبطة في الذاكرة بملف سجل رئيسي (ملف السجل الذي تُضاف إليه).
يعمل هذا البناء على النحو التالي:
تفرض هذه العملية تخصيص مساحة ذاكرة جديدة داخل ملف السجل الرئيسي، وستقوم بتعديل كائنات مختلفة داخل تخطيط الذاكرة لملف السجل الرئيسي. تعديل تخطيط الذاكرة سيبدو مفاهيميًا هكذا:

كما هو الحال مع كل هيكل بيانات/ملف مهم، قبل استخدامه، يجري برنامج تشغيل CLFS فحوصات سلامة على تنسيق ملف السجل:
النقطة الثانية هي نقطة البداية للثغرة. يمكن للمهاجم تعديل ملف سجل تم إنشاؤه مسبقًا، وإعادة حساب تجزئته، وتعديل حقل التجزئة لجعل برنامج التشغيل يجتاز اختبار السلامة. تتعلق التعديلات بأطوال ترويسات الملف. مجموعة قيم مصممة بعناية تحدد اجتياز فحص نطاق-طول كان سيفشل لولا ذلك ويعيد خطأ. وهكذا يقبل برنامج التشغيل الطول المزيف كقيمة صالحة، ويواصل تنفيذ الكود بشكل طبيعي. يُستخدم الطول المزيف لحساب إزاحة داخل الملف حيث سيتم كتابة عنوان ثابت (hardcoded). وهذا يمنح المهاجم القدرة على التحكم في العنوان الذي ستحدث عنده عملية الكتابة السابقة.
يجب ربط هذه الثغرة مع قراءة-كتابة النواة عبر الأنابيب لإكمال الاستغلال. إثبات بصري:

في الشكل السابق نرى الدالة AllocSymbol داخل CLFS.sys المسؤولة عن فحص النطاق المزيف. فحص الطول القابل للاستغلال هو الذي سيعيد رمز الخطأ 0xC0000023. (BaseLogRecord + v9 + Size_1 + 0x1338 > v8 + *(0x68)) جزء من مدخلاته في الشرط يتحكم فيه المهاجم. من الممكن حقن قيمة في الصيغة تجعل الفحص ينجح حتى لو كان سيفشل عادةً. من خلال التحكم في v8 و v9 يمكننا التلاعب بالقيمة المنطقية للشرط لتكون FALSE، ومنع إرجاع الخطأ، والوصول إلى حساب عشوائي لقيمة المتغير v10 باستخدام قيمة v9 التي يتحكم فيها المهاجم.
يُستخدم V10 بعد ذلك في عملية memset بقيمة 0 كقيمة يجب تعيينها. لذا يكتسب المهاجم القدرة على تعيين الذاكرة بشكل عشوائي إلى 0. ليس هذا كل شيء. قبل الإرجاع النهائي، *a3=v10. A3 هو معامل يُرسل بالعنوان إلى الدالة AllocSymbol. وبالتالي يستقبل المستدعي لـ AllocSymbol قيمة V10 بعد العودة من AllocSymbol.
المستدعي لـ AllocSymbol هو FindSymbol.

داخل FindSymbol، V33 هو المعامل المسمى a3 في AllocSymbol. لذا يحصل v33 على قيمة v10. ثم تُعيّن القيمة عند عنوان v33 إلى ثابت (0xc1fdf006) وتُسبق لاحقًا بالقيمة 0x30.

إذن يكتسب المهاجم القدرة على تعيين موقع ذاكرة عشوائي بقيمة ثابتة.
لماذا سيكون هذا مهمًا؟ إذا تمكنا من الكتابة فوق القيمة الافتراضية لمؤشر دالة بعنوان ثابت يمكن الوصول إليه من كل من مساحة المستخدم ومساحة النواة، وكان لدينا ضمان بأن المؤشر المعني سيُستدعى، فإننا نحصل على التحكم في تنفيذ الكود. على الرغم من أن هذه هي الفكرة العامة، إلا أن هناك عوائق تقنية سيتم شرحها لاحقًا.
CClfsContainer* pContainer;. When the respective container is deallocated, in the parent file, clean-up operation take place and dereference pContainer and use values [[pContainer]+0x18] and [[pContainer]+0x8] as function pointers.So the ability to control pContainer and execute the container specific API's for adition and deletion guarantee control flow redirection.
Where is pContainer located?
CLFS file structure is undocumented, but some individual attempts at reversting the file structure do exist.
A bird's eye view of a log file structure:

And a detailed view on the base block:

Differences between the structure and memory view:
CLFS files are allocated in big-pool pages with a tag value of Clfs . When a clfs's log address will be obtained in userspace (the same method used in obtaining the kernel space address of a pipe object), the kernel will return THE ADDRESS WHERE THE BASE BLOCK BEGINS. ( at offset 0x800 in the previous picture ). At offset 0xb98 we see a datastructure named regContainers. This is an array of 32bit values. Each value is related to one container and represents the offset (counting from 0x870) where a container's internal data structures are located in the base block file.
The memory layout of a containter file is as follows:
CLFS_CONTAINER_CONTEXT structure
At offset 0x18 into CLFS_CONTAINER_CONTEXT structure it is our target pContainer.
Apparently the offset to pContainer is constant, that is the first element of regContainers array. And it's value is 0x1468.
إلغاء الإشارة إلى pContainer والوصول إليه كمؤشر دالة هو جوهر الاستغلال. يحدث هذا داخل دالة Remove container في برنامج تشغيل CLFS.SYS. لذلك سيتم تشغيل الاستغلال عند إزالة الحاوية.
الإثبات:

لنستعرض خطوات إزالة حاوية لإظهار أن المؤشر pContainer يُستخدم بالفعل كمؤشر إلى دالة
GetBaseLogRecord والتي تُرجع عنوان kernel للكتلة الأساسية + 0x70 (تحرّك مؤشر الملف متجاوزة الترويسة)BaseRecord_1)a4 بالقيمة v10LODWORD(a4) = *( (_DWORD*)BaseLogRecord_1 + StartingIndex_1 + 0xCA); إذا كانت هناك حاوية واحدة مضافة إلى الملف، فإن StartingIndex_1 يساوي صفرًا. القيمة التي تُرجعها GetBaseLogRecord تُحوَّل إلى متجه DWORD (32 بت) وتُفهرس بعناصر 0xCA. لاحظ أن (_DWORD*)BaseLogRecord_1 + 0 + 0xCA لا يضيف ببساطة القيمة 0xCA إلى BaseLogRecord_1. بسبب التحويل، فإن هذا يعادل BaseLogRecord_1[0xCA]. وينعكس ذلك في الكود المفكك المقابل، mov eax, [rdi+r12*4+328h] حيث rdi هو الأساس، وr12 هو StartingIndex_1 (لاحظ أنه مضروب في 4 وهو طول عنصر بحجم 32 بت (DWORD)) ويُضاف إليه 0x328 وهو 0xCA*0x4.قيمة a4 في النهاية هي BaseBlock+0x70+0+328=(BaseBlock+0x398) وهو ما يوصلنا إلى أول قيمة من RgConrainers (0x398+0x800=0xB98).
pContainer.82 & 83 يتم إلغاء الإشارة إلى pContainer، وإضافة 0x18 و0x8، وتفسيره كمؤشر إلى دالة وتنفيذه. (استدعاء cs:__guard_dispatch_icall_fptr هو في الواقع استدعاء لتعليمة jmp eax)يثبت هذا أنه من خلال إعادة كتابة قيمة pConainter يمكننا تغيير تدفق التحكم في برنامج التشغيل وتوجيهه إلى عناوين يتحكم بها المهاجم.
سابقًا تم شرح أنه في بعض الأحيان تحتاج إلى ملء جزء من الذاكرة بنمط قيم محدد مسبقًا لضمان التنفيذ الناجح للاستغلال. وذلك لأن المهاجم لا يمكنه التحكم إلا في مجموعة فرعية من المتغيرات التي تشكّل حالة البرنامج وقت الاستغلال. في المثال البسيط لرش الذاكرة سابقًا، استخدمنا فقط قيمًا مكتوبة في متجه لتلبية شرط معين.
في الحالة الحقيقية، تكون الشروط أكثر تعقيدًا. نحتاج إلى ترتيب ملفات السجل في ذاكرة kernel بترتيب معين وبإزاحة معروفة بينها.
أولًا، لننشئ العديد من ملفات السجل في حلقة وندرس كيف يخصص نظام التشغيل الذاكرة لها. ستستخدم التجربة النمط التالي:
القطعة البرمجية التالية تقوم بذلك:

والنتيجة:

لنرتب العناوين:

من خلال فحص الإزاحة بين العناوين، يظهر نمط شبه منتظم فيما يخص التخصيصات: هناك تخصيصات متتالية بينها إزاحة ثابتة. على سبيل المثال من ffffd80faf444000 إلى ffffd80faf4ee000 الإزاحة بين أي تخصيصين متتاليين هي 0x11000.
هذا الافتراض حاسم لعمل الاستغلال. وسيتم الاعتماد عليه.
افتراض آخر: لنأخذ بعض الصفحات التي تفصل بينها مسافة 0x11000. على سبيل المثال: ffffd80faf444000,ffffd80faf455000,ffffd80faf466000,ffffd80faf477000,ffffd80faf488000,ffffd80faf499000. جميع الصفحات تقابل ملفات CLFS مفتوحة. إذا قمنا بإغلاق ملف واحد، فسيتم تحرير الصفحة وستكون الذاكرة عند العنوان المعني حرة.
سيبدو ذلك هكذا في الذاكرة (لنفترض أننا أغلقنا الملف المقابل للصفحة عند ffffd80faf466000):
ffffd80faf444000,ffffd80faf455000,xxxxxxxxxxxxxxxx,ffffd80faf477000,ffffd80faf488000,ffffd80faf499000
إذا فتحنا الملف مرة أخرى، فسيقوم نظام التشغيل بدرجة عالية من اليقين بتخصيص صفحة في نفس العنوان (ffffd80faf466000) لملء الفجوة وجعل الذاكرة متصلة. --> هذا أيضًا حاسم لتنفيذ الاستغلال.
الآن نقدم مخططًا للاستراتيجية التي سنستخدمها لضمان الكتابة فوق مؤشر *pConainter بعنوان يخضع لتحكمنا، في مساحة المستخدم.


في الصورة السابقة start(aux1)+0x11000=start(A),start(A)+0x11000=start(B),start(B)+0x11000=start(aux2)
سنستخدم Logfile A للكتابة فوق مؤشر *pcontainer الخاص بـ Logfile B، وتشغيل الاستغلال بإغلاق Logfile B باستخدام كود حذف خاص بعد الإغلاق.
إضافة حاوية سجل إلى Logfile B، من أجل تخصيص وتحديث الحقول التي تشير إلى أن B يحتوي على ملف حاوية صحيح. يمكننا إضافة aux2 كحاوية إلى B.
إغلاق LogfileA حتى نتمكن من تعديله على القرص. من المهم اختيار A وB ليكونا في منتصف تسلسل أقصى من الملفات المتباعدة مسافة 0x11000، وذلك لإنشاء فجوة في الذاكرة سيعطيها نظام التشغيل أولوية الملء. إذا لم يفعل ذلك، فسيفشل الاستغلال.
إعادة حساب تجزئة A وتعديل حقل التجزئة الخاص بها للحفاظ على السلامة، ثم فتح A مرة أخرى على أمل أن يضعها kernel في نفس العنوان وإلا سيفشل الاستغلال.
استدعاء AddLogContainer على A باستخدام ملف سجل عادي لتشغيل الكتابة فوق مؤشر B's *pContainer
حذف ملف B بحيث يتم استدعاء RemoveConainter ويتم نقل التنفيذ إلى B's *pContainer الذي أصبح تالفًا الآن ويشير إلى كود مخصص من قبل المستخدم.
الصورة التالية تصور الخطوات المذكورة سابقًا.

نبدأ أولاً بتخصيص 50 ملف CLFS. بعد كل تخصيص، نستعلم عن جميع صفحات CLFS في ذاكرة Kernel وننشئ قائمة بجميع العناوين التي خُصصت لكل ملف. بعد ذلك، نحدد ملفين تفصل بينهما مسافة 0x11000 (A وB في المخطط). (ونخزنهما في المتغيرين first، second)

بعد تحديد العنوان، ابحث عن ملفين بينهما مسافة 0x11000: A->first ، B->second

بعد ذلك أغلق A (first) وعدّله على القرص (شوّه ترويساته) وأعد فتحه.

تحتاج هذه الخطوة إلى مزيد من التوضيح لأننا نحتاج إلى حساب بعض القيم الدقيقة التي، عند تشغيل الكود القابل للاستغلال في AllocSymbol، ستؤدي إلى الكتابة فوق مؤشر B's *pConainter.
من التحليل السابق لدالة AllocSymbol ووظيفة FindSymbol نعلم أنه يجب تعديل ملف A بحيث يكتب فوق القيمة المنطقية لشرط IF لتكون FALSE ويحقن قيمة في المتغير v9 تؤدي إلى كتابة في موقع المؤشر *pContainer الخاص بـ B.
أولاً: ما القيمة التي يجب ضبطها لـ v9؟
يحسب AllocSymbol v10 (العنوان المستهدف للكتابة) كالتالي: v10=BaseLogRecord + v9 + 0x1338. يُحسب هذا في سياق مساحة عناوين A. إذن BaseLogRecord هو: عنوان صفحة Kernel الخاصة بـ A + 0x70. v9 يتحكم به المهاجم و0x1338 ثابت.
أين يقع B's *pContainer بالنسبة إلى عنوان صفحة Kernel الخاصة به؟
تم تفصيل هذا سابقًا؛ لذا، من عنوان صفحة Kernel الخاصة بـ B نحتاج إلى إضافة 0x398 للوصول إلى متجه regContainter الخاص بـ B وفهرسته بالعنصر الأول للحصول على الإزاحة إلى أول بنية CONTEINER_CONTEXT. كما ذُكر سابقًا، بالنسبة للحاوية الأولى تم تحديد الفهرس بـ 0x1468 (محسوبًا من B BaseBlock +0x70).
إذن موقع B's *pContainer هو B's Kernel_Address + 0x70 + 0x1468 + 0x18 (third element in CONTEINER_CONTEXT structure)
B's Kernel_Address=A's Kernel Address+0x11000 (لأننا بنينا الذاكرة بهذه الطريقة)
A's BaseRecordAddress=A's Kernel Address+0x70
V10=A's Kernel Address+0x70+v9+0x1338
يجب أن تكتب v10 فوق B's *pContainer
يجب أن تكون V10: v10=B's Kernel_Address + 0x70 + 0x1468 + 0x18 (third element in CONTEINER_CONTEXT structure)
بتعويض عنوان Kernel الخاص بـ B: v10=A's Kernel Address+0x110000+x70 + 0x1468 + 0x18
بتبسيط v10: A's Kernel Address+0x70+v9+0x1338=A's Kernel Address+0x11000+x70 + 0x1468 + 0x18
بحل المعادلة نحصل على v9 = 0x11000+0x70+0x1468+0x18-0x1338-0x70=0x11148
الآن، ما الحقول في A التي يجب الكتابة فوقها؟ --> هناك فئتان من الحقول:
IF في AllocSymbol على التقييم إلى FALSEلن يتم تفصيل الحقول في الفئة 1. يقابل V9 الحقل الموجود عند الإزاحة 0x1b98 على القرص داخل ملف A. لن يكون هناك مزيد من التفاصيل حول سبب قيام هذه الحقول بتشغيل الاستغلال؛ القارئ مدعو للاطلاع على ذلك إذا كان مهتمًا.
إليك القيم التي يجب تعديلها:

لاحظ أنه (little endian)، القيمة المستخدمة هي 0x11149 بدلاً من 0x11148. وذلك لأننا نحتاج إلى التحكم في الثمانيات ذات الترتيب الأعلى في *pContainer. سيكون الثُماني الأقل دلالة بالشكل X0 (10 أو 20 أو 30...). يمكننا عمل رش ذاكرة عند كل قيمة لمراعاة هذا الشرط.
لاحظ أن v10 سيُستخدم أيضًا في memset بكتابة 00 لمنطقة ذاكرة طولها 0xa0

ثم عند نفس العنوان تتم الكتابة فوقها بقيمة ثابتة

لاحظ القيمة الثابتة التي تكون بالشكل 0x30c1fdf006X0
يحدث هذا عند إضافة حاوية سجل إلى ملف A، لكننا لم نتطرق إلى عملية إعادة حساب التجزئة بعد تعديل حقول A.
إن عرض ملف السجل على القرص مختلف إلى حد ما عن عرضه في الذاكرة. نحن نعدّل فقط محتويات base block الذي يبلغ طوله 0x7a00 ويبدأ على القرص عند إزاحة الملف 0x800. خوارزمية التجزئة المستخدمة لتجزئة الكتلة الأساسية هي CRC32. الحقل الذي يحمل قيمة التجزئة مخزّن أيضًا داخل الكتلة الأساسية عند الإزاحة 0x80c.
إجراء إعادة حساب التجزئة:
0x80c0x800 وطول 0x7a00لتوجيه تنفيذ الكود إلى مساحة المستخدم نحتاج إلى:
*pConainter بقيمة ثابتة من الشكل 0x30c1fdf006X0.RemoveContainer.
هناك بعض الشروط المفروضة على ذاكرة المستخدم التي هي هدف إعادة توجيه تدفق التنفيذ باستخدام *pContainer. لا يمكننا ببساطة البدء في تنفيذ تعليمات عشوائيًا من مساحة المستخدم بينما البرنامج في وضع kernel.
الغرض من هذا القسم هو تسريب قيمة SystemToken في مساحة المستخدم، دون التسبب في تعطل النظام (BSOD).
كيف يمكنك القراءة من عنوان في kernel وتخزين النتيجة في مساحة المستخدم؟ تذكير سريع بقسم الأنابيب (Pipe) في البرنامج التعليمي:

هنا نقوم بـ:
الفكرة التي تربط كائن الأنبوب بهدفنا في الحصول على قيمة رمز النظام هي استخدام NtFsControlFile PipeReadAttribute للقراءة من عنوان رمز النظام بدلاً من بداية المخزن المؤقت الذي يحمل المعلومات التي مررناها إلى kernel باستخدام PipeWrite Attribute.
هذا يعني أنه يجب علينا تعديل قيمة المؤشر عند العنوان PIPE_BEGIN+=0x20 ليحمل العنوان الذي يقع فيه رمز النظام.
العنوان معروف، تم الحصول عليه في المراحل السابقة من البرنامج التعليمي، عندما حددنا وحللنا بنية EPROCESS.
هنا نحتاج إلى إيجاد آلية تسمح لنا بالكتابة في بنية الأنبوب في موقع ثابت.
لهذا نستخدم إعادة توجيه الكود التي تم الحصول عليها عبر استغلال دالة CLFS AddLogConainer. قد يظن المرء أنه يكفي كتابة شيلكود يقوم بالاستبدال المباشر، لكن (على الرغم من أنه لم يتم اختباره) من المحتمل ألا يعمل ذلك. وذلك لأن برنامج التشغيل يعمل في سياق Kernel وينفذ كودًا من منطقة عملية مستخدم.
لتجاوز هذا القيد، يجب علينا إيجاد ROP's خاصة بـ Kernel تحقق كتابة قيمة عشوائية. أي العثور على بعض أجزاء كود kernel الموجودة في نهاية دالة وتنتهي بتعليمة ret، وتوفير عناوينها كأهداف لإعادة التوجيه. بهذه الطريقة يظل الكود منفذًا بواسطة Kernel.
هذه هي الفكرة الرئيسية، لكن تظهر قيود من الطريقة التي يستدعي بها برنامج تشغيل CLFS الكود داخل *pConainer:

لصياغة نمط ذاكرة قابل للاستخدام، يجب دراسة الطريقة التي تصل بها دالة RemoveContiner إلى كود *pContainer.
في الصورة السابقة نبرز أجزاء الكود التي تلغي الإشارة إلى *pContainer وتستخدمه كمؤشر دالة.
RDI هو الثابت الذي تمت كتابته داخل *pcontainer عن طريق استغلال AllocSymbol. كما ترى، الثابت ليس ثابت القيمة تمامًا؛ فهو يتغير في البايت الأول (الشكل هو 6X0، حيث X أي قيمة).
mov rax, [rdi] يلغي الإشارة إلى القيمة الثابتة. لتجنب التسبب في قراءة غير صالحة (و BSOD)، يجب التأكد من أن rdi يحمل مؤشرًا إلى عنوان صالح. للقيام بذلك، يجب رش ذاكرة المستخدم من 0x30C1FDF00000 إلى 0x30C1FDF006FF على الأقل. يتم ذلك عن طريق تخصيص جزء من الذاكرة باستخدام VirtualAlloc. بالطبع إذا تعذر على النظام تخصيص ذاكرة لأي سبب، فسيفشل الاستغلال.
LPVOID dirtySpray = VirtualAlloc((LPVOID)0x000030c1fdf006d0, 0x1000000, 0x3000, 0x4);
ثم لأي قيمة محتملة لـ X، نخزن عند 0x30C1FDF000X0 إلى 0x30C1FDF00FX0 قيمة أخرى تقابل ذاكرة مستخدم صالحة، باختيار قيمة عشوائية لنقل 0x5000000. بهذه الطريقة نضمن أن rax ستساوي 0x5000000 لأي rdi من الشكل 0x30C1FDF006X0.
بعد هذه الخطوة نرى عمليتي إلغاء إشارة إضافيتين: mov rax, [rax+0x18] و mov rax ,[rax+0x8]
لضمان بقاء الذاكرة متسقة عند هذه العناوين، يجب تخزين مؤشرات إلى دوال kernel عند 0x5000008 و 0x5000018 (ROP1 وROP2، سيتم حسابها لاحقًا).
هذه هي خوارزمية توليد نمط الرش.

ملاحظة: سيتطور النمط عندما نقدم الـ ROP's لأنها تحتوي أيضًا على معاملات ستؤخذ في الاعتبار، لكن هذا هو الحد الأدنى حتى الآن لمنع عنونة ذاكرة غير صالحة.
هكذا تبدو المرحلة الأولى من الذاكرة المرشوشة في المصحح (لاحظ أين توضع القيمة 0x5000000 --> محاذاة X0):

هذا ما تبدو عليه الذاكرة عند 0x5000000 (الـ ROP's التي سيتم تفصيلها)

وعملية إعادة توجيه الكود في مصحح kernel:

لاحظ قيمة سجل RDI وعنوان تعليمة التنفيذ في المصحح. RDI بعد إلغاء الإشارة مرة واحدة هو 0x5000000 ويُخزن في RAX. [RAX+0x18] هو ROP الثاني، وفي الأسفل [RAX+0x8] سيكون عنوان ROP الأول.
في هذه المرحلة، لدينا القطع التالية من الاستغلال التي سنربطها معًا:
الهدف في هذه المرحلة هو ربط إعادة توجيه الكود بجزء من الكود سيكتب فوق مؤشر الأنبوب إلى مخزن السمات بعنوان رمز النظام ويستخدم خاصية قراءة الأنبوب لاستعادة المعلومات في مساحة المستخدم.كما ذكرنا، يجب أن يكون الكود الذي تتم إعادة توجيهه موجودًا أيضًا في النواة. علاوة على ذلك، لدينا وظيفتان فقط لاستخدامهما. لن يغطي هذا البرنامج التعليمي كيفية العثور على هاتين الوظيفتين المحددتين، ولكن من المحتمل وجود قائمة بمرشحي الـ ROP التي تُستخدم غالبًا.
سندرس وظيفتين:
SeSetAccessStateGenericMapping في ntoskrnl.exe تُستدعى ثانيًا ([rax+0x8])ClfsEarlierLsn في CLFS.SYS تُستدعى أولًا ([rax+0x18])تحليل ClfsEarlierLSn:

الدور الوحيد لهذه الوظيفة عند استدعائها بمعاملات غير صحيحة هو تعيين EDX إلى 0xFFFFFFFF ثم الإرجاع. سيتضح سبب الحاجة إلى ذلك عند تحليل الـ ROP الثانية.
تحليل SeSetAccessStateGenericMapping:

يجب مناقشة هذه الوظيفة سطرًا بسطر بالتفصيل.
المدخلات: عند الدخول إلى هذه الوظيفة:
0x30C1FDF006X00x30C1FDF00YX8، وهو دائمًا محاذٍ لـ 0x8 ولن يتعارض مع 0x30C1FDF006X0 المحاذي لـ 0 والذي يحتوي على 0x5000000تنفيذ كود الوظيفة:
mov rax, [rcx+48h] سيقوم بإلغاء الإشارة من 0x30C1FDF00YX8 ونقل القيمة إلى RAXmovups xmm0, xmmword ptr [rdx] سينقل 16 بايتًا من RDX إلى سجل XMM0. تذكر هنا أنه من ClfsEarlierLSn تكون قيمة RDX هي 0xFFFFFFFFmovdqu xmmword ptr [rax+8], xmm0 سينقل إلى [RAX+0x8] قيمة XMMOفي الأساس، هذا يقرأ من عنوان ويخزن داخل النواة. ويمكن تفسيره على النحو التالي:

لذا، من أجل استبدال مؤشر مخزن سمات الأنبوب بمؤشر إلى رمز النظام، يجب علينا تخصيص 16 بايتًا عند 0xFFFFFFFF وتخزين عنوان رمز النظام هناك. ثم يجب علينا رشّ الذاكرة عند 0x30C1FDF00YX8 بشكل متكرر بقيمة العنوان الهدف ناقص 0x8. أي قيمة الإزاحة إلى مخزن سمات الأنبوب ناقص 0x8. أي PIPE_KERNEL_PAGE_ADDRESS+0x20-0x8.
إليك كيف يتم تحقيق ذلك في الكود الفعلي:

جانب آخر لم يتم شرحه: كيف تحصل على عناوين النواة لوظائف ROP في مساحة المستخدم؟
إنها خدعة. أولًا، يمكنك الحصول على العنوان الأساسي لأي وحدة نواة باستخدام NtQuerySystemInformation مع المعاملات التالية: NtQuerySystemInformation(SystemModuleInformation, (HANDLE)ModuleInfo, ModuleInfoSize, &retlen)
ثم تقوم بالتصفية حسب قيمة ModuleInfo->Modules[i].Name لمطابقة المكتبة المطلوبة. وهذا يعطيك العنوان الأساسي في النواة.
ثم نستغل حقيقة أن الإزاحة بين العنوان الأساسي وعنوان التصدير ثابتة بغض النظر عن مساحة العنوان التي يُحمَّل فيها الملف التنفيذي.
نقوم بتحميل الوحدة في مساحة المستخدم (يمكنك فعل ذلك) باستخدام LoadLibrary ونحصل على عنوان التصدير باستخدام Getprocaddress. نحسب الفرق بين عنوان التصدير والقاعدة في مساحة المستخدم، ونضيفه إلى قاعدة النواة (التي تم العثور عليها باستخدام NtQuerySystemInformation). وهكذا نحصل في مساحة المستخدم على عنوان التصدير المحمَّل في مساحة النواة.
بمجرد إفساد المؤشر إلى السمة وتعيينه ليشير إلى موقع رمز النظام، فإن استدعاء MyNtFsControlFile بالمعاملات الصحيحة يجب أن يقرأ من العنوان ويُسرّب قيمة رمز النظام إلى مساحة المستخدم.

بعد الحصول على القيمة الصحيحة لرمز النظام، ولإكمال الاستغلال، يجب علينا كتابتها مكان رمز عمليتنا الخاصة. أي استبدال رمز العملية التي تنفّذ الاستغلال بقيمة رمز النظام.
الخطوات المطلوبة لإجراء هذا التغيير لا تتضمن أي تقنيات أو منهجيات جديدة، بل تعتمد على إعادة استخدام الكود السابق. تتضمن خوارزمية استبدال قيمة الرمز الخاص بنا عملية كتابة في النواة عند عنوان معين. يتم تحقيق ذلك عبر وظيفتي ROP SeSetAccessStateGenericMapping و ClfsEarlierLSn. وهذا يعني أن علينا تشغيل استغلال CLFS مرة ثانية. هذا صحيح، يجب علينا القيام بتخصيص الحاوية ورشّ الذاكرة مرة ثانية ونأمل ألا يتعطل نظام التشغيل.
خطوات استبدال قيمة الرمز الخاص بنا:
بما أننا لا نحتاج إلى قراءة من النواة في المرة الثانية، فإننا لا نحتاج إلى استخدام الأنبوب هذه المرة.
وبما أن الخطوات لا تتطلب معرفة إضافية، يمكن أن ينتهي هذا البرنامج التعليمي هنا مع إثبات نهائي على cmd.exe بصلاحيات nt authority\system

لم يكن التركيز الرئيسي لهذا البرنامج التعليمي على التفاصيل الداخلية لـ CVE نفسها، بل وصفًا مبسّطًا لعملية إنشاء مثال على تصعيد الصلاحيات في Windows. تشترك العديد من الاستغلالات في نفس الأساليب واللبنات الأساسية مثل رشّ الذاكرة أو التعامل مع بعض هياكل بيانات Windows. وفي معظم الأحيان، توجد فجوة كبيرة بين امتلاك معرفة نظرية بالتفاصيل الداخلية لنظام Windows وبين كتابة خوارزمية فعّالة تستغل ثغرة ما.