Windows软件调试初探-验证机制

碎碎念

驱动程序验证器主体在内核中,名字包含Verifier字样或以Vi/Vf开头。

还有应用程序验证器,一部分为NTDLL.DLL中函数,都以AVrf开头。

若一个驱动程序项取得Windows徽标和数字签名,一定要通过驱动验证器的验证,后者是WHQL测试的一部分内容。

蓝屏终止BSOD

两个DDI用于蓝屏错误:

1
2
3
4
5
6
7
8
9
10
VOID KeBugCheck(
_In_ ULONG BugCheckCode //停止码
);
VOID KeBugCheckEx(
_In_ ULONG BugCheckCode,
_In_ ULONG_PTR BugCheckParameter1,
_In_ ULONG_PTR BugCheckParameter2,
_In_ ULONG_PTR BugCheckParameter3,
_In_ ULONG_PTR BugCheckParameter4
);

上面这俩函数的实现都在nt!KeBugCheck2中,加上这个一共仨函数统称为BugCheck函数,工作过程如下:

将全局变量nt!KeBugCheckActive设为真,标志系统进入特殊错误检查状态,产生描述系统状态的上下文机构。

根据参数中停止码寻找合适的错误提示信息,即蓝屏第一部分内容。对于某些停止码,KeBugCheck2将对应模块名赋给全局变量KiBugCheckDriver。例如内核代码在IRQL不低于DISPATCH_LEVEL时访问分页内存,则停止码为IRQL_NOT_LESS_OR_EQUAL,BugCheckParameter1为被访问的内存地址,BugCheckParameter2为不当的IRQL值,BugCheckParameter3为访问方式,0读1写,BugCheckParameter4为执行访问的指令地址。对于该停止码,BugCheck函数根据BugCheckParameter4用KiPcToFileHeaderMmLocateUnloadedDriver寻找对应模块名称。

若启用了内核调试,则用KdPrint打印停止码和蓝屏参数信息。再判断是否真正连接内核调试器,是则用KiBugCheckDebugBreak中断到内核调试器。

BugCheck函数用KeDisableInterrupts禁止中断,用KeRaiseIrql将IRQL设置为HIGH_LEVEL,用KiSendFreeze冻结其他CPU,使系统进入单纯错误检查状态。

启用启动过程时使用的简单显示驱动BootVid,用nt!KiDisplayBlueScreen绘制蓝屏。

调用先前驱动程序通过KeRegisterBugCheckCallback注册的错误检查回调函数,目的是通知驱动程序系统进入错误检查阶段,驱动程序应执行必要清理工作或通知自己的硬件。

判断是否需要启动内核调试引擎,若需要用KdEnableDebuggerWithLock启用内核调试引擎。

追呗系统转储数据,用IoWriteCrashDump将转储信息写入硬盘。后者中用KeRegisterBugCheckReasonCallback注册的回调函数,让驱动程序可以附加自己的转储数据或写入其他存储介质。

再次扫描并调用KeRegisterBugCheckReasonCallback注册的回调函数。

判断是否需要自动重启,是则用HalReturnToFirmware重启系统。

KiBugCheckDebugBreak试图中断到调试器。

手工触发蓝屏可用WinDBG的.crash命令。也可在注册表“HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\i8042prt\Parameters”下加一个REG_DWORD类型的键值名为CrashOnCtrlScroll,值为1,重启后在PS2键盘上按Ctrl+ScrollLock。

驱动验证器

基本设计思想是在驱动程序调用设备驱动接口DDI函数时,对驱动程序执行各种检查。

Windows系统修改被验证驱动程序的IAT来挂接驱动程序的DDI调用,即IAT Hook。具体来说就将被验证驱动程序IAT中DDI函数地址替换为验证函数地址。

验证函数执行为:更新计数器或全局变量啥的、检查参数等并在必要时用KeBugCheckEx(DRIVER_VERIFIER_DETECTED_VIOLATION,...)、没问题则调用原函数并返回原函数返回值。

Windows启动的执行体阶段0初始化期间,内存管理器初始化函数MmInitSystem调用驱动验证器初始化函数。后者创建并初始化被验证驱动程序信息链表,称为可疑驱动链表,记录在全局变量MiSuspectDriverList

1
2
3
4
5
nt!MiInitializeDriverVerifierList //初始化可疑驱动链表
nt!MmInitSystem //内存管理器阶段0初始化
nt!ExpInitializeExecutive //执行体阶段0初始化
nt!KiInitializeKernel //内核初始化
nt!KiSystemStartup //系统入口

