-
-
[翻译]通过静态分析检测二进制代码中的 UAF 漏洞
-
发表于: 2018-1-9 14:04 5237
-
Use-After-Free是现代漏洞利用中一种知名的漏洞(比如,2016年的Pwn2own)。在AnaStaSec中,AMOSSYS一直致力于通过静态分析检测二进制代码中的UAF漏洞。在这篇博客中,我们描述了科学界建议采用的检测这种漏洞的方法。我们的最终目标是定义一种通用的方法,帮助我们开发出一个满足我们需要的POC(译者注:Proof of Concept,概念验证)工具。
UAF的概念非常容易理解。Use-After-Free漏洞出现于程序试图访问一个已被释放的内存。在这个例子中,我们创建了一个悬挂指针,并让它指向一块已被释放的内存。
举例来说,下面的例子就会引发一个UAF漏洞。如果这块代码被执行,且error(译者注:指if语句中的error变量)分支被执行,就可以会发生一些无法预料的事情,因为ptr指向了一块非法内存区域。
···
char * ptr = malloc(SIZE);
图片 1: UAF的例子
也就是说,UAF漏洞出现于下列3步发生的时候:
这个伪代码分配了两个内存块,分别由变量A
和B
引用。然后,内存块A在释放(Free(A)
)之前被访问(Use(A)
)。之后,内存块被再次访问。
通过定义两个主域(一个是被分配的堆元素集合,另一个是被释放的元素集合)。这些集合可能在每一条指令中被更新,并检查是否访问引用了被分配内存块。如图4所示。
图4:通过域检查检测UAF
当内存块·A·两次被访问时,它已经在之前的步骤中被释放了,所以这个分析会给出一个警告:一个UAF漏洞被检测到。
另一个方法是通过一个简化的状态机检测想要的特征,如论文[Ye14]中描述的那样。这个想法是在内存被分配后,指向内存块的指针被设为“已分配”状态,如果他们没被释放,就会一直保持这个状态。当他们被释放之后,他们就会转为“已释放”状态。如果在释放状态的指针被使用了,就会导致“UAF”状态。然而,如果指针和它的别名被删除并不再引用,它们就不再有害,并进入“结束”状态。简化的状态机如图5所示。
图5:用于检测UAF的简化状态机
论文[Gol10]还提出了另一种方法,使用图论。在这篇论文中,作者会检查使用指针的语句是在释放内存的代码之前还是之后。如果在之后,就发现了一个UAF漏洞。
图6:有潜在UAF漏洞的图
在所有的例子中,当悬挂指针被检测到,最后阶段的分析描述了通过提取出子图会导致UAF漏洞。这个子图不得不包含所有的需要人手工检查的元素,以避免失误。
我们已经展示了几种使用静态方法检测UAF漏洞的方法。我们尝试解释这些方法可能会引发的问题,人们很容易理解,没有一个直接的方法可以用于检测这种漏洞。
我们在这篇论文中看到的只是一小部分的研究者发布的开源项目。项目GUEB就是其中之一,它由Verimag团队的Josselin Feist开发,如果你对这个项目感兴趣(并且熟悉Ocaml代码),我们欢迎你查看他的Github。
作为结束,研究项目AnaStaSec对AMOSSYS来说,是一次分析当前学术界利用并改进现有工具,对UAF检测的机会。尽管这篇文章没有发布任何新的东西,我们期待在接下来的几年这样做。敬请期待!
略(参见原文)
本文章由看雪翻译小组 梦野间 编译
原文链接:http://blog.amossys.fr/intro-to-use-after-free-detection.html
if (error){ free(ptr); } … printf("%s", ptr);
1. malloc(A); 2. malloc(B); 3. use(A); 4. free(A); 5. use(A);
- 指针指向一块被分配的内存区域
- 内存区域被释放但指针还在
- 指针被用于访问之前已被释放的内存
大多数时候,一个UAF漏洞可能导致信息泄漏。但更有趣的是UAF可以通过下列步骤导致代码被执行。 - 程序分配然后释放了内存块A
- 攻击者被分配了内存块B,内存块B重新使用了先前已被分配给内存块A的内存
- 攻击者向B中写入数据
- 程序使用了被释放的内存块A,访问了攻击者留下来的数据
在c++中,这通常发生于类A被释放而攻击者成功地给类B分配了类A的内存区域。然后,类A的方法被调用,类B中加载的代码就会被执行。
既然我们已经明确了UAF是什么,让我们深入研究一下学界是怎么检测这样的漏洞的。静态和动态分析的正反两方面
有两种分析二进制文件的方法:静态和动态。目前想要动态分析整个代码是非常难的,因为我们难以找到一个输入数据,使程序能执行到所有的分析路径。因此,如果你更关注代码覆盖率的话,静态分析是一种更合适的方法。
然而,尽管如此,论文 [Lee15] 和 [Cab12]里指出,大多数学术作品仍然使用动态分析检测UAF漏洞。因为在动态分析中能够更容易地检测到相同的指针,也就是别名。也就是说,当考虑用动态的方式时,内存区域中的值可以被直接访问,从分析的角度来说,这是不可忽视的。当进行动态分析时,会获得更高的准确性但难以保证完整性。
从我们的角度来说,我们重点关注静态分析,学术界针对静态分析给出了两个难点:
- 事实表明循环中出UAF漏洞的可能不大。因此,在处理二进制时,处理循环的第一次迭代是必要的。因此,之前介绍的第 1 个难点中的停机问题就可以避免了。
- 为了解决第 2 个困难,我们需要建立一个中间表示(IR,Intermediate Representation)来使表示独立于处理器架构。比如,x86汇编代码太复杂了,因此它有非常多的指令。因此,其中一个解决方案就是在一套更小的指令集里进行分析。使用中间表示,每一条指令被转换为几条原子指令。至于如何在多种IR之中进行选择,取决于分析的类别。在大多数情况中,我们使用逆向工程中间语言(REIL,Reverse Engineering Intermediate Language),而BAP([Bru11])或Bincoa([Bar11])是其他的用于学术工作的IR。
只有17条指令构成REIL IR,每一条只有最多一个值。BinNavi工具可以用于将原生x86汇编翻译成REIL。BinNavi是一个开源项目,目前由Google进行维护(之前是Zynamics)。BinNavi可以把IDA Pro 的数据库文件作为输入,非常方便。符号执行对抽象解释
在生成IR之后,我们目前有两种方法可以用于分析二进制文件的行为:抽象解释([Gol10]和[Fei14])和符号执行([Ye14])。
粗略地说,由符号执行进行的分析都遵循指令集。符号执行使用程序中表达式和变量的符号,而不是输入的实际值。因此,这样的分析并不会跟踪变量的值,但是会将各个值用符号代替,建立算术表达式。这些计算出来的表达式可以用于检查条件分支。
在另一部分中,抽象解释是一种基于被分析的程序可以进行抽象的想法。因此,不需要追踪每个变量的精确值。语义可以被替代为抽象语义,这样我们就可以描述指令对变量的影响。例如,变量可以由它们的符号进行定义。对于一个加法指令,操作符的符号会被检查用于设置结果的符号。因此,如果操作符是是+,结果就是+,但是变量的精确值并不会被计算。我们可能会定义许多抽象域名而不是符号。例如,我们可能通过存储单元上的变量区间追踪变量的值。(全局,堆或是栈)。一种著名的分析使用这种称之为值设置分析(Value Set Analysis,VSA)的表示方法。
作为一个具体的例子,框架monoREIL是一种依赖于REIL IR的VSA引擎。它简化了VSA算法的开发以使开发者在他们的抽象域名中进行VSA。分析中间表示
下一个问题是在通过DFG浏览的时候怎么实现分析算法。这一次,仍然有两种方法: - 程内分析,限制是只能在当前函数域中进行
- 程间分析,能够进入子函数当中
不用多说,程内分析比程间分析更容易实现。然而,如果想要检测UAF漏洞,就必须跟踪他们被分配的内存块...并且他们可能发生在不同的函数中。
这就是为什么论文[Gol10]提议首先进行程内分析,然后再扩大到全局的程间分析。如图2所示。对于每一个函数,都会创建一个块,这些块对函数的行为进行总结,并把它们的输入和输出链接起来。因此,当分析扩大到程间分析的时候,每一个函数调用都会被替换为先前程内分析这个函数的时候的结果。该方法的主要优点是只需要分析一次,尽管他们会被调用很多次。此外,程内分析在非常小的代码块内进行,因此非常精准。
图2:程间分析合并许多程内分析的结果
另一个解决方案在论文[Fei14]提出。第二个方法(如图3所示)把一些被调用函数以内联函数的形式嵌入调用者函数中。因此,函数调用不再是一个问题。这个方法可能更容易实现,但是限制是,函数被调用两次就会被分析两次。因此,这个方法会消耗更多的时间和内存。
图3:把内联函数放进单一函数中的程间分析检测UAF
到现在,我们已经可以在语义上分析二进制代码以及绘制控制流图。我们现在想要检测的是UAF特征。让我们再看一下下列定义:一个UAF漏洞的出现有两个不同的特征: - 悬挂指针的创建
- 对这个指针指向内存的访问
为了检测这个特征,[Fei14]这篇论文建立了一个被释放堆内存区域的集合,并且检查每一次指针的使用看它使用的区域是否已经被释放了。
举一个简单的例子,让我们看下列伪代码。注意,为了简化,这个例子并没有展现一个复杂的CFG。确实,这个方法依靠选中的分析方法和它的实现处理CFG...这个例子的目的只是在进行代码分析的时候,展示一种检测UAF的方法。
- 最大的问题是如何管理程序中的循环。确实,当计算一个循环中变量所有可能的值时,我们需要知道循环进行了多少次。这就是计算机界著名的停机问题。在计算理论中,停机问题也就是确定,程序是否能运行结束,或是说会一直运行下去。不幸的是,这个问题被证明为不可判定。也就是说,对于任意的程序和任意的输入,没有通用的算法,可以用于解决停机问题。在这种环境下,如果要解决这个问题,静态分析工具对问题进行简化。
- 另一个困难是如何表示内存。一个天真的解决方法是使用一个大数组保存内存中值的指针。然而,这并不像看起来那么简单。一块内存地址可能被填充为许多可能的值,或是一些变量可能有很多的地址。此外,如果有很多可能的值,把每个可能的值都保存下来是不合理的。正如先前提到的,在表示内存的时候,不得不做一些简化。
为了减少静态分析的复杂性,一些论文,比如说[Ye14],或是像 Polyspace和Frama-C这样工作在C源码级的工具,因为在这个级别中包含最多的信息。然而,我们通常都没有这些需要被分析的应用程序源码的访问权。从二进制到中间层表示
如果我们关注二进制分析,第一步就是要建立相关的控制流图(CFG)。控制流图是一种有向图,它表示了程序执行时所有的路径。CFG中的每一个节点都是一条指令。与边相连的两个节点表示两条相继执行的指令。一个有两条边相连的节点表示一个条件跳转。因此,通过建立一个CFG,我们可以把二进制代码组织成指令的逻辑序列。建立一个可执行文件的CFG的一个著名方法是使用反汇编器IDA Pro.
当处理二进制时,学术论文似乎总想以同一种方式处理UAF漏洞。论文 [Gol10] 和[Fei14]的处理细节如下:
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!
赞赏
- [原创]分享一个基本不可能被检测到的hook方案 44347
- [翻译]利用Qiling框架实现带有代码覆盖率信息的PE文件模拟执行 22914
- [翻译]android11中的系统加固 18068
- [翻译]使用hook绕过EDR内存保护 18310
- [翻译]SATURN反混淆框架 15721