Skip to content
KitploitKITPLOIT
أدواتالمدونة
إرسال
أدواتالمدونة
إرسال

أدوات الاختراق واختبار الاختراق والأمن السيبراني لترسانتك الأمنية!

Kitploit هو دليل لأدوات الاختراق والأمن السيبراني واختبار الاختراق. اكتشف آخر تحديثات المشاريع للعثور على الثغرات وتحليل الأنظمة وأتمتة الاختبارات وتعزيز أمنك.

··الخلاصات·اتصال·الخصوصية·© 2026 Kitploit

دليل الأدوات

الفئات

عرض جميع الفئات
Loading categories
CVE-2022-37969PoC — درس تعليمي حول CVE-2022-37969 مع التركيز على منهجية استغلال النواة، وليس على الأسباب الداخلية لـ CVE | Kitploit
أدوات/GitHubGitHub/emilc3978/cve-2022-37969poc
تصعيد الامتيازاتتحليل الذاكرة الجنائيتحليل الثغرات الأمنيةالاستغلالالهندسة العكسيةالتعلم والتعليماستغلال الملفات الثنائية
GitHubemilc3978/cve-2022-37969poc

CVE-2022-37969PoC

درس تعليمي حول CVE-2022-37969 مع التركيز على منهجية استغلال النواة، وليس على الأسباب الداخلية لـ CVE

عرض المستودع
22منذ 9 أشهرلم تتم المراجعة بعد

الأكثر شعبية

عرض الكل →

اكتشف الأدوات الأكثر استخدامًا من قبل مجتمعنا.

استكشف جميع الأدوات

تصفح مجموعتنا من الأدوات

عرض جميع الأدوات →
مشاركة

المحتويات

مقدمة عامة

تم إنشاء هذا لتوضيح الجوانب العامة المتعلقة باستغلال ثغرات 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

مساحة عناوين Windows مقسمة تقريبًا بين مساحة المستخدم (تشغيل البرامج العامة) ومساحة النواة (تشغيل نظام التشغيل نفسه وبرامج مكونات العتاد --> برامج التشغيل). لا يجب على المستخدم العادي الوصول إلى مساحة النواة، لكن هناك آليات يمكن من خلالها لبرامج المستخدم العادي الوصول إلى أجزاء من كود النواة (استدعاءات النظام، إجراءات برامج التشغيل). لماذا نحتاج إلى الوصول؟ للتفاعل مع نظام التشغيل بطريقة آمنة ومنضبطة، وهذا ما يوفره مصممو نظام التشغيل.

تستخدم بعض برامج التشغيل مدخلات بيانات يقدمها المستخدم للعمل على هياكل بيانات في مساحة النواة. إذا ولّد الإدخال خطأً، فقد تتلف النواة. إحدى الحالات هي Common Log File System Driver. باستخدام بعض المدخلات الخاصة، يمكننا إجبار برنامج التشغيل على تغيير هياكل بيانات النواة التي تحتوي على مستوى وصول الامتيازات للمستخدم، والكتابة فوق المستخدم العادي بقيمة المسؤول.

ما الذي يجب تعديله لرفع الامتياز إلى صلاحيات المسؤول؟

نبدأ بالهدف النهائي في أذهاننا. يخزن Windows داخل هيكل بيانات في النواة يُسمى _EPROCESS معلومات لكل عملية تعمل على النظام. مثال على _Eprocess

من الحقول المهمة struct _EX_FAST_REF Token. هذا هيكل بيانات آخر يشير بدوره إلى بيانات تتعلق بمستوى امتياز تلك العملية. في الصورة التالية، تمتلك عملية System رمز نظام (system token)، وعملية Explorer تمتلك رمز مستخدم عادي.

Tokens

إذن، لرفع امتياز Explorer.exe نحتاج إلى نسخ القيمة من System's _EPROCESS-->Token إلى Explorer's _EPROCESS-->Token. سنحقق شيئًا مشابهًا عن طريق نسخ Token النظام إلى Token الخاص ببرنامجنا وتشغيل موجه أوامر من العملية المرفوعة الامتيازات (العمليات الفرعية ترث Token العملية الأب).

لإكمال هذه الإجراءات، نحتاج إلى آليات من أجل:

  1. الحصول على عنوان هيكل بيانات _EPROCESS في النواة
  2. قراءة قيمة حقل Token لعملية System
  3. الحصول على عنوان هيكل بيانات _EPROCESS لعملية Explorer
  4. كتابة قيمة Token الخاص بـ System في إزاحة Token الخاصة بـ Explorer داخل هيكل _EPROCESS الخاص بها

