某驱动的内核调试检测学习内核调试引擎加载机制

来源:互联网 发布:multiselect.zh cn.js 编辑:程序博客网 时间:2024/05/21 09:18

https://bbs.pediy.com/thread-186091.htm


作者:Tiany
QQ:304400230

如果大家调试过某驱动 就知道新版本的驱动已经改了很多 很主要的一点就是只要我们在boot.ini 里加入
multi(0)disk(0)rdisk(0)partition(1)\WINDOWS="Microsoft Windows 2003  - debug" /fastdetect /noguiboot /debug /debugport=com1 /baudrate=115200 
启动游戏的时候就会导致蓝屏 



这次测试 我只是加入了 /debug /debugport=com1    并没有开启 windbg 也产生了 系统错误 0x99999999
可以得出一个结论 就是某驱动对我们的启动模式有检测(地球人都知道)
首先我们来了解一下 加入了 /debug /debugport=com1 对系统启动的时候做了些什么操作


所有命令详情请查看微软官方说明
http://msdn.microsoft.com/en-us/library/windows/hardware/ff556253(v=vs.85).aspx

实际上在windows 内核初始化会对CPU做必要的初始化工作 包括16位实模式切换到32位保护模式 启动分页机制等 然后通过启动配置信息(Boot.ini 或 BCD)得到windows系统的系统目录并加装系统内核文件 而我们加入了 /debug ...这些 就是告诉了系统要初始化内核调试引擎

上图表示了windows的初始化逻辑图

而内核调试引擎初始化 主要在 KdInitSystem 这个函数里 
  第一次调用KdInitSystem
  从图18-10可以看到,系统在启动过程中会两次调用内核调试引擎的初始化函数,KdInitSystem。第一次是在系统内核开始执行后由入口函数KiSystemStartup调用。
  调用时,KdInitSystem会执行以下动作。
  初始化调试器数据链表,使用变量KdpDebuggerDataListHead指向这个链表。
  初始化KdDebuggerDataBlock数据结构,该结构包含了内核基地址、模块链表指针、调试器数据链表指针等重要数据,调试器需要读取这些信息以了解目标系统。
  根据参数指针指向的LOADER_PARAMETER_BLOCK结构寻找调试有关的选项,然后保存到变量中(见下文)。
  对于XP之后的系统,调用KdDebuggerInitialize0来对通信扩展模块进行阶段0初始化。对于XP之前的版本,调用KdPortInitialize来初始化COM端口。如果使用的是串行通信方式,那么KDCOM中的KdDebuggerInitialize0函数会调用模块内的KdCompInitialize函数来初始化串行口。不论是KdPortInitialize还是KdCompInitialize函数,成功初始化COM口后,都会设置HAL模块中所定义的KdComPortInUse全局变量,记录下已被内核调试使用的COM端口,此后的串行口驱动程序(serial.sys)会检查这个变量,并跳过用于调试用途的串行端口,因此,在目标系统的设备管理器中我们看不到用于内核调试的串行端口。
  此外,KdInitSystem会初始化以下全局变量:
  KdPitchDebugger:布尔类型,用来标识是否显式抑制内核调试。当启动选项中包含/NODEBUG选项时,这个变量会被设置为真。
  KdDebuggerEnabled:布尔类型,用来标识内核调试是否被启用。当启动选项中包含/DEBUG或/DEBUGPORT而且不包含/NODEBUG时,这个变量会被设置为真。
  KiDebugRoutine:函数指针类型,用来记录内核调试引擎的异常处理回调函数,当内核调试引擎活动时,它指向KdpTrap函数,否则指向KdpStub函数。
  KdpBreakpointTable:结构数组类型,用来记录代码断点,每个元素为一个BREAKPOINT_ENTRY结构,用来描述一个断点,包括断点地址。
  对于Windows XP之前的版本,还会初始化如下变量。
  KdpNextPacketIdToSend:整数类型,用来标识下一个要发送的数据包ID,KdInitSystem将这个变量初始化为0x80800000 | 0x800,即INITIAL_PACKET_ID(初始包)或SYNC_PACKET_ID(同步包)。
  KdpPacketIdExpected:整数类型,用来标识期待收到的下一个数据包ID ,初始化为0x80800000,即INITIAL_PACKET_ID。
  对于XP和之后的系统,当使用串行通信方式时,在KDCOM.DLL中定义了两个同样用途的变量KdCompNextPacketIdToSend和KdCompPacketIdExpected。
  对于Windows Vista,会初始化如下变量。
  KdAutoEnableOnEvent:布尔类型,如果调试设置中的启动策略为AUTOENABLE,则设为真。
  KdIgnoreUmExceptions:布尔类型,代表了处理用户态异常的方式,如果调试设置中的启动策略包含/noumex,则设为真,含义是忽略用户态异常。
