内核-桥接框架:Ring0中的桥接

您是否曾经想过看一下操作系统的内幕,看一下其机制的内部结构,拧紧螺钉并看一下已经打开的机会? 也许他们甚至想直接使用硬件,但认为驱动程序是火箭科学?

我建议沿着桥走到核心,看看兔子洞有多深。

因此,我介绍了用C ++ 17编写的内核黑客驱动程序框架,并在可能的情况下设计了该框架,以消除内核和用户模式之间的障碍或尽可能地消除它们的存在。 此外,还提供了一组用户模式和内核API和包装器,以供初学者和高级程序员在Ring0中快速便捷地进行开发。

主要特点:

  • 访问I / O端口,以及通过IOPL将inoutclisti指令转发到用户模式
  • 环绕系统高音扬声器
  • 访问MSR (特定于模型的寄存器)
  • 一组用于访问其他进程的用户模式内存和内核内存的函数
  • 使用物理内存, DMI / SMBIOS
  • 创建用户模式和核流量,APC交付
  • 用户模式Ob ***Ps ***回调以及文件系统过滤器
  • 下载未签名的驱动程序和内核库

...还有更多。

我们将从加载框架并将其连接到我们的C ++项目开始。

Github

对于组装,强烈建议使用最新版本的Visual Studio和最新可用的WDK(Windows驱动程序工具包),可以从Microsoft官方网站上下载这些文件。

对于测试,任何容量的免费VMware播放器都安装了Windows,但不低于Windows 7,是完美的。

该程序集很简单,不会引起任何问题:

  1. 打开内核桥
  2. 选择所需的位深度
  3. Ctrl + Shift + B

结果,我们获得了驱动程序,用户模式库以及相关的实用程序文件(用于手动安装的* .inf ,用于在Microsoft Hardware Certification Publisher上对驱动程序进行签名的* .cab等)。

要安装驱动程序(如果x64不需要数字签名-相应的EV证书),则需要将系统置于测试模式,而忽略驱动程序的数字签名。 为此,以管理员身份运行命令行:

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

...然后重新启动计算机。 如果一切操作正确,Windows处于测试模式的右下角将出现一个题词。

完成测试环境的设置,让我们开始在项目中使用API​​。

该框架具有以下层次结构:

/ Kernel-Bridge / API-一组用于驱动程序和内核模块的函数,没有外部依赖性,可以在第三方项目中自由使用
/ 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; } 

太好了! 现在,我们可以使用API​​并与内核进行交互。
让我们从作弊开发人员环境中最流行的功能开始-读写另一个进程的内存:

 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 //  ,     ); 

与铁相互作用的功能如何? 例如,I / O端口。

我们通过在EFlags寄存器中锁定2个IOPL位将它们转发到用户模式,这些位负责in / out / 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(); 

但是真正的自由呢? 毕竟,人们经常想用内核特权执行任意代码。 我们在用户模式下编写所有内核代码,然后从内核转移控制权(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. 用户模式应用程序连接到端口,并从驱动程序接收有关发生的事件的数据,执行过滤(截断权限中的句柄,阻止对文件的访问等),然后将事件返回内核
  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); //   }); 

因此,我们简要介绍了该框架的用户模式部分的要点,但核心API仍然处于幕后。

所有API和包装器都位于相应的文件夹中: / Kernel-Bridge / API /
它们包括使用内存,使用进程,使用字符串和锁以及更多内容,从而大大简化了自己的驱动程序的开发。 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函数中Handlers数组的末尾添加一个处理程序
  3. 将查询索引添加到SAME POSITION中CtlTypes.h中的Ctls :: KbCtlIndices枚举中,如项目2中的Handlers数组中所示
  4. 通过在User-Bridge.cpp中编写包装器,从用户模式调用处理程序,并使用KbSendRequest函数进行调用

支持所有三种类型的I / O(METHOD_BUFFERED,METHOD_NEITHER和METHOD_IN_DIRECT / METHOD_OUT_DIRECT),默认情况下,使用METHOD_NEITHER。

仅此而已! 本文仅涵盖所有可能性的一小部分。 我希望该框架对内核组件的新手,逆向工程师,作弊,反作弊和防护的开发人员有用。

并且,每个人都应邀参与开发。 在未来的计划中:

  • 用于直接处理PTE记录并将核内存转发到用户模式的包装器
  • 基于现有APC流程创建和传递功能的注入器
  • 用于实时逆向工程和Windows内核研究的GUI平台
  • 用于执行部分内核代码的脚本引擎
  • 在动态加载的模块中支持SEH
  • 通过HLK测试

感谢您的关注!

Source: https://habr.com/ru/post/zh-CN429198/


All Articles