USBFuzz - 论文学习与代码复现

论文阅读

USB用于主机与各种外部设备的连接与通信。攻击者可以利用该接口对操作系统内核、设备驱动进行恶意攻击。模糊测试(fuzzing)作为一种广泛应用的自动化测试技术,可以用于发现软件中的漏洞。然而,对USB设备驱动进行模糊测试需要克服众多挑战:跨越软硬件障碍、输入设备数据到驱动程序等。

该论文提出的 USBFuzz 是一种可移植、灵活且模块化的 USB 驱动模糊测试框架,其核心是使用软件仿真 USB 设备为驱动程序提供随机设备数据。作者对 Linux 内核中的大量且广泛的 USB 驱动程序应用了 (i) 覆盖引导的模糊测试;(ii) 在 FreeBSD、MacOS 和 Windows 中通过 Linux 输入进行的dump fuzzing;以及 (iii) 针对 USB 网络摄像头驱动程序的重点模糊测试。

最后,作者使用 USBFuzz 共发现了 26 个新漏洞,包括 16 个在不同 Linux 子系统(USB 核心、USB 声音和网络)中具有高安全影响的内存漏洞,FreeBSD 中发现了一个漏洞,MacOS 中发现了三个漏洞,Windows 8 和 Windows 10 中发现了四个漏洞,以及在 Linux USB 主控制器驱动程序中发现了一个漏洞,USB 摄像头驱动程序中又发现了一个漏洞。在 Linux 漏洞中,修复并上游了 11 个漏洞,并获得了 10 个 CVE。

引言

USB 的普遍性和外部可访问性导致了一个较大的攻击面,可以从不同类别进行探索:

  • USB 设备的权限问题(例如,autorun攻击,允许 USB 存储设备在插入时启动程序)
  • 利用物理设计缺陷攻击
  • 利用宿主操作系统中的软件漏洞

针对广泛权限的攻击可以通过定制防御措施重新配置操作系统来解决,例如,禁用autorun、使用各类过滤器/USB保护工具,而硬件攻击可以通过改进接口设计来保护。改论文专注于宿主操作系统中的软件漏洞,因为这些问题难以发现且具有高安全影响。

设备驱动程序从连接的设备中获取输入。未能处理意外的输入会导致内存错误,如缓冲区溢出、使用后释放或双重释放错误(buffer-overflows, use-after-free, double free errors)——这可能会造成灾难性的后果。由于设备驱动程序直接在内核或特权进程中运行,驱动程序中的错误具有很高的安全风险。

模糊测试是一种自动化的软件测试技术,广泛用于通过向软件输入随机生成的输入来发现漏洞。覆盖引导的模糊测试是最先进的模糊测试技术,能够有效地发现用户空间程序中的漏洞。近年来,开发了几种内核模糊测试工具(如 syzkaller、TriforceAFL、trinity、DIFUZE、kAFL 和 RAZZER),用于模糊测试系统调用参数,并在流行的操作系统内核中发现了许多漏洞。

对设备驱动进行模糊测试具有挑战性,因为从设备提供随机输入是困难的。专用的可编程硬件设备价格昂贵,且不具备扩展性,一个设备只能用于对一个目标进行模糊测试。更重要的是,由于每次测试所需的物理操作(连接和断开设备),在真实硬件上自动化模糊测试非常具有挑战性。

一些解决方案对内核进行了调整。例如,内核模糊测试工具 syzkaller 的 usb-fuzzer,通过扩展的系统调用向 USB 堆栈注入随机数据。PeriScope 在 DMA 和 MMIO 接口处注入随机数据。这些方法不具备可移植性,紧密耦合于特定的操作系统和内核版本,并且需要对硬件规范及其在内核中的实现有深入的理解。此外,由于它们在 IO 堆栈的某一层注入随机数据,某些代码路径无法被测试,从而错过了未测试代码中的漏洞。vUSBf 通过重新利用网络 USB 接口来注入随机数据到驱动程序,从而减轻了对理解硬件规范的要求。然而,vUSBf 与内核的耦合度过低,只支持dump fuzzing,而不收集覆盖反馈。

本文提出的 USBFuzz,是一种廉价、可移植、灵活且模块化的 USB 模糊测试框架。USBFuzz 的核心是使用仿真 USB 设备为虚拟化内核提供模糊输入。在每次迭代中,模糊测试工具虚拟地连接到目标系统的仿真 USB 设备执行测试,当驱动程序进行 IO 操作时,它将模糊测试生成的输入转发给正在测试的驱动程序。虚拟化内核中的一个辅助设备使得外部模糊测试工具(fuzzer)高效地与模糊测试目标(target)同步覆盖映射(coverage maps)。

