TP保护的研究和学习-内核调试探索篇(一)

TP保护的研究和学习-内核调试探索篇(一)

引言:

​ 本篇系列旨在研究学习腾讯保护系统的保护方案实现,文中尽量以“发现问题然后尝试解决问题”的模式来处理遇到的问题,此前网上有太多相关的资料都只是给出了一个结论式答案或者方法,对于想要深入学习的人太不友好。 我相信有很多人和我一样在尝试去深入分析的时候遇到了各种问题,我的处理方式就是遇到实际问题实际分析的方式来推进学习;

​ 计算机理论到现在为止已经非常完善了, 它是一门科学;我们要相信科学,那么一切的问题和现象都是有据可依的,耐心肯定能找到原因的。

​ 调试目标是DNF,囊括了老版本的70版本,90版本,以及95版本;也希望给同样对操作系统内核和保护机制感兴趣的人能起到抛砖引玉之用。

测试环境:

​ 操作系统环境:Win7 7600 32

​ 游戏环境: 90版本

​ 调试器:WinDbg 10.0.18351.1 AMD64

以下内容看起来会比较散乱,因为在实际做笔记的时候,会比较匆忙,读者见谅。

阻止内核调试器的研究分析

​ 想要了解程序的行为,除开去分析代码的方法,再一个方法就是去让目标程序程序运行起来,观测其行为然后再来判断它的功能.

测试开始

  • **第一波测试:**现象:当启用内核调试运行游戏时,游戏启动时加载TesSafe.sys,在TesSafe.sys的DriverEntry的时候,就会触发Trap0E异常,并且在Trap0E的内部再次循环产生0E异常,然后会导致蓝屏,以下是调用堆栈:
...
KiTrap0E+0x175
KiTrap0E+0x175
KiTrap0E+0x175
KeUpdateRunTime
TesSafe.sys+xxxxx
DriverEntry

经测试,以上的蓝屏属于特殊情况,在之后的测试中没有再次出现这个蓝屏。

  • **第二波无修改测试:**直接启动游戏,在游戏点击登录之后,出现蓝屏。

在这里插入图片描述

