翻译原文:https://blahcat.github.io/2018/03/11/fuzzing-arbitrary-functions-in-elf-binaries/
前言
我决定对 LIEF 项目进行 descent 测试。可执行的解析器不是什么新鲜事物,而让我好奇的(就像大多数 Quarkslab 项目一样)是,因为它还提供了简单的检测功能。最重要的是,LIEF 易于使用且有好的文档支持,这已成为 Infosec 工具集中罕见的优点。
- 通过阅读一些有关 LIEF 的博客文章,我遇到了一个新功能:轻松地向 ELF 导出表添加任意函数。我强烈建议您仔细阅读这篇文章。
当我完成阅读后,我意识到此功能的许多不错的应用场景之一就是模糊测试。但是为什么不使用 AFL 呢?嗯,AFL 是一个很棒的(很棒的)工具,但是它通过提供一些本地的突变输入来模糊整个二进制文件。这对于精确的目标功能模糊有两个缺点:
- 性能:在默认模式(即非持久性)下,AFL 会生成并运行整个二进制文件,这显然会增加进程创建/删除时间,以及所有代码,然后再达到我们想要的功能;
- 模块化:用它来模糊网络服务解析机制并不容易。我知道已经存在解决此问题的尝试,但是我发现它们太过分整洁并且扩展性很差。
另一方面,我们有 LLVM 自己的 LibFuzzer,这是一个很棒的库,非常适合……模糊库。幸运的是,并非所有内容都是库(sshd,httpd)
这正是 LIEF 发挥作用的地方……如何使用 LIEF 将我们目标的 ELF 二进制文件中的一个(或多个)函数导出到共享对象中,然后使用 LibFuzzer 对其进行模糊处理!最重要的是,我们还可以使用编译器清理程序来跟踪无效的内存访问!但这甚至行得通吗?
事实证明,确实如此,花了很多时间,并且在成功尝试简单的 PoC 之后,我意识到这项技术很重要,因此我选择通过尝试发现实际漏洞将其付诸实践。
具体示例:找到CVE-2018-6789
通过一个例子来说明:本周早些时候,mehqq 发布了一篇有关 CVE-2018-6789 的精彩博客文章,详细介绍了她在 Exim 中发现的一个单一漏洞的利用步骤。该问题已在 cf3cd306062a08969c41a1cdd32c6855f1abecf1 中修复,并已提供 CVE-2018-6789。
Exim 是一个 MTA,一旦编译,它就是一个独立的二进制文件。因此,AFL 几乎没有帮助(网络服务),但这对于 LIEF + LibFuzzer 是一个完美的实践案例。
我们必须将 Exim 编译为 PIE(通常使用 CFLAGS 中的 -fPIC
和 LDFLAGS 中的 -pie
设置)。但是我们还需要开启 address sanitizer,因为如果没有它们,堆中的一次性溢出可能不会引起注意。
使用 ASAN & PIE 来编译目标程序
1 | # on ubuntu 16.04 lts |
- 在某些情况下,使用 ASAN 无法创建编译所需的配置文件。因此,编辑
$ EXIM/src/scripts/Configure-config.h
的 shell 脚本以避免过早结束:
1 | diff --git a/src/scripts/Configure-config.h b/src/scripts/Configure-config.h |
编译将正常进行,一旦编译,我们就可以使用二进制文件中 pwntools 的 checksec 并确保其 PIE
和 ASAN
兼容:
1 | $ checksec ./build-Linux-x86_64/exim |
导出目标函数
从 write-up 中可以看出,存在漏洞的函数是 src/base 64.c
中的 b64decode()
,其原型为:
1 | int b64decode(const uschar *code, uschar **ptr) |
该函数不是静态的,二进制文件也没有被 stripped,因此我们可以使用 readelf 轻松解析它:
1 | $ readelf -a ./build-Linux-x86_64/exim |
因此,现在我们知道我们要在 PIE 偏移量 0xcb0bd 处导出函数 b64decode
。我们可以使用以下简单脚本使用 LIEF(> = 0.9)导出函数:
我们还需要导出 store_reset_3()
,用于释放结构。
1 | $ ./exe2so.py ./build-Linux-x86_64/exim 0xcb0bd:b64decode 0x220cde:store_reset_3 |
编写一个 LibFuzzer 加载器以调用目标函数
首先,我们需要一个库的句柄:
1 | int LoadLibrary() |
并根据其原型重构函数 b64decode()·
:
1 | typedef int(*b64decode_t)(const char*, char**); |
现在可以调用 b64decode()
:
1 | $ clang-6.0 -O1 -g loader.cpp -no-pie -o runner -ldl |
这样可行!通过将任意函数的工具设置为子游戏,我们只能为此感谢 LIEF。
开始 Fuzzing
现在,我们可以使用此框架围绕此构建基于 LibFuzzer 的模糊器:
编译,运行它,并惊叹😎:
1 | $ clang-6.0 -DUSE_LIBFUZZER -O1 -g -fsanitize=fuzzer loader.cpp -no-pie -o fuzzer -ldl |
我们在 b64decode
函数上执行了每秒超过 100 万次执行/秒/内核,这不错吧?
在不到1秒的时间内,我们得到了 @mehqq_(CVE-2018-6789)发现的堆溢出:
注意:本周早些时候,mehqq_ 通知我,这是 OOB 读取的错误。我将尽快发布更新,以显示实际的错误。我对混乱很不好。
最后的工作
尽管此技术不像 AFL
那样点击即播放,因为它需要更多的工作,但它提供了不可忽视的优点:
- 出色的可靠性,易于 fuzzing 网络服务 → 着重于解析功能(无网络堆栈可处理等)。非常适合可以专注于特定点(数据包解析,消息处理等)
- 优秀的表现:无需生成整个二进制文件
- 实际上不需要源代码,我们可以在黑盒二进制文件上使用 LibFuzzer
- 较低的硬件要求甚至在硬件较弱的情况下也可以以很高的速率进行模糊测试(并将您的 RaspberryPis 转换为模糊测试群集😎)
但是,没有什么是完美的,显然也有缺点:
- 需要编写几乎每个模糊器的代码(因此仅适用于 C/C++ 编码人员)
- 您可能需要考虑的特殊情况(提防内存泄漏!)
- 我们必须确定函数原型。当打开源代码(FOSS 项目)时,这很容易,但是黑盒二进制文件可能需要先进行一些反转。Binary Ninja Commercial License 之类的工具也可能对自动执行此任务有很大帮助。
总而言之,这是一个非常巧妙的方法,可通过 2 个出色的工具来实现。我希望 LIEF 不断发展,为我们带来更多类似的东西!
感谢阅读😁!