Securing software by enforcing data-flow integrity是一篇2006年发表的论文,首次提出了DFI数据流完整性的概念,对今后防御不改变控制流的攻击形式有启发意义。
本文是对该文的部分核心内容的整理,其中也有一些本人自己的思考。如文中有不当和有误之处,敬请指出,谢谢。
欢迎转载交流,但请注明出处。
Data-flow Integrity,数据流完整性,简称DFI。数据流完整性执行(DFI enforcement),可以分为三个阶段。
第一阶段,对程序使用静态分析计算数据流图(Data flow graph,DFG)。
第二阶段,给程序装载保护机制,确保程序在执行中的数据流被控制流图所允许。
第三阶段,运行装载了保护机制的程序,当程序的数据流完整性被破坏时,抛出异常。
我们使用一个简单的例子说明这三个阶段的工作。
int authenticated = 0;
char packet[1000];
while (!authenticated) {
PacketRead(packet);
if (Authenticate(packet))
authenticated = 1;
}
if (authenticated)
ProcessPacket(packet);
图1 示例漏洞C代码,模拟了SSH认证登录的过程。PacketRead函数读取客户端的数据并解析,如果认证通过则调用ProcessPacket函数处理。
我们假设PacketRead函数没有对输入的packet长度做检查,因此这段代码有明显的缓冲区溢出漏洞。攻击者可以对这个程序执行两种攻击:
- 覆盖返回地址RET。这是典型的Control-data attack控制数据攻击,允许攻击者获得修改执行控制流。
- 覆盖局部变量authenticated。这是Non-control-attack,攻击者因此可以绕过认证,使其发送的packet被ProcessPacket函数处理。
我们接下来可以看到,DFI执行可以防御上述两种攻击。
第一阶段
我们使用可达定义(到达-定值)分析(RDA,Reaching definitions analysis,这是代码分析学里数据流分析的一种方法,请参见论文AHO, A. V., SETHI, R., AND ULLMAN, J. D. Compilers: Principles, Techniques and Tools. Addison Wesley, 1986.。还可以看看南大的软件分析课程)来计算静态数据流图。使用可达定义分析的术语,我们称呼:一个向某内存位置写数据的指令,“定义”(Define)了该内存位置的值;一个读取该值的指令,“使用”(Use)了这个值。RDA计算了每个use(为了表意清楚,下文都会使用英文)的可达定义(Reaching definition)集合,并给每一个definition都赋给了一标识符作为标记。RDA最终返回了一个从指令到definition标识符的映射和一个包含每个use的可达定义标识符集合,我们称之为“静态数据流图”。
还是以图1为例。变量authenticated在第4行和第11行被Use。如果我们对源代码使用RDA,它可能会告诉我们第1行和第8行的definition可达这两个use(想要理解这句话最好还是详细了解RDA)。因此,这两个use的可达definition标识符的集合可表示为{1, 8}——如果我们使用行号作为定义的标识符的话。
这种分析不一定是精确的,换言之,它有些保守。我们在代码分析学里把RDA划分为“may analysis”一类。简单说,RDA要求所有可能在运行时到达一个指定use的definition,都应当被包含到集合当中去,这可能导致集合里有些冗余的元素。还是以图1为例,不难看出,在实际执行中,如果authenticated不为1,程序不可能跳出循环,因此事实上只有第8行的定义才能到达第11行的use。但在RDA分析里,集合必须包含第1行的标识符。这说明,RDA的分析可能导致漏报(假阴性),但不可能有误报(假阳性)。因此,数据流完整性执行可能会漏报攻击,但不可能在没有错误的情况下报警。从作者的观点看,这是一件好事。因为大多数用户并不喜欢误报,即在没有错误的情况下让程序崩溃。
第二阶段
我们为程序装载能够保证DFI的防御机制。在此作者为DFI给出了高度概括的定义:无论何时一个值被读取,写值的语句的definition标识符都必将在被读的语句(use语句)的可达定义集合当中。
程序装载的防御机制,会在运行时计算到达每个read的definition,检查definition是否在静态分析计算出来的可达定义标识符集合当中。为了在运行时计算可达定义,我们维护了一个数据结构:运行时可达定义表,runtime definition table,简称RDT。RDT可以记录每个内存位置最后一个写入指令的标识符。每一个写入指令都将会被用于更新RDT。我们用被读值的存储地址来从RDT种检索标识符。然后,我们检查这个标识符是否在之前静态分析计算出来的可达定义标识符集合当中。
以图1为例,在程序运行到第8行的时候,防御机制会将RDT[&authenticated]赋值为8,表示目前最后一个define变量authenticated的指令在第8行;当程序运行到第11行时,则会执行检查,看看RDT[&authenticated]是否属于集合{1, 8}。
在文中,我们假设攻击者可以向任何内存地址写入任何数据,并且拥有执行权限。因此,DFI的防御机制必须能够避免攻击者篡改RDT,篡改代码,或者用其他手段绕过我们的防御机制。
如何避免上述bypass呢?为了避免攻击者篡改RDT,我们将检查写指令的目标地址是否在给RDT所在的内存空间范围中,任何企图写入RDT内存的尝试都会触发异常;至于其他的bypass,交给DEP和ASLR等等现有防御机制即可。