详解Windows渗透测试工具Mimikatz的内核驱动 - 嘶吼 RoarTalk – 网络安全行业综合服务平台,4hou.com

详解Windows渗透测试工具Mimikatz的内核驱动

41yf1sh 资讯 2020-01-26 12:23:27
1046087
收藏

导语:本文详细分析Mimidrv提供的功能,介绍核心概念,并提出用于缓解基于驱动的威胁的防范建议。

一、概述

Mimikatz通过其中包含的Mimidrv驱动程序,提供了利用内核模式的功能。Mimidrv是已经签名的Windows驱动模型(WDM)内核模式软件驱动程序,在相关命令前加上感叹号(!)即可与标准Mimikatz可执行文件共同使用。Mimidrv没有相应文档,并且很少被人使用,但它却提供了一个非常值得关注的方式,让我们可以在ring 0执行一些操作。

本文详细分析Mimidrv提供的功能,提出了一些可供大家参考的文档,同时向不太了解内核的读者介绍一些核心概念,最后将提出用于缓解基于驱动的威胁的防范建议。

二、为什么要使用Mimidrv?

简而言之,拥有了内核就相当于拥有了一切。实际上,一些Windows功能往往无法从用户模式调用,例如修改正在运行进程的属性,或直接与其它已加载的驱动程序进行交互等等。而驱动程序为我们提供了一种通过用户模式应用程序来调用这些函数的方法,我们也将在本文的后面部分进行深入探讨。

三、加载Mimidrv

要使用Mimikatz驱动程序,第一步可以使用命令!+,该命令会从用户模式启动驱动程序,并请求为当前令牌分配SeLoadDriverPrivilege。

1.png

Mimikatz首先检查驱动程序在当前工作目录中是否存在,如果找到磁盘上的驱动程序,则开始创建服务。服务的创建是通过服务控制管理器(SCM)API函数来完成的。具体而言,advapi32!ServiceCreate将用于注册具有以下属性的服务:

CreateService(
              hSC, //Handle to the SCM database provided by OpenSCManager
              'mimidrv', //Service name
              'mimikatz driver (mimidrv)', //Service display name
              READ_CONTROL | WRITE_DAC | SERVICE_START,  //Desired access
              SERVICE_KERNEL_DRIVER,  //Kernel driver service type
              SERVICE_AUTO_START, //Start the service automatically on boot
              SERVICE_ERROR_NORMAL, //Log driver errors that occur during startup to the event log
              'C:\\path\\to\\mimidrv.sys', //Absolute path of the driver on disk 
              NULL,  //Load order group (unused)
              NULL,  //Not used because the previous argument is NULL
              NULL,  //No dependencies for the driver
              NULL,  //Use NT AUTHORITY\SYSTEM to start the service
              NULL   //Unused because we are using the SYSTEM account
);

如果成功创建了服务,则“Evervone”组将被授予对该服务的访问权限,从而允许系统上的任何用户与该服务进行交互。例如,低特权的用户可以停止该服务。

2.png

注意:我们特别要强调后期进行清理的重要性。在完成全部操作后,请记得删除驱动程序(!-),以免植入被其他人滥用。

如果成功完成,最终将通过StartService来启动服务。

3.png

四、加载后操作

服务启动后,就应该进入到Mimidrv来完成设置。该驱动程序在启动过程中没有进行任何不同寻常的操作,但如果对于没有WDM驱动程序开发经验的人来说,应该会比较复杂。

每个驱动程序都必须具有已经定义的DriverEntry函数,该函数在加载驱动程序后立即被调用,并且用于设置驱动程序要运行所需满足的要求。我们可以将其类比于用户模式代码中的main()函数。在Mimidrv的DriverEntry函数中,主要进行4项工作。

1、创建设备对象

客户端不会直接与驱动程序进行通信,而是与设备对象进行通信,因此内核模式驱动程序至少必须创建1个设备对象。然而,如果没有符号链接,用户模式代码仍然无法直接访问该设备对象。我们将在稍后详细介绍符号链接,第一步首先是要创建设备对象。

要创建设备对象,需要对nt!IoCreateDevice进行一些关键的调用。最值得关注的是第三个参数DeviceName。在globals.h中,将其设置为“mimidrv”。

在WinObj中,可以看到这个新创建的设备对象。

4.png

2、设置DispatchDeviceControl和Unload函数

如果该设备对象创建成功,则会定义DispatchDeviceControl函数,在MajorFunction分配表的IRP_MJ_DEVICE_CONTROL索引处注册,并使用MimiDispatchDeviceControl函数。这意味着,每当其接收到IRP_MJ_DEVICE_CONTROL请求(例如来自kernel32!DeviceIoControl)时,Mimidrv都会调用其内部的MimiDispatchDeviceControl函数,该函数将处理这一请求。我们将在后文的“通过MimiDispatchDeviceControl进行用户模式交互”一节中详细介绍其工作原理。

就如同每个驱动程序都必须制定一个DriveryEntry一样,在这里必须定义一个相应的Unload函数,该函数将在驱动程序被卸载时执行。

Mimidrv的DriverUnload函数非常简单,其唯一作用是删除符号链接,然后删除设备对象。

3、创建符号链接

如前文所述,如果驱动程序需要允许用户模式代码与其交互,那么就必须创建一个符号链接。用户模式应用程序将使用这个符号链接,例如通过调用nt!CreateFile和kernel32!DeviceIoControl来替代“常规”文件,以向驱动程序发送数据,或从驱动程序接收数据。

5.png

