对双机调试的探索
最近学习游戏保护,发现VM+WINDBG双机调试不能使用,以游戏保护*P为例,经过论坛搜索,发现有以下几点动了手脚:
1. kdDebuggerEnabled变量不停的清零,清零的代码还做了检测,所以意味着不能轻易修改*P的代码,
kdDebuggerEnabled是windows全局变量,用来标识内核调试是否被启用
开启调试状态:*(PBYTE)KdDebuggerEnabled=0x01;
禁止调试状态:*(PBYTE)KdDebuggerEnabled=0x00;
2. KiDebugRoutine变量不停设置为KdpStub函数, 其写入的代码同上做了检测,不能随便修改
KiDebugRoutine是函数指针,内核调试引擎的异常处理回调函数指针。当内核调试引擎活动时,它指向KdpTrap函数,否则指向KdpStub函数
3. KdSendPacket和KdReceivePacket函数的IAT HOOK,且在HOOK的地方也做了检测,所以同样不能简单修改来达到目的
KdSendPacket KDCOM函数,发送数据包
KdReceivePacket KDCOM函数,接收数据包
以上就是*p的3块防护及其含义,下面一块一块来探索一番
第一块:
正常情况下ctrl+break中断到windbg,
发现此时kdDebuggerEnabled为1,
修改后F5虚拟机跑起来,但是ctrl+break不能断下了
首先想到观察下ctrl+break的函数流程, 看看这个变量对流程中的哪个地方造成了阻碍,
双机调试正常情况下ctrl+break中断到windbg栈回溯如下:
或者
无论从哪里发起最后的几个函数流程都是一样KeUpdateRunTime-> KdCheckForDebugBreak-> RtlpBreakWithStatusInstruction
自然想看看这几个函数中的代码,看看是否和kdDebuggerEnabled有关,
如果kdDebuggerEnabled为0则跳到83eb409d,这样就不会执行KdCheckForDebugBreak函数了,但是流程中KdCheckForDebugBreak函数必须要执行此函数后,ctrl+break才能断下,所以证明了kdDebuggerEnabled设置为0直接导致ctrl+break的失效,以此类推对余下的两个函数进行反汇编,发现如下代码:
可见此函数也用到了kdDebuggerEnabled变量,除此之外还可以发现2个问题:
1. 除了kdDebuggerEnabled变量,貌似KdPitchDebugger变量也比较重要,百度以下发现: 如果KdPitchDebugger为TRUE,即在bcd中指定了nodebug,这样也就不支持调试了,所以也算一个内核调试是否启用的标志,
2. 流程中KdCheckForDebugBreak调用的是RtlpBreakWithStatusInstruction,但是代码中却没有发现他的调用,唯一调用的两个函数是KdPollBreakIn和DbgBreakPointWithStatus,抱着试一试的心态反汇编这两个函数发现:
发现RtlpBreakWithStatusInstruction其实是DbgBreakPointWithStatus的一个标签,所以流程中KdCheckForDebugBreak实际上是调用的DbgBreakPointWithStatus
由于KdCheckForDebugBreak中有对KdPollBreakIn的调用,所以顺道反汇编以下KdPollBreakIn函数,偶然也发现一处和kdDebuggerEnabled相关的代码:
总结一下上面的发现: 当把kdDebuggerEnabled设置为0后,ctrl+break失效,跟踪流程发现kdDebuggerEnabled和KdPitchDebugger标志直接影响中断到windbg,反汇编流程中的函数发现:
包含KdDebuggerEnabled的函数有:
KeUpdateRunTime
KdCheckForDebugBreak
KdPollBreakIn
包含KdPitchDebugger的函数有:
KdCheckForDebugBreak
KdPollBreakIn
接下来就是编程解决问题,思路是转移变量,把这些函数中的KdDebuggerEnabled和KdPitchDebugger变量设置为自己驱动模块中的全局变量,部分代码如下:
经过以上转移以后 再次清零KdDebuggerEnabled变量后,也可以ctrl+break断点到windbg中,第一块问题就基本解决了
第二块:
正常情况下观察KiDebugRoutine
将其设置为KdpStub函数
Windbg F5运行起来,接着ctrl+break发现VM死机了,看来这个影响还蛮大的,重启后IDA打开内核搜索全部KiDebugRoutine,发现调用它的代码全部都在KiDispatchException中,反汇编KiDispatchException搜索KiDebugRoutine相关代码,过程和第一块一样,发现以下三个结果:
看来先前修改了KiDebugRoutine,造成的死机就是在KiDispatchException中出现的问题,现在编程解决问题,思路和上一块一样,转移KiDebugRoutine变量,部分代码如下:
这样一来第二块问题也基本解决了,这个和第一块原理一样,接下来就是第三块了
第三块:
反汇编KdSendPacket
可以发现其他call KdSendPacket其实是通过jmp访问了83e501cc地址里边的80bc757c,这里的83e501cc也就是iat导入表中的地址,KdSendPacket函数属于kdcom.dll,内核文件是引入的这个dll,IAT HOOK也就是把83e501cc地址里边的80bc757c修改成了自己的函数地址,80bc757c本来是真正的KdSendPacket函数地址,KdReceivePacket函数原理也是一样,解决的思路是把call KdSendPacket的代码修改为call 80bc757c这样就不经过iat表了,当然此方法牵涉的函数量比较大,部分代码实现如下:
经过以上的替换基本就可以完成第三块的功能了,需要说明的是,上面的函数特征码是通过IDA的全部搜索功能完成的,后经尝试并不需要替换全部的函数,这个可以自行实验删减
继续升级:
上面三块经编译测试发现已经可以ctrl+break断在windbg中了,如图:

但是用PCHunter观察内核修改,发现修改的地方全部显示出来了,多且乱,这样一来很容易被封堵掉,具体如图:

这里显示的是47个修改点,再对比下正常情况下的内核修改

只有17个修改点,可见动的手脚太大了,所以需要进一步升级程序,目标是是尽可能减少原内核的修改,思路是加载一个新内核,把调试相关的流程引入到新内核中,然后在新内核中进行以上修改,这样一来基本满足先前的目标了.具体原理如下:
第一点:
在双机调试win7时,在windbg中用ctrl+break中断到windbg后,栈回溯有以下结果:
可以发现各个流程都会经过KeUpdateRunTime,这里HOOK这个函数,让流程进入新内核
第二点:
调试的符号加载过程是通过KdpSendWaitContinue来发送消息的 这个函数的调用来源于
KdpReportLoadSymbolsStateChange和KdpReportCommandStringStateChange 栈回溯如下:
这是一个新线程来执行的,所以必须在其中找一个函数来HOOK,让其进入新内核,这样才便于修改,
暂时定为HOOK KiDispatchException函数让其转入新内核
第三点:
KdpSendWaitContinue函数中有个switch的代码,新内核KdpSendWaitContinue中switch反汇编如下:
8907e9ea是指令地址,跳向的地方是841480b4,它也是switch的跳转表了 内容如下:
可以发现switch中的跳转是到原内核,如果执行了switch跳转指令,流程又会回到原内核,所以跳转表需要相应修改
第四点:
关于int3断点和调试信息打印KdPrint都是要经过KiDispatchException的,前边第二点修改以后这里也就OK了
总结:
调试有3个流程,
一个基本的流程通过HOOK KeUpdateRunTime搞定
一个加载符号的流程,通过HOOK KiDispatchException搞定
一个int3断点和调试信息打印,也是通过HOOK KiDispatchException搞定
当整个流程都在新内核中以后 就可以任意的修改新内核了 不容易被检测到 一劳永逸
具体代码见附件,代码只是代表思路,里边有一些自定义函数,除加载新内核外,其他功能比较单一,如有需要自行实现,加载新内核的代码可以搜索论坛得到,升级后的程序,运行后ctrl+break断到windbg中,查看栈回溯可以看到:
的确是进入新内核了,再程序运行前后修改点的变化
运行前:

运行后:

这里增加了2个修改点,就正是为了实现进入新内核而HOOK的两个函数,至此就升级完毕了.
后记:
从这个双机调试的探索可以看出来,其实这就是内核调试的一部分基本原理,在张老师的<软件调试>一书中全部都有,但是现在*P只是用了一部分来做修改,以后还有很大升级空间,所以跟着知识走才是王道,有空之余完全可以把<软件调试>上的内核调试知识,写入代码中,这样一来就不怕其他程序升级造成影响了,如果后期这两个函数被检测,可以根据栈回溯上下来换成其他函数, ,目的只有一个进入新内核,可换的函数从数量上来说还是可观的,此方法整体看来比较繁杂,但是后面用起来比较方便,不受至于其他程序,自己按照调试流程来就行,一劳永逸.
1. kdDebuggerEnabled变量不停的清零,清零的代码还做了检测,所以意味着不能轻易修改*P的代码,
kdDebuggerEnabled是windows全局变量,用来标识内核调试是否被启用
开启调试状态:*(PBYTE)KdDebuggerEnabled=0x01;
禁止调试状态:*(PBYTE)KdDebuggerEnabled=0x00;
2. KiDebugRoutine变量不停设置为KdpStub函数, 其写入的代码同上做了检测,不能随便修改
KiDebugRoutine是函数指针,内核调试引擎的异常处理回调函数指针。当内核调试引擎活动时,它指向KdpTrap函数,否则指向KdpStub函数
3. KdSendPacket和KdReceivePacket函数的IAT HOOK,且在HOOK的地方也做了检测,所以同样不能简单修改来达到目的
KdSendPacket KDCOM函数,发送数据包
KdReceivePacket KDCOM函数,接收数据包
以上就是*p的3块防护及其含义,下面一块一块来探索一番
第一块:
正常情况下ctrl+break中断到windbg,
首先想到观察下ctrl+break的函数流程, 看看这个变量对流程中的哪个地方造成了阻碍,
双机调试正常情况下ctrl+break中断到windbg栈回溯如下:
自然想看看这几个函数中的代码,看看是否和kdDebuggerEnabled有关,
1. 除了kdDebuggerEnabled变量,貌似KdPitchDebugger变量也比较重要,百度以下发现: 如果KdPitchDebugger为TRUE,即在bcd中指定了nodebug,这样也就不支持调试了,所以也算一个内核调试是否启用的标志,
2. 流程中KdCheckForDebugBreak调用的是RtlpBreakWithStatusInstruction,但是代码中却没有发现他的调用,唯一调用的两个函数是KdPollBreakIn和DbgBreakPointWithStatus,抱着试一试的心态反汇编这两个函数发现:
由于KdCheckForDebugBreak中有对KdPollBreakIn的调用,所以顺道反汇编以下KdPollBreakIn函数,偶然也发现一处和kdDebuggerEnabled相关的代码:
总结一下上面的发现: 当把kdDebuggerEnabled设置为0后,ctrl+break失效,跟踪流程发现kdDebuggerEnabled和KdPitchDebugger标志直接影响中断到windbg,反汇编流程中的函数发现:
包含KdDebuggerEnabled的函数有:
KeUpdateRunTime
KdCheckForDebugBreak
KdPollBreakIn
包含KdPitchDebugger的函数有:
KdCheckForDebugBreak
KdPollBreakIn
接下来就是编程解决问题,思路是转移变量,把这些函数中的KdDebuggerEnabled和KdPitchDebugger变量设置为自己驱动模块中的全局变量,部分代码如下:
第二块:
正常情况下观察KiDebugRoutine
将其设置为KdpStub函数
Windbg F5运行起来,接着ctrl+break发现VM死机了,看来这个影响还蛮大的,重启后IDA打开内核搜索全部KiDebugRoutine,发现调用它的代码全部都在KiDispatchException中,反汇编KiDispatchException搜索KiDebugRoutine相关代码,过程和第一块一样,发现以下三个结果:
第三块:
反汇编KdSendPacket
继续升级:
上面三块经编译测试发现已经可以ctrl+break断在windbg中了,如图:
但是用PCHunter观察内核修改,发现修改的地方全部显示出来了,多且乱,这样一来很容易被封堵掉,具体如图:
这里显示的是47个修改点,再对比下正常情况下的内核修改
只有17个修改点,可见动的手脚太大了,所以需要进一步升级程序,目标是是尽可能减少原内核的修改,思路是加载一个新内核,把调试相关的流程引入到新内核中,然后在新内核中进行以上修改,这样一来基本满足先前的目标了.具体原理如下:
第一点:
在双机调试win7时,在windbg中用ctrl+break中断到windbg后,栈回溯有以下结果:
第二点:
调试的符号加载过程是通过KdpSendWaitContinue来发送消息的 这个函数的调用来源于
KdpReportLoadSymbolsStateChange和KdpReportCommandStringStateChange 栈回溯如下:
暂时定为HOOK KiDispatchException函数让其转入新内核
第三点:
KdpSendWaitContinue函数中有个switch的代码,新内核KdpSendWaitContinue中switch反汇编如下:
第四点:
关于int3断点和调试信息打印KdPrint都是要经过KiDispatchException的,前边第二点修改以后这里也就OK了
总结:
调试有3个流程,
一个基本的流程通过HOOK KeUpdateRunTime搞定
一个加载符号的流程,通过HOOK KiDispatchException搞定
一个int3断点和调试信息打印,也是通过HOOK KiDispatchException搞定
当整个流程都在新内核中以后 就可以任意的修改新内核了 不容易被检测到 一劳永逸
具体代码见附件,代码只是代表思路,里边有一些自定义函数,除加载新内核外,其他功能比较单一,如有需要自行实现,加载新内核的代码可以搜索论坛得到,升级后的程序,运行后ctrl+break断到windbg中,查看栈回溯可以看到:
的确是进入新内核了,再程序运行前后修改点的变化
运行前:
运行后:
这里增加了2个修改点,就正是为了实现进入新内核而HOOK的两个函数,至此就升级完毕了.
后记:
从这个双机调试的探索可以看出来,其实这就是内核调试的一部分基本原理,在张老师的<软件调试>一书中全部都有,但是现在*P只是用了一部分来做修改,以后还有很大升级空间,所以跟着知识走才是王道,有空之余完全可以把<软件调试>上的内核调试知识,写入代码中,这样一来就不怕其他程序升级造成影响了,如果后期这两个函数被检测,可以根据栈回溯上下来换成其他函数, ,目的只有一个进入新内核,可换的函数从数量上来说还是可观的,此方法整体看来比较繁杂,但是后面用起来比较方便,不受至于其他程序,自己按照调试流程来就行,一劳永逸.