服务热线:010-65014696

安全研究

椒图关注 FOCUS ON JOWTO

某安全软件ROP防护分析笔记

[2014-12-10]
原理

ROP是用来绕过DEP防护的常用方法, 攻击者通常会利用ROP链将真正的ShellCode拷贝到一个具有可执行权限的内存处并执行。为了达到这个目的,攻击者会使用一些类似VirtualAlloc这样的API进行分配或修改内存的属性。而这些函数是有限的,并且最终都会进入内核,某安全软件通过在内核层拦截这些关键函数,在关键函数内根据从当前线程获取到的陷阱帧拿到返回用户层时的调用栈信息。通过对关键函数调用栈的每一层栈帧进行检查,以此来进行ROP检测。

实现

在关键函数内首先进行Stack Pivot检查:
栈置换利用类似下面的指令序列

1. xchg eax, esp   

2. retn  

检测方法:如果调用 关键函数时, esp 指针不在用户空间栈范围内,则说明进行了栈置换。
绕过栈置换方法:首先通过简单的ROP链来调整原栈空间,将 包含关键函数操作的ROP链 拷贝到原栈空间,此时再利用栈置换重新将esp指向调整后的原栈空间,继续执行ROP链,可绕过此处的栈置换检测。
检测代码实现:

view plaincopy to clipboardprint?

1. if ( func_switch & STACK_PIVOT )            // 栈置换攻击防护  

2. {                                         

3.       if ( _teb->FiberData < 0x10000 )  

4.       {  

5.         if ( (_esp < (unsigned int)stack_limit || _esp >= (unsigned int)stack_base) )  

6.        {  

7.           insert_list_notification_user(4, 0, 0, index, 0, 1) != 3 ) ;// 通知应用层  

8.           return 0;  

9.         }  

10.       }  

11.       else  

12.       {  

13.         DbgPrint("Thread running with fiber!\n");  

14.       }  

15.     }  

16. }  

ROP检测函数1:

下面是一个mona生成的ROP链:

view plaincopy to clipboardprint?

1. 0x77c1f88c,    # POP EBP # RETN [msvcrt.dll]   

2. 0x77c1f88c,    # skip 4 bytes [msvcrt.dll]  

3. 0x77c5335d,    # POP EBX # RETN [msvcrt.dll]   

4. 0x00000201,    # 0x00000201-> ebx  

5. 0x77c4d2f2,    # POP EDX # RETN [msvcrt.dll]   

6. 0x00000040,    # 0x00000040-> edx  

7. 0x77c35f14,    # POP ECX # RETN [msvcrt.dll]   

8. 0x77c5ec64,    # &Writable location [msvcrt.dll]  

9. 0x77c3aeca,    # POP EDI # RETN [msvcrt.dll]   

10. 0x77c39f92,    # RETN (ROP NOP) [msvcrt.dll]  

11. 0x77c4ec62,    # POP ESI # RETN [msvcrt.dll]   

12. 0x77c2aacc,    # JMP [EAX] [msvcrt.dll]  

13. 0x77c34fcd,    # POP EAX # RETN [msvcrt.dll]   

14. 0x77c11120,    # ptr to &VirtualProtect() [IAT msvcrt.dll]  

15. 0x77c12df9,    # PUSHAD # RETN [msvcrt.dll]   

16. 0x77c51025,    # ptr to 'push esp #  ret ' [msvcrt.dll]  

当调用VirtualProtect进入R0时,堆栈如下:

正常堆栈回调:

1. 1: kd> dds 0x12f308  

2. 0012f308  7c90d6dc ntdll!NtProtectVirtualMemory+0xc    <--- ret1 ring3 esp  

3. 0012f30c  7c801a81 kernel32!VirtualProtectEx+0x20      <--- ret2  

4. 0012f310  ffffffff  

5. 0012f314  0012f338  

6. 0012f318  0012f33c  

7. 0012f31c  00000040  

8. 0012f320  0012f364  

9. 0012f324  0054c640  