要创建符号链接,Mimidrv使用符号链接的名称和设备对象作为参数,来调用nt!IoCreateSymbolicLink。我们可以在WinObj中看到新创建的设备对象和相关的符号链接。

6.png

4、初始化Aux_klib

最后,使用AuxKlibInitialize初始化Aux_klib库。必须先完成初始化操作,然后才能调用该库中的任意函数(详细细节请参考“模块”一节)。

五、通过MimiDispatchDeviceControl进行用户模式交互

在初始化之后,驱动程序的任务仅仅是处理对其发出的请求。具体而言,是通过被称为I/O请求包(IRP)的一些不透明功能来实现的。这些IRP中包含映射到函数代码的I/O控制代码(IOCTL)。IOCTL通常是从0x8000开始,但是Mimikatz是从0x000开始,这一点与Microsoft的推荐用法相违背。Mimikatz在ioctl.h中定义了23个IOCTL,每一个IOCTL都映射到一个函数。当Mimidrv收到其中一个定义的IOCTL之后,它将会调用映射函数。这就是Mimidrv的核心功能所在。

发送IRP

为了让驱动程序执行映射到IOCTL的功能,我们必须借助此前创建的符号链接从用户模式发送IRP。Mimikatz在kuhl_m_kernel_do函数中处理该问题,该函数对nt!CreateFile进行调用,以获取设备对象的句柄,并通过kernel32!DeviceIoControl来发送IRP。这一过程将命中定义为MimiDispatchDeviceControl的IRP_MJ_DEVICE_CONTROL核心函数,并通过其IOCTL代码来查看内部定义的函数列表。在输入包含前缀“!”的命令后,将会检查KUHL_K_C结构kuhl_k_c_kernel,以获取与该命令相关联的IOCTL。该结构定义为:

typedef struct _KUHL_K_C {
    const PKUHL_M_C_FUNC pCommand;
    const DWORD ioctlCode;
    const wchar_t * command;
    const wchar_t * description;
} KUHL_K_C, *PKUHL_K_C;

在该结构中,有19个命令,其定义如下:

函数       IOCTL     命令       描述
kuhl_m_kernel_add_mimidrv      N/A       +    安装和(或)启动Mimikatz驱动程序
kuhl_m_kernel_remove_mimidrv       N/A       -    删除Mimikatz驱动程序(Mimidrv)
N/A       IOCTL_MIMIDRV_PING       ping      Ping驱动程序
N/A       IOCTL_MIMIDRV_BSOD      bsod      产生BSOD蓝屏
N/A       IOCTL_MIMIDRV_PROCESS_LIST       process  列出进程
kuhl_m_kernel_processProtect   N/A       processProtect     保护进程
kuhl_m_kernel_processToken     N/A       processToken       复制进程令牌
kuhl_m_kernel_processPrivilege  N/A       processPrivilege   设置进程的所有特权
N/A       IOCTL_MIMIDRV_MODULE_LIST       modules       列出模块
N/A       IOCTL_MIMIDRV_SSDT_LIST      ssdt       列出SSDT
N/A       IOCTL_MIMIDRV_NOTIFY_PROCESS_LIST  notifProcess  列出进程通知回调
N/A       IOCTL_MIMIDRV_NOTIFY_THREAD_LIST   notifThread   列出线程通知回调
N/A       IOCTL_MIMIDRV_NOTIFY_IMAGE_LIST     notifImage    列出映像通知回调
N/A       IOCTL_MIMIDRV_NOTIFY_REG_LIST  notifReg        列出注册表通知回调
N/A       IOCTL_MIMIDRV_NOTIFY_OBJECT_LIST    notifObject   列出对象通知回调
N/A       IOCTL_MIMIDRV_FILTER_LIST     filters     列出FS过滤器
N/A       IOCTL_MIMIDRV_MINIFILTER_LIST    minifilters      列出minifilters
kuhl_m_kernel_sysenv_set   N/A       sysenvset      系统环境变量设置
kuhl_m_kernel_sysenv_del   N/A       sysenvde       系统环境变量删除

尽管存在23个IOCTL,但在Mimikatz中只有19个命令可用。这是因为有4个与虚拟内存交互相关的函数没有映射到命令。这些IOCTL和相关函数分别是:

IOCTL_MIMIDRV_VM_READ→kkll_m_memory_vm_read
IOCTL_MIMIDRV_VM_WRITE→kkll_m_memory_vm_write
IOCTL_MIMIDRV_VM_ALLOC→kkll_m_memory_vm_alloc
IOCTL_MIMIDRV_VM_FREE→kkll_m_memory_vm_free

六、驱动程序内部功能

这些命令可以具体分为7组——通用、进程、通知、模块、过滤器、内存和SSDT。在大多数情况下(不包括通用函数),这些文件在Mimidrv源代码中按逻辑汇总,文件名格式为kkll_m_

6.1 通用

!ping

Ping命令可以用于测试将数据写入Mimidrv,以及从Mimidrv接收数据的能力。这是通过Benjamin的kprintf函数来实现的,该函数实际上只是对nt!RtlStringCbPrintfExW的简化版调用,它允许使用KIWI_BUFFER结构来保持代码简洁。

!bsod

顾名思义,这个功能可以导致蓝屏。具体是通过调用带有错误代码MANUALLY_INITIATED_CRASH的KeBugCheck来完成,该代码将显示在蓝屏中的“停止代码”(Stop Code)中。

7.png

!sysenvset & !sysenvdel

