创建 Nix 工作流来对 netconsd 进行模糊测试
最近,每当我睡不着的时候,就会找一些软件来做模糊测试(fuzzing)。
本周早些时候,我想起了 Fady Othman(法迪·奥斯曼)的文章 “Meta Bug Bounty — Fuzzing ’netconsd’ for fun and profit.”。这是一篇关于如何彻底地对代码进行模糊测试的优秀教程。
和大多数关于模糊测试的博客文章一样,我发现这项工作不太容易复现,因为读者需要自己弄清楚如何复现作者的环境和工具链。
我打算尝试用 Nix 制作一个易于复现的 Othman 模糊测试器版本,就像我之前对 xpdf 所做的那样。我那篇关于 xpdf 的文章要详尽和完善得多,而这些只是给好奇者的一些快速笔记。
fuzz-netconsd
这是我花了几个小时为 netconsd 创建的模糊测试工作流:
如果你安装了 git 和 Nix,就可以用以下命令运行我的工作流:
nix run gitlab:mtlynch/fuzz-netconsd
这是我写的第一个模糊测试器:模糊测试工具不是通过 shell 执行编译好的二进制文件,而是直接通过函数调用进入被测代码。在 honggfuzz 和 AFL++ 中,这被称为“persistent mode(持久模式)”,我猜是因为模糊测试器可以让一个进程持续存在,而不是每次模糊测试运行都生成一个新进程。
通过函数调用进行模糊测试很有意思,因为即使目标代码根本没有发布共享库,你也可以这样做。我是在工作流的 installPhase 中做到这一点的:把包含我想调用的代码的 .o 文件以及对应的 .h 头文件复制到该步骤的 Nix 输出目录中。
拿到所需的头文件和目标文件后,我写了一个简单的测试 harness(测试驱动程序),它只包含刚好够调用我要模糊测试的函数 ncrx_process 的代码:
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include "ncrx.h"
int LLVMFuzzerTestOneInput(const uint8_t *buf, size_t len) {
// Convert the random bytes of the payload to a null-terminated string.
char *payload = (char *)malloc(len + 1);
if (!payload) return 0;
memcpy(payload, buf, len);
payload[len] = '\0';
struct timespec ts;
if (clock_gettime(CLOCK_MONOTONIC, &ts)) {
perror("clock_gettime");
return 1;
}
uint64_t now = ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
struct ncrx *ncrx = ncrx_create(NULL);
// Call the function that we're fuzzing.
ncrx_process(payload, now, 0, ncrx);
// Clean up the resources we allocated.
ncrx_destroy(ncrx);
free(payload);
return 0;
}
我发现持久模式下的模糊测试比预想的要容易。我甚至不需要深入了解 netconsd。我至今仍不完全清楚它到底是做什么的,但我已经理解到足以在模糊测试器下调用它的一个解析函数。
我可能不会发现什么问题
Othman 报告说他的模糊测试工作达到了 100% 的覆盖率,这是极其彻底的。
我查看了 netconsd 的提交历史,自 Othman 的工作之后几乎没有任何活动。他没有在文章中明确说明,但我怀疑下面这个提交就是修复他所发现的漏洞的改动:
我怀疑在这个时间点上通过模糊测试能发现的东西不多。在我的模糊测试器运行了 12 个小时没有出现任何崩溃之后,我就把它停掉了。
未完成的工作
- Othman 使用 klee 工具为模糊测试语料库生成了边界情况输入。我没有实现这一点,但它可以很好地融入这个工作流,因为 klee 已经被 Nix 打包了。
- 我用的是 honggfuzz 而不是 Othman 所用的 AFL++。我更喜欢 honggfuzz 的简洁性,但 Othman 对 AFL++ 的各种参数足够熟悉,能够获得更高的覆盖率。
- 如果你想玩玩那个版本,可以看看我第一版草稿实现,它用的是 AFL++。
随机一篇博客