Themida虚拟机简单介绍


之所以写这篇东西,是我打算放弃跟v1.5.0.0的VM代码了:-(,另外,有些东西已经忘了,
所以下面的描述可能有错,也不完整。

1. Themida v1.1.1.0虚拟机代码的简单介绍

   v1.1.1.0的VM代码由多个动态分配的内存块组成,除代码外,有一个全局的
   数据结构,可称之为VM_CONTEXT,所有的虚拟机代码都要使用这个结构,v1.5.0.0
   的数据结构没有改变.

   结构中最重要的是+34处的dword,代表VM的register。

   代码通过push/jmp指令进入VM,格式为:

   push xxxxxxx
   jmp  xxxxxxx
   push xxxxxxx
   jmp  xxxxxxx
   ...
   push xxxxxxx
   jmp  xxxxxxx
   dd   xxxxxxx
   dd   xxxxxxx
   ...
   dd   FFFFFFFF

   最后的jmp下面为VM相关数据,第1个dword加delta offset为pcode数据起始地址,
   第2个dword为数据size。下面每3个dword为1组,对应一块独立的pcode数据,也对应
   上面的1个push/jmp指令对。以FFFFFFFF结束。

   原始代码按call分块,即原来代码内的call指令为 push/jmp指令对数 - 1。估计是
   因为call需要脱离VM,执行完后重新进入VM。

   
   每句PCODE包含如下数据(解码用到4个key):

   flag_zero_key;  // 是否清零4个key
   flag_to_init_key1;  // 取值0 - 3,决定如何初始变换key1
   num_of_rotate;  // vm_context内保存有进入VM时的寄存器(执行过程中会改变),
         这些寄存器视为环状,会随机顺序滚动,这样同样地址的数据
         可能代表不同的reg32。这个值决定滚动多少字节

   opcode;    // opcode类型,用key1解码
   opcode_flag;    // opcode的附加信息,用key1解码
   argument;    // pcode的dword参数,用key1解码
   next_pcode;    // 下一条pcode数据地址,用key3解码
   index1;    // 用key4解码
   index2;    // 用key2解码,这2个index合起来用于查表,搜索下一条pcode
         解释函数地址

   解释函数的动作包括:初始化各key,解码上述数据,如果需要滚动context内寄存器,解释
   执行pcode。计算下一条pcode地址(保存在全局数据结构+0处)及其handler地址,然后
   jmp esi执行下一句pcode。

   以类似下面的指令退出VM:

   push dword ptr ds:[edi+94]
   push dword ptr ds:[edi+9C]
   mov dword ptr ds:[edi+28],0
   popad
   popfd
   retn

   用解码程序可以得到类似下面的pcode,其中第1列为pcode地址,第2列为解释函数地址,
   register为vm_context内的register,即VM自己的register。VM使用自己的栈,push都
   是压入vm栈。

   00A186CE:011D80CA  mov  register,addr_of_context.ebp  
   00A186EA:011D0C5D  push  dword ptr [register]   
   00A18706:011DA3DB  mov  register,context.esp   
   00A18722:011F0000  sub  register,00000004   
   00A186F8:011D3F6E  rep  movsb [register],[esp] (04 bytes)   
   00A186DC:011D18AF  push  register   
   00A18714:011D744B  rep  movsb context.esp,[esp] (04 bytes)   
   00A18730:0123157F  mov  context.ebp,context.esp   
   00A1873E:01240B8E  mov  register,0D70178C       ; nop
   00A1874C:012352F6  add  context.esp,FFFFFFE0

   等价的真实代码:
   push ebp
   mov  ebp,esp
   add  esp,FFFFFFE0

   当然,也可以考虑自己定义一套pcode,把得到的vm pcode转换为自己的pcode(或直接跟
   Themida自己的编码),然后把上面的结果转换为真正的汇编码。我是自己读出来的;-)

   加壳时,对VM代码做了随机化处理,pcode数据的各个field摆放的顺序是变化的,对应的
   handler执行的操作顺序也在变化,虽然干的事情相同,但处理先后顺序可变。

   这样,修复VM保护代码时,难以写出通用的解码程序,即使是用同一版本加壳。必须逐个
   分析handler。从跟的结果看,很多handler是相同的,区别只是处理顺序。