!sysenvset命令设置系统环境变量,但不是传统意义上的变量(例如:修改了%PATH%)。相反,在配置了Secure Boot的系统上,它会修改UEFI固件中保存的变量,特别是Kernel_Lsa_Ppl_Config,该变量与RunAsPPL值关联。将这个值写入到77fa9abd-0359–4d32-bd60–28f4e78f784b的GUID,这是Windows用于存储需要保护其免受用户和管理员修改的受保护存储。这一过程有效覆盖了注册表,因此,即使修改了RunAsPPL密钥并重新启动,LSASS也仍然会受到保护。

!sysenvdel进行截然相反的操作,负责删除这个环境变量。随后,删除RunAsPPL注册表项,重新启动系统,然后获得LSASS的句柄。

6.2 进程

我们真正要深入研究的第一组模块是“进程”,它允许交互并修改用户模式进程。因为我们将在本节中详细分析进程,所以从内核的角度了解它们就显得至关重要。内核中的进程以EPROCESS结构为中心,EPROCESS结构是不透明的结构,用作进程的对象。在结构内部,是我们熟悉的进程的所有属性,例如进程ID、令牌信息和进程环境块(PEB)。

8.png

内核中的EPROCESS结构通过环形双链表连接。列表头存储在内核变量PsActiveProcessHead中,并作为列表的“开始”。每个EPROCESS结构都包含一个类型为LIST_ENTRY的成员ActiveProcessLinks。LIST_ENTRY结构具有两个组件——前向链接(Flink)和后向链接(Blink)。其中,Flink指向列表中下一个EPROCESS结构的Flink。Blink指向列表中前一个EPROCESS结构的Flink。列表中最后一个结构的Flink指向PsActiveProcessHead的Flink。这将形成一个EPROCESS结构循环,我们可以以简化的图形来表示。

9.png

!process

第一个模块为我们提供了系统上正在运行的进程的列表,以及有关它们的一些其他信息。通过使用两个Windows版本特定的偏移量——EprocessNext和EprocessFlags2来遍历上面所说的链表,可以实现列举进程的目的。EprocessNext是当前EPROCESS结构中的偏移量,其中包含ActiveProcessLinks成员的地址,可以在其中读取到下一个进程的Flink(例如:Windows 10 1903中的0x02f0)。EProcessFlags2是Windows Vista引入的第二组ULONG位域,只有在Windows Vista和更高版本的系统上运行时才会显示此信息,可以为我们提供更多的细节。

(1)PrimaryTokenFrozen:如果主令牌被冻结,则使用Ternary返回“F-Tok”,否则不返回任何值。如果未设置PrimaryTokenFrozen,例如在挂起进程的情况下,则可以交换令牌。在大多数情况下,我们会发现主令牌被冻结。

(2)SignatureProtect:实际上包含两个值,分别是SignatureLevel和SectionSignatureLevel。SignatureLevel定义了柱模块的签名要求,SectionSignatureLevel定义了要加载到进程中的DLL的最低签名级别要求。

(3)Protection:Type、Audit、Signer这三个值是PS_PROTECTION结构的成员,它们表示进程的保护状态。其中最为重要的是Type,它映射到以下状态,我们可以将其认为是PP/PPL:

10.png

!processProtect

!processProtect函数是由Mimidrv提供的最为常用的功能之一。其作用是在进程(比如最常见的LSASS)中添加或删除进程保护。修改保护状态的方式相对简单:

1、使用nt!PsLookupProcessByProcessId通过PID获取进程的EPROCESS结构的句柄;

2、转到EPROCESS结构中SignatureProtect针对特定版本的偏移量;

3、修补SignatureLevel、SectionSignatureLevel、Type、Audit和Signer这五个值(后三个是PS_PROTECTION结构的成员),具体要取决于它是保护还是取消保护该进程;

4、如果进行保护,则修改后的值分别是0x3f、0x3f、2、0、6,代表WinTcb的受保护签名和最高的保护级别。

5、如果取消保护,则修改后的值分别是0、0、0、0、0,代表不受保护的进程。

6、最后,取消引用EPROCESS对象。

11.png

这个模块与攻击者的关系非常密切,最明显的原因在于,攻击者可以从LSASS取消保护以提取凭据。但是,还有一种另外的方式,我们可以保护任意进程,并使用它来处理另一个受保护的进程。例如,我们使用!processProtect来保护我们正在运行的mimikatz.exe,然后运行特定命令从LSASS中提取凭据,尽管LSASS受保护,但仍然可以正常工作。该用法的示例如下所示:

12.png

!processToken

与另一个操作相关的功能是!processToken,它可以用于复制进程令牌并将其传递给攻击者指定的进程。这一方法在DCShadow攻击期间最经常使用,类似于token::elevate,但不同之处在于其修改了进程令牌,而非线程令牌。

在不传递任何参数的情况下,该函数将授予所有cmd.exe、powershell.exe和mimikatz.exe进程一个NT AUTHORITY\SYSTEM令牌。或者,它使用to和from参数,这些参数可用于定义希望复制令牌的源进程,或是要将其复制到的目标进程。

13.png

要复制令牌,Mimikatz首先将to和from PID设置为用户提供的值,如果用户未提供,则设置为0,然后将它们放在MIMIDRV_PROCESS_TOKEN_FROM_TO结构中,该结构通过IOCTL_MIMIDRV_PROCESS_TOKEN发送给Mimidrv。

一旦Mimidrv收到用户指定的PID,它将使用nt!PsLookupProcessByProcessId获取to和from进程的句柄。如果能够获取到这些进程的句柄,则会使用nt!ObOpenObjectByPointer来获取from进程的内核句柄(OBJ_KERNEL_HANDLE)。以下对于nt!ZwOpenProcessTokenEx的调用时必须的,该调用将返回from进程的令牌上的句柄。