关于KdInitSystem第一次被调用,还有如下两点值得说明。第一,只有当0号CPU执行KiSystemStartup函数时才会调用KdInitSystem,所以它不会被KiSystemStartup多次调用。第二,不管系统是否启用内核调试,调用都会发生。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
当我们启动/debug模式启动时候 
KdPitchDebugger = FALSE;
KdDebuggerEnabled = TRUE;
KiDebugRoutine = KdpTrap;
 
0: kd> dd KdPitchDebugger
808948a8  00000000 00000000 00000000 00000000
0: kd> dd KdDebuggerEnabled
808a5b90  00000001 00000000 00000014 00000000
 
0: kd> dd KiDebugRoutine 
8089aa8c  809cf27a fffff000 0000000f fffff000
 
0: kd> u 809cf27a 
nt!KdpTrap:
809cf27a 8bff            mov     edi,edi
809cf27c 55              push    ebp
809cf27d 8bec            mov     ebp,esp


这个3个全局变量都可以成为我们判断是否启动了内核调试引擎的标志 相信逆过以前某驱动的朋友都知道 他早期版本就是循环调用 KdDisableDebugger 来清0 KdDebuggerEnabled 达到防止调试的目的 靠着这样的思路 我们做以下测试  
//让KdPitchDebugger  = TRUE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
0: kd> ed KdPitchDebugger 1
0: kd> dd KdPitchDebugger
808948a8  00000001 00000000 00000000 00000000
//恢复KiDebugRoutine  = KdpStub
0: kd> u KdpStub
nt!KdpStub:
80824e88 8bff            mov     edi,edi
80824e8a 55              push    ebp
80824e8b 8bec            mov     ebp,esp
//得到KdpStub 的地址 0x80824e88 然后修改
0: kd> ed KiDebugRoutine 80824e88
//查看新的KiDebugRoutine  地址
0: kd> dd KiDebugRoutine
8089aa8c  80824e88 fffff000 0000000f fffff000
8089aa9c  0000000f ffffffff ffffffef ffffffff
0: kd> u 80824e88
nt!KdpStub:
80824e88 8bff            mov     edi,edi
80824e8a 55              push    ebp
80824e8b 8bec            mov     ebp,esp

//最后我们恢复KdDebuggerEnabled 说明一下 因为如果我们先恢复 KdDebuggerEnabled 就会导致 windbg 收不到消息了 所以最后才给他赋值
1
2
3
4
0: kd> ed KdDebuggerEnabled 0
0: kd> dd KdDebuggerEnabled 
808a5b90  00000000 00000000 00000014 00000000
808a5ba0  00000000 00000000 00000000 00000000


这时候我们点击windbg  的中断(Ctrl+Break) 已经断不下来了  启动游戏 让某驱动加载 还是蓝屏 系统错误 0x9999999

很明显了 某驱动检测内核引擎并不是靠读取这几个标志位的值 所以我们还要继续深入分析

内核对话
Windows启动内核调试后,主要做了以下几个工作
1.  建立连接
2.  调试器读取目标系统信息,初始化调试引擎(目标机)。
3.  内核调试引擎通过状态变化信息包通知调试器加载初始模块的调试符号(目标机)。
4.  调试器端发送中断包,将目标系统中断到调试器,交互调试后又恢复执行的过程。
5.  因断点命中,目标系统中断到调试器的过程。
6.  内核中的模块输出调试字符串(DbgPrint)到调试器
《软件调试》(490-491)总结的表如下:



内核调试引擎
内核调试引擎相当于再目标系统中进行调试的一个代理,调试引擎代表调试器来访问和控制目标系统。
内核调试的几个关键函数:
KdEnterDbugger
它用于冻结内核,调用后首先会禁止中断,对于多处理器的系统,它会将当前CPU的IRQL升高到HIGH_LEVEL并冻结所有的其他的CPU。锁定调试调试通信端口,调用KdSave让通信扩展模块保存通信状态,并将全局变量KdEnteredDebugger设置为真。当KdEnterDebugger执行后,整个系统进入一种简单的单任务状态,当前的CPU只执行当前的线程,其他CPU处于冻结状态。
KdExitDebugger 恢复内核运行,主要工作有调用KdRestore让通信扩展模块恢复通信状态,对锁定的通信端口解锁。调用KeThawExecution来恢复系统进入正常的运行状态,包括恢复中断,降低当前CPU的IRQL,对多CPU系统,恢复其他CPU。
KdpReportExceptionStateChange CPU报告异常类状态变化
KdpReportLoadSymbolsStateChange CPU报告符号加载类异常
KdpSendWaitContinue 函数用来发送信息包,与调试器对话。
KeUpdateSystemTime函数在每次更新系统时间时会检查全局变量KdDebuggerEnable来判断内核调试引擎是否被启动,如果为真则调用KdPollBreakIn函数来查看调试器是否发送了终端命令,如果是,则调用DbgBreakPointWithStatus触发断点异常,中断到调试器。
kdpTrap来处理内核态的异常。当内核态中发生异常时,KiDispatchException函数会调用全局变量KiDebugRoutine所指向的函数。当调试引擎启用时,这个变量的值是函数KdpTrap的地址。所以一旦异常发生时,系统就会调用KdpTrap。KdpTrap调用KdpReport向调试器报告异常。
KiSaveProcessorControlState保存CPU的控制状态
KiRestoreProcessorControlState恢复CPU状态
DbgPrint,DbgPrintEx,vDbbgPirntEx打印调试信息
//经过以上的了解我们大概知道了windbg 和 内核调试引擎的一些交互的流程和机制 更多的一些结构在《软件调试》一书中都有比较详细的说明 

这里我们要注意的是 当收到复位包的时候 清0 KdDebuggerNotPresent 冻结内核的时候 KdEnteredDebugger 会赋值KdEnteredDebugger = TRUE

这里我们为了验证这2个全局变量是不是当内核调试引擎启动后 才会  KdDebuggerNotPresent  = FALSE; KdEnteredDebugger = TRUE;
我们打开虚拟机不启用/debug 模式 启动 然后开启windbg 用本地内核调试 查看内容


