ThemidaVMv1.1.1.0v1.8.0.0基本没有大的变化。从文档看新版本支持更多的指令集,不过在逆向的过程中我并没有看出来,也许是只动过Demo版本的原因。原来准备看注册版的,恰好变形代码部分增强,又放下了。

前几次手工修复代码后,累得失去了写文章的兴趣,这次是文章先行。目前我写不出通用的解码程序,所以仅仅是篇文章。截图是调试时做的,有的图上的注释可能不够准确,没有再一一修改。细节太多,错误难免。另外,以前零零散散写过一些东西,难免会有炒冷饭的部分,见谅J

下面的所有讨论都针对Themida v1.8.0.0 Demo

Virtual Machine入口代码

下面以Themida v1.8.0.0 Demo自己的stolen codes执行过程为例。

 

Dump点的代码如图。连续的PUSH/JMP代码,JMP的目的地址相同。代码下面是这块被保护代码(stolen codes)对应 PCODE数据地址信息。

2DWORD有自己的含义。后面每3DWORD1,对应上面的1PUSH/JMP指令对,具体含义后面解释。以FFFFFFFFh结束。可以看到,PUSHDWORD就是对应组内的第1DWORD

跟进loc_BD8AC2

这部分代码是stolen codes专有的,每处用VM保护的代码都有自己的1份。在VM_Context内保存了2个值, PCODE数据表地址和这块代码内包含的PUSH/JMP指令对数。ret到公用的context保存代码。

 

VM_Context内保存进入VM前的环境。

 

 

 

现在再来看看Dump点的代码和数据。

 

首先,为什么会有多个PUSH/JMP? VM对代码的保护是“平面”的,如果被保护代码中包含有call,Themida不会将被调用函数的代码也纳入VM保护。执行到该处时,控制退出VM。执行完返回时,需要用PUSH/JMP重新进入VMstolen codes对应的代码出现了8PUSH/JMP指令对,意味着这段代码内存在7call(8次进入VM,退出VM时控制返回到OEPstolens codes结束后的代码地址)

 

相应的,每次进入VM,需要指定对应的PCode数据(可以看作PCode的指令指针),即整个stolen codes对应的PCode数据也被分成8部分。PUSH/JMPPUSH的值实际是个key,用来在下面的数据表中搜索对应的PCode地址。

 

Delta Offset加到地址数据上可以看得更清楚。

 

上面数据组内的第3项代表了PUSH/JMPPUSH指令的地址。实际上,前面的描述不够准确,这里不一定是PUSH Imm32。看看下面这段代码(来自Code Virtualizer Demo 1.0.1.0)

注意最后的3组。PUSH/JMP前出现了别的指令。

FILD DWORD PTR SS:[EBP-230]

FSTP TBYTE PTR SS:[EBP-20]

WAIT

 

这些是Themida VM不能模仿的指令。此时上述第3dword指向非模仿指令的地址。VM_Context+0x6C处的dword代表可模仿指令对应的数据项数。这种情况下,该值小于PUSH/JMP下面的实际数据项数。

 

被保护代码中每出现1VM不能模仿的指令,就会生成1组这样的指令。执行时退出VM,执行非模仿指令,再重新进入VM。不过这些细节暂时不必理会。后面会详细解释handler的类型与执行细节。

 

VM的内存结构_________________________________________________________

 

在较早的版本中,VM代码保存在多块内存中。大约从1.3.0.0开始,代码合并到了1块内存。VM6部分组成。下面为注入的tracer(hi shooooJ)VirtualAlloc的拦截记录(实际分配时会按页对齐)Dump后补这6个区段就可以了(Virtual Machine Anti-Dumper会导致运行失败)

 

 

VM Stack                                                                           

VM使用的8KB stack

 

VM 2-Bytes-Opcode Table1                                                           

这是张地址表,指针地址是连续的(这里了和截图不同的名字)

 

VM 2-Bytes-Opcode Table2                                                           

Handler地址表,3个地址为1,被上面表内某项所指。非0值代表1handler地址,158项。

 

VM 1-Byte-Opcode Table                                                          

1Handler地址表,62项。用PUSH/JMP进入 VM,1handler地址是从这张表查的。反过来,158Handler地址数据中,除去这62项外的其余Handler永远不会在VM入口使用。注意这张表内所有的handler地址,在上面包含158个非0值的表内都有。

进入VM时的查表代码。

PCode数据第1字节,其低7位用来查VM 1-Byte-Opcode Table

 

VM Codes                                                                             

虚拟机handler代码。

 

VM Context and Stub Codes                                                         

全局的VM_Context及上图的进入VM查表代码。

 

 

VM OPCODE_______________________________________________________

 

为了理解opcode,先复习一下Intel指令格式。以下内容Copy<The Art of Disassembly>

 

 

文本框: We said that the processor has a decoding table, that means it requires a signature to be decoded. We can think about the [CODE] block as this signature,that tells the processor exactly what instruction to execute, and what kinds of rules should the instruction have.

 

 

 

唯一的非可选项为Opcode

Opcode的本质是处理器用来查解码表的索引。通过查表,才能获知究竟要执行什么操作,需要什么操作数。实际的操作取决于处理器对查表结果的解释。再来看Thmida VM中出现的3张表。分两种情况。

 

进入VM时的查表动作,使用1-Byte-Opcode Table                                   

 

PCode数据第1字节的低7,用作查1-Byte-Opcode表的索引。该索引值就是1字节的opcode编码。7位的可取值范围为0-127(当前该表有62项数据,0-61)。当执行的VM指令为JMP/Jxx,如果要执行跳转操作,也会使用1字节opcode,用前面提到的查表代码进入对应的handler

 

VM内连续执行PCode,使用2-Bytes-Opcode Table                               

 

VM内连续执行时,会从PCode数据内解出2个字节值,用于定位执行下一条pcode指令的handlerByte1用来查2-Bytes-Opcode Table1。假设为0,取第1项。

 

地址数据为CF0000。指向2-Bytes-Opcode Table2内某项。

 

 

假设Byte21,定位到CF00003dword的第2,handler地址。2byte 实际就是2字节opcode编码(00 01)

 

1张表当前有63,opcode1取值为0-62。第2张表内的dword3个为1,只能为0-2,opcode2取值为0-2。地址值为0的数据项可能代表保留的opcode编码。

 

注意,1字节opcode表内所有的handler地址,2字节opcode表内都存在,即所有的1字节opcode都与某个2字节opcode重叠,指向相同的handler。看起来1字节opcode完全是多余的,为什么不只使用2字节opcode?

 

为了提高执行效率,Themida在每个handler内包含了分发代码, pcode数据内解码出下一条指令的opcode,查表得到对应的handler地址,jmp esi跳过去继续执行。这样可避免用一个巨大的switch-case语句来判断opcode值。这种技巧称为Threaded Interpretation

 

当前执行指令对应的pcode数据内包含有下一条指令的opcode(2 bytes)pcode数据地址(1 byte)

 

进入VM,1条指令的pcode数据内包含有下一条指令的2字节opcode数据。第1条指令自己的opcodepcode+0字节的低7(pcode数据的地址来自PUSH/JMP下的对应数据,由前面图中的准备代码保存到VM_Context+0)。即这项pcode数据包含了2条指令的opcode信息。如果只用2字节编码,显然pcode数据将占用更多的空间。我的理解是使用1字节编码的目的是节省空间。

 

另外,1字节opcode实际上涵盖了VM的全部指令,所有的2字节opcode都与某个1字节opcode重叠。这个后面再讲。