栈布局随机化与Android二进制文件的最小重写
1 引言
栈在维护和管理执行过程中的运行时数据(例如函数调用上下文、参数和局部变量)方面起着至关重要的作用。许多攻击基于对栈上此类信息的泄露或篡改。例如,传统的代码注入攻击通过覆盖敏感数据(如返回地址和函数指针)来执行注入的恶意代码[1],以及最近出现的代码复用攻击,通过将现有的代码小工具串联起来实施恶意行为[2–5]。
此类基于栈的攻击通常要求攻击者对栈布局有充分了解。具有可预测栈布局的应用程序通常面临较高的此类攻击风险。这一对了解栈布局的要求在近期的面向返回编程中变得更加关键(ROP)攻击中尤为突出,因为攻击者需要投入更多精力在栈上安排数据,以将各个代码小工具串联起来[2,6–9]。
随机化栈布局是一种自然的应对措施,可使攻击者更难定位关键数据。已有研究提出对操作系统进行修改以引入此类随机化。例如,地址空间布局随机化(ASLR)会对许多代码/数据段的基址进行随机化,广泛应用于x86和移动平台[10–12]。然而,研究人员一直在质疑这些通过修改操作系统实现的随机化技术在有效性(或随机性程度)[13,14],和完备性[15–17],方面的表现,许多人声称它们可能被诸如面向返回编程之类的高级攻击技术所绕过[5,8]。
随机性也可以仅通过修改应用程序本身来引入,而无需修改操作系统[18–20]。然而,这种方法目前被认为是一种较不理想的解决方案,主要原因是应用程序的二进制重写难度较大,且其适用性相对较低,引入的随机性程度也有限[21]。对可执行文件进行二进制重写可能会带来问题,特别是当结果二进制文件中某个函数的大小增加时,这意味着后续所有函数中的指令都必须移动,所有受影响的跳转目标也都需要重新计算。尽管有时可以通过诸如函数重排序之类的技巧来避免此类问题[13],但总体而言,这仍是一个尚未解决的问题,严重限制了其适用性。
本文探讨了在无需操作系统支持的情况下,通过对可执行二进制文件进行最小化重写,能在多大程度上引入栈布局的随机性。通过这种最小化的二进制重写,我们施加了限制,即不允许插入或删除指令,这也意味着程序大小将保持不变。
我们表明,在对安卓应用程序进行二进制重写时,利用ARM架构的特殊特性,可以为许多函数实现最高达7位的合理随机化程度。我们的方案无需对Android操作系统进行任何修改。其主要思想是在函数的前言中,随机化需要被压栈的一组寄存器(以及相应需要弹栈的寄存器)。例如,一个函数可能被包围在push {r3,r4,r5,lr}和pop {r3,r4,r5,pc}指令之间,以保存调用函数中使用的寄存器。我们的技术会随机选择一组寄存器的超集,例如{r1,r3,r4,r5,r8,r9,lr},将其压入(并弹出)栈。这一改动实际上在栈上添加了随机数量的数据,并通过随机偏移量使栈帧中的所有其他数据发生位移。这种设计背后的直觉是,该改动仅需对压栈和弹栈指令进行简单的变异,既不会改变ARM架构下指令的长度,也不会改变应用程序的整体大小。
我们实现了一个概念验证的二进制重写器,以自动对安卓应用进行这种随机化。我们证明了许多现有的代码复用攻击在我们的随机化安卓应用上不再有效。我们在谷歌应用商店中最受欢迎的20个免费安卓应用上的实验还表明,该随机化成功应用于超过97.6%的函数,这是一个显著的结果比之前最先进的随机化技术所能达到的65%有所提升[15]。当函数使用32位ARM/Thumb指令时,平均可获得7位随机性以改变其在栈上的位置;若使用16位Thumb指令,则可获得4位随机性。针对格式化字符串漏洞和真实世界ROP攻击的实验表明,我们提出的随机化方法在防御现实世界攻击方面是有效的。
2 背景与威胁模型
由于我们提出的技术涉及对在ARM设备上执行的安卓应用进行二进制重写,因此我们首先简要介绍一些有关ARM指令和寄存器的必要背景知识。同时,我们也介绍了我们提出方案所基于的威胁模型。
任何ARM二进制文件(包含来自共享库或Dalvik字节码编译的原生代码)都可能同时包含ARM和Thumb‐2指令。ARM是一种32位固定长度指令集。Thumb‐2由16位Thumb指令发展而来,构成了一种混合使用16位和32位指令的指令集。这带来了灵活性和性能;然而,指令长度的差异也使得二进制分析和重写更加困难。
ARM架构为ARM和Thumb‐2指令提供了16个32位长度的核心寄存器。这些寄存器被标记为r0到r15。其中r12到r15也被称为ip、sp、lr和pc寄存器。在函数调用期间,r0到r3寄存器用于存储参数(如有需要),lr用于存储返回地址,r0用于保存返回值。一个函数通常会使用部分但并非全部寄存器。
我们假设一种威胁模型,其中攻击者拥有原始应用(无随机化)的副本,并了解我们随机化算法的全部细节。攻击者可能还拥有多份已随机化的应用程序副本,但其并不掌握受害者正在使用的特定随机化版本。我们还假设该应用可能包含某些攻击者已知的可利用的漏洞。
3 栈布局随机化和应用场景
回想一下,我们的目标是在安卓应用执行时向栈布局引入随机性,并且在没有操作系统支持的情况下通过最小化的二进制重写来实现。在本节中,我们介绍设计的高层设计思想以及一些可能应用我们提出方案的场景。
3.1 随机化设计
图1展示了安卓应用中一个函数的原生代码及其执行时对应的栈布局。该函数首先将寄存器r4、r5、r6和lr压栈,然后执行其操作,在此期间r4、r5和r6用作临时存储,最后将数据从栈中弹出到r4、r5、r6和pc。随机化此栈布局的目的是使攻击者无法预测栈帧上数据的位置。考虑到仅通过二进制重写实现此目的的可能方法(回顾我们不想修改操作系统),可以在基址处引入随机填充(如之前的一种最先进的随机化技术[10]中所做的那样),或在栈帧中的数据对象之间引入随机填充[15,16]。
然而,由于我们需要在不增加或删除指令的前提下进行最小化二进制重写,因此无法在各个数据对象之间引入填充。但我们可以通过修改压栈指令,使其额外随机压入一组通用寄存器,如图2所示,从而有效实现栈帧基址的随机化。在此示例中,我们额外压入了r2和r7。一般而言,对于16位Thumb指令,可被压栈和弹栈的通用寄存器集合包括8个寄存器r0到r7;而对于ARM和32位Thumb指令,该集合包含13个寄存器r0到r12。此外,lr和pc也可能出现在该列表中。
这种压栈和弹栈随机寄存器组的设计满足了我们对minimal二进制重写的需求,因为ARM架构使用单条压栈或出栈指令即可压入或弹出任意数量的寄存器,且压栈/弹栈不同寄存器组的指令长度相同——这一特性与x86平台大不相同。
为了在正确执行时与原始应用保持语义等价,我们需要注意几点。首先,必须压栈和弹栈相同的寄存器集合;否则我们的修改可能会改变调用函数的执行上下文。其次,在压栈和出栈指令之间的栈帧上对内存地址的任何引用都应使用修改后的偏移量进行更新。最后请注意,当r0用于存储返回值时,不能将其添加到填充寄存器集合中,因为弹栈r0会覆盖已存储的返回值。
3.2 应用场景
我们的系统可以实现为一个第三方安卓应用,以对目标应用引入随机化(并可能对其进行重新打包和重新签名)。这满足了我们在不修改操作系统的情况下实现栈布局随机化的目标。然而,我们可以通过一些方式来提升用户体验和安全性。
我们可以在应用安装后立即执行二进制重写。需要重写的二进制文件可能是安装包中包含的原始原生代码,也可能是安装过程中编译的oat文件(当使用ART运行时时)。这种方法无需对重写后的应用进行重新打包。
我们还可以在每次应用程序启动执行时都进行二进制重写。这样做的优点是,每次加载应用程序时都会使用新的、不同的随机化方式,使得攻击者更难预测栈布局。
在这两种情况下,只需对Android应用安装器或加载器进行minimal的修改,且二进制重写对最终用户完全透明。还需注意,我们提出的随机化可与其他现有的安全性机制(例如安卓上的ASLR)结合使用。
4 实现
作为概念验证实现,我们的自动二进制重写已在Python中实现,约2000行代码。它以任意安卓应用为输入,输出可用于在安卓4.0或更高版本上安装和执行的随机化应用。我们首先从安卓应用中提取二进制文件,对其进行反汇编,并识别函数以及压栈和pop指令对。对于每个已识别的指令对,我们通过抛硬币决定,并应用我们的随机化设计相应地更改寄存器集合。之后,我们更新受影响指令中操作数的偏移量,以与所应用的随机化保持一致。在本节其余部分,我们将详细介绍每一步的实现及其涉及的复杂性。
4.1 静态分析
在此步骤中,我们发现所有可作为随机化候选对象的函数,找出所有push和pop指令对,并识别出所有需要更新偏移量的指令。我们使用Hopper1,一个功能强大的反汇编器,来反汇编二进制文件。
然而,ARM指令(32位)与Thumb指令(16位和32位)的混合,以及指令之间存在的嵌入式常量,有时会导致Hopper在反汇编所有指令时出现错误。我们以Hopper的分析结果作为参考,并进行更深入的分析,以确保静态分析的完整性和正确性。
函数发现。 我们的分析通过从导出函数表中列出的函数开始,并追踪blx和bl的控制流目标,递归地反汇编指令和函数。当blx和bl的目标是一个永远不会返回其调用者的函数时(例如,目标为异常处理程序),Hopper无法识别这些调用后的函数。附录A中的列表1展示了这种情况的一个示例。
我们通过识别Hopper识别出的函数中的多个函数序言指令来解决此问题,这些序言指令表明了一个新函数的识别。
压栈/弹栈指令。 对于发现的每个函数,我们需要找到所有收尾指令以应用随机化,从而确保正确的执行和语义等价。以下是需要特别注意的情况。
情况1:多个尾声指令。 一个函数具有多个返回以及相应的(多个)收尾指令的情况并不少见(附录B中的列表2给出了一个示例)。为了保持栈的平衡,我们必须识别并修改所有这些函数尾声指令。为此,我们通过为每个函数构建过程内控制流图并识别所有叶节点,确保不会遗漏任何收尾指令。
情况2:压栈-已进行的函数前言。 在函数序言之前可能还存在额外的压栈指令(附录B中的列表3展示了一个示例)。为简化处理,我们仅对涉及寄存器压栈和弹栈指令对进行随机化。lr。
情况3:不匹配的压栈和弹栈。 存在一些场景,其中函数序言和函数尾声中压栈与弹栈的寄存器集合不匹配(附录B中的列表4和5是两个示例)。为了最大化随机化的机会,我们仍在这两种情况下继续应用二进制重写。然而,在这些情况下我们会格外谨慎,以确保压栈和弹栈的寄存器序列(而不仅仅是集合)在随机化前后保持相同的逐一对应关系(参见附录B中的示例)。
4.2 随机化和更新偏移量
我们在此抛一枚硬币来决定除了原始函数前言和函数尾声中包含的寄存器外,还需压栈哪些寄存器。可供选择的候选寄存器集合包括ARM指令中的r1到r11,以及Thumb(16位和32位)指令中的r1到r7,但需排除原始函数前言和函数尾声中已包含的寄存器。例外情况是一些使用特殊常量编码的ARM指令,如下文所述。
如第3.1节所述,我们必须修改随机化函数中的指令,以确保它们能够访问现在被随机偏移量移动后的栈上正确的数据。这通常适用于使用栈指针sp(或直接通过r11)寻址的数据。8图位于附录B中,展示了四种不同区域的数据,在更新时需要不同的处理方式。
除了局部变量外,涉及访问数据的指令(包括存储的调用上下文、函数参数以及之前栈帧中的数据)都需要通过加上随机化的填充量来更新原始偏移量。
当尝试将偏移量更新为无法在ARM指令中正确表示的数值时,会出现复杂性。这是由于设计上仅使用12位指令空间来表示一组有用的32位常量所致。如果新的偏移量无法在12位“旋转”格式中正确表示,我们simply将对应的寄存器组从随机化候选中排除。需要注意的是,所需的偏移量有可能通过(多个)替代指令来表示;但由于minimal二进制重写要求,我们未探索此选项。
5 评估与讨论
我们的评估重点在于防御基于栈的内存攻击时的安全有效性。在本节中,我们对函数覆盖率、引入的随机化程度进行了分析,并展示了我们应对现实世界攻击的能力。为了将我们的分析置于现实世界背景下,我们选取了2015年1月谷歌应用商店中排名前20的免费安卓应用,并对这些应用程序包中包含的原生代码应用我们的随机化技术。这二十个应用中的一个,即二维码阅读器,在其应用程序包中不包含任何原生代码,因此我们未将其纳入分析;然而,如第3节所述,通过一些工程上的努力,我们的随机化技术也可应用于加载时或安装时编译的原生代码。我们的实验包括了一些广泛使用的原生库,例如 libffmpeg.so 和 libcocos2dcpp.so。我们直接在搭载安卓4.4.4系统的谷歌Nexus 5手机上执行每个经过随机化的应用,以确保我们的修改保持了执行语义和正确性。
在性能方面(本节评估的重点不在此),由于执行指令随机化和最小二进制重写时未插入额外指令,因此在运行时没有可观察到的性能开销。
5.1 函数覆盖率和随机化程度
我们的首次评估重点关注可进行随机化的函数数量以及使用我们提出的方案所能获得的随机化程度。无法被随机化的函数是指其函数前言和函数尾声原本就覆盖了所有候选寄存器的函数,即在16位Thumb函数中r0‐r7全部被压栈/弹栈,或在ARM或32位Thumb函数中r0‐r11全部被压栈/弹栈的情况。
图3显示了具有不同数量寄存器可用于随机化的函数的百分比(0表示该函数无法被随机化)。我们的评估表明,无法被随机化的函数占比在16位和32位函数中分别为0.8%和2.4%,两者均较小。我们还注意到,许多函数具有较大的随机化机会(≥ 6针对16位函数,以及 ≥ 10针对32位函数),其平均值分别占所有16位和32位函数的32.75%和30.28%。
在此,我们将我们的函数覆盖率与另一种同样不需要操作系统支持的最先进的栈布局随机化技术进行比较。Bhatkar等人提出通过修改创建局部变量空间的指令,在栈帧基址和局部变量之间引入随机填充,例如典型的 sub esp, # 0x100 [15]。他们报告的函数覆盖率为65% – 80%。我们将Bhatkar的方法应用于实验中的19个安卓应用,结果更差,平均函数覆盖率为9.94%。这一相对较低的覆盖率主要是因为只有至少包含一个局部变量的函数才会具有类似 sub esp, #immediate 的指令。然而,安卓系统拥有更多的通用寄存器,应用程序通常倾向于使用寄存器而非局部变量。由于许多函数不使用局部变量,Bhatkar方法在Android应用程序上的适用性较低。
我们还统计了可用于随机化的寄存器数量,因为这能告诉我们为函数帧引入的随机性位数。我们的评估结果显示,16位和32位函数平均享有分别为4位和7位随机性,相对于寄存器r0到 r7整体的最大8位随机性,以及寄存器r0到 r11整体的最大12位随机性,分别对应16位和32位函数2。请注意,这是应用于每个单独函数的随机性程度(独立应用)。函数通常仅使用寄存器的一个小子集,其余寄存器可供我们引入随机性。
5.2 函数内对象间的随机性
上一小节评估了我们方案的函数覆盖率以及引入的随机化程度。在本小节中,我们深入每个函数内部,观察应用于各种对象的随机化程度。具体而言,我们在图4中统计了数据对象在四个不同栈区域中的分布情况。我们发现,当前函数访问的大多数数据对象位于可被随机化的区域中,这些区域包括调用上下文、参数和前一个函数帧。平均而言,仅有4.83%的数据对象位于非随机化调用上下文和位置变量区域中。
5.3 防御基于栈的漏洞
如前所述,我们的方法可以对栈数据对象进行随机化,具有广泛的随机性覆盖范围,以防御基于栈的内存漏洞(例如缓冲区溢出)。在这里,我们通过一个具体示例进一步展示此能力。图5展示了 一个自设计的格式化字符串漏洞,该漏洞会导致栈数据泄漏。函数sprintf 在vulnerable(char* fmt)中允许攻击者插入恶意的格式控制字符串(例如, “%s”+4×”%p”),通过再提供四个”%p”来获取关键安全数据key。我们的实验表明,此类有效的利用在我们的随机化应用中无法成功。这是因为我们的方法在栈上的对象之间插入了随机填充,并改变了相对距离,如图5所示。这些随机填充r7,r2,r3成功地重新定位了前一个函数帧,并对关键安全数据key进行了随机化。
5.4 缓解基于ROP的攻击
我们的方法还会对可能用于构建ROP攻击执行路径的小工具进行随机化。ROP攻击倾向于使用位于函数结尾部分的间接跳转指令作为小工具,以构建和驱动恶意控制流。我们的方法对函数结尾部分进行随机化,有效降低了攻击者对小工具的了解,使ROP攻击更难实施。我们以一个近期著名的安卓系统漏洞 CVE‐2014‐7911[22,23]为例,该漏洞可能导致任意代码执行,并可被利用来获取系统权限[24,25]。
我们测试并分析了其公开的利用代码[25]。在图6中,我们展示了该利用代码使用的小工具以及攻击者通过栈迁移技术[26]构建的迁移栈。
通过分析图6中所示的四个主要小工具,我们可以发现其中两个已被我们的方法进行了随机化。更具体地说,小工具pop指令(以红色标记)在小工具和中添加了随机寄存器。因此,攻击者预期的栈布局(用于进入小工具)被改变,从小工具到的小工具的原始控制流将被破坏。利用代码因此无法成功调用 system函数。值得注意的是,除了弹栈‐based指令,一些sp‐based数据寻址指令被随机化(见第4.2节)。
5.5 局限性
尽管我们提出的技术在许多方面简单且有效,但仍存在可能绕过我们随机化的攻击。特别是,攻击者可能会利用内存泄漏漏洞来查明被压栈的寄存器的随机化集合,或引入的有效额外偏移量。最近提出的即时代码复用攻击[5] 似乎是实现这一目标的一种合理策略。然而,据我们所知,这类攻击仅在支持脚本环境的应用程序中被证明可行,而大多数Android应用程序对此类攻击是免疫的。
6 相关工作
地址空间布局随机化(ASLR)可能是最广泛部署的随机化技术,用于增加内存攻击的难度。传统的粗粒度ASLR[10]会对每个程序的数据/代码段的基址进行随机化,在32位平台[13]上提供的随机化熵相对较小。最近提出的细粒度ASLR技术[18,21,27]主要关注对代码段进行随机化,以防御代码复用技术。
原地代码随机化[21]对基本块的指令进行置换和替换。而我们的技术则是通过替换指令来随机化内存布局,而不是对代码本身进行随机化。STIR[18]在加载时对基本块的地址进行随机化,主要关注代码段。相比之下,我们的技术对程序内存布局中更细粒度的元素进行随机化,并聚焦于数据段。此外,由于利用了ARM架构上固定的指令长度,我们的工作更容易实现,并且因采用最小化二进制重写而更有可能获得用户接受。
Bhatkar等人提出的栈帧填充是高级ASLR技术之一,[15],这可能与我们的工作最为接近。Bhatkar等人通过向原始二进制文件中插入额外代码,在栈帧内引入填充以实现基址随机化。而我们的方法在不插入新代码也不删除现有代码的情况下实现了相同的目标,同时实现了更高的函数覆盖率(见第5.1节)。
还有其他为提高安全性而提出的随机化技术,例如指令集随机化[28,29]和控制流随机化[30]。此外,大量研究工作致力于在不使用随机化的情况下防御栈泄露和修改[10,31–33]。与这些技术截然不同的是,我们的工作重新分配了栈帧上的不同类型数据,能够防御更广泛的内存攻击。
7 结论
在本文中,我们提出了一种新颖的栈数据随机化方法,该方法通过一种轻量级的特定于ARM的指令随机化策略实现。通过在函数前言压栈和结尾出栈指令的操作数中随机更新寄存器数量,在函数调用上下文之间插入了随机填充。在真实应用程序上的评估表明,我们的技术覆盖了应用程序中超过97.6%的函数,平均为16位和32位函数分别引入了4位和7位随机性。超过95%的函数内对象被分配了新的地址。我们还展示了该方法在防御基于栈的内存漏洞和现实世界中的ROP攻击方面的有效性。
200

被折叠的 条评论
为什么被折叠?



