概述:驱动开发过程中,相关 API 的学习整理

本文部分内容由 AI 生成,经人工修订。

[toc]

链表

定义链表结构体

typedef struct
{
	DWORD Pid;
	UCHAR ProcessName[2048];
	DWORD Handle;
	LIST_ENTRY ListEntry;
} ProcessList;

初始化 InitializeListHead

LIST_ENTRY linkListHead;
 
// 初始化链表头部
InitializeListHead(&linkListHead);

插入链表 InsertTailList

pData = (ProcessList*)ExAllocatePool(PagedPool, sizeof(ProcessList));
 
InsertTailList(&linkListHead, &pData->ListEntry);

判断是否为空 IsListEmpty

if(!IsListEmpty(&linkListHead))
{
    DbgPrint("Empty");
}

移除链表 RemoveHeadList

LIST_ENTRY* pEntry = RemoveHeadList(&linkListHead);

获取链表当中的结构

pData = CONTAINING_RECORD(pEntry, ProcessList, ListEntry);
DbgPrint("Pid[%d], ProcessName[%s], Handle[0x%x] \n", pData->Pid, pData->ProcessName, pData->Handle);

安装卸载相关

WdfDriverCreate

WdfDriverCreate 是 Windows Driver Frameworks (WDF) 中用于创建驱动程序的函数。它用于初始化驱动程序,并将其与 Windows 操作系统进行集成。

WdfDriverCreate 函数需要填写一个 WDFDRIVER 结构体来定义驱动程序的属性和行为。以下是一些常见的参数:

  • DriverName: 驱动程序的名称,用于在调试和日志记录中标识驱动程序。
  • DriverVersion: 驱动程序的版本号,用于标识驱动程序的版本。
  • DeviceAddVersion: 用于添加新设备的 WDF 函数版本。
  • DriverUnload: 驱动程序卸载时的回调函数,用于清理资源并执行其他必要的操作。
  • DispatchTable: 一个包含驱动程序处理 IRP 请求的回调函数的表。
  • RegistryPath: 驱动程序在注册表中的路径,用于存储配置信息和设置。

WdfDeviceCreate

WdfDeviceCreate 是 Windows Driver Frameworks (WDF) 中的一个函数,用于创建设备对象并初始化与设备相关的数据结构。它是 WDF 中非常重要和常用的函数之一。

WdfDeviceCreate 函数需要填写一个 WDFDEVICE_INIT 结构体来定义设备的属性和行为。以下是一些常见的参数:

  • Driver: 指向驱动程序的指针,用于关联设备和驱动程序。

  • DeviceAttributes: 指向调用方分配的 WDF_OBJECT_ATTRIBUTES 结构的指针,该结构包含新设备的属性。

  • StackSize: 设备的堆栈大小,用于定义设备对象的执行环境。

  • DeviceInit: 指向 WDFDEVICE_INIT 结构的指针的地址,用于初始化设备的状态和行为。

在调用 WdfDeviceCreate 之后,WDF 将使用提供的参数来创建并初始化一个设备对象。然后,设备可以使用 WDF 提供的其他函数来管理设备的状态、处理请求以及与其他设备或驱动程序进行交互。

需要注意的是,WdfDeviceCreate 是一个高级别的函数,通常用于创建完整的设备对象。对于较简单的设备或需要更精细控制的情况,可能需要使用其他 WDF 函数来手动创建和配置设备对象。同时,在使用 WdfDeviceCreate 和其他 WDF 函数时,应注意遵循正确的使用方法和规范,以避免产生潜在的问题和风险。

实用函数

绕过签名检查

// 绕过签名检查
BOOLEAN BypassCheckSign(PDRIVER_OBJECT pDriverObject)
{
#ifdef _WIN64
	typedef struct _KLDR_DATA_TABLE_ENTRY
	{
		LIST_ENTRY listEntry;
		ULONG64 __Undefined1;
		ULONG64 __Undefined2;
		ULONG64 __Undefined3;
		ULONG64 NonPagedDebugInfo;
		ULONG64 DllBase;
		ULONG64 EntryPoint;
		ULONG SizeOfImage;
		UNICODE_STRING path;
		UNICODE_STRING name;
		ULONG   Flags;
		USHORT  LoadCount;
		USHORT  __Undefined5;
		ULONG64 __Undefined6;
		ULONG   CheckSum;
		ULONG   __padding1;
		ULONG   TimeDateStamp;
		ULONG   __padding2;
	} KLDR_DATA_TABLE_ENTRY, * PKLDR_DATA_TABLE_ENTRY;
#else
	typedef struct _KLDR_DATA_TABLE_ENTRY
	{
		LIST_ENTRY listEntry;
		ULONG unknown1;
		ULONG unknown2;
		ULONG unknown3;
		ULONG unknown4;
		ULONG unknown5;
		ULONG unknown6;
		ULONG unknown7;
		UNICODE_STRING path;
		UNICODE_STRING name;
		ULONG   Flags;
	} KLDR_DATA_TABLE_ENTRY, * PKLDR_DATA_TABLE_ENTRY;
#endif
 
	PKLDR_DATA_TABLE_ENTRY pLdrData = (PKLDR_DATA_TABLE_ENTRY)pDriverObject->DriverSection;
	pLdrData->Flags = pLdrData->Flags | 0x20;
 
	return TRUE;
}

