浅析EMET 3.5中的ROP Mitigation


在Blackhat USA 2011上,微软发起了一项名为Bluehat的技术挑战赛,旨在于全球范围内征集漏洞利用缓解机制(说实话我不太喜欢这个蹩脚的翻译,还是Exploit Mitigation更通俗易懂吧)。大赛为期一年,近日方落下帷幕。说来有趣,本届大赛最终杀入决赛的mitigation方案,竟都是用于Anti-ROP的,皆被微软集成在最新版本的EMET中。


本人好奇,是何等犀利的技术能配得上微软500,000刀的奖励,于是写点代码进行了一番研究,现贴点“研究成果“,以备忘。

 

EMET中对于ROP,共有以下五种ANTI措施:
1.StackPviot:check if stack is pviotted
2.Caller:check if critical functions was called and not returned into 
3.SimExecFlow:Simulate the execution flow after the return address to detect subsequent 

   ROP Gadgets
4.MemProt:specail check on memory protections API
5.LoadLib:check and prevent LoadLibrary calls againts UNC paths

 

首先需要说明的是,以上所有“check”或者“detect”,只有当ROP Chain将控制流转向受EMET保护的函数时才会进行。以下是受保护的函数:

kernel32.MapViewOfFileEx
kernel32.MapViewOfFile
kernel32.CreateFileMappingW
kernel32.CreateFileMappingA
kernel32.CreateFileW
kernel32.CreateFileA
kernel32.CreateFile
kernel32.WinExec
kernel32.WriteProcessMemory
kernel32.CreateRemoteThread
kernel32.CreateProcessInternalW
kernel32.CreateProcessInternalA
kernel32.CreateProcessW
kernel32.CreateProcessA
kernel32.HeapCreate
kernel32.VirtualAllocEx
kernel32.VirtualAlloc
kernel32.LoadLibraryExW
kernel32.LoadLibraryExA
kernel32.LoadLibraryA
kernel32.VirtualProtectEx
kernel32.VirtualProtect

EMET会对这些函数进行inline-hook,一旦其被调用,就会先跳转到EMET的钩子函数上进行check/detect,为行文方便,下文中统称这些函数为关键函数。

 

1.StackPviot
EMET采用的这种ANTI技术的实现细节我并没有深入分析,因为我知道去年放出来的Windows 8 DP中就尝试性的引入了这种ANTI-ROP的技术,其原理非常简单,即在关键函数的入口处,调用PsValidateUserStack对当前ESP中保存的栈顶指针进行检验。如果发现ESP指向的地址,不在当前线程的栈地址范围内,就认为检测到ROP!正因为简单,所以很快就有安全研究人员找到了旁路办法,并放出了POC(详见本人拙文《Debug ROP Chain For W8》)。不知道是不是因为此种方法很快就被旁路,在随后的Windows8 CP和Windows8 DP中,已经看不到PsValidateUserStack的身影了。

 

2.Caller
Caller对关键函数的返回地址进行验证,如果返回地址的前一条指令是call指令,而且目标跳转地址是关键函数的地址,那么则通过验证。反之,如果返回地址的前一条指令不是call,或者目标跳转地址不是关键函数的地址,都认为检测到ROP。在看伪代码之前,先定义一个枚举类型,用于表示指令的类型。这个枚举类型在下文分析SimExecFlow的检测原理时是还要提到。

 

typedef enum INSTRUCTION_TYPE
{
   ERROR_INS,
   ...
   MOV_ESP_EBP_INS   //MOV ESP,EBP指令
   CALL_INS,                  //CALL指令
   PUSH_INS,                 //PUSH指令
   POP_INS,                   //一般的POP指令
   POP_ESP_INS,           //POP ESP指令
   POP_EBP_INS,           //POP EBP指令
   ADD_ESP_INS,          //ADD ESP,XXX指令
   ADD_EBP_INS,          //ADD EBP,XXX指令
   SUB_ESP_INS,           //SUB ESP,XXX指令
   SUB_EBP_INS,           //SUB EBP,XXX指令
   LEAVE_INS,               //LEAVE指令
   RETURN_INS,            //RETURN指令
}INSTURCTION_TYPE

 