背景与相关工作

USB 架构

通用串行总线(USB)作为一种行业标准被引入,用于连接商品计算设备及其外部设备。自其诞生以来,已经实施了多个版本的 USB 标准(1.x、2.0、3.x),带宽逐步增加,以适应更广泛的应用需求。目前有超过 10,000 种不同的 USB 设备。USB 采用主从架构,分为单一的主机端和多个设备端。设备端充当从属,实施其自身的功能。主机端充当主控,管理所有连接到它的设备。所有数据通信必须由主机发起,设备在未获得主机请求之前不得传输数据。

USB 架构最显著的特点是允许单一主机管理不同类型的设备。USB 标准定义了一组每个 USB 设备必须响应的请求,其中最重要的是设备描述符(包含供应商和产品 ID)和配置描述符(包含设备的功能定义和通信要求),以便主机端软件可以根据这些描述符使用不同的驱动程序来服务不同的设备。

主机端采用了分层架构,硬件上配备 USB 主控制器。主控制器提供物理接口,并支持设备访问的复用,主控制器驱动程序(USB Host Controller Driver)为访问物理接口提供了与硬件无关的抽象层(hardware-independent abstraction layer)。构建在主控制器驱动程序之上的 USB 核心层负责为连接的设备选择适当的驱动程序,并提供与 USB 设备通信的核心例程。各个 USB 设备的驱动程序首先根据提供的描述符初始化设备,然后与主机操作系统的其他子系统进行交互。用户空间程序使用各种内核子系统提供的 API 与 USB 设备进行通信。

USB 驱动程序由两个部分组成:(i) 探测例程(probe routine)用于初始化驱动程序,(ii) 功能例程(function routine)用于与其他子系统(例如声音、网络或存储)交互,并在设备拔出时注销驱动程序。现有的 USB 模糊测试工具主要集中在探测例程上,忽略了其他功能例程,因为探测函数在设备插入时会自动调用,而其他功能例程通常由用户空间程序驱动。

USB 接口模糊测试

目前已有多个针对 USB 接口的模糊测试工具。第一代 USB 模糊测试工具针对设备层。 vUSBf [49] 使用网络 USB 接口(usbredir),而 umap2 使用可编程硬件(FaceDancer)向主机 USB 堆栈注入随机硬件输入。虽然它们可以轻松移植到其他操作系统,但它们是简单的模糊测试工具,无法利用覆盖信息来指导输入变异,因此效率较低。

最近的 usb-fuzzer(是内核模糊测试工具 syzkaller 的扩展)使用自定义的软件实现主控制器,结合覆盖引导的模糊测试技术,将模糊输入注入到 Linux 内核的 IO 堆栈中。采用覆盖引导模糊测试技术已发现许多 Linux 内核 USB 堆栈中的漏洞。然而,usb-fuzzer 与 Linux 内核紧密耦合,难以移植到其他操作系统。

所有现有的 USB 模糊测试工具都仅专注于驱动程序的探测例程,而不支持对其余功能例程的模糊测试。现有 USB 模糊测试工具的现状激励我们构建一个灵活且模块化的 USB 模糊测试框架,该框架可移植到不同环境,并且易于定制,以应用覆盖引导的模糊测试或简单模糊测试(在尚不支持覆盖收集的内核中),并允许对广泛的探测例程进行模糊测试或专注于特定驱动程序的功能例程。

方法设计

模糊测试硬件输入

USBFuzz 的输入生成组件扩展了 AFL——最流行的覆盖引导模糊测试引擎之一。AFL 使用文件与目标程序通信,传递模糊测试生成的输入。模糊测试设备(fuzzing device)对设备驱动程序的读取请求做出响应,返回文件的内容。

模糊测试器——客户系统通信

在覆盖引导的模糊测试工具中,覆盖信息需要从客户系统传递到模糊测试器。为了避免重复的内存复制操作,USBFuzz 使用 QEMU 通信设备将 bitmap(模糊测试器中的一个内存区域)映射到客户系统。在客系统完全初始化后,位图被映射到目标内核的虚拟内存空间,目标内核中的插装代码可以将覆盖信息写入该内存区域。由于它也是模糊测试器中的共享内存区域,覆盖信息可以立即被其访问,避免了内存复制操作。

