LWN:6.4里也许会支持用户空间影子堆栈了!

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

User-space shadow stacks (maybe) for 6.4

By Jonathan Corbet
March 24, 2023
DeepL assisted translation
https://lwn.net/Articles/926649/

x86 架构的影子堆栈(shadow stacks)已经支持很久了;LWN 在 2018 年首次报道了这项工作。不过经过五年的时间和众多的版本演进之后,似乎 x86 上的用户空间影子堆栈(user-space shadow stack)可能只能在 6.4 内核版本中才得到支持。自从我们上次在 2022 年初赶上这项工作以来,它已经经过了不少变化。

影子堆栈是防御面向返回编程(ROP, return-oriented programming)攻击的一种方式,也能防止其他一些针对进程调用栈(call stack)的攻击。影子堆栈本质上就是硬件维护的在每个函数的调用被放到 call stack 里的时候对返回地址所做的一个副本记录。任何破坏调用栈的攻击都无法改变影子堆栈中的相应项目;因此,在函数返回时就会发现出现了问题,并在攻击者能够控制之前就把该进程终止。2022 年的 LWN 文章中有更多关于 x86 影子堆栈工作方式的细节信息。

当前版本的 patch set 是 Rick Edgecombe 发布的第八个修订版(他在 Yu-cheng Yu 发布了大约 30 个修订版后接手了这组 patch)。

API changes

用于处理影子堆栈的用户空间 API 在过去一年中没有什么变化。大多数操作都是通过 arch_prctl()调用发起的,具体来说是:

  • ARCH_SHSTK_ENABLE 为当前线程打开影子堆栈;影子堆栈在进程启动时不会被内核缺省打开。

  • ARCH_SHSTK_DISABLE 让当前线程停止使用影子堆栈。

  • ARCH_SHSTK_LOCK 阻止对线程的影子堆栈状态进行后续更改。这个操作的用途之一是可以阻止攻击者在破坏调用栈之前以某种方式禁用影子堆栈。

  • ARCH_SHSTK_UNLOCK 可以撤销 ARCH_SHSTK_LOCK 的效果。这个选项在 12 月被添加到第四版 patch set 中;它之所以存在,是为了支持像用户空间的 Checkpoint/Restore 这样的功能,需要在进程启动后能够改变影子堆栈的状态。这个选项只可以通过 ptrace() 调用;进程不能直接对自己发起这个调用。

  • ARCH_SHSTK_STATUS 返回当前的影子堆栈状态。

通常情况下内核会处理影子堆栈的分配和存放位置,但在某些情况下,应用程序需要自己直接管理其影子堆栈。map_shadow_stack()系统调用就是为了这个目的而创造的;它的原型在过去的一年中发生了一些改变:

void *map_shadow_stack(unsigned long address, unsigned long size,
         unsigned int flags);

这个调用将尝试在指定地址上建立一个指定 size 的影子堆栈,成功后返回实际映射的地址。flag 的一个可能的值是 X86_FEATURE_USER_SHSTK;它要求必须提供 "restore token" (用途之一是防止多个线程共享同一个影子堆栈) 被存储到新创建的堆栈中。

还是以前那个 SHSTK
有一次,Andrew Morton 抱怨 "shstk" 这个缩写,说它 "听起来像我在醉酒时用俄语骂人"。结果,这个术语被从大部分通用代码中抽出,但在 x86 部分仍然存在。

map_shadow_stack()还有一个微妙的改动,会影响到影子堆栈的通常处理方式。影子堆栈功能跟 32 位代码不兼容,具体来说是在牵涉到 signal 时。内核会拒绝为运行在 32 位模式下的线程启用影子堆栈,在第四版 patch set 中,如果一个进程在影子堆栈启用后切换到 32 位模式,就用代码来直接禁用所有的 signal handler。

除了看起来有点像 hack 的做法外,它也没有完全解决这个问题。事实证明,一个 64 位线程可以在内核不知道或不允许的情况下切换到 32 位模式,这意味着信号处理程序的禁用可以被规避掉。在人们讨论了一段时间如何避免发生这种情况之后,大家决定(在第 5 版中)改成总是将影子堆栈映射到 4GB 以上的虚拟地址,使其无法被 32 位代码访问到。因此,当影子堆栈被启用时,任何试图切换到 32 位模式的做法都会导致立即 crash。

这个改动引入了一个新的 mmap()标志 MAP_ABOVE4G,它会强迫在 4GB 的虚拟地址边界之外来创建 mapping 映射。传递给 map_shadow_stack() 的地址(如果是 0 的话就表示不指定特定地址)也必须在 4GB 以上,否则调用会失败。今后如果有一天某位开发者也许有动力的话会找到一种方法来使 32 位代码也能配合影子堆栈一起工作,但是考虑到人们对 32 位代码的兴趣非常小,这似乎不太可能发生了。

The glibc problem

尽管在运行所有程序时都启用影子堆栈可能是件好事,但有些应用程序在这种环境下会被破坏掉。任何会修改自己的调用栈的程序(例如 just-in-time compiler)都会发现自己修改后会跟影子堆栈不再同步,从而导致异常终止。因此,影子堆栈要想启用就必须限制在能够处理它的代码上。

