coverage-guided fuzzing是一种强大的模糊测试技术,因为代码覆盖率(Code Coverage)是反映测试用例对被测软件覆盖程序的重要指标,自动化程序将半随机输入注入到测试程序中,这样做的目的是找到触发漏洞的输入,模糊测试在查找C或c++程序中的内存损坏漏洞时特别有用。通常情况下,建议选择一个非常熟悉但很少涉及的、大量解析的库。依据经验看,libjpeg、libpng和libyaml都是完美的攻击目标。不过现在很难找到一个好的目标,不过从另外一个角度来说,这也说明软件越来越安全了!不过我还是决定尝试一下Linux内核的netlink机制。
Netlink是linux提供的用于内核和用户态进程之间的通信方式。但是注意虽然Netlink主要用于用户空间和内核空间的通信,但是也能用于用户空间的两个进程通信。只是进程间通信有其他很多方式,一般不用Netlink。除非需要用到Netlink的广播特性时。
从原理上来说,Netlink是供“ ss”,“ ip”,“ netstat”之类的工具使用的内部Linux工具。它用于低层网络任务——配置网络接口、IP地址、路由表等。所以,这是一个很好的渗透测试目标,因为它是内核中一个不为人知的部分,而且自动编写有效的消息相对容易。最重要的是,在这个过程中我们可以学到很多关于Linux内部的东西。不过,netlink中的漏洞不会对安全造成影响,因为netlink套接字通常需要特权访问。
在这篇文章中,我们将运行AFL代码覆盖率工具,在一个定制的Linux内核上驱动我们的netlink shim程序,所有这些都在KVM虚拟化中运行。
通常,在使用AFL时,我们需要检测目标代码,以便以AFL兼容的方式报告代码覆盖率。但是我们想要模糊内核!我们不能只是用“afl-gcc(AFL的编译器)”重新编译它。相反,我们将使用一个技巧。我们将准备一个二进制文件,它会欺骗AFL,让它认为是用它的工具编译的。这个二进制文件将报告从内核中提取的代码覆盖率。
内核代码的代码覆盖率
内核至少有两个内置的代码覆盖率机制- GCOV和KCOV:
1. 在Linux内核中使用gcov;
KCOV在设计时考虑到了模糊性测试,因此我们将使用它。
Kcov是一个代码覆盖测试工具,最初基于Bcov,它可在FreeBSD、Linux、OSX系统中使用,支持的语言包括编译语言(compiled languages)、Python和Bash。与Bcov一样,Kcov对编译的程序使用DWARF调试信息,以便无需特殊编译器开关即可收集覆盖信息。
KCOV使用起来非常简单,我们必须使用正确的设置来编译Linux内核。首先,启用KCOV内核配置选项:
cd linux ./scripts/config \ -e KCOV \ -d KCOV_INSTRUMENT_ALL
KCOV能够记录整个内核的代码覆盖率,可以使用KCOV_INSTRUMENT_ALL选项进行设置。但是,这样做也有缺点,会减慢我们不想分析的内核部分,并且会在我们的测量中引入噪音降低测量的准确性。首先,让我们禁用KCOV_INSTRUMENT_ALL并有选择地在我们实际要分析的代码上启用KCOV。在这篇文章中,我们只关注netlink机制,因此我们会在整个“ net”目录树上启用KCOV:
find net -name Makefile | xargs -L1 -I {} bash -c 'echo "KCOV_INSTRUMENT := y" >> {}'
在理想的情况下,我们只对我们真正感兴趣的几个文件启用KCOV。但是netlink处理遍及整个网络堆栈代码,因此我们在这篇文章中没有时间对其进行微调。
使用KCOV后,就有必要添加“内核黑客”选项,以增加报告内存损坏漏洞的可能性。关于Syzkaller建议的选项列表,请参阅自述,其中最重要的是KASAN。
有了这个设置,我们就可以编译启用KCOV和KASAN的内核。哦,还有一件事,我们将在kvm中运行内核。我们将使用“virtme”,所以我们需要一些切换:
./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套接字。但是我们对netlink的工作原理一无所知!好的,让我们使用前5个字节的输入作为netlink协议和组id字段。这将允许AFL计算并猜测这些字段的正确值。代码测试netlink(简化):
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 shim的结果代码如下:
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); } }
点此,参见完整的源代码。
如何运行自定义内核
现在来说说,如何实际运行我们构建的自定义内核,目前共有三个选项:
“本地”选项:你可以完全在你的服务器上启动构建的内核,并在本地对其进行模糊处理。这是最快的技术,但是问题很多。如果模糊测试成功地找到了一个漏洞,你的设备很可能会崩溃,从而丢失测试数据。
“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 "<what to run inside kvm>"
不过关键的一步似乎忘了,就是为我们的模糊器准备输入语料库数据!
建立输入语料库
每个模糊测试都采用了一个精心设计的测试用例作为输入,以引导第一个突变。测试用例应该是简短的,并覆盖尽可能多的代码。遗憾的是,我对netlink一无所知。
于是,我就让AFL“找出”哪些输入是有意义的,以下就是我们的输入语料库:
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状态屏幕:
现在,你就有了一个自定义的强化内核,该内核会运行一个以代码覆盖率为测试标识的模糊测试进程。
这样的努力值得吗?即使有了这个基本的模糊测试器,也没有输入语料库。不在两三天后,该模糊器还是找到了一条有趣的代码路径:NEIGH:BUG,双计时器加法,状态为8。使用更专业的模糊器,可以进行一些改善“稳定性”的工作指标和像样的输入语料库,我们可以期待更好的测试结果。
如果你想了解更多有关netlink套接字实际功能的信息,请参阅此文。
不过在这篇文章中,我们没有提到:
1. 详细的AFL shared_memory设置;
2. AFL的持久模式是如何实现的;
3. 如何创建一个网络名称空间来隔离奇怪的netlink命令的影响,并提高AFL的“稳定性”;
4. 如何读取dmesg (/dev/kmsg) 来查找内核崩溃;
5. 在KVM之外运行AFL的想法,为了速度和稳定性,目前的测试在发现崩溃后还不稳定。