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余处。留给有坚强意志的朋友完成吧。