分享自:

基于二进制重写的ARM64系统调用钩子

期刊:Middleware '25: Proceedings of the 26th International Middleware ConferenceDOI:10.1145/3721462.3770771

针对ARM64架构的系统调用挂钩新方法:SVC-Hook

本报告基于Akira Moroo、Hajime Tazaki与Kenichi Yasukata三位研究者发表于2025年12月(在线发表日期为2025年12月14日)的论文,该论文收录于第26届ACM International Middleware Conference (Middleware ‘25)的会议论文集。三位作者均来自日本:Akira Moroo隶属于Ricerca Security, Inc.,而Hajime Tazaki与Kenichi Yasukata均来自IIJ Research Laboratory。他们的工作题为“svc-hook: hooking system calls on arm64 by binary rewriting”,提出并实现了一种针对ARM64 CPU架构、基于二进制重写(binary rewriting)的新型系统调用挂钩(system call hook)机制。

一、研究背景与目标

系统调用是用户空间程序访问操作系统(OS)内核功能的核心接口。系统调用挂钩机制允许用户在系统调用被触发时拦截执行流,并将其重定向到用户自定义的处理函数中。这类机制是众多软件(如追踪工具、沙箱、操作系统仿真层)的基础组件。近年来,出于性能与安全性的考量,将操作系统子系统置于用户空间的设计趋势日益显著,这使得通过系统调用接口透明地“粘合”用户空间OS子系统与现有程序的需求变得尤为迫切,从而对高效、可靠的系统调用挂钩机制提出了更高要求。

在诸多挂钩方法中,基于二进制重写的方案因其一系列优势而备受关注。相较于依赖内核通用功能(如ptrace、seccomp)或BPF的方案,二进制重写通常能带来更低的性能开销,且能用于系统调用仿真,这对于用户空间OS子系统的透明集成至关重要。此外,它不依赖于对内核或标准库的非上游(non-upstreamed)修改,更易于用户接受和使用。

然而,现有研究存在显著的架构局限性。绝大多数基于二进制重写的系统调用挂钩机制都是为x86 CPU架构设计的。虽然已有少数针对ARM64架构的提案,但它们存在明显的缺陷,可能导致被挂钩程序出现预期外的行为。例如,Hermitux for ARM64方案结合了多种技术,但仍有约10%的系统调用无法通过二进制重写进行挂钩,必须依赖高开销的陷阱(trap)机制;其部分技术接受覆盖关键寄存器(如x30),可能引发程序逻辑错误。另一项工作ASC-Hook通过修改用于传递系统调用号的寄存器(x8)来规避问题,但这同样改变了程序预期的CPU寄存器状态,存在引发意外行为的风险。因此,尽管ARM64是一种极为流行的CPU架构,但其软件生态一直缺乏能够同时满足低性能开销、支持系统调用仿真、易于使用指令级挂钩这些理想属性的挂钩机制。这一缺口尤其阻碍了用户空间OS子系统设计者将其系统集成到ARM64平台软件中。

为解决上述问题,本研究旨在提出一种名为SVC-Hook的新型系统调用挂钩机制。其技术目标是:在ARM64架构上,通过二进制重写挂钩系统调用,且不因重写操作而破坏被挂钩程序的应用程序逻辑。为此,SVC-Hook设定了两个核心要求:1) 仅替换目标程序中的svc指令,不覆盖其他指令;2) 在挂钩函数调用前后,被挂钩程序能维持除系统调用调用约定(calling convention)允许更改之外的通用CPU寄存器状态不变。通过满足这两点,SVC-Hook有望规避前述Hermitux和ASC-Hook方案因改变寄存器状态而带来的风险。

二、技术方案与工作流程

SVC-Hook的核心设计围绕解决ARM64架构特有的一个挑战展开。该挑战源于bl指令的设计:bl在跳转时会自动将返回地址保存到x30寄存器。如果简单地将触发系统调用的svc指令替换为bl指令以跳转到挂钩函数,那么原始函数原本保存在x30中的返回地址将被覆盖,导致其无法正确返回。