extern DWORD g_CallInsLen[5]={2,3,5,6,7};                    //全局变量,保存call指令所有可能的长度
BOOL ROP_CallerCheck(LPVOID ReturnAddress,            //返回地址
                                        LPVOID CriticalFuncAddress)    //关键函数地址
{
    ...省略无关代码...
    for(int i=0;i<5;i++)
    {
         //计算返回地址前一条指令的地址
        LPVOID RreviousInsAddress = (LPVOID)((DWORD)ReturnAddress + g_CallInsLen[i]);
        int    InsLen;
        //对返回地址前一条指令进行解码,判断指令的类型,指令的长度保存在ins_len中
       if(CALL_INS == DecodeOpcode(PreviousInsAddress,&InsLen))
       {
             //如果是call指令
            if(NULL == CriticalFuncAddress)
            {
                 return TRUE;
            }
           else
           {
              //如果前一条指令是call指令,则进一步判断其目标地址是否是CriticalFuncAddress
              if(CriticalFuncAddress == GetTargetBranchAddress(PreviousInsAddress))
              {
                  return TRUE;
               }
           }
        }
    }
    return FALSE;
}

3.SimExecFlow
SimExecFlow技术通过模拟执行控制流,来检测ROP Gadgets。实际上其仅仅模拟的只是特定的几条指令对ESP/EBP进行的修改,重头戏还是在对返回地址的检测上。
看伪代码之前,先定义一个数据结构,用于表示模拟的上下文环境。

typedef _struct _SIMULATE_CONTEXT
{
    DWORD UNKONWN;
    DWORD EBP;
    DWORD ESP;
    DWORD ESP;
}SIMULATE_CONTEXT;

 

再定义一个枚举类型,用于表示指令的控制流类型
typedef enum _EXE_FLOW_TYPE
{
  EXE_FLOW_ERROR    = 0;
  EXE_FLOW_BRANCH = 1;        //call或者jmp跳转指令
  EXE_FLOW_SEQ         = 2;        //顺序执行指令
  EXE_FLOW_RETURN  = 3;       //返回指令
}
以下是伪代码:

SIMULATE_CONTECT sim_cxt;
sim_cxt.EBP = 关键函数的返回地址
sim_cxt.EIP  = 关键函数返回地址             
sim_cxt.ESP = 从关键函数返回后的栈顶地址

BOOL bIsNormal = ROP_SimExecFlow(&sim_cxt);
if(FALASE == bIsNormal)
{
      //ROP detected
}
else
{
      //no ROP
}

....


BOOL ROP_SimExecFlow(SIMULATE_CONTEXT* sim_cxt)
{
   ...省略无关代码...
   
   for(int i=0; i<15; i++)
   {
        EXE_FLOW_TYPE flow_type = SimulateExeFlow(sim_cxt);
        if(EXE_FLOW_ERROR == flow_type || EXE_FLOW_BRANCH == flow_type)
       {
           //如果模拟执行的是跳转指令,或者函数SimulateExeFlow函数执行错误,则返回TRUE
           return TRUE;
       }
       else if(EXE_FLOW_RETURN == flow_type)
       {
          //如果模拟执行的是ret/ret n指令,则需要对返回地址的有效性进行检验,这里主要是判断
          //返回地址的前一条指令是否为call指令
          if(FALSE == ROP_CallerCheck(sim_cxt->EIP,NULL))
         {
              return FALSE;
         }
      }
   }
   return TRUE;
}

 

以下是SimulateExeFlow函数的伪代码
EXE_FLOW_TYPE SimulateExeFlow(SIMULATE_CONTEXT* sim_cxt)
{
     EXE_FLOW_TYPE flow_type         = EXE_FLOW_TYPE_SEQ;
     INT                       ins_len              = 0;
     LPVOID                return_addr      = NULL;
 
     //对当前sim_cxt中EIP所指向的指令进行解码,判断指令的类型,指令的长度保存在ins_len中
     INSTRUCTION_TYPE ins_type    = DecodeOpcode(sim_cxt->EIP,&ins_len);
    switch(ins_type)
    {
       CASE ERROR_INS:
             //如果解码失败
            flow_type = EXE_FLOW_ERROR;
            break;
       CASE CALL_INS:
       CASE JMP_INS:
            flow_type   = EXE_FLOW_BRANCH;
            break;
       CASE MOV_ESP_EBP_INS:
            sim_cxt->ESP = sim_cxt->EBP;
            break;
       CASE PUSH_INS:
            sim_cxt->ESP = sim_cxt->ESP - 4;
            break;
       CASE POP_INS:
            sim_cxt->ESP = sim_cxt->ESP + 4;
            break;
       CASE POP_EBP_INS:
            sim_cxt->EBP = DWORD PTR[sim_cxt->ESP]
            sim_cxt->ESP = sim_cxt->ESP + 4;
            break;
       CASE POP_ESP_INS:
           sim_cxt->ESP = DWORD PTR[sim_cxt->ESP]
            sim_cxt->ESP = sim_cxt->ESP + 4;    //此处值得商榷,按说POP ESP之后,无须再对新的 

                                                                       //ESP进行调整但EMET的代码里确实是真么做的-_-!!
            break;
       CASE LEAVE_INS:
           sim_cxt->EBP = DWORD PTR[sim_cxt->ESP]
           sim_cxt->ESP = sim_cxt->ESP + 4; 
           sim_cxt->ESP = sim_cxt->EBP;
           break;
       CASE RETURN_INS:
           flow_type    = EXE_FLOW_RETURN;
           return_addr  = DWORD PTR[sim_cxt->ESP];
           sim_cxt->ESP = sim_cxt->ESP + 4; 
           break;
      

        ...还有对sub esp,xxx/add esp,xxx/sub ebp,xxx/sub esp,xxx指令的模拟,以下省略...

    }
 
      //根据指令类型,修改EIP
     if(EXE_FLOW_TYPE_SEQ == flow_type)
     {
        //如果顺序执行,那么下一条指令地址,为当前EIP加上指令长度
        sim_cxt->EIP = sim_cxt->EIP + ins_len;
     }
     else if(EXE_FLOW_RETURN == flow_type)
     {
       //如果是返回指令,那么EIP更新为返回地址
       sim_cxt->EIP = return_addr;
      }
     return flow_type;
}

分析清楚SimExeFlow的检测方式,不难想到这样的一种旁路办法,即构造一条由15个形如CALL XXX/n POP XXX/ret n的ROP Gadget组成的ROP Chain。

试举一例,假如原本的ROP Chain,在执行关键函数VirtualProtect之前形式下:
rop_gadget_1--------------->pop ebp,ret
shellcode_address
0x100
0x40
writeable_address
JUNK_DATA
rop_gadget_2--------------->pop esp,ret
shellcode

如果rop_gadget_2地址上的上一条指令不是call指令,那么就无法通过SimExeFlow的验证。现对这个ROP Chain做以下修改
rop_gadget_1--------------->pop ebp,ret
shellcode_address
0x100
0x40
writeable_address
JUNK_DATA
rop_gadget_3--------------->pop ebp,ret------|
JUNK_DATA                                                        |
......                                                                        |
rop_gadget_3--------------->pop ebp,ret     15个
JUNK_DATA                                                        |
rop_gadget_3--------------->pop ebp,ret------|
JUNK_DATA
rop_gadget_2
shellcode
rop_gadget_3在pop ebp指令之前,恰好有一条call指令。这样在ROP_SimExeFlow函数15次循环执行完之后,就会返回TRUE,从而通过SimExeFlow的验证。当然了,这种形如call xxxx/n POP/ret n的rop gadget的存在,需要一定的机缘,但是确实是存在的。比如,Bypass DEP+ASLR 必备的msvcr71.dll中就有两个call xxxx/pop ebp/retn指令序列。

 

4.MemProt
MemProt通过对传入VirtualProtect/VirtualProtectEx的参数进行检验,以判断是否存在ROP的。其这两个API用于改写内存页访问保护属性,有以下三个关键参数:
    a.lpAddress     

    b.dwSize           

    c.flwNewProtect   

MemProt先对flwNewProtect进行检验,如果发现flwNewProtect中设置了可执行标记位,那么则进一步对lpAddress进行检验。如果发现[lpAddress,lpaddress+dwSize]这段内存恰好在当前线程的栈地址范围内,即以下不等式成立:
    TEB.StackTop < lpAddress && (lpAddress+dwSize) < TEB.StackBottom 
则认为检测到ROP。
个人觉得MemProt旨在防护exploit栈溢出漏洞的ROP。虽说“GS+SafeSEH+SEHOP”的重重防护使得一般的栈溢出利用技术已毫无用武之地,但在某些特殊的情形下,比如攻击者能够指哪儿写哪儿,或者在函数返回前利用被改写的函数指针等来说,攻击者还是有机会的。

 

5.LoadLib
LoadLib用于防止攻击者通过执行ROP Chain来从远程服务器上加载恶意的DLL,所以LoadLib对LoadLibraryA/LoadLibraryW/LoadLibraryEx/LoadLibraryExW的参数lpFileName做如下检验:

   if(lpFileName[0] == '\\' && lpFileName[1] != '?')
   {
       //检测到ROP
      return FALSE;
   }
   else
   {
       //正常,没有检测到ROP
       return TRUE;
   }
在SyScan'10上Brett Moore的演讲《Depth In DEP》中,提到了这种通过WebDAV/SMB加载DLL来旁路DEP的技术,不过我本人并没有见过使用了这种技术的POC。

 

 

以上是对EMET中5种ROP Mitigation的简单分析,个人觉得最狠的还是Caller和SimExeFlow这两种检测技术!当然了,在高人(比如......你懂的)眼里,一切Mitigation都是纸老虎,统统bypass!我等初学者只有顶礼膜拜的份儿了

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值