在这一点上,有两条不同的逻辑分支。在用户提供了to进程的第一种情况下,Mimidrv调用kkll_m_process_token_toProcess。该函数首先使用nt!ObOpenObjectByPointer来获取to进程的内核句柄。然后,它调用ZwDuplicateToken从from进程中获取令牌,并将其存储在未记录的PROCESS_ACCESS_TOKEN结构中,作为令牌属性。如果运行的是Windows Vista或更高版本系统,将设置PrimaryTokenFrozen(具体将在!process一节中介绍),然后调用未记录的nt!ZwSetInformationProcess函数以将重复的令牌提供给to进程。完成之后,将关闭to进程和PROCESS_ACCESS_TOKEN结构的句柄来实现清理。

如果未指定to进程,那么Mimidrv会利用!process中使用的kkll_m_process_enum函数来遍历系统上的进程列表。在这里没有使用kkll_m_process_list_callback回调,而是使用了kkll_m_process_systoken_callback,后者是使用ntdll!RtlCompareMemory来检查ImageFileName是否匹配“mimikatz.exe”、“cmd.exe”或“powershell.exe”。如果匹配,则会将该进程的句柄传递给kkll_m_process_token_toProcess,向该进程授予重复的令牌,然后继续在链表中查找其他匹配项。

14.png

!processPrivilege

这是一个相对简单的函数,它将授予所有特权(例如:SeDebugPrivilege、SeLoadDriverPrivilege),但其中包含一些值得关注的代码,这些代码展现了在Ring 0中进行操作的功能。在我们深入了解Mimidrv如何修改目标进程令牌之前,首先需要了解令牌在内核中是什么样子的。

如前所述,EPROCESS结构中包含进程的属性,包括令牌(Windows 10 1903中偏移量为0x360)。我们可能会注意到类型为EX_FAST_REF的令牌,而不是TOKEN。

15.png

这其实是Windows内部的一些不同寻常之处,但是这些指针是充分考虑到内核结构在x64系统上的16字节边界对其而创建的。由于这种对齐方式,指针的最后1个字节是对我们的对象的引用。具体而言,是指向TOKEN结构的指针。

为了实际展现这一点,我们可以在WinDbg中查找系统进程的标记。首先,我们要获取该进程的EPROCESS结构的地址。

16.png

因为我们知道令牌EX_FAST_REF的偏移量为0x360,所以我们可以使用WinDbg的计算器进行一些快速的数学运算,并根据计算结果得到内存地址。

17.png

现在,我们已经有了EX_FAST_REF的地址,我们可以将最后一个字节更改为0,以获得TOKEN结构的地址,我们将使用!token扩展进行检查。

18.png

既然我们已经可以识别TOKEN的结构,那么也就可以检查其具有的某些属性。

19.png

与!processPrivileges最为相关的是Privileges属性(在Vista及更高版本中,偏移量为0x40)。该属性的类型为SEP_TOKEN_PRIVILEGES,其中包含三个属性:Present、Enabled以及EnabledByDefault。其中存储的是代表我们曾经使用过的令牌权限的位掩码(例如:SeDebugPrivilege、SeLoadDriverPrivilege等)。

20.png

如果我们在发出!processPrivileges命令时检查Mimidrv调用的函数,就可以看到这些位掩码被覆盖以启用对目标进程主令牌的所有特权。如下所示的是GUI中的结果。

21.png

它位于调试器中,同时以Privileges偏移量检查内存。

22.png

总结一下,!processPrivileges将覆盖目标进程的TOKEN结构中的特定位掩码,该结构将授予目标进程所有权限。

6.3 通知

内核通过注册在特定事件发生时要执行的回调函数,为驱动程序提供“订阅”系统中特定事件的方法。最常见的例子是关闭处理程序,它允许驱动程序在系统关闭时执行某些操作(通常是为了保证持久性),同时还允许进程创建通知,从而在系统启动新进程时通知驱动程序(通常被EDR使用)。

这些模块让我们能够找到订阅特定事件通知的驱动程序及其回调函数所在的位置。Mimidrv用来执行这一操作的代码有些难以阅读,但是一般流程是:

1、搜索一个字符串字节,特别是在包含指向系统内存中结构指针的LEA指令之后直接搜索操作码。

2、在LEA指令中传递的地址处使用结构(或指向结构的指针)以查找回调函数的地址。

3、返回有关该函数的详细信息,例如其所属的驱动程序。

!notifProcess

通过使用nt!PsSetCreateProcessNotifyRoutine(Ex/Ex2)和第一个参数中指定的回调函数,在创建或销毁进程时,驱动程序可以选择接收通知。在创建进程后,将返回新创建进程的进程对象以及PS_CREATE_NOTIFY_INFO结构,该结构中包含有关新创建进程的大量相关信息,包括其父进程ID和命令行参数。

与Windows事件跟踪(ETW)相比,这种类型的通知具有一定的优势,即:没有延迟地接收创建或终止通知。并且,由于进程对象已经传递给我们驱动程序的信息,因此我们有办法防止在前期操作回调期间启动进程。看起来,这似乎对于EDR产品来说非常有帮助。

首先,我们在nt!PsSetCreateProcessNotifyRoutine and nt!IoCreateDriver的地址之间搜索字节模式(操作码从LEA RCX,[RBX*8]开始,在下面的截图中为[RBX*8]),标记未记录的nt!PspSetCreateProcessNotifyRoutine的开始。

23.png

24.png

在nt!PspSetCreateProcessNotifyRoute的地址位置,是指向EX_FAST_REF结构的指针数组(不大于64个)。