تحديد موقع هيكل بيانات _EPROCESS لعملية مستهدفة باستخدام PID

مقدمة: طبيعة Windows عبر السنوات: مع اكتشاف ثغرات جديدة، احتاج Windows إلى تصحيحات للتخفيف من حدتها. ومع ظهور تقنيات جديدة، احتاج Windows إلى تحديثات ليبقى قادرًا على المنافسة. كان أحد المتطلبات الحاسمة هو التوافق مع الإصدارات السابقة. وأحيانًا كان يتم تحقيق الأمن عبر الغموض (obscurity). أُزيلت هياكل البيانات وتعريفات الدوال من الأدلة، لكن الوظائف بقيت. من خلال الهندسة العكسية، تمكن الباحثون من استخدام تلك الوظائف لأغراض متنوعة.

للعثور على عنوان _EPROCESS في النواة، سنستخدم دالة غير موثقة: NtQuerySystemInformation (انظر الرابط للاطلاع على المعاملات). باستخدام معامل SystemInformationClass يمكننا تحديد نوع المعلومات التي نريد استرجاعها. سنسترجع معلومات عامة عن العمليات بتحديد قيمة SystemExtendedHandleInformation (#define SystemExtendedHandleInformation 0x40).

من المحاذير في استخدام NtQuerySystemInformation أننا لا نعرف مسبقًا طول البيانات التي سيتم إرجاعها، لكن NtQuerySystemInformation لديها آلية تساعد في ذلك. إذا تم استدعاؤها بمصفوفة ذات حجم خاطئ للبيانات المطلوبة، فإنها تُرجع ERROR والحجم الصحيح للبيانات الذي كان يجب طلبه. يمكن استخدام هذا لقراءة معلومات العملية بشكل صحيح بالطريقة التالية:

  1. استدعِ NtQuerySystemInformation بمعامل SystemInformationLength وهمي
  2. اقرأ قيمة المعامل ReturnLength المُعاد
  3. استدعِ 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 الخاص بها في النواة.

مقتطف من الكود:

proc_1 proc_2

يُعلَن NtQuerySystemInformation كمؤشر إلى دالة، ويتم الحصول على عنوانه ديناميكيًا في وقت التشغيل عبر loadlibrary و getprocaddress.

  1. typedef NTSTATUS(WINAPI* ptr_NtQuerySystemInformation)(int, PVOID, ULONG, PULONG);
  2. ntdll=LoadLibrary(L"ntdll.dll");
  3. MyNtQuerySystemInformation = (ptr_NtQuerySystemInformation)GetProcAddress(ntdll, "NtQuerySystemInformation");

لقراءة وكتابة Token، سيتعين علينا الاعتماد على ثغرات داخل clfsw32.sys و NamedPipes.

قراءة وكتابة البيانات في النواة باستخدام الأنابيب (Pipes)

هذا يشبه إلى حد ما صندوقًا أسود، ومفصّل في مقالات أخرى، لكن سيتم شرح الحد الأدنى من المعرفة اللازمة لهذه الوظيفة من أجل اكتساب فهم أساسي لعملية الاستغلال. الأنابيب هي آليات اتصال بين العمليات. يمكن للعمليات تمرير المعلومات لبعضها البعض باستخدام الأنابيب. تُمثَّل الأنابيب كهياكل بيانات في النواة تحتوي على بعض الحقول التي يمكن ملؤها من مساحة المستخدم. مثال على ذلك سمات الأنابيب (Pipe Attributes).

(لاحقًا سنربط هذا بثغرة CLSF للحصول على قراءة/كتابة عشوائية من مساحة المستخدم إلى مساحة النواة)

تخصيص البيانات في النواة

لمزيد من القراءة، يرجى الاطلاع على fengshui-spraying-big-kids-pool. شرح مبسط للآلية سيكون:

لدى النواة طريقتان لتخصيص الذاكرة فيما يتعلق بالحجم الذي نريد تخصيصه: تجمع صغير (small-pool) للكائنات <4KB+header وتجمع كبير (big-pool) للكائنات >4KB+header. صفحات التجمع الكبير مهمة لأن يمكن تعدادها من مساحة المستخدم. هذا يعني أن المستخدم العادي يمكنه العثور على جميع عناوين بداية النواة التي تحتوي على صفحة تجمع كبير.

كيف؟ تحتوي كل صفحة تجمع كبير على حقل يُسمى Tag (يمكن استخدامه للحصول على معلومات حول نوع البيانات المخزنة هناك). يمكن تعداد جميع صفحات التجمع الكبير في النظام باستخدام NtQuerySystemInformation مع SystemBigPoolInformation كقيمة لمعامل SystemInformationClass. ثم من بين جميع الصفحات يمكننا التصفية حسب Tag والحصول على عنوان صفحات التجمع الكبير التي نهتم بها. على سبيل المثال، يستخدم CLFS صفحات تجمع كبير بعلامة 'Clfs'. يمكننا الحصول على عنوان جميع الصفحات في النواة حيث يتم تخصيص كائنات CLFS.

proc_1

بالعودة إلى الأنابيب، يمكننا العمل بنفس الطريقة: تخصيص أنبوب كبير بما يكفي لاستخدام آلية التجمع الكبير، وتعداد صفحات التجمع الكبير، والبحث عن Tag الخاص بالأنبوب وتصفية تلك الصفحات.

وبالتالي يمكننا تسريب إلى مساحة المستخدم المكان الذي خصصت فيه النواة أنابيبنا. ماذا عن التحكم في البيانات التي تدخل إلى النواة وقراءتها؟

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

الكتابة والقراءة من سمة الأنبوب في النواة، مثال:

هنا نخصص أنبوبًا كبيرًا بما يكفي لاستخدام صفحات التجمع الكبير (0x2000)، ونضبط مخزن إدخال ومخزن إخراج بقيمة خاضعة للتحكم. تحذير: يجب أن تكون أول 2 بايت من قيمة الإدخال 0x5a 0x00 حتى يعمل.

pipewr

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

peprd

والنتيجة:

piperes

كيف تبدو صفحة التجمع الكبير للأنبوب في الذاكرة، ولماذا العملية السابقة مفيدة لنا؟

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

pipe_kern

عندما نستدعي دالة قراءة سمة الأنبوب، سيقوم نظام التشغيل بما يلي:

  1. تحديد موقع الأنبوب في النواة
  2. إضافة 0x20، وفك الإشارة عن المؤشر، وإلقاء المحتوى من مساحة النواة في المخزن المؤقت الخاص بنا في مساحة المستخدم.

لماذا هو مفيد؟ تخيل لو استطعنا استبدال المؤشر عند pipe_begin+0x20 بموقع رمز الأمان لعملية System واستدعينا قراءة سمة الأنبوب. سنقوم بتسريب قيمته إلى مساحة المستخدم. يتم الاستبدال عبر ثغرة CLFS.SYS.

رش الذاكرة (Memory Spraying)

فهم هذه التقنية أمر حاسم لفهم الاستغلال.

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

مثال:

لنقل أن 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).

للتأكد من نجاح الاستغلال، يجب على المبرمج القيام بما يلي:

  1. تخصيص قطعة ذاكرة في مساحة المستخدم بحجم 0x1000000 (تقريبًا): memory=virtualalloc(dest=0x1000000,size=0x1000000,....)
  2. for (i=0x1000000;i<0x2000000;i+=0x100) ((QWORD*)i)[0]=system_token_value;
  3. الكود في النقطة 2 يملأ الذاكرة عند أي قيمة محتملة لـ alfa بقيمة رمز النظام. لذلك فهو يضمن أنه بغض النظر عن قيمة معامل alfa، سيكون رمز النظام موجودًا في العنوان.

هذا في الواقع هو رش الذاكرة. معالجة الذاكرة بنمط محدد يطابق جميع القيم الممكنة لتعبير احتمالي ناتج عن استغلال.

أحد المتطلبات لكي يعمل هذا في حالتنا هو أن الذاكرة يمكن تخصيصها عند العنوان 0x1000000. هذا قيد يجب أن نتعامل معه أيضًا في حالة الاستغلال الحقيقي.

نظام ملفات السجل العام (Common Log File System)

بعد توضيح بعض الجوانب التقنية، نحتاج الآن إلى فهم أساسي حول CLFS. رابط Microsoft. يُستخدم CLFS لسجلات التطبيقات وسجلات قواعد البيانات والمعاملات وما إلى ذلك.

تتطلب الاستغلالات عمومًا تخطيطًا معينًا للذاكرة لكي تعمل. شكل التخطيط تمليه قيم المتغيرات داخل البرنامج في اللحظة الدقيقة للاستغلال.

لن يشرح هذا البرنامج التعليمي بالتفصيل الكود القابل للاستغلال، أو تنسيق نظام ملفات السجل. سوف يقدم فهمًا أساسيًا للعمليات التي تولّد الاستغلال.

ملفات السجل هي نوع خاص من الملفات لها تنسيق معين ويمكن التفاعل معها عبر برنامج تشغيل CLSF وواجهات برمجة التطبيقات (APIs) الخاصة بـ dll. ضمن هذا النظام يوجد أيضًا مفهوم حاويات السجل (log containers). حاويات السجل هي أيضًا ملفات سجل، لكنها مرتبطة في الذاكرة بملف سجل رئيسي (ملف السجل الذي تُضاف إليه).

يعمل هذا البناء على النحو التالي:

  1. إنشاء أو فتح ملف سجل رئيسي
  2. إنشاء أو فتح ملفات سجل ثانوية
  3. استخدام واجهة برمجة التطبيقات AddLogConstainer لإضافة ملفات السجل الثانوية كحاويات إلى ملف السجل الرئيسي

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

containet_concept

نظرة عامة عالية المستوى لآلية الاستغلال

كما هو الحال مع كل هيكل بيانات/ملف مهم، قبل استخدامه، يجري برنامج تشغيل CLFS فحوصات سلامة على تنسيق ملف السجل:

  1. مقاومة العبث (السلامة): يتم تخزين تجزئة الملف في إزاحة ثابتة داخل الملف. لفتح الملف بنجاح، يحسب برنامج التشغيل تجزئة الملف ويقارنها بالقيمة المخزنة. إذا لم تكن القيم متطابقة، فهذا يعني أن الملف قد عُبث به بعد إنشائه، ويتم إرجاع خطأ.
  2. فحوصات النطاق وبنية الملف: يتم فحص العديد من الترويسات والأطوال للتأكد من احترام تنسيق الملف.

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

يجب ربط هذه الثغرة مع قراءة-كتابة النواة عبر الأنابيب لإكمال الاستغلال. إثبات بصري:

range-check

في الشكل السابق نرى الدالة 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.

find-symbol

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

constant-fix

إذن يكتسب المهاجم القدرة على تعيين موقع ذاكرة عشوائي بقيمة ثابتة.

لماذا سيكون هذا مهمًا؟ إذا تمكنا من الكتابة فوق القيمة الافتراضية لمؤشر دالة بعنوان ثابت يمكن الوصول إليه من كل من مساحة المستخدم ومساحة النواة، وكان لدينا ضمان بأن المؤشر المعني سيُستدعى، فإننا نحصل على التحكم في تنفيذ الكود. على الرغم من أن هذه هي الفكرة العامة، إلا أن هناك عوائق تقنية سيتم شرحها لاحقًا.

إفساد مؤشر CLSF، أو أي موقع يجب الكتابة فوقه لاختطاف تدفق التنفيذEarlier we talked about clfs containers. When a container is added to a file, in the parent file a structure gets populated. The structurea has a pointer named 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:

bird_eye_file_Structure

And a detailed view on the base block:

detailed_base_blok

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:

  1. CLFS_CONTAINER_CONTEXT structure

container

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 واستخدامه كمؤشر دالة

إلغاء الإشارة إلى pContainer والوصول إليه كمؤشر دالة هو جوهر الاستغلال. يحدث هذا داخل دالة Remove container في برنامج تشغيل CLFS.SYS. لذلك سيتم تشغيل الاستغلال عند إزالة الحاوية.

الإثبات:

container_removal

لنستعرض خطوات إزالة حاوية لإظهار أن المؤشر pContainer يُستخدم بالفعل كمؤشر إلى دالة

  1. في السطر 22 يوجد استدعاء لواجهة GetBaseLogRecord والتي تُرجع عنوان kernel للكتلة الأساسية + 0x70 (تحرّك مؤشر الملف متجاوزة الترويسة)
  2. في السطر 23 تتم تهيئة نسخة من النتيجة (BaseRecord_1)
  3. في السطر 41 تتم تهيئة نسخة أخرى من متغير a4 بالقيمة v10
  4. في السطر 50: هناك بنية فهرسة متجهية مثيرة للاهتمام: LODWORD(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).

  1. في السطر 76، يتم الحصول على v13 من v10 (نسخة من a4) عبر التحويل إلى متجه QWORD (حجم العنصر 8) وفهرسته بثلاثة مواضع. لاحظ في السطر 16 من التفكيك أن a4 من النوع _CLFS_CONTAINER_CONTEXT. إذن v10 هو الحقل الثالث من البنية الذي يقابل pContainer.
  2. في السطرين 82 & 83 يتم إلغاء الإشارة إلى pContainer، وإضافة 0x18 و0x8، وتفسيره كمؤشر إلى دالة وتنفيذه. (استدعاء cs:__guard_dispatch_icall_fptr هو في الواقع استدعاء لتعليمة jmp eax)

يثبت هذا أنه من خلال إعادة كتابة قيمة pConainter يمكننا تغيير تدفق التحكم في برنامج التشغيل وتوجيهه إلى عناوين يتحكم بها المهاجم.

الرش في النواة باستخدام الكائنات

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

في الحالة الحقيقية، تكون الشروط أكثر تعقيدًا. نحتاج إلى ترتيب ملفات السجل في ذاكرة kernel بترتيب معين وبإزاحة معروفة بينها.

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

  1. تحديد ملفات CLFS الموجودة على النظام افتراضيًا
  2. تخصيص ملف CLFS جديد
  3. تحديد عنوان kernel للكتلة الأساسية للملف الجديد
  4. تكرار ذلك لنحو 50 ملفًا
  5. دراسة العناوين التي خُصصت فيها الملفات

القطعة البرمجية التالية تقوم بذلك:

page_study

والنتيجة:

result_addresses

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

ordered_addresses

من خلال فحص الإزاحة بين العناوين، يظهر نمط شبه منتظم فيما يخص التخصيصات: هناك تخصيصات متتالية بينها إزاحة ثابتة. على سبيل المثال من ffffd80faf444000 إلى ffffd80faf4ee000 الإزاحة بين أي تخصيصين متتاليين هي 0x11000.

هذا الافتراض حاسم لعمل الاستغلال. وسيتم الاعتماد عليه.

افتراض آخر: لنأخذ بعض الصفحات التي تفصل بينها مسافة 0x11000. على سبيل المثال: ffffd80faf444000,ffffd80faf455000,ffffd80faf466000,ffffd80faf477000,ffffd80faf488000,ffffd80faf499000. جميع الصفحات تقابل ملفات CLFS مفتوحة. إذا قمنا بإغلاق ملف واحد، فسيتم تحرير الصفحة وستكون الذاكرة عند العنوان المعني حرة. سيبدو ذلك هكذا في الذاكرة (لنفترض أننا أغلقنا الملف المقابل للصفحة عند ffffd80faf466000): ffffd80faf444000,ffffd80faf455000,xxxxxxxxxxxxxxxx,ffffd80faf477000,ffffd80faf488000,ffffd80faf499000

إذا فتحنا الملف مرة أخرى، فسيقوم نظام التشغيل بدرجة عالية من اليقين بتخصيص صفحة في نفس العنوان (ffffd80faf466000) لملء الفجوة وجعل الذاكرة متصلة. --> هذا أيضًا حاسم لتنفيذ الاستغلال.

مخطط الاستغلال حتى الآن

الآن نقدم مخططًا للاستراتيجية التي سنستخدمها لضمان الكتابة فوق مؤشر *pConainter بعنوان يخضع لتحكمنا، في مساحة المستخدم.

مخطط الاستغلال

  1. تخصيص مجموعة من ملفات clfs مما ينتج عنه تخطيط ذاكرة مثل هذا:

first_allocation

  1. تحديد تسلسل (يفضل أن يكون أقصى) من الملفات التي تفصل بينها مسافة 0x11000 عن بعضها البعض.

second_Allocation

في الصورة السابقة start(aux1)+0x11000=start(A),start(A)+0x11000=start(B),start(B)+0x11000=start(aux2)

  1. سنستخدم Logfile A للكتابة فوق مؤشر *pcontainer الخاص بـ Logfile B، وتشغيل الاستغلال بإغلاق Logfile B باستخدام كود حذف خاص بعد الإغلاق.

  2. إضافة حاوية سجل إلى Logfile B، من أجل تخصيص وتحديث الحقول التي تشير إلى أن B يحتوي على ملف حاوية صحيح. يمكننا إضافة aux2 كحاوية إلى B.

  3. إغلاق LogfileA حتى نتمكن من تعديله على القرص. من المهم اختيار A وB ليكونا في منتصف تسلسل أقصى من الملفات المتباعدة مسافة 0x11000، وذلك لإنشاء فجوة في الذاكرة سيعطيها نظام التشغيل أولوية الملء. إذا لم يفعل ذلك، فسيفشل الاستغلال.

  4. إعادة حساب تجزئة A وتعديل حقل التجزئة الخاص بها للحفاظ على السلامة، ثم فتح A مرة أخرى على أمل أن يضعها kernel في نفس العنوان وإلا سيفشل الاستغلال.

  5. استدعاء AddLogContainer على A باستخدام ملف سجل عادي لتشغيل الكتابة فوق مؤشر B's *pContainer

  6. حذف ملف B بحيث يتم استدعاء RemoveConainter ويتم نقل التنفيذ إلى B's *pContainer الذي أصبح تالفًا الآن ويشير إلى كود مخصص من قبل المستخدم.

الصورة التالية تصور الخطوات المذكورة سابقًا.

steps_first

الكود - الحصول على تخطيط ذاكرة صحيح

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

allocate_clfs

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

find_tw_files

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

malform_first_file_o

تعديل ترويسات A وإعادة حساب تجزئتها

تحتاج هذه الخطوة إلى مزيد من التوضيح لأننا نحتاج إلى حساب بعض القيم الدقيقة التي، عند تشغيل الكود القابل للاستغلال في 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 التي يجب الكتابة فوقها؟ --> هناك فئتان من الحقول:

  1. حقول لإجبار شرط IF في AllocSymbol على التقييم إلى FALSE
  2. الحقل المقابل لـ v9 لحساب المسافة الدقيقة إلى B's *pContainer

لن يتم تفصيل الحقول في الفئة 1. يقابل V9 الحقل الموجود عند الإزاحة 0x1b98 على القرص داخل ملف A. لن يكون هناك مزيد من التفاصيل حول سبب قيام هذه الحقول بتشغيل الاستغلال؛ القارئ مدعو للاطلاع على ذلك إذا كان مهتمًا.

إليك القيم التي يجب تعديلها:

disk_a_modification

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

لاحظ أن v10 سيُستخدم أيضًا في memset بكتابة 00 لمنطقة ذاكرة طولها 0xa0

first_memset

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

overwrite_constant

لاحظ القيمة الثابتة التي تكون بالشكل 0x30c1fdf006X0

يحدث هذا عند إضافة حاوية سجل إلى ملف A، لكننا لم نتطرق إلى عملية إعادة حساب التجزئة بعد تعديل حقول A.

إن عرض ملف السجل على القرص مختلف إلى حد ما عن عرضه في الذاكرة. نحن نعدّل فقط محتويات base block الذي يبلغ طوله 0x7a00 ويبدأ على القرص عند إزاحة الملف 0x800. خوارزمية التجزئة المستخدمة لتجزئة الكتلة الأساسية هي CRC32. الحقل الذي يحمل قيمة التجزئة مخزّن أيضًا داخل الكتلة الأساسية عند الإزاحة 0x80c.

إجراء إعادة حساب التجزئة:

  1. تصفير قيمة CRC32 القديمة عند 0x80c
  2. تعديل الحقول وفقًا للقيم والإزاحات المثبتة
  3. حساب CRC32 للكتلة الأساسية بدءًا من 0x800 وطول 0x7a00
  4. استبدال القيمة التي تم تصفيرها بقيمة CRC32 الجديدة

الكود الكامل المسؤول عن تشغيل الاستغلال

لتوجيه تنفيذ الكود إلى مساحة المستخدم نحتاج إلى:

  1. الحصول على تخطيط الذاكرة لملفي CLFS الخاصين بـ A وB (تم شرحه مسبقًا)
  2. إغلاق A، وتعديله على القرص بترويسات تالفة، وإعادة فتحه (تم شرحه)
  3. إضافة حاوية سجل إلى B (المسمى second في الكود) لتهيئة بنى الحاوية. يجب أن تكون الحاوية ملف سجل عاديًا (أحد الملفات الخمسين المخصصة سابقًا)
  4. تحضير ذاكرة المستخدم التي ستمثل مسار التنفيذ المعدَّل (سيتم شرحه لاحقًا)
  5. إضافة حاوية سجل إلى A (المسمى first في الكود) باستخدام ملف سجل عادي كحاوية. إضافة حاوية إلى النسخة المشوهة من A ستؤدي إلى الكتابة فوق قيمة B's (second) *pConainter بقيمة ثابتة من الشكل 0x30c1fdf006X0.
  6. ضبط B (second) ليتم حذفه تلقائيًا بعد الإغلاق باستخدام NtSetInformationFile. من الضروري استخدام هذه الواجهة البرمجية، لأن مجرد إغلاق مقبضها لن يؤدي إلى تشغيل واجهة RemoveContainer.

ntsetinfofile

  1. إغلاق B وتشغيل إعادة التوجيه.