此外,模糊测试器在每次模糊测试迭代中需要与在客系统中运行的用户模式代理进行同步。为了避免高开销的进程间通信操作,通信设备中增加了一个控制通道,以促进用户模式代理与模糊测试组件之间的同步。

运行与监控

现有的内核模糊测试工具遵循一种迭代模式,对于每个测试,都会创建、执行和监控一个进程,然后模糊测试工具等待该进程的终止,以检测测试的结束。在 USBFuzz 中,由于测试是通过模糊测试设备进行的,在每次迭代中,测试以虚拟地连接(仿真)模糊测试设备到客系统开始。然后,内核接收到新 USB 设备的请求,这由内核设备管理的低端部分处理,该部分加载必要的驱动程序并初始化设备状态。然而,如果没有内核的支持,例如类似于退出系统调用的进程抽象,监控内核在与设备交互期间的执行状态(例如,是否触发了内核漏洞)将非常具有挑战性。

在 USBFuzz 中,采用经验性的方法通过检查内核的日志消息来监控测试的执行。例如,当 USB 设备连接到客系统时,如果内核能够处理来自设备的输入,内核会记录包含一组关键词的消息,指示与设备交互的成功或失败。否则,如果内核无法处理来自设备的输入,内核会冻结或表明触发了漏洞。USBFuzz 用户模式代理(user mode agent)组件通过扫描虚拟化目标系统中的内核日志来监控测试的执行状态,并将其状态与模糊测试组件同步,从而记录触发漏洞的输入并继续进行下一次迭代。

为了避免每次迭代都重复启动客系统,USBFuzz 提供了一种持久化的模糊测试技术,类似于其他内核模糊测试工具(如 syzkaller、TriforceAFL、trinity 或 kAFL),在这种技术中,正在运行的目标内核会被重用进行多次测试,直到其冻结,此时模糊测试工具会自动重启内核。

Linux 上覆盖率引导的模糊测试

到目前为止,USBFuzz 框架为在不同操作系统上模糊测试 USB 设备驱动程序提供了基本支持。然而,为了实现覆盖引导的模糊测试,系统必须收集执行覆盖信息。覆盖引导的模糊测试工具跟踪测试输入所覆盖的代码覆盖率(code coverage),并变异那些触发新代码路径的有趣输入。

在内核空间中,驱动代码的覆盖率收集具有挑战性。一方面,来自设备端的输入可能在不同的上下文中触发代码执行,因为驱动程序可能包含在中断和内核线程中运行的代码。另一方面,由于内核执行多任务,单个线程中执行的代码可能会被其他无关的代码执行(由定时器中断或任务调度触发)抢占。据我们所知,Linux 内核仅通过 kcov 支持静态插装的覆盖率收集。然而,kcov 的覆盖率收集仅限于单个进程,忽略了中断上下文和内核线程。为了扩展 kcov 的静态插装,USBFuzz 设计了一种类似 AFL 的边缘覆盖方案,以收集 Linux 内核中 USB 设备驱动程序的覆盖率。

为了在不同上下文中收集覆盖率,(i) 在每个代码执行线程(中断或内核线程)的上下文中保存之前执行的代码块,以便边缘转换不会被被抢占的代码执行流程混淆;(ii) 插桩限制在相关代码中:USB 核心、主控制器驱动程序和 USB 驱动程序。

实现与评估

USBFuzz 框架的实现扩展了多个开源组件,包括 QEMU(在其中实现了通信设备和模拟 USB 设备)、AFL(对其进行了修改,以针对 USB 设备,通过从虚拟内核收集覆盖信息并与用户模式代理交互)以及 kcov(对其进行了扩展,以跟踪整个 USB 堆栈的边缘覆盖,包括中断上下文)。USBFuzz 从头开始实现了用户模式代理。

当模糊测试工具启动时,它为位图分配了一个内存区域,并将其导出为共享内存区域,通信设备在 QEMU 启动时进行初始化。目标内核启动后,用户模式代理运行并通知模糊测试工具开始测试。

在每次模糊测试循环的迭代中,模糊测试工具通过虚拟连接模糊设备到目标系统来启动测试。随着模糊设备的连接,内核开始与该设备交互并加载适当的 USB 驱动程序。加载的 USB 驱动程序在与模糊设备交互时使用模糊输入进行测试。用户模式代理通过扫描内核日志监控执行,并通知模糊测试工具测试结果。模糊测试工具通过虚拟断开模糊设备与目标系统的连接来完成测试。

通信设备

