玩玩IDA Graph View Fake Jcc的清除
___________________________________________________________________
下面的部分与IDA本身没有多大关系了。另外,只是个POC,能否做出有用的东西我也不清楚,Just for fun。
1. Fake Jcc的搜索_________________________________________
对变形代码的压缩分为2部分。首先是Basic Block内的压缩(这部分本文不包括,还有很多未解决的问题)。然后是BB之间参考关系的清理,即删除Fake Jcc,合并BB(并再次压缩合并后的结果)。
Fake Jcc之所以假是因为其跳转动作是预定的,我最早是在ExeCryptor里看到这种代码的。
ja指令即fake jcc,这里永远也不会跳转,等同于nop(要跳转则等于jmp)。fake jcc导致对代码递归分析时插入大量无关的BB,生成虚假分枝。Themida构造fake jcc时既使用寄存器也使用内存变量。另外,mov指令有可能压缩后才出现,这也是要先在BB内压缩的原因。
前4句被合并为mov edx,imm32。
对fake jcc的搜索看起来很简单,逐个检测压缩过的BB,若出口为jcc指令,则逆向搜索影响该jcc的指令。对于用寄存器实现的fake jcc,其操作数为:
RegImm: add | sub | and | or | xor | cmp | test…
Reg: not | neg (inc|dec用add|sub替换了)
用内存变量时类似。如果找到,再继续搜索是否存在对应的mov reg|mem,imm。符合条件时即可进行emulate。
在执行Basic Block内的压缩时,必须保留对jcc有影响的指令(图中的neg),否则结果变成mov + ja,无法判断jcc走向了。我现在的做法是写了个GetLastInstr函数,返回BB的“最后”指令(对于出口为jcc的BB即影响跳转的指令),在BB内压缩时不能超过这条指令。当然你也许有更好的办法。
上面的搜索是在一个BB内进行的。下面看看更复杂的情况。
这个fake jcc被permutation jmp分成了2段。第2段存在2个参考者,初始处理无法合并2块。第1块BB出口不是jcc,于是在压缩BB过程中,第1块被处理为:
mov ebp, imm32
jmp loc_1D95C94
在处理fake jcc的过程中,第2块有个参考关系被删除了,于是2块可以合并了,变成:
mov ebp, imm32
jnz loc_1D8515E
这个问题的本质是影响jcc的指令与jcc不在同一个BB,造成严重bug。下面讨论可能的几种解决方式。将2个BB分别称为A,B。
1) 可考虑在A内对mov reg,imm的压缩保留eflags,并传递给B。但在压缩过程中A内可能有多个被压缩的mov指令,在不分析B的前提下无法判断应保留哪个(数量可变)。另外,B可能有多个参考者。
2) 将B合并到A,即对于某个以jcc为出口的BB,如果该块内不存在决定跳转方向的指令,则尽可能将该BB合并到其参考者。这是我一个同事的建议,有道理,以图说明。
A(jmp) B(jcc) A+C(jcc) B(jcc)
\ / \ / \ / \
c(jcc) ? => D E C(jcc) ?
/ \ / \
D E D E
C包含无法确定跳转方向的jcc。C被A,B参考,其中A是jmp到C,B是jcc的一个分枝。这种做法是将C(代码及参考关系)合并到A。
对于A而言,原来的jmp指令被删除,A将使用C的出口。从语义上说,A的出口指令实际变成:
jcc D (假设D为dst,E为ordinary flow)
jmp E
当然,对A可以不必真正插入1条jmp指令,我们已经约定BB的cref_to第1项保存ordinary flow,只要替换这个数据即可。此时可以删除A->C的参考。
对于D和E,参考者原本只有C,现在要加上A。
随着对fake jcc的处理,B->C的参考可能被删除。此时C变成了永远不会执行的孤立节点,可以删除C及C对DE的参考。
这想法不错,但我觉得有点太复杂了,编程要考虑的问题太多。
3) 也是我现在的做法,即增强GetLastInstr函数,使A中保存下影响jcc的指令。
需要增强的部分是,当A只有1个出口时(包括jmp和ordinary flow),增加额外的检测:
顺着A的被参考者搜索,直到遇见这样的节点:该节点没有被参考者(叶节点)或以jcc为出口。如果最后1个节点以jcc为出口,并且该BB内不存在决定跳转方向的指令,则逐块逆向搜索这样的指令,若直到A才找到,则应保留该指令,不能压缩。
2. 反汇编引擎的修改______________________________________________
显然,对jcc的处理需要emulate。开始考虑把涉及的指令直接copy到buffer内,真正执行一下(只要调整一下jcc的操作数),但这样做需要把pcode再汇编为机器码,暂时不想搞这个,我也不大喜欢这种做法。实际用的是直接分析pcode。
将机器码反汇编为pcode时,我用的是z0mbie的xde v1.02,没有直接使用IDA的结果。对IDA的数据结构不熟悉,另外部分代码是以前写的,换成用IDA要修改不少。而且xde开源,便于修改。
Intel的<Basic Architecture>附录有2张表,EFLAGS Cross-Reference和EFLAGS Condition Codes,说明了指令对eflags各标记位的影响及jcc指令的跳转条件。修改xde,把这2张表加进去。在xde_instr内增加成员:
unsigned long eflags;
修改xde_disasm,将指令相关的eflags数据保存到该成员内(最终保存到pcode内)。
对于jcc指令,保存的是jcc检测的标记位。对其他指令保存指令影响的标记位(嗯,我的表比较简单,也许不大可靠。Intel作了详细的区分,对eflags的访问包括T,M,0,1,R。我没做这样细的划分)。
要搜索影响jcc走向的指令,只要从jcc回推,将各指令的eflags成员与jcc的and即可。
2. emulate______________________________________________
emulate过程相对简单。定义以下结构:
typedef struct _EFlags
{
ulong CF : 1;
ulong unused1 : 1;
ulong PF : 1;
ulong unused2 : 3;
ulong ZF : 1;
ulong SF : 1;
ulong unused3 : 3;
ulong OF : 1;
ulong unused4 : 20;
} EFlags, *PEFlags;
typedef union _Status
{
EFlags eflags;
ulong status;
}Status, *PStatus;
union
{
uchar r8;
ushort r16;
ulong r32;
} r; //emulator的寄存器
Status s; //my "efalgs"
搜索到mov reg|mem, imm后,将imm及当前的eflags保存。
r.r32 = codes[first].operand.imm32; //first即mov
__asm
{
pushfd
pop s.status
}
下面是对add reg|mem,imm的模仿:
switch(opcode)
{
case Op_ADD_RegImm: case Op_ADD_RegImm_B8:
case Op_ADD_MemImm: case Op_ADD_MemImm_B8:
if (InstrBit8Attr(i)) //是否为8位操作,这玩艺是个macro,不必管它
{
__asm {
push s.status
popfd
mov al, r.r8
add al, imm8 //add的操作数
pushfd
pop s.status
mov r.r8, al
}
}
else if (codes[i].p_66) //16位
{
__asm {
push s.status
popfd
mov ax, r.r16
add ax, imm16
pushfd
pop s.status
mov r.r16, ax
}
}
else //32位
{
__asm {
push s.status
popfd
mov eax, r.r32
add eax, imm32
pushfd
pop s.status
mov r.r32, eax
}
}
break;
完成模仿后,即已提取到eflags,根据jcc类型判断跳转是否发生。
bool CEmulate::IsJccTaken(Status status, PCode& pcode, bool& action)
{
// status=决定jcc的eflags
// pcode =jcc指令
// 返回: true=成功,false=失败
// 出参数action=true: jcc=jmp, false:jcc=nop
bool success=true;
switch(pcode.opcode)
{
case Op_JO : action = (status.eflags.OF==1)? true:false; break;
case Op_JNO: action = (status.eflags.OF==0)? true:false; break;
case Op_JB : action = (status.eflags.CF==1)? true:false; break;
case Op_JAE: action = (status.eflags.CF==0)? true:false; break;
case Op_JE : action = (status.eflags.ZF==1)? true:false; break;
case Op_JNE: action = (status.eflags.ZF==0)? true:false; break;
case Op_JBE: action = (status.eflags.CF | status.eflags.ZF)? true:false; break;
case Op_JA : action = !(status.eflags.CF | status.eflags.ZF)? true:false; break;
case Op_JS : action = (status.eflags.SF==1)? true:false; break;
case Op_JNS: action = (status.eflags.SF==0)? true:false; break;
case Op_JP : action = (status.eflags.PF==1)? true:false; break;
case Op_JNP: action = (status.eflags.PF==0)? true:false; break;
case Op_JL : action = (status.eflags.SF ^ status.eflags.OF)? true:false; break;
case Op_JGE: action = !(status.eflags.SF ^ status.eflags.OF)? true:false; break;
case Op_JLE: action = ((status.eflags.SF ^ status.eflags.OF) | status.eflags.ZF)?
true:false; break;
case Op_JG : action = !((status.eflags.SF ^ status.eflags.OF) | status.eflags.ZF)?
true:false; break;
default: //不支持,jecxz?
success=false;
break;
}
return success;
}
如果jcc=jmp,且jcc的dst只有这1个参考者,可合并2块。若存在其它参考者,将jcc替换为jmp。删除对ordinary flow bb的参考。其余情况类似。
附件的图 和前文处理的是同一handler,原始BB数为68082,结果为125。代码的结构已经基本正确,只是BB内的处理有不少问题(那些看起来比较大的块垃圾成堆)。需要大幅度修改。