蓝屏错误代码为0x99999999,很明显微软并没有这个蓝屏错误,那么就只有一个可能就是游戏检测到了当前正处于调试模式;

  • 第三波尝试,这一次的思路是,先启动70版本的游戏,然后再次启动90的程序,意图是先让70版本的游戏”初始化“系统环境,然后再启动90版本,看看有什么反应。

    ​ 先启动70版本的DNF,该版本的防护系统运行时会调用nt!KdDisableDebugger以及其他的什么操作,然后再运行90版本DNF,但是此时会直接出现蓝屏,

    ​ 针对以上问题的思路,因为游戏尝试调用了KeBugCheck函数来产生一个蓝屏,那么此时尝试来中断KeBugCheck来看看能不能阻止蓝屏。 (未测试成功,当修改了KdDebuggerEnabled等变量值时,不会再次产生蓝屏。修改了这些用于调试的变量时,Windbg就不能连接观察了。 关于KdDebuggerEnabled等变量的测试分析参考70版本DNF的分析笔记。

2019.1.17

第四波尝试,其实已经是昨天的尝试了,这里和今天的进展一起记录。

虚拟机进入调试模式,然后在运行游戏之前,使用Windbg进行本地调试修改如下的数据;

;; 注意:这些命令需要在本地调试的时候执行,因为这些数据一旦修改,双机调试会立即中断而不能接收到任何调试信息
;; 修改nt!KiDebugRoutine为nt!KdpStub; 名称来自于ntoskrnl.exe 的导出
; nt!KdpTrap是当前调试器连接的时候需要调用的函数,该函数内部会将异常发送到调试器进行处理;
ed nt!KiDebugRoutine nt!KdpStub

; 修改nt!KdDebuggerEnabled,这个变量也是一个关键变量,所有有关于调试的的函数都会使用该变量进行判断;
eb nt!KdDebuggerEnabled 0

通过以上修改后测试发现已经不会蓝屏,但是在游戏启动阶段虚拟机直接重启,也就是说连蓝屏的机会都不给了。也就是说游戏仍然检测了更多的位置;

既然以上的操作没用,那么此时重新回归异常和调试的流程重新开始分析;

这里可以参考另一篇分析文档。[原创]某驱动的内核调试检测学习内核调试引擎加载机制

通过观察WRK和ReactOS的源码以及查看ntoskrnl.exe的导出函数(通过LordPE等工具可以查看导出函数)得知,ntoskrnl.exe有且导出了如下的变量且可以用这些变量来感知当前操作系统是否正处于调试模式:

  • nt!KdDebuggerNotPresent, 当此变量为TRUE时,则代表当前调试器未就绪,在给调试器发送的数据的时候如果该值为TRUE,函数会直接返回;
  • nt!KdEnteredDebugger, 这个变量没有什么实质的作用,但是该变量在进入调试器的时候,会将此变量置为1,参见函数nt!KdEnterDebugger

通过以上的分析所得,结合70版本的分析再加上以下命令继续测试:

;可选修改项
;; 修改nt!KdDebuggerNotPresent;名称来自于ntoskrnl.exe 的导出
;; 有可能修改此名称会导致虚拟机越来越卡,
eb nt!KdDebuggerNotPresent 1


;; 修改nt!KdEnteredDebugger; 
; nt!KdEnterdDebugger的修改仅仅来源于函数nt!KdEnterDebugger,在KdEnterDebugger调用结束时进行赋值;
; 注意:此变量的修改需要在以上变量修改之后进行修改,否则如果一旦在过程中中断到了调试器,那么此值会被重新覆盖
eb nt!KdEnteredDebugger 0


;; 注意:nt!KdDebuggerEnabled 变量需要最后进行修改,因为一旦此变量被置为FALSE,在WIN7以后,本地调试连接也将中断

通过以上的修改,发现当前虚拟机已经不蓝屏也不重启了,并且已经进入了游戏。这就说明该防护系统的确是通过判断以上变量的值来判断操作系统是否处于调试模式(准确说是nt!KdEnteredDebugger这个变量来判断是否处于调试模式的,当处于游戏中时,我尝试通过重新赋值该变量为1,这时候会立即重启)。

重新建立双机调试的探索

还原测试第一波

经过以上修改,现在已经可以正常的进入游戏并且游戏不会报错了。那么接下来需要解决的问题就是,怎样才能重新建立器调试连接?

根据以上的修改,我们已经明确的知道了游戏检测的位置,那么是否通过重新还原以上的操作,就可以重新建立起来双机调试呢? 那么这里做一下测试:

这里贴出KdEnterDebugger的函数代码

kd> uf KdEnterDebugger
nt!KdEnterDebugger:
8437016d 8bff            mov     edi,edi
8437016f 55              push    ebp
84370170 8bec            mov     ebp,esp

;............ 省略部分代码

nt!KdEnterDebugger+0xb1:
8437021e b980e41a84      mov     ecx,offset nt!KdpDebuggerLock (841ae480)
84370223 e88cd3d3ff      call    nt!KeTryToAcquireSpinLockAtDpcLevel (840ad5b4)
84370228 57              push    edi
84370229 a2d4363b84      mov     byte ptr [nt!KdpPortLocked (843b36d4)],al
8437022e e8b6c2cdff      call    nt!KdSave (8404c4e9)
84370233 5f              pop     edi
84370234 8935e48c1a84    mov     dword ptr [nt!KdEnteredDebugger (841a8ce4)],esi ; 此处进行状态赋值
8437023a 5e              pop     esi
8437023b 8ac3            mov     al,bl
8437023d 5b              pop     ebx
8437023e 59              pop     ecx
8437023f 5d              pop     ebp
84370240 c20800          ret     8

在上列代码0x84370234处针对nt!KdEnteredDebugger进行的修改, mov dword ptr [nt!KdEnteredDebugger (841a8ce4)],esi,那么既然要重新建立调试,KdEnterDebugger在不重新写的情况下,就需要对这里进行处理,

;直接对这句代码进行NOP操作
; 至于为什么要修改,上面已经说明白了,是因为TesSafe.sys检测了nt!KdEnteredDebugger变量的值,当它为1时,系统就会直接重启,因此在不重写函数的情况下,对这句代码进行NOP是最快的测试方法了。
eb 84370234 90 90 90 90 90 90 

; 变量nt!KdDebuggerNotPresent为0
eb nt!KdDebuggerNotPresent 0

; 让异常处理流程走内核调试器分发路线
eb nt!KiDebugRoutine nt!KdpTrap

; 让调试器生效
eb nt!KeDebuggerEnabled 1

测试结果: 通过以上的还原测试之后,双机调试的Windbg并没有收到任何的调试信息,按下CTRL+BREAK仍然没有中断, 很明显TP防护做的事情还更多更深入;

还原测试第二波

既然以上的流程没用,那么很显然TP防护在处理异常的流程里面做了更多的事情(为什么说是异常处理流程?调试器的中断等等都是产生异常进入异常处理流程,然后发送异常到调试器)。既然如此,那么这里就需要重新再翻一翻有关于异常的产生和处理了。

这里写一段会产生异常的代码,如下:

__try
{
	__asm int 3;
}
__except(1)
{
    //printf("Exception handled");
}

当这段代码执行的时候是什么样的流程呢(流程取自Win7 32 7600)?

  1. 当进入当前函数时,压入SEH结构体到SEH链表中;
  2. 执行int3时,触发int 3中断,并进行栈切换;
  3. CPU查表找到KiTrap03函数并执行;
  4. KiTrap03执行Trap03Handler函数,随后执行CommonExceptionDispatcher, 并随后执行KiDispatchException
  5. KiDispatchException中执行KiDebugRoutine函数,此时KiDebugRoutine中保存的函数需要为nt!KdpTrap时才会进入调试器处理函数
  6. KdpTrap判断当前的异常类型,因为我们这里是触发的INT3断点,所以会走KdpReport函数;
  7. KdpReport函数中的流程:
    1. 首先调用nt!KdEnterDebugger冻结其他的CPU的执行,并且申请nt!KdpDebuggerLock(自旋锁);
    2. 调用KdpReportExceptionStateChange, 然后在此函数中会调用KdpSendWaitContinue函数进行数据发送和命令接受; KdpReportExceptionStateChange中首先检查了当前的异常是否在断点表里面并且断点是否是无效的,如果是的话,KdpReportExceptionStateChange将会直接返回,但是此处我们的异常(INT3)是主动产生的,没有在断点表里面,所以这里可以直接略过,所以即便是TesSafe.sys清空该表那么也不影响该测试
  8. nt!KdpSendWaitContinue首先调用nt!KdSendPacket进行数据的发送, 但nt模块中的KdSendPacket是读取IAT表跳转至kdcom模块KdSendPacket的跳转代码, 此时流程进入kdcom模块,执行kdcom!KdSendPacket函数进行数据的发送;
  9. kdcom模块中数据的发送流程:kdcom!KdpSendString->kdcom!KdCompPutByte->kdcom!CpPutByte->kdcom!KdReadUchar->更多的细节;
  10. 更多的流程

以上为触发INT3断点时的发送异常信息的部分流程,更详细的流程参见《软件调试》;

在以上的流程中,可以确定的是,无论TP如何修改,那么肯定会执行到第5步,无论nt!KiDebugRoutine是否nt!KdpTrap, 因此在保证KiDebugRoutine的值为nt!KdpTrap的情况下,流程必然会经过第6步进入第7步,为什么会经过第6步?因为这一步里面除了修改代码亦或者修改上层传递的异常信息,否则非常容易被发现。用PCHunter一扫描,什么都看到了,很容易暴露;

第7步的时候里面有个变量值得注意一下: nt!KdpDebuggerLock如果这个变量为1的话,那么就说明被程序占用了。而且如果修改这个变量的话,那么PCHunter之类的工具是扫描不出来的,因为它属于.data区段,是一个全局变量。但是这里测试的结果表明:TesSafe.sys并没有使用占用nt!KdpDebuggerLock来达到阻止调试的目的,在调试器没有中断的情况下该值并没有发生变化

接下来观察第8步的流程,nt!KdpSendWaitContinue函数中一开始就调用了nt!KdSendPacket,此时NT中使用的KdSendPacket是导入kdcom!KdSendPacket,因此此时程序流程转入kdcom;

kdcom中就是真正的数据发送流程,该函数首先检查数据的检验和,然后进入KdpSendString函数, 该函数中调用KdCompPutByte,然后继续跟踪,在KdCompPutByte调用了CpPutByte,此时在调用CpPutByte的时候,他的第一个参数是PortPort是属于kdcom模块中的全局变量:

.text:40311F2A 8B FF                   mov     edi, edi
.text:40311F2C 55                      push    ebp
.text:40311F2D 8B EC                   mov     ebp, esp
.text:40311F2F FF 75 08                push    [ebp+arg_0]
.text:40311F32 68 20 30 31 40          push    offset _Port  ; Com端口信息的结构体
.text:40311F37 E8 D6 FA FF FF          call    _CpPutByte@8  ; CpPutByte(x,x)
.text:40311F3C 5D                      pop     ebp
.text:40311F3D C2 04 00                retn    4

如果Port被修改,那么PCHunter是扫描不到的,那么现在来看看虚拟机里面kdcom!Port这些的值是不是有变化:

1: kd> db kdcom!Port
80bc7020  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
80bc7030  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
80bc7040  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
80bc7050  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
80bc7060  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
80bc7070  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
80bc7080  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
80bc7090  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................

此处明显就不对了。Port的结构体如下:

typedef struct _CPPORT
{
    PUCHAR Address;     // Com端口的基地址
    ULONG  BaudRate;    // 数据发送波特率, 在双机调试设置的时候该值一般被设置为115200
    USHORT Flags;       // Com端口标志位
} CPPORT, *PCPPORT;

在调试已经建立的情况下,_CPPORT的Address字段和BaudRate字段是不可能为0的,那就只能说明一个问题,这些数据已经被破坏了。重启虚拟机不开游戏观察这个数据:

1: kd> db kdcom!Port
80bc7020  f8 02 00 00 00 c2 01 00-04 00 00 00 00 00 00 00  ................
80bc7030  00 00 00 00 00 00 00 00-00 00 00 00 f8 02 00 00  ................
80bc7040  00 c2 01 00 01 00 00 00-00 00 00 00 00 00 00 00  ................
80bc7050  00 00 00 00 00 00 00 00-01 00 00 00 ae 5b bc 80  .............[..
80bc7060  c0 5b bc 80 26 06 c4 d9-d9 f9 3b 26 00 00 00 00  .[..&.....;&....
80bc7070  02 00 00 00 00 c2 01 00-00 00 80 80 01 00 80 80  ................
80bc7080  00 00 00 00 00 00 00 00-00 00 00 00 b0 fe 00 00  ................
80bc7090  00 00 00 00 00 00 00 00-02 00 00 00 02 00 00 00  ................

此时注意到这个变量的确有值了。而且第二个字段的值为0x1c200,十进制正是115200,再次启动游戏继续观察:

1: kd> db kdcom!Port
80bc7020  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
80bc7030  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
80bc7040  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
80bc7050  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
80bc7060  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
80bc7070  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
80bc7080  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
80bc7090  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................

看来的确这里是存在问题了,那么现在就测试一下修改这里到底会不会有什么反应。

测试流程如下:

  1. 修改nt!KdDebuggerEnabled的值为1;
  2. 恢复KiDebugRoutine为nt!KdpTrap,
  3. NOP掉KdEnterDebugger函数中对nt!KdEnteredDebugger的修改代码
  4. eb 80bc7020 f8 02 00 00 00 c2 01 00 04 00, 恢复结构体Port的内容
  5. 尝试触发一个异常;

当修改到第三步,直接就蓝屏了。这说明刚刚的修改里面还存在问题,现在来分析一下蓝屏DUMP,看看到底什么地方出现的蓝屏。

3: kd> k
ChildEBP RetAddr  
WARNING: Frame IP not in any known module. Following frames may be wrong.
8fd6cb20 80bbb828 0x0
8fd6cb70 80bbbab1 kdcom!CpReadLsr+0x18
8fd6cb84 80bbbf1e kdcom!CpGetByte+0x2f
8fd6cb98 80bbb317 kdcom!KdCompPollByte+0x14
8fd6cbb8 84088437 kdcom!KdReceivePacket+0x17
8fd6cbe4 84088384 nt!KdPollBreakIn+0x94
8fd6cbe8 84088361 nt!KdCheckForDebugBreak+0x17
8fd6cc18 84430430 nt!KeUpdateRunTime+0x164
8fd6cc18 93d9f5d6 hal!HalpClockInterruptPn+0x158
8fd6cc98 840a2695 intelppm!C1Halt+0x4
8fd6cd20 8408500d nt!PoIdle+0x538
8fd6cd24 00000000 nt!KiIdleLoop+0xd

经过以上的调用栈分析得知,产生蓝屏的原因是kdcom!CpReadLsr+0x18访问不存在的函数地址(0)导致,那么在IDA里面看一下这里的代码到底是什么。

.text:40311810
.text:40311810 8B FF                  mov     edi, edi
.text:40311812 55                     push    ebp
.text:40311813 8B EC                  mov     ebp, esp
.text:40311815 83 EC 40               sub     esp, 40h
.text:40311818 56                     push    esi
.text:40311819 8B 75 08               mov     esi, [ebp+arg_0]
.text:4031181C 8B 06                  mov     eax, [esi]
.text:4031181E 83 C0 05               add     eax, 5
.text:40311821 50                     push    eax             ; _DWORD
.text:40311822 FF 15 60 30 31 40      call    _KdReadUchar    ; CpReadPortUchar(x) ;此处为蓝屏产生的地方
.text:40311828 8A C8                  mov     cl, al                                  + 0x18
.text:4031182A B0 FF                  mov     al, 0FFh
.text:4031182C 88 4D 0B               mov     byte ptr [ebp+arg_0+3], cl

查看上面的代码得知,当然在调用地址40311822的代码call _KdReadUchar发生了异常,_KdReadUchar的值为零。双击_KdReadUchar它是个啥:

在这里插入图片描述

结果这里发现,KdReadUchar和Port同样处于kdcom的.data段中(.data段是PE文件中用来存放全局变量的地方), 此时DUMP一下kdcom的模块内存看看。

在这里插入图片描述

那么这里就很明了了。TP防护针对kdcom的.data段做了清零操作,因为kdcom的代码比较完善,即便是全部清零了也没有出现错误。那么此时尝试在游戏运行后恢复一下.data段。

注意:里面的函数地址需要针对当前kdcom的实际函数地址进行设置,否则会出错

eb 80b9b000  14 00 00 00 14 00 00 00 01 00 00 00 20 50 52 54 20 4F 56 4C 20 46 52 4D 0A 0D 41 54 0A 0D 00 00 F8 02 00 00 00 C2 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 F8 02 00 00 00 C2 01 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 00 00 AE 9B B9 80 C0 9B B9 80 86 06 5A 83 79 F9 A5 7C 00 00 00 00 02 00 00 00 00 C2 01 00 00 00 80 80 01 00 80 80 00 00 00 00 00 00 00 00 00 00 00 00 B0 00 00 00 00 00 00 00 00 00 00 00 02 00 00 00 02 00 00 00

// 恢复kdcom模块.data段的内容,里面的内容需要根据实际的环境进行变动,不能直接复制。

那么此时再用上一步的流程来测试,额外加上恢复kdcom段的内容:

  1. 修改nt!KdDebuggerEnabled的值为1;
  2. 恢复KiDebugRoutine为nt!KdpTrap,
  3. NOP掉KdEnterDebugger函数中对nt!KdEnteredDebugger的修改代码;
  4. 使用上面的命令进行.data修复;
  5. 尝试触发一个异常(R3程序或者R0程序都可以);
.reload 
; u KdCompPollByte

; CpWritePortUchar
; CpReadPortUchar

eb 80b99000  14 00 00 00 14 00 00 00 01 00 00 00 20 50 52 54 20 4F 56 4C 20 46 52 4D 0A 0D 41 54 0A 0D 00 00 F8 02 00 00 00 C2 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 F8 02 00 00 00 C2 01 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 00 00 AE 7b B9 80 C0 7B B9 80 86 06 5A 83 79 F9 A5 7C 00 00 00 00 02 00 00 00 00 C2 01 00 00 00 80 80 01 00 80 80 00 00 00 00 00 00 00 00 00 00 00 00 B0 00 00 00 00 00 00 00 00 00 00 00 02 00 00 00 02 00 00 00
eb 84345234 90 90 90 90 90 90
ed nt!KiDebugRoutine nt!KdpTrap
eb nt!KdDebuggerNotPresent 0
eb nt!KdDebuggerEnabled 1

本次测试结果:已经可以正常断下了。

在这里插入图片描述

阻止内核调试器分析结语

总体来说,TP保护通过两点来阻止内核调试器:

  1. 监控nt!KdEnteredDebugger的值,该在KdEnterDebugger函数中被修改;
  2. 清零kdcom.dll的.data段来达到kdcom里面的所有功能失效的目的;

不得不说TP防护的开发者,很细心找到这么隐秘的方式来阻止内核调试器的加载。不过还是范围太大了些,这样大范围的去清空一个段的行为很让人发现端倪,如果只是修改了其中某一个小地方的值导致了调试流程的中断,那么估计要找到这样的点估计需要花更多的时间更大的精力才能搞定了。亦或者尝试去改变和内核调试器通信的核心数据,让调试器和内核调试引擎产生冲突而出现错误(猜想)。

额外的测试方式:当确定异常的处理流程时,在流程里一步一步下断点来确定是否正常执行(虽然下了断点会蓝屏,但是可以用来确定执行流程),可能比较麻烦,但这个理论应该有用。

  • 4
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 16
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 16
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值