10. 0012f328  7c8449fd kernel32!SetUnhandledExceptionFilter  

11. 0012f32c  0012f348                                     <--- ebp  

12. 0012f330  7c801aec kernel32!VirtualProtect+0x18        <--- ret  

13. 0012f334  ffffffff  

14. 0012f338  7c8449fd kernel32!SetUnhandledExceptionFilter  

15. 0012f33c  00000005  

16. 0012f340  00000040  

17. 0012f344  0012f364  

18. 0012f348  0012ff30                                     <--- ebp  

19. 0012f34c  00410f85                                     <--- ret  

20. 0012f350  7c8449fd kernel32!SetUnhandledExceptionFilter  

21. 0012f354  00000005  

22. 0012f358  00000040  

23. 0012f35c  0012f364  

24. 0012f360  000206ec  

25. 0012f364  00000000  

26. 0012f368  00410f1e  

27. 0012f36c  00410f3d  

28. 0012f370  00000000  

29. 0012f374  ffffffff  

30. 0012f378  00410e26  

31. 0012f37c  0043c48d  

32. 0012f380  005883f8  

33. 0012f384  85c1f93a  

ROP链中堆栈回调:

1. 0:009> dds esp  

2. 0c0c0c08  7c90d6dc ntdll!NtProtectVirtualMemory+0xc     <--- ret1 ring3 esp  

3. 0c0c0c0c  7c801a81 kernel32!VirtualProtectEx+0x20       <--- ret2  

4. 0c0c0c10  ffffffff  

5. 0c0c0c14  0c0c0c38  

6. 0c0c0c18  0c0c0c3c  

7. 0c0c0c1c  00000040  

8. 0c0c0c20  77c5ec64 msvcrt!__old_small_block_heap+0x1984  

9. 0c0c0c24  77c39f92 msvcrt!_getptd+0x6d  

10. 0c0c0c28  77c2aacc msvcrt!__sbh_resize_block+0x12c  

11. 0c0c0c2c  0c0c0c48  

12. 0c0c0c30  7c801aec kernel32!VirtualProtect+0x18  

13. 0c0c0c34  ffffffff  

14. 0c0c0c38  0c0c0c64  

15. 0c0c0c3c  00000201  

16. 0c0c0c40  00000040  

17. 0c0c0c44  77c5ec64 msvcrt!__old_small_block_heap+0x1984  

18. 0c0c0c48  77c1f88c msvcrt!_rmdir+0x2e                    < ---- ebp  

19. 0c0c0c4c  77c1f88c msvcrt!_rmdir+0x2e                    < ---- ret2  

20. 0c0c0c50  0c0c0c64  

21. 0c0c0c54  00000201  

22. 0c0c0c58  00000040  

23. 0c0c0c5c  77c5ec64 msvcrt!__old_small_block_heap+0x1984  

24. 0c0c0c60  77c11120 msvcrt!_imp__VirtualProtect  

25. 0c0c0c64  77c51025 msvcrt!_atan_pentium4+0x28d  

26. 0c0c0c68  cccccccc  

27. 0c0c0c6c  0c0c0c0c  

28. 0c0c0c70  0c0c0c0c  

29. 0c0c0c74  0c0c0c0c  

30. 0c0c0c78  0c0c0c0c  

31. 0c0c0c7c  0c0c0c0c  

32. 0c0c0c80  0c0c0c0c  

33. 0c0c0c84  0c0c0c0c  

34. 0:009> u  

35. ntdll!KiFastSystemCall+0x2:  

36. 7c90e4f2 0f34            sysenter  

37. ntdll!KiFastSystemCallRet:  

38. 7c90e4f4 c3              ret  

1. 1: kd> u 7c801a7b   

2. kernel32!VirtualProtectEx+0x1a:  

3. 7c801a7b 50              push    eax  

4. 7c801a7c ff7508          push    dword ptr [ebp+8]  

5. 7c801a7f ffd6            call    esi  