这样可以得出结论 这2个全局变量也可以 判断系统是否是调试模式启动
但在我测试的过程中发现我虽然eb KdEnteredDebugger 0 了 操作系统还会把他置 1 我们知道置 1 的代码是KdEnterDebugger () 这个函数所干的 
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> u KdEnterDebugger l50
nt!KdEnterDebugger:
809cd36c 8bff            mov     edi,edi
809cd36e 55              push    ebp
809cd36f 8bec            mov     ebp,esp
809cd371 837d0800        cmp     dword ptr [ebp+8],0
809cd375 742c            je      nt!KdEnterDebugger+0x37 (809cd3a3)
809cd377 ff7508          push    dword ptr [ebp+8]
809cd37a e83f200000      call    nt!KdpQueryPerformanceCounter (809cf3be)
809cd37f a388869e80      mov     dword ptr [nt!KdTimerStop (809e8688)],eax
809cd384 2b0580869e80    sub     eax,dword ptr [nt!KdTimerStart (809e8680)]
809cd38a 89158c869e80    mov     dword ptr [nt!KdTimerStop+0x4 (809e868c)],edx
809cd390 1b1584869e80    sbb     edx,dword ptr [nt!KdTimerStart+0x4 (809e8684)]
809cd396 a390869e80      mov     dword ptr [nt!KdTimerDifference (809e8690)],eax
809cd39b 891594869e80    mov     dword ptr [nt!KdTimerDifference+0x4 (809e8694)],edx
809cd3a1 eb0e            jmp     nt!KdEnterDebugger+0x45 (809cd3b1)
809cd3a3 832588869e8000  and     dword ptr [nt!KdTimerStop (809e8688)],0
809cd3aa 83258c869e8000  and     dword ptr [nt!KdTimerStop+0x4 (809e868c)],0
809cd3b1 53              push    ebx
809cd3b2 56              push    esi
809cd3b3 648b3520000000  mov     esi,dword ptr fs:[20h]
809cd3ba ff1594108080    call    dword ptr [nt!_imp__KeGetCurrentIrql (80801094)]
809cd3c0 ff750c          push    dword ptr [ebp+0Ch]
809cd3c3 888645050000    mov     byte ptr [esi+545h],al
809cd3c9 ff7508          push    dword ptr [ebp+8]
809cd3cc e8ab31e6ff      call    nt!KeFreezeExecution (8083057c)
809cd3d1 b980438b80      mov     ecx,offset nt!KdpDebuggerLock (808b4380)
809cd3d6 8ad8            mov     bl,al
809cd3d8 e83fb8ebff      call    nt!KeTryToAcquireSpinLockAtDpcLevel (80888c1c)
809cd3dd 6a00            push    0
809cd3df a220879e80      mov     byte ptr [nt!KdpPortLocked (809e8720)],al
809cd3e4 e81597ebff      call    nt!KdSave (80886afe)
809cd3e9 5e              pop     esi
809cd3ea 8ac3            mov     al,bl
809cd3ec c705885b8a8001000000 mov dword ptr [nt!KdEnteredDebugger (808a5b88)],1    [COLOR="Red"]// 重点在这里[/COLOR]
809cd3f6 5b              pop     ebx
809cd3f7 5d              pop     ebp
809cd3f8 c20800          ret     8


我们就直接asm KdEnterDebugger 这个函数 让他对 nt!KdEnteredDebugger 置 0 
1
2
3
4
//windbg 执行修改代码
0: kd> eb 809cd3ec c7 05 88 5b 8a 80 00
//结果
809cd3ec c705885b8a8000000000 mov dword ptr [nt!KdEnteredDebugger (808a5b88)],0


大家要注意 我这样修改是为了方便马上得到结果才这样做的 这样可能会对系统照成不稳定 所以希望大家不要学我
//已经验证了 KdEnteredDebugger  不会被置 1 了 我们运行游戏看看
0: kd> g
[Tenio_Error] Can't find Component(33554448) Creator From Dll List!
[Tenio_Error] Can't find Component(33554448) Creator From Dll List!
[Tenio_Error] Can't find Component(33554448) Creator From Dll List!
[Tenio_Error] Can't find Component(33554448) Creator From Dll List!
[Tenio_Error] Can't find Component(33554448) Creator From Dll List!
[Tenio_Error] Can't find Component(33554448) Creator From Dll List!
Tenio[Error]: To remove a component not exist
Tenio[Error]: IComponent* = 0x0130C00C
[Tenio_Error] Can't find Component(33554448) Creator From Dll List!
[Tenio_Error] Can't find Component(33554448) Creator From Dll List!
[Tenio_Error] Can't find Component(33554448) Creator From Dll List!
[Tenio_Error] Can't find Component(33554448) Creator From Dll List!
[Tenio_Error] Can't find Component(33554448) Creator From Dll List!
[Tenio_Error] Can't find Component(33554448) Creator From Dll List!
[Tenio_Error] Can't find Component(33554448) Creator From Dll List!
[Tenio_Error] Can't find Component(33554448) Creator From Dll List!
[Tenio_Error] Can't find Component(33554448) Creator From Dll List!
[Tenio_Error] Can't find Component(33554448) Creator From Dll List!
Rule ModuleEntry Start---------------------------
LPDispachIoRequest=F662C008
Rule ModuleEntry end-----------------------------


