LWN: 改进GCC的 -fanalyzer 选项!

共 3533字,需浏览 8分钟

 ·

2021-10-11 18:45

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

Improvements to GCC's -fanalyzer option

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

GNU Tools Cauldron(GNU 工具链开发者的年度聚会)已经是连续第二年在 Linux Plumbers 在线大会上作为一个专门 track 来进行了。在 2021 年的会议上,David Malcolm 首先介绍了 GCC -fanalyzer 选项相关的工作,该选项提供了一些静态分析功能。在 -fanalyzer 这一个领域已经有了很多进展,而且在即将发布的 GCC 12 版本中还会有更多的功能,包括增加的一系列检查可能已经发现内核中的若干漏洞了。

GCC 在被调用时如果带有 -fanalyzer 选项的话,它会运行一个 module 来创建一个 "爆炸图,exploded graph",可以将程序的控制流和数据流的状态信息结合在一起显示出来。其包括对内存内容的抽象描述、对变量值的已知限制、以及代码是不是可能正在 signal handler 中运行等等信息。然后,analyzer 就可以使用这个图来尝试探索代码中所有感兴趣的路径,看看可能会发生什么。

GCC 10 版本中,针对潜在错误方面已经新增了 15 个 warning 信息,如两次释放内存、释放内存后继续使用、signal handler 程序中不安全的函数调用、可能在将敏感数据写入日志文件等等。两次释放的检测正是让人们有动力开发这些检查功能的主要因素。GCC 11 中又增加了五个 warning,包括可以检查出从一个 allocator 中获得的内存是否被意外地释放到另一个 allocator 中了,以及对位移运算中一些未定义行为的检查。GCC 11 中还支持使用插件来扩展 analyzer。在 test suite 里面就可以找到一个插件样例,用于检查出 Python 全局解释器锁(GIL)的错误使用。

最近,Malcolm 在思考针对 GCC 12 应该做些什么。他原本想增加对 C++ 的支持,但后来发现他自己更愿意改进 C 语言相关的功能。在得出这个结论之前,他已经在 GCC 11 中实现了 new 和 delete 的支持,并把异常处理(exception handling)作为了他自己的下一个目标,但结果发现这个领域 "相当复杂"。同时,谷歌夏季代码挑战项目的学生 Ankur Saini 也增加了对虚拟函数的支持。因此,C++ 这方面已经取得了一些进展,但 Malcolm 的工作目前将继续集中在 C 语言上。

有几个 C 语言的问题引起了他的注意,其中之一是缓冲区溢出的检测。他实现了一个原型方案,可以根据 symbol 捕捉记录动态分配的 size,并在后续读写时可能超出这个 size 的时候给出 diagnostics 诊断信息。但是很难确定应该在什么时候来报出 warning,现有代码中产生了太多的误报(他的说法是 "a wall of noise"),因此无法真正实用起来。

不过,他想到有一种方法可能可以找到一类特定的问题:就是针对那些可能被攻击者影响某次 access 是否有效的位置。这就引出了 taint detection(污染检测)以及如何确定程序中的信任边界(trust boundaries)在哪里的问题。要想对随便一段 C 代码来确认这两点,这会是一个非常困难的问题。不过,针对特定程序的话,是可以进行标注(annotate)并获得有用的诊断信息。他的注意力转向了内核,因为内核具有一个定义非常明确的信任边界,以及一个用来跨越该边界进行数据搬移的 API。通过对 copy_from_user() 以及 system-call handler 函数的标注,就可能可以发现那些没有对用户所提供的数据进行检查(sanitize)的代码。

GCC 有一个属性(access),用来描述数据是如何在某个的变量或函数中移动的。Malcolm 为该属性增加了两个新的可用值(untrusted_read 和 untrusted_write),用来标记读取来自于(或写入目标是)一个不受信任的位置的数据。因此,比如通过 copy_from_user() 读入内核的数据就会被标记为 untrusted_read。他还为这些函数添加了一个新的 tainted 属性,用来表示该函数的所有参数都应被视为不可信任的。通过修改内核头文件中的一个宏,他就能够将内核中所有的 system-call handler 函数都打上这个 tainted 属性。也可以对其他一些函数进行类似标记,比如说内核生成的文件系统的 callback 函数。

有了这些标注之后,analyzer 就可以检测到两类问题:信息泄露(information leaks)和使用了污染数据(tainted data)。信息泄露都是发生在将未进行初始化的数据直接写回到用户空间的时候。这种情况相对比较容易检测,或者至少他是这样认为的。作为这类问题的一个例子,Malcolm 提出了 CVE-2017-18549,这是一个将 stack 中的随机数据写回用户空间的驱动程序 bug。在这种情况下,被写入的这个未进行初始化的数据是在一个原本被正常进行了初始化的结构中填入的 padding 数据,analyzer 就可以发现这个问题。要让这个问题得到 fix,需要对未初始化数据的跟踪记录代码进行重构,这不是一个容易的任务。

还有一个类似的问题,是从用户空间读取数据进行修改,然后把结果 copy 回用户空间,也许是 copy 到另一个位置。如果 read 操作失败了的话,内核可能会对未初始化的数据进行一些处理然后直接写入用户空间。要 fix 这个问题,需要对处理 copy_from_user()失败的情况。一旦完成了这个工作之后,analyzer 也就可以处理 realloc() 了,尽管它有三种可能结果。

当用户提供的数据被拿来当作数组的索引时,就会出现数据被污损(tainted)的情况。这种情况更难检测,但似乎更重要一些,因为这种类型的漏洞往往可以被利用来攻破内核。例如 CVE-2011-0521 这种情况下,内核代码会读取一个有符号的 "size" 值,根据所允许的最大值来进行检查,但是没有检查是不是负数就直接使用它了。改进后的 analyzer 就能够捕捉到这种情况。

他仍在研究如何实现这个功能的原型,期待可以展示给大家看。作为这个工作的一部分,他开发了一个世界上的最糟糕的内核 module,其中包含了他能想到的所有问题。不过,由于内核使用了大量的 inline 汇编代码,这使得 analyzer 要做的事情在分析完整内核的情况下就变得更加复杂。他已经为这个功能添加了一些基本的处理代码,但是目前并没有去检查实际的汇编操作码(opcodes)。

他一直在针对 upstream 的内核来不断运行自己的检查功能,并且已经发现了一个真正的漏洞,这个漏洞已经被报告出来但是尚未被修复或披露出来。因此,Malcolm 的最新成果代码仍然只能放在在公司内部的代码仓库中。因为它发现了真正的系统漏洞,那么在它所发现的问题被修复之前,他可不想把工具发布出来成为帮助攻击者的 zero-day-finding 工具。其他大部分工作现在都提交到 GCC 12 的主分支中了。他希望能够在 GCC 12 stage 1 周期结束前就完成这项工作并将其推到 upstream(https://gcc.gnu.org/develop.html 这里可以看到 GCC 开发周期的各个 stage 的含义)。

关于这个项目的更多信息可以在 GCC wiki (https://gcc.gnu.org/wiki/DavidMalcolm/StaticAnalyzer) 中找到。

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

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

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



浏览 21
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报