6. 7c801a81 8bf8            mov     edi,eax  

检测逻辑为: 根据ret1确定其所在函数地址NtProtectVirtualMemory, 然后解析ret2的前一条指令,为CALL ESI,寄存器寻址模式,CALL指令,但不能确定目标地址,检查通过。继续向上一层栈帧检查,将ret2赋值给ret1, *(ebp+4)赋值给ret2.

到这里,返回地址ret2的前一条指令:

1. 1: kd> u 77c1f884   

2. msvcrt!_rmdir+0x26:  

3. 77c1f884 59              pop     ecx  

4. 77c1f885 83c8ff          or      eax,0FFFFFFFFh  

5. 77c1f888 5d              pop     ebp  

6. 77c1f889 c3              ret  

7. 77c1f88a 33c0            xor     eax,eax  

8. 77c1f88c 5d              pop     ebp     <--- ret addr   

9. 77c1f88d c3              ret  

view plaincopy to clipboardprint?

1. 1: kd> u 77c1f887  

2. msvcrt!_rmdir+0x29:  

3. 77c1f887 ff5dc3          call    fword ptr [ebp-3Dh]  

4. 77c1f88a 33c0            xor     eax,eax  

5. 77c1f88c 5d              pop     ebp  

6. 77c1f88d c3              ret  

7.   

8. // ebp  

9. 1: kd> dd 77c1f88c l4  

10. 77c1f88c  ccccc35d 8bcccccc ec8b55ff 5608458b  

如上,检查到返回地址的前一条指令正好为一个CALL指令,但是为call [ebp-3Dh]形式的指令,无法确定目标地址,只能算检查通过。继续向上一个栈帧检测,ebp 为*ebp等于ccccc35d,ret2为*(ebp+4)等于8bcccccc,由于8bcccccc 是一个未映射地址,所以不存在前一条指令为CALL,这就表明检测到了ROP。

根据以上分析,可以总结为:
遍历函数调用栈的每一层栈帧, 检查调用栈中当前函数是否被上层函数调用: 首先根据当前栈帧的返回地址ret1 和一个函数信息表,找到返回地址ret1所在的函数地址Func,然后定位到上一层栈帧的返回地址ret2的前一条指令Inst1,如果Inst1不为CALl,返回非法。为CALL指令,如果不能计算出CALL的目标地址,则检查通过,继续向上一层栈帧检测。如果可以计算出目标地址,则检查目标地址是否为Func, 如果为Func则合法,否则继续检查目标地址是否为JMP [XXXX]类型的指令,如果不是,则返回合法,如果是,则继续检查[xxxx]处存放的是否为Func,如果是则合法,否则非法。

view plaincopy to clipboardprint?

1. if ( !inst || inst->type != INSTRUCTION_TYPE_CALL )  

2.   return 0;  //不为CALL,非法  

3. opcode = inst->opcode;  

4. if ( opcode == 0xE8u ) // 0xE8 类型的 Call指令  

5. {  

6.   operand = get_destination_operand(inst);  

7.   if ( operand && operand->type == OPERAND_TYPE_IMMEDIATE )  

8.   {  

9.     dst_addr = _eip + inst->length + operand->immediate;  

10.     true = 1;  

11.     while ( dst_addr )  

12.     {  

13.       if ( dst_addr == func_addr )  

14.         return true;  

15.       if ( dst_addr <= (unsigned int)MmUserProbeAddress )  

16.       {  

17.         // FF25 12345678   jmp     dword ptr [78563412]  

18.         if ( *(_WORD *)dst_addr != 0x25FF )     

19.           return true;  

20.         while ( 1 )                           // *dst_addr  == 0x25FF  

21.         {  

22.           addr = *(_DWORD *)(dst_addr + 2);  

23.           if ( addr > (unsigned int)MmUserProbeAddress )  

24.             break;  

25.           dst_addr = *(_DWORD *)addr;  

26.           if ( dst_addr > (unsigned int)MmUserProbeAddress )  

27.             break;  

28.           if ( *(_WORD *)dst_addr != 0x25FF )  

29.             return dst_addr == func_addr;  

30.         }  

31.       }  

32. ccess_voilation:  

33.       ExRaiseAccessViolation();  

34. isp32:  

35.       dst_addr = *(_DWORD *)_displacement;  

36. t_true:  

37.       if ( sib == true )  

38.         return true;  

39.     }  

40.   } // if ( operand && operand->type == OPERAND_TYPE_IMMEDIATE )  

41.   return 0;   // 如果eip指向的指令不含立即数,返回非法  

42. }  

