Windows内核中的猫鼠游戏,Part2

匿名者  830天前

在本文中,我们将为读者详细介绍“驱动程序的构造和功能”,并尝试实现我们自己的内核钩子。

1. Windows内核编程101

在本系列的第一篇文章中,我们研究了EDR是如何与用户和内核空间进行交互的,以及如何利用@fdiskyou的“WindowsKernel Ps Callback Experiments”项目给内存中的代码打补丁,同时,探索了一种称为内核回调的常用功能。实际上,内核回调只是现代EDRAV解决方案在部署内核驱动程序以识别恶意活动时的第一道防线。为了更好地理解我们的对手,我们需要后退一步,先来熟悉一下驱动程序本身的相关概念。

为此,我本周花了大部分时间来拜读Pavel Yosifovich的《Windows内核编程》一书,这本书非常好地介绍了Windows内核及其组件和机制,以及驱动程序及其结构和功能。

在这篇文章中,我想仔细考察驱动程序的构成,并尝试一种不同的钩子技术,称为IRP MajorFunction钩子技术。

2. 深入剖析驱动程序的构造

我们大多数人都熟悉经典的C/C++项目及其特点;例如,int main(intargc, char* argv[]){ return 0; }函数,它是C++控制台应用程序的典型入口点。那么,它跟普通程序的区别在哪里呢?

就像C++控制台应用程序一样,驱动程序也需要入口点。这个入口点以DriverEntry()函数的形式出现;该函数的原型如下所示:

NTSTATUS DriverEntry(_In_ PDRIVER_objectDriverobject, _In_ PUNICODE_STRING RegistryPath);

DriverEntry()函数主要负责2项任务:

  1.     设置驱动程序的Deviceobject和相关的符号链接
  2.     设置调度例程

每个驱动程序都需要一个“端点(endpoint)”,以供其他应用程序与驱动程序进行通信。端点以Deviceobject的形式出现,它是DEVICE_object结构体的一个实例。Deviceobject通常以符号链接的形式被抽象出来,并注册在对象管理器的GLOBAL??目录下(我们可以使用sysinternalWinObj工具来查看对象管理器)。用户模式的应用程序可以使用像NtCreateFile这样的函数,用符号链接作为句柄来与驱动程序进行通信。

winobj.png

下面是使用CreateFile与注册为registered as的驱动程序(提示:这就是我的驱动程序)进行交互的C++应用程序示例:

HANDLE hDevice =CreateFile(L"\\\\.\\Interceptor)", GENERIC_WRITE | GENERIC_READ, 0,nullptr, OPEN_EXISTING, 0, nullptr);

一旦驱动程序的端点被配置好,DriverEntry()函数就需要确定如何处理来自用户模式的通信和其他操作,如卸载自己。为此,它可以使用Driverobject来注册调度例程,或与特定驱动程序操作相关的函数。

Driverobject包含一个数组,其中保存的是函数指针,称为MajorFunction数组。这个数组决定了驱动程序支持哪些特定的操作,如创建、读取、写入等。MajorFunction数组的索引由主功能码(MajorFunction codes)控制,它们是通过其IRP_MJ_前缀定义的。

除了DriverUnload操作外,还有3个重要的主功能码必须初始化,否则驱动程序将无法正常工作,它们分别是:

// prototypes

void InterceptUnload(PDRIVER_object);

NTSTATUS InterceptCreateClose(PDEVICE_object,PIRP);

NTSTATUSInterceptDeviceControl(PDEVICE_object, PIRP);

 

//DriverEntry

extern "C" NTSTATUS

