在开始讲述handler的执行过程之前,有必要了解VM中用到的2个重要数据结构,VM_ContextPCODE数据。在手工还原代码时,熟悉这些数据结构尤其重要。我修复代码基本没有用到调试器,是用IDA静态分析完成的。

 

执行进入VM的准备代码时,edi置为VM_Context的地址,并将VM_Context+0DWORD置为第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

0C

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的返回值,+5C

34

dword

register

可以看作VM自己的唯一一个寄存器

38

dword

rotated_bytes

 VM_Context内通用寄存器环滚动字节数

3C

dword

addr_of_regs_ring

通用寄存器环起始地址,这个地址等于VM_Context基地址+74,VM_Contexteax的地址

40

dword

2-bytes-opcode-table

即前文的2-Bytes-Opcode Table1地址

44

dword

1-byte-opcode_table

1-Byte-Opcode Table地址

48

dword

Jmp_flag

跳转指令是否执行跳转的标记

4C

dword

pcode_beginning

VM保护代码对应pcode数据起始地址,PUSH/JMP下第1dword+DeltaOffset

50

dword

pcode_endding

Pcode数据结束地址,等于+4C的值加PUSH/JMP下第2dword(pcode data size)

54

dword

DeltaOffset

J

58

dword

vm_entry

指向1字节opcodehandler搜索代码(即使用pcode+0的低7位定位handler的代码),当执行跳转指令,控制转移到目的地址时也用这里的代码

5C

dword

pfnGetRegister

这里保存的是个函数地址,用于获取VM_Context内某个通用寄存器的当前地址(这些寄存器随机滚动,Themida自己也需要一个方便获取register地址的方式),参数为寄存器在VM_Context内的offset(74h->eax),返回值保存在+30

60

dword

null_const_1

2dword含义不明,在一定条件下用于调整解码出的pcode数据,由于这2dword总为0, 似乎并没有实际的作用

64

dword

null_const_2

68

dword

addr_of_pcode_info

指向PUSH/JMPpcode相关数据的起始地址

6C

dword

num_of_pcode_items

PUSH/JMP下数据项数(不含非模仿指令)

70

dword

eflag

从这里开始为原执行环境数据

74

dword

eax

这里(包括cs前的空间)即所谓的通用寄存器闭合环,handler连续执行的过程中会随机滚动,调试时不容易看清代码究竟要访问哪一个寄存器。

 

这对解码程序不存在干扰,pcode解码数据不包含滚动因素。当访问某个通用寄存器时,提供的参数为偏移量数据,该值减F0000000后代表闭合环内的offset,F0000000 –>eax,F0000008->ebx

7C

dword

ebx

84

dword

ecx

8C

dword

edx

94

dword

esi

9C

dword

edi

A4

dword

ebp

AC

dword

esp

E4

word

cs

 

E6

word

ds

 

E8

word

es

 

EA

word

ss

 

 

有些field虽然sizebyte,但常用32位方式访问,只用了最低字节,如几个key。另外,key的编号只是我的习惯,没什么特殊意义。这些field后面的空间是实现寄存器滚动用的buffer。再后面即搜索1字节opcode对应handler的代码。

 

VM_Context内各成员的offset和含义是固定的(除寄存器滚动部分)

 

PCODE数据__________________________________________________________

 

PCODE数据的长度是固定的,14字节(并没有用满)。每条指令都有自己的PCODE数据,所以也可以把PCODE数据的地址看作VMEIP。如果与x86指令类比,PCODE数据相当于opcode以外的部分。

 

Name

Size

Decode Key

Description

Init_keys

1 byte

 

这个field始终位于+0,b71,4key使用前先清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

 

key3data原值更新:

key3 ^= old_data

Next_opcode1

1 byte

Key4

下条指令的opcode1,解码方式为:

data = ((data ^ key4) ^ seed_xor) – seed_sub

 

key4data原值更新:

key4 ^= old_data

Next_opcode2

1 byte

Key2

下条指令的opcode2,解码方式为:

data = ((data ^ key2) ^ seed_xor) – seed_sub

 

key2data原值更新:

key2 ^= old_data

 

表内所有的seed_xxx,均为加壳时生成的随机字节值,各不相同。对1字节opcode表内的handler,其使用的pcode数据内各fieldoffset及含义是固定的。2字节opcode表内的handler(1字节opcode表内数据重叠的除外)fieldPCODE结构内的offset可变,所以这里没有列出各field的偏移量。还原代码时,熟悉PCODE的各field用哪一个key解码很重要,否则难以识别代码究竟在处理什么数据。