⚠️ 警告:本项目严格用于教育和演示目的。它不打算在恶意环境中使用。目标是学习逆向工程方法论以及 Windows 驱动的利用步骤。
我将在这里说明我为解决 d1rk(SaadAhla) https://github.com/SaadAhla 提出的练习所采取的方法,该练习涉及对一个合法的、已签名的、并且不在阻止列表(HVCI、LOLBIN...)中的驱动执行逆向工程和利用。 一个 C 程序可用于通过此内核模式驱动终止系统上任何活动进程,我将在下文详细介绍其工作原理。

📃 用法:DriverKiller.exe <进程名.exe> [-d]
选项 -d:允许在利用后从系统中删除服务和驱动。
必须在目标机器上启用测试签名模式,因为驱动的证书已过期。
第 1 部分 - 逆向工程:
该练习提供了一个 .sys 文件,以其 SHA-256 哈希命名。
第一步是使用 IDA 打开该文件。
IDA 免费可用。只需访问 Hex-Rays 网站生成许可证并下载软件即可。
首先列出驱动的 IAT(导入地址表),并查找我们感兴趣的 API 调用:ZwTerminateProcess。
双击 ZwTerminateProcess 后,IDA 会将我们重定向到该函数的已编译代码。选中该条目并显示交叉引用后,即可获得调用它的驱动函数列表。
我们可以观察到,是位于偏移量 1CE 处的函数 sub_12EF4 使用了 ZwTerminateProcess。双击后,IDA 会显示其编译后的代码。
反编译代码显示了对 ZwOpenProcess(打开指向目标进程的句柄)和 ZwTerminateProcess(通过该句柄终止进程)的调用。
查阅 ZwOpenProcess 的文档 (https://learn.microsoft.com/fr-fr/windows-hardware/drivers/ddi/ntddk/nf-ntddk-zwopenprocess) 后,可以看到 ClientID 参数是一个指向目标进程 PID 的指针。
在上面一行,ClientId.UniqueProcess 使用变量 v22 进行初始化。后者就在上面定义:
v22 = (void )((_QWORD *)i + 10);
要理解这个赋值,需要识别变量 i 和 +10 字段。
在该函数更上方,我们可以看到对带有 SYSTEM_PROCESS_INFORMATION 参数的 ZwQuerySystemInformation 的调用。我们还可以理解到,i 是使用变量 v6 遍历此结构条目的迭代器。
根据 ZwQuerySystemInformation 的文档:(https://learn.microsoft.com/en-us/windows/win32/sysinfo/zwquerysysteminformation),该函数返回一个数组,其中包含系统中每个活动进程的一个条目。
SYSTEM_PROCESS_INFORMATION 结构在此处描述:https://learn.microsoft.com/en-us/windows/win32/api/winternl/nf-winternl-ntquerysysteminformation
typedef struct _SYSTEM_PROCESS_INFORMATION {
ULONG NextEntryOffset;
ULONG NumberOfThreads;
BYTE Reserved1[48];
UNICODE_STRING ImageName;
KPRIORITY BasePriority;
HANDLE UniqueProcessId;
PVOID Reserved2;
ULONG HandleCount;
ULONG SessionId;
PVOID Reserved3;
SIZE_T PeakVirtualSize;
SIZE_T VirtualSize;
ULONG Reserved4;
SIZE_T PeakWorkingSetSize;
SIZE_T WorkingSetSize;
PVOID Reserved5;
SIZE_T QuotaPagedPoolUsage;
PVOID Reserved6;
SIZE_T QuotaNonPagedPoolUsage;
SIZE_T PagefileUsage;
SIZE_T PeakPagefileUsage;
SIZE_T PrivatePageCount;
LARGE_INTEGER Reserved7[6];
} SYSTEM_PROCESS_INFORMATION;
提醒:Windows x64 上某些类型的大小
typedef struct _UNICODE_STRING {
USHORT Length; -> 2
USHORT MaximumLength; -> + 2 = 4
PWSTR Buffer; -> + 8 = 12 (12 n'est pas un multiple de 8 donc padding de 4 ajouté en amont de Buffer) = 16
} UNICODE_STRING;计算 UniqueProcessId 的偏移量:
ULONG NextEntryOffset; -> 4
ULONG NumberOfThreads; -> + 4 = 8
BYTE Reserved1[48]; -> + 48 = 56
UNICODE_STRING ImageName; -> + 16 = 72
KPRIORITY BasePriority; -> + 4 = 76 (76 n'est pas un multiple de 8 donc padding de 4 ajouté) = 80
HANDLE UniqueProcessId; -> + 8 = 88
因此,UniqueProcessId 成员位于偏移量 0x50(十进制 80)处。
查看变量 v22 的赋值,可以看到 i 被转换为 QWORD(8 字节)指针
v22 = (void )((_QWORD *)i + 10);
v22 对应 i 的地址 + 10 * 8 = 80 字节。所以该变量确实包含从 SYSTEM_PROCESS_INFORMATION 结构检索到的 PID。
为了确定哪个 PID 会被传递给 ZwTerminateProcess,需要分析这个赋值周围的条件。
我们可以看到,首先会检索进程映像名称:
v9 = (wchar_t )((_QWORD *)i + 8);
v9 = i 的地址 + 8 × 8 = 64 字节。这对应于 ImageName 成员的 Buffer,因为该成员位于偏移量 56 + 2(USHORT)+ 2(USHORT)+ 4(填充)= 64 处。
根据下面的操作和循环,可以推测,在参数中传递的进程名称(a2)与系统上活动进程 v9/String 之间进行了比较。
sub_1C078(String, v9, (int)v13); v17 = strupr(a2); v18 = strupr(String);
因此,参数 a2 应当包含要经由 ZwTerminateProcess 终止的进程名称。我们注意到 a2 是函数 sub_12EF4 的一个参数。为了进一步分析,需要查看该函数的引用(为了更好的可读性,我将其重命名为 ZwTerminateProcessCaller)。
可以看到 ZwTerminateProcessCaller 被函数 sub_13624 在偏移量 61A 处调用。
在分析这个反编译代码之前,我将查找函数 sub_13624(重命名为 ZwTerminateProcessCallerCaller)的引用,以确保该代码确实是在从用户模式调用 DeviceIoControl API 之后使用的。
可以看到 ZwTerminateProcessCallerCaller 被函数 sub_14130(重命名为 ZwTerminateProcessCallerCallerCaller ...对我们来说幸运的是,这是进入入口点之前的最后一个 😅)调用。
可以看到 ZwTerminateProcessCallerCallerCaller 被函数 sub_1A4A8 在偏移量 306 处调用。
我们找到了 ZwTerminateProcessCallerCallerCaller 的分配位置:
memset64(DriverObject->MajorFunction, (unsigned __int64)ZwTerminateProcessCallerCallerCaller, 0x1Cu);
在回到函数 sub_13624(别名 ZwTerminateProcessCallerCaller)之前,我们获取 Symbolic Name 和 Device Name(此处相同):Viragtlt。
回到 ZwTerminateProcessCallerCaller,我们注意到它的第二个参数(即 a2)对应于 MasterIrp->AssociatedIrp.SystemBuffer。
就在调用 ZwTerminateProcessCaller 的上方,我们找到了 IOCTL 代码:-2106392528(十六进制:0x82730030)。
借助这些信息,我们可以推断,要利用此驱动,需要向该驱动发送一个 DeviceIoControl API 调用,并在 SystemBuffer 中包含要终止的进程名称。
🔷 通过逆向工程获得的信息:
0x82730030ViragtltViragtltSystemBuffer 必须包含目标进程的名称第 2 部分 - 利用
要利用此驱动(如果它已安装在目标机器上并处于活动状态),需要打开一个指向该驱动的句柄,然后使用包含要终止进程名称的 Buffer 进行 DeviceIoControl API 调用。
针对这个练习,我开发了一个 C 项目,它:
我还添加了一个 -d 选项,用于在利用后从系统中删除服务和驱动。
以下是 C 程序在其完整执行周期中的行为:
AV/EDR 规避
在这种情况下,DriverKiller.exe 不会被 Microsoft Defender 检测到,无论是静态还是动态。
在这里,规避的意义不大,因为被利用的驱动具有过期的证书,因此在现实条件下几乎无法使用。
但为了更好的隐蔽性,本可以实现:
截至 2025 年 8 月 29 日对驱动的检测结果(已有结果,出于显而易见的原因我没有向 VirusTotal 提交任何内容):
⚠️ 本项目是在学习框架内完成的。它可能包含不准确之处或错误。欢迎任何建议、更正或讨论!😃 感谢 d1rk(SaadAhla):https://github.com/SaadAhla !