POMP: 硬件辅助的程序崩溃分析

作者:Jun Xu    Pennsylvania State University

在软件开发周期中,尽管代码会经过多次的审查和测试,被发布的版本仍然可能包含缺陷。当这些缺陷被触发后,软件通常会异常终止(崩溃)。据统计,大型软件提供商每天可能会接收数以百万计的崩溃报告 [1]。 引起这些崩溃的缺陷可能有相当部分是安全漏洞,比如缓冲区溢出和Use-After-Free。由此可见,不能及时有效的分析这些崩溃可能遗漏安全问题,进而导致入侵事件的发生。

当软件崩溃时,操作系统通常会保存程序的终止状态。例如,Linux内核观测到一个软件进程异常终止时,会将这个进程的内存以及寄存器状态存入Core Dump文件中,以方便后续分析。分析过程中,程序员通常采取一种回溯的方式。简单来说, 他们从引起程序崩溃的指令出发,首先定位导致问题的异常数据,然后往回推导这个异常数据是通过哪些指令传播的。通常情况下,最根本的缺陷便位于这些传播指令的集合中。

在本文[2]中,我们提出POMP系统。POMP借助于Intel处理器提供的硬件特性Processor Tracing (PT) 来自动化实现以上提到的程序崩溃分析。在介绍POMP的技术细节之前,我们先简要总结 PT 的背景知识。PT是Intel从第五代处理器引入的新特性,其主要功能是记录程序执行过程中的指令跳转信息。比如,在执行间接跳转指令 [ jmp *eax ] 时,PT会将eax的实时数值记录下来。因此离线分析工具就知道程序在这条指令后的目标指令。简而言之,根据PT记录的信息,我们可以完全重构程序执行过的指令序列。并且由于PT的记录完全在硬件层次实现,对于程序的执行效率基本没有损耗。

图1: 栈溢出实例以及指令序列

POMP将程序崩溃时的状态 (Core Dump) 以及PT记录的信息为输入,分两步进行自动化分析。第一步中,POMP首先根据PT提供的信息以及可执行文件重构控制流,即软件崩溃前执行过的指令序列。图 1 展示了一个缓冲区溢出并且导致程序崩溃的实例。在这个例子中,main函数调用了子函数child (15行)。child函数中出发了缓冲区溢出(7行)导致main函数中的函数指针func被覆盖。child函数退出后,main函数调用func函数指针(16行)。由于此时该函数指针被溢出污染,程序在这时候发生崩溃。图 1 右侧展示了根据PT提供的信息重构的指令序列。

图 2:根据图 1 恢复的内存状态

重构指令序列后,POMP会进行第二部,即尝试逆向恢复数据流。具体来说,程序崩溃时,POMP从Core Dump得到最后一条指令执行时的内存状态。然后从最后一条指令出发,进行逆向计算来重现各指令执行前的内存状态。

图 2 展示了图 1 示例的内存恢复状况。第 T20 列展示了程序崩溃时(即指令A20执行前)的内存状态。我们开始逆向计算A19,A18 …。逆向计算指令A19: mov eax, [ebp – 0xc] 时,POMP期待恢复eax在这条指令之前的数值。然后由于mov指令的不可逆特性,我们无法完成数值恢复。实际上,存在一个类似于mov的不可逆指令集合,比如xor等。回滚不可逆指令的效果是POMP面临的第一个挑战。这里我们采取结合正向执行的信息来辅助分析。再次以A19为例,我们发现在正向执行的过程中,eax的值在A19之前是由指令 A15 定义的而且 A15 的语义确定 eax 的值是0。借助于此,我们可以完成对指令A19的逆向计算。其结果如图 2 第 T18 列所示。我们接着逆向计算指令A18 (add esp, 0x4)。这条指令可逆并且其逆函数是 (sub esp, 0x4)。据此我们可以完成对于指令A18的逆向计算。继续上述过程,POMP可以完成整个指令序列的逆向计算。