43. if ( opcode != 0xFFu ||   

44.      (destination_operand = get_destination_operand(inst)) == 0 )  

45. {  

46.     return 0;    // 如果OPCODE 不是0xFF, 即不是GRP5类型的CALL,返回非法  

47. }  

48.   

49. // 下面检查GRP5类型的CALL指令  

50.   

51.  // 目标操作不是寄存器寻址,即如果是内存寻址,则继续检查  

52. if ( destination_operand->type != OPERAND_TYPE_REGISTER ||   

53.      destination_operand->reg > REGISTER_EDI )   

54. {                                               

55.   true = 1;  

56.   // 如果目标操作数是内存寻址, 即是call [eax+xx]这种类型  

57.   if ( destination_operand->type == OPERAND_TYPE_MEMORY  )         

58.   {  

59.     if ( destination_operand->indexreg != REGISTER_NOP )  

60.       sib = 1;  

61.     if ( destination_operand->basereg != REGISTER_NOP )  

62.       sib = 1;  

63.     if ( sib )  

64.       goto ret_true;  

65.   

66.     /* 

67.     call [1234123] 类型的指令 

68.     FF15 3810807C   call    dword ptr [7C801038] 

69.  

70.     FF 15 

71.     FF为GRP5属性的OPCODE, 具体指令由之后的MODRM.REG决定。 

72.     15为MODRM 

73.     00  010 101 

74.     mod reg r/m 

75.  

76.     00  : 内存寻址 

77.     010 : GRP5 calln 

78.     101 : disp32 立即数 

79.     最后得到 : call [12341412] 

80.     */  

81.     displacement = destination_operand->displacement;  

82.     if ( displacement )  

83.       _displacement = displacement;  

84.     if ( _displacement <= (unsigned int)MmUserProbeAddress )  

85.       goto _disp32;  

86.     goto _access_voilation;  

87.   }  

88.   return 0;  

89. }  

90. return 1;  // 寄存器寻址的CALL指令直接返回合法  

ROP检测函数2
遍历调用栈的每一个栈帧,模拟执行每一层栈帧返回地址处的指令。如果为不改变执行流程的普通指令,则继续模拟执行15条指令,如果全部合法,则继续检查上一层栈帧。如果是非法指令或是除ret之外的改变程序流程的指令,则通过检查,继续检查上一层栈帧。如果为RET指令,则检查返回地址的前一条指令是否为CALL,如果不为CALL,则说明检测到ROP。如果为CALL,则继续模拟执行,如果15条指令全部合法,则继续向上一层栈帧检查。

ROP_I检测代码:

view plaincopy to clipboardprint?