如何使用

// 驱动入口地址
NTSTATUS DriverEntry(IN PDRIVER_OBJECT Driver, PUNICODE_STRING RegistryPath)
{
	if (BypassCheckSign(Driver))
		DbgPrint("Bypass Sign Success.");
 
	DbgPrint("Driver loaded. \n");
 
	Driver->DriverUnload = UnDriver;
	return STATUS_SUCCESS;
}

内存相关

申请内存 ExAllocatePool

申请内存使用 ExAllocatePoolExAllocatePoolWithTagExAllocatePool2 接口

原型

PVOID ExAllocatePool(
  IN POOL_TYPE PoolType,
  IN SIZE_T NumberOfBytes
);
  • PoolType:指定内存池的类型,可以是 NonPagedPool(非分页内存池)或 PagedPool(分页内存池)等。
  • NumberOfBytes:指定要分配的内存大小,最好是 4 的倍数。
  • 返回值:返回分配的内存地址,一定是内核模式地址。如果返回 NULL,则代表分配失败。

ExAllocatePool 与 ExAllocatePool2/ExAllocatePool3

从 Windows 10 版本 2004 开始,推荐使用 ExAllocatePool2ExAllocatePool3 来替代 ExAllocatePool。新的 API 默认会将分配的内存初始化为零,有助于避免内存泄漏相关的 bug。

示例代码

PVOID Allocation = ExAllocatePool(PagedPool, 100);
if (Allocation == NULL) {
    // 处理内存分配失败的情况
} else {
    // 使用分配的内存
}

在新的驱动中,应该使用 ExAllocatePool2 接口

PVOID Allocation = ExAllocatePool2(POOL_FLAG_PAGED, 100, 'abcd');
if (Allocation == NULL) {
    // 处理内存分配失败的情况
} else {
    // 使用分配的内存
}

填充内存 RtlZeroMemory

// 分配内核堆空间
pData = (ProcessList*)ExAllocatePool(PagedPool, sizeof(ProcessList));
RtlZeroMemory(pData, sizeof(ProcessList));

拷贝内存 RtlCopyMemory

// 设置变量
pData->Pid = (DWORD)PsGetProcessId(eproc);
RtlCopyMemory(pData->ProcessName, PsGetProcessImageFileName(eproc), strlen(PsGetProcessImageFileName(eproc)));
pData->Handle = (DWORD)PsGetProcessInheritedFromUniqueProcessId(eproc);

释放内存 ExFreePool

ExFreePool(pData);

进程相关

#include <windef.h>
 
// 函数返回给定进程的进程环境块(Process Environment Block,PEB)的指针
extern PVOID PsGetProcessPeb(_In_ PEPROCESS Process); 
 
// 此函数通过给定的进程 ID 查找对应的进程,并将进程的 EPROCESS 结构指针存储在提供的指针变量中。
NTKERNELAPI NTSTATUS PsLookupProcessByProcessId(HANDLE ProcessId, PEPROCESS* Process); 
 
// 此函数返回给定进程的 Wow64 进程环境的指针。Wow64 进程环境是用于在 64 位 Windows 上运行 32 位应用程序的兼容性层。
extern NTKERNELAPI PVOID PsGetProcessWow64Process(_In_ PEPROCESS Process); 
 
// 此函数返回给定进程的映像文件名,即执行该进程的可执行文件的名称。
extern NTKERNELAPI UCHAR* PsGetProcessImageFileName(IN PEPROCESS Process); 
 
//  此函数返回给定进程所继承的唯一进程 ID, 即父进程 ID。
extern NTKERNELAPI HANDLE PsGetProcessInheritedFromUniqueProcessId(IN PEPROCESS Process);