25.png

在创建或终止进程时,nt!PspCallProcessNotifyRoutines将遍历这个数组,并调用系统上驱动程序注册的所有回调。在这个数组中,我么将处理第三个条目(0xffff9409c37c7e6f)。这些指针地址的最后四位并不重要,因此我们将其删除,最后得到了EX_CALLBACK_ROUTINE_BLOCK的地址。

26.png

EX_CALLBACK_ROUTINE_BLOCK结构是未记录的,但是感谢ReactOS上的朋友,我们得到了其定义:

typedef struct _EX_CALLBACK_ROUTINE_BLOCK {
    EX_RUNDOWN_REF RundownProtect;
    PEX_CALLBACK_FUNCTION Function;
    PVOID Context;
} EX_CALLBACK_ROUTINE_BLOCK, *PEX_CALLBACK_ROUTINE_BLOCK;

其中,前8个字节表示EX_RUNDOWN_REF结构,因此我们可以跳过它们,获得驱动程序内部回调函数的地址。

27.png

然后,我们获取该地址,并查看在该地址加载了哪个模块。

28.png

在这个位置,我们可以看到Defender驱动程序WdFilter.sys的进程通知回调的地址。

29.png

我们是否能在该地址写入RET指令,以中止驱动程序中的这个功能?

!notifThread

!notifThread命令与!notifProcess命令几乎相同,但是它是负责搜索nt!PspCreateThreadNotifyRoutine的地址,以查找指向线程通知回调函数的指针,而不是nt!PspCreateProcessNotifyRoutine。

30.png

31.png

!notifImage

这些通知导致驱动程序可以在映像(例如:驱动程序、DLL、EXE)映射到内存时接收事件。就像上面的函数一样,!notifImage将搜索的数组更改为nt!PspLoadImageNotifyRoutine,以便找到指向映像加载通知回调例程的指针。

32.png

从这里开始,进行完全相同的位位移过程,以获取回调函数的地址。

33.png

!notifReg

驱动程序可以使用nt!CmRegisterCallback(Ex)注册注册表事件的操作前和操作后回调,例如在读取、创建或修改键时。尽管这个功能不像我们前文所讨论的类型那么常见,但也为开发人员提供了一种防止修改受保护的注册表项的方法。

这个模块比之前的3个模块更为简单,因为它实际上集中于查找和使用单个未记录的结构。Mimidrv搜索到nt!CallbackListHead的地址,该地址是一个双向链接的列表,其中包含指向注册表通知回调例程地址的指针。该结构可以记录为:

typedef struct _CMREG_CALLBACK {
    LIST_ENTRY List;
    ULONG Unknown1;
    ULONG Unknown2;
    LARGE_INTEGER Cookie;
    PVOID Unknown3;
    PEX_CALLBACK_FUNCTION Function;
} CMREG_CALLBACK, *PCMREG_CALLBACK;

在该结构的偏移量0x28处,是已注册的回调例程的地址。

34.png

Mimidrv只是简单地遍历链表,以获取回调函数的地址,并将其传递给kkll_m_modules_fromAddr,以获取函数在其驱动程序中的偏移量。

35.png

!notifObject

注意:此命令在Win 10 1903的2.2.0 2019122版本中不起作用,并在调用kernel32!DeviceIoControl时返回0x490(ERROR_NOT_FOUND),这可能是因为无法找到nt!ObTypeDirectoryObject的地址导致的。在对这一部分进行修改并确保可用后,我会再次更新这一章的内容。

最后,当我们尝试打开或复制进程、线程或桌面的句柄时(例如在令牌被盗用的情况下),驱动程序可以注册一个回调以接收通知·。这对于许多不同类型的软件来说非常有用,并且AVG驱动程序利用它来保护其用户模式进程免于调试。

这些回调可以是操作前或操作后,操作前回调让驱动程序可以在返回句柄的操作完成之前修改请求的句柄,例如请求的访问权限。操作后回调允许驱动程序在操作完成后执行某些操作。

Mimidrv首先搜索nt!ObpTypeDirectoryObject的地址,该地址包含指向OBJECT_DIRECTORY结构的指针。

36.png

该结构的HashBuckets成员是OBJECT_DIRECTORY_ENTRY结构的链表,每个结构都包含偏移量为0x8的对象值。

37.png

38.png

每个对象都是OBJECT_TYPE结构,其中包含有关特定对象类型(进程、令牌等)的详细信息,使用WinDbg的!object扩展可以更轻松地查看这些对象。哈希值是上面HashBucket中的索引。

39.png

然后,Mimidrv从OBJECT_TYPE结构中提取Name成员。

40.png

在这里,需要关注的另一个成员是CallbackList,它定义了由nt!ObRegisterCallbacks注册的操作前和操作后回调的列表。它是一个LIST_ENTRY结构,指向未记录的CALLBACK_ENTRY_ITEM结构。Mimidrv遍历CALLBACK_ENTRY_ITEM结构的链接列表,将每个结构传递给kkll_m_notify_desc_object_callback,以在操作前或操作后回调中提取指针,并将其传递到kkll_m_modules_fromAddr,以便在回调所属的驱动程序中找到偏移量。

最后,Mimidrv从OBJECT_TYPE + 0x70开始循环遍历8个对象方法的数组。如果设置了指针,则Mimidrv会将其传递给kkll_m_modules_fromAddr以获取对象方法的地址,并将其返回给用户。在下面的进程对象类型示例中可以看到。

进程对象类型的对象方法指针:

41.png

尽管此功能在Windows 10的最新版本上无效,但输出类似于以下内容:

42.png

6.4 模块

尽管本节中只包含1条命令,但其中还包含了另一个核心概念——内存池。

内存池是内核对象,可以从指定的内存区域(分页或非分页)分配内存块。其中的每一个类型都有特定的用例。

分页缓冲池是虚拟内存,可以将其分页输入或输出(即:读/写)到磁盘上的页面文件C:\pagefile.sys。这是推荐驱动程序使用的池。

Nonpaged的池无法分页,并且始终存在于RAM中。在无法接受页面错误的特定情况下(例如:处理中断服务例程ISR和延迟过程调用DPC期间),这是必要的。

下面是页面缓冲池内存的标准分配示例:

#define POOL_TAG 'TTAM'
ULONG size = 512;
VOID ptr*;
 
ptr = ExAllocatePoolWithTag(PagedPool, size, POOL_TAG); //Allocate 512 bytes from the paged pool
KdPrint(("Paged memory allocated at %x\n",(int*)s)); //Print the address of the pointer to our allocated memory
ExFreePoolWithTag(ptr, POOL_TAG) //Free the memory

需要注意的是池标签nt!ExAllocatePoolWithTag的第三个也是最后一个参数。这通常是一个唯一的4字节ASCII值,用于帮助跟踪具有内存泄漏的驱动程序。在上面的示例中,内存将被标记为MATT(标签为短端对齐)。Mimidrv使用池标记“kiwi”,该标记将显示为“iwik”,如下面Pavel Yosifovich的PoolMonX所示。

43.png

!modules

!modules命令列出了有关系统上加载的驱动程序的详细信息。该命令主要围绕与aux_klib!AuxKlibQueryModuleInformation函数相关。

Mimidrv首先使用aux_klib!AuxKlibQueryModuleInformation获取需要分配的内存总量,以容纳包含模块信息的AUX_MODULE_EXTENDED_INFO结构。一旦接收到该消息,它将使用nt!ExAllocatePoolWithTag使用其池标记“kiwi”从页面缓冲池分配所需的内存量。

通过将第一次调用aux_klib!AuxKlibQueryModuleInformation返回的大小除以AUX_MODULE_EXTENDED_INFO结构的大小,可以计算出来加载的映像数量。随后,对aux_klib!AuxKlibQueryModuleInformation进行调用,以获取所有模块信息,并将其存储并进行处理。然后,Mimidrv使用回调函数kkll_m_modules_list_callback遍历这个内存池,以将基地址、映像大小和文件名复制到输出缓冲区中,然后将其发送回用户。

44.png

6.5 过滤器

尽管我们主要研究软件驱动程序,但还存在Mimidrv允许与之交互的其他两种类型,分别是过滤器和微过滤器。

过滤器驱动程序被认为是旧版本的驱动程序,但目前仍然受支持。过滤器驱动程序的类型很多,但都是通过筛选IRP来扩展设备的功能。存在用于驱动特定任务的过滤器驱动程序地不同子类,例如:文件系统过滤器驱动程序和网络过滤器驱动程序。文件系统过滤器驱动程序的最典型示例是反病毒引擎、备份代理或加密代理。

我们见到的最为常见的过滤器驱动程序是FltMgr.sys,它公开了文件系统过滤器所需要的功能,以便开发人员可以更轻松地开发微过滤器驱动程序。

微过滤器驱动程序时Microsoft推荐的过滤器驱动程序开发建议,它具有一些明显的优势,包括无需重新启动即可卸载,可以有效降低代码的复杂性。这类驱动程序与旧的过滤器驱动程序相比更为常见,并且可以使用fltmc.exe列出或管理。

45.png

在Mimidrv上下文中,这两种类型的最大区别就是,微过滤器驱动程序通过过滤器管理器API进行管理。

!filters

!filters命令的工作原理与!modules命令几乎完全相同,但是利用nt!IoEnumerateRegisteredFiltersList获取系统上已注册文件系统过滤器驱动程序的列表,将它们存储在DRIVER_OBJECT结构中,并将驱动程序的索引输出为DriverName成员。

46.png

!minifilters

!minifilters命令显示在系统上注册的微过滤器驱动程序。该函数难以读取,原因在于Mimidrv需要调用的函数在运行时并不清楚内存需求,因此它仅仅发出请求以获取所需的内存空间,然后分配该内存,最后再发出真正的请求。为了深入探究其原理,我们可以按照主要函数来分解成不同步骤。

1、FltEnumerateFilters:第一个调用是fltmgr!FltEnumerateFilters,它枚举系统上所有已经注册的微过滤器驱动程序,并返回一个指针列表。

2、FltGetFilterInformation:接下来,遍历该列表,调用fltmgr!FltGetFilterInformation以获取FILTER_FULL_INFORMATION结构,其中包含有关每个微过滤器的详细信息。

3、FltEnumerateInstances:对于每个微过滤器,fltmgr!FltEnumerateInstances用于获取实例指针的列表。

4、FltGetVolumeFromInstance:接下来,使用fltmgr!FltGetVolumeFromInstance返回每个微过滤器连接的卷(例如:\Device\HarddiskVolume4)。需要注意的是,微过滤器可以将多个实例附加到不同的卷。

5、获取有关操作前和操作后回调的详细信息。我们将在接下来做深入讨论。

6、FltObjectDereference:在遍历完所有实例后,将使用fltmgr!FltObjectDereference来引用每个实例和微过滤器列表。

