今天要为大家推荐的论文来自ASPLOS 2024上的UBfuzz: Finding Bugs in Sanitizer Implementations,由于苏黎世联邦理工学院(ETH Zurich)Zhendong Su研究组完成并投稿。
背景介绍:Sanitizers
使用 C/C++ 等程序语言编写的代码可能包含未定义行为(Undefined Behavior),比如 Buffer Overflow、Use After Free 等。实际上这些未定义行为,尤其是跟内存访问相关的行为,可能导致很多的安全漏洞。比如,Google 的公开报告[1] 显示 Chromium 中超过70%的安全漏洞都源自于内存安全问题。为了更好的检测程序中的未定义行为,学术界及工业界广泛使用 Sanitizer 作为一种动态检测手段。常用的 Sanitizer 包括 AddressSanitizer (ASan,用于检测内存错误,比如栈溢出),UndefinedBehaviorSanitizer (UBSan,用于检测多种未定义行为,比如整数溢出),以及 MemorySanitizer (MSan,用于检测未初始化内存的使用)。Sanitizer 目前已经集成在主流编译器中,比如 GCC 和 LLVM。给定源代码,我们可以在编译的过程中开启 Sanitizer 选项来得到插桩后的可执行文件。例如,下图左边的代码包含一个栈溢出漏洞 (第8行),下图右上部分显示开启 ASan (-fsanitize=address) 之后编译并运行代码可以正确的检测出栈溢出。但是,如果我们同时使用优化选项 -O2,ASan就检测不出来了 。这其实是由于 GCC 中 ASan 的一个实现 bug 导致的(注意:并不是由于 -O2 的优化导致的,后文会讨论),而这篇文章的目的就是自动地检测出这种 Sanitizer 实现中的 bug。
这种 Sanitizer bug 的直接结果就是可能导致程序中的未定义行为不能被检测出来,从而严重影响 Sanitizer 的有效性。例如,即使现在大家使用 Fuzzing 技术结合 Sanitizer 来大规模检测内存安全漏洞,由于 Sanitizer 的实现 bug,程序中的安全漏洞即使被 Fuzzer 触发也有可能不被报出来,从而导致 Fuzzer 无法有效地发现漏洞。
方法及实现
自动化地检测 Sanitizer 实现中的 bug 看起来好像很直接:首先生成大量的包含各种未定义行为的程序,然后测试 Sanitizer 是否能够检测出来。如果不能的话,那就证明我们“发现”了一个 Sanitizer 的 bug。理论上这种方法的确可行,但是最关键的问题是目前并没有有效地方法来自动地生成这种包含未定义行为的程序。这篇文章的第一个贡献就是设计并实现了一种通用的生成这种程序的方法:Shadow Statement Insertion。通俗地来说,该方法需要使用者先提供一个正确的程序 P 和想要得到的未定义行为类型 T,然后通过各种静态/动态分析来决定程序 P 中可以引入未定义行为 T 的具体位置,最后通过精确地变异 P 来得到新的包含未定义行为 T 的程序 P’。例如,假设想要在下图左边的正确程序 P 中引入一个栈溢出。该方法首先分析出其可能的引入位置是 a[x],然后分析出数组 a 的大小是5,且索引值 x 为1。为了引入一个栈溢出,该方法随之插入对应的Shadow Statement (下图右中的 x=5 )。当然,实际的插入方法和过程更为复杂,大家可以通过阅读原文来了解具体的实现过程。
论文中分析了 9 种常见的未定义行为的触发条件并分别定义了他们的 Shadow Statement。
但是,这种方法并不能有效地检测 Sanizier 实现中的 bug。这是为什么呢🤔️ 原因是由于编译器的优化带来的影响。在现在的编译器中,Sanitizer 实际上是众多编译环节(Pass) 中的其中一个,如下图所示。
所以 Sanitizer 接收到的程序实际上是经过优化处理的,而这种情况下,编译器的优化可能导致包含未定义行为的代码被转化成正确代码(可以参考作者在 ASPLOS 2023的文章[2]或者 VUSec 组的 PLDI 2023 [3])。例如,下图左包含栈溢出的程序会被 GCC -O2 优化为下图右,从而导致ASan无法将其正确检测出来。
这就导致一个很严重的问题:对于一个生成的包含未定义行为的程序,当 Sanitizer 没有报告时,是无法知道这是因为 Sanitizer 的实现 bug 还是由于编译器的过度优化。为了解决这个问题,本文的第二个贡献是提出了一个针对 Sanitizer 测试的Test Oracle:Crash-Site Mapping。其核心思想是通过校验最终生成的Assembly Code 中是否仍然存在源程序中未定义行为对应的代码片段来判断编译器是否过度优化。当最终的 Assembly Code 中仍然存在未定义行为对应的代码片段且 Sanitizer 并没有报告时,就知道这是一个 Sanitizer 的实现 Bug。具体的技术细节可以参考论文原文。
实验结果
作者把该测试框架(程序生成器+Test Oracle)称作 UBfuzz,具体的复现代码可以在文章对应的 Artifact 中找到。作者测试了 GCC 和 LLVM 中的 ASan、UBSan、以及 MSan,最终结果是作者一共找到了 31 个 Sanitizer 的实现 bug。值得说一下的是,所有 Sanitizer 都有 bug。
一点思考
这个工作最开始的出发点其实是作者在分析 Fuzzing 找到的一个栈溢出时,发现 LLVM 编译的版本能快速找到但是 GCC 编译的版本无法找到。所以作者猜想可能是 GCC 的Sanitizer 有问题,从而启发了这个工作。在这个工作里,作者设计了一个程序生成器,能够大规模地生成想要的包含各种未定义行为的程序。所以理论上,这个生成器也可以用来测试其他的程序分析工具,比如各种静态分析工具。
论文PDF:https://arxiv.org/pdf/2401.04538.pdf
欢迎感兴趣的同学联系作者呀~作者开源并维护其中的未定义代码程序生成器 UBGen,代码在这儿 https://github.com/shao-hua-li/UBGen 欢迎大家点点小星星~
https://dl.acm.org/doi/abs/10.1145/3591257