反反调试思想方法探索
如今,软件安全已经成为了开发软件项目的必备组成部分,反调试则是其中关键的一环,然而,正如矛与盾的对立一样,反反调试与反调试必将永久的并立共存。为了防止软件被调试,现今的软件大多都利用了驱动来检测制止,对于关键的系统函数进行hook(包括各种SSDT hook、inline hook、iat hook等)能有效地遏制进程被打开和读写等,然而,hook是很容易定位和被恢复的,基于没有任何验校检测的hook保护技术就像一面纸墙一般不堪一击。因此,验校检查成为越来越多的反调试代码中不可或缺的一个部分,特别是对于商业性的网络游戏客户端,一旦反调试代码检测到自身的hook地址被修改或者自身的代码验校不一致时,便立刻选择结束游戏进程,甚至蓝屏或重启,以此强硬的对待那些有调试企图的人。检测代码往往无处不在,你很难全部的定位和找到它们,而且它们相互交织检测和代码验校。另一方面,为了对抗硬件断点,在检测调试之前,往往对DR调试寄存器做了相关清除和手脚,并在之后予以恢复,检测代码往往加了VM保护,使人很难弄清程序的流程。
我们知道,在线程切换时会根据是否是同一进程而决定知否切换cr3寄存器,即使切换了cr3,所有进程的内核空间视图是一致的(除了某些特殊页),因此当某一进程通过驱动hook内核函数后,系统所有进程都将改变执行路径,同样当我们恢复了hook之后亦是如此。要是有什么办法能打破这样的规则就好了,当我们的调试器进程运行时执行原始的函数路径,当hook进程运行时执行它自己的hook之后的路径。我们知道,hook技术通常只是修改内核函数的开头几个字节jmp到自己的函数,或者内联修改函数的内部call地址等,不论通过什么形式的hook,一般就是修改一个dword或者几个字节,在已知hook地址和原始字节内容的情况下,恢复hook只需一个mov指令即可,虽然进程切换时并不影响内核地址空间,但是我们也可以在切换时临时修改一些字节。我们的反反调试思想是:在系统从反调试进程切换到其他进程时,恢复原始的hook地址内容,在要切换到反调试进程时,再修改为hook地址。
windows的线程切换散布在内核的各个点上,而且调用形式各不相同,主要函数包括KiSwapThread、KiSwapContext、SwapContext。在线程抢占的情景中,KiDispatchInterrupt直接调用SwapContext完成线程切换;在线程时限用完时,KiQuantumEnd调用KiSwapContext进行切换(KiSwapContext再调用SwapContext完成真正的切换);在线程自愿放弃执行时,则调用KiSwapThread,该函数又调用KiSwapContext完成执行权的转移。在此,我们看到实际完成切换的是核心汇编函数SwapContext。SwapContext也是我们需要处理的函数,在系统线程切换时,我们判断2个线程的进程之一是否含有反调试进程,有的话则进行相关动作,具体是:如果老线程是反调试进程则恢复还原原始hook地址处的内容,如果新线程是反调试进程则还原它自己的原来的hook地址。这里有一个问题,我们是直接在SwapContext函数的开头跳到我们的函数进行以上的判断和恢复吗?我们知道线程切换是系统最频繁调用的函数了,SwapContext本身就是用汇编来写的(为了保证性能),我们的处理是否得当也将直接影响到系统的整体速度,刚才提到的在函数开头进行判断显然不够优雅~在线程切换时,SwapContext会根据是否是同一进程而决定切换cr3寄存器的内容,看一下相关代码:
(代码截自XP sp3)
lkd> x nt!*SwapContext
80546a90 nt!SwapContext = <no type information>
8054696c nt!KiSwapContext = <no type information>
805fcd34 nt!VdmSwapContexts = <no type information>
lkd>uf nt!SwapContext
.
.
.
nt!SwapContext+0x8c:
80546b1c 8b4b40 mov ecx,dword ptr
80546b1f 894104 mov dword ptr ,eax
80546b22 8b6628 mov esp,dword ptr
80546b25 8b4620 mov eax,dword ptr
80546b28 894318 mov dword ptr ,eax
80546b2b fb sti
80546b2c 8b4744 mov eax,dword ptr
80546b2f 3b4644 cmp eax,dword ptr 比较是否是同一进程
80546b32 c6475000 mov byte ptr ,0
80546b36 7440 je nt!SwapContext+0xe8 (80546b78)是同一进程无需切换,直接跳过
nt!SwapContext+0xa8:
80546b38 8b7e44 mov edi,dword ptr 取EPROCESS
80546b3b 8b4b48 mov ecx,dword ptr
80546b3e 314834 xor dword ptr ,ecx
80546b41 314f34 xor dword ptr ,ecx
80546b44 66f74720ffff test word ptr ,0FFFFh
80546b4a 7571 jne nt!SwapContext+0x12d (80546bbd)
nt!SwapContext+0xbc:
80546b4c 33c0 xor eax,eax
nt!SwapContext+0xbe:
80546b4e 0f00d0 lldt ax
80546b51 8d8b40050000 lea ecx,
80546b57 e850afffff call nt!KeReleaseQueuedSpinLockFromDpcLevel (80541aac)
80546b5c 33c0 xor eax,eax
80546b5e 8ee8 mov gs,ax
80546b60 8b4718 mov eax,dword ptr 取cr3也即EPROCESS->DirectoryTableBase
80546b63 8b6b40 mov ebp,dword ptr
80546b66 8b4f30 mov ecx,dword ptr
80546b69 89451c mov dword ptr ,eax
80546b6c 0f22d8 mov cr3,eax 完成切换
80546b6f 66894d66 mov word ptr ,cx
80546b73 eb0e jmp nt!SwapContext+0xf3 (80546b83)
.
.
.
为了不影响性能,我们所要做的只是在不同进程切换时做判断,若是同一进程则无需做任何处理,SwapContext函数内部本身就会做相应的判断,我们为什么不直接利用呢?地址80546b36处的je跳转是同一进程的分支,否则接下来的语句便是不同进程,我们修改80546b38处为跳到我们的函数里并进行判断:
(edi老线程,esi新线程)
cmpdword ptr , 反调试进程_EPROCESS
jmp_恢复hook分支
cmpdword ptr , 反调试进程_EPROCESS
jmp_hook分支
mov edi,dword ptr SwapContext函数内部地址80546b38的原指令
jmp 80546b3b
_恢复hook分支:
cr0去保护位
mov , 原始内容1
mov , 原始内容2
'
'
'
cr0恢复
mov edi,dword ptr
jmp 80546b3b
_hook分支
cr0去保护位
mov , 反调试进程hook函数地址1
mov , 反调试进程hook函数地址2
'
'
'
cr0恢复
mov edi,dword ptr
jmp 80546b3b
其中hook地址和值是在驱动中定位并收集好的
如果你觉得上面的方法还是不够优雅的话,下面我就再来介绍一种相对而言稍微优雅的方法。
我们知道,windows的内存寻址是通过三级的页目录,页表来映射的,每个进程都有独立的页表,且进程的系统空间视图是共享相同的页目录的。这一次,我们就来对反调试进程的页表做相应的手脚~我们的思想方法是:修改反调试进程的页表项,让其hook代码的页面为一私有页,这样,反调试进程与其他进程将拥有不同内核代码页,其检测机制便荡然无存了。然后,再在我们的进程里恢复hook地址(当然也可以在反调试进程创建后,加载驱动前修改,这样就不用恢复了~)。
windows内核映象是如何映射的?我们来看一下:
lkd> lm
start end module name
804d8000 806e5000 nt (pdb symbols) c:\symbols\ntkrpamp.pdb\7D6290E03E32455BB0E035E38816124F1\ntkrpamp.pdb
806e5000 80705d00 hal (pdb symbols) c:\symbols\halmacpi.pdb\9875FD697ECA4BBB8A475825F6BF885E1\halmacpi.pdb
a32db000 a331ba80 HTTP (pdb symbols) c:\symbols\http.pdb\B5A46191250E412D80E9D9E9DDA2F4DA1\http.pdb
a3610000 a3613d80 vstor2_ws60 (no symbols)
a3614000 a3665c00 srv (pdb symbols) c:\symbols\srv.pdb\069184FEBE104BFDA9E51021B9B472D92\srv.pdb
a368e000 a375ce00 vmx86 (no symbols)
a3785000 a37b1180 mrxdav (pdb symbols) c:\symbols\mrxdav.pdb\EDD7D9E6E63B43DBA5059A72CE89286E1\mrxdav.pdb
a3a46000 a3a5a480 wdmaud (pdb symbols) c:\symbols\wdmaud.pdb\D3271BFD135D4C2B9B1EEED4E26003E22\wdmaud.pdb
a3ae3000 a3af2a00 vmci (export symbols) \??\C:\WINDOWS\system32\Drivers\vmci.sys
a3b27000 a3b2ae80 DbgMsg (no symbols)
a3cb3000 a3cc8880 irda (no symbols)
'
'
'
lkd> !pte 804d8000
VA 804d8000
PDE at 00000000C0602010 PTE at 00000000C04026C0
contains 00000000004009E3contains 0000000000000000
pfn 400 -GLDA--KWEV LARGE PAGE pfn 4d8
其中L是指使用大页面来映射的,这表明内核的代码和数据是在一页(4m或pae下2m的大页)中,而我们要修改的只是代码页,数据页必须映射到相同物理页以维持系统的一致性。因此,我们在反调试进程中,为内核映象对应的PDE申请相应的页表,在页表中,我们将原内核映象的数据页对应的pte设置为相同的pfn,而代码页设置为我们私有页,事实上,代码页中也无需全部私有,只需要把hook函数所在的页面改为私有pfn即可,其他页面可仍为原始pfn,从而避免不必要的内存浪费。然后我们恢复hook,结果反调试进程和其他进程会拥有不同内核函数的执行路径了,反调试保护也随之为我们突破~
仔细看看,经过上面的处理真的就可以了吗?答案当然是否定的。看看上面的 !pte 804d8000命令的结果-GLDA--KWEV,其中G表示全局页,全局页标志是为了提升系统性能,因为内核地址空间是共用的,所以cpu在冲刷内部TLB时,只是冲走了没有G标志的TLB项,当然,这并不是说全局页就永远不会消失,TLB缓存项是有限的,cpu会以FIFO规则替换所有的TLB项。可能有人感到奇怪,在SwapContext函数中并没有显示的冲刷TLB的指令,这是因为:如果是同一进程中,则无需冲刷;如果是不同进程,那么在更改cr3的同时,已经隐式的执行了冲刷命令。我们的目的是在切换到反调试进程时,冲刷掉全局页,使其使用自己的私有页。那么如何做到呢?cpu内部的cr4寄存器中位7是PGE(Page Global Enable)位,为1时启用全局页功能,为0是禁止。当全局页禁用时,冲刷TLB的话则全部TLB项都会无效。所以我们上面说的修改反调试进程的pde及pte中都不得含有G标志,我们在SwapContext非同一进程的分支做如下处理:
cmp dword ptr , 反调试进程_EPROCESS
mov edi,dword ptr 执行80546b38原始指令
jne 80546b3b 不是,直接跳回
mov eax , cr4 eax内容无需保存,见代码即知
push eax 保存cr4内容
and eax , ~(1 << CR4_PGE) 去PGE位
mov cr4 , eax
mov cr3 , _反调试进程cr3值 冲刷所有TLB项
pop cr4 恢复cr4
jmp 80546b3b
这样,在反调试进程自身上下文中任何检测都将无效,因为我们根本不会碰它的任何代码逻辑,当然,上面的代码无法突破一些在任意上下文中运行代码的检测机制,比如dpctimer,workitem,Watchdog Timers以及System Threads,然而这些机制其实很可以很容易的突破,比如枚举查找系统的dpc定时器并删除是很简单的,系统线程也很容易被停掉。
再将思维发散一下,驱动在改变一个内核函数的路径时必定先要获得该函数的地址,或者一个相对的基准函数,无论其是通过静态IAT导入函数,还是手工IAT搜索,还是动态MmGetSystemRoutineAddress,还是read内核文件,我们在之前做相关手脚,在其获取函数时提供给他一个虚假地址,当然,这是原函数的一个副本,以便他能找到内部相应的hook地址。好的,让他hook修改然后检测去吧~~
以上只是本人的拙劣想法和见解而已,希望它对你有用~
80546b36 7440 je nt!SwapContext+0xe8 (80546b78)是同一进程无需切换,直接跳过
nt!SwapContext+0xa8:
80546b38 8b7e44 mov edi,dword ptr 取EPROCESS
应该是在在80546b36这个地址上写 E9xx xx xx xx 写跳转,跳到我们的函数里面吧,还有就是cmpdword ptr , 反调试进程_EPROCESS
jmp_恢复hook分支
cmpdword ptr , 反调试进程_EPROCESS
jmp_hook分支
mov edi,dword ptr SwapContext函数内部地址80546b38的原指令
jmp 80546b3b
上面2个jmp应该是写成 JE 才对吧。
页:
[1]