玩玩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内的处理有不少问题(那些看起来比较大的块垃圾成堆)。需要大幅度修改。