前段时间开发的方案是在程序的可执行镜像的 .note.gnu.property ELF 部分放置一个特殊的 note 注释。如果这个注释存在的话(编译器在构建程序时有选项可以指定),这表明可以安全地启用影子堆栈来运行程序。但这个 note 并不足以让内核做出决定,所以启用影子堆栈的工作就留给了用户空间,具体来说就是 C 库的程序加载器(program loader)。

在 GNU C 库(glibc)的社区中,热情的开发者很快就为开启影子堆栈提供了支持,只等它准备好了;当前版本的 glibc 已经准备好在内核支持影子堆栈的时候打开这个功能。只有一个小问题:glibc 的支持是根据早期版本的用户空间 API 来编写的。那个 API 已经不存在了;试图使用它的话,会导致程序 crash 并且无法运行。这的确可以保证它不受 ROP 攻击,但用户可能会对采用这种方式保证安全的做法会很愤怒,可能会找我们抱怨。

这个问题之前通过改变 API 来解决了,这样 glibc 根本找不到这个 API,于是就认为影子堆栈的功能不存在了。不过,glibc 开发者已经说过,他们打算在新的影子堆栈 API 被合并到内核之后就马上用起来。这样一来当新版 glibc 在系统上运行起来时,任何表示准备使用影子堆栈的程序都会可以真的用起来。

这就导致了一个新的问题,正如在第三版的邮件说明中所指出的:并不是所有被标记为准备好的应用程序都是真的准备好了:

但是今天存在许多应用程序二进制文件都带上了这个 bit,关键是,它在一些流行的发行版编译的时候就直接对大批量的应用程序直接打开了,而根本没有验证这些软件包是否真的支持影子堆栈。因此,在 glibc 更新时,影子堆栈将突然会被大量应用程序打开,而缺少测试验证。

在这种环境下会崩溃的应用程序就包括了 node.js 和 PyPy,所以这似乎是一个真正的麻烦。在 Fedora 37 系统上的快速检查显示,PyPy 确实是在启用影子堆栈的情况下编译出来的:

$ readelf -n /usr/bin/pypy
Displaying notes found in: .note.gnu.property
  Owner                Data size        Description
  GNU                  0x00000040       NT_GNU_PROPERTY_TYPE_0
      Properties: x86 feature: IBT, SHSTK
[...]

即使根本原因在于用户空间,那它也可以通过升级到新的内核来触发问题,因此看起来就好像是内核的改动导致了问题一样(regression)。内核开发者通常喜欢避免破坏大家的系统,哪怕可以说是由于别人的错误。

Edgecombe 认为,最理想的解决方案是直接换一个新的 ELF bit 来识别真正的影子堆栈的准备情况,并让 glibc 使用它来判断。然后可以鼓励发行商更小心地将应用程序标记为可以使用影子堆栈。但是他说,"看起来 glibc 的开发者对解决这个问题并不感兴趣",所以还需要其他一些改动。在第 3 版中就提供了这样一个 patch,当检测到 ELF bit 时禁用影子堆栈 API。我们的想法是,一旦发行商确认他们运送的所有软件包中的二进制文件都是正确标记过的,那么他们就会禁用该检查。

这个 patch 被人们称为 "a bit dirty",并且是为了搜集人们的反馈意见而加入的,结果确实达到了目的。H.J. Lu 建议,正确的做法就是避免升级 glibc,直到系统准备好为止。Florian Weimer 补充说,大多数不兼容的代码都是在进程启动后加载的库文件中;内核测试不会发现这些,而且要再禁用影子堆栈的话可能就太晚了。

过了一段时间,Edgecombe 问 Linus Torvalds 他认为应该如何处理这个问题。Torvalds 回答说,他不想在没有理由的情况下预先禁用影子堆栈的支持:

一旦[影子堆栈功能]在内核中被启用,并且人们抱怨它破坏了现有的二进制文件,在那个时候,我想它会被再次禁用。那时可能会使用类似你建议的这个 patch。但在实际问题出现之前,以及在我们真正地在内核中使用这段代码之前,我不会这么做。

禁用影子堆栈 API 的 patch 已经从这个系列中删除了。Weimer 描述了几个方案,以确保影子堆栈可以在发行版中安全地启用,并声称采用新的 ELF bit 会让这个过程大大延迟。他说,支持影子堆栈与支持一个新的系统调用没有什么不同;新增系统调用也会破坏现有的应用程序的,主要是因为 seccomp() 过滤器不理解新的调用而引起的。

On to 6.4

讨论的结果是,内核不会采取特别的措施来避免破坏那些被错误地标记为准备使用影子堆栈的二进制文件。至少在出现问题之前不会这么做。大多数其他悬而未决的问题似乎都得到了解决,因此 Edgecombe 在当前版本的封面信中就说:"我们当前有了一个相当好的初始版本的影子堆栈实现了"。他说,还有一些需要改进的地方,但在对现在的代码进行了一些实际使用之后,这些改进可能可以做得更好。

因此,在所有这些工作之后,40 个影子堆栈 patch 已经被添加到了 tip tree 中,它会把这些 patch 引入到 linux-next 中。如果在接下来的一个月左右的时间里没有出现令人震惊的问题的话,x86 系统的用户空间影子堆栈支持很可能会在 6.4 合并窗口中走到 upstream 里。经过漫长的开发期,影子(堆栈)最终将真正知道 ROP 攻击者的心里都在打着什么算盘。

全文完
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、付费专栏及课程。

余额充值