本文题为《System Call Interposition Without Compromise》,发表在2024年IEEE/IFIP国际可信赖系统与网络会议(DSN)上。论文由Adriaan Jacobs、Merve Gülmez、Alicia Andries、Stijn Volckaert和Alexios Voulimeneas合作完成。其中,Adriaan Jacobs、Alicia Andries、Stijn Volckaert来自比利时KU Leuven的DistriNet研究组,Merve Gülmez来自瑞典爱立信安全研究中心,同时也隶属于DistriNet,而Alexios Voulimeneas则来自荷兰代尔夫特理工大学。这项研究针对系统调用劫持技术领域,旨在解决当前主流方法在性能、完整性和表达能力之间无法兼顾的经典难题。
学术背景 系统调用是用户空间程序与操作系统内核交互的主要接口,因此,系统调用劫持成为监视或修改程序行为的关键技术,广泛应用于跟踪调试、提升程序可靠性与安全性、操作系统仿真、二进制兼容性支持以及自定义网络栈切换等场景。然而,设计一种同时满足高效、完整且表达力强的通用劫持机制在实践中面临巨大挑战。
现有技术主要分为三类,但各有显著局限:第一类是基于内核接口的传统方法,如ptrace和系统调用用户分派(SUD, Syscall User Dispatch)。ptrace虽然功能强大且表达力完整,但每次劫持都需要进行昂贵的上下文切换和进程调度,性能开销巨大;SUD通过信号机制将劫持处理移交回用户空间,其性能优于ptrace,但对于系统调用密集型的应用程序,其信号处理开销依然显著。第二类是基于seccomp的伯克利包过滤器(BPF)方法,它在内核中直接运行BPF程序,性能极高,但由于安全考虑,BPF程序的表达能力受到严格限制,无法进行深入的参数检查或修改,难以满足复杂的劫持需求。第三类是基于二进制重写的方法,如Zpoline和SABRE,它们将程序中的syscall指令静态重写为直接跳转到用户空间拦截器的指令,从而完全绕过内核,获得了极高的效率和完整的表达能力。然而,这类方法严重依赖静态二进制分析和重写,无法识别和劫持动态加载或运行时生成的代码中的系统调用指令,其完整性无法保证。
上述现状表明,学术界和工业界缺乏一种能够同时提供完整表达力、无遗漏的完整性以及接近原生性能的非侵入式系统调用劫持方案。一些对安全或性能有极端要求的项目甚至被迫修改操作系统内核或定制硬件,这不仅增加了系统复杂性,也扩大了可信计算基(TCB),不利于长期的安全与维护。因此,本研究的核心目标就是填补这一空白,设计并实现第一个能同时满足表达力、完整性和效率三大属性的非侵入式系统调用劫持方案。
研究设计:工作流与核心方法 本研究提出了一种名为LazyPoline的混合劫持机制,其核心洞见在于巧妙结合了基于内核接口的“慢速路径”和基于二进制重写的“快速路径”。研究主要包含设计与实现、完整性验证、性能评估三个核心工作流程,研究对象为LazyPoline原型系统本身,并通过微基准测试、宏基准测试和真实应用案例对其进行全面评估。
设计与实现工作流:首先,研究人员设计了一个两阶段的混合架构。其关键在于利用SUD这一内核机制作为“慢速路径”或“捕获网”。当目标程序首次执行某条系统调用指令时,SUD会被触发,产生一个SIGSYS信号。在信号处理程序中,LazyPoline不仅处理这次系统调用,更重要的是,它会将触发此次调用的syscall指令原地重写为call rax指令(X86-64架构下,系统调用号约定存放在RAX寄存器中)。重写完成后,通过修改信号处理返回的上下文,使程序执行流跳转至一个名为Zpoline技术的“快速路径”入口。Zpoline是团队改进并采用的一种先进的二进制重写方案,它预先在虚拟地址0附近设置一个NOP指令“雪橇”,使得call rax指令能根据RAX中的系统调用号,通过雪橇最终跳转到用户定义的拦截器函数。自此之后,该特定位置的所有后续系统调用都将直接通过这条高效的快速路径进行拦截,完全绕过了SUD的信号机制。慢速路径始终保持激活状态,以捕获和重写任何新出现的系统调用指令。这一“首次捕获并重写,后续直通”的惰性重写策略是LazyPoline的核心创新。
在实现层面,研究团队解决了若干关键技术挑战。1)选择器专用SUD:不同于传统的SUD用法,LazyPoline不在信号处理函数内部直接调用拦截器,而是修改上下文使其返回后执行快速路径。这样做避免了需要将拦截器代码地址加入SUD“白名单”的麻烦,简化了实现并增强了安全性,将安全问题简化为对SUD选择器字节的内存保护。2)ABI兼容性:为确保最大兼容性,LazyPoline在进入拦截器时,不仅保存通用寄存器,还完整保存了SSE、AVX等扩展状态寄存器,确保拦截器运行环境与原生系统调用ABI完全一致,防止因寄存器破坏导致程序错误。团队通过动态分析工具发现,某些常见库函数确实依赖内核保持这些扩展状态的完整性。3)信号处理:设计了一套包装机制来正确处理应用程序注册的信号处理器,确保在信号处理器内执行的系统调用也能被正确拦截,并在信号返回时恢复正确的SUD选择器状态。4)多进程/多线程支持:为每个任务(进程或线程)维护独立的SUD选择器,并在fork、clone等操作后重新激活SUD,确保劫持在任务间正确传递。
完整性验证工作流:为了证明LazyPoline能够劫持动态生成的系统调用,研究团队选取了Tiny C编译器(TCC)作为测试对象。TCC能够进行即时(JIT)编译,即在运行时生成包含syscall指令的可执行代码。实验设计为:在C程序中嵌入一个非libc库的getpid系统调用,然后使用TCC的-run选项直接JIT编译并运行该程序。研究人员对比了SUD、纯Zpoline和LazyPoline三者的拦截效果。结果如预期所示,SUD和LazyPoline都能够打印出完全相同的系统调用序列,包括动态生成的getpid调用,证明了LazyPoline继承了SUD的完整性。而纯Zpoline的跟踪记录中则缺失了这个getpid调用,因为它无法在程序加载时预见到运行时才产生的代码。
性能评估工作流:评估分为微基准测试和宏基准测试两部分,旨在量化LazyPoline的效率开销,并与现有技术进行对比。所有测试均使用一个“哑”拦截函数,仅原样执行系统调用并返回结果,以单独衡量拦截框架本身的性能损耗。
wrk进行压测。对比方案包括基线、Zpoline、LazyPoline(保存/不保存扩展状态)和SUD。结果表明,在最坏的单工作进程、小文件服务场景下,不保存扩展状态的LazyPoline能维持基线约95%的吞吐量,仅比Zpoline低约2-4个百分点,但提供了Zpoline所不具备的完整性保证。保存完整扩展状态的LazyPoline,其吞吐量仍显著优于SUD,在多数情况下几乎是SUD的两倍。随着服务文件变大,系统调用频率降低,所有方案的开销差异逐渐缩小,但SUD的相对开销依然明显。主要结果 1. 完整性结果:在TCC动态代码生成的测试中,LazyPoline成功拦截了所有系统调用,包括运行时生成的指令,其拦截完整性达到了与内核接口SUD相同的水平,证明了混合设计中慢速路径的有效性。 2. 性能结果: * 微基准测试定量揭示了各技术的性能层次:Zpoline最快,LazyPoline的快速路径在关闭SUD检查后与之等同;启用SUD检查带来固定开销;保存扩展状态是LazyPoline内部最大的可控开销项;SUD因信号处理机制开销巨大。 * 宏基准测试证明,在真实的、高强度的应用场景中,LazyPoline在牺牲极小性能代价(与纯重写方案Zpoline相比)的前提下,实现了完整的拦截能力。尤其在网络服务器场景中,即使保存完整扩展状态,其性能也远优于目前最常用的高效内核接口SUD。这证实了“惰性重写”策略在实际工作负载中的有效性:绝大多数系统调用在首次执行后被重写,后续均通过高效路径执行。 3. 兼容性与实用性结果:研究通过动态分析工具发现,部分常见程序(如某些coreutils工具)确实依赖内核保存扩展寄存器状态。这证明了LazyPoline默认完整保存扩展状态的设计对于确保广泛兼容性是必要的。同时,研究也提供了配置选项,允许对性能有极致要求的用户选择性放弃保存部分状态,体现了设计的灵活性。
结论 本研究成功提出并实现了第一个同时满足表达力完整、拦截无遗漏、运行高效率三大目标的非侵入式系统调用劫持方案。LazyPoline通过创新的混合设计,将SUD的完整捕获能力与Zpoline的高效二进制重写能力相结合,采用“首次捕获并惰性重写”的策略,从根本上解决了现有技术在性能与完整性之间的矛盾。其科学价值在于为系统监控、安全沙箱、兼容层等众多依赖系统调用拦截的领域提供了一种理论上更优越的通用底层抽象。其实用价值在于,开发者无需再在功能、安全与性能之间做出艰难妥协,也无需诉诸修改内核或硬件等复杂且侵入性的手段,即可构建强大的用户空间拦截工具。论文开源了LazyPoline原型,为后续研究和工业应用提供了坚实基础。
研究亮点 1. 创新性的混合架构:首次将内核接口的“完整性”与二进制重写的“高效性”动态结合,提出了“慢速路径捕获、快速路径执行”的惰性重写范式,是解决该系统领域经典难题的巧妙思路。 2. 同时达成三大核心目标:这是首个被验证能同时实现表达力、完整性和效率的非侵入式方案,填补了研究空白。 3. 深入的实际系统构建:研究不止于设计,还深入解决了ABI兼容、信号处理、多任务支持等实际实现中的棘手问题,并进行了详尽的性能剖析(如量化了SUD固定开销和扩展状态保存开销),使工作具备很高的工程参考价值。 4. 严谨且全面的评估:通过从微基准到宏基准,从合成测试到真实应用(TCC、Nginx、Lighttpd)的多层次评估,令人信服地验证了方案的有效性和性能优势。
其他有价值的内容 论文在讨论部分展望了未来工作,包括:1)多操作系统支持:当前方案依赖于Linux的SUD机制,在其他缺少类似完备拦截接口的OS上应用可能受限。2)安全性增强:像大多数用户空间拦截方案一样,LazyPoline默认不提供针对恶意程序攻击拦截器本身(如篡改选择器字节)的安全保证。但论文指出,可以结合现有的内存隔离技术(如Intel MPK)来保护关键状态,而LazyPoline自身高效且完整的拦截能力,恰好可用于实现这些隔离机制所需的安全系统调用过滤,形成了一个有趣的互促关系。3)多架构支持:虽然当前实现针对x86-64,但原理可适配其他架构,前提是目标架构存在类似SUD的机制并有相应的二进制重写技术。