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用KiPcToFileHeader
或MmLocateUnloadedDriver
寻找对应模块名称。
若启用了内核调试,则用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初始化,内存管理器工作函数MiLoadSystemImage
用MiApplyDriverVerifier
给驱动验证器检查机会:
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
|
应用程序验证器
不学了不学了要疯了。