中国IT动力,最新最全的IT技术教程
最新100篇 | 推荐100篇 | 专题100篇 | 排行榜 | 搜索 | 在线API文档
首 页 | 程序开发 | 操作系统 | 软件应用 | 图形图象 | 网络应用 | 精文荟萃 | 教育认证 | 硬件维护 | 未整理篇 | 站长教程
ASP JS PHP工程 ASP.NET 网站建设 UML J2EESUN .NET VC VB VFP 网络维护 数据库 DB2 SQL2000 Oracle Mysql
服务器 Win2000 Office C DreamWeaver FireWorks Flash PhotoShop 上网宝典 CorelDraw 协议大全 网络安全 微软认证
硬件维护  CPU  主板  硬盘  内存  显卡  显示器  键盘鼠标  声卡音箱  打印机  机箱电源  BIOS  网卡  C#  Java  Delphi  vs.net2005
  当前位置:> 看雪学院专区 > 加壳与脱壳
Themida的VM分析--变形代码清理
作者:softworm 时间:2006-12-13 19:44 出处:pediy.com 责编:月夜寒箫
              摘要:Themida的VM分析--变形代码清理
这是我的部分笔记,没有完成,供参考。可能最近几个月没时间搞了。

变形代码清理:Mission Impossible?


1. 变形代码长啥样?

   变形代码在壳里用得越来越多,这种源于病毒的技巧被用来掩盖代码的真实目的,
   以及对cracker进行身心折磨:-(。变形代码不象普通的花指令那样,用简单的字
   符串匹配即可清理(其生成过程也不同,花指令一般以在编程时插入宏的方式创建)。
   下面的实例来自Themida虚拟机代码:

   01596EEF  push    edx      
   01596EF0  add     ebx, ecx  *     
   01596EF2  mov     edx, 0    
   01596EF7  mov     bh, 35h  *  
   01596EF9  add     edx, edi        
   01596EFB  shr     al, 3  *           
   01596EFE  push    ecx             
   01596EFF  sub     eax, ebx  *    
   01596F01  jmp     loc_14F562A     
   014F562A  mov     ecx, 7445h      
   014F562F  neg     ecx             
   014F5631  not     ecx             
   014F5633  not     ecx             
   014F5635  or      ecx, 629Ah      
   014F563B  jmp     loc_14FA94D     
   014FA94D  sub     al, dl  *        
   014FA94F  or      ecx, 205Ch      
   014FA955  xor     ecx, 0FFFFEBFFh 
   014FA95B  mov     bl, 8Ah  *  
   014FA95D  add     ecx, edx        
   014FA95F  add     ah, dh     *  
   014FA961  jmp     loc_1594A3C     
   01594A3C  mov     esi, [ecx]      
   01594A3E  mov     bl, ch  *  
   01594A40  pop     ecx             
   01594A41  mov     ebx, 7570h  *  
   01594A46  mov     edx, [esp]   
   01594A49  add     esp, 4  

   这段代码非常典型,我们可以尝试手工清理。其中的3个jmp是所谓的置换jmp,即人为将
   代码分块,打乱其物理存放位置,再用JMP指令链接。

   除了JMP指令,其中还夹杂了一些垃圾指令(带*号的行,从代码的逻辑可以识别),拿掉这
   些JMP和垃圾指令后剩下:


   01596EEF  push    edx      
   01596EF2  mov     edx, 0    
   01596EF9  add     edx, edi        
   01596EFE  push    ecx             
   014F562A  mov     ecx, 7445h      
   014F562F  neg     ecx             
   014F5631  not     ecx             
   014F5633  not     ecx             
   014F5635  or      ecx, 629Ah      
   014FA94F  or      ecx, 205Ch      
   014FA955  xor     ecx, 0FFFFEBFFh 
   014FA95D  add     ecx, edx        
   01594A3C  mov     esi, [ecx]      
   01594A40  pop     ecx             
   01594A46  mov     edx, [esp]   
   01594A49  add     esp, 4

   014F562A - 014FA955对ecx的赋值及计算,其结果为0,可以压缩为1句,现在很容易
   看出代码的真正含义了:

   01596EEF  push    edx ----------------------\    
   01596EF2  mov     edx, 0                     \
   01596EF9  add     edx, edi                    \
   01596EFE  push    ecx -------\                 \
   014F562A  mov     ecx, 0      \       \
   014FA95D  add     ecx, edx     mov esi,[edx]   mov esi,[edi]
   01594A3C  mov     esi, [ecx]  /                /
   01594A40  pop     ecx -------/                /
   01594A46  mov     edx, [esp]--- pop edx  ----/
   01594A49  add     esp, 4 -----/

   这里实际使用了2种代码模式:

   MOV Reg,[ESP]/ADD ESP,4 -> POP Reg
   PUSH Reg1/MOV Reg1,Imm/ADD Reg1,Reg2/Operate Reg3,Mem/POP Reg1 -> Operate Reg3,[Reg2+Imm]
   (这里Reg1用来生成内存地址)


2. 怎样生成变形代码? 

   置换JMP的生成过程如下,将代码随机划分为片断,例如每段1-10行,用数据结构
   (如List)管理代码块信息,然后混洗,以下为伪码:

   ESI = Initial address of instructions
   EDI = Address of last instruction

    ; 用第1-60h行代码创建若干代码片段,每段8-16行

    Given ESI = 00000000h,
          EDI = 00000060h

    while(ESI < EDI)
       Store ESI
       ESI += Random(8)+8
       Store ESI
       if((ESI+0F > EDI)
          Store ESI,EDI
          break;
       end if
    end while
       
    Result (for example):
          DD    00000000h,0000000Ah <- 1 (第0-10行)
          DD    0000000Ah,00000017h <- 2 
          DD    00000017h,00000023h <- 3
          DD    00000023h,00000032h <- 4
          DD    00000032h,0000003Dh <- 5
          DD    0000003Dh,00000049h <- 6
          DD    00000049h,00000052h <- 7
          DD    00000052h,00000060h <- 8

   洗完后的结果:
       DD    00000032h,0000003Dh <- 5
          DD    00000023h,00000032h <- 4
          DD    0000000Ah,00000017h <- 2
          DD    00000000h,0000000Ah <- 1
          DD    00000017h,00000023h <- 3
          DD    00000052h,00000060h <- 8
          DD    0000003Dh,00000049h <- 6
          DD    00000049h,00000052h <- 7

   按洗完的顺序物理摆放代码,每块代码在末尾用jmp跳到逻辑上的下一块,保持代码原来的
   执行顺序,由于代码物理位置的变化,有些指令需要修正(call,jmp,jcc等)。

   程序执行时,eip会随着执行自然落到物理位置的第1块(但这并不是程序逻辑的第1块,具体
   说是第5块,第1块现在排在第4),所以在混洗后的第1个物理块前,应该有1句:

   JMP EntryPoint(处理前代码的首句,在第1块内)


   变形代码可按预定义的模式生成。随机选择合适的模式对代码进行扩展,这是前面压缩过程
   的逆反。这个过程是递归的,需要定义停止条件(如递归深度)。如:

   push eax -> sub esp,4      -> push ebp          -> ...
               mov [esp],eax     mov  ebp,esp
                                 add  ebp,4
                                 sub  ebp,4
                                 xchg ebp,[esp]
                                 pop  esp

                                 mov [esp],eax


   代码的物理膨胀同样需要修正指令。显然,变形代码的生成过程必须有反汇编引擎的支持。


   仅仅有这些是不够的。cracker同样可以借助反汇编引擎重建代码,只要识别出匹配的代码序列
   即可对扩展后的代码进行压缩。然而,如果在有效的代码中夹杂入垃圾代码,重建难度就直线上
   升了。例如:

   sub esp,4
   shl ebp,2 <- 垃圾指令
   mov [esp],eax

   中间的1句是垃圾指令,这用肉眼很容易识别,但用程序来做就没这么简单了。这意味着在检测代
   码序列,与预定义模式进行比对时,允许对不连续指令进行匹配,而这样做误判几乎是无法避免的。
   或许更好的解决方式是使用emulator,从代码的算法(而不是具体的指令)来分析,类似启发式反
   病毒。

   注意,在上面的例子中,加入shl ebp,2(或者别的什么指令,如add ebp,1234),前提是当前ebp是
   空闲寄存器,可随意使用。简单的处理可以是对空闲寄存器进行约定,比如约定某段代码不使用
   ebp,但这样做强度不够,更好的办法是能判断出当前位置可用的空闲寄存器。

   
   z0mbie的XDE反汇编引擎就带有这样的功能(实际上这大概是其最重要的设计目的之一)。XDE中最
   重要的概念是对象集(object set),即指令读写的registers,memory等,表示为1个DWORD中的对应
   位。被反汇编的指令,结果数据结构中包含了该指令读取的源对象集(src_set)与写入的目的对象
   集(obj_set)。具体细节可参考XDE源码及z0mbie的<Permutation conditions>。下面的代码是
   XDE v1.02带的例子,演示了获取空闲对象集(英文注释是原代码带的)。


   int AnalyzeRegs(/* IN/OUT */ CMistfall* M)
   {
  HOOY* h;   // current element of the file, instruction or anything else
    
  // step 1/3 -- mark all registers as "used"
  // 对List内的每条指令,初始化为所有对象都被占用

  ForEachInList(M->HooyList, HOOY, h)
  {
     h->regused = XSET_UNDEF;  // XSET_UNDEF=0xFFFFFFFF
  }
  
  // step 2/3 -- for each instruction, clear dst_set&~src_set
  
  ForEachInList(M->HooyList, HOOY, h)
  {
     if(h->flags & FL_OPCODE)
     if((h->flags & FL_ERROR) == 0)
     {
    xde_instr instr;
    xde_disasm(h->dataptr, &instr);
        
    // update h->regused

    // 对链表上的每个指令节点,计算出对应的空闲对象集。仔细看看这个位运算:
    // 
    // 1. ~instr.src_set: 
    //    不作为当前指令src_set使用的obj_set
    //
    // 2. instr.dst_set & (~instr.src_set): 
    //    取1的结果与当前指令目的对象集的重叠。1取到的onj_set只是当前指令未用,但后续的
    //    指令可能会使用,并不能判断为空闲。与的结果是,未作为当前指令src使用,同时被用作
    //    当前指令的dst,这个结果就是从当前指令可以判断出的空闲obj_set
    //
    //    这种策略很保守,取到的结果可靠,但能取到的obj_set很有限,大部分时候为空。
    //    如add eax,ebx:
    //    
    //    ~instr.src_set = {ecx|edx|esp|ebp|esi|edi|memory}
    //    dst_set = {eax|flags}
    //    与的结果为0,即仅凭这条指令,取不到任何空闲obj_set数据。
    //
    //    那么什么指令能取到空闲obj_set? 可以立刻想到1个;-)
    //    mov eax,12345678
    //    此时eax为空闲寄存器,即在这条指令之前,可以插入指令,随意使用eax
    //    (add eax,ebx/shl eax,cl/...)
    //
    // 3.  ~(instr.dst_set & (~instr.src_set))
    //     取反的结果对应不能判断为空闲的obj_set,这个结果可能比指令真正使用的obj_set大。
            
    h->regused &= ~(instr.dst_set & (~instr.src_set));
        
    //
    // incorrect, need to be replaced with recursive subroutine analysis.
    // however, the following means:
    //   "CALL'ed subroutines doesnt use FLAGS as a source_object"
    //
    if (XDE_CMD(instr.flag) == C_CMD_CALL)
        h->regused &= ~XSET_FL;
      }
  }
  
  // step 3/3 -- propagate zero bits within freeset, until possible;
  // works similar to "wave" algo
  
  // 从2得到的空闲obj_set是很少的,这里使用"波浪"算法传递free set

  for(;;)
  {
     int modified = 0;
    
     ForEachInList(M->HooyList, HOOY, h)
     {
    if (h->flags & FL_OPCODE)  // 当前节点为代码
    if ((h->flags & FL_ERROR) == 0)
          
    if (h->next)      // 存在后续节点,也为代码
    if (h->next->flags & FL_OPCODE)
    if ((h->next->flags & (FL_LABEL|FL_ERROR)) == 0)
    {
        xde_instr instr;
        xde_disasm(h->dataptr, &instr);

        if (((instr.src_set|instr.dst_set) & (~h->next->regused)) == 0)
        {
      // 1. instr.src_set|instr.dst_set为当前指令读写的obj_set
      //
      // 2. ~h->next->regused 为下一句指令的空闲obj_set(来自第2步)
      //
      // 这个条件为true的含义是: 当前指令使用的obj_set(包括src和dst)
      // 与下一句指令的空闲obj_set不存在交叉,即下一条指令的空闲obj_set
      // 当前指令没有使用
      //  
      // 条件满足,会执行h->regused &= h->next->regused
      // 即h->next->regused内对应为0的位(空闲obj_set)被传递到当前指令
      //
      // ForEachInList循环遍历所有指令,每次只影响了相邻的2条指令
      //
      // 由for(;;)和ForEachInList构成的2重循环的目的,在于尽可能地传递
      // 空闲obj_set信息,在每条指令位置提取出尽可能多的空闲obj_set


      if ((h->regused & h->next->regused) != h->regused)
      {
          // h->regused的某位为1,h->next->regused的对应位为0时条件为
          // true,即某个obj,对下一条指令确定为空闲,对当前指令不能判断
          // 为空闲。注意这个检查是在修改当前指令的regused(即下面的
          // h->regused &= h->next->regused)之前做的。

          // 有了外面的if作保证,这个原来对当前指令"可能使用"的obj可明
          // 确判断为空闲。
          
          // 注意对1条指令,src_set,dst_set是通过反汇编得到的确切数据,
          // 即当前指令肯定要使用的obj_set。这和regused是不同的概念。
          // regused中为0的位代表空闲obj,为1的位代表不能确切判定为空
          // 闲obj(当前指令不一定使用)。regused涵盖的范围>= src_set|dst_set。

          // regused预置为0xFFFFFFFF(全部占用),通过第2步的分析及前后
          // 指令空闲obj_set的传递来剔除空闲obj_set。
          
          // 当条件为true,增加modified,表示相邻指令间出现了空闲obj_set
          // 传递。结束ForEachInList后进入下一轮for(;;)再次遍历指令链表

          modified++;
      }

      h->regused &= h->next->regused;
                  
        }
      }
     }
    
     if (!modified) break;
  }
  
  return 1;
  
   } // AnalyzeRegs   
   
   用这段代码测试了几个kernel32中的API,对空闲obj_set的识别效果似乎还不够好,不过这种做法
   是可行的。另外,这里的代码虽然很短,读起来并不容易,但愿我没有理解错;-)。

   MetaPHOR还包括寄存器变换(随机选择不同的寄存器),变量选择,用不同的opcode编码实现同样的
   功能等等.


关闭本页
 
首页 | 投资与合作 | 服务条款 | 隐私政策 | 收藏本站 | 设为首页 | 新用户注册 | 免责声明 | 使用帮助
Copyright ©2005-2008 chinaitpower.com All rights reserved. www.chinaitpower.com 版权所有