首页
社区
课程
招聘
[旧帖] [原创]HYING 调试经历 0.00雪花
发表于: 2008-12-5 20:42 4629

[旧帖] [原创]HYING 调试经历 0.00雪花

2008-12-5 20:42
4629
HYING 调试经历
    本人初学脱壳,水平一直停留在脱压缩壳的阶段,前段时间心血来潮,想调试一下HYING的壳,于是到看雪上下载了PE-Armor0.765,默认属性加壳,大概调试了2个星期,终于到达了OEP(严格地来说,我只是看到了OEP-_-!),本想继续把壳脱掉,但由于近日虚拟机崩溃,所有快照(包括脱壳的)都被毁坏,也没有心思再往下研究了,在此我把自己在调试HYING壳过程中遇到的一些问题和调试经历拿出来和大家分享(高手看了别笑...)。由于是经历,所以我没有列出过多的代码,而是尽量地用文字来描述,也力求简单易懂。在这里,我得感谢前辈阿鑫的热心指导,让我在很多迷茫的时候知道了前进的方向。
那么下面我们开始了。首先我还是依照以前调试压缩壳的经验,OD加载后一直F7,压缩壳可以通过寻找一些关键的指令,比如UPX的popad,跨段跳转来实现快速到达OEP,同时还有ESP定律,2次内存断点法等等,但这些在HYING面前都没有效果,ESP定律中会用到硬件断点,但是HYING壳中会用硬件断点解码,让硬断失效。OD内存断点原理是页面异常,而HYING壳正是利用异常的高手。一开始是大段的花指令(怀疑壳里大多数指令都是花指令),但是注意每两段花指令中间一般都会有一条正常的指令,后面的花指令都遵守此规则(刚开始所谓的正常指令也就是解码指令了)。
004365BD    8D340A          lea     esi, dword ptr [edx+ecx]         
004365C0    8128 0C83B95E   sub     dword ptr [eax], 5EB9830C        // EAX地址解码
004365C6    8D92 7243FA44   lea     edx, dword ptr [edx+44FA4372]
004365CC    BA 2DF1FE02     mov     edx, 2FEF12D
004365D1    83E8 FC         sub     eax, -4                          // 改变EAX的值
004365D4    8D92 CCCC8D27   lea     edx, dword ptr [edx+278DCCCC]    // 典型的无用指令
004365DA    81C5 A4D5DD28   add     ebp, 28DDD5A4
004365E0    8D3C4B          lea     edi, dword ptr [ebx+ecx*2]
004365E3    8128 BE3E6721   sub     dword ptr [eax], 21673EBE       // EAX地址解码       
004365E9    8D3C4B          lea     edi, dword ptr [ebx+ecx*2]

这个地方我一直F7,很快程序通过VirtualAlloc申请了一片空间,并很快地跳到了上面来执行。从这里开始,壳大量地使用了SEH。比如:
003B0000    E8 13000000        call    003B0018                   // 将3B0005压栈
003B0005    8B4C24 0C          mov     ecx, dword ptr [esp+C]
003B0009    FF81 B8000000      inc     dword ptr [ecx+B8]
003B000F    FF81 B8000000      inc     dword ptr [ecx+B8]
003B0015    33C0               xor     eax, eax
003B0017    C3                 retn
// 这两句是经典的注册异常处理函数的代码
003B0018    64:FF35 00000000   push    dword ptr fs:[0]                  
003B001F    64:8925 00000000   mov     dword ptr fs:[0], esp      
// 这样3B0005就成为了异常处理函数
                ......                  ......
// 触发异常,程序将跳转到异常处理函数
003B0033    CD EB              int     0EB                        
WINDOWS的异常处理链是放在栈上的,由fs:[0]指向的位置代表了异常处理的链头:
0012FF94   0012FFE0  指向下一个 SEH 记录的指针
0012FF98   003B0005  SE处理程序
      ......      ......
