使用AFL对Linux内核Fuzzing的总结

模糊测试是一种很好的漏洞挖掘技术,Fuzzer将半随机输入喂到到测试程序,目的是找到触发错误的输入。模糊测试在查找C或C ++程序中的内存破坏漏洞时特别有用。

通常情况下,建议选择一个众所周知但很少探索的库,这个库在解析时很重要。历史上,像libjpeg,libpng和libyaml这样的东西都是完美的目标。如今找到一个好目标更难 - 一切似乎都已经被模糊化了。这是好事!我猜软件越来越好了!我没有选择用户空间目标,而是选择了Linux内核netlink机器。

Netlink是一个Linux内核工具,它用于配置网络接口,IP地址,路由表等。这是一个很好的fuzzing 目标:它是内核的一个小模块,并且生成畸形有效消息相对比较容易。最重要的是,我们可以在此过程中学到很多关于Linux内核的知识。

在这篇文章中,我将使用AFL模糊器,将netlink shim程序与自定义Linux内核相对应,所有这些都在KVM虚拟机中运行。

技术参考

我们将要使用的技术被称为“覆盖引导模糊测试”。有很多以前的文献:

很多人过去都在Fuzzing Linux内核:

我们将使用AFL,可能是大家最喜欢的模糊器。AFL由MichałZalewski撰写。它以其易用性,速度和非常好的变异逻辑而闻名,这是开始模糊测试之旅的完美选择!

如果您想了解有关AFL的更多信息,请参阅几个文件:

覆盖引导的模糊测试

覆盖引导的模糊测试基于反馈回路的原理:

  • 模糊测试选择最有希望的测试用例
  • 模糊测试将测试变为大量新的测试用例
  • 目标代码运行变异的测试用例,并报告代码覆盖率
  • 模糊器根据报告的覆盖范围计算得分,并使用它来确定有效的变异测试的优先级并删除冗余的测试

例如,假设输入测试是“hello”。Fuzzer可能会将其变为多种测试,例如:“hEllo”(位翻转),“hXello”(字节插入),“hllo”(字节删除)。如果这些测试中的任何一个将产生有趣的代码覆盖,那么它将被优先化并用作下一次测试的基础。

有关如何完成突变以及如何有效地比较数千个程序运行的代码覆盖率报告的细节是模糊测试的秘诀,阅读AFL的技术白皮书,可以了解更多细节。

通常,在使用AFL时,我们需要检测目标代码,以便以AFL兼容的方式报告覆盖范围。但我们想要Fuzzing 内核!我们不能只用“afl-gcc”重新编译它!。我们将准备一个二进制文件,让AFL认为它是用它的工具编译的。这个二进制文件将报告从内核中提取的代码覆盖率。

内核代码覆盖率

内核至少有两个内置的覆盖机制–GCOV和KCOV:

KCOV的设计考虑了模糊测试,因此我们将使用它。

使用KCOV非常简单。我们必须使用正确的设置编译Linux内核。首先,启用KCOV内核配置选项:

cd linux
./scripts/config \
    -e KCOV \
    -d KCOV_INSTRUMENT_ALL

KCOV能够记录整个内核的代码覆盖率。可以使用KCOV_INSTRUMENT_ALL选项进行设置。有个缺点是,它会减慢我们不想分析的内核部分,并且会在Fuzzing 中引入噪声(降低“稳定性”)。对于初学者,让我们禁用KCOV_INSTRUMENT_ALL并有选择地在实际想要分析的代码上启用KCOV。

我们专注于Fuzzing netlink,所以在整个“net”目录树上启用KCOV:

find net -name Makefile | xargs -L1 -I {} bash -c 'echo "KCOV_INSTRUMENT := y" >> {}'

在一个理想环境中,我们只能为真正感兴趣的几个文件启用KCOV。但是netlink处理遍及网络堆栈代码,现在没有时间进行微调。

有了KCOV,将增加报告内存损坏错误的可能性。最重要的是KASAN,使用该集合,可以编译我们的KCOV和KASAN启用的内核。

我们将以kvm运行内核,所以需要切换一下:

./scripts/config \
    -e VIRTIO -e VIRTIO_PCI -e NET_9P -e NET_9P_VIRTIO -e 9P_FS \
    -e VIRTIO_NET -e VIRTIO_CONSOLE  -e DEVTMPFS ...

如何使用KCOV

KCOV非常易于使用。首先,请注意代码覆盖率记录在每个进程的数据结构中。这意味着您必须在用户空间进程中启用和禁用KCOV,并且无法记录非任务事项的覆盖范围,例如中断处理。这对我们的需求来说完全没问题。

KCOV将数据报告给环形缓冲区。设置非常简单,请参阅我们的代码。然后你可以使用一个简单的ioctl启用和禁用它:

ioctl(kcov_fd, KCOV_ENABLE, KCOV_TRACE_PC);
/* profiled code */
ioctl(kcov_fd, KCOV_DISABLE, 0);

在此序列之后,环形缓冲区包含启用KCOV的内核代码的所有基本块的%rip值列表。要读取缓冲区,请运行:

n = __atomic_load_n(&kcov_ring[0], __ATOMIC_RELAXED);
for (i = 0; i < n; i++) {
    printf("0x%lx\n", kcov_ring[i + 1]);
}

使用像addr2line这样的工具可以将%rip解析为特定的代码行。我们不需要它 - 原始的%rip值对我们来说已经足够了。

将KCOV喂入AFL

我们旅程的下一步是学习如何欺骗AFL。请记住,AFL需要一个特制的可执行文件,但我们想要提供内核代码覆盖率。首先,我们需要了解AFL的工作原理。

AFL设置一个64K 8位数字的数组。该存储器区域称为“shared_mem”或“trace_bits”,并与跟踪的程序共享。数组中的每个字节都可以被认为是检测代码中特定(branch_src,branch_dst)对的命中计数器。

重要的是要注意AFL更喜欢随机分支标签,而不是重用%rip值来识别基本块。这是为了增加熵 - 我们希望数组中的命中计数器均匀分布。AFL使用的算法是:

cur_location = <COMPILE_TIME_RANDOM>;
shared_mem[cur_location ^ prev_location]++; 
prev_location = cur_location >> 1;

在我们使用KCOV的情况下,我们没有每个分支的编译时随机值。相反,我们将使用哈希函数从KCOV记录的%rip生成统一的16位数。这是如何将KCOV报告提供给AFL“shared_mem”数组:

n = __atomic_load_n(&kcov_ring[0], __ATOMIC_RELAXED);
uint16_t prev_location = 0;
for (i = 0; i < n; i++) {
        uint16_t cur_location = hash_function(kcov_ring[i + 1]);
        shared_mem[cur_location ^ prev_location]++;
        prev_location = cur_location >> 1;
}

从AFL读取测试数据

最后,我们需要实际编写核心netlink接口的测试代码!首先,我们需要从AFL读取输入数据。默认情况下,AFL将测试用例发送到stdin:

/* read AFL test data */
char buf[512*1024];
int buf_len = read(0, buf, sizeof(buf));

然后我们需要将此缓冲区发送到netlink套接字。但我们对netlink的工作原理一无所知!好吧,让我们使用前5个字节的输入作为netlink协议和组ID字段。这将允许AFL找出并猜测这些字段的正确值。代码测试netlink(简化):

netlink_fd = socket(AF_NETLINK, SOCK_RAW | SOCK_NONBLOCK, buf[0]);

struct sockaddr_nl sa = {
        .nl_family = AF_NETLINK,
        .nl_groups = (buf[1] <<24) | (buf[2]<<16) | (buf[3]<<8) | buf[4],
};

bind(netlink_fd, (struct sockaddr *) &sa, sizeof(sa));

struct iovec iov = { &buf[5], buf_len - 5 };
struct sockaddr_nl sax = {
      .nl_family = AF_NETLINK,
};

struct msghdr msg = { &sax, sizeof(sax), &iov, 1, NULL, 0, 0 };
r = sendmsg(netlink_fd, &msg, 0);
if (r != -1) {
      /* sendmsg succeeded! great I guess... */
}

基本上就是这样!为了速度,我们将它包装在一个模仿AFL“fork服务器”逻辑的短循环中。我将跳过此处的解释,请参阅我们的代码了解详细信息。我们的AFL-to-KCOV垫片的结果代码如下所示:

forksrv_welcome();
while(1) {
    forksrv_cycle();
    test_data = afl_read_input();
    kcov_enable();
    /* netlink magic */
    kcov_disable();
    /* fill in shared_map with tuples recorded by kcov */
    if (new_crash_in_dmesg) {
         forksrv_status(1);
    } else {
         forksrv_status(0);
    }
}

查看完整的源代码

如何运行自定义内核

我们遗漏了一个重要的部分 - 如何实际运行我们构建的自定义内核。有三种选择:

“native”:您可以在服务器上完全启动构建的内核并在本机模糊它。这是最快的技术,但很有问题。如果模糊测试成功找到错误,您将崩溃机器,可能会丢失测试数据。应该避免切割我们坐的树枝。

“uml”:我们可以将内核配置为以用户模式Linux运行。运行UML内核不需要任何权限。内核只运行用户空间进程。UML非常酷,但遗憾的是,它不支持KASAN,因此减少了查找内存损坏错误的可能性。最后,UML是一个非常神奇的特殊环境 - 在UML中发现的错误可能与真实环境无关。有趣的是,Android network_tests框架使用UML 。

“kvm”:我们可以使用kvm在虚拟化环境中运行我们的自定义内核。这就是我们要做的。

在KVM环境中运行自定义内核的最简单方法之一是使用“virtme”脚本。有了它们,我们可以避免创建专用的磁盘映像或分区,只需共享主机文件系统。这就是我们运行代码的方式:

virtme-run \
    --kimg bzImage \
    --rw --pwd --memory 512M \
    --script-sh "" 

但坚持下去。我们忘记了为我们的模糊器准备输入语料库数据!

构建输入语料库

每个模糊器都需要精心设计的测试用例作为输入,以引导第一个突变。测试用例应该简短,并尽可能覆盖大部分代码。可悲的是 - 我对netlink一无所知。我们怎么不准备输入语料库…

相反,我们可以要求AFL“弄清楚”哪些输入有意义。这就是Michał在2014年用JPEG制作的,并且对他有用。考虑到这一点,这是我们的输入语料库:

mkdir inp
echo "hello world" > inp/01.txt

有关如何编译和运行整个事情的说明都在我们的github上的README.md中。归结为:

virtme-run \
    --kimg bzImage \
    --rw --pwd --memory 512M \
    --script-sh "./afl-fuzz -i inp -o out -- fuzznetlink" 

通过此运行,您将看到熟悉的AFL状态截图:

研究总结

而已。现在你有了一个自定义的强化内核,运行一个基本的覆盖引导模糊器。所有KVM内部。

值得努力吗?即使有这个基本的模糊器,也没有输入语料库,一两天后,模糊器发现了一个有趣的代码路径:NEIGH:BUG,双定时器添加,状态为8。使用更专业的模糊器,一些改进“稳定性”度量和一个体面的输入语料库,我们可以期待更好的结果。

如果您想了解更多关于netlink套接字实际执行的内容,请参阅我的同事Jakub Sitnicki 在Linux中的多路径路由的博客文章- 第1部分。然后在Rami Rosen的Linux内核网络书中有一篇很好的章节。

在这篇博文中我们没有提到:

  • AFL shared_memory设置的详细信息
  • 执行AFL持久模式
  • 如何创建一个网络命名空间来隔离怪异的netlink命令的效果,并提高AFL得分的“稳定性”
  • 关于如何读取dmesg(/ dev / kmsg)以查找内核崩溃的技巧
  • 想要在KVM之外运行AFL,以获得速度和稳定性 - 目前在发现崩溃后测试不稳定

但是我们实现了我们的目标 - 我们针对内核建立了一个基本但仍然有用的模糊器。最重要的是:可以重复使用相同的机制来模糊Linux子系统的其他部分 - 从文件系统到bpf验证程序。

我还学到了一个艰难的教训:调整模糊器是一项全职工作。正确的模糊测试绝对不是像启动它并无所事事地等待崩溃一样简单。总有一些东西需要改进,调整和重新实现。Mateusz Jurczyk在上述演讲开头的一句话引起了我的共鸣:

“模糊很容易学,但很难掌握。”

快乐虫狩猎!


   转载规则


《使用AFL对Linux内核Fuzzing的总结》 0xbird 采用 知识共享署名 4.0 国际许可协议 进行许可。
 上一篇
Javascript基础学习总结 Javascript基础学习总结
简介JavaScript 是一种脚本语言,学一种编程语言,首先就要从这种语言的基础语法入手。本节我们就将对 JavaScript 的基础语法进行学习。 知识点 JavaScript 是什么 变量 数字与运算符 数组  null &
2019-08-27
本篇 
使用AFL对Linux内核Fuzzing的总结 使用AFL对Linux内核Fuzzing的总结
模糊测试是一种很好的漏洞挖掘技术,Fuzzer将半随机输入喂到到测试程序,目的是找到触发错误的输入。模糊测试在查找C或C ++程序中的内存破坏漏洞时特别有用。 通常情况下,建议选择一个众所周知但很少探索的库,这个库在解析时很重要。历史上,像
  目录