USBFuzz 中的通信设备用于模糊测试组件与目标系统之间的轻量级通信,包括位图区域的共享以及用户模式代理与模糊测试组件之间的同步。通信设备的实现基于 IVSHMEM(虚拟机间共享内存)设备,这是 QEMU 中的一个模拟 PCI 设备。模糊测试组件的共享内存区域作为 IVSHMEM 设备中的内存区域导出到客户系统,并映射到客户系统的虚拟内存空间。寄存器BAR2(即内存或 IO 空间的基地址寄存器)用于模糊测试组件与用户模式代理之间的通信通道。

模糊测试器

模糊测试器使用两个管道与虚拟机进行通信:一个控制管道和一个状态管道。模糊测试器通过控制管道向虚拟机发送消息以启动测试,并通过状态管道接收来自虚拟机的执行状态信息。

在虚拟机端,注册了两个回调以便与模糊测试组件进行接口。当从控制管道接收到新消息时,一个回调将模糊测试工具生成的输入附加到虚拟机监控器中的新模糊设备实例。当通过通信设备从用户模式代理接收到执行状态信息时,另一个回调从虚拟机监控器中断开模糊设备,并通过状态管道将执行状态信息转发给模糊测试工具。

模糊测试设备

模糊测试设备是 USBFuzz 中的关键组件,使内核的硬件输入空间能够进行模糊测试。它作为 QEMU 设备仿真框架中的一个模拟 USB 设备实现,模拟在现实场景中由攻击者控制的恶意设备。

Hypervisors 拦截来自客户内核的所有设备读/写请求。客户操作系统内核的每个读/写操作都被分派到模糊测试设备实现中的注册函数,该函数按照硬件规范执行操作并将数据返回给内核。模糊设备通过注册read函数来实现,这些函数将模糊测试工具生成的数据转发给内核。更具体地说,设备驱动程序读取的字节按顺序映射到模糊测试工具生成的输入,设备和配置描述符则单独处理。

用户模式代理

用户模式代理用于在客户操作系统中作为守护进程运行,并在目标操作系统启动时自动启动。它根据内核日志监控测试的执行状态,并通过通信设备将信息传递给模糊测试工具。初始化后,它通知模糊测试工具目标内核已准备好进行测试。

在 Linux 和 FreeBSD 中,用户模式代理组件监控内核日志文件(Linux 中为 /dev/kmsg,FreeBSD 中为 /dev/klog),并扫描其中的错误消息,以指示内核漏洞或测试结束。如果检测到任一事件,它会使用通信设备驱动程序导出到用户空间的设备文件通知模糊测试工具停止当前迭代并继续进行下一次。错误消息的集合借鉴了 syzkaller。在 Windows 和 MacOS 中,由于内核在设备连接/断开时缺乏明确信号,用户模式代理使用固定的超时(MacOS 为 1 秒,Windows 为 5 秒)来允许设备正确初始化。

Linux kcov 的调整

为了在 Linux 内核的 USB 驱动程序上应用覆盖引导的模糊测试,USBFuzz 使用静态插桩技术从目标内核收集覆盖信息。该实现是基于 kcov 进行调整的,已经得到了 Linux 内核的支持。

1
2
3
index = (hash(IP) ^ hash(prev_loc))% BITMAP_SIZE;
bitmap[index] ++;
prev_loc = IP;

USBFuzz 通过扩展 kcov 实现了 AFL 风格的边缘覆盖模式。该修改支持跨多个线程和中断处理程序的多条执行路径,解开非确定性问题。每当发生非确定性时,会保存上一个代码块。对于进程,USBFuzz 在 struct task(Linux 内核中的进程控制块数据结构)中保存 prev_loc,而对于中断处理程序,则将 prev_loc 保存到栈上。

每当发生非确定性时,当前的上一个位置会被溢出(在内核线程的 struct task 中,或在中断处理程序的栈上),并设置为覆盖映射中的一个明确位置,从而将非确定性解开到特定位置。当执行恢复时,溢出的 prev_loc 会被还原。请注意,这种精心设计允许 USBFuzz 跟踪中断(以及嵌套中断)的执行,并将它们的覆盖分开,而不会通过虚假更新污染覆盖映射。

插桩代码被修改为将覆盖信息写入通信设备的内存区域,而不是每个进程的缓冲区。Linux 构建系统也进行了修改,以限制插桩仅针对感兴趣的代码。在 USBFuzz 的评估中,将覆盖跟踪限制为与 USB 子系统相关的任何内容,包括主控制器和设备的驱动程序。

总结

