cgnail's weblog | A quantum of academic

Practical Control Flow Integrity & Randomization for Binary Executables (2013)

| categories notes 
tags 漏洞检测  控制流完整性  内存错误  程序分析  binary分析  Oakland 

原文:Practical Control Flow Integrity & Randomization for Binary Executables

##解决的问题

现有的ASDL、DEP、SafeSEH等内存保护机制,对控制流劫持(control-flow hijacking)攻击不太有效。针对该问题一个自然的办法是实现控制流完整性(Control Flow Integrity,CFI):保证程序运行时的控制流转移和其控制流图一致。它可以阻止各种控制流劫持,包括ROP(Return Oriented Programming)攻击。

现有的CFI技术或者功能不全,或者代价、负载很高。本文则提出了一种只需修改二进制代码的,低负载、兼容未保护代码的CFI保护方法。可以抵御ROP攻击和return-to-libc攻击。

##相关工作

###二进制反汇编和重写

反汇编的一个难点在于区分代码段中的数据,各家都用了很多假设和启发式算法。DEP的部署使得编译器混合代码和数据时更受约束。

第一个指出开启DEP/ASLR模式生成的二进制代码也可以用relocation table彻底分析。

###控制流劫持

DEP/ASLR防不住ROP和return-to-libc攻击。现有的一些ROP防止技术会带来新的攻击变种,或者负载很高,或者功能有限。

###控制流完整性

CFI最初由Abadi提出,CFI保护需要标记间接跳转的目标,然后对每次跳转的出发点和返回点,检查标号的匹配性。他们在控制流图上对目标进行标记。然而,精确的CFG需要用到指针分析,对于没有源代码或调试符号的时候很难处理。

软件错误隔离(Software-based Fault Isolation)采用了另一条思路:利用指令重写对软件提供隔离而不是加强

##工作原理

控制流转移分为四类:异常、直接跳间接跳转和返回指令。前两种现有方法已能提供保护,所以这里只考虑后两种。其中,间接跳转的目标则从内存中读入或写入,返回目标则是在运行时计算的。

alt text

###前提假设

  1. ASLR和DEP启用
  2. 目标文件不会自修改或动态生成代码
  3. 漏洞提供的信息泄漏有限,如不会泄漏完整的内存布局等

###找到间接转移目标

正确的反汇编x86的PE文件比较困难,但对于使用了ASLR/DEP技术的文件,却存在更多的推导规则。利用ASLR所使用的重定位表,作者可以找出间接代码的入口、跳转的生成点和使用点。

这部分工作由BitCover部件完成。

###插入跳板段(springboard section)

思路:将间接跳转的目标改为指向新开辟的springboard section,在跳板段中写入stub代码,由stub跳转至原定目标。这样,可以在跳转前/返回前加入检查,只有来自跳板段的地址才是合法的。从而阻断攻击者对间接跳转地址的篡改。

跳板段定义在地址第27位为0的位置,即:将程序地址空间分为2^27bit(128M)的块,跳板段位于第偶数个块,其它代码段则位于第奇数个块。通常一个exe文件的代码段不会大于10M。这样只要作比特位检查就可以知道是否在跳板段了。

间接跳转目标可分为三类:函数指针、安全敏感函数的返回值和一般函数的返回值。对这三类分配不同的位掩码,可将它们区分开,以便检查时应用不同的规则。

这部分工作由BitRewrite完成:插入跳转至跳板区的代码,以及在原跳转代码前插入检查,确保跳入跳板区。如果没有跳入,则you异常处理函数记录相关信息。

###安全检查

总的来说,安全检查保证所有的间接跳转都从跳板段过

这部分工作由BitVerify完成。

###兼容性问题 不可能把系统里所有的可执行文件都改写(Win 7 甚至不允许改写DLL文件),所以从改写过的可执行文件跳转至未改写的可执行文件的时候会产生兼容性问题:一个有跳板段,一个没有。这些对外部目标的跳转可分为四类:

  1. 导入的函数指针
  2. 运行时解析的函数指针
  3. 未导出函数指针
  4. 返回未保护的模块

对于1,正常情况下,其地址在程序加载时由动态链接器存入IAT(Import address table)表,代码段中用IAT的entry指代。作者将代码段中的IAT的entry用stub的entry代替,stub也放在跳板段中,它负责跳往正确的函数。

对于2,这些函数通常在动态链接库中,在运行时用_LoadLibrary_调用。通常地址可以由_GetProcAddress_获得,显然地址是在运行时计算出来的。对于这种情况,需要保证包含此类函数stub的跳板段在运行时可写。作者将代码段中的_GetProcessAddress_的调用用其stub代替,和上面不同的是,stub并不直接调用_GetProcessAddress_,而是它的封装函数,封装函数除了调用_GetProcessAddress_之外,还要负根据_GetProcessAddress_获得的函数地址在跳板段创建该函数的stub,并将stub的指针作为返回值传给_GetProcessAddress_的调用者。

这两种情况涵盖了大部分情况,后两种很少见,BitRewrite也没法直接处理。解决办法是对无法增强的dll遍历,收集可能的间接跳转目标,做成hash表。当保护机制报错时,错误处理函数检查出错处是否在hash表内,如果在,则返回原有的控制流,否则终止进程,返回需要保存调用前的寄存器信息。

###辅助安全增强措施和随机化

上述工作可以阻止诸如ROP gadget的攻击,但缓冲区溢出仍可以将函数指针用其它合法的指针替换,如果这些指针的值可以被猜到或被泄漏的话。

应对措施是:

  1. 对于安全敏感函数,如:_system, excel, WinExec, CreateProcess, LoadLibrary, GetProcAddress,Virt*, fopen, CreateFile, longjmp_等,统统使用直接调用
  2. 在加载时对跳板段的stub随机化其位置,再用一个段记录stub的地址,就像重定位表一样。

##实验

CCFIR借用了反汇编库Udis86,BitCover+BitVerify有5000行C++代码,BitRewrite也是5000行。

利用SPECint2000和SPECfp2000,在Win7 32位机器上做benchmark。BitCover和BitRewrite平均开销在秒级,BitVerify也不超过1分钟。

加强后的二进制文件的运行开销最多不超过10%,而且消灭了ROP问题。

If you liked this post, you can share it with your followers !