多模块ROP碎片化自动布局方法
2020-07-10黄曙光潘祖烈
黄 宁,黄曙光,潘祖烈,常 超
(国防科技大学 电子对抗学院, 安徽 合肥 230000)
随着信息技术的发展,软件漏洞的发掘与利用成了一个热点问题。针对不同类型的漏洞利用技术,各种保护机制也层出不穷。但是,多年的漏洞利用实践证明,由于各方面条件的限制,依然存在许多可绕过这些保护机制,成功实施漏洞利用的技术手段。
以二进制程序漏洞利用为例,由于计算机无法区分内存空间中二进制码的代码或数据属性,可能导致进程执行外部数据,从而造成控制流劫持攻击[1]。针对这一问题,Linux和Windows等主流操作系统相继引入了数据执行保护(Data Execution Prevention, DEP)机制[2]。该机制的基本原理是,通过标记程序内存页为可执行/不可执行,实现内存空间代码区和数据区的区分。在DEP环境下,位于数据区的恶意代码将无法被执行,从而阻止控制流劫持攻击[3]。
由于DEP机制不会拦截可执行页面中的代码指令,solar designer提出了ret to libc(ret2libc)方法。该方法通过劫持程序控制流,使程序跳转至已有的系统函数。Schacham等[4-5]在ret2libc思想的基础上,提出了返回导向式编程(Return Oriented Programming, ROP)技术。相比ret2libc,ROP使用更小的汇编指令片段(gadget),提高了该类方法的泛用性。Lu等[6]基于Rix的方法,提出了可压缩、可打印的ROP构造方法,提高了ROP载荷的灵活性。
近年来出现多种针对控制流劫持类漏洞自动化分析[7]与测试用例自动生成技术[8-10]。Schwartz等提出了面向二进制程序漏洞的ROP自动构造框架Q[11-12]。其工作流程:首先,向Q框架可执行文件,搜索其中具备特定功能的gadget集合;对面向gadget的高级语言进行语义分析,构建中间指令序列;分析中间指令序列,为每条中间指令分配合适的gadget集合,形成ROP链。Q框架的局限性[13]在于,该方案生成的测试用例仅从功能实现的角度出发,未考虑ROP布局对程序内存可控性条件的要求,降低了ROP链的实用性。
为解决上述问题,本文提出了基于符号执行的ROP碎片化自动布局方法。该方法将ROP链以模块为单位,切割成长度不一的碎片;使用导向式符号执行技术,引导源程序运行至控制流劫持点的同时,检查程序中可控内存区域是否满足ROP模块布局要求;以ROP模块切片与可控内存区域布局为根据,构建碎片化ROP链的数据约束;通过求解数据约束,自动生成满足程序可控内存分布条件的碎片化ROP链。该方法解决了ROP链对内存可控性要求高的问题,提高了ROP链的实用性。
1 ROP技术原理
ROP技术基于ret2libc技术发展而来。该技术主要针对DEP机制未限制代码页中已有代码的执行权限这一缺陷,实现DEP机制绕过[14]。其主要原理是,通过搜索程序内存代码页,构建以ret等跳转指令结尾的汇编指令片段gadget集合。从gadget集合中筛选出符合条件的部分,组合成一段可实现特定功能的ROP代码链。图1表示了一个ROP链的代码执行顺序及其相应的栈空间数据分布。
图1 ROP链在栈内存中布局结构示意图Fig.1 ROP chain and the structure of stack
Q框架以自定义高级语言ROPL表示ROP链的目标功能程序。ROPL高级语言到汇编指令序列的转换过程为:ROPL高级语言—ROPL中间表示序列—中间指令序列—gadget汇编指令序列(ROP链)。
目标程序模块指的是一个可执行相对独立功能的ROPL代码序列的集合,其结构类似于C语言中的函数。
ROPL语言框架下,多模块目标程序定义为:目标程序包含至少两个以上模块,且调用了至少一个以上非main模块。多模块ROP中,除main模块外的ROP模块的切换过程由以下三个子过程组成:目标模块开始调用过程,目标模块参数初始化过程,目标模块返回调用过程。
2 整体思路
已有的ROP自动生成技术主要解决了gadget搜索与分类,高级语言语义分析,以及面向中间指令的gadget分配与排列三个方面的问题,实现了ROP链自动构造。但从实际运用效果看,Q框架仍然无法满足多数场景下源程序的内存可控性状态对ROP链布局的限制。为解决这一问题,本文在Q框架的基础上,提出了基于碎片化布局的ROP自动构造方法。该方法构造碎片化布局的ROP链过程如图2所示。
图2 ROP碎片化自动布局过程示意图Fig.2 Overview of ROP fragmented layout and automatic generation
在Q框架生成ROP载荷与目标程序符号表的基础上,本文将结合对源程序可控内存区域的检查结果,构建碎片化ROP约束,求解约束,生成碎片化ROP链。
为了避免符号执行对每条程序路径遍历导致的路径爆炸问题,本文在符号执行工具S2E的基础上,采用了经过路径选择算法优化的导向式符号执行技术[15-16]。以crash文件作为源程序的输入文件,可引导源程序沿着确定的程序路径动态运行,直至触发控制流劫持状态。图3是通过导向式符号执行触发源程序控制流劫持状态的过程。
图3 导向式符号执行路径选择过程示意图Fig.3 Path selection of source program with path-oriented symbolic execution
本文在导向式符号执行过程中,收集程序堆栈状态与可控内存区域状态。结合ROP载荷,分析上述状态是否满足ROP链的布局条件,并构建相应的ROP数据约束。通过约束求解,可实现碎片化布局的ROP测试例自动生成。
3 具体实现
3.1 可控内存区域搜索
如图1所示,ROP链是由一个特定顺序的gadget序列组成的。由于每个gadget均以ret指令结束,并以此控制程序跳转至下一个gadget所在地址,其地址及gadget的相关操作数需存放于程序的堆栈中。当源程序处于控制流劫持状态时(即指令寄存器中的数值为符号值),堆栈是否有足够的可控空间用于ROP布局,决定了ROP是否适用于源程序。为此,本文首先对源程序内存状态进行分析,确定ROP布局条件。
针对本文方法的内存分析过程涉及的关键数据检查与状态变化,本文有如下定义:
symbolicBlock
stackPtr:表示首次控制流劫持状态下的当前栈顶指针。
stack_symbolicLength:若当前栈顶位置处于符号化区域中,该数值表示以stackPtr为起始地址的一段连续的符号化内存长度。
mLenid表示ROP中名称为id的模块占用内存长度。每个模块的长度信息均记录于中间表示符号表中。
在一般的溢出漏洞中(比如栈溢出漏洞),覆盖当前栈顶位置的污点数据的起始地址通常位于上一个函数栈帧中。但对于执行gadget序列来说,需要关注的只是源程序的控制流劫持时刻的栈顶数据属性。因此,本文将在源程序控制流劫持时刻,从栈顶位置开始进行符号化检查。若源程序栈顶位置不为符号化数据,表示源程序无法跳转至第二个gadget,不满足ROP开始执行的初始条件;若栈顶数据为符号化数据,计算以栈顶位置开始的符号化数据长度。图4显示的是源程序控制流劫持时刻,满足ROP布局条件的栈结构示意图。
图4 源程序控制流劫持时刻的栈内存结构示意图Fig.4 Structure of stack at the time of control-flow hijacked
为满足当前栈帧可控空间不足情况下ROP的布局要求,可控内存搜索算法将搜索并记录进程用户态内存空间中其余的符号化区域信息。其过程如算法1所示。
算法1 可控内存区域搜索
3.2 ROP链碎片化自动布局
本文从可控内存区域集合memSet中寻找满足ROP模块布局条件的元素。候选的符号化区域长度symbolicSize需要至少满足容纳某一ROP模块,且该区域包含的范围与源程序控制流劫持状态下的栈顶指针不应相互冲突。对集合memSet中的所有symbolicBlock元素进行第一轮过滤,选出若干符合ROP模块长度条件的内存区域,将该元素加入对应ROP模块的候选区域集合lenBlocksid中,该模块的候选区域集合需满足如式(1)所示条件。lenBlocksid表示经过第一轮过滤后,模块名为id的ROP对应的所有候选区域集合。
lenBlocksid:
∀block∈symbolicBlock|(symbolicSize>mLenid)∧[(stackPtr
(1)
在候选区域长度满足ROP模块长度要求的基础上,通过比较候选区域数据可控性约束与ROP模块数据约束的兼容性,对每个ROP模块的候选区域集合进行第二轮过滤。第二轮过滤完成后,ROP模块id对应的候选区域集合conBlocksid应满足式(2)所示条件。
conBlocksid:
∀block∈lenBlockid|(canArea⊆block)∧ (canArea.Size≥ropModuleid.Size)∧Eq(area,moduleid)=true
(2)
式中,canArea表示候选区域block中任意连续的可控内存区域;canArea.Size表示canArea的长度;ropModuleid.Size表示ROP模块id的长度,moduleid表示该模块的数据约束;函数Eq用于实现约束条件A与约束条件B的兼容性比较。
result=Eq(A,B)
当约束比较函数Eq(A,B)的返回值result为true时,表示约束条件A与B可兼容;为false时,表示A与B不可兼容。
当且仅当模块ropModule的数据约束module与canArea区域可控性约束area的兼容性判断结果为true时,该ROP模块是可执行的。数据约束module与area相兼容的检查条件包括以下两项:module中所有连续字节的约束与area所有连续字节的可控约束相兼容;module中剩余的字节数应不大于area中剩余的字节数,即canArea应有足够的可控空间容纳该ROP模块。
针对area与module的兼容性比较过程以字节为单位。对ropModuleid中的每个字节与canArea区域中每个字节的约束条件逐一比较,构造待求解的模块数据约束mConstraint。该过程如算法2所示。
算法2 ROP模块数据约束构造
算法2中,若isAvailable返回值为true,表示canArea区域满足ROP模块id的布局条件,并将canArea区域标记为不可控区域后,重新构建可控内存区域集合memSet。图5表示完成碎片化自动布局后,多模块ROP的执行过程。针对剩余ROP模块,重复执行第一轮与第二轮过滤,直至所有ROP模块完成布局区域分配。该过程如算法3所示。
图5 碎片化多模块ROP执行过程示意图Fig.5 Execution process of fragmented multi-module ROP
算法3 ROP碎片化约束构造
4 实验与分析
4.1 ROP碎片化布局可行性分析
以代码1中的ROPL高级语言作为目标程序。通过对目标程序进行语义分析,其对应的高级语言中间表示符号表如代码2所示。
代码1
代码2
针对代码1所示目标程序生成的ROP链长度为224(0xE0)B。该ROP链分为3个模块:模块main的长度为76(0x4C)B;模块f1的长度为76(0x4C)B;模块foo的长度为72(0x48)B。
为验证通过本文方法实现的ROP链在实际程序中的碎片化布局效果,选取了7个包含控制流劫持漏洞的源程序进行实验验证。本文将可能覆盖程序内存关键位置的污点数据标记为符号值,并通过构建ROP数据约束及约束求解,判断相应的内存位置是否满足ROP布局条件。表1为各实验程序的实验环境。
表1 漏洞程序实验环境
通过漏洞触发代码触发源程序首次控制流劫持状态。本文对首次控制流劫持时刻的程序内存状态进行分析,内存各区域可控污点数据情况如表2所示。
表2 控制流劫持状态时刻源程序可控内存区域分布情况Tab.2 Layout of controllable tainted data in memory at the first time of control flow hijacked
注:“*”表示栈内存中的可控数据长度从当前栈顶位置开始计算表示。
对比表1与表2中的数据可发现,部分漏洞程序的引入污点数据长度与首次控制流劫持状态时刻的内存各部分可控污点数据长度并不完全一致。造成这一现象的原因主要有以下几种:
1)引入污点数据通过复制等操作传播至内存其他区域,造成部分污点数据的约束条件有重合部分。属于该类情况的漏洞程序包括CVE-2017-11882等。
2)污点数据覆盖其他内存区域的关键数据。属于该类情况的漏洞程序包括CVE-2014-0322等。
此类漏洞利用方式为,通过数组越界读写,实现程序任意内存地址读写,进而导致函数地址覆盖与程序控制流劫持。由于函数地址在内存代码段,因此不在本文对ROP布局内存分析的范围内。
3)污点数据符号属性丢失或污点数据不可控。属于该类情况的漏洞程序包括CVE-2010-3333等。
CVE-2010-3333是栈溢出漏洞,但在栈内存中,可控污点数据仅在栈顶位置向下16 B的范围内。对于不可控的污点数据,由于不具有利用价值,因此,本文未做统计与分析。
在对源程序内存可控污点数据布局状态分析的基础上,针对代码1目标程序构造的ROP链经过碎片化布局处理后,各模块ROP在内存空间中的分布如表3所示。
表3 各模块ROP在源程序内存中的布局情况
MS06-055的栈内存仅满足main模块的布局条件。本文通过堆喷射漏洞触发代码,实现源程序内存中的堆块大量布局,并将写入堆块的数据标记为污点数据。通过内存分析,确定进程的堆内存满足f1与foo模块的布局条件。
CVE-2010-3333的栈内存不满足任一模块布局条件。本文对该漏洞实验做了手工调整,在其栈内存中布置了简化的堆栈伪造指令,使其堆栈指针指向堆内存区域。通过分析,该进程堆中的可控数据区域满足代码2所有模块的布局条件。
CVE-2012-0158漏洞的栈内存布局情况满足代码2所有模块的布局条件,因此未将任何一个模块布置于其他内存区域中。
CVE-2014-0322与CVE-2015-5119漏洞的栈内存不符合代码1中模块布局条件。为了验证碎片化布局方法,本文首先通过伪造栈空间的方法,在堆内存中开辟一段伪造的栈内存。与上述两个漏洞布局进行对比,f1与foo模块均布置于堆内存中。实验结果证明,CVE-2014-0322与CVE-2015-5119的实验程序内存满足表3所示的布局条件。
根据表2,CVE-2017-11882栈内存的可控空间大于代码1 ROP所需的内存空间长度。但通过进一步分析发现,该漏洞程序的栈内存空间过于碎片化,其中最大的一块栈内存可控区域长度仅为120 B,不足以容纳代码1中的任意两个模块。因此,本文仅将main模块布局于栈内存中,f1与foo模块布局于堆内存中。CVE-2017-11882的ROP碎片化布局实验结果如表3所示。
CVE-2018-8174漏洞程序的栈内存可控空间仅满足进程跳转功能,不满足代码1 ROP模块布局功能,因此,该漏洞程序的ROP模块全部布局于堆内存中。
4.2 案例分析
本文挑选了CVE-2014-0322漏洞实验样本,对其ROP碎片化布局过程进行具体分析与阐述。
当符号执行工具S2E检测CVE-2014-0322样本程序进入控制流劫持状态时,首先检查源程序状态结果为:
栈顶指针寄存器ESP:0x0307B7D4;
符号化寄存器:EAX;EIP;
符号化内存区域:0x0307B7D4~0x0307B7D7;0x10000000~0x28180000;0x28180000~0x28280000。
如上文所述,控制流劫持状态下的样本程序栈内存不符合任何一个ROP模块的布局要求。为此,需要向样本程序中加入伪造栈空间的步骤。该步骤通过下述gadget完成。
XCHG EAX, ESP;
RETN;
该gadget的内存地址为0x5ED6E850。
伪造的栈空间栈顶指针为:0x1A1B3010。根据对比,发现该区域位于符号化区域中。
针对main 模块进行两轮可控内存区域过滤后,该模块的最终布局范围为:0x1A1B3010~0x1A1B305C。
针对f1模块进行两轮可控内存区域过滤后,该模块的最终布局范围为:0x1A1B4010~0x1A1B405C。
针对foo模块进行两轮可控内存区域过滤后,该模块的最终布局范围为:0x1A1B5010~0x1A1B5058。
4.3 系统时间开销
为验证本文方法的时间开销,本文记录了该方法的时间开销为t1。时间开销t1的定义为:从源程序开始符号执行,到生成碎片化布局的ROP测试用例所消耗时间。
作为对比,本文还记录了符号执行工具S2E对源程序进行分析的时间开销t2。时间开销t2的定义为:从源程序开始符号执行,到生成控制流劫持路径测试用例的消耗时间。对各实验样本进行10次实验,各样本的平均时间消耗如图6所示。
图6 碎片化ROP布局方法与S2E系统时间开销对比Fig.6 Comparison of analysis time by ROP fragmented layout method and S2E
由于本文提出的碎片化ROP布局方法是在源程序控制流劫持状态下,通过内存分布状态分析实现的,因此,图6中各实验样本的时间开销之差,即t1-t2,基本可被视为可控内存区域分析过程的时间消耗。
图6所示实验样本中,t1与t2的差值较大。原因是crash文件触发源程序控制流劫持状态过程中,使用了堆喷射技术,使源程序内存空间存在大量可控区域,导致内存搜索算法运行时间增加。
5 结论
针对目前ROP自动化构造过程中存在的空间效率低与内存布局要求高等问题,提出了基于符号执行的多模块ROP碎片化自动布局方法。该方法以ROP模块为单位,在动态分析源程序可控污点数据分布情况的基础上,实现各模块ROP的碎片化分布,降低了ROP布局对源程序内存布局条件的要求。
此外,本文提出的基于碎片化布局的多模块ROP自动生成方法仍然存在局限性。首先,ROP模块调用过程未考虑地址随机化对目标模块地址寻找的影响。其次,符号执行过程中造成的污点数据符号属性丢失等问题会影响该方法对源程序内存状态分析的结果。如何减少上述问题对ROP自动生成过程的影响,是下一步工作的研究重点。