通过 USB 接口攻击面,可以利用操作系统中的软件漏洞。现有的 USB 模糊测试工具效率低下(例如,像 vUSBf 这样的简单模糊测试工具),不具可移植性(例如,syzkaller usb-fuzzer),且仅能覆盖驱动程序的探测功能。该文章提出了 USBFuzz,是一个灵活且模块化的框架,用于对操作系统内核中的 USB 驱动程序进行模糊测试。USBFuzz 可移植于不同操作系统上的 USB 驱动程序,利用 Linux 中的覆盖引导模糊测试,并在尚不支持覆盖收集的其他内核上进行简单模糊测试。USBFuzz 支持广泛的模糊测试(针对整个 USB 子系统和多种 USB 驱动程序),也可针对特定设备的驱动程序进行专注模糊测试。

代码复现

环境准备

系统环境如下:

代码分析

编译内核

首先需要获取 Linux 内核源码,并编译内核,编译内核时需要添加 kcov 支持,应用该论文所实现 patch,具体操作如下:

需要获取 Linux 内核源码:

1
2
3
wget -O linux-5.5.tar.gz https://web.git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/snapshot/linux-5.5.tar.gz
tar xzf linux-5.5.tar.gz
cd linux-5.5/

应用 patch:

1
2
3
4
git apply /data1/tools/USBFuzz/OSes/linux-target/kernel-patches/0001-Add-ivshmem-device-driver.patch 
git apply /data1/tools/USBFuzz/OSes/linux-target/kernel-patches/0002-Update-kcov.patch
git apply /data1/tools/USBFuzz/OSes/linux-target/kernel-patches/0003-Limit-KCOV-to-usb-drivers.patch
cp /data1/tools/USBFuzz/OSes/linux-target/kconfig .

编译内核:

1
2
3
4
mv kconfig .config
make clean
make menuconfig
make bzImage -j4

编译 USBFuzz:

1
2
# USBFuzz 根目录下
./build.sh

USBFuzz 入口程序即为根目录下的USBFuzz

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#!/usr/bin/env python

import os
import sys
import argparse
import signal

from fuzzer import USBFuzz

def main():
parser = argparse.ArgumentParser(description="USBFuzz: a tool for fuzzing usb drivers by device emulation")
parser.add_argument("--workdir", default="workdir", help="the work directory of the fuzzer")
parser.add_argument("--seeddir", default="seeddir", help="the directory containing the seeds")
parser.add_argument("--kernel_image", help="kernel image")
parser.add_argument("--os_image", default="./images/linux/stretch.img", help="OS image")
parser.add_argument("--aflfuzz_path", default="./usbfuzz-afl/afl-fuzz", help="path to afl-fuzz")
parser.add_argument("--afl_opts", help="Additional options to afl-fuzz")
parser.add_argument("--qemu_path", default="usbfuzz-afl/qemu_mode/qemu-build/x86_64-softmmu/qemu-system-x86_64", help="path to qemu")
args = parser.parse_args()

os_image = os.path.abspath(args.os_image)
kernel_image = None
if args.kernel_image != None:
kernel_image = os.path.abspath(args.kernel_image)
fuzzer = USBFuzz(args.workdir, args.seeddir,
args.aflfuzz_path, args.qemu_path,
os_image, kernel_image, args.afl_opts)

def ctrlc_handler(sig, frame):
print("stopping the fuzzer")
fuzzer.stop()
sys.exit(-1)


fuzzer.start()


if __name__ == '__main__':
main()

在fuzzer.py中,USBFuzz 类的初始化方式为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
import os
import sys
import subprocess

class USBFuzz(object):

def __init__(self, workdir, seeddir, aflfuzz_path, qemu_path,
os_image, kernel_image=None, afl_opts=None):
self.os_image = os_image
self.kernel_image = kernel_image
self.workdir = workdir
self.seeddir = seeddir
self.aflfuzz_path = aflfuzz_path
self.qemu_path = qemu_path
self.afl_opts = afl_opts
self.args = []

self._setup_fuzzer()

self.process = None

def _setup_fuzzer(self):
self.args.append(self.aflfuzz_path)
self.args.append("-QQ")
if self.afl_opts != None:
self.args.extend(self.afl_opts.split(" "))

self.args.append("-i")
self.args.append(self.seeddir)
self.args.append("-o")
self.args.append(self.workdir)
self.args.append("--")
self.args.append(self.qemu_path)
self.args.extend("-M q35 -snapshot -device qemu-xhci,id=xhci -m 4G -enable-kvm".split(" "))
self.args.extend("-object memory-backend-shm,id=shm -device ivshmem-plain,id=ivshmem,memdev=shm".split(" "))

