ROP攻击检测技术-ROPGuard

摘要:本文将介绍一种用于防止返回导向编程(ROP)攻击的技术,称之为ROPGuard。 该技术不需要知道所保护程序的源代码以及其它信息,从而确保它可以应用于任何进程,且不会遇到编译器级解决方案的常见问题。此外,它也不会重写受保护进程的整个可执行代码,以确保受保护进程的稳定性及低开销,即使进程已经在运行,也可以添加对该进程的保护。后面将通过实际测试统计,证明该技术速度非常快,并且不会在受保护的应用程序中引起任何稳定性问题。

该系统基于漏洞利用攻击的特点:当排除在进程环境中可以完全执行攻击(如XSS攻击)的情况时,攻击者必须使用ROP攻击技术,并且与其他进程或者内核进行交互,这种交互包括创建进程、打开和写入文件等。

基于这种特点,我们可以定义一个关键函数的概念:关键函数是攻击者通过调用,能达到不再需要ROP技术转而直接进行攻击的那些函数。比如:

  • CreateProcess :调用之后,攻击者可以创建另一个进程,然后来直接执行恶意行为。
  • VirtualProtect、VirtualAlloc、LoadLibrary:调用之后,攻击者可以绕过系统自身的漏洞利用缓解机制,不要需要ROP这种复杂且有诸多限制的技术,从而轻松执行恶意代码。
  • OpenFile、WriteFile:调用之后,攻击者可以打开并写入任意文件

ROPGuard会在这些关键函数时执行检查,通过以下几点,来确定ROP攻击是否发生。

  • 关键函数是如何被调用的?
  • 关键函数执行后会发生什么?
  • 系统当前状态是否与正常程序执行时一致?
  • 执行关键函数是否会危害系统的安全性?

仅在关键函数调用的时候执行检查可以以很小的性能开销取得不错的保护效果,通过配置文件,还可以灵活的增加或减少触发检查的关键函数,来达到增强保护或减少性能开销的效果。如果为了更好的安全效果,甚至可以添加关键函数对应的内核函数,以避免攻击者绕过用户层函数。

下面将详细描述ROPGuard执行检查的技术细节。

一、检查堆栈指针

在绝大多数的ROP攻击中,攻击者都需要控制堆栈,当利用缓冲区溢出漏洞时这很容易,但是在利用其它内存破坏型漏洞时,就需要通过特殊的技巧,比如通过EIP和一个通用寄存器(为简单起见假设为EAX),那么控制堆栈的常用技巧就是将值从EAX移动到堆栈指针,因此EAX指向的内存将成为“堆栈”,ROP gadgets如下:

1
2
3
4
MOV ESP, EAX
RETN
XCHG EAX, ESP
RETN

除非EAX本身就指向堆栈,否则这会导致堆栈指针不再指向当前线程指定的内存区域,当前线程堆栈指定的区域可以在运行时从未记录的线程信息快中获取Win32 Thread Information Block,因此在每个关键函数的调用中,ROPGuard都会检查堆栈指针是否位于当前线程堆栈的边界内,如果不是,这就表明正在发生ROP攻击。

二、在堆栈上检查是否存在关键函数地址

如果通过RETN进入关键函数而不是通过CALL或者JUMP类指令,那么执行流程在进入关键函数时,当前堆栈指针(ESP)上方必定存在着关键函数地址。如果使用RETN指令(opcode C3)来进入关键函数,则在ESP-4处,如果使用RETN n指令(opcode C2),则在ESP-n-4处。

根据这一观察结果,在进入关键函数之后,ROPGuard将堆栈上指定数量的DWORDS保存下来,防止在检查前发生了改变。之后检查是否存在关键函数地址,如果存在,则可能是通过RETN进入的关键函数,这表明可能正在发生ROP攻击。为了避免误报,默认仅保存4个字节(单个DOWRD,对应不带参数的RETN指令),可以通过配置文件轻松修改。

三、检查返回地址

在进入关键函数时,返回地址就位于栈顶,返回地址必须满足以下条件:

  • 它必须是可以执行的
  • 返回地址的指令必须紧跟在call指令之前

除了上述两项检查外,ROPGuard还可以验证返回地址之前的CALL指令的目标是否与当年关键函数的地址相同,这项检查兼容直接调用和间接调用,因为ROPGuard在每个关键函数的入口点保存所有通用寄存器的状态。例如返回地址之前的指令是CALL EAX,则ROPGuard检查eax中的地址是否指向当前关键函数的地址。如果EAX指向不可读的内存位置,则ROPGuard将在此触发异常而不是显示告警,这同样会停止ROP利用尝试。

要注意的是,在有些情况下,CALL指令的目标不会与关键函数的地址相同,比如CALL指令目标是重定向到关键函数的跳转指令。ROPGuard可以检测大部分这种情况,但为了避免误报的可能性,默认配置禁用了CALL指令目标检查(开头两项检查默认启用)。

四、检查栈帧

如果程序的编译方式使用EBP寄存器作为栈帧指针,则利用此特点可以检查堆栈上信息的一致性,不仅可以检查关键函数的返回地址(第三节内容),还可以检查调用关键函数的函数的返回地址。

例如如果程序仅使用EBP作为栈帧指针,那么在检查到EBP指向堆栈外的位置时,这就表明堆栈已被破坏并且正在发生ROP攻击(ROP攻击通常都会覆盖掉EBP),下面给出执行栈帧检查算法的伪代码。