DriverEntry(PDRIVER_object Driverobject,PUNICODE_STRING RegistryPath) {

   Driverobject->DriverUnload = InterceptUnload;

   Driverobject->MajorFunction[IRP_MJ_CREATE] = InterceptCreateClose;

   Driverobject->MajorFunction[IRP_MJ_CLOSE] =  InterceptCreateClose;

   Driverobject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = InterceptDeviceControl;

 

   //...

Driverobject->DriverUnload调度例程负责在驱动程序卸载前完成清理工作,以防止发生内存泄漏。内核中的泄漏将持续到机器重新启动。主功能IRP_MJ_CREATEIRP_MJ_CLOSE用来处理CreateFile()CloseHandle()调用。没有它们,驱动程序的句柄将无法创建或销毁,因此在某种程度上来说,驱动程序将无法使用。最后,主功能IRP_MJ_DEVICE_CONTROL负责I/O操作和各种通信。

一个典型的驱动程序通过接收请求进行通信,处理这些请求或将它们转发给设备堆栈中适当的设备(这些内容已经超出本文的范围)。这些请求以I/O请求包或IRP的形式出现,IRP是一个半文档化的结构体,伴随着一个或多个IO_STACK_LOCATION结构体,位于IRP之后的内存中。每个IO_STACK_LOCATION都与设备堆栈中的一个设备有关,驱动程序可以调用IoGetCurrentIrpStackLocation()函数来检索与自己有关的IO_STACK_LOCATION

前面提到的调度例程决定了这些IRP是如何被驱动程序处理的。就本文来说,我们主要对IRP_MJ_DEVICE_CONTROL调度例程感兴趣,它对应于用户模式下的DeviceIoControl()调用或内核模式下的ZwDeviceIoControlFile()调用。一个以IRP_MJ_DEVICE_CONTROL为目标的IRP请求包含两个用户缓冲区,一个用于读取,一个用于写入,以及一个由IOCTL_前缀指示的控制码。这些控制码是由驱动程序开发人员定义的,用于表示所支持的操作。

控制码是使用CTL_CODE宏建立的,定义如下所示:

#define CTL_CODE(DeviceType, Function,Method, Access)((DeviceType) << 16 | ((Access) << 14) | ((Function)<< 2) | (Method)) 

下面是我的驱动程序Interceptor中的相关定义:

#define IOCTL_INTERCEPTOR_HOOK_DRIVERCTL_CODE(0x8000, 0x800, METHOD_BUFFERED, FILE_ANY_ACCESS)

#define IOCTL_INTERCEPTOR_UNHOOK_DRIVERCTL_CODE(0x8000, 0x801, METHOD_BUFFERED, FILE_ANY_ACCESS)

#define IOCTL_INTERCEPTOR_LIST_DRIVERSCTL_CODE(0x8000, 0x802, METHOD_BUFFERED, FILE_ANY_ACCESS)

#defineIOCTL_INTERCEPTOR_UNHOOK_ALL_DRIVERS CTL_CODE(0x8000, 0x803, METHOD_BUFFERED,FILE_ANY_ACCESS)

3. 内核空间钩子

现在,我们已经对驱动程序如何与其他驱动程序和应用程序通信有了大致的了解,接下来,我们将考察如何拦截这些通信,其中一种技术被称为IRP MajorFunction钩子技术。

hook-mfa.png

由于驱动程序和所有其他内核进程共享相同的内存空间,因此,我们也可以访问和覆盖这些内存,只要我们不修改关键结构,就不会惊扰PatchGuard。实际上,我写了一个叫做Interceptor的驱动程序,它首先定位目标驱动程序的Driverobject,并检索其MajorFunction数组(MFA)。这是用未公开的ObReferenceobjectByName()函数完成的,该函数能够通过驱动设备名称来获取指向Driverobject的指针。

UNICODE_STRING targetDriverName =RTL_CONSTANT_STRING(L"\\Driver\\Disk");

PDRIVER_object Driverobject = nullptr;

 

status = ObReferenceobjectByName(

   &targetDriverName,

   OBJ_CASE_INSENSITIVE,

   nullptr,

   0,

   *IoDriverobjectType,

   KernelMode,

   nullptr,

   (PVOID*)&Driverobject

);

 

if (!NT_SUCCESS(status)) {

   KdPrint((DRIVER_PREFIX "failed to obtain Driverobject(0x%08X)\n", status));

   return status;

一旦它获得了MFA,它将遍历所有的调度例程(IRP_MJ_),并将指向目标驱动程序的函数(0x1000-0x1003)的指针替换为我自己的指针,指向由Interceptor驱动控制的*InterceptHook函数(0x2000-0x2003)。

for (int i = 0; i <IRP_MJ_MAXIMUM_FUNCTION; i++) {

   //save the original pointer in case we need to restore it later

   globals.originalDispatchFunctionArray[i] =Driverobject->MajorFunction[i];

   //replace the pointer with our own pointer

   Driverobject->MajorFunction[i] = &GenericHook;

}

//cleanup

ObDereferenceobject(Driverobject); 

作为一个例子,我钩住了磁盘驱动程序的IRP_MJ_DEVICE_CONTROL调度例程并拦截了这些调用。

hooked-irp-disk-driver.png

这种方法可以用来拦截与任何驱动程序的通信,但很容易被检测到。一个由EDR/AV控制的驱动程序可以遍历自己的MajorFunction数组,并检查函数指针的地址,看它是否位于自己的地址范围内。如果函数指针位于它自己的地址范围之外,就意味着调度例程被钩住了。

4. 小结

要对抗内核空间中的EDR,重要的是要知道内核部分,即驱动程序中到底发生了什么。在这篇文章中,我们研究了驱动程序的结构、功能,以及它们的主要职责。我们知道,驱动程序需要与其他驱动程序和用户空间的应用程序进行通信,而这些都是通过在驱动程序的MajorFunction数组中注册的调度例程来实现的。

然后,我们简要演示了如何通过IRP MajorFunction钩子技术来拦截这些通信,这种技术用指向我们自己的函数的指针来替换内存中目标驱动程序的调度例程的地址,因此,我们可以通过该技术来检查或重定向这些通信内容。


原文地址:https://blog.nviso.eu/2021/10/29/kernel-karnage-part-2-back-to-basics/

最新评论

昵称
邮箱
提交评论