if self.kernel_image != None:
# this is a linux system
self.args.append("-kernel")
self.args.append(self.kernel_image)
self.args.append("-append")
self.args.append("root=/dev/sda")

self.args.append("-hda")
self.args.append(self.os_image)
self.args.extend("-no-reboot -nographic -usbDescFile @@".split(" "))

def start(self):
# setting up some env variables
if len(self.args) == 0:
print("fuzzer not setup")
return
env = os.environ.copy()
env["AFL_NO_UI"] = "1"
env["AFL_NO_ARITH"] = "1"
env["AFL_FAST_CAL"] = "1"
env["AFL_SKIP_CPUFREQ"] = "1"
self.process = subprocess.Popen(self.args, env=env)

def stop(self):
if self.process != None:
self.process.kill()

实现fuzzer与客户系统及其用户程序通信的核心在于:

1
self.args.extend("-object memory-backend-shm,id=shm -device ivshmem-plain,id=ivshmem,memdev=shm".split(" "))

USBFuzz 对 Linux 内核进行三部分修改:增加 ivshmem 设备,调整 kcov 覆盖率计算方法,只允许 USB 驱动使用 kcov。

为了增加 ivshmem 设备,USBFuzz 使用了 https://github.com/cmacdonell/ivshmem-code 中的代码,并进行了修改,以支持 Linux 5.5。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
diff --git a/drivers/staging/Makefile b/drivers/staging/Makefile
index 0a4396c9067b..f4398f57339e 100644
--- a/drivers/staging/Makefile
+++ b/drivers/staging/Makefile
@@ -55,3 +55,5 @@ obj-$(CONFIG_EXFAT_FS) += exfat/
obj-$(CONFIG_QLGE) += qlge/
obj-$(CONFIG_NET_VENDOR_HP) += hp/
obj-$(CONFIG_WFX) += wfx/
+obj-$(CONFIG_IVSHMEM) += ivshmem/
+
diff --git a/drivers/staging/ivshmem/Kconfig b/drivers/staging/ivshmem/Kconfig
new file mode 100644
index 000000000000..2f2e883986e0
--- /dev/null
+++ b/drivers/staging/ivshmem/Kconfig
@@ -0,0 +1,7 @@
+config IVSHMEM
+ tristate "InterVM shared memory device driver"
+ depends on PCI
+ ---help---
+
+ Device driver for InterVM shared memory device, borrowed from:
+ https://github.com/cmacdonell/ivshmem-code

为了调整 kcov 覆盖率计算方法,USBFuzz 修改了以下文件:

1
2
3
4
5
6
---
arch/x86/entry/entry_64.S | 10 ++++++++
include/linux/sched.h | 2 ++
kernel/Makefile | 3 +++
kernel/kcov.c | 54 ++++++++++++++++++++++++++++++++-------
4 files changed, 60 insertions(+), 9 deletions(-)

为了存储覆盖率数据,USBFuzz 修改了sched.h文件,给task_struct添加了一个新的字段:

1
2
3
4
5
6
7
8
9
10
11
12
diff --git a/include/linux/sched.h b/include/linux/sched.h
index 716ad1d8d95e..72dcc5ad44c5 100644
--- a/include/linux/sched.h
+++ b/include/linux/sched.h
@@ -1224,6 +1224,8 @@ struct task_struct {
/* KCOV descriptor wired with this task or NULL: */
struct kcov *kcov;

+ unsigned long prev_loc;
+
/* KCOV common handle for remote coverage collection: */
u64 kcov_handle;

最后,为了只允许 USB 驱动使用 kcov,USBFuzz 修改了 kcov.c 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
diff --git a/kernel/kcov.c b/kernel/kcov.c
index f50354202dbe..00fa11daf8aa 100644
--- a/kernel/kcov.c
+++ b/kernel/kcov.c
@@ -21,6 +21,7 @@
#include <linux/debugfs.h>
#include <linux/uaccess.h>
#include <linux/kcov.h>
+#include <linux/hash.h>
#include <linux/refcount.h>
#include <linux/log2.h>
#include <asm/setup.h>
@@ -172,6 +173,10 @@ static notrace unsigned long canonicalize_ip(unsigned long ip)
return ip;
}

