前言:
    很久以前写了篇文章是穷举vm保护的CM算法的,当时对vm保护一无所知,只好穷举解决,很长时间“耿耿于怀”。现在对vm保护有了点初步的了解,就拿这个简单的CM开刀吧,权当抛砖引玉,高手莫笑。(此Cm在论坛中搜jackozoo_2009.rar即可),非常感谢jackozoo贡献了这个例子。

一、解决的思路和方法
   由于字节码没有加密等特殊处理,并且只vm了一段关键的算法代码,vm_context也没有变化。人肉应该不成问题。首先要知道有多少个字节码指令。

找到虚拟引擎的入口而后单步进去。
004010FB  |.  68 68F14000   PUSH ZooCMa.0040F168 //该处开始存放字节码
00401100  |.  E8 7B1A0000   CALL ZooCMa.00402B80 //虚拟引擎的入口
00401105  |.  83F8 01       CMP EAX,1  //判断返回值

进入之后立即来到下面的字节码指令循环处理的地方。
00402BD6 > > /8B9D ECFEFFFF MOV EBX,DWORD PTR SS:[EBP-114]
00402BDC   . |0FB613        MOVZX EDX,BYTE PTR DS:[EBX]
00402BDF   . |FF85 ECFEFFFF INC DWORD PTR SS:[EBP-114]
00402BE5   . |BB 00C04000   MOV EBX,ZooCMa.0040C000  //ebx就是handler表的首地址
00402BEA   . |FF2493        JMP DWORD PTR DS:[EBX+EDX*4]   ;  dispatch
 
 到handler表去看看,简单算一下一共59个,即十六进制0-3a
