LWN:内核里硬件CFI的支持!

关注了就能看到更多这么棒的文章哦~

Kernel support for hardware-based control-flow integrity

July 11, 2022
This article was contributed by Mike Rapoport
DeepL assisted translation
https://lwn.net/Articles/900099/

很久以前,只需要一个简单的栈溢出攻击就足以将代码注入到运行中的系统了。但是,在现代系统中,stack 不再是可执行的了,因此,简单的使用栈溢出方式来进行攻击就不再可行了。于是攻击者已经转向了控制流(control-flow)攻击,也就是利用目标系统中已经存在的代码。硬件供应商已经加上了一些措施希望能挫败控制流攻击;其中一些功能在 Linux 内核中的支持比其他平台更加有优势。

控制流完整性(CFI, Control-flow integrity)是一项旨在阻止控制流攻击的技术,或者至少可以削弱攻击者劫持程序的 control flow 的能力。CFI 的总体思想是对间接跳转(比如 call、branch 和 return 指令)的来源和目的地进行标记(label),并在运行时来验证实际跳转的目标是否与 label 相符。CFI 可以完全用软件实现,但来自不同供应商也有一些硬件机制可以协助 CFI 的实现。

Coarse-grain forward-edge protection

破坏程序 control flow 的一种常见攻击方式就是是通过 indirect jump 来进行的,比如通过指针变量来调用函数。如果该指针被篡改了,那么 CPU 执行的代码就会被篡改为攻击者所选择的位置,从而产生预期之外的结果。几乎所有的正常规模的软件程序中都会包含一些可以被利用的代码段,如果控制权落在这些代码段中就可以做一些原本不期望发生的事情。术语 "forward edge" 就被用来描述对出站控制流(outgoing control flow)的攻击。已经有一些纯粹基于软件保护来阻止 forward-edge attach(例如 LLVM 中的 forward-edge CFI),也有基于硬件的机制。

arm64 和 x86 架构使用专用的 BTI(arm64)和 ENDBR{32,64}(x86)指令对 forward edge 进行粗粒度的保护,这些指令为 indirect branch 这种间接跳转创造了一个 "landing pad" 着陆点。在执行一个间接跳转时,CPU 将检查目标地址的 landing pad;如果没有找到,CPU 就会进入 trap 来捕捉到这个意外情况。Linux 在 arm64 和 x86 上都支持对内核本身代码进行这种保护,但目前只有 arm64 支持在用户空间也启用这个保护功能;x86 版本仍在开发中。

虽然对 forward edge 的保护可以防止跳转到代码块的中间位置,但仍有可能在这些 landing pad 之后找到可以被攻击者利用的代码,并且,还有 backward edge 也可以被攻击者所利用。

Return address integrity

面向返回的编程(ROP, return-oriented programming)攻击会利用堆栈溢出,把函数的返回地址替换为另一段机器码的地址,用它来执行攻击者需要的一些动作,并最终用 return 指令来结束。这样的一段代码就被称为 "gadget"。还可以把多个 gadget 串起来用,被攻击的程序规模越大,攻击者就越容易创建一串 gadget 从而执行他想要完成的操作。人们经常会用术语 "backward edge" 来描述 ROP 攻击。

许多架构都会将返回地址存储在一个特殊的 link register 寄存器中。但是,那些不是最底层的函数(non-leaf function)在调用另一个函数之前,都会将 link register 的内容保存在堆栈上,然后在该函数返回后再把 link register 的值恢复回去。为了保护堆栈上的这个 return address 的记录,arm64 和 powerpc 架构可以在地址旁边同时存储一个经过加密计算出来的哈希值。这些架构中提供了特殊的指令用来给加密操作生成一个随机的密钥,根据该密钥和地址值来创建一个哈希值,并验证该哈希值与从堆栈加载出来的值是匹配的。

哈希值的存储位置各有差异。在 powerpc 系统上保存在它所保护的指针旁边的堆栈中,而在 arm64 系统上,哈希值占据了指针本身最高那些 bit (指针本身未使用这些 bit)。powerpc 和 arm64 的机制都在 GCC 和 LLVM 中都有支持,但只有 arm64 在 Linux 内核中对这个特性(称为 pointer authentication)有相应的支持。

另一种确保 backward-edge 完成性的方法是使用 shadow stack 来作为通常的 stack 的补充。这个影子堆栈中存储了每个被调用的函数的返回地址;每当一个函数返回时,正常堆栈上的返回地址会与影子堆栈上的地址进行比较。只有当这两个地址匹配时,才允许继续执行。英特尔和 AMD 都在硬件上实现了对影子堆栈的支持,因此每条函数调用指令都会将返回地址保存在正常堆栈和影子堆栈中,每条 RET 指令都会验证返回地址是否匹配。当返回地址不同时,CPU 会产生一个 control-protection fault。影子堆栈的实现也会影响其他那些会改变控制流的指令的行为,包括 INT、IRET、SYSCALL 和 SYSRET。