+
+extern unsigned char *ivshmem_bar2_map_base(void);
+DEFINE_PER_CPU(unsigned long, prev_loc) = 0;
+
/*
* Entry point from instrumented code.
* This is called once per basic-block/edge.
@@ -179,20 +184,50 @@ static notrace unsigned long canonicalize_ip(unsigned long ip)
void notrace __sanitizer_cov_trace_pc(void)
{
struct task_struct *t;
- unsigned long *area;
+ /* unsigned long *area; */
unsigned long ip = canonicalize_ip(_RET_IP_);
- unsigned long pos;
+ unsigned long prev;
+ unsigned long hash;
+ int pos;
+ unsigned char *bitmap;

- t = current;
- if (!check_kcov_mode(KCOV_MODE_TRACE_PC, t))
+ bitmap = ivshmem_bar2_map_base();
+
+ if (!bitmap)
return;

- area = t->kcov_area;
+
+ // printk("%s B called\n", __func__);
+
+ t = current;
+ /* if (!check_kcov_mode(KCOV_MODE_TRACE_PC, t)) */
+ /* return; */
+
+ // area = t->kcov_area;
/* The first 64-bit word is the number of subsequent PCs. */
- pos = READ_ONCE(area[0]) + 1;
- if (likely(pos < t->kcov_size)) {
- area[pos] = ip;
- WRITE_ONCE(area[0], pos);
+ /* pos = READ_ONCE(area[0]) + 1; */
+ /* if (likely(pos < t->kcov_size)) { */
+ /* area[pos] = ip; */
+ /* WRITE_ONCE(area[0], pos); */
+ /* } */
+
+
+ if (!in_task()) {
+ prev = this_cpu_read(prev_loc);
+ } else {
+ prev = t->prev_loc;
+ }
+
+
+ hash = hash_long(ip, BITS_PER_LONG);
+ pos = (prev ^ hash) & 0xFFFF;
+
+ bitmap[pos] ++;
+
+ if (!in_task()) {
+ this_cpu_write(prev_loc, hash);
+ } else {
+ t->prev_loc = hash;
}
}
EXPORT_SYMBOL(__sanitizer_cov_trace_pc);
@@ -339,6 +374,7 @@ static void kcov_task_reset(struct task_struct *t)
t->kcov = NULL;
t->kcov_sequence = 0;
t->kcov_handle = 0;
+ t->prev_loc = 0;
}

在客户系统内,USBFuzz 实现了用户模式代理(uma)来与fuzzer进行通信。其使用/dev/ivshmem访问内核模块的共享内存,使用/dev/kmsg与epoll机制监听内核中的日志更新。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
#include <stdio.h>     // for fprintf()
#include <stdlib.h> // for fprintf()
#include <unistd.h> // for close(), read()
#include <sys/time.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/epoll.h> // for epoll_create1(), epoll_ctl(), struct epoll_event
#include <sys/ioctl.h>
#include <string.h> // for strncmp
#include <time.h>
#include <errno.h>

#define BUF_SIZE 1024*1024*32
static char buf[BUF_SIZE];

#define IVSHMEM_IOCTL_COMM _IOR('K', 0, int)

const char *ivshmem_dev_file = "/dev/ivshmem";
static int dev_fd;

static void notify_fuzzer(int value) {
if (dev_fd == -1) {
return;
}

int ret = ioctl(dev_fd, IVSHMEM_IOCTL_COMM, value);
printf("ioctl ret: %d\n", ret);

return;
}

static void drain_fd(int fd) {
while (read(fd, buf, BUF_SIZE) != -1)
;

if (errno != EAGAIN) {
fprintf(stderr, "drainig fd error, maybe it is not open in nonblocking mode\n");
return;
}
}

// TODO: add more to this list
static char *crash_key_words[] = {
"BUG:",
"WARNING:",
"INFO:",
"Unable to handle kernel paging request",
"general protection fault:",
"Kernel panic",
"PANIC: double fault",
"kernel BUG",
"BUG kmalloc-",
"divide error:",
"invalid opcode:"
"UBSAN:",
"unregister_netdevice: waiting for",
"trusty: panic",
"Call Trace:",
NULL
};

static int containsCrash(char *msg) {
int i = 0;

for (i = 0; crash_key_words[i] != NULL; i++) {
if (strstr(msg, crash_key_words[i])) {
return 1;
}
}

return 0;
}


static char *endMsgs[] = {
"unable to enumerate USB device",
"usb_probe_device: usb_probe_device completed",
"usbcore: registered new interface",
NULL
};

static int endOfTest(char *msg) {
int i = 0;

for (i = 0; endMsgs[i] != NULL; i++) {
if (strstr(msg, endMsgs[i])) {
return 1;
}
}

return 0;
}

