Handler
Type 3
这是最复杂的1种,在1字节opcode中只有1个。其功能为控制转移,将其命名为Vm_Jxx不是很确切,这个指令可实现JMP/Jxx/Call的功能。使用的OpcodeDetail数据为4字节。
还是先看代码。这种handler没有显式地为flag赋初值,而是直接初始化自己感兴趣的位。
解码4个字节。若byte1的b7为1,设置flag.b4位。若byte4的b6为1,设置flag.b5(代表这是个call)。byte4的低4位值若为2,设置flag.b1。
解出的4个字节合并为1个dword(其中byte4丢弃了b7)。
b7 |
b6 |
b5 |
b4 |
b3 |
b2 |
b1 |
b0 |
|
|
Call |
Unknown |
|
|
Unknown |
|
这是handler type3对flag的解释。遗憾的是一共出现了3个标记位,却有2个位不清楚其含义。不过在手工还原代码的过程中这2位没有用上,后面会列出使用了flag.b1和flag.b4的代码。
下面与Type 1类似,是根据byte4的低3位switch-case分枝。这里也会在VM栈压入NewArgument(就是控制转移的dst)。
Case 0
在进入handler时vm栈内至少有前面handler执行压入的2个dword。到这里的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包含跳转dst及flag标记位设置的信息。下面检测解码结果dword的高24位(由byte1-byte3组成)以确定控制转移类型。为了方便称这个dword为dwJmpType。
预置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)。
检测dwJmpType的b9位,若为0则跳过本段检测代码,意思是当前操作对下面几行代码所检测的VM_Context.eflag位不感兴趣J。否则取b8并移动到b0位。
取VM_Context.eflag的b0位,也移动到b0位置(程序生成的代码J)。eflag的b0位为CF。
此时dwJmpType和VM_Context.eflag的被检测位都在b0位置了。然后XOR/NOT/AND。
如果ebx等于ecx,and后ebx为1,会执行跳转。
已经够清楚了J。dwJmpType的b9为1,表示要用b8检测VM_Context.eflag的CF位。b8为1代表JC,为0是JNC。是否真正发生跳转要看VM_Context.eflag对应位的取值。
dwJmpType的高24位中用于检测跳转类型的数据基本是这样两位1组使用的。
下面列出后续检测代码。
JZ/JNZ。
JD/JND,并没有对应的x86指令。测试这个标记是用于判断在模仿串操作指令(movsb,lodsb…)时
ESI/EDI应递增还是递减。从shr ecx,0Ah可以看出代码感兴趣的是eflag的b10即DF位。
注意and ecx,4。下面是原始代码(这里并没有清理错J)。
这导致用来比较的DF位始终为0,即串操作指令永远不会按ESI/EDI递减的方式执行,这应该是Themida的bug,正确值为400h。如果有兴趣,可以用VM保护设置DF位后执行串操作指令的代码测试一下。
JS/JNS。
JO/JNO。
JP/JNP。
JL/JGE。这里用了eflag的2个标记位。
JCXZ。这里使用了VM_Context.pfnGetRegister函数,参数为寄存器在VM_Context内的偏移。
返回值为VM_Context.ecx的实际地址,保存在VM_Context.dwAddrOfRegister。当cx值为0时直接将edx置1了。
JECXZ。ecx为0直接将edx置1。注意JCXZ和JECXZ没有增加计数器esi的值。
下面的这几行代码我没有看明白L。
先看标记检测。解码byte4.b4位若为0,将结果保存到VM_Context.JmpFlag,结束跳转类型判断。
一般情况下,跳转类型应为上述9组(16个)中的1种,即如果对eflag的检测符合跳转条件,edx中的结果值只有一位被置1(由于edx每次左移了1位,这个被置1的位应在edx的b1-b9范围内),计数器esi应为1。例如,若跳转指令为JC/JNC,且符合跳转条件,则edx的b1=1,esi=1。
但检测代码并非互斥的switch-case,而是连续的if语句。若dwJmpType内的数据符合多组判断条件,即有多段检测代码被执行,则edx内可能有多个位被置1,计数器esi>1。什么样的跳转指令会造成这样的结果? 可以看到上述的跳转类型确实不足以覆盖x86的全部Jxx指令(但所有Jxx指令可能检测的eflag标记位这里都出现过了)。另外,即使检测eflag同样的位,也可解释为不同的指令。
这2个指令检测的是同1位。如何解释还需要看代码的上下文(虽然其效果是一样的)。
猜测byte4的这个标记代表了9组之外的其它一些跳转类型。当byte4.b4为1时,没有将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_flag清0,不跳转。
虽然不知道esi是因为执行哪2段得到结果值2的,edx要等于11b,根据其移位操作,可以明确地判定为最开始的2组检查,即检测了VM_Context.eflag的CF,ZF位。
再假设dwJmpType内用来与eflag对应位XOR操作的位取值为0,即eflag的CF,ZF为0导致edx=11b,则这里的跳转类型应该是:
对这个问题的讨论到此为止。我没有看清楚,也许仔细分析x86的Jxx指令对eflag标记位的使用会更明白。一般情况下前面的9组指令足够了。Themida的作者够CrazyJ。
下表列出dwJmpType高24位含义。
位 |
TRUE |
FALSE |
b8 |
JC |
JNC |
b9 |
是否用b8测试VM_Context.eflag的CF位 |
|
b10 |
JZ |
JNZ |
b11 |
是否用b10测试VM_Context.eflag的ZF位 |
|
b12 |
JS |
JNS |
b13 |
是否用b12测试VM_Context.eflag的SF位 |
|
b14 |
JO |
JNO |
b15 |
是否用b14测试VM_Context.eflag的OF位 |
|
b16 |
JP |
JNP |
b17 |
是否用b16测试VM_Context.eflag的PF位 |
|
b18 |
JL |
JGE |
b19 |
是否用b18测试VM_Context.eflag的SF,OF位 JGE->
SF=OF,JL-> SF != OF,这里要检测2个位 |
|
b20 |
未用 |
|
b21 |
JCXZ |
|
b22 |
未用 |
|
b23 |
JECXZ |
|
b24 |
JD |
JND |
b25 |
是否用b24测试VM_Context.eflag的DF,这并没有对应的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这个值相当大(看看oep处VM保护代码PCODE数据的size)。
猜测这种跳转的dst位于另外1块VM保护代码内。
第2个地址范围检查,若NewArgument落到当前PCode数据地址范围内,则是在同一VM保护代码块内的PCODE间跳转。同样将dst对应PCODE数据地址保存到VM_Context.pcode_data,跳转到1字节opcode解析代码。
显然,控制转移的dst对应PCODE数据,其Init_keys域必须包含1字节opcode。执行dst的handler时,各个Key会重新清0。
若Argument没有落在上述2个地址范围内,跳转的dst不是被VM保护的代码,而是原程序中的真实地址。若VM_Context+60h的dword非0,检测flag的b1,b4,条件满足用 VM_Context+60h, VM_Context+64h的2个dword调整地址值(操作和Type1中对OldArgument的调整完全相同)。
由于这2个dword总为0,第1个JZ就跳走了,不清楚flag.b1和flag.b4的确切含义。
接下来检测flag.Call标记。若为0,表示该控制转移不是个CALL调用,跳到1544D3F。如果为Call,下面是个循环。搜索当前指令的PCODE数据地址落在PCODE数据地址表(即PUSH/JMP下的数据)中哪1个数据项的地址范围,找到后取其下一个数据项的第3个dword,加上DeltaOffset作为CALL的返回地址,写入原程序栈。
先列出1544D16处JNZ的dst代码。
这段代码看起来比较复杂,用1张图来说明就直观了。
执行这句add后,ebp即返回地址。注意接下来将原程序的esp指针加了4。这是因为若控制转移为CALL,前面执行的handler在原程序的栈内先压了1个dword,这里要平衡栈指针。
先看一段解码的数据。
其对应的原始代码为:
可以看到,在CALL前原程序栈内压入了一个dword。如果是JMP/Jxx不会这样。不知道Themida为什么这么做。
下面是CALL与JMP/Jxx的公用代码。
切换堆栈。如果是CALL(ebp非0),将返回地址压入原程序栈。
将VM_Context内通用寄存器恢复到原位置。恢复执行环境,退出VM。