03.03 基于AFL对Linux内核模糊测试的过程详述

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;

2. KCOV:模糊的代码覆盖率。

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

Kcov是一个代码覆盖测试工具,最初基于Bcov,它可在FreeBSD、Linux、OSX系统中使用,支持的语言包括编译语言(compiled languages)、Python和Bash。与Bcov一样,Kcov对编译的程序使用DWARF调试信息,以便无需特殊编译器开关即可收集覆盖信息。

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

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

KCOV能够记录整个内核的代码覆盖率,可以使用KCOV_INSTRUMENT_ALL选项进行设置。但是,这样做也有缺点,会减慢我们不想分析的内核部分,并且会在我们的测量中引入噪音降低测量的准确性。首先,让我们禁用KCOV_INSTRUMENT_ALL并有选择地在我们实际要分析的代码上启用KCOV。在这篇文章中,我们只关注netlink机制,因此我们会在整个“ net”目录树上启用KCOV:

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

在理想的情况下,我们只对我们真正感兴趣的几个文件启用KCOV。但是netlink处理遍及整个网络堆栈代码,因此我们在这篇文章中没有时间对其进行微调。

使用KCOV后,就有必要添加“内核黑客”选项,以增加报告内存损坏漏洞的可能性。关于Syzkaller建议的选项列表,请参阅自述,其中最重要的是KASAN。

有了这个设置,我们就可以编译启用KCOV和KASAN的内核。哦,还有一件事,我们将在kvm中运行内核。我们将使用“virtme”,所以我们需要一些切换:

<code>./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 .../<code>

如何使用KCOV

KCOV非常容易使用,首先,请注意,代码覆盖率记录在每个进程的数据结构中。这意味着你必须在用户空间进程中启用和禁用KCOV,并且不可能记录诸如中断处理之类的非任务内容的覆盖范围,这完全符合我们的需要。

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

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

在这个序列之后,循环缓冲区包含所有启用kcov的内核代码的基本块的%rip值的列表。要读取缓冲区,只需运行以下代码即可:

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

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

将KCOV注入AFL

下一步是学习如何欺骗AFL,请记住,AFL需要一个专门设计的可执行文件,但是我们希望提供内核代码覆盖率。首先,我们需要了解AFL是如何工作的。

AFL设置一个64K的8位数字数组,这个内存区域称为“shared_mem”或“trace_bits”,与跟踪程序共享。可以将数组中的每个字节看作插装代码中特定(branch_src、branch_dst)对的命中计数器。

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

<code>cur_location = <compile>;
shared_mem[cur_location ^ prev_location]++; 
prev_location = cur_location >> 1;/<compile>/<code>

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

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

从AFL读取测试数据

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

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

Netlink的模糊测试

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

<code>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... */
}/<code>

基本上就是这样!为了提高速度,我们将其包装在一个简短的循环中,该循环模仿AFL的“fork服务器”逻辑。我将在此处跳过说明,有关详细信息,请参见我们的代码。我们的AFL-to-KCOV shim的结果代码如下:

<code>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);
    }
}/<code>

点此,参见完整的源代码。

如何运行自定义内核

现在来说说,如何实际运行我们构建的自定义内核,目前共有三个选项:

“本地”选项:你可以完全在你的服务器上启动构建的内核,并在本地对其进行模糊处理。这是最快的技术,但是问题很多。如果模糊测试成功地找到了一个漏洞,你的设备很可能会崩溃,从而丢失测试数据。

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

“kvm”:我们可以使用kvm在虚拟环境中运行自定义内核。

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

<code>virtme-run \\
    --kimg bzImage \\
    --rw --pwd --memory 512M \\
    --script-sh "<what>"/<what>/<code>

不过关键的一步似乎忘了,就是为我们的模糊器准备输入语料库数据!

建立输入语料库

每个模糊测试都采用了一个精心设计的测试用例作为输入,以引导第一个突变。测试用例应该是简短的,并覆盖尽可能多的代码。遗憾的是,我对netlink一无所知。

于是,我就让AFL“找出”哪些输入是有意义的,以下就是我们的输入语料库:

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

编译和运行整个程序的过程,请参见github上的README.md ,简单来说,如下所示:

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

运行此命令,你将看到熟悉的AFL状态屏幕:

基于AFL对Linux内核模糊测试的过程详述

现在,你就有了一个自定义的强化内核,该内核会运行一个以代码覆盖率为测试标识的模糊测试进程。

这样的努力值得吗?即使有了这个基本的模糊测试器,也没有输入语料库。不在两三天后,该模糊器还是找到了一条有趣的代码路径:NEIGH:BUG,双计时器加法,状态为8。使用更专业的模糊器,可以进行一些改善“稳定性”的工作指标和像样的输入语料库,我们可以期待更好的测试结果。


分享到:


相關文章: