分享自:

无需控制流恢复的静态二进制重写

期刊:ACM SIGPLAN International Conference on Programming Language Design and Implementation (PLDI)DOI:10.1145/3385412.3385972

(文档类型判断:类型a。这是一篇报告原创性研究的学术论文。)

关于静态二进制重写技术E9PATCH的学术研究报告

作者、机构与发表信息 本研究的主要作者是Gregory J. Duck、Xiang Gao和Abhik Roychoudhury,均来自新加坡国立大学(National University of Singapore)计算机科学系。该项研究以论文《Binary Rewriting without Control Flow Recovery》(《无需控制流恢复的二进制重写》)的形式,于2020年6月在美国计算机协会(ACM)编程语言设计与实现国际会议(PLDI ’20)上发表。

学术背景与研究目的 本研究属于软件安全与系统领域的静态二进制重写技术范畴。二进制重写是指直接修改可执行文件(二进制文件)的机器码,广泛应用于软件加固(Hardening)、自动化修复(Automated Repair)、程序插桩(Instrumentation)、性能优化(Patching)和调试(Debugging)等多个关键领域。其核心优势在于能够在软件源代码不可得(例如商业现成品COTS软件)的情况下直接对程序进行修改。

然而,传统的静态二进制重写工具面临一个根本性挑战:为了在重写过程中插入、删除或移动指令,工具必须精确地恢复程序的控制流信息,包括识别所有的跳转目标(Jump Targets)、基本块(Basic Blocks)和函数边界等,以便在修改代码后相应地更新所有跳转指令的目标地址。在静态环境下,准确且完整地恢复控制流是一个众所周知的难题,通常无法实现。因此,大多数现有工具采用了基于特定编译器、编程语言模式或启发式方法的简化假设来近似推断控制流。这种依赖假设的方法具有固有的脆弱性,难以扩展到大型、复杂的二进制程序。例如,文中指出,许多工具无法处理如Google Chrome或Firefox这类包含数万条间接跳转指令、体积超过100MB的大型剥离(Stripped)二进制文件。

基于此背景,本研究旨在解决这一长期存在的难题。研究的目标是开发一种全新的静态二进制重写工具——E9PATCH,其核心设计理念是完全避免对控制流信息的需求。通过设计一系列控制流不可知(Control Flow Agnostic)的重写方法,确保在修改程序时保持原程序所有潜在的跳转目标集合不变,从而从根本上绕开控制流恢复这一复杂且不可靠的分析步骤,实现高健壮性和可扩展性。

详细研究流程与方法 研究工作的核心是设计、实现并评估E9PATCH工具。整个工作流程可分为几个主要阶段:首先,基于“指令双关”(Instruction Punning)思想构建基础的重写能力;其次,针对基础方法覆盖率不足的问题,创新性地发展出一套补丁策略;然后,引入物理页分组(Physical Page Grouping)优化以管理内存和文件大小;最后,在广泛的标准测试集和大型实际程序上进行系统性评估。

1. 基础重写方法:指令双关(Baseline B2) 这是E9PATCH的理论基石,灵感来源于动态插桩工具LiteInst。其核心思想是:在需要打补丁的位置,用一条相对近跳转(jmpq rel32)指令覆盖原有的指令。这条跳转指令将控制流重定向到一个“蹦床”(Trampoline),该蹦床执行所需的插桩或补丁操作,然后再跳回主程序继续执行。关键挑战在于,x86_64架构的跳转指令长度为5字节,而待覆盖的原始指令可能短于5字节。 指令双关的解决方案是:精心构造一条“双关”跳转指令,使其机器码字节表示与覆盖到的相邻指令(被“双关”的指令)的字节表示完全相同。这样,即使跳转指令覆盖了后续指令的部分字节,由于字节值未变,被覆盖指令的执行语义也得以保留。更重要的是,由于没有移动任何指令,程序的所有地址(包括潜在的跳转目标)都保持不变,实现了控制流不可知。

2. 扩展补丁策略:提升覆盖率 研究发现,仅凭基础的指令双关方法,补丁成功率(覆盖率)有限(根据后续实验,约在42%-94%之间),因为“双关”跳转所允许的蹦床目标地址范围(由rel32偏移决定)可能受限,导致找不到有效的虚拟地址来放置蹦床。为此,研究团队开发了三种新的补丁“战术”(Tactics): * 战术T1:填充跳转(Padded Jumps):利用x86_64指令中不影响语义的冗余前缀(如REX前缀、段覆盖前缀),将跳转指令扩展到5字节以上。虽然这进一步约束了蹦床地址的范围,但也为寻找有效的“双关”匹配提供了更多尝试机会(每个待补丁指令可获得“指令长度-1”次尝试)。 * 战术T2:后继驱逐(Successor Eviction):当T1失败时,此战术将待补丁指令的直接后继指令“驱逐”。方法是用一条“双关”跳转指令覆盖该后继指令,将其重定向到一个“被驱逐者蹦床”(Evictee Trampoline),该蹦床简单地模拟执行被驱逐的原指令后返回。驱逐操作改变了后继指令的字节表示,从而可能为之前失败的针对原补丁位置的“双关”尝试创造新的、有效的字节匹配模式。 * 战术T3:邻居驱逐(Neighbour Eviction):当T1和T2均失败时,此战术作为最后手段。它选择一个在待补丁指令附近(-128到+127字节范围内)的“邻居”指令进行驱逐。在驱逐释放出的空间里,插入两条(可能也是“双关”的)跳转:一条(jpatch)指向最终的补丁蹦床,另一条(jvictim)指向被驱逐指令的模拟蹦床。最后,将待补丁指令本身替换为一条短跳转(jshort),跳转到jpatch的位置。这样就通过一个“双重跳转”(jshortjpatch → 补丁蹦床)实现了补丁,同时通过jvictim保持了被驱逐指令的语义。

所有战术都遵循控制流不可知原则:任何指令要么被原样保留,要么被语义等价的指令(或跳转序列)替换,要么被补丁功能替换,而程序的地址空间布局不变。研究还提出了逆序补丁策略(S1),按照地址从高到低的顺序应用补丁,以避免多个补丁点之间的相互干扰,确保了重写过程的正确性。

3. 内存与文件大小优化:物理页分组 由于指令双关和驱逐战术对蹦床位置有特定约束,大量蹦床可能散布在虚拟地址空间的不同页面中,导致虚拟内存碎片化和输出文件尺寸剧增。为了解决这个问题,研究提出了物理页分组优化。 传统方法是为每个包含蹦床的虚拟内存页面分配一个独立的物理内存页面(及对应的文件块),导致大量物理页面内容空洞,利用率低。物理页分组则寻找那些所包含蹦床在页面内相对偏移不冲突的多个虚拟页面,将它们“合并”到同一个物理页面中。然后,通过内存映射(mmap)技术,将这个合并后的物理页面多次映射到程序虚拟地址空间中相应的不同位置。这样,虽然每个蹦床在虚拟地址空间中仍位于原定地址,但底层的物理内存(和磁盘文件)得到了共享,显著减少了物理内存占用和输出文件大小。研究通过一个贪心算法实现页面分组,并允许用户通过设置粒度参数m来权衡映射数量与内存利用率。

4. 系统实现与评估对象 研究实现了E9PATCH原型工具。它采用低层次设计,本身不包含反汇编器,而是接受外部前端提供的指令位置和大小信息作为输入,专注于重写操作。这使其能够灵活地与不同的反汇编技术结合。E9PATCH支持位置无关可执行文件(PIE)和非PIE代码,也支持对共享库进行重写,并且允许混合使用经过重写和未经过重写的代码(例如,只重写主程序而不重写其依赖库)。 为评估E9PATCH,研究选取了两类具有挑战性的插桩应用场景作为测试用例: * 应用A1:插桩所有跳转指令(包括jmp和条件跳转jcc),这类似于传统的基本块计数基准测试,但E9PATCH无需基本块信息。 * 应用A2:插桩所有可能向堆指针进行写操作的指令,这是为后续的堆内存错误检测应用做准备。 评估对象包括: * 标准性能测试集:完整的SPEC CPU2006基准测试程序套件(共26个程序,涵盖C、C++、Fortran语言)。 * 大型实际程序:包括从Ubuntu系统中选取的多个常用工具(如gitvimlibc.so等)。 * 超大型程序(可扩展性验证):Google Chrome(约152MB)和Firefox的核心库libxul.so(约115MB)。

主要研究结果 实验从覆盖率、运行时性能开销、输出文件大小和可扩展性等多个维度对E9PATCH进行了全面评估。

1. 补丁覆盖率 E9PATCH展示了接近完美的补丁覆盖率。在SPEC2006测试集上,对于应用A1(跳转指令),平均覆盖率达到99.94%;对于应用A2(堆写指令),平均覆盖率达到99.99%。只有极少数程序(如gamesszeusmp,因其静态分配了巨大的.bss段,挤占了可用于放置蹦床的虚拟地址空间)未能达到100%,但仍高于98.5%。当这些程序以PIE模式编译时,覆盖率均可达100%。 战术有效性分析:数据清晰地显示了各战术对提升覆盖率的贡献。仅使用基础方法(B1+B2),对于A1和A2的平均覆盖率分别只有72.79%81.63%。战术T1将覆盖率提升了约14%和16%,战术T2提升了约4%和0.6%。而关键的战术T3进一步将覆盖率推至接近100%,对于A1提升了约9.5%。这证明了T3作为“最后手段”的重要价值。研究还发现,PIE二进制文件由于允许使用负偏移地址放置蹦床,其基础覆盖率(>93%)远高于非PIE文件,突显了现代安全特性(如地址空间布局随机化ASLR)对E9PATCH这类工具的友好性。

2. 运行时性能开销 在SPEC2006程序上测量“空”插桩(仅执行跳转至蹦床并返回,无实际逻辑)的性能开销。对于A1(跳转指令插桩),平均运行时开销为+110.81%;对于A2(堆写指令插桩),平均开销为+64.71%。作为对比,文中提及其他需要代码重定位的静态重写工具(如Mulitverse, PEBIL, Dyninst)进行类似空插桩或基本块计数的开销通常在+60%到+70%左右。E9PATCH的略高性能开销是其为换取健壮性可扩展性(避免控制流分析)所做的权衡,因为它至少需要执行两次额外的jmpq指令。

3. 文件大小与内存优化 启用物理页分组优化(最积极模式m=1)后,输出二进制文件的平均大小增长得到有效控制:对于A1,平均文件大小增加+57.43%;对于A2,增加+30.90%。作为对比,如果禁用该优化(采用简单的一对一物理-虚拟页面映射),文件大小将急剧膨胀,分别增加+2239.83%+568.96%。这有力地证明了物理页分组优化在管理由大量约束性蹦床地址导致的内存碎片问题上的关键作用。

4. 可扩展性验证 在Google Chrome和Firefox这两个超大型、结构复杂的真实世界程序上的成功应用,是E9PATCH可扩展性的最有力证明。研究使用Dromaeo DOM基准测试了浏览器性能。对于应用A2(堆写指令插桩),Chrome的运行时开销约为+113%,Firefox约为+46%。实验结果证实,E9PATCH能够处理包含数百万条指令、体积超过100MB的剥离二进制文件,突破了传统工具的能力边界。

5. 应用案例:二进制堆写加固 作为概念验证,研究利用E9PATCH实现了一个实际的二进制加固应用:堆指针空间内存错误检测。该应用插桩所有向堆指针的写操作,并在蹦床中调用检查函数,利用低脂指针(Low Fat Pointers) 技术编码的边界信息来验证写入地址是否越界(例如,检查是否侵犯了对象前后的16字节“红区”)。实验结果显示,相比空插桩(A2),增加了实际边界检查逻辑后,SPEC2006程序的平均性能开销从+64.71%上升到+127.27%;Chrome和Firefox的开销分别从+113%/+46%上升到+170%/+60%。这证明了E9PATCH能够作为基础工具,支撑实现复杂的、源代码不可得情况下的二进制安全增强功能。

研究结论与价值 本研究成功提出并实现了E9PATCH,一种强大且可扩展的静态二进制重写工具。其核心贡献在于通过指令双关指令驱逐等创新性方法,构建了一套完整的、控制流不可知的二进制重写技术体系。这使得E9PATCH能够在不依赖脆弱启发式或特定编译器假设的情况下,稳健地对二进制程序进行修改。

科学价值:该研究挑战了静态二进制重写领域长期依赖控制流恢复的传统范式,提供了一种全新的、更为根本的解决方案思路。它证明了通过精心设计的局部代码变换来保持全局地址空间不变是可行且高效的,为解决二进制分析中的固有难题开辟了新途径。

应用价值:E9PATCH工具具有广泛的实用价值。其高健壮性和对大型剥离二进制文件的支持,使得它非常适合应用于软件安全加固(如内存安全检测)、二进制补丁/修复(在源代码不可得时修复漏洞)、性能剖析以及软件维护等多个关键场景。特别是对于保护广泛使用的COTS软件或进行大规模的遗留系统现代化改造具有重大意义。

研究亮点 1. 根本性创新:提出了“无需控制流恢复”的静态二进制重写新范式,从根本上规避了该领域最棘手的问题。 2. 方法论突破:发展的“指令双关”及“T1/T2/T3驱逐战术”构成了一套系统性的、控制流不可知的重写方法学,显著提升了补丁覆盖率。 3. 高效优化:提出的“物理页分组”内存优化技术,有效解决了由地址约束导致的文件膨胀问题,使技术具备实用性。 4. 卓越的可扩展性:成功应用于Chrome、Firefox等超大型现实世界程序,验证了工具处理极端复杂情况的能力,这是许多现有工具无法做到的。 5. 实用性强:工具设计务实,支持PIE/非PIE、可执行文件/共享库,允许混合重写,并提供了开源实现,便于学术界和工业界使用与后续研究。

其他有价值内容 研究在讨论中对比了E9PATCH与静态、动态二进制重写领域的主要相关工作,清晰阐述了其差异化和优势所在。例如,指出许多静态工具依赖于特定编译器或符号信息;而像LiteInst这样的动态工具虽然也使用指令双关,但其备用策略(插桩前驱指令或使用非法操作码)在静态、控制流未知的上下文中不适用。这些对比进一步确立了E9PATCH在其设计空间中的独特定位和价值。

上述解读依据用户上传的学术文献,如有不准确或可能侵权之处请联系本站站长:admin@fmread.com