可疑驱动链表每个节点是一个MI_VERIFIER_DRIVER_ENTRY结构,如下,没找到官方文档只有Vista下的https://www.nirsoft.net/kernel_struct/vista/MI_VERIFIER_DRIVER_ENTRY.html。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
typedef struct _MI_VERIFIER_DRIVER_ENTRY {
LIST_ENTRY Links;
ULONG Loads; //加载计数
ULONG Unloads; //卸载计数
UNICODE_STRING BaseName;
PVOID StartAddress; //起始内存地址
PVOID EndAddress; //结束内存地址
ULONG Flags; //标志
ULONG Signature; //结构签名
SLIST_HEADER PoolPageHeaders; //记录使用内存池情况的链表头
SLIST_HEADER PoolTrackers;
ULONG CurrentPagedPoolAllocations; //目前分页内存池中分配数
ULONG CurrentNonPagedPoolAllocations; //目前非分页内存池中分配数
ULONG PeakPagedPoolAllocations; //分页内存分配数峰值
ULONG PeakNonPagedPoolAllocations; //非分页内存分配数峰值
ULONG PagedBytes; //分配的分页内存字节数
ULONG NonPagedBytes; //分配的非分页内存数
ULONG PeakPagedBytes; //分配的分页内存峰值
ULONG PeakNonPagedBytes; //分配的非分页内存峰值
} MI_VERIFIER_DRIVER_ENTRY, * PMI_VERIFIER_DRIVER_ENTRY;

ViInsertVerifierEntry项可疑驱动链表插入表项,参数为一个指向该结构的指针。

系统加载一个内核模块时系统用MiApplyDriverVerifier查询要加载的模块是否在可疑驱动链表中,如果在则用MiEnableVerifier对其IAT修改。

对于NTLDR加载的模块,当驱动验证器初始化时,其MiInitializeVerifyingComponents遍历已加载的模块,并依次对其用MiApplyDriverVerifier

1
2
3
4
5
6
7
nt!MiEnableVerifier //挂接验证函数
nt!MiApplyDriverVerifier //应用驱动程序验证逻辑
nt!MiInitalizeVerifyingComponents //初始化验证器
nt!MmInitSystem //内存管理器阶段0初始化
nt!ExpInitializeExecutive //执行体阶段0初始化
nt!KiInitializeKernel //初始化内核
nt!KiSystemStartup //系统入口函数

对于之后的阶段1初始化,内存管理器工作函数MiLoadSystemImageMiApplyDriverVerifier给驱动验证器检查机会:

1
2
3
4
5
6
7
8
9
10
nt!MiEnableVerifier
nt!MiApplyDriverVerifier
nt!MiLoadSystemImage //加载系统映像文件
nt!MmLoadSystemImage //同上
nt!IopLoadDriver //加载驱动程序
...
nt!IoInitSystem //I/O子系统初始化
nt!Phase1Initialization //阶段1初始化
nt!PspSystemThreadStartup //系统线程启动函数
nt!KiThreadStartup //线程起始函数

Win10下nt!MiEnableVerifier的工作机制尚不明朗先鸽着。

对于观察验证情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
0: kd> x nt!MmVerifierData
fffff800`2b62a7c0 nt!MmVerifierData = <no type information>
0: kd> dt nt!_MM_DRIVER_VERIFIER_DATA fffff800`2b62a7c0
+0x000 Level : 0 //验证项目
+0x004 RaiseIrqls : 0 //对KeRaiseIrql的调用次数
+0x008 AcquireSpinLocks : 0 //获取自旋锁次数
+0x00c SynchronizeExecutions : 0 //同步总次数
+0x010 AllocationsAttempted : 0 //请求内存分配的次数
+0x014 AllocationsSucceeded : 0 //内存分配成功的次数
+0x018 AllocationsSucceededSpecialPool : 0 //从特殊内存池分配成功的次数
+0x01c AllocationsWithNoTag : 0 //不带标记的分配次数
+0x020 TrimRequests : 0 //请求修剪系统内存的次数
+0x024 Trims : 0 //实际修剪系统内存的次数
+0x028 AllocationsFailed : 0 //不带标记的分配次数
+0x02c AllocationsFailedDeliberately : 0 //故意的分配失败计数
+0x030 Loads : 0 //加载次数
+0x034 Unloads : 0 //卸载次数
+0x038 UnTrackedPool : 0 //没有追踪的内存次数
+0x03c UserTrims : 0 //整理用户态内存的次数
+0x040 CurrentPagedPoolAllocations : 0 //目前分页内存池中分配数
+0x044 CurrentNonPagedPoolAllocations : 0 //目前非分页内存池中分配数
+0x048 PeakPagedPoolAllocations : 0 //分配分页内存累计次数
+0x04c PeakNonPagedPoolAllocations : 0 //分配非分页内存的累计次数
+0x050 PagedBytes : 0 //分配的分页内存字节数
+0x058 NonPagedBytes : 0 //分配的非分页内存字节数
+0x060 PeakPagedBytes : 0 //累计分配的分页内存字节数
+0x068 PeakNonPagedBytes : 0 //累计分配的非分页内存字节数
+0x070 BurstAllocationsFailedDeliberately : 0 //故意失败的突发性内存分配次数
+0x074 SessionTrims : 0 //会话修剪次数
+0x078 OptionChanges : 0
+0x07c VerifyMode : 4
+0x080 PreviousBucketName : _UNICODE_STRING ""
+0x090 ExecutePoolTypes : 0
+0x094 ExecutePageProtections : 0
+0x098 ExecutePageMappings : 0
+0x09c ExecuteWriteSections : 0
+0x0a0 SectionAlignmentFailures : 0
+0x0a4 IATInExecutableSection : 0

应用程序验证器

不学了不学了要疯了。