0040C000  ED 2B 40 00 EF 2B 40 00 0D 2C 40 00 2F 2C 40 00  ?@.?@..,@./,@.
0040C010  4F 2C 40 00 7A 2C 40 00 A8 2C 40 00 ED 2C 40 00  O,@.z,@.?@.?@.
0040C020  1D 2D 40 00 50 2D 40 00 7C 2D 40 00 A0 2D 40 00   -@.P-@.|-@.?@.
0040C030  C7 2D 40 00 EC 2D 40 00 10 2E 40 00 37 2E 40 00  ?@.?@. .@.7.@.
0040C040  5C 2E 40 00 83 2E 40 00 AA 2E 40 00 AF 2E 40 00  \.@.?@.?@.?@.
0040C050  B4 2E 40 00 DB 2E 40 00 02 2F 40 00 29 2F 40 00  ?@.?@. /@.)/@.
0040C060  57 2F 40 00 85 2F 40 00 8A 2F 40 00 8F 2F 40 00  W/@.?@.?@.?@.
0040C070  BD 2F 40 00 EB 2F 40 00 19 30 40 00 43 30 40 00  ?@.?@. 0@.C0@.
0040C080  6D 30 40 00 98 30 40 00 C4 30 40 00 EE 30 40 00  m0@.?@.?@.?@.
0040C090  18 31 40 00 42 31 40 00 8D 31 40 00 DA 31 40 00   1@.B1@.?@.?@.
0040C0A0  27 32 40 00 43 32 40 00 72 32 40 00 A1 32 40 00  '2@.C2@.r2@.?@.
0040C0B0  D0 32 40 00 FF 32 40 00 34 33 40 00 73 33 40 00  ?@.2@.43@.s3@.
0040C0C0  B2 33 40 00 E7 33 40 00 F8 33 40 00 20 34 40 00  ?@.?@.?@. 4@.
0040C0D0  4D 34 40 00 7B 34 40 00 A8 34 40 00 D6 34 40 00  M4@.{4@.?@.?@.
0040C0E0  F9 34 40 00 1C 35 40 00 4C 35 40 00              ?@. 5@.L5@. 

那么这0-3a到底是虚拟了哪3b条x86汇编指令呢?这里只有一个一个分析。

当然[EBP-124]等局部变量到底是表示vm_context中的哪个虚拟寄存器或虚拟临时变量需要参考虚拟引擎初始化或结束部分以及在调试过程中加以确定。例如:
00402B89   .  8985 CCFEFFFF MOV DWORD PTR SS:[EBP-134],EAX//[EBP-134]存放__eax
00402B8F   .  898D D0FEFFFF MOV DWORD PTR SS:[EBP-130],ECX//[EBP-130]存放__ecx
00402B95   .  8995 D4FEFFFF MOV DWORD PTR SS:[EBP-12C],EDX//[EBP-12c]存放__edx
00402B9B   .  899D D8FEFFFF MOV DWORD PTR SS:[EBP-128],EBX//[EBP-128]存放__ebx
00402BA1   .  8D85 00FFFFFF LEA EAX,DWORD PTR SS:[EBP-100]
00402BA7   .  05 00010000   ADD EAX,100
00402BAC   .  8985 DCFEFFFF MOV DWORD PTR SS:[EBP-124],EAX //[EBP-124]存放__esp
00402BB2   .  8B45 00       MOV EAX,DWORD PTR SS:[EBP]
00402BB5   .  8985 E0FEFFFF MOV DWORD PTR SS:[EBP-120],EAX
00402BBB   .  89B5 E4FEFFFF MOV DWORD PTR SS:[EBP-11C],ESI
00402BC1   .  89BD E8FEFFFF MOV DWORD PTR SS:[EBP-118],EDI
00402BC7   .  8B45 08       MOV EAX,DWORD PTR SS:[EBP+8]   //eax=入口参数 字节码首地址
00402BCA   .  8985 C8FEFFFF MOV DWORD PTR SS:[EBP-138],EAX    //[EBP-138] 字节码首地址
00402BD0   .  8985 ECFEFFFF MOV DWORD PTR SS:[EBP-114],EAX //__eip

简单分析两条指令:VCmp 、VJl
分析一下VCmp
004031DA   .  8B9D DCFEFFFF MOV EBX,DWORD PTR SS:[EBP-124]
004031E0   .  8B13          MOV EDX,DWORD PTR DS:[EBX]    //虚拟栈顶弹出一个DWORD
004031E2   .  83C3 04       ADD EBX,4
004031E5   .  8B0B          MOV ECX,DWORD PTR DS:[EBX]    //虚拟栈顶再弹出一个DWORD
004031E7   .  8385 DCFEFFFF>ADD DWORD PTR SS:[EBP-124],8  //调整__esp
004031EE   .  3BD1          CMP EDX,ECX               //进行比较
004031F0   .  9C            PUSHFD
004031F1   .  58            POP EAX            //save EFL to eax
004031F2   .  8BC8          MOV ECX,EAX 
004031F4   .  83E0 01       AND EAX,1             
004031F7   .  8985 F8FEFFFF MOV DWORD PTR SS:[EBP-108],EAX         //save CF  to __CF
004031FD   .  8BC1          MOV EAX,ECX
004031FF   .  83E0 40       AND EAX,40
00403202   .  8985 F0FEFFFF MOV DWORD PTR SS:[EBP-110],EAX        //save ZF  to __ZF
00403208   .  8BC1          MOV EAX,ECX
0040320A   .  25 80000000   AND EAX,80
0040320F   .  8985 F4FEFFFF MOV DWORD PTR SS:[EBP-10C],EAX        //save  SF to __SF
00403215   .  8BC1          MOV EAX,ECX
00403217   .  25 00080000   AND EAX,800
0040321C   .  8985 FCFEFFFF MOV DWORD PTR SS:[EBP-104],EAX        //save  OF to __OF
00403222   .^ E9 AFF9FFFF   JMP <ZooCMa.handler>                  //  VCmp

分析一下VJl
004032FF   .  8B9D ECFEFFFF MOV EBX,DWORD PTR SS:[EBP-114]  //__eip
00403305   .  0FB713        MOVZX EDX,WORD PTR DS:[EBX]    //edx=跳转偏移
00403308   .  8B85 F4FEFFFF MOV EAX,DWORD PTR SS:[EBP-10C] // __SF to eax
0040330E   .  8B9D FCFEFFFF MOV EBX,DWORD PTR SS:[EBP-104] // __OF to ebx
00403314   .  3BC3          CMP EAX,EBX                //  __SF==__OF
00403316   .  74 10         JE SHORT ZooCMa.00403328    //not jmp
00403318   .  8B8D C8FEFFFF MOV ECX,DWORD PTR SS:[EBP-138]  //ecx=字节码首地址
0040331E   .  03CA          ADD ECX,EDX                      //ecx=字节码中的目的地址
00403320   .  898D ECFEFFFF MOV DWORD PTR SS:[EBP-114],ECX  //__eip=跳转目的地址
00403326   .  EB 07         JMP SHORT ZooCMa.0040332F       
00403328   >  8385 ECFEFFFF>ADD DWORD PTR SS:[EBP-114],2
0040332F   >^ E9 A2F8FFFF   JMP <ZooCMa.handler>            //  VJl
如此这般你就可以得到opcode 0-3a实现了哪些x86汇编指令的功能。如下表所示:
VNop           0       VPushImm8    1   VPushImm16    2    VPushImm32   3 
VPushReg8      4       VPushReg16    5   VPushReg32    6    VPopReg8      7  
VPopReg16      8       VPopReg32     9   VPushMem8    a    VPushMem16   b  
VPushMem32    c       VpopMem8     d    VPopMem16    e    VPopMem32   f  
VAdd8         10       VSub8         11   VMul8         12   VDiv8         13  
VAnd8         14       VOr8          15   VXor8         16    VAdd16       17
VSub16        18       VMul16        19   VDiv16        1a    VAnd16       1b 
VOr16         1c        VXor16       1d    VAdd32       1e    VSub32       1f 
VMul32       20        VDiv32       21   VAnd32       22    VOr32         23 
VXor32         24        VTest8       25   VTest32       26    VCmp         27 
VJmp          28         VJz         29    VJnz         2a     VJs           2b 
VJns           2c         VJl          2d    VJle         2e     VJge          2f
VJg            30         VRetN       31   VExit         32     VPushMem8Reg 33    
VPushMem32Reg 34     VPopMem8Reg  35   VPopMem32Reg 36    VsaveReg       37  
 VloadReg       38      VPopMovsx    39    VsaveEsp  3a

二、逆向VM保护的算法
为了了解算法的大致流程,我们可以重点关注这段代码,因为算法循环总以jl语句判断是否要退出循环。

vjl.JPG

004032FF   .  8B9D ECFEFFFF  MOV EBX,DWORD PTR SS:[EBP-114] //关注ebx当前地址 下断
00403305   .  0FB713         MOVZX EDX,WORD PTR DS:[EBX]
00403308   .  8B85 F4FEFFFF  MOV EAX,DWORD PTR SS:[EBP-10C]
0040330E   .  8B9D FCFEFFFF  MOV EBX,DWORD PTR SS:[EBP-104]
00403314   .  3BC3           CMP EAX,EBX
00403316   .  74 10          JE SHORT ZooCMa.00403328
00403318   .  8B8D C8FEFFFF  MOV ECX,DWORD PTR SS:[EBP-138]
0040331E   .  03CA           ADD ECX,EDX
00403320   .  898D ECFEFFFF  MOV DWORD PTR SS:[EBP-114],ECX  //关注ecx 目的地址下断
00403326   .  EB 07          JMP SHORT ZooCMa.0040332F
00403328   >  8385 ECFEFFFF >ADD DWORD PTR SS:[EBP-114],2
0040332F   >^ E9 A2F8FFFF    JMP <ZooCMa.handler>               ;  VJl

通过测试可以发现关键算法循环开始于40fa26处。大致可以看出算法是:判断当前密码字符+当前用户名字符是否等于000000db,在调试时发现进入算法循环后用户名字符的第二、三位总是为固定值,是由于进入循环前的如下两段代码所致。
在40f991处开始的一段字节码,修改用户名第二个字符为’q’
37 03           mov temp,_ebx 初始值0
03 01 00 00 00   push 01
06 08           push 00
20              mul 
03 01 00 00 00   push 01
1E             00+01=01
06 05          push _ebp 用户名首地址入虚拟栈
1E             用户名首地址+1
09 03          pop _ebx   这样_ebx=用户名首地址+1
01 71          push 71   即’q’
35 03          mov [_ebx],71 修改用户名第二个字符为q
38 03          mov _ebx,temp  即mov _ebx,0
3A 

在40f9d7处开始的一段字节码,修改用户名第三个字符为’p’
37 03 03 01 00 00 00 06 08 20
03 02 00 00 00  push 02
1E             00+02=02
06 02
1E
09 03        pop _ebx   这样_ebx=用户名首地址+2
01 70        push 70   即’p’
35 03        mov [_ebx],70 修改用户名第三个字符为’p’
38 03 
3A

vadd.JPG

在40fa26处开始的一段字节码 关键算法call
3A ;           jl 指令跳转到此 算法call开始处
37 03         mov temp,当前字符索引  保存计数变量i
03 01 00 00 00  push 1
06 02          push _edx ;用户名首地址
20             mul  ;= = push _edx
03 64 00 00 00  push 00000064  ;+100
1E             add  ;= = push _edx+100
06 00          pus _eax ; + 字符索引 初始值为0
1E             add
09 03          mov _ebx,当前密码字符地址
33 03          push 当前密码字符
39 06          mov _esi, 当前密码字符
38 03          mov _ebx, 当前字符索引
3A 
37 03          mov temp,当前字符索引
03 01 00 00 00  push 00000001
06 02          push _edx ; 用户名首地址
20             mul; = = push _edx
03 00 00 00 00  push 0
1E             add   ; _edx + 0
06 00          push _eax ; 即为push I   字符索引
1E             add  ; _edx + 0+i
09 03          mov _ebx,当前用户名字符地址
33 03          push 当前用户名字符
39 07          mov _edi, 当前用户名字符
38 03          mov _ebx, 当前字符索引
3A 
06 07        push 当前用户名字符
06 06        push 当前密码字符
1E          add;当前密码字符+当前用户名字符
09 06        mov _esi,sum
3a
03 DB 00 00 00  push 000000db
06 06          push sum
27             cmp          //当前密码字符+当前用户名字符=000000db
3A 
2A 43 09        jnz   ;错误注册码结束
3A 
03 01 00 00 00   push 00000001
06 00           push I ;计数变量
1E             ; i++
09 00           mov _eax, 计数变量值  ;更新当前比较的字符数
3A 
06 01          push _ecx  ;用户名长度
06 00          push计数变量值
27             cmp
3A 
2D BE 08       Jl   ;继续循环   40f168 +8be = 40fa26
3A
09 07            pop _edi
3A 
09 06            pop _esi
3A 
09 05            pop _ebp
3A 
03 01 00 00 00   push  00000001
09 00           pop _eax ;mov _eax,1
3A 
09 03           pop _ebx
3A 
03 70 00 00 00   push  00000070
06 04           push _esp
1E             _esp+00000070
09 04           pop _esp 调整虚拟堆栈指针
3A             
31             add _esp,4
               Mov eax,_eax  ;return 1
               Ret

算法大致如下:
           name[1] ='q';
  name[2] ='p';
  for (int i=0;i<namelen;i++)
  {    
                            Regcode=name+100;
             If  ((name[i] + regcode[i])<>0xdb) return 0;//出错
  }
  return 1;//成功

总结:VM保护非常有效,它可以浪费逆向者大量的时间和精力。简单的VM况且如此,遇到商业的,你的破解成本就太高了,得不偿失。
                                                                                    by 天易love 09-12-5