<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