影子堆栈比指针认证方案的支持需求更加强烈。最终版本的 patch 使用户空间能够支持影子堆栈,其中包含了 35 个 patch,并在 https://lwn.net/Articles/885220 进行过深入介绍。除了启用硬件的影子堆栈功能并将其引入内核核心代码之外,这些 patch 还定义了新的内核 API 来为影子堆栈保留内存、启用和禁用影子堆栈功能,并可以把影子堆栈的状态 lock 下来。这些 API 是希望供 C 库使用的,影子堆栈的存在整体来说对于大多数应用程序应该是透明的。不过这些 patch 还没有被合并到 mainline 内核中。

Special cases

像 GDB 和 CRIU 之类的一些程序都需要控制其他进程的执行,这意味着它们需要采用某些非标准的方式来处理影子堆栈。例如,GDB debugger 经常需要在被调试程序的各个 stack frame 之间跳转;需要在 call chain 上下移动中保持影子堆栈与正常堆栈要是同步的。

影子堆栈的内容可以被更新,只要在调用 ptrace() 的时候加上 PTRACE_POKEUSER 命令就好,但随后需要再调用 ptrace() 来更新影子堆栈的指针。有一个提案是扩展 PTRACE_GETREGSET 和 PTRACE_SETREGSET,从而支持对控制影子堆栈机制的寄存器的访问;这个功能是作为 "Control-flow Enforcement: Indirect Branch Tracking, PTRACE" 这组功能的第 11 版改动的一部分发布出来的。但此后没有再进一步发布过。这个接口被英特尔的 GDB fork 所使用,但最终操纵影子堆栈控制寄存器的内核 API 是个什么样的形式,目前还不清楚。

CRIU 像 GDB 一样,在对各个进程抓取 checkpoint 以及恢复的时候,需要能密切控制这些进程。用于 GDB 的 ptrace()接口对 CRIU 来说当然是有用的,但还不够。除了要能调整影子堆栈的内容以及影子堆栈指针之外,CRIU 还必须能够将影子堆栈恢复到与 checkpoint 抓取时完全相同的虚拟地址。有一个可能的方案是扩展 map_shadow_stack() 系统调用,来接受一个额外的参数作为地址;当这个参数不为零时,map_shadow_stack()的行为类似于 mmap(MAP_FIXED),并会试图在所申请的位置来给影子堆栈预留分配内存。

CRIU 所要处理的另一个问题是需要恢复影子堆栈的内容。当然,这可以用 ptrace() 来完成,但会很慢,而且需要对 restore 逻辑进行大幅度的修改。更好的方法是使用一个特殊的 WRSS 指令来让应用程序对自己的影子堆栈进行写入。但是这个指令只有在影子堆栈被启用后才可用,于是它本身就是有问题的。

在 GNU C 库加载一个程序时,会根据 ELF 头为该程序启用或禁用影子堆栈功能,然后将该功能状态永久地 lock 下来。CRIU 使用 clone() 来把要 restore 的 task 创建出来,最终结果就是它们继承了 CRIU 控制进程的 shadow-stack 的状态。因此,如果 CRIU 是在启用了影子堆栈的情况下建立的,它就不能 restore 那些没有影子堆栈的 task,反之亦然。解决这个问题的方法是额外调用 ptrace() 来覆盖掉影子堆栈的 feature lock。

CRIU 还需要一些其他 patch 来支持 pointer authentication,因为尽管这个功能已经存在一段时间了,但没人能在 CRIU 中支持它。

What comes next

我们还没有看到影子堆栈的实现所暴露的内核 API 的最终形式是什么,需要等它真正进入 upstream kernel 才知道。在这一步之后,GDB、CRIU、也许还有其他对其控制流做了棘手的事情的应用程序都需要进行更新从而应对影子堆栈带来的麻烦。而且,在用户空间的影子堆栈支持问题解决之后,还有一个有趣的问题需要回答:为 Linux 内核支持影子堆栈需要做些什么?如果要做这个工作,它所引入的复杂性以及工作量,是否真的值得?

全文完
LWN 文章遵循 CC BY-SA 4.0 许可协议。

欢迎分享、转载及基于现有协议再创作~

长按下面二维码关注,关注 LWN 深度文章以及开源社区的各种新近言论~

format,png

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值