Handler Type 3

这是最复杂的1,1字节opcode中只有1个。其功能为控制转移,将其命名为Vm_Jxx不是很确切,这个指令可实现JMP/Jxx/Call的功能。使用的OpcodeDetail数据为4字节。

还是先看代码。这种handler没有显式地为flag赋初值,而是直接初始化自己感兴趣的位。

解码4个字节。若byte1b71,设置flag.b4位。若byte4b61,设置flag.b5(代表这是个call)byte4的低4位值若为2,设置flag.b1

解出的4个字节合并为1dword(其中byte4丢弃了b7)

b7

b6

b5

b4

b3

b2

b1

b0

 

 

Call

Unknown

 

 

Unknown

 

 

这是handler type3flag的解释。遗憾的是一共出现了3个标记位,却有2个位不清楚其含义。不过在手工还原代码的过程中这2位没有用上,后面会列出使用了flag.b1flag.b4的代码。

 

下面与Type 1类似,是根据byte4的低3switch-case分枝。这里也会在VM栈压入NewArgument(就是控制转移的dst)

 

Case 0      

                                                                    

在进入handlervm栈内至少有前面handler执行压入的2dword。到这里的mov,[esp]OldArgument。即NewArgument = 进入handler时的[esp+4]。我没有碰到过这种情况,猜测是在模仿ret指令。

 

Case 1       

                                                                   

NewArgument = OldArgument,即用Key1解码出的imm32。如果写解码程序,这是唯一能确定dst真实地址的情况。

Case 2            

                                                               

 

if(VM_Context.register > 80000000h)

NewArgument = VM_Context.register

else

NewArgument = [VM_Context.register]

 

Case 3                                   

                                       

NewArgument = VM_Context.register

 

byte4包含跳转dstflag标记位设置的信息。下面检测解码结果dword的高24(byte1-byte3组成)以确定控制转移类型。为了方便称这个dworddwJmpType

预置VM_Context.jmp_flag标记为1,handler最终会检查这个值,只有非0才真正发生控制转移,否则会jmp esi继续执行下一条指令。dwJmpType的高24位若为0,不再执行后续判断,这样必然执行跳转。即dwJmpType & 0xFFFFFF00 == 0 -> JMP

 

先看第1段检测代码。

edx初始化为0,用于保存检测结果。每段检测若符合跳转条件,会将edx的对应位置1。最后将edx赋给VM_Context.jmp_flag以决定是否执行跳转。esi为计数器,每执行一段检测代码加1(无论检测结果如何,edx对应位是否被置1)

 

检测dwJmpTypeb9,若为0则跳过本段检测代码,意思是当前操作对下面几行代码所检测的VM_Context.eflag位不感兴趣J。否则取b8并移动到b0位。

 

VM_Context.eflagb0,也移动到b0位置(程序生成的代码J)eflagb0位为CF

此时dwJmpTypeVM_Context.eflag的被检测位都在b0位置了。然后XOR/NOT/AND

 

如果ebx等于ecx,andebx1,会执行跳转。

 

已经够清楚了JdwJmpTypeb91,表示要用b8检测VM_Context.eflagCF位。b81代表JC,0JNC。是否真正发生跳转要看VM_Context.eflag对应位的取值。

 

dwJmpType的高24位中用于检测跳转类型的数据基本是这样两位1组使用的。

 

下面列出后续检测代码。

JZ/JNZ

 

JD/JND,并没有对应的x86指令。测试这个标记是用于判断在模仿串操作指令(movsb,lodsb…)

ESI/EDI应递增还是递减。从shr ecx,0Ah可以看出代码感兴趣的是eflagb10DF位。

 

注意and ecx,4。下面是原始代码(这里并没有清理错J)

这导致用来比较的DF位始终为0,即串操作指令永远不会按ESI/EDI递减的方式执行,这应该是Themidabug,正确值为400h。如果有兴趣,可以用VM保护设置DF位后执行串操作指令的代码测试一下。

 

JS/JNS

 

JO/JNO

 

JP/JNP

 

JL/JGE。这里用了eflag2个标记位。

 

JCXZ。这里使用了VM_Context.pfnGetRegister函数,参数为寄存器在VM_Context内的偏移。

 

 

返回值为VM_Context.ecx的实际地址,保存在VM_Context.dwAddrOfRegister。当cx值为0时直接将edx1了。

 

JECXZecx0直接将edx1。注意JCXZJECXZ没有增加计数器esi的值。

 

下面的这几行代码我没有看明白L

 

先看标记检测。解码byte4.b4位若为0,将结果保存到VM_Context.JmpFlag,结束跳转类型判断。

 

一般情况下,跳转类型应为上述9(16)中的1,即如果对eflag的检测符合跳转条件,edx中的结果值只有一位被置1(由于edx每次左移了1,这个被置1的位应在edxb1-b9范围内),计数器esi应为1。例如,若跳转指令为JC/JNC,且符合跳转条件,edxb1=1,esi=1

 

但检测代码并非互斥的switch-case,而是连续的if语句。若dwJmpType内的数据符合多组判断条件,即有多段检测代码被执行,edx内可能有多个位被置1,计数器esi>1。什么样的跳转指令会造成这样的结果? 可以看到上述的跳转类型确实不足以覆盖x86的全部Jxx指令(但所有Jxx指令可能检测的eflag标记位这里都出现过了)。另外,即使检测eflag同样的位,也可解释为不同的指令。

 

