<0>
------------------------------------
SF3差不多是SF系列中最强的版本,有相当多的强悍ANTI,其中VM修复是最困难的
关于SF3 unpack的文章比较少,仅有的一些文章也很少提到VM修复,只有reload的sf3逆向工具(RLD_SFRT)给出VM的修复方案,不过似乎只是针对SF3早期版本,并不试用SF3.4 3.7等
这篇文章我将描述脱壳一个SF3.4游戏的思路和方法,不是很完美,但确实可以工作,其中VM没有还原成X86代码,只是拼装到dump文件中运行,以下提到SF3,都是指SF3.4,和之后的版本可能有些细节不同
<1>anti-debugger及调试环境的建立
------------------------------------
尝试调试SF3的程序会发现单步和CC都没用,一旦试图中断程序就会蓝屏(BSOD),原因在于SF3修改了IDT,同时HOOK WINDOWS的进程切换,在SF保护的程序中查看IDT,会发现1号和3号中断被替换了,而在其他进程中IDT是正常的
通常调试器总是依赖x86的调试机制,即INT1用于单步和硬件断点,INT3用于软件断点,
softice,trw,windbg这些ring0调试器都是在int1int3 handler里处理调试断点,基于debugAPI的ring3调试器也是间接通过windows的interupter handler来实现调试,因此在SF保护的进程里都无法工作
如果调试器强行把IDT改回去,也会出现问题:
1,SF会检测int1 int3的interupter handler是否被修改,发现修改就BSOD
2,SF通过INT 1指令更新VM并将VM切换到ring0下运行,如果改了SF的int1 handler,VM也没法正确运行了
一个可行的调试方案就是使用不依赖int1 int3的断点,调试器最主要的功能就是让目标程序在某行代码处停下来,记录、修改,并能恢复运行,用int1 int3做断点是因为这是控制转移到debugger的最好的方法,但实际上还有很多控制转移的方法,比如jmp或者int XX,都可以用来做断点。当然,没有INT3完美,因为JMP是5字节,INT XX 2字节,而INT3只有一个字节,但这并不是太大的问题。
用INT XX(2byte)做断点的话,大多数代码都可以正常中断,少数不能的情况比如:
00410070 74 01 je short loaddll.00410073
00410072 50 push eax
00410073 53 push ebx
不能在00410072下断点,因为如果下了断点,而je跳转的话就会出错,但这种情况单步可以运行(单步时能正确预测下条指令)
另一种情况是单步也不能运行:
00410072 50 push eax
00410073 EB FD jmp short loaddll.00410072
当执行到00410073时不能对00410072下断点
不过这些并不常见,绝大多数的情况都可以中断,只要记住断点是2bytes就行
用INTXX做断点就可以调试SF保护的程序,包括SF和windows的int1 interupter handler也能调试
也可以调试其他很多壳,原则上,只要CR3 GDT IDT还有效,并且没有内存效验,就可以调试
rdtsc的anti也可以对付,只要在中断前保存msr_tsc,中断返回后恢复msr_tsc即可(可惜我的CPU有点问题无法恢复msr...)
<2>除VM之外的部分处理
-----------------------------------
DUMP:
----------
在OEP处DUMP很容易,就我分析的这个程序来说,VC6入口第一个API是GetVersion,在GetVersion处中断,DUMP并修正entrypoint即可
中断不一定非要用调试器,让程序在GetVersion处非法也行,因为SF只改INT1 INT3,没改其他中断,所以在GetVersion入口加条指令读无效内存,或者除零,都可以非法操作断下来,或者甚至还可以加个MessageBox或者直接jmp cur_addr,方法很多,只要让它停下来就行
修复IAT:
----------
importrec修复IAT的原理:
如果壳重定向了IAT中的API thunk,importrec HOOK所有API,然后去call重定向的thunk,如果全部HOOK中发现哪个API被调用说明此thunk就是这个API
SF可以anti importrec,原因:
1,importrec的HOOK通过异常进行,直接导致被SF发现并BSOD,在SF保护的进程里是不能随便异常的
2,importrec HOOK时,壳代码已结束,已经正式转到主程序,对于模拟方式加密IAT无效,因为这时壳早已读取API代码并模拟了
3,HOOK后API识别受到干扰,比如壳加密GetTickCount,在调用GetTickCount前先调用了Sleep来干扰,这时importrec会识别为Sleep
修复的方法:
修改API第一行指令为JMP(解决1),并且要在壳尚未开始加密IAT前就修改并HOOK,这样等壳模拟API的代码时会把JMP的HOOK代码也模拟进去(解决2),然后call IAT中的thunk来探测API,call的时候使用“密码参数”,就是第一个参数为密码值0x12345678,HOOK代码发现以此参数调用就识别IAT并返回,否则就跳到正常API,这样不会被壳干扰到,比如壳调用Sleep干扰GetTickCount,但它用的参数不是0x12345678,忽略(解决3)
这个方法可以自动修复SF所有版本的IAT,也可以修复其他的壳
事实上IAT本来就不可能很好的加密,除非那个加密的IAT是用户DLL。如果是系统DLL,壳最终总要去执行这个API,原则上HOOK总是能搞定问题(这相当于API执行时报告它自己)关键HOOK的时机要比壳早(比如CreateProcess(CREATE_SUSPENDED)后),这样不管壳怎么干扰怎么变形怎么COPY都没用,就是变了形一样还是会报告THUNK&API
那么如果壳不去读取系统DLL中的代码就用自己的代码来模拟API又怎么办?的确,这样的话这个方法就无效了。但这种加密并不会成为问题。因为壳的作者既然有信心用自己的代码来代替API,说明这个API没有操作系统间的移植问题,直接将这些代码COPY到脱壳的文件里就行了,等于没加密,而且这种函数也非常少
段区修复:
----------
SF会申请一些内存,里面包括有当前路径之类的东西,补段区或者用loader载入到脱壳后程序中就行
<3>VM的一般分析
-----------------------------------
SF除了VM之外的部分都没多少难度,主要的困难在于修复VM
SF调用VM时代码类似于:
004221E2->E8 03FFFFFF CALL 004220EA
004221E7 59 POP ECX
004221E8 C3 RETN
->
004220EA - E9 6B222C03 jmp 036E435A
->
036E435A 68 C2A9F03C push 3CF0A9C2 //VM的ID,SF通过这个来识别是哪次VM调用
036E435F - E9 9C4CBFFE jmp 022D9000 //进入VM
VM入口:
022D9000 /EB 14 jmp short 022D9016
...
022D9016 \60 pushad
022D9017 9C pushfd //保存当前状态
022D9018 FC cld
022D9019 E8 00000000 call 022D901E
022D901E 5E pop esi
...
022D9077 015F 1C add dword ptr ds:[edi+1C],ebx
022D907A 03CB add ecx,ebx
022D907C FFE1 jmp ecx
-->
027264CC 0FBA6F 24 02 bts dword ptr ds:[edi+24],2
027264D1 8B0F mov ecx,dword ptr ds:[edi]
...
0272653C 895F 14 mov dword ptr ds:[edi+14],ebx
0272653F 8B5F 20 mov ebx,dword ptr ds:[edi+20]
02726542 FFE3 jmp ebx
到了jmp ebx处VM REM就设置好了,可以开始正式运行VM OPCODE,jmp ebx跳转到OPCODE的第一个“解释器”,每个解释器运行时首先读取opcode并解密,然后执行一些操作,最后跳到下一个解释器
在整个运行过程中EDI指向VM REM
VM REM前几个DWORD的意义如下:
EDI-->
-----------------
0013E1B0 0013E5B0 022D9000 00000026
rem_start rem_end vm_base des_base
00000031 00336E94 00000000 0234D788
src_base vm_ip intprter_tbl
02306550 000000C4 00000202 002DBAE0
A B vm_eflags C
-----------------
des_base的作用是,OPCODE中的目标寄存器索引总是和des_base相加后再索引。
src_base类似,和OPCODE中源寄存器相加
vm_ip是OPCODE buffer的offset,和vm_base相加后是线性内存地址
intprter_tbl是伪指令解释器入口offset表,此表中有500个offset,下文中把此值(0-499)称为解释器的id
ABC意义不大清楚
A好象保存一个临时解释器入口
B和解密下一个解释器id有关,1byte
C保存一个临时vm_ip,跳转时使用
下面举一些解释器的例子
VM入口第一个解释器
head:
02306550 8B47 08 mov eax,dword ptr ds:[edi+8] //eax=vm_base
02306553 0347 14 add eax,dword ptr ds:[edi+14] //eax=vm_base+vm_ip
02306556 8B08 mov ecx,dword ptr ds:[eax] //从opcode buffer取一个dword
02306558 8B50 04 mov edx,dword ptr ds:[eax+4] //取第2个dword
0230655B 8347 14 08 add dword ptr ds:[edi+14],8 //这个opcode的长度是8bytes
0230655F 51 push ecx
02306560 8BC1 mov eax,ecx
02306562 C1E0 07 shl eax,7
02306565 C1E8 1D shr eax,1D
02306568 8BD9 mov ebx,ecx
0230656A C1E3 02 shl ebx,2
0230656D C1EB 1B shr ebx,1B
02306570 8BCA mov ecx,edx
02306572 C1E1 04 shl ecx,4
02306575 C1E9 1B shr ecx,1B //此处eax,ecx,ebx=opcode中的某些位
02306578 0BC0 or eax,eax //从这里开始根据eax,ecx来解密B,由ecx索引B中的某一bit位
0230657A 74 20 je short 0230659C
0230657C 83F8 01 cmp eax,1
0230657F 74 21 je short 023065A2
02306581 83F8 02 cmp eax,2
02306584 74 22 je short 023065A8
02306586 83F8 03 cmp eax,3
02306589 74 23 je short 023065AE
0230658B 83F8 04 cmp eax,4
0230658E 74 23 je short 023065B3
02306590 83F8 05 cmp eax,5
02306593 74 23 je short 023065B8
02306595 83F8 06 cmp eax,6
02306598 74 28 je short 023065C2
0230659A EB 30 jmp short 023065CC
0230659C 0FAB4F 24 bts dword ptr ds:[edi+24],ecx //eax=0, B.bit[ecx]=1
023065A0 EB 2A jmp short 023065CC
023065A2 0FB34F 24 btr dword ptr ds:[edi+24],ecx //eax=1, B.bit[ecx]=0
023065A6 EB 24 jmp short 023065CC
023065A8 0FBB4F 24 btc dword ptr ds:[edi+24],ecx //eax=2, B.bit[ecx]=~B.bit[ecx]
023065AC EB 1E jmp short 023065CC
023065AE D247 24 rol byte ptr ds:[edi+24],cl //eax=3, left rotate B
023065B1 EB 19 jmp short 023065CC
023065B3 D24F 24 ror byte ptr ds:[edi+24],cl //eax=4, right rotate B
023065B6 EB 14 jmp short 023065CC
023065B8 C1E1 05 shl ecx,5
023065BB 0BD9 or ebx,ecx
023065BD 895F 0C mov dword ptr ds:[edi+C],ebx // eax=5, 更新des_base
023065C0 EB 0A jmp short 023065CC
023065C2 C1E1 05 shl ecx,5
023065C5 0BD9 or ebx,ecx
023065C7 895F 10 mov dword ptr ds:[edi+10],ebx // eax=5, 更新src_base
023065CA EB 00 jmp short 023065CC
023065CC 59 pop ecx
body:
023065CD 8B1F mov ebx,dword ptr ds:[edi] //此处开始执行opcode操作
023065CF 8BF2 mov esi,edx
023065D1 C1E6 09 shl esi,9
023065D4 C1EE 18 shr esi,18 // 源操作数寄存器索引
023065D7 0377 10 add esi,dword ptr ds:[edi+10] //和src_base相加
023065DA 81E6 FF000000 and esi,0FF
023065E0 8B34B3 mov esi,dword ptr ds:[ebx+esi*4] // 取src_reg值,ebx=rem_start
023065E3 8BC1 mov eax,ecx
023065E5 C1E0 15 shl eax,15
023065E8 C1E8 18 shr eax,18 // 目标操作数寄存器索引
023065EB 0347 0C add eax,dword ptr ds:[edi+C] //和des_base相加
023065EE 25 FF000000 and eax,0FF
023065F3 893483 mov dword ptr ds:[ebx+eax*4],esi //des_reg=src_reg
tail:
023065F6 8BF1 mov esi,ecx //到此处opcode操作执行完毕,开始运算下一个解释器id
023065F8 C1E6 0A shl esi,0A
023065FB C1EE 17 shr esi,17
023065FE 8BC2 mov eax,edx
02306600 C1E0 11 shl eax,11
02306603 C1E8 17 shr eax,17
02306606 2347 24 and eax,dword ptr ds:[edi+24]
02306609 33F0 xor esi,eax // xor后esi=下一个解释器id
0230660B 8B47 1C mov eax,dword ptr ds:[edi+1C] // eax=intprter_tbl
0230660E 8B34B0 mov esi,dword ptr ds:[eax+esi*4] // esi=下一个解释器offset
02306611 0377 08 add esi,dword ptr ds:[edi+8] // esi=下一个解释器入口地址
02306614 FFE6 jmp esi
显然这个解释器作用就是mov des_reg,src_reg
其他的解释器基本上head tail部分都差不多,只是shl shr所取的位不同,body部分执行各种操作
VM调用有两种,一种仅仅只是拦截一下程序(hook vm),比如:
00545D21 E8 194FF3FF call fixVM.0047AC3F
-->
0047AC3F - E9 12982603 jmp 036E4456
-->
036E4456 68 7937E3D3 push D3E33779
036E445B - E9 A04BBFFE jmp 022D9000
在运行过一次以后发现036E4456变为:
036E4456 68 7937E3D3 push D3E33779
036E445B - E9 A04BBFFE jmp 038C01B0
-->
038C01B0->8D6424 04 LEA ESP,[ESP+4] <---- disasm address
038C01B4 E9 395EC8FC JMP 00545FF2
因此VM仅是HOOK了一下,这种情况很好修复,把call fixVM.0047AC3F改成call 00545FF2就行了
另一种VM则是虚拟执行X86代码(emulate vm),这种是真正难修复的,下面介绍这种VM的修复方法
<4>emulate VM修复
-----------------------------------
第一个emulate vm出现在:
0040C7CC FF7424 04 PUSH DWORD PTR [ESP+4]
0040C7D0 51 PUSH ECX
0040C7D1->E8 C2FFFFFF CALL 0040C798
0040C7D6 59 POP ECX
0040C7D7 59 POP ECX
-->
0040C798 - E9 E57A2D03 jmp 036E4282
-->
036E4282 68 9E172296 push 9622179E
036E4287 - E9 744DBFFE jmp 022D9000
CALL 0040C798之前的CPU状态为:
flag=00000246
edi=00000003 esi=009779F0 ebp=00000000 esp=0013F8A8
ebx=00000001 edx=000001F5 ecx=00C10CC0 eax=0013F8DC
通常的思路当然是把VM整个的用loader载入到脱壳后的文件中运行,这样做的话,会发现异常出现:
023EF508 B1 05 mov cl,5
023EF50A D247 24 rol byte ptr ds:[edi+24],cl
023EF50D CD 01 int 1 // 异常
023EF50F C647 0C AD mov byte ptr ds:[edi+C],0AD
023EF513 8B9F 90010000 mov ebx,dword ptr ds:[edi+190]
跟踪IDT中SF的int1 handler:
int1 handler=F0BD7D28
seg000:F0BD7D28 jmp short loc_F0BD7D2E
seg000:F0BD7D28 ; ---------------------------------------------------------------------------
seg000:F0BD7D2A dd 1E3BEh
seg000:F0BD7D2E ; ---------------------------------------------------------------------------
seg000:F0BD7D2E
seg000:F0BD7D2E loc_F0BD7D2E: ; CODE XREF: seg000:F0BD7D28j
seg000:F0BD7D2E push 0 ; flag=00000203
seg000:F0BD7D2E ; edi=03300000 esi=02307268 ebp=00000020 esp=0013F91C
seg000:F0BD7D2E ; ebx=0000000B edx=001D4E01 ecx=B202CE05 eax=03300000
seg000:F0BD7D30 mov word ptr [esp+2], 0
seg000:F0BD7D37 mov ebp, esp
seg000:F0BD7D39 cld
seg000:F0BD7D3A mov ecx, [esp+8] ; outer_cs
seg000:F0BD7D3E test ecx, 11b ; RPL==0 ?
seg000:F0BD7D44 jz loc_F0BD7E33
seg000:F0BD7D4A xor eax, eax
seg000:F0BD7D4C mov dr7, eax ; clean dr7
seg000:F0BD7D4F mov ecx, [esp+4] ; outer_eip
.......
seg000:F0BD7DA0 push eax ; ---->SEH handler
seg000:F0BD7DA1 push large dword ptr fs:0
seg000:F0BD7DA8 mov large fs:0, esp ; set SEH
seg000:F0BD7DAF
seg000:F0BD7DAF loc_F0BD7DAF: ; CODE XREF: seg000:F0BD7D98j
seg000:F0BD7DAF sti
seg000:F0BD7DB0 lea eax, [ebp+16] ; eax=outer_esp
seg000:F0BD7DB3 mov [edi+84h], eax
seg000:F0BD7DB9 mov [edi+88h], esp
seg000:F0BD7DBF sub esp, 20h
seg000:F0BD7DC2 mov eax, edi
seg000:F0BD7DC4 mov esi, [edi+84h] ; esi=outer_esp
seg000:F0BD7DCA mov esi, [esi]
seg000:F0BD7DCC mov edi, esp ; new VM rem
seg000:F0BD7DCE push ecx
seg000:F0BD7DCF mov ecx, 8
seg000:F0BD7DD4 rep movsd ; copy mem from outerstack to new VM rem
seg000:F0BD7DD6 pop ecx
seg000:F0BD7DD7 mov edi, eax
seg000:F0BD7DD9 jmp ecx ;ecx=outer_eip
显然int 1的作用是复制VM前8个dword到ring0 stack,然后返回INT 1的下一行,此后VM运行在ring0下
注意F0BD7DAF sti这一行,INT 1中断时IF自动清除,禁中断,此处开中断是必须的
因为之后VM仍然在用户空间中运行,本来这样不安全,分页内存可能PAGE FAULT,但因为开了中断,此后代码等于在IRQL=PASSIVE_LEVEL运行,因此可以正常访问内存,此外还可以保证正常线程切换
运行一些代码后VM跳回SF驱动:
022E4D57 035F 0C add ebx,dword ptr ds:[edi+C]
022E4D5A 81E3 FF000000 and ebx,0FF
022E4D60 - FF2498 jmp dword ptr ds:[eax+ebx*4] ---> F1085BFC
seg000:F6A4DBFC jmp short loc_F6A4DC02
seg000:F6A4DBFC ; ---------------------------------------------------------------------------
seg000:F6A4DBFE dd 0FFFDA4EAh ; =-25B16
seg000:F6A4DC02 ; ---------------------------------------------------------------------------
seg000:F6A4DC02
seg000:F6A4DC02 loc_F6A4DC02: ; CODE XREF: seg000:F6A4DBFCj
seg000:F6A4DC02 pop ebx ; 栈顶第一个值
seg000:F6A4DC03 mov eax, [edi+88h]
seg000:F6A4DC09 sub eax, 20h ; ' '
seg000:F6A4DC0C sub eax, esp
seg000:F6A4DC0E mov ecx, [edi+84h]
seg000:F6A4DC14 sub [ecx], eax
seg000:F6A4DC16 mov eax, [edi+88h]
seg000:F6A4DC1C sub eax, esp
seg000:F6A4DC1E shr eax, 2
seg000:F6A4DC21 mov edx, edi
seg000:F6A4DC23 mov esi, esp
seg000:F6A4DC25 mov edi, [ecx]
seg000:F6A4DC27 mov ecx, eax
seg000:F6A4DC29 rep movsd // copy VM rem back
seg000:F6A4DC2B mov edi, edx
seg000:F6A4DC2D mov esp, [edi+88h]
....
seg000:F6A4DC50 mov eax, 7FFh
seg000:F6A4DC55 mov dr7, eax //恢复dr7
....
seg000:F6A4DC7B add esp, 4
seg000:F6A4DC7E mov [esp], ebx // ebx=返回驱动前栈顶第一个值
seg000:F6A4DC81 iret
IRET执行后INT1中断才返回,此后VM运行于ring3
除了INT 1指令,SF还直接设置dr0-dr3来触发INT1中断,所以清dr7、恢复dr7是必须的
一个可能的修复VM的方法就是,模拟INT 1和IRET,在ring3执行以上两段代码,但实际发现行不通
VM在ring0下执行很多操作,比如,读cs,读页表,读IDT,读cr4,rdstc效验,内存检查,甚至调用驱动中的函数(估计是检查光驱),要全部模拟掉几乎是不可能的,因而要想办法绕开ring0的操作
为了获得对VM执行流程大概的了解,可以HOOK intprter_tbl,截取并记录每个opcode的id
HOOK只要注入个DLL到SF进程,替换intprter_tbl就行了,很幸运,SF没有check intprter_tbl
例:
记录文件:
opcode trace log:
[0]183
[1]333
[2]452
[3]165
...
[37760]444 *
[37761]014 *
[37762]048 *
...
[69235]230
[69236]448
[69237]235
[69238]398
log end
[]中是数目,后面是解释器id(opcode id),*的地方表示运行于ring0
因为VM可能运行于ring0,HOOK时除了记录流程外,不能有其他代码
简单看一下,VM执行了60000多个opcode,反复进入ring0 160多次
很显然这69238个opcode不可能都是emulate x86code,很多肯定都是各种ANTI、解密之类
那么如果找到emulate x86开始的地方?想象一下VM开始模拟X86代码时会发生什么?
再看看VM入口代码:
022D9000 /EB 14 jmp short 022D9016
...
022D9016 \60 pushad
022D9017 9C pushfd
很容易想到开始emulate x86时,VM肯定要恢复eflags和8个register!
于是方案就是,在pushfd后修改栈中的eflags(magic eflags),同时对每个opcode记录vm_eflags
然后在记录文件中搜索magic eflags,看什么地方出现
例:magic eflags=FF000246,因为eflags最高字节是reserved的,即使这个值被popf,也不会对程序运行产生什么影响
记录文件中第一个dword是vm_eflags
opcode trace log:
[0]183 00000202
[1]333 00000202
[2]452 00000202
...
[46087]408 00000246
[46088]323 00000246
[46089]090 FF000246
[46090]374 FF000246
[46091]041 FF000246
...
[69221]230 00000246
[69222]448 00000246
[69224]398 00000246
log end
第46089个opcode时magic eflags出现了!很明显前面都是各种ANTI,这里才开始正式emulate x86code
修改程序,在magic eflags时输出VM REM:
00 00 43 03 00 04 43 03 00 90 39 02 55 00 00 00
D4 00 00 00 A0 74 60 00 00 00 00 00 88 D7 40 02
B8 EF B4 03 7A 00 00 00 46 02 00 FF CC 9C 0E 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 D8 FD DB B1 58 FD DB B1 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
DC F8 13 00 C0 0C C1 00 F5 01 00 00 01 00 00 00
vm_eax vm_ecx vm_edx vm_ebx
A4 F8 13 00 00 00 00 00 F0 79 97 00 03 00 00 00
vm_esp vm_ebp vm_esi vm_edi
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 46 02 00 FF
和调用VM之前的CPU状态对比一下,发现vmrem+A0开始是8个通用寄存器,已标记在上面
那么emulate是什么时候结束的?同样可以用magic eflags的方法
修改HOOK,使之显示[n]id vm_eflags vm_ip esp
虽然每次调用VM执行的opcode数目不完全相同,但最后的终止vm_ip是确定的:
[68638]398 00000246 003808B4 0013E1B4
log end
修改HOOK,在vm_ip=003808B4处弹个MessageBox,根据intprter_tbl[398]+vm_base算出最后一个解释器地址
发现这是一个跳转opcode
...
0230347D 8B07 mov eax,dword ptr ds:[edi]
0230347F 8BD9 mov ebx,ecx
02303481 C1EB 1C shr ebx,1C
02303484 8BEA mov ebp,edx
02303486 C1E5 1C shl ebp,1C
02303489 C1ED 18 shr ebp,18
0230348C 0BDD or ebx,ebp
0230348E 035F 0C add ebx,dword ptr ds:[edi+C]
02303491 81E3 FF000000 and ebx,0FF
02303497 FF2498 jmp dword ptr ds:[eax+ebx*4] // jmp des_reg
-->
VM exit
0239A830->8B87 D0020000 MOV EAX,[EDI+2D0]
0239A836 03E0 ADD ESP,EAX
0239A838 9D POPFD
0239A839 61 POPAD
0239A83A C3 RETN //此后0040C7D6返回 VM结束
修改HOOK,在emulate start后对每个opcode检测vm_eflags,如果高10位(都是reserved的)为0,则在高10位写入一个计数值:
DWORD eflags=vm_rem->eflags;
BYTE id=eflags>>24;
if(id==0)
{
DWORD eflags_mask=magic_eflags_id;
eflags_mask=eflags_mask<<22;
vm_rem->eflags=vm_rem->eflags | eflags_mask;
magic_eflags_id++;
if(magic_eflags_id==256*4)magic_eflags_id=1;
}
然后在0239A830的VM exit代码处下断点
0239A838 9D POPFD
看popfd前的栈中eflags,发现eflags=04400246,044就是计数值,在记录文件中搜索此值,发现
[46231]374 00400246 006074A8 0013F8A4 //emu start
[46487]230 04400246 005FB088 0013F8A0 //emu end
整个VM运行过程中只有这200多个opcode是真正emulate x86code的
修改HOOK,在emulate开始处记录VMREM状态,然后修复EXE,调用VM时载入此VMREM,更新vm_eflags和8个寄存器字段,然后从id=374的解释器开始执行,这时执行的就是emulate x86的代码!绕开了之前所有的anti!
继续运行下去发现在到达emu end前居然还有int 1进入ring0
修改HOOK,在emu start和emu end之间输出详细的VM状态,包括eflags和8个register,可以发现在进入ring0前记录如下:
n id vm_eflag vm_ip esp vm_eax vm_ecx vm_edx vm_ebx vm_esp vm_ebp vm_esi vm_edi
[2878]374 24400246 008B8018 0013F6D4 00C27CC4 0000000E 00000000 00C4D9A8 0013F6D4 0013F898 00C27CC4 00C4DE1E
[2879]476 24400246 008B8024 0013F6D4 00C27CC4 0000000E 00000000 00C4D9A8 0013F6D4 0013F898 00C27CC4 00C4DE1E
[2880]172 24400246 008B8030 0013F6D4 00C27CC4 0000000E 00000000 00C4D9A8 0013F6D4 0013F898 00C27CC4 00C4DE1E
[2881]222 24400246 0035F648 0013F6D4 00C27CC4 0000000E 00000000 00C4D9A8 0013F6D4 0013F898 00C27CC4 00C4DE1E
[2882]476 24400246 0035F650 0013F6D4 00C27CC4 0000000E 00000000 00C4D9A8 0013F6D4 0013F898 00C27CC4 00C4DE1E
[2883]098 24400246 0035F65C 0013F6D4 00C27CC4 0000000E 00000000 00C4D9A8 0013F6D4 0013F898 00C27CC4 00C4DE1E
[2884]201 24400246 0035F664 0013F6D4 00C27CC4 0000000E 00000000 00C4D9A8 0013F6D4 0013F898 00C27CC4 00C4DE1E
[2885]054 24400246 0035F670 0013F6D4 00C27CC4 0000000E 00000000 00C4D9A8 0013F6D4 0013F898 00C27CC4 00C4DE1E
[2886]043 24400246 0035F678 0013F6D4 00C27CC4 0000000E 00000000 00C4D9A8 0013F6D4 0013F898 00C27CC4 00C4DE1E
[2887]332 24400246 0035F684 0013F6D4 00C27CC4 0000000E 00000000 00C4D9A8 0013F6D4 0013F898 00C27CC4 00C4DE1E
[2888]020 24400246 0035F68C 0013F6D4 00C27CC4 0000000E 00000000 00C4D9A8 0013F6D4 0013F898 00C27CC4 00C4DE1E
[2889]083 24400246 0035F698 0013F6D4 00C27CC4 0000000E 00000000 00C4D9A8 0013F6D4 0013F898 00C27CC4 00C4DE1E
[2890]248 24400246 0035F6A0 0013F6D4 00C27CC4 0000000E 00000000 00C4D9A8 0013F6D4 0013F898 00C27CC4 00C4DE1E
[2891]263 24400246 0035F6AC 0013F6D4 00C27CC4 0000000E 00000000 00C4D9A8 0013F6D4 0013F898 00C27CC4 00C4DE1E
[2892]173 24400246 0035F6B4 0013F6D4 00000000 0000000E 00000000 00C4D9A8 0013F6D4 0013F898 00C27CC4 00C4DE1E
[2893]185 24400246 0035F6C0 0013F6D4 00000000 0000000E 00000000 00C4D9A8 0013F6D4 0013F898 00C27CC4 00C4DE1E
[2894]444 24400246 0035F6C8 0013F6D4 00000000 00000000 00000000 00C4D9A8 0013F6D4 0013F898 00C27CC4 00C4DE1E
[2895]407 24400246 0035F6D4 0013F6D4 00000000 00000000 00000000 00C4D9A8 0013F6D4 0013F898 00C27CC4 00C4DE1E
[2896]393 24400246 0035F6DC 0013F6D4 00000000 00000000 00000000 00C4D9A8 0013F6D4 0013F898 00C27CC4 00C4DE1E
[2897]068 24400246 0035F6E8 0013F6D4 00000000 00000000 00000000 00C4D9A8 0013F6D4 0013F898 00C27CC4 00C4DE1E
[2898]165 24400246 0035F6F0 0013F6D4 00000000 00000000 00000000 00000000 0013F6D4 0013F898 00C27CC4 00C4DE1E
[2899]495 24400246 0035F6FC 0013F6D4 00000000 00000000 00000000 00000000 0013F6D4 0013F898 00C27CC4 00C4DE1E
[2900]230 24400246 0035F704 0013F6D4 00000000 00000000 00000000 00000000 00000000 0013F898 00C27CC4 00C4DE1E
[2901]263 24400246 0035F710 0013F6D4 00000000 00000000 00000000 00000000 00000000 0013F898 00C27CC4 00C4DE1E
[2902]058 24400246 0035F718 0013F6D4 00000000 00000000 00000000 00000000 00000000 00000000 00C27CC4 00C4DE1E
[2903]312 24400246 0035F724 0013F6D4 00000000 00000000 00000000 00000000 00000000 00000000 00C27CC4 00C4DE1E
[2904]139 24400246 0035F72C 0013F6D4 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00C4DE1E
[2905]157 24400246 0035F738 0013F6D4 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00C4DE1E
[2906]393 24400246 0035F740 0013F6D4 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
[2907]062 24400246 0035F74C 0013F6D4 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
[2908]165 24400246 0035F754 0013F6D4 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
[2909]312 24400246 0035F760 0013F6D4 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
这里清除了8个register,然后往下翻记录,发现经历几个ring0后来到
[3868]472 2D400206 003ECED8 0013F6D4 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
[3869]189 2D400206 003ECEE0 0013F6D4 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
[3870]226 2D400206 003ECEEC 0013F6D4 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
[3871]058 2D400206 003ECEF4 0013F6D4 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
[3872]495 2D400206 003ECF00 0013F6D4 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
[3873]230 2D400206 003ECF08 0013F6D4 00C27CC4 00000000 00000000 00000000 00000000 00000000 00000000 00000000
[3874]103 2D400206 003ECF14 0013F6D4 00C27CC4 00000000 00000000 00000000 00000000 00000000 00000000 00000000
[3875]393 2D400206 003ECF1C 0013F6D4 00C27CC4 00000000 00000000 00C4D9A8 00000000 00000000 00000000 00000000
[3876]103 2D400206 003ECF28 0013F6D4 00C27CC4 00000000 00000000 00C4D9A8 00000000 00000000 00000000 00000000
[3877]146 2D400206 003ECF30 0013F6D4 00C27CC4 0000000E 00000000 00C4D9A8 00000000 00000000 00000000 00000000
[3878]185 2D400206 003ECF3C 0013F6D4 00C27CC4 0000000E 00000000 00C4D9A8 00000000 00000000 00000000 00000000
[3879]139 2D400206 003ECF44 0013F6D4 00C27CC4 0000000E 00000000 00C4D9A8 00000000 00000000 00000000 00000000
[3880]302 2D400206 003ECF50 0013F6D4 00C27CC4 0000000E 00000000 00C4D9A8 00000000 00000000 00000000 00000000
[3881]356 2D400206 003ECF58 0013F6D4 00C27CC4 0000000E 00000000 00C4D9A8 00000000 00000000 00C27CC4 00000000
[3882]302 2D400206 003ECF64 0013F6D4 00C27CC4 0000000E 00000000 00C4D9A8 00000000 00000000 00C27CC4 00000000
[3883]173 2D400206 003ECF6C 0013F6D4 00C27CC4 0000000E 00000000 00C4D9A8 00000000 00000000 00C27CC4 00C4DE1E
[3884]062 2D400206 003ECF78 0013F6D4 00C27CC4 0000000E 00000000 00C4D9A8 00000000 00000000 00C27CC4 00C4DE1E
[3885]146 2D400206 003ECF80 0013F6D4 00C27CC4 0000000E 00000000 00C4D9A8 0013F6D4 00000000 00C27CC4 00C4DE1E
[3886]062 2D400206 003ECF8C 0013F6D4 00C27CC4 0000000E 00000000 00C4D9A8 0013F6D4 00000000 00C27CC4 00C4DE1E
[3887]093 2D400206 003ECF94 0013F6D4 00C27CC4 0000000E 00000000 00C4D9A8 0013F6D4 0013F898 00C27CC4 00C4DE1E
[3888]236 2D400206 003ECFA0 0013F6D4 00C27CC4 0000000E 00000000 00C4D9A8 0013F6D4 0013F898 00C27CC4 00C4DE1E
[3889]048 2D400206 003ECFA8 0013F6D4 00C27CC4 0000000E 00000000 00C4D9A8 0013F6D4 0013F898 00C27CC4 00C4DE1E
[3890]333 2D400206 003ECFB0 0013F6D4 00C27CC4 0000000E 00000000 00C4D9A8 0013F6D4 0013F898 00C27CC4 00C4DE1E
[3891]408 2D400206 003ECFB8 0013F6D4 00C27CC4 0000000E 00000000 00C4D9A8 0013F6D4 0013F898 00C27CC4 00C4DE1E
到了此处8个register又恢复,而中间的过程中8reg都是0
于是,可以这么处理,在清8register前中断,将vmrem修改为恢复8register后的状态
运行程序,发现这个VM调用居然顺利运行结束了,那么emulate end之后的ring0为什么没触发呢?
跟踪修复后的VM发现,最后一个opcode跳转之后来到:
03664754 58 pop eax
03664755 5B pop ebx
03664756 897B 3C mov dword ptr ds:[ebx+3C],edi
03664759 8943 68 mov dword ptr ds:[ebx+68],eax
0366475C 61 popad
0366475D 9D popfd
0366475E 8D6424 04 lea esp,dword ptr ss:[esp+4]
03664762 FF6424 FC jmp dword ptr ss:[esp-4] //0040C7D6 返回
对比SF运行时的VM,发现这段代码也会被执行,但jmp dword ptr ss:[esp-4]跳回了VM,原来栈中保存的return address被修改了!
由此可以推测SF在emu start后返回ring0修改了return address
另外也可以直接patch opcode,使之绕过ring0部分
这里清理8reg前发生了一次vm_ip切换,分析切换附近的opcode
[2879]476 24400246 008B8024 0013F6D4 00C27CC4 0000000E 00000000 00C4D9A8 0013F6D4 0013F898 00C27CC4 00C4DE1E//mov reg_2C,35F648
[2880]172 24400246 008B8030 0013F6D4 00C27CC4 0000000E 00000000 00C4D9A8 0013F6D4 0013F898 00C27CC4 00C4DE1E//mov vm_ip,reg_2C
[2881]222 24400246 0035F648 0013F6D4 00C27CC4 0000000E 00000000 00C4D9A8 0013F6D4 0013F898 00C27CC4 00C4DE1E
VM切换vm_ip时总是用类似这样的代码:
008B8024 mov reg_2C,35F648
008B8030 mov vm_ip,reg_2C
8B8024这个opcode是3个dword(8B8030-8B8024=C),第3个dword就是常数35F648
修改这个dword,改为ring0处理后的vm_ip,同时前面2个dword和运算下一个解释器id相关的位也要修改,使下一个解释器id正确
这样修复后可以直接运行不用再中断了,更完美些
这么处理后这个call就算是正常运行了,再修复掉几个hook vm后可以进入游戏菜单界面
再修复掉3个emulate vm(和上面的大同小异)和N个hook vm后可以进入游戏了,玩了5分钟,没什么问题,但点其他菜单还有vm,搞不动了,投降,脱壳结束
<5>VM的反编译
-----------------------------------
像上面这样修复,虽然不是很完美,但可以工作,虽然VM仍在运行不过并不会太影响效率(那300个emulate opcode和全部60000个opcode比只是九牛一毛而已)。
后来尝试了下反编译VM的opcode,太麻烦,弄了一点搞不动了
以下的描述和脱壳无关,仅是个示例,弄了点开头,不是完整的解决方法
500个解释器基本结构都是head+body+tail,对每个解释器用一个struct描述
struct VM_OP_DEF
{
int id; // zero based, 0-499
int op_len;
DWORD p_eax;
DWORD p_ebx;
DWORD p_ecx;
DWORD src;//+[edi+10]
DWORD des;//+[edi+C]
char*cmd;
DWORD p_ebp;
DWORD ext;
};
#define MAKEDWORD(a,b,c,d) (DWORD(a)<<24 | DWORD(b)<<16 | DWORD(c)<<8 | DWORD(d))
#define OP(type,n_dword,left,right) (MAKEDWORD(type,n_dword,left,right))
#define D1(left,right) (OP(1,1,left,right))
#define D2(left,right) (OP(1,2,left,right))
#define D3(left,right) (OP(1,3,left,right))
VM_OP_DEF vm_op_def[]=
{
// eax ebx ecx +10 src +C des
{256,8, D1(0x7,0x1D),D1(0x2,0x1B),D2(0x4,0x1B) ,D2(0x9,0x18),D1(0x15,0x18), "mov @d,@s" ,0,0},
{183,8, D1(0x8,0x1D),D2(0x4,0x1B),D2(0x1B,0x1B) ,D1(0x0,0x18),D1(0xD,0x18), "or @d,@s" ,0,0},
{333,8, D1(0x13,0x1D),D2(0x9,0x1B),D2(0x4,0x1B) ,D1(0x18,0x18),D2(0xE,0x18), "mov @d,@s" ,0,0},
{452,8, D1(0x2,0x1D),D2(0x9,0x1B),D2(0x4,0x1B) ,D1(0x10,0x18),D2(0xE,0x18), "mov @d,@s" ,0,0},
{165,12, D1(0x15,0x1D),D1(0x1B,0x1B),D1(0,0x1E) ,D1(0x18,0x1D),D2(0x4,0x18), "mov#s @d,=3" ,P2(1,0x1D,0x1B),0},
...
};
这些D1,D2里记录对vm_ip buffer中的dword的位,对应于解释器中的shl/shr
char*cmd表示这个指令,@d表示操作数是des+des_base寄存器,#s表示这是个条件指令,=3表示第3个dword的常数值
然后写个函数根据这个表来反编译(就像OD反编译原代码那样)
问题是这样要分析500个解释器,相当麻烦,如果只算emulate x86部分使用的解释器大概100多个,很多都是功能相同只是shl/shr不同
我想不出更好的方法来解决shl/shr的麻烦,reload的工具包反编译opcode的代码在sf3.dll中,可惜没原代码,IDA里看了下似乎没有shl/shr的麻烦,不知道是他们有什么办法解决还是以前的版本里没这东西,估计也许可以自动搜索并分析shl/shr
这样反编译出来的代码仍然不是x86代码,而是用几个opcode来模拟一个x86代码
比如,类似这样的代码:
mov reg_320,FFFFFFFC
add vm_esp,reg320
push vm_edi
模拟的是push edi
<6>一些启示
-----------------------------------
关于anti-debugger:
-----------
SF在SF4及之后的版本中放弃了INT1 INT3,基本上没什么强悍的ANTI,OD就可以调试,VM也可以直接loader,难度直线下降,据说这么做是为了提高兼容性
其实我到觉得放弃这些ANTI很可惜,理想的ANTI应该这样,不仅替换INT1 INT3,甚至可以把CR3、IDT、GDT整个的替换掉!建立一个虚拟环境,在虚拟环境中执行代码!
VMWare的驱动进入VMM(vitual machine monitor)就是这样:
seg000:F8BC0010 push eax
seg000:F8BC0011 mov eax, [esp+oldinfo]
seg000:F8BC0015 sgdt qword ptr [eax+0]
seg000:F8BC0019 sidt qword ptr [eax+10h]
seg000:F8BC001D mov [eax+4Ch], edx
seg000:F8BC0020 mov edx, cr3
seg000:F8BC0023 mov [eax+38h], edx
seg000:F8BC0026 pop dword ptr [eax+44h]
seg000:F8BC0029 mov [eax+50h], ebx
seg000:F8BC002C mov [eax+48h], ecx
seg000:F8BC002F mov [eax+5Ch], esi
seg000:F8BC0032 mov [eax+60h], edi
seg000:F8BC0035 mov [eax+58h], ebp
seg000:F8BC0038 mov [eax+54h], esp
seg000:F8BC003B mov word ptr [eax+70h], ds
seg000:F8BC003E mov word ptr [eax+6Ch], ss
seg000:F8BC0041 mov word ptr [eax+64h], es
seg000:F8BC0044 mov edx, [esp+0] ; ret
seg000:F8BC0048 mov [eax+3Ch], edx
seg000:F8BC004B mov eax, [esp-4+newinfo]
seg000:F8BC004F lgdt qword ptr [eax+0]
seg000:F8BC0053 mov edx, [eax+38h]
seg000:F8BC0056 mov ecx, [eax+70h]
seg000:F8BC0059 mov eax, [esp-4+arg_C]
seg000:F8BC005D mov cr3, edx
seg000:F8BC0060 mov ds, cx
seg000:F8BC0062 ltr word ptr [eax+18h]
seg000:F8BC0066 mov ss, word ptr [eax+6Ch]
seg000:F8BC0069 mov es, word ptr [eax+64h]
seg000:F8BC006C lidt qword ptr [eax+10h]
seg000:F8BC0070 mov ebx, [eax+50h]
seg000:F8BC0073 mov ecx, [eax+48h]
seg000:F8BC0076 mov edx, [eax+4Ch]
seg000:F8BC0079 mov esi, [eax+5Ch]
seg000:F8BC007C mov edi, [eax+60h]
seg000:F8BC007F mov ebp, [eax+58h]
seg000:F8BC0082 mov esp, [eax+54h]
seg000:F8BC0085 mov eax, [eax+44h]
seg000:F8BC0088 retf 0Ch
在lgdt qword ptr [eax+0]之后完全没有办法调试,哪怕用jmp或者intxx做断点也无法跨越虚拟与真实之间的界限
要是壳在这样的虚拟环境下解密,运行VM,难度可想而知
那么这样是否会降低兼容性或者被杀毒软件kill掉呢?实际上也不会有这些问题
什么GDT IDT CR3这些机制都是从386就有的,只要设计良好完全可以做到兼容,我们运行VMWare也几乎不会出现BSOD
只要虚拟运行前建立虚拟环境,运行结束后还原,杀毒软件也不会发难(也没机会发难)
不过intel vt技术出来后,估计离虚拟硬件调试器也不远了,那样的话即使如此也是没法anti的....
关于VM:
-----------
VM一方面要隐藏代码,一方面要防止load,总的来说应该是离x86体系越远越好,离x86越远越难以被分析
SF的VM基本还是很接近x86的,有诸如vm_eflags,vm_eax这样的东西,分析它只是时间问题
事实上我觉得从已经编译的EXE开始加壳是不可能离x86太远的,因为VM最终要归结到模拟x86指令,而如果要反汇编x86指令抽象出背后的逻辑又是非常困难的
可能的更好的方法也许是在编译级加壳,这个级别可以直接获得程序的逻辑,从而可能设计出实现这些逻辑的远离x86的VM
甚至可以在神经网络中实现逻辑,这个连冯诺依曼体系都远离了,更难分析
<-1>
-----------------------------------
SF3.4这东西我从06年末开始分析,断断续续弄到现在才终于搞定了,中间学会了不少东西,当初连ring0为何物还未清楚...
感谢forgot jojo lovezero machoman和其他很多朋友的帮助
DonQuixote[CCG][iPB]
Email:DonQuixote@mail.nankai.edu.cn
2008/2/16
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!