int main() {
struct epoll_event event;
int epoll_fd = epoll_create1(0);

dev_fd = open(ivshmem_dev_file, O_RDWR);

if (dev_fd == -1) {
fprintf(stderr, "Failed to open the device file, maybe the dev node is not created\n");
return -1;
}

if(epoll_fd == -1) {
fprintf(stderr, "Failed to create epoll file descriptor\n");
return -1;
}
const char *kmsg_file = "/dev/kmsg";
int kmsg_fd = open(kmsg_file, O_RDONLY | O_NONBLOCK);
if (kmsg_fd == -1) {
fprintf(stderr, "Failed to open the kmsg file\n");
return 1;
}

drain_fd(kmsg_fd);

// notify the fuzzer to start
notify_fuzzer(0x52);

event.data.fd = kmsg_fd;
event.events = EPOLLIN;

if (-1 == epoll_ctl(epoll_fd, EPOLL_CTL_ADD, kmsg_fd, &event)) {
fprintf(stderr, "Failed to add kmsg_fd to epoll\n");
goto fail1;
}

#define MAX_EVENTS 1
struct epoll_event events[MAX_EVENTS];
int event_count;

while (1) {

event_count = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
int i = 0;

for (i = 0; i < event_count; i ++) {

memset(buf, 0, BUF_SIZE);

read(events[i].data.fd, buf, BUF_SIZE);

if (containsCrash(buf)) {
notify_fuzzer(0x51);
} else if (endOfTest(buf)){
const char *cmd = getenv("USB_TEST_CMD");
int notified = 0;
if (cmd != NULL) {
struct timespec ts;
ts.tv_sec = 0;
ts.tv_nsec = 500000;
nanosleep(&ts, &ts);
// execute the command
// sleep(3);

printf("Running testing command: %s\n", cmd);
system(cmd);

memset(buf, 0, BUF_SIZE);
event_count = epoll_wait(epoll_fd, events, MAX_EVENTS, 3);

for (int j = 0; j < event_count; j++) {
read(events[j].data.fd, buf, BUF_SIZE);

if (containsCrash(buf)) {
notified = 1;
notify_fuzzer(0x51);
}
}
}

if (notified == 0) {
notify_fuzzer(0x50);
}
}

fflush(stdout);
}
}

goto done;

fail1:
if(close(epoll_fd)) {
fprintf(stderr, "Failed to close epoll file descriptor\n");
return 1;
}

done:
return 0;
}

在 AFL-fuzz 中,为了适配当前情境下 QEMU 的使用,USBFuzz 使用-QQ增加了qemu_mode == 2的状态,并在setup_shmrun_target等处进行相应修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
case 'Q': /* QEMU mode */

// if (qemu_mode) FATAL("Multiple -Q options not supported");
qemu_mode += 1;

if (!mem_limit_given && qemu_mode == 1) {
mem_limit = MEM_LIMIT_QEMU;
} else if (qemu_mode > 1) {
mem_limit = 0;

}

break;

设置 shm 共享内存:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
EXP_ST void setup_shm(void) {

u8* shm_str;

if (!in_bitmap) memset(virgin_bits, 255, MAP_SIZE);

memset(virgin_tmout, 255, MAP_SIZE);
memset(virgin_crash, 255, MAP_SIZE);

shm_id = shmget(IPC_PRIVATE, MAP_SIZE, IPC_CREAT | IPC_EXCL | 0600);

if (shm_id < 0) PFATAL("shmget() failed");

atexit(remove_shm);

shm_str = alloc_printf("%d", shm_id);

/* If somebody is asking us to fuzz instrumented binaries in dumb mode,
we don't want them to detect instrumentation, since we won't be sending
fork server commands. This should be replaced with better auto-detection
later on, perhaps? */

if (!dumb_mode || qemu_mode > 1) setenv(SHM_ENV_VAR, shm_str, 1);

ck_free(shm_str);

trace_bits = shmat(shm_id, NULL, 0);

if (!trace_bits) PFATAL("shmat() failed");

}

实验结果

运行如下命令:

运行一段时间后,可以发现探测到了 crash:

总结

USBFuzz 是一个针对 USB 设备的模糊测试工具,通过 QEMU 模拟 USB 设备,并使用 AFL-fuzz 进行模糊测试,从而发现 USB 设备的漏洞。其在实现过程中,通过修改 AFL-fuzz、Linux kernel 的源码,使其能够与 QEMU 模拟的 USB 设备进行交互,从而实现模糊测试。