在上述逆向执行以及正向分析的过程中,POMP还可能遇到由于未知地址的内存访问引入的问题。仍以图 1 为例,指令A15会覆盖eax的值。根据正向分析,eax的值可以由指令A12确定。该条指令 mov eax, [ebp+0x8] 将位于[ebp+0x8]位置的内存拷贝进入eax。直观上来说,我们只需要将[ebp+0x8]位置的内存值从Core Dump中取出即可。但是我们发现指令A14对于内存位置[eax]进行了写入操作。由于eax的值此时未知,那么我们可能面临两种情况。第一种,A14中的eax等于A12中的ebp + 0x8;第二种反之。第二种情况下,我们可以直接将Core Dump中位于[ebp+0x8]位置的内存拷贝进入eax,然后第一种则不允许我们如此操作。这个问题我们总结为指针同名问题。POMP通过引入假设检验来解决这个问题。对于不知是否同名的两个内存访问,我们作出两个假设:第一个假设这两个内存访问同一个位置,而第二个假设反之。在某一个假设前提下,我们继续逆向执行的过程。如果出现数值冲突,那我们就可以拒绝这个假设。在以上的例子中,如果我们假设A14中的eax等于A12中的ebp + 0x8,那么我们有 eax = ebp + 0x8。进一步的分析我们可以发现这个假设引起了冲突。由此我们得知这两个内存访问的是不同的位置。当然在一个假设的前提下,我们可能需要作出另一层假设。由此我们的假设可能出现迭代的情况。在迭代的状态下,必须出现所有的迭代假设都不成立,我们才能拒绝最原始的假设。

图 3. 样例测试

在解决指针同名问题的时候,传统的二进制分析方法比如value-set-analysis [3] 也可以被使用。但是静态分析往往要求程序的执行会服从编译器的规定,比如对于栈和堆内存的修改有固定的指令模式。然后在安全漏洞,尤其是memory corruption发生的状态下,编译器的规定往往被破坏,使得静态分析不再可靠。我们的假设检验的优势便在于没有额外的条件要求。

完成数据流的分析后,我们沿着数据流做逆向的污点分析并且标记出最终导致崩溃的异常数据的传播途径。此途径便是POMP生成的最终的报告。以图 1 的示例为例。导致崩溃的数据是eax;eax在指令A19从 [ebp –  0xc] 传播而来;[ebp –  0xc] 从指令A14传播而来。由此POMP在这个例子中将报告A14, A19, A20为传播路径。

我们采集了32个不同软件的漏洞,并且通过公开的PoC触发了这些漏洞以造成对应软件的崩溃。利用POMP对于这些崩溃进行自动化分析,结果展示如图 3。我们发现POMP能够在短时间内(通常是几分钟)完成逆向分析而且POMP可以将导致崩溃的根本原因包含在一个比较小的指令集合内。

POMP的源码发布在 https://github.com/junxzm1990/pomp。欢迎使用和建议。

[1] Cui, W., Peinado, M., Cha, S. K., Fratantonio, Y., & Kemerlis, V. P. (2016, May). Retracer: Triaging crashes by reverse execution from partial memory dumps. In Proceedings of the 38th International Conference on Software Engineering (pp. 820-831). ACM.

[2] Jun Xu, Dongliang Mu, Xinyu Xing, Peng Liu, Ping Chen, and Bing Mao. Postmortem Program Analysis with Hardware-Enhanced Post-Crash Artifacts. In Proceedings of 26th USENIX Security Symposium (USENIX Security 17), Vancouver, Canada.

[3] Balakrishnan, G., & Reps, T. (2004). Analyzing memory accesses in x86 executables. In Compiler Construction (pp. 2732-2733). Springer Berlin/Heidelberg.

作者简介:

Jun Xu (徐军), Pennsylvania State University 博士在读,师从刘鹏教授。本科毕业于中国科学技术大学,曾获郭沫若奖。研究兴趣为软件和系统安全,以及恶意软件防护。研究成果曾发表于CCS, Usenix Security, SenSys等会议。个人主页: http://www.personal.psu.edu/jxx13

Bookmark the permalink.

Comments are closed.