0012FFE0   FFFFFFFF  SEH 链尾部
0012FFE4   7C839AC0  SE处理程序
比如这个SEH链,当发生异常时,WINDOWS会先调用003B0005进行处理,如果该处理函数无法处理异常那么西一个处理函数7C839AC0就会被调用,当所用处理函数都不能处理时应用程序被关闭。那么当程序进入了异常处理函数的时候可以做什么事呢?
003B0005    8B4C24 0C          mov     ecx, dword ptr [esp+C]       
003B0009    FF81 B8000000      inc     dword ptr [ecx+B8]
003B000F    FF81 B8000000      inc     dword ptr [ecx+B8]
003B0015    33C0               xor     eax, eax
003B0017    C3                 retn
看上面的代码mov     ecx, dword ptr [esp+C],在刚进入异常处理函数的时候[esp+c]的值保存了线程在发生异常之前的各个寄存器的状态,其实就是CONTEXT这个结构体,可以在VC里查到,(另外[esp+4]存放了_EXCEPTION_RECORD结构体)。CONTEXT结构体的B8位是发生异常之前的EIP,这段代码把这个EIP加了2,于是异常处理完成返回程序的时候,系统会从栈上取出CONTEXT,用于将线程恢复到发生异常之前的状态,这样EIP的值就增加了2,这样的改变可以反跟踪,因为异常处理成功后返回是靠着系统提供的ZwContinue来实现的,而ZwContinue又是调用了SSDT下的NtContinue,OD这种RING3级的调试器是没有办法跟踪过去的。注意后面的xor eax, eax这条指令将返回值置为0,这是表示异常处理成功,如果把这个返回值改成1,表示异常函数处理失败,那么下一个异常函数7C839AC0就会被调用。继续向下跟踪发现以下代码:
003B0085    9D                 pushfd
003B007E    810C24 00010000    or      dword ptr [esp], 100
003B0085    9D                 popfd
003B0086    90                 nop
003B0087    EB F4              jmp     short 003B007D    // 这个跳转将导致反调试成功

pushfd是标志寄存器EFL压栈,popfd是EFL出栈,这段代码也是一个反调试用的,这段代码其实就是将TF标志位置1以引发异常,当OD单步运行到这里的时候,估计因为OD会以为TF被置一是因为调试器的单步执行程序,而不将TF的改变传给程序,这里还会看到十分有趣的现象,明明[esp]的值是346,当popfd后,EFL寄存器的值变成了246。不单步运行就不会有这种问题,还有一个解决办法就是将调试选项里的“使用硬件断点单步执行或跟踪代码”去掉即可,当然也可以在执行完popfd指令的时候将TF手动置1。这样程序在运行到了nop指令的时候就会进入SEH。(在后面还有一个类似的代码,没有了or  dword ptr [esp], 100,但作用是反的,可能与压栈有关,总之,其实这里不太清楚,期待高手现身中...)
下面是异常处理函数的一部分:
003B004A    8B4424 04          mov     eax, dword ptr [esp+4]    // 刚才所说的_EXCEPTION_RECORD结构体
003B004E    8B00               mov     eax, dword ptr [eax]
003B0050    3D 04000080        cmp     eax, 80000004             // 检查异常类型
可以看出这里是检查异常的类型的代码,_EXCEPTION_RECORD的第一个成员就是异常类型,80000004表示单步异常,看来壳不仅要触发异常还要检查异常的类型是否正确-_-!  
下面又是一大堆的SEH和花指令,不过这里我还是遇到了一个小问题,我在异常返回的点上下断点,当异常返回的时候TF会被置1,我想应该是因为我是一直F7进入异常的,单步执行导致在异常触发保存现场的时候TF被当成了1,于是异常处理完返回的时候,TF也就变成了1。
在经历了好长一段SEH的折磨后,终于看到了有意义的API:VirtualAlloc。其实后面想了一下,从最开始到这里只需要在VirtualAlloc下断点,断两次之后就到这里了,但是如果没有前面的调试经历,又怎么会知道呢?:)
从下面开始,壳就开始设置硬件断点了(对,是壳!)。设置硬件断点的方法还是利用SEH。大家都知道硬件断点的原理是利用INTER提供的调试寄存器(Dr0-Dr7)实现的,Dr0~Dr3存放设置断点的地址(所以硬件断点只能设置4个),Dr4和Dr5保留(有地方这两个寄存器根本就没有),Dr6保存了调试状态,Dr7是调试控制寄存器,之前我们看到的CONTEXT结构体中的+4,+8,+C,+10位分别代表了Dr0-Dr3,下面的指令就开始改变调试寄存器了:
(注:在此之前ECX指向CONTEXT结构体地址)
003B3A7F    8B81 B4000000      mov     eax, dword ptr [ecx+B4]        // 将CONTEXT的B4位(发生异常时候EBP的值)放入EAX
003B3AB2    8D80 D6354000      lea     eax, dword ptr [eax+4035D6]   
003B3AE5    8941 04            mov     dword ptr [ecx+4], eax         // 设置DR0寄存器
最后的这一句将CONTEXT的04位设置成了EAX,这样当异常处理程序返回后,Dr0寄存器就会被设置成刚才EAX的值。当然,这是在没有调试器的情况下,在OD中就不一样了,OD有时候会自动将调试寄存器清零(特别是在遇到异常的时候),这样当程序运行到这些原本被设置了硬件断点的地方就不会有所反映,而正确的流程应该是程序在到达这些位置的时候触发异常进入SEH。不过没关系,既然发现了那就有办法解决,我用的方法是把壳设置硬件断点的地方手动改成int3来触发异常(注意在调试设置中要把int3异常忽略传给应用程序)。这样改的话,后面有个地方也需要改改,就是异常类型,壳在后面会检查本次异常的类型,int3异常的类型是80000003,硬件断点触发的异常是80000004(单步异常,至于为什么是这个,我也不知道),改跳转或者改数值均可,当然这个方法其实比较笨,据说有专门PATCH OD这个BUG的补丁。壳在这里一共设置了4个硬件断点,如果之前我们用OD设置过硬件断点的话,在这里将被清洗掉。在四个硬件断点异常都被触发了之后,后面的部分代码也被解码了出来(这里不要误会,解出来的仍然是壳代码)。
很快便进入了非常精彩的Drx调试寄存器解码的部分,首先程序在上一次的异常中把4个调试寄存器的值设置为定值(dr0=0fff0123,dr1=0fff4567,dr2=0fff89ab,dr3=0fffcdef),然后每次从目标地址取出一个字节,进入异常处理函数中对这个字节进行解码,异常处理结束后,将解码后的字节放回,当然每个解码循环结束后调试寄存器的值是会改变的。这样的话OD对Drx稍微的改变就会引起解码的错误。这里我采用的方法是比较笨的方法:选择四个不用的内存地址来代替Dr0-Dr3(我选用的是12fff0--12ffff),将解码的代码提出来,重写代码,把代码中的dr0-dr3用自己的四的寄存器代替(幸好HYING给了大量的花指令,让我们可以写更大体积的代码...),将异常处理函数中的异常类型比较的代码改一改,然后运行就可以了:)。这样的好处是硬件断点也可以解放出来。
解码的部分是个循环,大概是这样的:(快照没有了,只好凭印象写)
nop
mov al, [esi-ecx]
int3   // 触发异常,利用异常处理函数解码
...... // 这中间有一些操作,实际是混淆视听的,以为在解码
mov [esi-ecx], al
loop
其实这里过了之后,后面的部分都不那么麻烦了,一个古老的通过CreateFileA来检查调试器的方法,检查诸如"\\.\NTICE","\\.\SICE"一类的字符串,当然这些字符串都是动态解密的,要用的时候解密,用完了马上又加密,以防在内存中被查到,通过简单的not指令实现。不过这里并没有针对OD的检查。
下面一个值得一提的地方是API代码抽取,壳会针对一些常用的API,如GetModuleHandleA,VirtualAlloc等,将它们头部代码先做变形,然后提到壳代码段执行,这样可以从中间进入API,有效防止了API头部的断点。比如GetModuleFileNameA头部代码:
mov edi, edi
push ebp
mov ebp, esp
sub esp, 10
push edi
这些代码会被变形成下面形式,放在壳里执行:
push edi
pop edi
push ebp
push esp
pop ebp
push esp
sub [esp], 10
pop esp
push edi
当然在API中间下断点还是有效果的:)
最后是通过GetFileSize,CreateFileMapping,MapViewofFile等API进行文件校验,主要是通过文件的大小看文件是否已经脱壳,其实调试到这里代码段已经被解密出来了(我试了一下,这个时候忽略所有异常,已经可以正确执行到OEP了,因为被加壳的文件是自己写的,所以知道OEP的位置....汗!)。到这里应该已经离OEP很近了,但是由于虚拟机崩溃,我也就没有再继续调试下去了....

