首页
社区
课程
招聘
[原创]反反调试思想方法探索
发表于: 2011-1-8 13:11 23366

[原创]反反调试思想方法探索

2011-1-8 13:11
23366

如今,软件安全已经成为了开发软件项目的必备组成部分,反调试则是其中关键的一环,然而,正如矛与盾的对立一样,反反调试与反调试必将永久的并立共存。为了防止软件被调试,现今的软件大多都利用了驱动来检测制止,对于关键的系统函数进行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 [ebx+40h]
80546b1f 894104          mov     dword ptr [ecx+4],eax
80546b22 8b6628          mov     esp,dword ptr [esi+28h]
80546b25 8b4620          mov     eax,dword ptr [esi+20h]
80546b28 894318          mov     dword ptr [ebx+18h],eax
80546b2b fb              sti
80546b2c 8b4744          mov     eax,dword ptr [edi+44h]
80546b2f 3b4644          cmp     eax,dword ptr [esi+44h]         比较是否是同一进程
80546b32 c6475000        mov     byte ptr [edi+50h],0
80546b36 7440            je      nt!SwapContext+0xe8 (80546b78)  是同一进程无需切换,直接跳过

nt!SwapContext+0xa8:
80546b38 8b7e44          mov     edi,dword ptr [esi+44h]         取EPROCESS
80546b3b 8b4b48          mov     ecx,dword ptr [ebx+48h]     
80546b3e 314834          xor     dword ptr [eax+34h],ecx
80546b41 314f34          xor     dword ptr [edi+34h],ecx
80546b44 66f74720ffff    test    word ptr [edi+20h],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,[ebx+540h]
80546b57 e850afffff      call    nt!KeReleaseQueuedSpinLockFromDpcLevel (80541aac)
80546b5c 33c0            xor     eax,eax
80546b5e 8ee8            mov     gs,ax
80546b60 8b4718          mov     eax,dword ptr [edi+18h]         取cr3也即EPROCESS->DirectoryTableBase

80546b63 8b6b40          mov     ebp,dword ptr [ebx+40h]
80546b66 8b4f30          mov     ecx,dword ptr [edi+30h]
80546b69 89451c          mov     dword ptr [ebp+1Ch],eax
80546b6c 0f22d8          mov     cr3,eax                         完成切换
80546b6f 66894d66        mov     word ptr [ebp+66h],cx
80546b73 eb0e            jmp     nt!SwapContext+0xf3 (80546b83)
.
.
.

    为了不影响性能,我们所要做的只是在不同进程切换时做判断,若是同一进程则无需做任何处理,SwapContext函数内部本身就会做相应的判断,我们为什么不直接利用呢?地址80546b36处的je跳转是同一进程的分支,否则接下来的语句便是不同进程,我们修改80546b38处为跳到我们的函数里并进行判断:
(edi老线程,esi新线程)

cmp  dword ptr [edi+44h] , 反调试进程_EPROCESS                 
jmp  _恢复hook分支
cmp  dword ptr [esi+44h] , 反调试进程_EPROCESS  
jmp  _hook分支
mov     edi,dword ptr [esi+44h]      SwapContext函数内部地址80546b38的原指令   
jmp     80546b3b 

_恢复hook分支:
cr0去保护位
mov   [_hook地址1], 原始内容1 
mov   [_hook地址2], 原始内容2
'
'
'
cr0恢复
mov     edi,dword ptr [esi+44h]     
jmp     80546b3b

_hook分支
cr0去保护位
mov   [_hook地址1], 反调试进程hook函数地址1
mov   [_hook地址2], 反调试进程hook函数地址2
'
'
'
cr0恢复
mov     edi,dword ptr [esi+44h]     
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 00000000004009E3  contains 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 [esi+44h] , 反调试进程_EPROCESS
mov     edi,dword ptr [esi+44h]                     执行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修改然后检测去吧~~

   以上只是本人的拙劣想法和见解而已,希望它对你有用~

特别感谢:CUBIE(酷毙哥) De_l_ta   hopy  坚持到底  keenjoy95  老衲 盟主  紫色秋枫  (无先后字母顺序)  曾经给予我的帮助!

  待业求职中...实习、打杂都行,只要管饭。