某驱动已经加载了 而且并没有蓝屏  这里我们得出结论了 某驱动就是对KdEnteredDebugger 的值进行判断 是否开启了内核

调试引擎的 因为我直接修改KdEnterDebugger() 函数 导致某驱动可能初始化出问题 没出现游戏界面一直卡着了

但是能定位到他是检测什么的了 我相信朋友们要绕过它 PASS它 FUCK它 都有各自的奇招 我就不献丑我如何逆向他的了 

定位到他关键代码:


大概的流程是获取到 KdEnteredDebugger 地址 然后创建MDL   然后判断用MDL返回的指针 来判断 KdEnteredDebugger 的值 所以我用了个很简单的方法来绕过他 就是最简单的HOOK 

1
2
3
4
5
6
7
8
typedef PMDL (__stdcall *_MyIoAllocateMdl)(
           __in_opt PVOID  VirtualAddress,
           __in ULONG  Length,
           __in BOOLEAN  SecondaryBuffer,
           __in BOOLEAN  ChargeQuota,
           __inout_opt PIRP  Irp  OPTIONAL);
 
_MyIoAllocateMdl old_IoAllocateMdl;


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
PMDL MyIoAllocateMdl(
        __in_opt PVOID  VirtualAddress,
        __in ULONG  Length,
        __in BOOLEAN  SecondaryBuffer,
        __in BOOLEAN  ChargeQuota,
        __inout_opt PIRP  Irp  OPTIONAL)
{
  if (pKdEnteredDebugger == VirtualAddress)
  {
    VirtualAddress = pKdEnteredDebugger + 0x20;  //+0x20  是让他读到其他的位置
  }
   
  return old_IoAllocateMdl(VirtualAddress,Length,SecondaryBuffer,ChargeQuota,Irp);
}
 
 
//////////////////////////////////////////////////////////////////////////
//模块加载回调函数例程
VOID LoadImageRoutine(IN PUNICODE_STRING  FullImageName,
            IN HANDLE  ProcessId, // where image is mapped
            IN PIMAGE_INFO  ImageInfo)
{
  PMDL pMdl;
  ULONG uHookAddr,uJmpAddr;
  if (wcsstr(FullImageName->Buffer,L"TesSafe.sys") != NULL)
  {
    KdPrint(("TesSafe.sys -------------加载-------------\r\n"));
    KdPrint(("TesSafe.sys --->ImageBase 0x%x\r\n",(ULONG)ImageInfo->ImageBase));
    KdPrint(("TesSafe.sys --->ImageSize 0x%x\r\n",(ULONG)ImageInfo->ImageSize));
    KdPrint(("断点命令: ba e 1 0x%x+ \r\n",(ULONG)ImageInfo->ImageBase));
    KdBreakPoint();
 
    InlineHook(IoAllocateMdl,MyIoAllocateMdl,(void**)&old_IoAllocateMdl);
  }
  return ;
}


编译后运行 成功绕过了 内核调试引擎检测 



但是虚拟机不能下断点了 呵呵 老样子 它还是回阻止你调试 但这已经不是本章的内容了 不蓝屏了 朋友们可以慢慢一步一步的分析 

总结:
1.通过了解win内核调试引擎的加载机制 解决了我们想要解决的问题 
2. 
KdPitchDebugger 
KdDebuggerEnabled 
KiDebugRoutine 
KdDebuggerNotPresent 
KdEnteredDebugger
3.知道了 以上5个全局变量 都可以来判断 内核调试是否开启
了解WINDBG 和 内核引擎是如何完成初始化操作的 

本章内容多数采自《软件调试》一书

上传的附件:
  •  1.jpg (84.70kb,34次下载)
  •  2.jpg (167.49kb,344次下载)
  •  3.jpg (123.09kb,150次下载)
  •  4.jpg (108.86kb,105次下载)
  •  5.jpg (117.59kb,59次下载)
  •  6.jpg (278.22kb,55次下载)
  •  7.jpg (71.34kb,8次下载)
  •  8.jpg (151.22kb,122次下载)
  •  9.jpg (408.75kb,39次下载)
  •  10.jpg (157.71kb,231次下载)

原创粉丝点击