2. Themida v1.5.0.0虚拟机代码的变化

   首先一个变化是混淆代码增强了,也许更早的版本就已经这样了。这是分析VM的主要障
   碍。对1110,可以把清理混淆代码后的VM贴回dumped_.exe直接运行,现在就难以做到了。
   基本的指令序列是类似的,但指令序列不连续,分隔指令序列的垃圾代码及其数量也在
   变化,难以识别或用NOP替代。

   我最后是这样做的: 跳着判断指令序列,如果匹配上,将中间的分隔指令全部作为junk
   清除。理由是,既然符合混淆指令序列,即由原来的正常指令expand而来,除匹配指令外,
   其余的应该是插入的垃圾。这样做的结果似乎还凑合,主要的问题是模式之间存在嵌套,
   对1个模式的处理破坏了别的代码。估计最好能判断出嵌套的模式,然后从内向外处理。
   总之费了不少劲,但结果不好。

   VM这次在1个连续的内存块内。本身的结构并无多大变化,pcode数据各field的含义,各
   key的使用都和原来一样。主要的区别有2处:

   pcode数据各field的offset固定了,这个不是很肯定,但看了4个handler都相同。各field
   在pcode数据内的偏移为:

   0x0,    // 是否清零keys,这个byte还参与key1的初始化
   0xC,    // 初始化key1
   0xC,    // 本轮rotate bytes,与上面使用1 byte不同位
   0x1,    // opcode
   0x4,    // opcode附加信息
   0x5,    // dword参数
   0xB,    // 下1条pcode地址
   0x9,    // index1
   0xA,    // index2

   这是好事。坏消息则"相当"肯定。每个handler在解码时使用的常量都不同。如下面一段
   代码,是解码v1.1.1.0使用的:

   void InitKeys(DWORD curr_pcode_data,int nIndex)
   {
  // 初始化4个解码key

  BYTE zero_key  = 0;
  BYTE init_key1  = 0;
  
  ReadProcessData(curr_pcode_data + data[nIndex].offset_zero_key,  &zero_key, 1);
  if(zero_key & 0x80)
  {
    key_1 = 0;  
    key_2 = 0;
    key_3 = 0;
    key_4 = 0;
  }

  ReadProcessData(curr_pcode_data + data[nIndex].offset_how_to_init_key1, &init_key1, 1);

  switch(init_key1 & 0x3)  // 对v1.5.0.0,使用的常数不同
  {
  case 0:
    
    key_1 ^= 0xD7;    
    break;

  case 1:
    key_1 += 0x89;    
    break;

  case 2:
    key_1 ^= zero_key;
    break;

  default:
    key_1 += zero_key;
  }
   }
 
   对于v1.1.1.0,这个函数是固定的,对1.5.0.0,下面case 0,case 1内的0xD7,0x89是变化的,
   每个handler都不同。不只是key的初始化,opcode,opcode_flag,dword参数,下句pcode地址
   及handler地址,2个index,所有数据的解码,不同handler都使用了不同的常数。看到这个我
   彻底失去了耐心。

   v1.5.0.0主程序用vm保护的代码有40余处。留给有坚强意志的朋友完成吧。

  • 标 题: 答复
  • 作 者:softworm
  • 时 间:2006-05-28 17:03

非常抱歉,1楼的好消息是假的,看过约20解释函数后发现各数据field的
offset仍然是变化的。

v1.1.1.0的pcode指令条数大约为被保护代码的几倍,应该不会超出10倍。
即,若用VM保护的汇编指令若为10行(从oep到call 401644就是10行),pcode
行数应该不会超出100。

v1.5.0.0有变化,如果在401644下断,在dump处trace into,断下后用
ultraedit统计run trace内的jmp esi个数为767。我把原来的程序大
概改了一下,先不考虑pcode的解释,仅跟踪pcode执行路径,还没有到
处理完,已经近千条了。

大致看看,下面的列表,没有显示pcode的行是还没有做。为了清晰一点,
稍微分行了一下。


0001  00BB3EDD:01501392  nop


0002  00BB3EEB:0148771E  mov  register,addr_of_context.ebp

0003  00BB3EF9:0150C5DD  push  dword ptr [register]

0004  00BB3F07:0148771E  mov  register,context.esp

0005  00BB3F15:014C7A77  sub  register,00000004

0006  00BB3F23:014A1AEA  rep  movsb [register],[esp] (04 bytes)

0007  00BB3F31:0150C5DD  push  register

0008  00BB3F3F:014A1AEA  rep  movsb context.esp,[esp] (04 bytes)  ; push ebp


0009  00BB3F4D:01450073  mov  register,0DD6C7B9


0010  00BB3F5B:0148771E  mov  register,addr_of_context.edi

