在开始讲述handler的执行过程之前,有必要了解VM中用到的2个重要数据结构,即VM_Context和PCODE数据。在手工还原代码时,熟悉这些数据结构尤其重要。我修复代码基本没有用到调试器,是用IDA静态分析完成的。
执行进入VM的准备代码时,将edi置为VM_Context的地址,并将VM_Context+0的DWORD置为第1条指令对应的PCODE数据地址(来自PUSH/JMP下的数据)。进入handler后将该地址值赋给esi,在handler执行过程中,esi/edi始终不变,用来访问这2个结构(变形代码使用这2个寄存器会用push/pop加以保护)。
VM_Context__________________________________________________________
有些offset对应的地址handler没有使用(我没看见?J),有的是为了对齐。下表只列出handler明确使用的field。
Offset |
Size |
Name |
Description |
00 |
dword |
pcode_data |
pcode数据地址 |
08 |
byte |
Key3 |
解码key3 |
|
byte |
next_opcode1 |
下一条指令的opcode1 |
10 |
byte |
Key4 |
解码key4 |
14 |
byte |
next_opcode2 |
下一条指令的opcode2 |
18 |
byte |
Key2 |
解码key2 |
20 |
byte |
Key1 |
解码key1 |
24 |
byte |
flag |
影响handler执行方式的标记,根据pcode数据设置 |
28 |
byte |
busy |
VM忙闲标记 |
30 |
dword |
AddrOfRegister |
VM_Context内某个通用寄存器的地址,调用pfnGetRegister的返回值,见+ |
34 |
dword |
register |
可以看作VM自己的唯一一个寄存器 |
38 |
dword |
rotated_bytes |
VM_Context内通用寄存器环滚动字节数 |
|
dword |
addr_of_regs_ring |
通用寄存器环起始地址,这个地址等于VM_Context基地址+74,即VM_Context内eax的地址 |
40 |
dword |
2-bytes-opcode-table |
即前文的2-Bytes-Opcode Table1地址 |
44 |
dword |
1-byte-opcode_table |
1-Byte-Opcode
Table地址 |
48 |
dword |
Jmp_flag |
跳转指令是否执行跳转的标记 |
|
dword |
pcode_beginning |
VM保护代码对应pcode数据起始地址,即PUSH/JMP下第1个dword+DeltaOffset |
50 |
dword |
pcode_endding |
Pcode数据结束地址,等于+ |
54 |
dword |
DeltaOffset |
J |
58 |
dword |
vm_entry |
指向1字节opcode的handler搜索代码(即使用pcode+0的低7位定位handler的代码),当执行跳转指令,控制转移到目的地址时也用这里的代码 |
|
dword |
pfnGetRegister |
这里保存的是个函数地址,用于获取VM_Context内某个通用寄存器的当前地址(这些寄存器随机滚动,Themida自己也需要一个方便获取register地址的方式),参数为寄存器在VM_Context内的offset(如74h->eax),返回值保存在+30处 |
60 |
dword |
null_const_1 |
这2个dword含义不明,在一定条件下用于调整解码出的pcode数据,由于这2个dword总为0, 似乎并没有实际的作用 |
64 |
dword |
null_const_2 |
|
68 |
dword |
addr_of_pcode_info |
指向PUSH/JMP下pcode相关数据的起始地址 |
|
dword |
num_of_pcode_items |
PUSH/JMP下数据项数(不含非模仿指令) |
70 |
dword |
eflag |
从这里开始为原执行环境数据 |
74 |
dword |
eax |
这里(包括cs前的空间)即所谓的通用寄存器闭合环,在handler连续执行的过程中会随机滚动,调试时不容易看清代码究竟要访问哪一个寄存器。 这对解码程序不存在干扰,pcode解码数据不包含滚动因素。当访问某个通用寄存器时,提供的参数为偏移量数据,该值减F0000000后代表闭合环内的offset,如F0000000 –>eax,F0000008->ebx |
|
dword |
ebx |
|
84 |
dword |
ecx |
|
|
dword |
edx |
|
94 |
dword |
esi |
|
|
dword |
edi |
|
A4 |
dword |
ebp |
|
AC |
dword |
esp |
|
E4 |
word |
cs |
|
E6 |
word |
ds |
|
E8 |
word |
es |
|
EA |
word |
ss |
|
有些field虽然size为byte,但常用32位方式访问,只用了最低字节,如几个key。另外,对key的编号只是我的习惯,没什么特殊意义。这些field后面的空间是实现寄存器滚动用的buffer。再后面即搜索1字节opcode对应handler的代码。
VM_Context内各成员的offset和含义是固定的(除寄存器滚动部分)。
PCODE数据__________________________________________________________
PCODE数据的长度是固定的,为14字节(并没有用满)。每条指令都有自己的PCODE数据,所以也可以把PCODE数据的地址看作VM的EIP。如果与x86指令类比,PCODE数据相当于opcode以外的部分。
Name |
Size |
Decode Key |
Description |
Init_keys |
1 byte |
|
这个field始终位于+0处,若b7为1,4个key使用前先清0。对于1字节opcode,低7位即opcode值,对2字节opcode,低7位未用 |
InitKey1_and _NumOfRotate |
1 byte |
|
b0,b1决定如何变换Key1: 0->
key1 ^= seed_xor(加壳时生成的imm8) 1->
Key1 += seed_add 2->
key1 ^= pcode[0] 3->
key1 += pcode[0] b2,b3,b4 3位对应值代表VM_Context内寄 存器环应如何滚动,为0不滚动,否则滚动字节数为该3位对应值 * 2 |
Argument |
4 bytes |
Key1 |
handler的参数,具体含义与handler相关 解码方式以伪码表示为: for(int
i = 0; i < 4; i++) { _asm ror Argument, 4 if(Key1 > 0x80) Argument ^= Key1; else Argument += Key1; } 与Key1的运算用最低字节 |
Opcode
detail |
2-4 bytes |
Key1 |
这个field我不知取什么名字好,暂且这样。不同类型的handler使用的字节数不同,位置也可以不连续,划到一起是因为其作用。每个字节的解码方式为: data
=( (data ^ Key1) + seed_add) + Key1 |
Next_pcode |
1 byte |
Key3 |
下条指令的pcode地址,解码方式为: data
= ((data ^ key3) ^ seed_xor) – seed_sub key3用data原值更新: key3
^= old_data |
Next_opcode1 |
1 byte |
Key4 |
下条指令的opcode1,解码方式为: data
= ((data ^ key4) ^ seed_xor) – seed_sub key4用data原值更新: key4
^= old_data |
Next_opcode2 |
1 byte |
Key2 |
下条指令的opcode2,解码方式为: data
= ((data ^ key2) ^ seed_xor) – seed_sub key2用data原值更新: key2
^= old_data |
表内所有的seed_xxx,均为加壳时生成的随机字节值,各不相同。对1字节opcode表内的handler,其使用的pcode数据内各field的offset及含义是固定的。2字节opcode表内的handler(与1字节opcode表内数据重叠的除外)各field在PCODE结构内的offset可变,所以这里没有列出各field的偏移量。还原代码时,熟悉PCODE的各field用哪一个key解码很重要,否则难以识别代码究竟在处理什么数据。