LWN:GCC 中两个安全相关的改进!

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

Two security improvements for GCC

By Jonathan Corbet
September 24, 2021
LPC
DeepL assisted translation
https://lwn.net/Articles/870045/

人们常说 GCC 和 LLVM 编译器之间的竞争对它们两者来说都是好事。竞争出现在多个方面,其中之一就是 security 功能上。如果一个编译器增加了某种加固程序(harden programs)的方法,那另一个编译器可能就会效仿。在 2021 年的 Linux Plumbers 会议上,Qing Zhao 讲述了 GCC 是如何在两个安全相关的功能上成功追赶上来的故事,这也是内核社区特别感兴趣的两个功能。

Call-used register wiping

Zhao 先列举了内核开发者一直要求具备的 security feature 列表,她指出 LLVM Clang 编译器已经具备了其中一些功能,但 GCC 尚未具备。她一直在努力来填补这一空白,首先是被称为 "call-used register wiping(将函数调用过程中使用到的寄存器擦除掉)" 的功能,也就是在函数返回之前清除掉此函数中所使用过的寄存器的内容。人们出于多个原因会希望在编译器中增加这一功能。

a2678190dac0ad2e02f5bdc8bb4de2f7.png

第一个原因是希望挫败那些针对返回值编程(ROP,return-oriented programming)攻击方式,公开漏洞之中经常可以看到这类攻击。ROP 攻击的工作原理是将一组 "gadgets,小工具"(用来执行一些对攻击者有用处的代码片段)连在一起(chain together),然后再返回。如果攻击者能在堆栈上放置好精心准备的一系列 "return address,返回地址",他们就能把这一系列的 gadget 代码串起来,使得内核会去做他们想要做到的事情。

ROP 攻击通常来说最终总是要调用其他内核函数来执行其任务的,被调用的函数会依赖处理器的寄存器中存放的参数。因此,要想使 ROP 攻击奏效,就需要将精心设计的数值预先放入这些寄存器,这样以来只要在每个函数返回时清除掉寄存器可以非常有效地挫败这类攻击。它会打破攻击者试图组装起来的 gadget 链路。

当然,在返回时清除寄存器的另一个原因是为了防止信息泄露。我们经常低估了攻击者能从 CPU 寄存器中留下的数据中了解到什么信息。

因此,清除寄存器是件好事,但这里仍有一个问题,也就是哪些寄存器需要被清除。如果目标是阻止 ROP 攻击,那么只清除用于存放函数参数的寄存器就足够了。相反,为了防止信息泄露,就需要清除掉所有被使用到的寄存器。还有一个相关问题,那就是寄存器应该被清零,还是应该设置为随机值。对于 GCC 来说,归零被认为是最安全的选择,因为 0 值是最不可能是对其他代码来说有意义的一个值。这也就引出了一个更小、更快的实现方式。

这个功能在 GCC 11 版本中已经带有了,由 -fzero-call-used-regs= 这个编译器选项来控制,有许多可选值来控制哪些寄存器应该被清除。还有一个新的函数属性(zero_call_used_regs),可以用来控制清除特定函数的寄存器。其实现方式是增加一个新的编译器步骤(a new compiler pass),它会查看所有的 exit 代码块,找到每个返回指令,计算出所要清除的寄存器集合(也就需要跟踪记录有哪些寄存器被实际使用了),并生成真正进行清除工作的指令。这个功能起初支持了 x86 和 Arm64 架构,SPARC 是在稍后加入的。

在 5.15 版本的 kernel 中已经包含了在用 GCC 编译时支持对寄存器进行清零。根据 changelog 中的说法,它将原先可以用于内核攻击中的 ROP gadget 数量减少了大约 20%。

Stack variable initialization

C 语言中一个著名规则是 automatic variable(位于 stack 中的变量)是不会被编译器初始化的。如果代码在给它赋值之前使用了这样的变量,那么它用到的很可能是一些随机垃圾数据,这就会导致各种问题。有可能出现错误的运算结果,但也有更糟糕的情况:如果攻击者能够找到一种方法将某个数值放在 automatic variable 会在 stack 中出现的位置上,那攻击者就可以攻破系统了;如果将某个未初始化的变量用来作为 lock,那么就可能会出现无法避免的 race condition。这些错误都需要极力避免。

有一些工具可以尝试着检测出这种使用到未初始化的 stack variable 的情况。例如,GCC 和 Clang 都支持 -Wuninitialized 选项,会在编译时给出 warning。这两个编译器还有一个 -fsanitize= 选项,可以在代码运行时使用这类变量的时候检测出来。除了编译器之外,还有像 Valgrind 这样的工具也可以用来发现使用未初始化变量的情况。

Zhao 认为这些工具虽然有用,但也都有局限性。静态工具(也就是 compile-time tool)只能在单个函数中进行分析,这就可能需要对其他函数的行为做出一些假设。它们在检测未初始化的数组元素或通过指针访问的值是否初始化过的时候,能力很有限。因此它们会漏掉一些问题,同时也无法去除代码中不可能走到的路径,从而产生误报 warning。动态工具(也就是 run-time tool)工具则不能覆盖所有的路径,所以它们会出现遗漏的情况,同时它们也会带来大量的 run-time 开销。

从即将发布的 GCC 12 开始,-ftrivial-auto-var-init= 这个选项将控制 stack 中变量的自动初始化。它的默认值是 uninitialized,也就是保持之前版本的行为。如果它被设置为 pattern,变量就会被初始化为一些可能导致程序 crash 的值,这个选项是给调试用的。而将其设置为 zero 的话则是直接将所有 stack 中的变量都初始化为零,这个选项是为了对生产代码进行加固。还有一个新增的变量属性 uninitialized,用来标记那些故意不初始化的变量。

不管这个新选项配置成什么值,只要设置了 -Wuninitialized,编译器就仍然会发出 warning。毕竟这个选项的目标不是为了导致 "语言分叉出新的变种",而是为了增加安全级别来提供额外保护,所以未能正确初始化变量的代码仍然应该被 fix。这项工作已经在 9 月初提交到 GCC 主线代码中,还有一些需要修复的 bug,应该很快就会解决完毕。

Zhao 没有谈到内核中对这个功能的支持情况。不过,Clang 在之前一段时间就已经支持了这个选项,内核直接可以利用这个功能了,所以一旦 GCC 具备了相关支持,那就应该可以直接用起来。这应该有助于阻止一大类 bug,同时可能意味着现在内核支持的 GCC 插件 structleak 可以结束生命周期了。虽然这些功能的开发工作都是由内核的 wish list 来驱动的,但它们应该远远不只是会对内核有用。

本讲座的视频可以在 YouTube 上找到。

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

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

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

2487cc40ddca71e87531e78e07569712.png

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值