![640](https://i-blog.csdnimg.cn/blog_migrate/c357d80c385913c39bb48cb0ab12f623.gif)
Comparing GCC and Clang security features
By Jonathan Corbet
LPC
安全加固需要对整个系统各方面同步进行,当然也包括要加固编译器。开源社区里面目前有两个主流编译器,分别提供了不同安全防护功能。Kees Cook在2019 Linux Plumbers Conference上一个Toolchain相关的microconference里面为大家解读了目前GCC和LLVM Clang所支持的安全功能,指出了它们哪里仍然需要改进。
Cook先介绍了长久以来两者都支持的那些旧有安全功能,包括stack canary(堆栈上写入特殊标记从而检测是否被破坏),使用不安全的format-string时给出警告,等等。他后面主要介绍了近期新加入的安全功能。
首先是per-function sections,把每个函数放入自己专用的ELF section。相应的编译开关是-ffunction-sections,两个编译器都支持。这个功能可以让地址空间排布随机化控制粒度更细。每个函数都可以独立于其他函数的位置来随机调整位置。这个功能他觉得是“bizarre and wonderful (很奇葩不过会很有用的功能)”
switch语句中的implicit fall-through行为很多时候会导致bug,许多项目里面都在想法避免。目前两个编译器都支持-Wimplicit-fallthrough选项。GCC还支持一个特殊attribute能让这个fall-through行为必须显式设置,Clang最近也支持了。不过Clang社区并不打算像GCC一样支持注释里的fall-through标记。Linux kernel在去年经过500来个patch修正了所有的fall-through warning之后,目前已经没有隐式fall through了。其中大概10%确实是真的bug。
LTO(link-time optimization)在这两个编译器里也都支持了。虽然这不是一个专门针对安全方面做的功能,不过人们发现要想确保control-flow integrity的话,就必须要实现LTO,只有这样才能对程序里的所有函数都有一个总体的掌握。虽然两个编译器都支持LTO,不过用起来还是很麻烦。也有一些人担心LTO会导致C memory model和kernel的memory model产生差异。目前还没人详细描述这个问题,可能是理论上存在吧。在有证据之前,不应该阻碍LTO的推广。
Stack probing是指在读取新扩展的stack空间时只能小步增长,这样就能避免跳过guard page。GCC可以用-fstack-clash-protection支持这个功能,而Clang还没法支持。这个功能对user space编译更加游泳,因为kernel里面已经彻底避免使用变长数组了。
Clang有个-mspeculative-load-hardening参数用来避免Spectre v1问题。GCC尚未支持。虽然打开这个选项会导致性能下降不少,不过还是要比在代码里到处加lfence barrier指令要好很多。也有一个attribute可以用来设置只在某些特定函数里生效,这样就不会让整个系统都被拖慢了。
因为被调用函数里并不需要保留调用方函数里已经保存了的内容,所以通常返回时这些寄存器里都是随机值。我们可以通过在返回时清除这些寄存器来在某种成都上组织一些speculative attack或者side channel攻击。Cook了解到这样只有很少的性能损失。Peter Zijlstra提出不同意见,他还是想先看看详细的描述,具体解决了什么问题。尽管每一处影响很小,不过积累起来也有可能导致大问题。Cook认为需要在函数返回时把architecture相关的东西恢复回已知状态,虽然目前没有阻止某个已知的攻击手段,不过有可能对某种未来的攻击有防护能力。GCC有个patch在做这件事情了,Clang暂时还没有。
还有一个大家在争论的措施,是在函数入口处自动初始化stack变量。GCC有个plugin插件可以做到,目前Clang也在加。不过它设计的行为让kernel社区不太满意。Clang会把stack变量初始化成poison pattern,而Linus Torvalds更希望都初始化为0。
还有一些人反对这个功能,首先,这可能会导致以前的warning信息没有了(使用未初始化的变量),GCC plugin处理之后会导致KASAN这些工具错过某些问题了。更重要的是,这个行为是C语法的语义的一个大改动,还能称为C语言吗?
还有个技巧是structure layout randomization。GCC几年前就可以对kernel实现这个功能了。目前也在往Clang加,不过暂停了。Cook的评论是这个功能对大多数人没有用,只有少数非常偏执于安全的场景下才需要。
带符号的整形变量如果溢出了改怎么办?C语言里面并没有清晰定义这一点。不过Zijlstra马上指出在kernel里面这个行为是规定好的,按照2的补码来实现。Cook认为绝大多数情况下signed int如果溢出了都是一个意外情况。两个编译器都支持-f-sanitize=signed-overflow参数,不过这个做法也并不完美。打开这个warning的话,会带来6%的代码增长,如果不打开的话,程序直接会死掉,kernel肯定不喜欢这样。打开warning的话,实际上并没有阻止这个溢出行为。Cook更希望的是这个值就停在最大值那里不动,最理想的是能实现一个用户可以定义的处理函数,来决定在signed overflow发生时该怎么办。
Unsigned integer overflow则通常是故意为之的,尤其是kernel里。C语言的定义很明确,不过有溢出的话还是很容易导致攻击。Clang可以捕获到unsigned overflow,GCC还不行。不过他还是希望能阻止overflow。
Control-flow integrity (CFI),简单来说,是确保代码的跳转一定不会出错。常见的一类问题就是发生在函数返回的时候,一定要跳回函数被调用的位置。X86处理器硬件上支持"backward-edge"检查,并不需要编译器支持。Arm64处理器有个PAC指令,依赖于编译器加入这个指令。两个编译器都能支持PAC指令。针对其他硬件上不支持backward-edge CFI的处理器,软件就需要实现一个shadow stack,来保证函数返回时不会被恶意程序控制导致跳错位置。Clang此前支持shadow stack,被人报了问题之后就被移除了;GCC则从来没支持过。
还有个"Forward-edge" CFI,则是要确保间接跳转(indirect jump)要能跳到目标位置。这就需要验证目标地址是否是一个合理的跳转目标。有的硬件能支持,不过也只能确保目标地址是一个函数的入口。这样就能大大减少入侵者的攻击面。不过攻击者可能会连锁调用多个函数,这样的保护就不够了。X86有个ENDBR指令实现这个功能,ARM的是BTI。这两个编译器都能支持。软件上来说,Clang管控的更严一些,它会检查调用到的这个函数是否有正确的prototype原型定义。不过Cook认为我们最需要的其实是更细粒度的forward-edge CFI。
最后Cook总结了演讲之后,大家又争论了一下integer overflow的问题,H.Peter Anvin建议说如果希望改变integer类型的语义的话,更好的方法是不是换成C++等其他语言,毕竟他们已经有了更好的定义。不过在场听众并不是都接受了这个说法。
[Your editor thanks the Linux Foundation, LWN's travel sponsor, for supporting his travel to this event.]
全文完
LWN文章遵循CC BY-SA 4.0许可协议。
热烈欢迎转载以及基于现有协议修改再创作~
长按下面二维码关注:Linux News搬运工,希望每周的深度文章以及开源社区的各种新近言论,能够让大家满意~