1. {                                            

2.      // 模拟执行15条指令,如果返回地址的前一条指令不为CALL,则说明检测到了ROP  

3.      for ( i = 0; (signed int)i < 15; ++i )  

4.      {  

5.        simret = simulate_exec_inst(&inst_info);  

6.        if ( !simret )      // 指令非法退出  

7.          break;  

8.        if ( simret == 1 )  // 除ret外改变程序流程类指令  

9.          break;  

10.        if ( simret == 3 )  // ret 类指令  

11.        {  

12.          // DebugPort 不为NULL,被调试,直接返回正常  

13.          if ( *((_DWORD *)IoGetCurrentProcess() + 47) )  

14.            goto _legal;      

15.          // 如果为ret指令,eip为栈中的返回地址  

16.          addr_ret = inst_info._eip;              

17.          if ( !is_page_execute(-1, inst_info._eip)  

18.            || addr_ret > (unsigned int)MmUserProbeAddress  

19.            || addr_ret < (unsigned int)&value_0x10000 )  

20.            goto _illegal;  

21.          if ( *(_BYTE *)(addr_ret - 1) != 0xCCu || *(_BYTE *)(addr_ret - 6) != 0xE9u )  

22.          {  

23.            // 检测返回地址的前一条指令是否为call指令  

24.            if ( !is_call_prev_inst(addr_ret) )   

25.              goto _illegal;  

26.          }  

27.          else  

28.          {  

29.            inst_info._eip = addr_ret + 6;  

30.          }  

31.        }  

32.      }  

33.    }  

view plaincopy to clipboardprint?

1. // 模拟执行返回地址处的指令  

2. // 如果非法返回0  

3. // 如果是除ret之外的改变程序类指令,如jmp等,返回1  

4. // 如果是普通不影响指令流程的指令则,返回2  

5. // 如果ret 类指令返回 3  

6.   

7. // 分析栈中返回地址处的指令  

8. // 如果非法返回0  

9. // 如果是除ret之外的改变程序类指令,如jmp等,返回1  

10. // 如果是普通不影响指令流程的指令则,返回2  

11. // 如果ret 类指令返回 3  

12. int __stdcall simulate_exec_inst(PINST_INFO rop_i_info)  