alloc_text

这段代码是 Windows 内核驱动开发(特别是使用 WDF — Windows Driver Framework,如 KMDF 或 UMDF)中常见的 内存分页控制指令,用于指定函数应被放置在可分页内存(PAGE 段)还是非分页内存(INIT 或默认代码段)中。


📌 代码解析

#ifdef ALLOC_PRAGMA
#pragma alloc_text (PAGE, EchoDeviceCreate)
#pragma alloc_text (PAGE, EchoEvtDeviceSelfManagedIoSuspend)
#endif

1. #ifdef ALLOC_PRAGMA

  • 这是一个 条件编译宏
  • ALLOC_PRAGMA 通常由驱动项目在需要显式控制函数内存属性时定义(例如在源文件开头或通过编译器选项)。
  • 如果未定义 ALLOC_PRAGMA,这些 #pragma 指令将被忽略,函数按编译器默认方式分配(通常是可分页的,但不保证)。

💡 为什么用条件编译?
有些构建环境(如静态分析、特定平台)可能不支持 #pragma alloc_text,或开发者希望在调试/发布版本中灵活控制。通过宏开关,可以统一管理。


2. alloc_text

PAGE 段

#pragma alloc_text(PAGE, FunctionName):

  • 这是 Microsoft 编译器(MSVC)特有的 pragma 指令。
  • 作用:将指定函数的代码放入名为 PAGE 的内存段中
  • PAGE 段 = 可分页内存(Paged Memory)
    • 系统内存紧张时,该段代码可被换出到磁盘(pagefile.sys)。
    • 只能在 IRQL APC_LEVEL(通常是 PASSIVE_LEVEL)时调用
    • 适用于 不会在高 IRQL(如 DISPATCH_LEVEL)或 DPC/ISR 中执行的函数

编译后的可行文件逆向结果如下所示,可见 EchoDeviceCreate 函数是直接保存在普通函数区的,并且该函数所使用的字符串等都会被编译到当前页:

image-20251214213447973

image-20251214213249314

INIT 段

#pragma alloc_text(INIT, DriverEntry):

  • INIT 段 = 可分页且可丢弃(Discardable)
  • 驱动加载完成后(即 DriverEntry 执行完毕),操作系统会 自动释放整个 INIT,回收物理内存。
  • 只能在 IRQL = PASSIVE_LEVEL 调用DriverEntry 正是在此 IRQL 下执行)。
  • 适用场景:仅在驱动加载阶段运行一次的初始化代码。

💡 为什么放这里?

  • DriverEntry 是驱动入口点,只在驱动加载时调用一次。
  • 放入 INIT 可减少驱动常驻内存占用,符合 Windows 驱动最佳实践。

⚠️ 错误示例:如果某个 INIT 段函数在驱动加载后被再次调用,会导致 Page Fault / BugCheck(蓝屏)

对比

内存段是否可分页安全调用 IRQL典型用途
PAGE✅ 是≤ APC_LEVEL(通常 PASSIVE_LEVEL)设备创建、初始化、清理、IOCTL 处理等
(默认 / 无标记)❌ 否(非分页)≤ DISPATCH_LEVELDPC、中断处理、CompletionRoutine 等
INIT✅ 是(且驱动加载后可释放)≤ APC_LEVELDriverEntryAddDevice 等仅在加载时运行的函数

⚠️ 错误示例:若将 DPC 回调函数放在 PAGE 段,在内存分页时触发 DPC 会导致 BugCheck(蓝屏),因为系统无法在高 IRQL 访问分页内存。

🛠 开发建议

  1. 始终确认函数调用上下文的 IRQL 使用 Static Driver Verifier (SDV) 或 Code Analysis 检查 IRQL 与内存段匹配性。
  2. 不要将 PAGE 函数用于以下场景
    • DPC / Timer callbacks
    • Interrupt Service Routines (ISR)
    • Completion routines(除非明确在 PASSIVE_LEVEL)
    • Any code path called at IRQL ≥ DISPATCH_LEVEL
  3. INIT 函数必须满足
    • 仅在驱动加载期间调用
    • 不被任何非-INIT 函数引用(否则链接器可能报错或导致运行时崩溃)
  4. 现代 WDF 驱动中
    • 大多数事件回调(EvtDeviceAdd, EvtIoRead, EvtDeviceD0Entry 等)默认可安全放入 PAGE
    • 只有涉及硬件中断、DPC、自旋锁保护的代码才需放在非分页段(默认段)。