SVC-Hook的解决方案新颖而精巧,其工作流程主要包括初始设置阶段和运行时执行流两个部分,具体步骤如下:

  1. 初始设置阶段

    • 扫描与列表构建:SVC-Hook的实现以一个名为libsvchook.so的共享库形式存在。该库通过LD_PRELOAD机制在被挂钩程序主逻辑开始前加载并执行。首先,它扫描程序的所有可执行内存区域。由于ARM64采用4字节定长指令集且指令必须4字节对齐,识别svc指令无需复杂的反汇编,只需逐4字节检查操作码即可。所有找到的svc指令的地址被记录在一个列表中。
    • 蹦床代码实例化:针对列表中的每一个svc指令,SVC-Hook需要为其创建一段蹦床代码。这是其创新关键。SVC-Hook选择将svc指令替换为b指令(跳转指令)。b指令不会保存返回地址到任何寄存器,从而避免了覆盖x30的问题。但随之而来的问题是:跳转后如何知道应该返回到哪里?SVC-Hook的答案是:将返回地址(即svc指令地址+4)作为常量数据,直接嵌入到为每个svc独立实例化的蹦床代码中。蹦床代码需要放置在距离原svc指令±128 MiB的地址范围内(这是b指令26位立即数偏移量经移位后所能覆盖的范围)。libsvchook.so会为每个svc在可用的内存区域中找到合适位置来放置其专属的蹦床代码。研究通过对Ubuntu软件仓库大量包的分析表明,绝大多数svc指令附近都能找到可用空间,此限制在实践中影响甚微。
    • 二进制重写:在创建好所有蹦床代码后,libsvchook.so遍历svc列表,将每个svc指令替换为一条b指令,该指令的跳转目标指向对应的专属蹦床代码地址。此过程通过mprotect系统调用临时将代码所在内存区域设为可写,完成替换后再恢复其原始权限。
    • 用户挂钩函数加载libsvchook.so会通过环境变量指定的路径,使用dlmopen加载一个用户提供的共享库,该库中包含用户实现的实际挂钩逻辑函数。
  2. 运行时执行流程: 当程序运行到被替换的b指令时,执行流如下(以挂钩一个svc为例):

    • 步骤1(跳转至蹦床):CPU执行b指令,跳转到为该svc定制的蹦床代码处。此时x30寄存器未被改动。
    • 步骤2(保存状态并调用挂钩):蹦床代码首先将当前的寄存器状态(包括x30的值)压入栈中进行保存。随后,它调用由libsvchook.so提供的中间函数,该函数最终会调用用户实现的挂钩函数。
    • 步骤3(挂钩函数执行与返回):用户挂钩函数执行其逻辑(例如,仿真系统调用、记录信息等),然后返回至蹦床代码。
    • 步骤4(恢复状态并返回):蹦床代码从栈中恢复之前保存的寄存器状态(包括正确的x30)。最后,它通过一条跳转指令,跳转到其内部嵌入的返回地址(即原svc指令后一条指令的地址)。至此,程序继续正常执行,仿佛系统调用刚刚完成(或被仿真),且整个过程中x30及其他受调用约定保护的寄存器状态均得以保持。

三、主要实验结果

研究者通过实验评估了SVC-Hook的性能开销及其在实际应用场景中的表现。

  1. 挂钩开销评估: 实验测量了挂钩并仿真最简单的getpid系统调用所需的时间。对比机制包括ptrace、seccomp、基于brk指令的信号处理、SVC-Hook以及作为参考的LD_PRELOAD函数调用挂钩。结果如表2所示:

    • Ptrace:开销最高(约30789纳秒),主要源于内核空间进程调度器在追踪器与被追踪进程间进行上下文切换的复杂操作。
    • Seccomp与Brk:开销次之(约3157纳秒和3045纳秒),主要开销来自内核信号子系统为处理信号而进行的CPU寄存器状态操作。
    • SVC-Hook:开销极低,仅28纳秒
    • LD_PRELOAD:开销最低(11纳秒),因其直接进行函数调用,路径最短。 SVC-Hook的28纳秒开销比LD_PRELOAD高约17纳秒,这正体现了执行蹦床代码(保存/恢复寄存器、跳转)所带来的额外成本。尽管如此,与基于内核机制的方案相比,SVC-Hook的开销低了两个数量级以上,证明了其极高的效率。
  2. 挂钩程序性能评估: 研究选取了“透明集成用户空间OS子系统”这一关键应用场景进行测试。实验将运行于DPDK之上的LWIP TCP/IP栈(用户空间网络栈)透明地集成到一个简单的HTTP服务器和Redis键值存储服务器中。系统调用被挂钩并重定向到用户空间的LWIP。测试平台为两台直接连接的10Gbps以太网机器。

    • 简单HTTP服务器测试:客户端通过16个持久连接尽可能快地向服务器请求64字节静态内容。
    • Redis测试:客户端通过16个持久连接发送GET请求,服务器不存储键值对以最大化网络子系统压力。 实验结果如图3所示,两个测试均呈现相同趋势:
    • Ptrace方案性能最差。
    • Seccomp和Brk方案性能相近,但远低于SVC-Hook。
    • SVC-Hook的性能接近LD_PRELOAD方案(后者在此场景下也可用,但如前文所述,它在通用性和指令级挂钩方面有局限)。
    • 更重要的是,使用SVC-Hook集成用户空间网络栈(LWIP+DPDK)后,两个服务器的性能均显著超过了使用原生Linux内核TCP/IP栈的性能(图中竖实线标示)。这证明了用户空间OS子系统确实具备性能优势,而SVC-Hook作为高效的“粘合剂”,足以维持这一优势,不会因挂钩机制本身引入的性能瓶颈而抵消用户空间子系统带来的收益。

四、结论与价值

本研究成功设计并实现了SVC-Hook,一种针对ARM64架构、基于二进制重写的系统调用挂钩机制。其核心贡献在于提出并实现了一种通过b指令跳转与每指令专属蹦床代码相结合的方法,在完全不破坏被挂钩程序寄存器状态(特别是x30)的前提下,实现了高效、可靠的指令级系统调用拦截。

该研究的科学价值与应用价值体现在: * 填补技术空白:为日益流行的ARM64平台提供了一个同时满足低开销、支持仿真、易于使用、指令级挂钩四大属性的系统调用挂钩方案,解决了该领域长期存在的选项有限问题。 * 推动用户空间OS发展:通过极低的性能开销,使得透明集成高性能用户空间OS子系统(如用户空间网络栈)成为可能,且能保持其性能优势,为未来系统软件设计提供了有力工具。 * 方法创新:其“替换为b指令+嵌入返回地址的专属蹦床”设计简洁而有效,规避了前人方案中改变寄存器状态可能导致的副作用,提升了挂钩的可靠性与安全性。

五、研究亮点

  1. 创新性的核心设计:针对ARM64架构bl指令覆盖x30的固有挑战,创造性地采用b指令与定制化蹦床代码组合的方案,在实现跳转的同时完美保存了完整的寄存器上下文,这是本工作最重要的技术亮点。
  2. 卓越的性能表现:实验数据充分证明,SVC-Hook的性能开销远低于依赖内核通用功能的挂钩机制(ptrace、seccomp等),与最理想的函数调用挂钩开销处于同一量级,使其非常适合用于高性能场景。
  3. 强大的实用性与兼容性:研究证实SVC-Hook可在Linux、FreeBSD和NetBSD上工作,并探讨了其在Android上的适用性。通过对大量现实世界软件包的分析,证明了其蹦床代码放置的空间限制在实践中很少构成障碍,具有很高的实用价值。
  4. 清晰的定位与对比:论文通过系统的相关工作梳理和属性对比表格,清晰阐述了二进制重写方法相对于其他类别(内核通用支持、BPF、非上游扩展、函数调用挂钩)的优势,以及SVC-Hook相对于已有ARM64方案(Hermitux, ASC-Hook)的改进,定位明确,贡献突出。

六、其他说明

论文还讨论了SVC-Hook的局限性,例如在那些通过运行时校验或严格内存权限限制来阻止二进制重写的系统(如OpenBSD、macOS)上可能无法应用。同时,作者指出其方法直接移植到x86或RISC-V架构存在困难,主要源于指令长度和跳转偏移量范围的差异。这些坦诚的讨论有助于读者全面理解该技术的适用范围。

SVC-Hook是一项针对实际需求、设计精巧、实验充分、结果出色的系统软件研究工作,为ARM64平台的系统级编程和性能优化提供了一个宝贵的新工具。

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