blog搬运:A gentle introduction to Linux Kernel fuzzing

原出处:https://blog.cloudflare.com/a-gentle-introduction-to-linux-kernel-fuzzing/

作者:Marek Majkowski
公司:cloudflare

前置条件

AFL为主的覆盖反馈引导的模糊测试相关的概念

  • 如何使用
  • 大概了解AFL工作原理,白皮书

文章简介

这篇文章主要介绍如何使用AFL对Linux内核中的Netlink接口进行模糊测试。不懂Netlink的可以先直接理解成这是一类系统调用,主要用于同一台主机上的过程间通信(包括用户态之间,用户态内核态之间的程序),阉割版的socket通信(不能用于不同主机),用于替代复杂且不灵活的ioctl。主流的Linux内核模糊测试都是针对系统调用这一用户态与内核态交互接口,一种思路是关注系统调用接口,另一种思路是关注系统调用的参数。前者可能更广,后者可能更专。像syzkaller本身是关注前者的,制定了系统调用模板和系统调用描述去细化系统调用的参数返回类型属性,更加关注合法化。后者主要用于测某些子系统,先规定一个(批)系统调用并组合好,变异其中的参数,毕竟参数才是最本质的外部输入,因此都是针对那些复杂的系统调用,如ioctl或者含有buf参数的系统调用等。本文是后者。

要达到上述目的,有几个难点:

  1. 怎么获取覆盖信息?
    • 原始的AFL依赖于编译时代码插桩(afl-gcc等)获取覆盖,但是这个没法直接用于编译内核。
    • Linux内核开发者为此提供了两种内置的覆盖机制:GCOVKCOV,其中KCOV专门用于模糊测试。
    • 所以剩下的问题是,怎么将AFL和KCOV联动?
  2. 怎么生成测试输入?
    • 首先测试输入是什么?发送给Netlink的网络数据包!
    • 其次怎么生成?格式如何?由AFL自己去理解(白皮书有AFL如何处理结构化输入的描述)
    • 所以模糊测试的输入是一个程序,这个程序由一系列系统调用组合,这个组合实际上是socket编程的标准流程而已(socket->bind->sendmsg),只不过发送的数据有AFL模糊器产生,然后这个程序丢给内核执行。
  3. 怎么运行被测对象?
    • 这个问题是内核模糊测试的一般问题。
    • 物理机直接运行是最简单的办法,但是缺点多,如效率低(一次只能一台机器,挂了重启开销大),如何复现(挂了就挂了,不一定会记录什么信息)
    • 虚拟化技术!尤其是KVM。具体技术不限,参考syzkaller使用的虚拟技术

关于怎么获取覆盖信息

AFL插桩后每个基本块会有一个16位的随机id,而KCOV所提供的覆盖信息是基本块入口指令的PC值,一般都是32位或64位,两者本质上来说都是数字罢了,因此直接复用AFL那一套理论上不是问题。

KCOV的使用在内核文档也有,主要是通过虚拟文件系统/sys/kernel/debug/kcov交互。

kcov_fd = open("/sys/kernel/debug/kcov", O_RDWR);
ioctl(kcov_fd, KCOV_ENABLE, KCOV_TRACE_PC);
/* profiled code */
ioctl(kcov_fd, KCOV_DISABLE, 0);
close(kcov_fd)

在编译内核时需要开启相关配置CONFIG_KCOV。想要复用AFL只需要设计一个hash函数将KCOV的值变成16位的“随机id”即可。

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]); // hash_function: PC -> 16-bit rand num
        shared_mem[cur_location ^ prev_location]++;
        prev_location = cur_location >> 1;
}

关于怎么生成测试输入

AFL通常需要提供一批种子作为输入,而这些种子一般以文件的形式输入,因此我们随机生成一些文本文件即可,然后让AFL读取到buf中。下面伪代码假设输入用例来自标准输入0。这段代码实际上就是上面提到的profiled code。

/* read AFL test data */
char buf[512*1024];
int buf_len = read(0, buf, sizeof(buf));
/* The code testing netlink (simplified) */
netlink_fd = socket(AF_NETLINK, SOCK_RAW | SOCK_NONBLOCK, buf[0]); // socket
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)); // bind
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); // sendmsg
if (r != -1) {
      /* sendmsg succeeded! great I guess... */
}

和上面AFL-KCOV合并后,整个AFL的循环逻辑如下:

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

关于怎么运行被测对象

本文使用virtme-run轻量级的虚拟工具,guest共享host的文件系统。afl实际上运行在guest中,所以做个wrapper丢进去guest即可,virtme-run做这个就相对容易。

还有一点是怎么获取运行信息?其实是通过读取被测内核的/dev/kmsg,但是本文没有详细介绍。

全部代码在cloudflare-blog的GitHub上。

posted @ 2021-04-18 23:55  辣条小布丁  阅读(300)  评论(0编辑  收藏  举报