إطار نواة الجسر: الجسر في الحلقة 0

هل سبق لك أن أردت النظر تحت غطاء نظام التشغيل ، والنظر في الهيكل الداخلي لآلياته ، وتحريف البراغي وإلقاء نظرة على الفرص التي فتحت؟ ربما أرادوا العمل مباشرة مع الأجهزة ، لكنهم اعتقدوا أن السائقين هم علم الصواريخ؟

أقترح أن أسير على طول الجسر حتى القلب ونرى مدى عمق حفرة الأرنب.

لذا ، أقدم إطار عمل محرك القرصنة لقرص kernel ، المكتوب بلغة C ++ 17 ، ومصممًا ، إذا أمكن ، لإزالة الحواجز بين وضع kernel ووضع المستخدم أو لتخفيف وجودهما قدر الإمكان. وأيضًا ، مجموعة من واجهات برمجة تطبيقات وضع المستخدم و kernel والأغلفة من أجل التطوير السريع والمريح في Ring0 للمبتدئين والمبرمجين المتقدمين.

الملامح الرئيسية:

  • الوصول إلى منافذ الإدخال / الإخراج ، بالإضافة إلى إعادة توجيه الدخول والخروج وتعليمات cli و sti إلى وضع المستخدم عبر IOPL
  • يلتف فوق نظام تويتر
  • الوصول إلى MSR (السجلات الخاصة بالنموذج)
  • مجموعة من الوظائف للوصول إلى ذاكرة وضع المستخدم للعمليات الأخرى وذاكرة kernel
  • العمل مع الذاكرة الفعلية ، DMI / SMBIOS
  • إنشاء نمط المستخدم والتدفقات النووية ، تسليم APC
  • استدعاءات وضع المستخدم *** و Ps *** وفلاتر نظام الملفات
  • قم بتنزيل برامج تشغيل غير موقعة ومكتبات kernel

... وأكثر من ذلك بكثير.

وسنبدأ بتحميل وتوصيل الإطار بمشروع C ++ الخاص بنا.

جيثب

للتجميع ، من المستحسن للغاية استخدام أحدث إصدار من Visual Studio وأحدث WDK (Windows Driver Kit) المتاح ، والذي يمكن تنزيله من موقع Microsoft الرسمي على الويب .

للاختبار ، يعد برنامج VMware Player المجاني مع Windows المثبت ، الذي لا يقل عن Windows 7 ، بأي سعة ، مثاليًا.

التجميع تافه ولن يسبب أسئلة:

  1. افتح Kernel-Bridge.sln
  2. اختر عمق البت المطلوب
  3. Ctrl + Shift + B

ونتيجة لذلك ، نحصل على برنامج تشغيل ومكتبة في وضع المستخدم بالإضافة إلى ملفات الأداة المساعدة ذات الصلة ( * .inf للتثبيت اليدوي ، * .cab لتوقيع برنامج التشغيل على Microsoft Hardware Certification Publisher ، إلخ).

لتثبيت برنامج التشغيل (إذا لم يكن هناك توقيع رقمي ضروري لـ x64 - شهادة EV المقابلة) ، فأنت بحاجة إلى وضع النظام في وضع الاختبار ، وتجاهل التوقيع الرقمي لبرامج التشغيل. للقيام بذلك ، قم بتشغيل سطر الأوامر كمسؤول:

bcdedit.exe /set loadoptions DISABLE_INTEGRITY_CHECKS
bcdedit.exe /set TESTSIGNING ON

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

اكتمل إعداد بيئة الاختبار ، فلنبدأ في استخدام واجهة برمجة التطبيقات في مشروعنا.

يحتوي الإطار على التسلسل الهرمي التالي:

/ Kernel-Bridge / API - مجموعة من الوظائف للاستخدام في برامج التشغيل ووحدات kernel ، وليس لها تبعيات خارجية ويمكن استخدامها بحرية في مشاريع الطرف الثالث
/ User-Bridge / API - مجموعة من أغلفة وضع المستخدم فوق برنامج التشغيل ووظائف الأداة المساعدة للعمل مع ملفات PE ، أحرف PDB ، إلخ.
/ SharedTypes / - كل من وضع المستخدم والعناوين النووية التي تحتوي على الأنواع الشائعة الضرورية

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

لذا ، قم بإنشاء مشروع وحدة تحكم في C ++ ، وقم بتوصيل ملفات الرأس الضرورية وتحميل برنامج التشغيل:

 #include <Windows.h> #include "WdkTypes.h" //    x32/x64    WDK #include "CtlTypes.h" //   IOCTL-   #include "User-Bridge.h" // API,   int main() { using namespace KbLoader; BOOL Status = KbLoadAsFilter( L"X:\\Folder\\Path\\To\\Kernel-Bridge.sys", L"260000" //       ); if (!Status) return 0; //   ! //     API ... // : KbUnload(); return 0; } 

عظيم! الآن يمكننا استخدام واجهة برمجة التطبيقات والتفاعل مع النواة.
لنبدأ بالوظيفة الأكثر شيوعًا في بيئة مطوري الغش - قراءة وكتابة ذاكرة عملية أخرى:

 using namespace Processes::MemoryManagement; constexpr int Size = 64; BYTE Buffer[Size] = {}; BOOL Status = KbReadProcessMemory( //  KbWriteProcessMemory,   ProcessId, 0x7FFF0000, //     ProcessId &Buffer, Size ); 

لا شيء معقد! دعونا ننزل إلى مستوى واحد - قراءة وكتابة الذاكرة النووية:

 using namespace VirtualMemory; constexpr int Size = 64; BYTE Buffer[Size]; //  "",  ""    , //       : BOOL Status = KbCopyMoveMemory( reinterpret_cast<WdkTypes::PVOID>(Buffer), //  0xFFFFF80000C00000, //  Size, FALSE //  ,     ); 

ماذا عن وظائف التفاعل مع الحديد؟ على سبيل المثال ، منافذ الإدخال / الإخراج.

سنقوم بإعادة توجيههم إلى وضع المستخدم من خلال تجميع 2 بت IOPL في سجل EFlags ، وهي مسؤولة عن مستوى الامتياز الذي تتوفر فيه تعليمات الدخول / الخروج / cli / sti .

وبالتالي ، سنتمكن من تنفيذها في وضع المستخدم بدون خطأ التعليمات المميزة:

 #include <intrin.h> using namespace IO::Iopl; //  ,   ! KbRaiseIopl(); //  in/out/cli/sti   ! ULONG Frequency = 1000; // 1 kHz ULONG Divider = 1193182 / Frequency; __outbyte(0x43, 0xB6); //     //      : __outbyte(0x42, static_cast<unsigned char>(Divider)); __outbyte(0x42, static_cast<unsigned char>(Divider >> 8)); __outbyte(0x61, __inbyte(0x61) | 3); //   (   ) for (int i = 0; i < 5000; i++); //   Sleep(),  IOPL      ! __outbyte(0x61, __inbyte(0x61) & 252); //   KbResetIopl(); 

لكن ماذا عن الحرية الحقيقية؟ بعد كل شيء ، غالبًا ما يرغب المرء في تنفيذ التعليمات البرمجية التعسفية بامتيازات kernel. نكتب كل كود النواة في وضع المستخدم وننقل التحكم إليه من النواة (يتم إيقاف تشغيل SMEP تلقائيًا ، قبل استدعاء برنامج التشغيل يحفظ سياق FPU ويتم إجراء المكالمة نفسها داخل كتلة try..except باستثناء):

 using namespace KernelShells; //    KeStallExecutionProcessor: ULONG Result = 1337; KbExecuteShellCode( []( _GetKernelProcAddress GetKernelProcAddress, PVOID Argument ) -> ULONG { //      Ring0 //     : using _KeStallExecutionProcessor = VOID(WINAPI*)(ULONG Microseconds); auto Stall = reinterpret_cast<_KeStallExecutionProcessor>( GetKernelProcAddress(L"KeStallExecutionProcessor") ); Stall(1000 * 1000); //      ULONG Value = *static_cast<PULONG>(Argument); return Value == 1337 ? 0x1EE7C0DE : 0; }, &Result, // Argument &Result // Result ); //   Result = 0x1EE7C0DE 

ولكن إلى جانب التدليل على الأصداف ، هناك أيضًا وظيفة جادة تسمح لك بإنشاء DLP بسيط استنادًا إلى النظام الفرعي لمرشحات الملف والكائن والمعالجة.

يسمح لك الإطار بتصفية CreateFile / ReadFile / WriteFile / DeviceIoControl ، بالإضافة إلى أحداث فتح / تكرار المقابض ( ObRegisterCallbacks ) وأحداث بدء العمليات / الخيوط وتحميل الوحدات ( PsSet *** NotifyRoutine ). سيسمح هذا ، على سبيل المثال ، بحظر الوصول إلى الملفات العشوائية أو استبدال معلومات حول الأرقام التسلسلية للقرص الثابت.

مبدأ العمل:

  1. يقوم برنامج التشغيل بتسجيل مرشحات الملفات وتثبيت عمليات الاسترجاع Ob *** / Ps ***
  2. يفتح برنامج التشغيل منفذ اتصال يتصل به العملاء الراغبون في الاشتراك في حدث
  3. يتم إرفاق تطبيقات وضع المستخدم بالمنفذ وتتلقى بيانات من برنامج التشغيل حول الحدث الذي حدث ، وإجراء التصفية (معالجات اقتطاع الحقوق ، ومنع الوصول إلى الملف ، وما إلى ذلك) وإعادة الحدث إلى kernel
  4. يقوم السائق بتطبيق التغييرات المستلمة.

مثال على الاشتراك في ObRegisterCallbacks وقطع الوصول إلى العملية الحالية:

 #include <Windows.h> #include <fltUser.h> #include "CommPort.h" #include "WdkTypes.h" #include "FltTypes.h" #include "Flt-Bridge.h" ... //   ObRegisterCallbacks: CommPortListener<KB_FLT_OB_CALLBACK_INFO, KbObCallbacks> ObCallbacks; //        PROCESS_VM_READ: Status = ObCallbacks.Subscribe([]( CommPort& Port, MessagePacket<KB_FLT_OB_CALLBACK_INFO>& Message ) -> VOID { auto Data = static_cast<PKB_FLT_OB_CALLBACK_INFO>(Message.GetData()); if (Data->Target.ProcessId == GetCurrentProcessId()) { Data->CreateResultAccess &= ~PROCESS_VM_READ; Data->DuplicateResultAccess &= ~PROCESS_VM_READ; } ReplyPacket<KB_FLT_OB_CALLBACK_INFO> Reply(Message, ERROR_SUCCESS, *Data); Port.Reply(Reply); //   }); 

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

توجد جميع واجهات برمجة التطبيقات والأغلفة في المجلد المقابل: / Kernel-Bridge / API /
وهي تشمل العمل مع الذاكرة ، والعمليات ، مع الأوتار والأقفال ، وأكثر من ذلك بكثير ، تبسيط تطوير برامج التشغيل الخاصة بهم إلى حد كبير. تعتمد واجهات برمجة التطبيقات والأغلفة على نفسها فقط ولا تعتمد على البيئة الخارجية: يمكنك استخدامها بحرية في برنامج التشغيل الخاص بك.

مثال على العمل مع الأوتار في النواة هو حجر عثرة لجميع المبتدئين:

 #include <wdm.h> #include <ntstrsafe.h> #include <stdarg.h> #include "StringsAPI.h" WideString wString = L"Some string"; AnsiString aString = wString.GetAnsi().GetLowerCase() + " and another string!"; if (aString.Matches("*another*")) DbgPrint("%s\r\n", aString.GetData()); 

إذا كنت تريد تنفيذ معالجك الخاص لرمز IOCTL الخاص بك ، فيمكنك القيام بذلك بسهولة وفقًا للمخطط التالي:

  1. اكتب معالجًا في /Kernel-Bridge/Kernel-Bridge/IOCTLHandlers.cpp
  2. في نفس الملف ، أضف معالجًا إلى نهاية صفيف معالجات في دالة DispatchIOCTL
  3. قم بإضافة فهرس الاستعلام إلى تعداد Ctls :: KbCtlIndices في CtlTypes.h في الموقع نفسه كما في صفيف معالجات في العنصر 2
  4. اتصل بمعالجك من وضع المستخدم من خلال كتابة غلاف في User-Bridge.cpp ، وإجراء مكالمة باستخدام وظيفة KbSendRequest

يتم دعم جميع أنواع I / O الثلاثة (METHOD_BUFFERED و METHOD_NEITHER و METHOD_IN_DIRECT / METHOD_OUT_DIRECT) ، بشكل افتراضي ، يتم استخدام METHOD_NEITHER.

هذا كل شيء! تغطي المقالة جزءًا صغيرًا فقط من جميع الاحتمالات. آمل أن يكون الإطار مفيدًا للمطورين المبتدئين لمكونات kernel والمهندسين العكسيين ومطوري الغش ومكافحة الغش والحماية.

وأيضاً ، الجميع مدعو للمشاركة في التطوير. في الخطط المستقبلية:

  • أغلفة للمعالجة المباشرة لسجلات PTE وإعادة توجيه الذاكرة النووية إلى وضع المستخدم
  • الحاقنات بناءً على ميزات إنشاء تدفق APC الحالية وتسليمها
  • منصة GUI للهندسة العكسية الحية وأبحاث Windows kernel
  • محرك برمجة لتنفيذ قطع من كود النواة
  • دعم SEH في الوحدات المحملة ديناميكيًا
  • اجتياز اختبارات HLK

شكرا لكم على اهتمامكم!

Source: https://habr.com/ru/post/ar429198/


All Articles