[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!

收藏
免费 7
支持
分享
最新回复 (22)
雪    币: 601
活跃值: (256)
能力值: ( LV11,RANK:190 )
在线值:
发帖
回帖
粉丝
2
驻足留名,顶了
2011-1-8 13:40
0
雪    币: 239
活跃值: (25)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
3
学习了~~~
2011-1-8 14:08
0
雪    币: 2105
活跃值: (424)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
4
写的不错。。。。很有启发
2011-1-8 16:23
0
雪    币: 154
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
5
强帖,膜拜
2011-1-8 16:39
0
雪    币: 7651
活跃值: (523)
能力值: ( LV9,RANK:610 )
在线值:
发帖
回帖
粉丝
6
支持一下楼主~~
2011-1-9 13:39
0
雪    币: 305
活跃值: (57)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
7
这个必须顶。。。我都想弄到我论文里去唬老头们了~
2011-1-9 13:53
0
雪    币: 197
活跃值: (82)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
8
顶,好厉害。。。。。。
2011-1-9 14:29
0
雪    币: 535
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
9
支持一下,希望还有后续
2011-1-9 14:42
0
雪    币: 53
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
10
顶,很给力,加油。
2011-1-9 17:36
0
雪    币: 331
活跃值: (57)
能力值: ( LV7,RANK:100 )
在线值:
发帖
回帖
粉丝
11
不错,前提是必须有一个反调试进程。。。
2011-1-10 10:55
0
雪    币: 123
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
12
其实hook swapcontext这个方法在两年前就以前思考过了,当你思考得更细节时,你会发现很多问题,
现在大部分检测都会在被保护程序自身进程空间进行,
比如你对他下一个断点,而当这个断点被触发时,是在该进程空间中,此时你的swap hook会让他所有的内核代码hook都仍然存在,想想吧,接下来会发生什么?呵呵
以上言论纯属技术探讨,无其他意思
2011-1-10 18:36
0
雪    币: 507
活跃值: (26)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
13
首先感谢楼上的指点交流,不过你的回复我咋有点看的不是很明白?我通篇说的就是过在进程上下文反调试,他hook存在当然就不会检测到被恢复了,不是么?  想请ls的再详细说下
另外,这篇文章现在可能还有点问题。有很多其他的方面没考虑到,但我相信,只要对原理的细腻的理解是终究是可以做到的
2011-1-11 11:52
0
雪    币: 123
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
14
就像之前我说的,目前大部分的保护方式都是,自身进程执行保护自己的代码.
而在切换到该进程时,所有的保护都会被你的程序恢复过来,就会有一些事情发生,

比如:
当程序通过hook IDT的中断处理程序来保护自己不被调试时,
一旦进程中触发了断点,转到中断处理程序里的时候,仍然是在该进程环境中,
由于你的hook swap忽悠反调试程序的方法,这时的IDT表是被该进程执行HOOK后的表,
那就会进入到保护程序HOOK后的中断处理程序中,至此,反反调试失败...
其他部分情况类似.
2011-1-11 22:25
0
雪    币: 507
活跃值: (26)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
15
回楼上,你说的没错
当然,恢复与不恢复主要还是看内核例程起什么作用和功能的,并非一成不变的全盘恢复,我所说的是对于大多数的在调试进程中需要用到的内核例程,楼主所说的是在被调试进程调试事件发送给调试器进程之前的反调试手段,希望和楼上一起探讨下这个问题~
2011-1-11 23:14
0
雪    币: 53
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
16
不错,学习中。
2011-1-12 17:38
0
雪    币: 1140
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
17
没能看懂,不过正在学习中,以后再加入此类问题的讨论!
2011-1-13 19:38
0
雪    币: 121
活跃值: (22)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
18
学习学习 相当给力呀~
2011-1-13 20:42
0
雪    币: 208
活跃值: (40)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
19
我离内核为什么这么远。
遥远的ring0啊
2011-1-13 21:30
0
雪    币: 322
活跃值: (113)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
20
好文章,留记号
2011-1-19 22:50
0
雪    币: 189
活跃值: (16)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
21
好文章,顶一个
2011-1-19 23:18
0
雪    币: 615
活跃值: (580)
能力值: ( LV4,RANK:40 )
在线值:
发帖
回帖
粉丝
22
内核中玩,,,  very good
2011-12-13 23:33
0
雪    币: 6
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
23
标记
2017-10-3 23:20
0
游客
登录 | 注册 方可回帖
返回
//