如我们所见,Mimidrv使用了一些非常标准的Filter Manager API函数。但是,其中的第5步有些奇怪,因为它使用了硬编码的偏移量来获取有关微过滤器的信息,并调用kkll_m_modules_fromAddr来获取偏移量,而没有去了解我们想看的内容。在!minifilters的输出中,有PreCallback和(或)PostCallback的地址,但是这些是什么呢?

Minifilter驱动程序可以为每个需要过滤的操作最多注册1个操作前回调和1个操作后回调。当筛选器管理器处理I/O操作时,它将请求从驱动程序栈向下传递,并从已注册操作前回调的最高级别的微过滤器开始。这是微过滤器在传递给文件系统以完成操作之前,对I/O操作采取行动的机会。在I/O操作完成后,筛选器管理器再次向下传递具有已注册的操作后回调的驱动程序的驱动程序栈。在这些回调中,驱动程序可以与数据进行交互,例如检查或修改数据。

为了了解Mimidrv解析的内容,我们可以根据系统上!minifilters的输出来深入研究一个示例,我们可以研究专门针对命名管道服务触发器的驱动程序——npsvctrig.sys。

47.png

我们将打开WinDbg,首先查找我们注册的过滤器。

48.png

在这里,我们可以看到地址为0xffffc18f97e34cb0的npsvctrig实例。在这个地址检查FLT_INSTANCE结构,将会显示出成员CallbackNodes,其偏移量为0x0a0。

49.png

有3个CALLBACK_NODE结构。

50.png

在0xffffc18f97e34d50处我们可以看到第一个CALLBACK_NODE结构,我们可以看到PostOperation属性(偏移量0x20)的地址为0xfffff8047e5f6010,与Mimikatz中为“CLOSE”显示的地址相同,与IRP_MJ_CLOSE相关。这意味着,这是指向操作后回调地址的指针!

51.png

但是,驱动程序内部在输出中显示的偏移量又是如何呢?为了给我们提供这个功能,Mimidrv调用了kkll_m_modules_fromAddr,后者又调用了kkll_m_modules_enum,我们在“模块”一节中曾经进行过介绍,但现在又有了kkll_m_modules_fromAddr_callback的回调函数。这一回调返回回调的地址、驱动程序的文件名(不包含路径)以及我们提供的地址与图片基址的偏移量。

通过快速浏览npsvctrig.sys内部的偏移量0x6010,我们可以看到它其实是NptrigPostCreateCallback函数的开始。

52.png

6.6 内存

这些函数尽管未能实现成用户可用的命令,但它们仍然允许与内核内存进行交互,并且在处理内核中的内存时需要考虑一些值得关注的细微差别。Mimikatz可以将它们称为IOCTL,因为它们确实具有相关的IOCTL。

kkll_m_memory_vm_read

如果其实际功能与名称没有差异,那么可以利用这个函数来读取内核中的内存。这是一个非常简单的功能,但引入了两个我们尚未探讨过的概念——内存描述符(MDL)和页面锁定。

虚拟内存应该是连续的,但是物理内存可以无处不在。Windows使用MDL来描述虚拟内存缓冲区的物理页面布局,这有助于正确描述和存储内存。

在某些情况下,我们可能需要快速直接地访问数据,并且我们不希望内存管理器会弄乱这些数据(例如:将其分页到磁盘)。为了确保不会发生这种情况,我们可以使用nt!MmProbeAndLockPages暂时将虚拟页面所映射的物理页面锁定在内存中,以使其无法分页。该函数要求在调用时指定一个操作,该操作用于描述将要执行的操作,可以是IoReadAccess、IoWriteAccess或IoModifyAccess。在操作完成后,将使用nt!MmUnlockPages来解锁页面。

这两个概念就构成了kkll_m_memory_vm_read的大部分。使用nt!IoAllocateMdl分配MDL,使用指定的nt!IoReadAccess锁定页面,使用nt!RtlCopyMemory将内存从MDL复制到输出缓冲区,然后我们通过调用nt!MmUnlockPages来解锁页面,这会使我们可以从内存读取任意数据。

kkll_m_memory_vm_write

这个函数是kkll_m_memory_vm_read的映像,但是Dest和From参数在我们写入MDL所描述的地址(而不是读取它时切换的)。

kkll_m_memory_vm_alloc

kkll_m_memory_vm_alloc函数允许通过调用nt!ExAllocatePoolWithTag从非页面缓冲池分配任意大小的内存,并返回指向分配内存的地址的指针。

它可以代替Mimidrv中对nt!ExAllocatePoolWithTag的一些直接调用,因为它实现了错误检查,这可以使代码变得更加稳定和更加阅读。

kkll_m_memory_vm_free

与所有其他类型的内容一样,必须释放非页面缓冲池内存。kkll_m_memory_vm_free函数通过调用nt!ExFreePoolWithTag来实现此目的。

与上面的函数类似,可以代替直接调用nt!ExFreePoolWithTag的方法,但Mimidrv目前不使用它。

6.7 SSDT

当用户模式应用程序需要使用kernel32!CreateFile创建文件时,用户访问磁盘和分配存储空间是怎样的状态呢?访问系统资源是内核的功能,但是用户模式应用程序可能也需要这些资源,因此需要一种向内核发出请求的方法。在Windows中,是利用系统调用或syscall来实现这一点的。

在幕后,这是kernel32!CreateFile实际运行情况的大致试图:

53.png

在用户模式和内核模式的边界处,我们可以看到对sysenter的调用(根据处理器的不同,该调用也可以代替syscall),该调用用于从用户模式转移到内核模式。让该指令在EAX寄存器中使用一个号码,特别是系统服务号码,该号码确定进行哪一个系统调用。@j00ru在他的博客上发表过Windows系统调用及其服务编号的列表。

在我们的kernel32!CreateFile示例中,ntdll!NtCreateFile在SYSCALL指令之前将0x55放入了EAX。

54.png

在SYSCALL上,Ring 0的KiSystemService接收请求,并在系统服务描述符表(SSDT)KeServiceDescriptorTable中查找系统服务的功能。SSDT拥有指向内核函数的指针,在这种情况下,我们开始寻找nt!NtCreateFile。

在过去,rootkit会钩住SSDT并替换指向内核函数的指针,以便在调用系统服务时改为执行rootkit内部的函数。值得庆幸的是,内核修补程序保护(KPP/PatchGuard)可以保护关键的内核结构(例如:SSDT)免受修改,因此该技术不适合64位系统。

!ssdt

!ssdt命令会定位到内存中的KeServiceDescriptorTable这个位置,通过搜索特定于OS版本的模式(Windows 10 1803中为0xd3、0x41、0x3b、0x44、0x3a、0x10、0x0f、0x83。

55.png

KeServiceDescriptorTable结构的内部是另一个结构KiServiceTable的指针,该结构包含相对于KiServiceTable本身的32位量偏移量。

56.png

由于我们无法在WinDbg中真正使用这些偏移量。因为它向左移动了4位,因此我们也可以将其添加到KiServiceTable以获取正确的地址。

57.png

我们还可以使用WInDbg的一些更高级的功能来处理偏移量,打印出位于计算的地址处的模块,以获取所有服务的地址。

58.png

这是Mimikatz在定位KeServiceDescriptorTable之后要执行的相同操作,以便定位指向服务的指针。如果首先打印出索引,然后才是地址。随后,调用kkll_m_modules_fromAddr来获取ntoskrnl.exe内部服务或功能的偏移。

59.png

利用WinDbg提供的索引,我们看到索引0指向nt!NtAccessCheck的地址。它位于ntoskrnl.exe中,偏移量为0x112340。

60.png

七、如何防御基于驱动的威胁

既然我们已经介绍了Mimidrv的内部工作原理,那么最后要说明如何防范攻击者将其植入我们的系统之中。在Windows 10系统上,使用驱动程序会为攻击者带来一些独特的挑战,其中最大的挑战就是必须对驱动程序进行签名。

Mimidrv具有许多易于修改的静态指示器,但需要使用新的EV证书重新编译和重新签名。由于修改Mimidrv会带来成本,因此仍然需要进行严格的脆弱性监测。Mimidrv植入的一些默认指标是依据来源分类的:

Windows事件ID 7045/4697 – 服务创建

服务名称:“mimikatz driver (mimidrv)”

服务文件名:*\mimidrv.sys

服务类型:内核模式驱动程序(0x1)

服务启动类型:自动启动(2)

注意:事件ID 4697包含有关加载驱动程序的帐户的信息,这可能有助于定位攻击者。必须通过组策略配置审计安全系统扩展,才能生成此事件。

61.png

Sysmon事件ID 11 – 文件创建

目标文件名:*\mimidrv.sys

Sysmon事件 ID 6 – 已加载驱动程序

加载的映像:*\mimidrv.sys

签名状态:已过期

解决这一问题的另一种更为广泛的方法是再退后一步,从整体上考虑恶意驱动程序的属性。

对于大多数组织来说,使用第三方驱动程序是不可避免的,但是了解组织的标准并确定异常场景是一个值得不断尝试的过程。Windows Defender应用程序控制(WDAC)使得在Windows 10操作系统上的审核变得非常轻松。

我的同事Matt Graeber在关于如何部署代码完整性策略并审计任何非Windows、早期加载反病毒软件(ELAM)或加载硬件抽象层(HAL)驱动程序方面写了一篇不错的文章。在重启后,系统将开始为基本策略之外的任意驱动程序生成事件ID为3076的日志。

62.png

根据这里,我们可以确定除了基本策略之外还需要哪些驱动程序,并将其加入到白名单中,从而调整检测逻辑以使分析人员可以更有效地发现异常的驱动程序加载。

八、扩展阅读

如果大家对这一方面感兴趣,可以阅读下面一些文章,它们涵盖了我在本文中忽略的一些细节。

[1] https://leanpub.com/windowskernelprogramming

[2] https://www.microsoftpressstore.com/store/windows-internals-part-1-system-architecture-processes-9780735684188

[3]  https://www.wiley.com/en-us/Practical+Reverse+Engineering%3A+x86%2C+x64%2C+ARM%2C+Windows+Kernel%2C+Reversing+Tools%2C+and+Obfuscation-p-9781118787311

[4] https://www.osr.com/nt-insider/

[5] https://github.com/MicrosoftDocs/windows-driver-docs-ddi/tree/a0486ec7b6480aec5233ba59c64c49578e540f52/wdk-ddi-src/content/wdm

[6] https://aaltodoc.aalto.fi/handle/123456789/38990

[7] https://www.geoffchappell.com/studies/windows/km/index.htm?tx=146,150

本文翻译自:https://posts.specterops.io/mimidrv-in-depth-4d273d19e148如若转载,请注明原文地址:
  • 分享至
取消

感谢您的支持,我会继续努力的!

扫码支持

打开微信扫一扫后点击右上角即可分享哟

发表评论

 
本站4hou.com,所使用的字体和图片文字等素材部分来源于原作者或互联网共享平台。如使用任何字体和图片文字有侵犯其版权所有方的,嘶吼将配合联系原作者核实,并做出删除处理。
©2022 北京嘶吼文化传媒有限公司 京ICP备16063439号-1 本站由 提供云计算服务