0011  00BB3F69:0150C5DD  push  dword ptr [register]

0012  00BB3F77:0148771E  mov  register,addr_of_context.edx

0013  00BB3F85:0150C5DD  push  dword ptr [register]

0014  00BB3F93:014D5EF3  test  [esp+4],[esp]        ; test edi,edx


0015  00BB3FA1:01450073  mov  register,01FDCEA1


0016  00BB3FAF:0148771E  mov  register,addr_of_context.eax

0017  00BB3FBD:0150C5DD  push  dword ptr [register]

0018  00BB3FCB:0148771E  mov  register,context.esp

0019  00BB3FD9:014C7A77  sub  register,00000004

0020  00BB3FE7:014A1AEA  rep  movsb [register],[esp] (04 bytes)

0021  00BB3FF5:0150C5DD  push  register

0022  00BB4003:014A1AEA  rep  movsb context.esp,[esp] (04 bytes)  ; push eax


0023  00BB4011:01450073  mov  register,0E111969


0024  00BB401F:0148771E  mov  register,addr_of_context.edx

0025  00BB402D:0150C5DD  push  dword ptr [register]

0026  00BB403B:0150C5DD  push  FFFFFFBA

0027  00BB4049:014D5EF3  test  [esp+4],[esp](字节操作)      ; test dl,BA


0028  00BB4057:01450073  mov  register,05C35893


0029  00BB4065:0148771E  mov  register,addr_of_context.ebp

0030  00BB4073:0150C5DD  push  dword ptr [register]

0031  00BB4081:0150C5DD  push  00000005

0032  00BB408F:01446674  shr  [esp+4],[esp](双字操作)

0033  00BB409D:014A1AEA  rep  movsb [register],[esp] (04 bytes)  ; shr ebp,5


0034  00BB40AB:01450073  mov  register,0A9C82A3


0035  00BB40B9:0148771E  mov  register,addr_of_context.eax

0036  00BB40C7:0150C5DD  push  dword ptr [register]

0037  00BB40D5:0148771E  mov  register,addr_of_context.edi

0038  00BB40E3:0150C5DD  push  dword ptr [register]

0039  00BB40F1:014FF38F             ; 未实现

0040  00BB40FF:0148771E  mov  register,addr_of_context.eax

0041  00BB410D:014A1AEA  rep  movsb [register],[esp] (04 bytes)


0042  00BB411B:01450073  mov  register,03CDA03D


0043  00BB4129:0148771E  mov  register,addr_of_context.ecx

0044  00BB4137:0150C5DD  push  dword ptr [register]

0045  00BB4145:0148771E  mov  register,addr_of_context.eax

0046  00BB4153:014A1AEA  rep  movsb [register],[esp] (01 bytes)  ; mov eax,ecx


0047  00BB4161:0148771E  mov  register,context.esp

0048  00BB416F:0150C5DD  push  dword ptr [register]

0049  00BB417D:0148771E  mov  register,addr_of_context.eax

0050  00BB418B:014A1AEA  rep  movsb [register],[esp] (04 bytes)  ; mov eax,[esp]


0051  00BB4199:0148771E  mov  register,context.esp

0052  00BB41A7:0152C47F              ; 未实现,可以猜测是add esp,4

0053  00BB41B5:0150C5DD  push  register    

0054  00BB41C3:014A1AEA  rep  movsb context.esp,[esp] (04 bytes)    


0055  00BB41D1:01450073  mov  register,0BA18350


0056  00BB41DF:0148771E  mov  register,addr_of_context.ebp

0057  00BB41ED:0150C5DD  push  dword ptr [register]

0058  00BB41FB:0150C5DD  push  00000005

0059  00BB4209:014F7A55              ; 未实现

0060  00BB4217:014A1AEA  rep  movsb [register],[esp] (04 bytes)

0061  00BB4225:01461D1F 


0062  00BB4233:0148771E  mov  register,context.esp

0063  00BB4241:0150C5DD  push  dword ptr [register]

0064  00BB424F:0148771E  mov  register,addr_of_context.ebp

0065  00BB425D:014A1AEA  rep  movsb [register],[esp] (04 bytes)

0066  00BB426B:0148771E  mov  register,context.esp      ; 可以看到mov ebp,esp在这里


结论是,被转换为pcode方式运行的代码,事先被混淆过了。再用这种手工式
的修复方法,已经不可行了。

1行真正的汇编指令,大约对应10行pcode代码,每条pcode执行时,对应的汇编
指令一般超出2000行,真正有点疯狂呵呵。