13. {  

14.  int inst_len; // edx@1  

15.  int result; // eax@2  

16.  ULONG __esp; // eax@7  

17.  ULONG ret_addr; // ecx@8  

18.  ULONG _esp1; // eax@8  

19.  ULONG __esp1; // eax@9  

20.  unsigned int ___ebp; // ecx@17  

21.  ULONG _esp; // ecx@23  

22.  ULONG _ebp; // ecx@24  

23.  ULONG __ebp; // ecx@38  

24.  INSTRUCTION inst; // [sp+8h] [bp-E4h]@1  

25.   

26.  inst_len = get_instruction(&inst, (BYTE *)rop_i_info->_eip, 0);  

27.  if ( !inst_len )  

28.  return 0;// 非法返回0  

29.  if ( inst.type != INSTRUCTION_TYPE_RET )  

30.  {  

31.  if ( inst.ptr->flags1 & P_x )  

32.  return 1;// 如果第一个操作数具有可执行属性,如jmp 1234指令,则返回1  

33.  result = 2;  

34.  if ( inst.type == 2 )// INSTRUCTION_TYPE_MOV  

35.  {  

36.  if ( inst.op1.reg != REGISTER_ESP || inst.op2.reg != REGISTER_EBP )  

37.  goto ret_normal2;  

38.  _ebp = rop_i_info->_ebp;  

39.  goto ret_normal2_ebp;  

40.  }  

41.  if ( inst.type == INSTRUCTION_TYPE_ADD )  

42.  {  

43.  if ( inst.op1.type != 2 || inst.op2.type != INSTRUCTION_TYPE_MOVSR )// 2 OPERAND_TYPE_REGISTER  

44.  goto ret_normal2;  

45.  if ( inst.op1.reg != REGISTER_ESP )  

46.  {  

47.  if ( inst.op1.reg != REGISTER_EBP )  

48.  goto ret_normal2;  

49.  rop_i_info->_ebp += inst.op2.immediate;// add ebp, xx, 调整ebp  

50.  __ebp = rop_i_info->_ebp;  

51. LABEL_41:  

52.  if ( __ebp > (unsigned int)MmUserProbeAddress )  

53.  return 0;// 非法指令  

54.  goto ret_normal2;  

55.  }  

56.  rop_i_info->_esp += inst.op2.immediate;// add esp, xxx, 调整esp  

57.  }  

58.  else  

59.  {  

60.  if ( inst.type != INSTRUCTION_TYPE_SUB )  

61.  {  

62.  if ( inst.type == INSTRUCTION_TYPE_PUSH )  

63.  {  

64.  rop_i_info->_esp -= 4;// push xxx, 调整esp  

65.  rop_i_info->_b_push = 1;// push 指令,置1  

66.  goto ret_normal2;  

67.  }  

68.  if ( inst.type != INSTRUCTION_TYPE_POP )  

69.  {  

70.  if ( inst.type != INSTRUCTION_TYPE_INT )  

71.  {  

72. ret_normal2:// 如果不是改变程序流程类指令,则返回2  

73.  rop_i_info->_eip += inst_len;// 调整eip  

74.  return result;  

75.  }  

76.  ___ebp = rop_i_info->_ebp;  

77.  rop_i_info->_esp = ___ebp;// int xxx, esp = ebp  

78.  goto LABEL_18;  

79.  }  

80.  if ( inst.op1.reg != REGISTER_ESP )// pop xxx  

81.  {  

82.  if ( inst.op1.reg != REGISTER_EBP )  

83.  goto LABEL_21;  

84.  ___ebp = rop_i_info->_esp;  

85. LABEL_18:  

86.  if ( ___ebp <= (unsigned int)MmUserProbeAddress )  

87.  {  

88. LABEL_20:  

89.  rop_i_info->_ebp = *(_DWORD *)___ebp;// pop ebp: ebp = *esp; esp = esp+4;  

90. LABEL_21:  

91.  rop_i_info->_esp += 4;// esp+4  

92.  rop_i_info->_b_push = 0;  

93.  goto ret_normal2;  

94.  }  

95. LABEL_19:  

96.  ExRaiseAccessViolation();  

97.  goto LABEL_20;  

98.  }  

99.  _esp = rop_i_info->_esp;  

100.  if ( _esp > (unsigned int)MmUserProbeAddress )  

101.  goto LABEL_19;  

102.  _ebp = *(_DWORD *)_esp;  

103. ret_normal2_ebp:  

104.  rop_i_info->_esp = _ebp;// pop esp, esp = *esp  

105.  goto ret_normal2;  

106.  }  

107.  if ( inst.op1.type != 2 || inst.op2.type != OPERAND_TYPE_IMMEDIATE )// 2 OPERAND_TYPE_REGISTER  

108.  goto ret_normal2;  

109.  if ( inst.op1.reg == REGISTER_ESP )// sub esp, xxx  

110.  {  

111.  rop_i_info->_esp -= inst.op2.immediate;// esp = esp - xxx  

112.  }  

113.  else  

114.  {  

115.  if ( inst.op1.reg != REGISTER_EBP )  

116.  goto ret_normal2;  

117.  rop_i_info->_ebp -= inst.op2.immediate;// sub ebp, xxx : ebp = ebp - xxxx;  

118.  }  

119.  }  

120.  __ebp = rop_i_info->_esp;  

121.  goto LABEL_41;  

122.  } // if ( inst.type != INSTRUCTION_TYPE_RET )  

123.  if ( rop_i_info->_b_push )//   

124.  // 前一条指令为push,当前指令为ret 。   

125.  // push xxx; ret 相当于jmp xxx, 属于改变程序流程指令,返回1  

126.  return 1;  

127.  __esp = rop_i_info->_esp;  

128.  if ( __esp > (unsigned int)MmUserProbeAddress  

129.  || (ret_addr = *(_DWORD *)__esp,  

130.  _esp1 = __esp + 4,  

131.  rop_i_info->_eip = ret_addr,// 执行了ret指令, 相当pop eip  

132.  rop_i_info->_esp = _esp1,// 调整esp  

133.  inst.op1.type)  

134.  && (__esp1 = inst.op1.immediate + _esp1, rop_i_info->_esp = __esp1, __esp1 > (unsigned int)MmUserProbeAddress) )// ret n : pop eip, esp = esp+ n  

135.  return 0;  

136.  return 3;// 如果为ret指令,则返回3  

137. }