رش ذاكرة المستخدم وتخطيطها

هناك بعض الشروط المفروضة على ذاكرة المستخدم التي هي هدف إعادة توجيه تدفق التنفيذ باستخدام *pContainer. لا يمكننا ببساطة البدء في تنفيذ تعليمات عشوائيًا من مساحة المستخدم بينما البرنامج في وضع kernel.

الغرض من هذا القسم هو تسريب قيمة SystemToken في مساحة المستخدم، دون التسبب في تعطل النظام (BSOD).

كيف يمكنك القراءة من عنوان في kernel وتخزين النتيجة في مساحة المستخدم؟ تذكير سريع بقسم الأنابيب (Pipe) في البرنامج التعليمي:

second_pipes

هنا نقوم بـ:

  1. تخصيص أنبوب (كائن kernel) بحجم كبير بما يكفي ليتم تخصيصه داخل BigPool
  2. كتابة بعض المعلومات في قسم سمات الأنبوب (سلسلة الحروف BCDEFBBBB)
  3. الحصول على عنوان كائن الأنبوب في مساحة المستخدم باستخدام حقل Tag لصفحات BigPool
  4. دراسة بنية الأنبوب في Kernel باستخدام WinDbg
  5. قراءة البيانات مرة أخرى من kernel إلى مساحة المستخدم باستخدام NtFsControlFile PipeReadAttribute

الفكرة التي تربط كائن الأنبوب بهدفنا في الحصول على قيمة رمز النظام هي استخدام NtFsControlFile PipeReadAttribute للقراءة من عنوان رمز النظام بدلاً من بداية المخزن المؤقت الذي يحمل المعلومات التي مررناها إلى kernel باستخدام PipeWrite Attribute.

هذا يعني أنه يجب علينا تعديل قيمة المؤشر عند العنوان PIPE_BEGIN+=0x20 ليحمل العنوان الذي يقع فيه رمز النظام.

العنوان معروف، تم الحصول عليه في المراحل السابقة من البرنامج التعليمي، عندما حددنا وحللنا بنية EPROCESS.

هنا نحتاج إلى إيجاد آلية تسمح لنا بالكتابة في بنية الأنبوب في موقع ثابت.

لهذا نستخدم إعادة توجيه الكود التي تم الحصول عليها عبر استغلال دالة CLFS AddLogConainer. قد يظن المرء أنه يكفي كتابة شيلكود يقوم بالاستبدال المباشر، لكن (على الرغم من أنه لم يتم اختباره) من المحتمل ألا يعمل ذلك. وذلك لأن برنامج التشغيل يعمل في سياق Kernel وينفذ كودًا من منطقة عملية مستخدم.

لتجاوز هذا القيد، يجب علينا إيجاد ROP's خاصة بـ Kernel تحقق كتابة قيمة عشوائية. أي العثور على بعض أجزاء كود kernel الموجودة في نهاية دالة وتنتهي بتعليمة ret، وتوفير عناوينها كأهداف لإعادة التوجيه. بهذه الطريقة يظل الكود منفذًا بواسطة Kernel.

هذه هي الفكرة الرئيسية، لكن تظهر قيود من الطريقة التي يستدعي بها برنامج تشغيل CLFS الكود داخل *pConainer:

mem_spray_user

لصياغة نمط ذاكرة قابل للاستخدام، يجب دراسة الطريقة التي تصل بها دالة 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، سيتم حسابها لاحقًا).

هذه هي خوارزمية توليد نمط الرش.

algo_spray_first_stage

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

هكذا تبدو المرحلة الأولى من الذاكرة المرشوشة في المصحح (لاحظ أين توضع القيمة 0x5000000 --> محاذاة X0):

sprayed_mem_constant

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

sprayed_mem_ROP

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

debugger_redir

لاحظ قيمة سجل RDI وعنوان تعليمة التنفيذ في المصحح. RDI بعد إلغاء الإشارة مرة واحدة هو 0x5000000 ويُخزن في RAX. [RAX+0x18] هو ROP الثاني، وفي الأسفل [RAX+0x8] سيكون عنوان ROP الأول.

ROP's الخاصة بـ Kernel: ما هي ولماذا مفيدة

في هذه المرحلة، لدينا القطع التالية من الاستغلال التي سنربطها معًا:

  1. عنوان رمز النظام
  2. إعادة توجيه الكود باستخدام استغلال CLFS
  3. أنبوب مع كائن سمات

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