由于我们通常不知道程序是如何编译的,因此默认禁用此项保护,当清楚程序是如何编译时启用此保护以提高安全性。在实验过程中,观察到绝大多数的Windows组件仅使用EBP作为堆栈指针(尽管有些函数根本不使用帧指针),因此是否启用此保护取决于第三方应用程序和插件(利用Internet Explorer包含的Flash模块、SKype、PGP)。

在大多数情况下,将“RequireFramePointers”配置选项的值设置为false,可以避免由此导致的误报,当此选项设置为false时,ROPGuard将假定某些函数将EBP用做其他目的而不是作为帧指针,因此无论何时遇到EBP的值不指向堆栈的情况,检查函数都会正常返回,而不是产生报警。但是在某些应用中,任然有可能遇到EBP指向堆栈但不用作栈帧指针的情况,之后可以通过制作已知不忽略帧指针检查的模块的白名单来解决。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
if (当前帧指针不指向堆栈){
if (RequireFramePointers选项 == true){
警告正在发生ROP攻击;
}
else {
return;
}
}
if (帧指针指向堆栈指针的上方){
警告正在发生ROP攻击;
}
for (配置文件中指定的帧数){
获取当前帧的返回地址; //在帧指针下面。
if (返回地址为空){
return; //到达了堆栈的底部
}
if (返回地址不可执行或不在CALL指令之前){
警告正在发生ROP攻击;
}
获取新的帧指针 //指向当前帧ptr。
if (新的帧指针不在前面的帧指针之下或者不指向堆栈){
if (RequireFramePointers选项 == true){
警告正在发生ROP攻击;
}
else {
return;
}
}
}

五、模拟执行流程

虽然使用堆栈帧结构来检查关键函数返回的函数的返回地址在检测ROP攻击时非常有用,但缺点在于程序必须编译为使用帧指针。为此,通过观察可以发现,大多数ROP gadgets是以RETN结尾的短序列指令,那么我们可以尝试在关键函数返回后模拟少量指令的执行,由此可以在不依赖帧指针的情况下检查出调用关键函数后会发生什么。

当前,ROPGuard仅模拟改变堆栈的指令(PUSH、POP、ADD ESP, #、 SUB ESP, #、RETN ),并跟踪这些指令对堆栈指针值的修改,其它指令只是简单的跳过。一旦遇到RETN指令,ROPGuard将检查返回地址是否可执行以及返回地址处的指令是否在调用指令之前。通过这种方式,不仅可以检查当前关键函数的返回地址,而且如果当前的关键函数是从RETN结束的ROP gadgets中调用的,那么还可以检查到后续的返回地址。当模拟到配置文件中指定的指令数或者遇到改变执行流的指令(RETN除外)时,模拟执行停止。

在以后的版本中,这种方法可以扩展为模拟其他指令(包括间接CALL和JMP指令),并跟踪除ESP之外的其他寄存器的更改,这将使ROPGuard更加有效的检测到非RETN结尾的ROP gadgets。下面给出检测算法的伪代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
instruction_pointer = 当前关键函数的返回地址;
for (配置文件中指定的指令数量){
current_instruction = instruction_pointer处的指令;
解码current_instruction并更新instruction_pointer;
if (当前指令修改堆栈指针){
计算堆栈指针的新值;
}
else if(当前指令是RETN或RETN n){
return_address = 堆栈顶部的值;
if (返回地址不可执行或不在CALL之前){
警告正在发生ROP攻击;
}
instruction_pointer = return_address;
stack_pointer = stack_pointer + (n+1)*4;
}
else (当前指令改变执行流程){
//在目前版本的ROPGuard中,我们只专注于寻找以RETN结束的ROP gadgets
停止模拟;
}
}

举个例子,假设从下面这条ROP Gadget调用了关键函数,并且堆栈已经对齐,以便此gadget中的RETN将程序执行流程引导至不可执行的内存区域(例如,当前的关键函数是VirtualProtect,攻击者的意图是使用它来使目标内存页变为可执行,然后返回进入这个内存页)。

1
2
3
CALL EAX; 
NEG EBX;
RETN;

在这种情况下,第三节中所述的关键函数返回地址检查将会通过,但是,在模拟执行流程检查中,ROPGuard将在返回地址(NEG EBX)处对指令进行解码,简单略过这条指令并执行下一条指令RETN。当遇到RETN指令时,ROPGuard将检查返回地址,而此时返回地址的内存区域还不可执行(VirtualProtect还未执行),所以会进行ROP攻击告警。

要启用关键函数返回后的流程模拟,必须知道每个关键函数返回时从堆栈弹出的DWORD的数量,并在配置文件中提供(通常对于Windows API,这个数字与参数个数相同)。

六、特殊函数的检查

除了前面提到的一般性检查之外,还可以执行与关键函数参数相关的附加检查,来判断关键函数是否可能会危害系统安全。ROPGuard目前执行两项特殊函数的检查,也可以轻松的添加其他额外检查。

  • ROPGuard可以阻止更改堆栈的内存保护选项的VirtaulProtect调用。调用VirtualProtect来使堆栈可执行是利用ROP攻击的常用方法之一:Corelan ROPdb
  • ROPGuard可以阻止通过SMB加载库的LoadLibrary(以及类似的)调用。通过SMB加载库是一种众所周知的方法,使用该方法可以使用单个(关键)函数调用来执行任意代码。Source Boston 2010: Practical Return-Oriented Programming 1/6

本文节选翻译自:Runtime Prevention of Return-Oriented Programming Attacks

原作者:Ivan Fratric

Demo项目地址:ROPGuard

由于译者水平有限,因此不能保证译文准确无误,敬请批评指正。

文章作者: TechOtaku
文章链接: http://techotaku.me/2018/06/14/ROP攻击检测技术-ROPGuard/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 徒然の博客