2个指令检测的是同1位。如何解释还需要看代码的上下文(虽然其效果是一样的)

 

猜测byte4的这个标记代表了9组之外的其它一些跳转类型。当byte4.b41,没有将edx赋给VM_Context.jmp_flag,而是跳到了1544C8D

 

这里对计数器esi的使用很奇特。esi的值代表执行了几段检测代码(dwJmpType可以确切判断出检测了VM_Context.eflag的哪些位,这里并没有进行这样的判断)

 

假设esi=2,代表执行了2段检测。仅从这个值并不知道执行的是哪2段。

这几行的执行结果是esi == 11b

 

 

edx右移1(每次保存结果后左移过1),esi==edx,则结束跳转类型判断。因VM_Context.jmp_flag预置为1,此时会发生跳转。否则VM_Context.jmp_flag0,不跳转。

 

虽然不知道esi是因为执行哪2段得到结果值2,edx要等于11b,根据其移位操作,可以明确地判定为最开始的2组检查,即检测了VM_Context.eflagCF,ZF位。

 

再假设dwJmpType内用来与eflag对应位XOR操作的位取值为0,eflagCF,ZF0导致edx=11b,则这里的跳转类型应该是:

 

 

对这个问题的讨论到此为止。我没有看清楚,也许仔细分析x86Jxx指令对eflag标记位的使用会更明白。一般情况下前面的9组指令足够了。Themida的作者够CrazyJ

 

下表列出dwJmpType24位含义。

TRUE

FALSE

b8

JC

JNC

b9

是否用b8测试VM_Context.eflagCF

b10

JZ

JNZ

b11

是否用b10测试VM_Context.eflagZF

b12

JS

JNS

b13

是否用b12测试VM_Context.eflagSF

b14

JO

JNO

b15

是否用b14测试VM_Context.eflagOF

b16

JP

JNP

b17

是否用b16测试VM_Context.eflagPF

b18

JL

JGE

b19

是否用b18测试VM_Context.eflagSF,OF

JGE-> SF=OF,JL-> SF != OF,这里要检测2个位

b20

未用

 

b21

JCXZ

 

b22

未用

 

b23

JECXZ

 

b24

JD

JND

b25

是否用b24测试VM_Context.eflagDF,这并没有对应的x86指令,VM测试这个位用于判断模仿串操作指令(movsb,lodsb…),ESI/EDI应递增还是递减

 

 

下面准备执行跳转操作。

VM_Context.JmpFlag值为0,不发生跳转,继续执行下一条PCODE指令。否则检测NewArgument,解析跳转的dst

 

80000000h <= NewArgument  < 80055730h,将该值减80000000作为offset,加当前VM保护代码对应PCODE数据起始地址,得到1个新的PCODE数据地址,保存到VM_Context.pcode_data,跳转到1字节opcode handler搜索代码(1字节opcode保存在PCODE.Init_Keys域的低7)。这是个pcode跳转,即跳转的dst仍然是VM保护代码。

 

对不同的被加壳程序,这里的地址上限值不同。注意55730h这个值相当大(看看oepVM保护代码PCODE数据的size)

猜测这种跳转的dst位于另外1VM保护代码内。

 

2个地址范围检查,NewArgument落到当前PCode数据地址范围内,则是在同一VM保护代码块内的PCODE间跳转。同样将dst对应PCODE数据地址保存到VM_Context.pcode_data,跳转到1字节opcode解析代码。

 

显然,控制转移的dst对应PCODE数据,Init_keys域必须包含1字节opcode。执行dsthandler,各个Key会重新清0

 

Argument没有落在上述2个地址范围内,跳转的dst不是被VM保护的代码,而是原程序中的真实地址。若VM_Context+60hdword0,检测flagb1,b4,条件满足用 VM_Context+60h, VM_Context+64h2dword调整地址值(操作和Type1中对OldArgument的调整完全相同)

 

由于这2dword总为0,1JZ就跳走了,不清楚flag.b1flag.b4的确切含义。

 

 

接下来检测flag.Call标记。若为0,表示该控制转移不是个CALL调用,跳到1544D3F。如果为Call,下面是个循环。搜索当前指令的PCODE数据地址落在PCODE数据地址表(PUSH/JMP下的数据)中哪1个数据项的地址范围,找到后取其下一个数据项的第3dword,加上DeltaOffset作为CALL的返回地址,写入原程序栈。

 

先列出1544D16JNZdst代码。

这段代码看起来比较复杂,1张图来说明就直观了。

执行这句add,ebp即返回地址。注意接下来将原程序的esp指针加了4。这是因为若控制转移为CALL,前面执行的handler在原程序的栈内先压了1dword,这里要平衡栈指针。

 

先看一段解码的数据。

 

 

其对应的原始代码为:

 

可以看到,CALL前原程序栈内压入了一个dword。如果是JMP/Jxx不会这样。不知道Themida为什么这么做。

 

下面是CALLJMP/Jxx的公用代码。

切换堆栈。如果是CALL(ebp0),将返回地址压入原程序栈。

 

VM_Context内通用寄存器恢复到原位置。恢复执行环境,退出VM