末了,以上就是我第一次调试加密壳的经历,感觉调试壳最需要的还是耐心,就像某位前辈对我讲话“只要有足够的时间,任何一个壳都是可以解的”。最后希望我的调试经历能给和我一样努力着的菜鸟一些帮助和启示,也欢迎大家的批评和指教。

[培训]科锐逆向工程师培训第53期2025年7月8日开班!

收藏
免费 0
支持
分享
最新回复 (7)
雪    币: 1564
活跃值: (3567)
能力值: ( LV13,RANK:420 )
在线值:
发帖
回帖
粉丝
2
单行第一个顶
2008-12-5 20:57
0
雪    币: 192
活跃值: (70)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
3
鼓励一下 多多研究
2008-12-5 20:58
0
雪    币: 200
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
顶起~~
忽忽
2008-12-5 20:59
0
雪    币: 315
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
5
嗯,第三是我~~,值得鼓励
2008-12-5 20:59
0
雪    币: 1564
活跃值: (3567)
能力值: ( LV13,RANK:420 )
在线值:
发帖
回帖
粉丝
6
楼上的,你是第5吧
2008-12-5 21:00
0
雪    币: 4
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
7
慢慢看。很高深啊
2009-11-19 12:30
0
雪    币: 35
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
8
写的不错~~又学习一招~~
2010-9-13 14:38
0
游客
登录 | 注册 方可回帖
返回