سندرس وظيفتين:

  1. SeSetAccessStateGenericMapping في ntoskrnl.exe تُستدعى ثانيًا ([rax+0x8])
  2. ClfsEarlierLsn في CLFS.SYS تُستدعى أولًا ([rax+0x18])

تحليل ClfsEarlierLSn:

ealier

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

تحليل SeSetAccessStateGenericMapping:

sesetAccess

يجب مناقشة هذه الوظيفة سطرًا بسطر بالتفصيل.

المدخلات: عند الدخول إلى هذه الوظيفة:

  1. قيمة RAX لا تهم لأنها تُستبدل في السطر الأول من الوظيفة
  2. RCX هو قيمة RDI، وهو الثابت 0x30C1FDF006X0
  3. [RCX+0x48] يكون على الشكل 0x30C1FDF00YX8، وهو دائمًا محاذٍ لـ 0x8 ولن يتعارض مع 0x30C1FDF006X0 المحاذي لـ 0 والذي يحتوي على 0x5000000

تنفيذ كود الوظيفة:

  1. mov rax, [rcx+48h] سيقوم بإلغاء الإشارة من 0x30C1FDF00YX8 ونقل القيمة إلى RAX
  2. movups xmm0, xmmword ptr [rdx] سينقل 16 بايتًا من RDX إلى سجل XMM0. تذكر هنا أنه من ClfsEarlierLSn تكون قيمة RDX هي 0xFFFFFFFF
  3. movdqu xmmword ptr [rax+8], xmm0 سينقل إلى [RAX+0x8] قيمة XMMO

في الأساس، هذا يقرأ من عنوان ويخزن داخل النواة. ويمكن تفسيره على النحو التالي:

mem_move

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

إليك كيف يتم تحقيق ذلك في الكود الفعلي:

mem_pattern

جانب آخر لم يتم شرحه: كيف تحصل على عناوين النواة لوظائف ROP في مساحة المستخدم؟

إنها خدعة. أولًا، يمكنك الحصول على العنوان الأساسي لأي وحدة نواة باستخدام NtQuerySystemInformation مع المعاملات التالية: NtQuerySystemInformation(SystemModuleInformation, (HANDLE)ModuleInfo, ModuleInfoSize, &retlen)

ثم تقوم بالتصفية حسب قيمة ModuleInfo->Modules[i].Name لمطابقة المكتبة المطلوبة. وهذا يعطيك العنوان الأساسي في النواة.

ثم نستغل حقيقة أن الإزاحة بين العنوان الأساسي وعنوان التصدير ثابتة بغض النظر عن مساحة العنوان التي يُحمَّل فيها الملف التنفيذي.

نقوم بتحميل الوحدة في مساحة المستخدم (يمكنك فعل ذلك) باستخدام LoadLibrary ونحصل على عنوان التصدير باستخدام Getprocaddress. نحسب الفرق بين عنوان التصدير والقاعدة في مساحة المستخدم، ونضيفه إلى قاعدة النواة (التي تم العثور عليها باستخدام NtQuerySystemInformation). وهكذا نحصل في مساحة المستخدم على عنوان التصدير المحمَّل في مساحة النواة.

قراءة قيمة رمز النظام

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

read_token

ماذا بعد؟

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

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

خطوات استبدال قيمة الرمز الخاص بنا:

  1. تحديد مكان تخزين الرمز الخاص بنا داخل النواة. (على غرار المرة الأولى حيث حددنا عنوان رمز النظام)
  2. تنفيذ رشّ ذاكرة النظام ورشّ الكائنات بملفات CLFS مرة أخرى
  3. داخل الذاكرة التي تم رشّها في مساحة المستخدم، اضبط محتويات المخزن المؤقت بحيث يكون DESTINATION_ADDRESS هو عنوان رمز النظام الخاص بنا، ويكون عنوان المصدر (عند العنوان 0xFFFFFFFF) هو قيمة رمز النظام التي تم الحصول عليها من أول تشغيل للاستغلال
  4. قم بتشغيل استغلال CLFS مرة ثانية
  5. شغّل cmd.exe وتحقق من الصلاحيات

بما أننا لا نحتاج إلى قراءة من النواة في المرة الثانية، فإننا لا نحتاج إلى استخدام الأنبوب هذه المرة.

وبما أن الخطوات لا تتطلب معرفة إضافية، يمكن أن ينتهي هذا البرنامج التعليمي هنا مع إثبات نهائي على cmd.exe بصلاحيات nt authority\system

final_proof

خلاصة

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

تنزيل الأداة