0x00 写在前面

本文主要是阅读了文章 Healer 后,对文章的内容进行理解,然后基于作者维护的开源工具 Healer 进行一个环境搭建和复现。希望以此记录学习过程。

0x01 方法原理

简介

考虑到 Syzkaller 在产生(generation&mutation)系统调用序列(program)的过程中,系统调用(syscall)是按照系统调用描述(syscall discriptions)和选择表(choice table)来添加的。这使得同一个输入中相邻两个系统调用之间可能没有影响关系,从而生成大量无意义或者等价的输入,降低模糊测试的效率。Healer 在 Syzkaller 的启发下,也使用了 Syzkaller 所提供的 syscall 信息来生成确认参数结构约束和部分语义约束的系统调用序列,并通过不断执行生成的调用序列来发现内核错误,导致内核崩溃。与 Syzkaller 不同的是,Healer 不使用 choice table,而是通过动态移除最小化调用序列中的系统调用并观察覆盖范围变化来检测不同系统调用之间的内部关系,并通过利用系统调用之间的关系来指导系统调用序列的生成和变异。

确认系统调用间的关系,选择有效的序列并建立有效的探索路径具有重要价值:

  1. 排除无效序列有助于提升fuzzing的效率。Linux拥有接近400个不同的系统调用,穷举组合的时间开销巨大。
  2. 通过建立有效的状态路径助于探索内核的深层逻辑

这个出发点是很好理解的,例如,在一个输入 $program=syscall_1, syscall_2, \cdots , syscall_n$ 中,如果前一个系统调用 $syscall_i$ 的执行对其后紧接着的下一个系统调用 $syscall_j$ 的执行毫无影响,则这个输入中,系统调用 $syscall_i$ 的存在将没有什么意义,即移除该系统调用后得到的新输入是等效的。而通过对关系的学习,可以在输入生成阶段有效避免生成这种等效的输入,进而提高模糊测试的效率。

工作流程

我不想再开启图床,也不想再发送这个文件的过程中还需要打包一份图片,于是选择了使用网图的方式嵌入图片,这个后期可以进行等效处理的,问题不大

Healer 使用了 14919 行 Rust 语言来实现,其工作流程如下图所示。

Healer 工作流程图

Healer 从读取系统调用描述(System call Description)和输入语料库(programs corpus)的数据,然后通过其关系学习模块(Relation Leaning)进行分析,获取不同系统调用之间的内部影响关系,生成一个系统调用关系表(Relation Table)。而随着模糊测试过程的继续,输入语料库也会不断进行更新,使得系统调用关系表也会进行同步更新,于是 Healer 将会得到更多的系统调用之间的影响关系。而得到的系统调用关系表将会用于指导后续的输入生成和变异。

Healer 中的关系(Relation)即指的是两个系统调用间是否存在影响,如果一个系统调用 $syscalli$ 的执行可以影响到另一个系统调用 $syscall_j$ 的执行路径(例如, $syscall_i$ 修改了 $syscall_j$ 将要依赖的内核内部状态),则称系统调用 $syscall_i$ 对系统调用 $syscall_j$ 产生了影响。此时将会在系统关系调用表 $R(\N \times \N)$ ($\N$ 表示系统调用个数)中记录 $R{i,j}=1$ 。

  • Healer 主体架构运行在主机上,负责多任务执行、虚拟机驱动、执行驱动、种子生产和变异、虚拟机状态同步操作。通过共享内存和 socket 与被 fuzz 的多个 QEMU 虚拟机实例进行通信。
  • 被 fuzz 的每个内核都会运行在一个自己的 QEMU 虚拟机上,通过 socket 与主体通信,通过共享内存获取测试数据。

关系学习模块

关系学习模块(Relation Learning)部分主要分为静态学习(Static Learning)和动态学习(Dynamic Learning)两个部分。通过这两个部分的学习,Healer 能成功通过覆盖范围信息来指导建立起系统调用之间的内部关系信息。

静态学习

初始时,Healer 根据系统调用描述提供的信息来初始化系统调用关系表,而静态分析主要关注其中的参数类型和返回值类型。

对于任意两个系统调用 $syscall_i$ 和 $syscall_j$ 如果满足下面两个条件:

  1. $syscall_i$ 的返回值是一种 resource 类型 $r$ 或者 $syscall_i$ 的任何参数时一个具有向外数据流方向的指针时。(很显然此时 $syscall_i$ 具有向外输出数据流的能力。
  2. $syscall_j$ 中至少有一个参数的类型是具有向内的数据流的 resource 类型 $r$ 或者与 $r$ 兼容的类型 $r’$ 时(例如继承)。(很显然此时 $syscall_j$ 具有被这种数据流影响的可能性。

则此时我们可以很容易通过静态分析得知, $syscalli$ 对 $syscall_j$ 是有影响的,所以此时记录 $R{i,j}=1$ 。

动态学习

通过在运行中实际测试覆盖率的变化可以获得更多静态学习无法学到的影响关系,用于更新和细化关系表,以便生成高质量的测试用例。

在进行动态学习之前,Healer 会先统计每个系统调用的覆盖范围,并存储其触发的基本块和边,以便于在后续的测试中发现新的覆盖范围信息。

之后会调用 minimization 算法,将触发新的覆盖率的系统调用序列进行最小化处理,以便得到尽可能小的覆盖范围不变的系统调用序列。minimization 算法则是通过反向遍历待处理的系统调用序列,通过尝试性移除某个系统调用,若移除后执行得到的覆盖率没有发生变化,则当前的移除的系统调用是无用的,可以直接丢弃,反之则不能丢弃。最终得到的就是最小化之后的系统调用序列。

再完成 minimization 之后,动态学习部分则会在最小化的系统调用序列中逐渐地移除单个系统调用并检测对下一个系统调用的影响。假设 $syscalli$ 和 $syscall_j$ 是最小化系统调用中紧邻的两个系统调用, $syscall_i$ 是 $syscall_j$ 的前一个系统调用,如果移除了 $syscall_i$ 后对 $syscall_j$ 的覆盖范围信息造成了影响,则说明 $syscall_i$ 是对 $syscall_j$ 有影响的,此时可以设置 $R{i,j}=1$ 。

变异规则

通过上述的关系学习模块可以得到系统调用关系表,而后,Healer 将使用这个信息来指导输入的变异和生成。在 Healer 中变异的方式一共有:插入系统调用(insert_calls)、变异系统调用参数(mutate_call_args)、拼接系统调用(splice)和移除系统调用(remove_call)四种。其中后三种方式是常规的变异方式,只有插入系统调用会使用到系统调用关系表来指导变异。

具体变异规则为,当 Healer 从语料库中随机选择了一个系统调用序列后,它会在该序列中随机选择一个位置作为插入点,然后将插入点前面的子序列作为变异算法(Guided Call Selection)的输入,然后将变异算法输出得到的系统调用插入在选中的地方。该变异算法的工作流程为:

  1. 以一个与 $\alpha$ 相关的随机概率返回一个随机的系统调用。
  2. 否则遍历输入的系统调用子序列中的每个系统调用,为其所有能影响的系统调用增加一个权重。遍历完毕之后会依据每个系统调用的权重返回一个与权重有关的随机选择。

伪代码如下图所示:

Guided Call Selection 伪代码

值得注意的是,上述参数 $\alpha$ 是一个可以调控的参数。在模糊测试进程开始之初,系统调用关系表中的信息并不是很多,如果过度从中选取系统调用将会导致整个生成的种子库多样性降低;而随着后期动态学习不断更新系统关系调用表,通过使用其中的信息来指导生成系统调用序列可以增加生成输入的有效性。因此通过调控这个参数,并在模糊测试过程中不断增大这个值,可以使得 Healer 在模糊测试初期和后期都能取得比较不错的效果。

0x02 环境配置

为了能对 Linux 内核进行模糊测试,我们需要进行下列的步骤:

  1. 选择一个合适的主机以便在其上进行实验。这里本文选择的是 Ubuntu 20.04 x86-64。
  2. 下载想 fuzz 的 Linux 内核的源码,然后开启配置选项后,在主机上对该 Linux 内核的源码进行编译并制作 image。
  3. 在主机上安装 QEMU启动待测试的 Linux 内核。
  4. 在主机上编译并配置好 fuzzer 。

为了便于管理和识别本次实验的环境,建议在某个位置开始新建一个本次实验相关的文件夹。

在实验机汇总打开一个终端(terminal),输入下面的命令,然后键入 Enter ↵ 。 这个命令会在当前位置创建一个名为“healer” 的文件夹,用于存储与本次实验相关的文件。请记住该文件夹的位置。

1
$ mkdir HEALER

依赖库安装

该部分主要参考了 Syzkaller set up 中对环境配置的描述,这里本次实验的主机是 Ubuntu,架构属于 x86-64,选择使用 QEMU 来启动待测试的内核,所以选择了对应的参考文档: Setup: Ubuntu host, QEMU vm, x86-64 kernel

注意不同的选择情况下的安装过程有些许区别,但是仔细看教程中的步骤做,也是可以的。

Prerequisites

为了编译 Linux 的内核,需要在主机上先安装一些必要的依赖库。

首先进行软件包的更新,在实验主机上打开一个终端,输入下面的命令,然后键入 Enter ↵

1
$ sudo apt update

可能会出现下面的情况:

1
2
$ sudo apt update
[sudo] password for [username]:

这时别慌,在当前终端中输入你此时登陆的用户的密码,(此时终端中可能没有出现你输入的密码,窗口中也不会有什么变化,不过别担心,这是为了安全的考虑。)

在输入完密码之后,然后键入 Enter ↵

之后就可以正常执行这个命令了。

然后进行依赖库的安装,在终端中输入下面的命令,然后键入 Enter ↵

1
$ sudo apt install make gcc flex bison libncurses-dev libelf-dev libssl-dev

Kernel

这个部分将进行内核源码的下载和内核的编译。

Linux 内核源码下载

在实验机器中,HEALER 文件夹中打开一个终端,输入下面的命令,然后键入 Enter ↵

下载完成时间与网络速度有关,请耐心等待命令执行完毕。

注意执行这个命令的位置,他会自动将 linux 文件夹保存在执行命令的位置。本实例中下载的位置是: /SOME_PATH/HEALER/linux 。 请根据自己的实际情况替换 SOME_PATH 的值。

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
$ sudo git clone --branch v5.11 git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git $KERNEL
[sudo] password for username:
Cloning into 'linux'...
remote: Enumerating objects: 68, done.
remote: Counting objects: 100% (68/68), done.
remote: Compressing objects: 100% (14/14), done.
remote: Total 9098039 (delta 59), reused 56 (delta 54), pack-reused 9097971
Receiving objects: 100% (9098039/9098039), 2.48 GiB | 1.32 MiB/s, done.
Resolving deltas: 100% (7458781/7458781), done.
Note: switching to 'f40ddce88593482919761f74910f42f4b84c004b'.

You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by switching back to a branch.

If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -c with the switch command. Example:

git switch -c <new-branch-name>

Or undo this operation with:

git switch -

Turn off this advice by setting config variable advice.detachedHead to false

Updating files: 100% (71277/71277), done.
生成默认配置文件

在刚才下载的文件夹 linux 中打开一个终端,或者也可以在任意一个终端中输入下面的命令,然后键入 Enter ↵

1
$ cd /SOME_PATH/HEALER/linux

然后在 linux 文件夹的终端中输入下面的命令,然后键入 Enter ↵ 。 该命令会自动生成一个与系统相关的默认配置文件 .config

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ sudo make defconfig
HOSTCC scripts/basic/fixdep
HOSTCC scripts/kconfig/conf.o
HOSTCC scripts/kconfig/confdata.o
HOSTCC scripts/kconfig/expr.o
LEX scripts/kconfig/lexer.lex.c
YACC scripts/kconfig/parser.tab.[ch]
HOSTCC scripts/kconfig/lexer.lex.o
HOSTCC scripts/kconfig/parser.tab.o
HOSTCC scripts/kconfig/preprocess.o
HOSTCC scripts/kconfig/symbol.o
HOSTCC scripts/kconfig/util.o
HOSTLD scripts/kconfig/conf
*** Default configuration is based on 'x86_64_defconfig'
#
# configuration written to .config
#

耐心等待上面的命令执行完毕后之后,接着输入下面的命令,然后键入 Enter ↵ 。 该命令会再次生成一些配置信息并合并后写入到配置文件 .config 中。

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
$ sudo make kvm_guest.config
Using .config as base
Merging ./kernel/configs/kvm_guest.config
Value of CONFIG_HYPERVISOR_GUEST is redefined by fragment ./kernel/configs/kvm_guest.config:
Previous value: # CONFIG_HYPERVISOR_GUEST is not set
New value: CONFIG_HYPERVISOR_GUEST=y

Value of CONFIG_VIRTIO_PCI is redefined by fragment ./kernel/configs/kvm_guest.config:
Previous value: # CONFIG_VIRTIO_PCI is not set
New value: CONFIG_VIRTIO_PCI=y

Value of CONFIG_VIRTIO_CONSOLE is redefined by fragment ./kernel/configs/kvm_guest.config:
Previous value: # CONFIG_VIRTIO_CONSOLE is not set
New value: CONFIG_VIRTIO_CONSOLE=y

Value of CONFIG_NET_9P is redefined by fragment ./kernel/configs/kvm_guest.config:
Previous value: # CONFIG_NET_9P is not set
New value: CONFIG_NET_9P=y

Value of CONFIG_SCSI_LOWLEVEL is redefined by fragment ./kernel/configs/kvm_guest.config:
Previous value: # CONFIG_SCSI_LOWLEVEL is not set
New value: CONFIG_SCSI_LOWLEVEL=y

#
# merged configuration written to .config (needs make)
#
#
# configuration written to .config
#
打开需要的配置选项

我发现了一个更简单更方便,也确实理论上也本来就应该有这么一个方式,通过命令行的方式修改配置文件的选项信息,不然太傻了。

1
2
3
4
5
6
7
sudo ./scripts/config \
-e CONFIG_KCOV \
-e CONFIG_DEBUG_INFO \
-e CONFIG_KASAN \
-e CONFIG_KASAN_INLINE \
-e CONFIG_CONFIGFS_FS \
-e CONFIG_SECURITYFS

以下是傻傻的旧方法

/SOME_PATH/HEALER/linux 目录下打开一个终端,输入下面的命令,然后键入 Enter ↵ 。 该命令会打开配置文件 .config 并在终端中显示。

也可以通过文本编辑器打开该文件修改下面对应的字段后保存。使用这个方法则可以跳过下面修改的这一步。

1
$ sudo vim .config

终端中会显示出生成的配置选项信息,找到下面列举的几项后,修改后的配置文件中的相关参数与下列一致。

在 vim 命令模式下,键入 i 可以进入插入模式,然后就可以修改对应的内容。

修改完毕后,键入 Esc 可以退出插入模式,返回命令模式。

在确认所有配置项都修改完毕后,在命令模式下键入 shift +; (即键入 :) 。然后再键入 w + q 可以进行保存并退出。

1
2
3
4
5
6
7
8
9
10
11
12
13
# Coverage collection.
CONFIG_KCOV=y

# Debug info for symbolization.
CONFIG_DEBUG_INFO=y

# Memory bug detector
CONFIG_KASAN=y
CONFIG_KASAN_INLINE=y

# Required for Debian Stretch
CONFIG_CONFIGFS_FS=y
CONFIG_SECURITYFS=y

在修改 .config 并保存之后, 再进行重新编译配置文件。输入下面的命令,然后键入 Enter ↵

1
2
3
4
5
6
7
8
9
$ sudo make olddefconfig
.config:4089:warning: override: reassigning to symbol CONFIGFS_FS
.config:4212:warning: override: reassigning to symbol SECURITYFS
.config:4604:warning: override: reassigning to symbol DEBUG_INFO
.config:4671:warning: override: reassigning to symbol KASAN
.config:4845:warning: override: reassigning to symbol KCOV
#
# configuration written to .config
#
构建内核

在完成配置文件的修改后,输入下面的命令,然后键入 Enter ↵

1
$ sudo make -j`nproc`

编译的过程会比较旧,取决于实验环境的机器的性能,在终端中会打印很多和编译相关的日志。下面是部分节选:

1
2
3
4
5
6
7
8
9
10
11
12
...
GZIP arch/x86/boot/compressed/vmlinux.bin.gz
MKPIGGY arch/x86/boot/compressed/piggy.S
AS arch/x86/boot/compressed/piggy.o
LD arch/x86/boot/compressed/vmlinux
ZOFFSET arch/x86/boot/zoffset.h
OBJCOPY arch/x86/boot/vmlinux.bin
AS arch/x86/boot/header.o
LD arch/x86/boot/setup.elf
OBJCOPY arch/x86/boot/setup.bin
BUILD arch/x86/boot/bzImage
Kernel: arch/x86/boot/bzImage is ready (#1)

Now you should have vmlinux (kernel binary) and bzImage (packed kernel image)。

到这里,如果看到终端停止打印日志,且最后一个日志显示为:“Kernel: arch/x86/boot/bzImage is ready (#1)” 则现在已经成功完成了内核的编译了。

现在,输入下面的命令,然后键入 Enter ↵ ,应该可以看到文件 vmlinux 的存在。

1
2
$ ls /SOME_PATH/HEALER/linux/vmlinux
/SOME_PATH/HEALER/linux/vmlinux

同理,输入下面的命令,然后键入 Enter ↵ ,应该可以看到文件 bzImage 的存在。

1
2
$ ls /SOME_PATH/HEALER/linux/arch/x86/boot/bzImage
/SOME_PATH/HEALER/linux/arch/x86/boot/bzImage

Image

安装 debootstrap

输入下面的命令,然后键入 Enter ↵ 。该命令可以完成安装 debootstrap。

1
$ sudo apt install debootstrap
创建 Debian Stretch Linux image

教程中有两种一种是: Create Debian Buster Linux image 另一种是 Create Debian Stretch Linux image

我查了一下就是选择要制作的 debian 的版本不同,一个是旧一点的稳定版,一个是更旧的版本。

/SOME_PATH/HEALER 目录下打开一个终端,输入下面的命令,然后键入 Enter ↵ 。 该命令会创建一个文件目录 disk_imglinux 同级,同时会切换到 disk_img 目录下。

1
$ mkdir disk_img && cd disk_img

然后在当前终端中,输入下面的命令,然后键入 Enter ↵

1
$ mkdir $IMAGE && cd $IMAGE/

然后在当前终端中,输入下面的命令,然后键入 Enter ↵ 。 该命令会下载一个文件 create-image.sh

1
$ wget https://raw.githubusercontent.com/google/syzkaller/master/tools/create-image.sh -O create-image.sh

使用文本编辑器打开 ./create-image.sh 在第 147 行附近按照下面的内容进行修改后保存。本次修改将进行换源,这里切换为使用清华源。

1
2
3
4
# before
sudo debootstrap $DEBOOTSTRAP_PARAMS
# after
sudo debootstrap $DEBOOTSTRAP_PARAMS "https://mirrors.tuna.tsinghua.edu.cn/debian/"

然后在当前终端中,输入下面的命令,然后键入 Enter ↵ 。 该命令会为文件 create-image.sh 添加执行权限。

1
$ chmod +x create-image.sh

然后在当前终端中,输入下面的命令,然后键入 Enter ↵ 。 该命令会执行文件 create-image.sh

1
$ ./create-image.sh

然后稍等片刻,运行时间取决于实验机器。

等待运行完毕后,在当前终端中输入 ls 命令,可以看到文件 stretch.img

1
2
$ ls
chroot create-image.sh stretch.id_rsa stretch.id_rsa.pub stretch.img

这里代理的问题还卡了我挺久,一直构建不成功,前面的下载慢我们还可以本地下载了再传上去,但是最后这个脚本执行的时候会从 debian 官方去下载文件,这下就头大了,分好几次,跑了好久都失败了(重复试、terminal 走本地局域网代理等)

然后都失败了,通过查看失败的位置,查看 create-image.sh 脚本代码,结合日志定位了是其中下面这个命令执行的时候太慢了,请求不到:

1
sudo debootstrap --arch=amd64 --include=openssh-server,curl,tar,gcc,libc6-dev,time,strace,sudo,less,psmisc,selinux-utils,policycoreutils,checkpolicy,selinux-policy-default,firmware-atheros,debian-ports-archive-keyring --components=main,contrib,non-free stretch chroot

觉得肯定是 特色 Nation 于是尝试换源解决,但是搜出来的 debootstrap 换源别人都是讲的换 apt 那种的意思,不是这样的。(虽然确实是同一个源,但是就是跑这个代码不会走换的源还是官方源。

于是加双引号终于搜到一个答案:【Linux学习笔记】debootstrap的使用技巧

在使用开发板厂家系统构建脚本时,系统构建过程非常慢,多次尝试也容易出现构建过程中因为包获取失败而中断的情况;

1.首先尝试为虚拟机设置梯子,主机上开启SSR,设置本地端口,虚拟机中开启代理,实测无明显改善。

2.尝试分析脚本提前下载所需文件,能改善镜像下载阶段的速度,但获取软件包过程依然无改善。

3.继续捋脚本发现软件包的获取及安装过程实际是debootstrap的执行过程,查询其使用说明

常用方式为:

1
debootstrap stable /stable-chroot http://deb.debian.org/debian/

虽然没看懂,但是我受到了启发,就是往执行命令后面添加一个 url 作为我们想指定的源,于是我随便选择了个清华源,在命令行单独试了一下:

1
sudo debootstrap --arch=amd64 --include=openssh-server,curl,tar,gcc,libc6-dev,time,strace,sudo,less,psmisc,selinux-utils,policycoreutils,checkpolicy,selinux-policy-default,firmware-atheros,debian-ports-archive-keyring --components=main,contrib,non-free stretch chroot https://mirrors.tuna.tsinghua.edu.cn/debian/

结果一会会这个命令就完美跑通了

所以,我们就可以定位文件 ./create-image.sh 中,执行上述命令相关的位置 (line:147) 进行一个简单修改:

1
2
3
4
# before
sudo debootstrap $DEBOOTSTRAP_PARAMS
# after
sudo debootstrap $DEBOOTSTRAP_PARAMS "https://mirrors.tuna.tsinghua.edu.cn/debian/"

QEMU

安装 QEMU

在终端中输入下面的命令,然后键入 Enter ↵ 。 该命令会安装 QEMU。

1
sudo apt install qemu-system-x86

验证测试

本部分将完成对 kernel boot 和 sshd 的有效性进行验证。

在终端中输入下面的命令,然后键入 Enter ↵ 。 该命令会使用 QEMU 去启动刚才编译得到的内核。

注意修改 /SOME_PATH 为文件的实际真实位置。

1
2
3
4
5
6
7
8
9
10
11
12
sudo qemu-system-x86_64 \
-m 2G \
-smp 2 \
-kernel /SOME_PATH/HEALER/linux/arch/x86/boot/ \
-append "console=ttyS0 root=/dev/sda earlyprintk=serial net.ifnames=0" \
-drive file=/SOME_PATH/HEALER/disk_img/stretch.img,format=raw \
-net user,host=10.0.2.10,hostfwd=tcp:127.0.0.1:10021-:22 \
-net nic,model=e1000 \
-enable-kvm \
-nographic \
-pidfile vm.pid \
2>&1 | tee vm.log

执行上述命令后,稍等片刻,终端中会打印很多相关日志,等待日志停止打印后,会出现一个等待输入的提示。大约如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
...
[ OK ] Started Serial Getty on ttyS0.
[ OK ] Started Getty on tty1.
[ OK ] Started Getty on tty3.
[ OK ] Started Getty on tty5.
[ OK ] Started Getty on tty4.
[ OK ] Started Getty on tty6.
[ OK ] Reached target Login Prompts.
[ OK ] Started OpenBSD Secure Shell server.
[ OK ] Reached target Multi-User System.
[ OK ] Reached target Graphical Interface.
Starting Update UTMP about System Runlevel Changes...
[ OK ] Started Update UTMP about System Runlevel Changes.

Debian GNU/Linux 9 syzkaller ttyS0

syzkaller login:

此时在当前终端中输入 root ,然后键入 Enter ↵ 。 表明将以 root 的身份进行登录。然后终端就会进入刚才编译得到的内核中(给出了一个内核的命令行接口)。

1
2
3
4
5
6
7
8
9
10
11
12
...
syzkaller login: root
Unable to get valid context for root
Linux syzkaller 5.11.0 #1 SMP Sun Nov 6 20:44:59 CST 2022 x86_64

The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
root@syzkaller:~#

但是喵喵的没法退出了呢。。。。 好像是要 ctrl-a + x

连接到一个 QEMU 实例中

在实验机中重新打开一个新的终端,输入下面的命令,然后键入 Enter ↵ 。注意修改下方 SOME_PATH 为实际路径。

1
sudo ssh -i /SOME_PATH/HEALER/disk_img/stretch.id_rsa -p 10021 -o "StrictHostKeyChecking no" root@localhost

稍等片刻,当前终端中会显示如下信息,表明通过 ssh 连接到 QEMU 实例通过。

1
2
3
4
5
6
7
8
9
10
11
12
Warning: Permanently added '[localhost]:10021' (ECDSA) to the list of known hosts.
Linux syzkaller 5.11.0 #1 SMP Sun Nov 6 20:44:59 CST 2022 x86_64

The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
Unable to get valid context for root
Last login: Mon Jan 23 02:02:21 2023
root@syzkaller:~#

Healer

Healer 是使用 Rust 语言进行编写的,要编译该工具首先要配置 Rust 的编译环境。

Rust 安装

在 Linux 终端中输入下面的命令,然后键入 Enter ↵

1
2
$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
(enter)

安装过程取决于网速和机器,稍等片刻。然后在 Linux 终端中输入下面的命令后,然后键入 Enter ↵ 。 如果你看到了类似下面的输入显示了 “rustc x.xx.x” 的版本号则为安装好了。

1
2
$ rustc --version # check install
rustc 1.65.0 (897e37553 2022-11-02)

请注意提示,如果想要在当前终端中使相关环境变量生效,则你需要运行命令 source "$HOME/.cargo/env" ,然后键入 Enter ↵

编译 Healer

在实验机中的 HEALER 文件夹中打开一个终端,并在终端中输入下面的命令,然后键入 Enter ↵ 。该命令会自动下载 Healer 到 SOME_PATH/HEALER/healer/

1
2
3
4
5
6
7
8
$ git clone https://github.com/SunHao-0/healer.git
Cloning into 'healer'...
remote: Enumerating objects: 4543, done.
remote: Counting objects: 100% (129/129), done.
remote: Compressing objects: 100% (26/26), done.
remote: Total 4543 (delta 109), reused 103 (delta 103), pack-reused 4414
Receiving objects: 100% (4543/4543), 16.16 MiB | 4.33 MiB/s, done.
Resolving deltas: 100% (2422/2422), done.

然后切换到刚才下载的 healer 的所在目录。在当前终端执行下面的命令,然后键入 Enter ↵

1
$ cd healer

然后在当前终端执行下面的命令,然后键入 Enter ↵ 。该命令会开始进行 Healer 的编译。

1
2
3
4
5
6
7
8
9
cargo build --release
Updating crates.io index
Downloaded autocfg v1.0.1
Downloaded num-traits v0.2.14
Downloaded unicode-xid v0.2.2
Downloaded strsim v0.8.0
...
Compiling mutate_prog v0.1.0 (/SOME_PATH/HEALER/Healer/tools/mutate_prog)
Finished release [optimized] target(s) in 3m 54s

当命令执行完毕后, Healer 编译成功。此时,如下所示,在对应 target/release/ 下可以看到编译之后的文件: healer , syz-bin

1
2
3
4
5
6
7
8
$ pwd
/SOME_PATH/HEALER/Healer

$ ls ./target/release
healer ...

$ ls ./target/release/syz-bin
linux_amd64 syz-repro syz-symbolize syz-sysgen

开始 Fuzz Linux Kernel

工作目录准备

开始进行 fuzz 之前,需要确保所有的必须的文件都准备妥当了。建议将这些可执行文件都放到同一个工作目录下,如按照如下的目录进行:

1
2
3
4
5
6
7
$ mkdir -p /SOME_PATH/HEALER/work_dir

$ cd /SOME_PATH/HEALER/work_dir && ls
bin bzImage stretch.id_rsa stretch.img

$ ls ./bin
healer linux_amd64 syz-repro syz-symbolize syz-sysgen

在本次实验中,在完成前面的编译之后,所有必须的文件的情况如下表所示:

文件名 原始位置 新位置
bzImage ..HEALER/linux/arch/x86/boot/bzImage /SOME_PATH/HEALER/work_dir/bzImage
stretch.id_rsa ..HEALER/disk_img/stretch.id_rsa /SOME_PATH/HEALER/work_dir/stretch.id_rsa
stretch.img ..HEALER/disk_img/stretch.img /SOME_PATH/HEALER/work_dir/stretch.img
healer ..HEALER/Healer/target/release/healer /SOME_PATH/HEALER/work_dir/bin/healer
linux_amd64 ..HEALER/Healer/target/release/syz-bin/linux_amd64 /SOME_PATH/HEALER/work_dir/bin/linux_amd64
syz-repro ..HEALER/Healer/target/release/syz-bin/syz-repro /SOME_PATH/HEALER/work_dir/bin/syz-repro
syz-symbolize ..HEALER/Healer/target/release/syz-bin/syz-symbolize /SOME_PATH/HEALER/work_dir/bin/syz-symbolize
syz-sysgen ..HEALER/Healer/target/release/syz-bin/syz-sysgen /SOME_PATH/HEALER/work_dir/bin/syz-sysgen

新打开一个终端,依次输入下面的命令,并逐一键入 Enter ↵ 。通过执行下面的命令可以实现对上表中的相关文件进行复制。注意修改下方 SOME_PATH 为实际路径。

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
# midir /SOME_PATH/HEALER/work_dir
$ mkdir work_dir

# cd && mkdir bin
$ cd /SOME_PATH/HEALER/work_dir && mkdir bin

# bzImage
$ sudo cp /SOME_PATH/HEALER/linux/arch/x86/boot/bzImage /SOME_PATH/HEALER/work_dir/

# stretch.id_rsa
$ sudo cp /SOME_PATH/HEALER/disk_img/stretch.id_rsa /SOME_PATH/HEALER/work_dir/

# stretch.img
$ sudo cp /SOME_PATH/HEALER/disk_img/stretch.img /SOME_PATH/HEALER/work_dir/

# healer
$ sudo cp /SOME_PATH/HEALER/Healer/target/release/healer /SOME_PATH/HEALER/work_dir/bin/

# linux_amd64
$ sudo cp -r /SOME_PATH/HEALER/Healer/target/release/syz-bin/linux_amd64 /SOME_PATH/HEALER/work_dir/bin/

# syz-repro
$ sudo cp /SOME_PATH/HEALER/Healer/target/release/syz-bin/syz-repro /SOME_PATH/HEALER/work_dir/bin/

# syz-symbolize
$ sudo cp /SOME_PATH/HEALER/Healer/target/release/syz-bin/syz-symbolize /SOME_PATH/HEALER/work_dir/bin/

# syz-sysgen
$ sudo cp /SOME_PATH/HEALER/Healer/target/release/syz-bin/syz-sysgen /SOME_PATH/HEALER/work_dir/bin/

执行完毕上述复制之后,执行下面的命令以检查是否所有二进制文件都拷贝到了正确的位置。

1
2
3
4
5
6
7
$ pwd
/SOME_PATH/HEALER/work_dir/
$ ls
bin bzImage stretch.id_rsa stretch.img
$ cd bin
$ ls
healer linux_amd64 syz-repro syz-symbolize syz-sysgen

开始 fuzz

-d path to disk image

-k path to kernel image

—ssh-key path to ssh key

打开一个终端,并切换到工作目录下。在当前终端执行下面的命令,然后键入 Enter ↵

1
$ cd /SOME_PATH/HEALER/work_dir/

在当前终端执行下面的命令,然后键入 Enter ↵ 。该命令将使用 Healer 对刚才编译的 Linux 内核进行 fuzz。

1
$ sudo ./bin/healer -d stretch.img --ssh-key stretch.id_rsa -k bzImage

在执行命令之后, Healer 会在当前终端打印其 banner ,然后开始输入和 fuzz 相关的一些日志信息。这个过程将会一直持续下去,一直对目标进行模糊测试。

当想停止进行 fuzz 时,可以在当前终端中键入: Ctrl + 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
$ sudo ./bin/healer -d stretch.img --ssh-key stretch.id_rsa -k bzImage

___ ___ ______ ________ __ ______ ______
/__/\ /__/\ /_____/\ /_______/\ /_/\ /_____/\ /_____/\
\::\ \\ \ \\::::_\/_\::: _ \ \\:\ \ \::::_\/_\:::_ \ \
\::\/_\ .\ \\:\/___/\\::(_) \ \\:\ \ \:\/___/\\:(_) ) )_
\:: ___::\ \\::___\/_\:: __ \ \\:\ \____\::___\/_\: __ `\ \
\: \ \\::\ \\:\____/\\:.\ \ \ \\:\/___/\\:\____/\\ \ `\ \ \
\__\/ \::\/ \_____\/ \__\/\__\/ \_____\/ \_____\/ \_\/ \_\/

[2023-01-23T02:44:46Z INFO ] loading target linux/amd64...
[2023-01-23T02:44:49Z INFO ] pre-booting one vm...
[2023-01-23T02:44:59Z INFO ] boot cost around 10s
[2023-01-23T02:45:00Z INFO ] detecting features...
[2023-01-23T02:45:00Z INFO ] code coverage : enabled
[2023-01-23T02:45:00Z INFO ] setuid sandbox : enabled
[2023-01-23T02:45:00Z INFO ] Android sandbox : enabled
[2023-01-23T02:45:00Z INFO ] net device setup : enabled
[2023-01-23T02:45:00Z INFO ] pre-setup one executor...
[2023-01-23T02:45:01Z INFO ] ok, fuzzer-0 should be ready
[2023-01-23T02:45:01Z INFO ] fuzzer-0: online
[2023-01-23T02:45:11Z INFO ] exec: 502, fuzz/repro 1/0, uniq/total crashes 0/0, cal/max cover 5835/5836, corpus: 48
[2023-01-23T02:45:13Z INFO ] fuzzer-1: online
[2023-01-23T02:45:18Z INFO ] fuzzer-2: online
[2023-01-23T02:45:21Z INFO ] exec: 999, fuzz/repro 3/0, uniq/total crashes 0/0, cal/max cover 8326/8921, corpus: 96
[2023-01-23T02:45:23Z INFO ] fuzzer-3: online
[2023-01-23T02:45:31Z INFO ] exec: 1089, fuzz/repro 4/0, uniq/total crashes 0/0, cal/max cover 8529/9173, corpus: 102
[2023-01-23T02:45:41Z INFO ] exec: 1501, fuzz/repro 4/0, uniq/total crashes 0/0, cal/max cover 9223/9809, corpus: 138
[2023-01-23T02:45:51Z INFO ] exec: 1512, fuzz/repro 4/0, uniq/total crashes 0/0, cal/max cover 9223/9809, corpus: 138
...

fuzzing

在经过一定的时间之后,终端中输出了一些和 crash 相关的日志。并打印出了执行的系统调用序列。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
[2023-01-23T03:10:01Z INFO ] exec: 70947, fuzz/repro 4/0, uniq/total crashes 0/0, cal/max cover 50366/52583, corpus: 2656
[2023-01-23T03:10:06Z WARN ] fuzzer-1: QEMU not alive, kernel maybe crashed, last executed prog:
r0 = syz_init_net_socket$nl_generic(0x10, 0x3, 0x10)
r1 = syz_genetlink_get_family_id$netlbl_unlabel(&(0x7f0000000000)='NLBL_UNLBL\x00', r0)
sendmsg$NLBL_UNLABEL_C_STATICADDDEF(r0, &(0x7f0000000040)={&(0x7f0000000080)={0x10, 0x0, 0x0, 0x4000}, 0xc, &(0x7f00000000c0)={&(0x7f0000000100)={0x14, r1, 0x20, 0x70bd2d, 0x25dfdbff}, 0x14}, 0x1, 0x0, 0x0, 0x80}, 0x2004c094)
r2 = syz_init_net_socket$nl_generic(0x10, 0x3, 0x10)
r3 = syz_genetlink_get_family_id$netlbl_unlabel(&(0x7f0000000140)='NLBL_UNLBL\x00', r0)
r4 = syz_genetlink_get_family_id$netlbl_mgmt(&(0x7f0000000180)='NLBL_MGMT\x00', r0)
sendmsg$NLBL_MGMT_C_REMOVEDEF(r0, &(0x7f00000001c0)={&(0x7f0000000200)={0x10, 0x0, 0x0, 0x20}, 0xc, &(0x7f0000000240)={&(0x7f0000000280)={0x14, r4, 0x1, 0x70bd25, 0x25dfdbfb, {}, [@NLBL_MGMT_A_CLPDOI={0x8}, @NLBL_MGMT_A_IPV4ADDR={0x8, 0x7, @multicast1}, @NLBL_MGMT_A_FAMILY={0x6, 0xb, 0x1d}, @NLBL_MGMT_A_IPV6ADDR={0x14, 0x5, @private0={0xfc, 0x0, '\x00', 0x1}}, @NLBL_MGMT_A_IPV4MASK={0x8, 0x8, @loopback}, @NLBL_MGMT_A_IPV6ADDR={0x14, 0x5, @private1={0xfc, 0x1, '\x00', 0x1}}, @NLBL_MGMT_A_DOMAIN={0x11, 0x1, 'NLBL_CIPSOv4\x00'}, @NLBL_MGMT_A_CLPDOI={0x8, 0xc, 0x3}]}, 0x14}, 0x1, 0x0, 0x0, 0x20000024}, 0x40004)
r5 = syz_genetlink_get_family_id$netlbl_cipso(&(0x7f00000002c0)='NLBL_CIPSOv4\x00', r0)

[2023-01-23T03:10:11Z INFO ] exec: 70964, fuzz/repro 4/0, uniq/total crashes 0/0, cal/max cover 50366/52583, corpus: 2656
[2023-01-23T03:10:21Z INFO ] exec: 70964, fuzz/repro 4/0, uniq/total crashes 0/1, cal/max cover 50366/52583, corpus: 2656
[2023-01-23T03:10:31Z INFO ] exec: 71115, fuzz/repro 4/0, uniq/total crashes 0/1, cal/max cover 50408/52658, corpus: 2660
[2023-01-23T03:10:36Z WARN ] fuzzer-1: QEMU not alive, kernel maybe crashed, last executed prog:
r0 = syz_init_net_socket$nl_generic(0x10, 0x3, 0x10)
r1 = syz_genetlink_get_family_id$netlbl_unlabel(&(0x7f0000000000)='NLBL_UNLBL\x00', r0)
sendmsg$NLBL_UNLABEL_C_STATICADDDEF(r0, &(0x7f0000000040)={&(0x7f0000000080)={0x10, 0x0, 0x0, 0x4000}, 0xc, &(0x7f00000000c0)={&(0x7f0000000100)={0x14, r1, 0x20, 0x70bd2d, 0x25dfdbff}, 0x14}, 0x1, 0x0, 0x0, 0x80}, 0x2004c094)
r2 = syz_init_net_socket$nl_generic(0x10, 0x3, 0x10)
r3 = syz_genetlink_get_family_id$netlbl_unlabel(&(0x7f0000000140)='NLBL_UNLBL\x00', r0)
r4 = syz_genetlink_get_family_id$netlbl_mgmt(&(0x7f0000000180)='NLBL_MGMT\x00', r0)
sendmsg$NLBL_MGMT_C_REMOVEDEF(r0, &(0x7f00000001c0)={&(0x7f0000000200)={0x10, 0x0, 0x0, 0x20}, 0xc, &(0x7f0000000240)={&(0x7f0000000280)={0x14, r4, 0x1, 0x70bd25, 0x25dfdbfb, {}, [@NLBL_MGMT_A_CLPDOI={0x8}, @NLBL_MGMT_A_IPV4ADDR={0x8, 0x7, @multicast1}, @NLBL_MGMT_A_FAMILY={0x6, 0xb, 0x1d}, @NLBL_MGMT_A_IPV6ADDR={0x14, 0x5, @private0={0xfc, 0x0, '\x00', 0x1}}, @NLBL_MGMT_A_IPV4MASK={0x8, 0x8, @loopback}, @NLBL_MGMT_A_IPV6ADDR={0x14, 0x5, @private1={0xfc, 0x1, '\x00', 0x1}}, @NLBL_MGMT_A_DOMAIN={0x11, 0x1, 'NLBL_CIPSOv4\x00'}, @NLBL_MGMT_A_CLPDOI={0x8, 0xc, 0x3}]}, 0x14}, 0x1, 0x0, 0x0, 0x20000024}, 0x40004)
r5 = syz_genetlink_get_family_id$netlbl_cipso(&(0x7f00000002c0)='NLBL_CIPSOv4\x00', r0)

corpus

在 Healer 的 corpus 中会保存 fuzz 过程中出现 crash 的输入,以便分析人员进行进一步分析,以确定是否是漏洞。

input1

1
2
3
$ cat crash-01.txt
fcntl$getflags(0x0, 0xb)

input2

1
2
3
4
$ cat crash-02.txt
pipe(&(0x7f0000000000)={<r0=>0xffffffffffffffff, <r1=>0xffffffffffffffff})
fcntl$setlease(r0, 0x400, 0x0)

input3

1
2
3
4
5
6
$ cat crash-03.txt
r0 = syz_open_dev$tty1(0xc, 0x4, 0x1)
ioctl$GIO_FONTX(r0, 0x4b6b, &(0x7f0000000000)={0x19e, 0x14, &(0x7f0000000040)=""/1024})
r1 = io_uring_setup(0x6169, &(0x7f0000000440)={0x0, 0xb690, 0x2, 0x0, 0x158})
r4 = syz_io_uring_setup(0x64c5, &(0x7f00000004c0)={0x0, 0xe348, 0x1, 0x0, 0x2fc, 0x0, r1}, &(0x7f0000ffc000/0x1000)=nil, &(0x7f0000ffb000/0x1000)=nil, &(0x7f0000000540)=<r2=>0x0, &(0x7f0000000580)=<r3=>0x0)

input4

1
2
3
4
5
$ cat crash-04.txt
r0 = syz_mount_image$iso9660(&(0x7f0000000000)='iso9660\x00', &(0x7f0000000040)='', 0x1, 0xd8, &(0x7f0000000040)=[{&(0x7f0000000140)="e55ca9626ea5c0d6aaf9f799a44d38ddebf5677b13d1d3f0c9560881bde576e0af6afdc4f8b02de5fb2969c78a9868139052d99e919eff4de6aca6cbfe143f56d24e4e0717881507bdec6656d6469ec6a0348c1e61f2dfe28420762a21dd4271f56b8f84211dd440ec8a0afa1e84c8dfc67c88bb8beaa08844f767586a98e315c5ea00ae6cd8952bd69c961e83043f63a70c4e6049d842431ea18ec8b08c46bd4eda31907bb958311073a563e95dc25a1f9ffbc4269ed56a88ae605d864ac5e2de9ad32555f9d5e3569484a72782bac0705775efa90d35ffcf1f2efb9aab2a57c9ff31768b06ad56e16ff4db106f2360a16bee97e3aa00b02593ec21fca3e0e9c356b0fff64bd1a8a847d16dfe46bd72c63a60ad1c70242c7434986f22e5a8e68207fe6bf4e0b27c8f77159170657a31a36b545470114cec8a9a3364b0c1075b9ccdff52b9bc62881755c40dde474c03859cf3833ec2e48781f1bc86ab728d31ee88d7ef7c6c7916f9c3569a21131009f87844cdf749225fd90a0f7f796d615b5551acfaa5f109814ec7db7c7ba880eb9486a062322571382d7ca956101be3429877cbaee83a669358bde7482eaac532c77840c16a042c764ccf4671b249b9d0588c22e45a13101c399d4cd1a03944a9ccb5eadf9b69fd4a1c79087279941ed5785ed1f86225e3752ab516305d4235da60b12542251654eb159aa1986382088810753b0d882df62576d4fff475be6a16538599abec19a0108c22eb3c3e691cc03984354d6f3851958454c58fa77f457d9772cbde2a27d3fe1d47e20aaaa750c0d85c7059d43364b634a84aae17262d267c0f28cc818e6253c59b4951b3ab3180fd67c8a68491e1c7d3c115cda3373fd74aac00d7610acc524d3565e7dc39b7a451c72f332e53114e5822bf99b7a8741f3b0f4dce59cc15196739037f87d8e91146899bf7e31b60c57641c3ca54f543b80516e489a4396074dead09f0c1b104664e324c7eaf44819247ceaa71c08c547e717adad55223699ba6b923f93454eb690a8872308a8b2cc33f3b3ccc1250cea1c39b13dbb1d9f043356b902b7a4cf9019151e392588a798ffe72b5658e6773bb2db47971766f4da163dd7b2b1aef67bb4a1740ae2f029655c17543b3f8a8fba3c1549ee4f9da0e67f9094e9fc091a83c8347af05d5c842c64e7d2fe3cf1c06d02dd57336ffef9149863033d7cb99b7cb697ed98385331e2f748ba6051bff3cecbd8077917590816bc2eae0886d7ab93958b1ca65056d3fabedd852c490f138f730bc660dba840a9abf81f7a55e9fc97c0013a27bf5cf09e8caf940c9294aa6bc0866e1d6cb64227f2e56ed9c11943ddc150f02280b71a72d8583714ef6b24077b227ff087d48de0c675d2d15e1622f49d6a6c3508fd4ec19efb04ac7d4d1829e9935188744923e519be462363182a4c314aec33a08e7dd5d5739efc2bce8e94f2d72147534b695c5fba8ec250f183f131d16eb89cb4740b97b35472159d48a28c5e6c75445fa86983d2765758dcc54dde4a2117b560a9a3afe45b3d648076f205446def70005a895084c0335264a9c4cc71b09bfec463066047ba0c2a721b17ba12671e63a5639b03623c639d906817066a772cafe57560e153ce9dabb29461fe25b6db6c1285165e81b75b69f52b5439cc90a32e86b7d2f93c057caf1abad2c561b76bf80895f5528ec61159329de0241e0e5512da0aff3b23417dfc20fe5fffa360d387e3f6d3ae98dd1500e830fc9a1dd7ebb1507cf340a54f7b53b68e87c83c56a1ce034f289dab7f2dc41121199d20ea54c69bc7a8efe87f9f2d9d7afa732eb0a300784881325caa5f40a75f8a1294c1d8fbe946564a45e9eb4e22e3687f0b1dac945e4ab85279b5e364b34e9269c9ea573a7fc86d465543a45be514df31ed868f88db3117edb6a934a5f3daf929ac6c024c4c428b1781c19b06969a596d279d11caf3b8a14c523e210187f6050f18e79547b119709090c6cbc19b4f71ec88a98b05ffd77e798270fa11fb47bc7b7acb70c185665ff3adae0cee76cebe5c867fcf60e0e9a4befff62396bf1f6398d4b378131679b5cdd4d94d862ba64f74355a92daa78dac9953aadb315908901714ec29b155f0163f85a5cc077751349b3aec718d25772884ef65d51631d5f0ccce4fa2237536cc7d848cae45e285c58d811c3fe1b575803f6c3ca4504d49f9f490f9a1198836d4632b24b4950108ba980567181199f0f4962fc6b66245b15ff6a1d2e7d5d3c907bc26b1e1f1fc0a89228ee653a5c78bbff80e0b0f74da8c040a6b3a29a25e56cfa3fbdfb55fe84f7afa4ab9e31aafa0ddabaac77b81e32073ea1d41fb7fb56f4387277860a153ab2928059b2386a27e73d4fa2bd69ce071dc9ad68f8e43e62050cb695c51ecfc96a32da38ee3bc1d908806d31092d7412b9c49072217086f36c77f6dd5afc38fc53c5b425ac4a74349b1d216256ea0a7d4748fa581d2a543bd5cb658f60dcf3d22849e763f517239012cb3eee43da0b0345cef9af55e050a8206d97c4009c264202316637040f828c175c22812198928cff51f256ff829636c84070b5b6eed1a40a26628f382a1cc3aab05664c8f001410959c15a3f8ec547f7b6e951097810b8d8c84fbb6d10cd88cfbf1e21ba5899d110cae83a3738d2a761f17a99acf208a14dbba7db962248226df2d86bc68b9203546481411ebdf6dd593c6e56e3ee9cb2528bbbb6cb9fafac815059acdcd344eea285bd5c2b0f82f37595c2f61c4d07a3eae53918eb4a9b4aa1ca301a73e5a84768d12c33dc9aa0aee1fd45336b14240d1ef1cbc89ebe066c4409291409053ced7b72e260caf0852df4aa2e2ed2ff4677efee9ad0ea585e38a987c7b5e51abf7fe7f47110a16022dba024dda1e616a61986ccdbf41657b596a0d3263c2b6f060ae2c258257dbfe5b0cd092fb4d4112ba6e3df27353e351c753a3495ae69d7b0166864771569d23bf3f244f2ba9235d482be71b512fa1c2d1ad619093f9b0b10c5d57238c78d6439f7f9c6c91588d24a9be1063f523f53882965ac5747d5ba58c0e20107e867fab231c2ff469b5a511199f87e0fe477c1237e3ac41dbbbfdc0aa45d5eefa208f89ad635bf6e9f45f6721f89c8f4b53b1b5e251ae10dfd027161956f529c70545b44ea943b59870d4d5856215add0ca4e60e94b6ac908b333fbba7b4caf5b873322db9b780f8ae23c894b8c118ba2b614f81d0e4ed5135c0d1dfb28023ccfbd470a682bd6abe167bc15f21b4910f5b41830726b2cbd799f4285a73d582abb31a4c8a2f3ec723c01de86ba54f508c0e71c332657876984e5e5f2780d10984d20251bf64ce1c3340eef538c3c1d2963cd4211ad8ab5ed591fe43882119c234d1cdb41bc9025312fe021474d0eca2c071edd872a91c18298b80848db533f93ddebb86f01d191d4dfcedd2a3332c8943c7c22229e29515b5df19d751878f42dbcd3e53e11ea85c6bf8f300a29aba850303ff6c75ee57b38e26a8e60bcf990e75f263318be16e4a03026a8f3f8fbeec1454bfc642a76bf1cf0e3915509023f81eabf59a8328d6d222358cbf01e6c99323fa65b5c463cc8b32faf03201f296c8f21945def1de8dc8b8bf4c0a5b7d612cc22426ad79f7f4ee255fb8a2", 0xa1c, 0xfe}, {&(0x7f0000000b80)="c269601c6347bde108f675177b8153f40e93a911a94587ed28d8b94db94870c0630d0d13ea580232319af5", 0x2b}, {&(0x7f0000000bc0)="6f376e741eae53029a7a1502e00015dd6a2048c9d6a5b39d44488dd83f334086e1843dc78ab1f9d1129ccef4fc64bb2285a1e190f9ea6ecfefe26b25fc", 0x3d}, {&(0x7f0000000c00)="3f247115f8bb7c29", 0x8, 0x4548e19cee878de2}, {&(0x7f0000000c40)="2d99181de433187dfe75ec343796331bcac6d72028650cea6ac891a4a4e9ab3294", 0x21, 0x7b1}, {&(0x7f0000000c80)=""}, {&(0x7f0000000c80)="b077687af86cd3bf422e338906ad72e337e88fb22e89a3c4c3a254bdc4013c57cfd5e1108064cfe6321fc972d866000106dae3814078f9689c019739b077c2b2442e910eb0ae03a72ed527da", 0x4c}, {&(0x7f0000000d00)="de22d24c937d0e6cdb6f5c32c10d1afe2d5ff59febc1c963e9febc5237b84001e7b1c4e8a259b071", 0x28, 0x2}, {&(0x7f0000000d40)="7ad4f0b3e417bf4e9efa50c80632b890", 0x10, 0x234c653282dd698c}], 0x800, &(0x7f0000000d80)={[{@hide}, {@map_normal}]})
r1 = socket$isdn(0x22, 0x3, 0x4)
r2 = syz_mount_image$btrfs(&(0x7f0000000dc0)='btrfs\x00', &(0x7f0000000e00)='', 0x0, 0x90, &(0x7f0000000e00)=[{&(0x7f0000000ec0)="6c948164a0b670f1e9886e5886646f6148a9df2298c0d53f7ec20744a49a8d00440c0b7aafcd48abe11e1c615d183252", 0x30}, {&(0x7f0000000f00)="0b504524de704bced6e5285d01c1ca054e2e6b785b95e78b239ca91ba649b0b20b6046b71adcc9f577b01d10c663fd388c8a20f867488aacb1", 0x39, 0xc}, {&(0x7f0000000f40)=""}, {&(0x7f0000000f40)="e26991ba5b3e9a4f1141cbe8568ec3c0087da8dc95873c18b06d3135e3cf0a748f48b2b8aeaf5e4633cb128e3648a6ae1bc16d5f8086860933423be90c02eb796f5171ab6e4ce0b61676d6924ea5b7fb0810bd86f79b3ff70efe0f1cb860b9ee16031cf13bbf1a4db93013403fb98b42cf759b564f5423c7133c9072f565deb9d8515b9fef201df49a66a9fe9731a3889bd9ecbfb4d01a6baefcbf720099687f030e8f0794eb89b0efc8ca72d14710819b40067f5d8300bf958ecf6c3b8374a09431590a18c39e68e7e2d6b0278dd4b1dc8f3d99d3878bb3670a81738cb62b9f550486ef11b53af9a69c1001831f01e1bb395b5571738720a11f14a49c56f80c9a87067d6bb660fba539896ab7443e44dff40032e07265afcd8e5ecded93952b95fe2ed633698057af4c950d5c16a267add25b05a24b683058edd881ce94717e546460333505b99d02f048d1ebeecb538227f75ab351f498bfd780e4c7ea9317fc7bfa9f0064903ec588d339f796b5bb721e9179b2e891dcaa1cb2770991b9b7a777d5f29f98a4da1777b0dfeec351674940840d204207887764ea89113c55f8a606d834d4722e8834356e6637e8c55e95542ff437db65e9a75b91d1a7e0902956eb46cc3d6c1fb54ce9bd649114209cb985860c172ed41c13feba283c1192e0b8216c3ad80768d24301344be12ebaeb123bd4ef0872a8541f52bf7efa6b969c96da9f80749b1dbac1c7e0c8480027dd0c095839498b3119fee1a09b652caba6ea1a79256f01456aa3b454fa21a66a59984cd56ecfc14454b96556a320d12bd8ba198cc65bd35ebbdf8b9b09e745247b8d5f5d16b43c60d31c691a596a5befca32d619165c4d502f6a588b4ae0ecc72e33fa07d900638432231c266bff739d7fe3be54f58fa4374b95b77d5b55faf17ecee9ef5ae1ddb4cd64fa416f697d298fd7259a70a9287d133c3cafc1b6f0591be86d189d9660094525411f0dd288ae0d8a89f5533ff69550313a1c80cea3b4ea70f4ae6a4c409f5bab9f0f22c1e91f66ff15cd1ff1a37657cc9aa6ad6342d5881c7ac87764d133b53b489d9edf22cfaccc468b0ae195c867f283f6b8f2d9e81e28f7e04f3bdf6a880029cac1e0afd62694c0aa95e2d053ad6b73df0ce1332248aaaa6dd42ca3be21912ccd114661e9da7ec0b2b28d79f60e223370cf9bf354497a0d3eedcbcb21b258d064550a0bbd14ffdfbc58e6ecdf119b37f7f11dc61e4e8ec1441a4eb063e53b1c6f6088b8f68b4f1637b6e2c9df2e30a8571f6fe6333644bf6c60a17945e38a6c35381e7f1a9324b5a4d42cf8958525374345621c841a5b8514c229171766c3431de6a4392e5f1d16e2e871b02b37f61bbc7c383cfb5d73b9b90b6c69db1d669ce8d19fc6843656ecfaff39e120ddbe2dfae6ff63b864f8529216cbe611ed7fdf796b2aeb641150b59e1b5963404b13cb7e5ae0e6b7fe41ee1609e0901676953fb5353b48bc5914e11f3201533edaadec6bf2e9e6aa143ca11027f67c1f1f1f7c0021e35bce11a78a2157514a0e2eee0bcabd2f150fe1eda4292845272959afd5e7edc39e943075390c0100687cfc816b5961d87812645318ba3bac96a610d6a60a64a1d9b1e29656788124a73ae6ca798875a530ceb195ba28497416d13a2a5a293046415a0125812fad8cd0c55d563113e648fff08d03e5b32c68589f7f37a2f7c119620dceeef8446e24c1a665054558739be5be88644659c864c135ed3d63b8ee8456db39c3cb7a71fbb9115302c8d6ba91053bd2a1b2de575d5537d29d38522a3cc8122d4dbb9be7ea567db2460a439827c70f19442c5250703f75c80211f33ffec21f818e59a25746b5881430052caf2cf70650bebd8202df0fd87a67ec6d24a615dca93938abebcefe2ccda9771f3696c2ef71ed62e6c844f0a202bce6f2724b89b1aa0d2fe576a9ce64edae2a722961d2af4e5cb2ee88b8b2a69599cbc0e3c6569f7ebae9fb2ec66a457ad89453121cc36eb8ebec84bb5648a17778b759487367b25949afa88a09f61df108004f8426edbf426d167e87ab14bc4a16236981e89a265475764d17d98940920eb603c56742b216831822b0549ab972b140d9f28261e18717d50527d2d72d70559a5aff54fa2a39af5ba863716816519ee40713e6c2d109b165eea6c2415bf213e99ed65c7a991a4bf5ab8ce647ed84bf140ecc854b9047e4", 0x621, 0xf8fcc2a4e31256dd}, {&(0x7f0000001580)="06512e7f8c431f55cc984d2e3b97a09318b9370bd5e0296ca77125ffaa4107d41b3e7df0db1d6aee372bbd02f6af18ba52e47df0384cf1ea5a98ffbc21fa2d8ff08ebb6fc814d42742e169a48b533e065fca0c41a8f8c87bbc9c55578a51d99d0b09a4f8a802f0662b2c541ff48f5d8b18131d403b184b8b8c61e37e1337c2", 0x7f, 0xb}, {0x0, 0x0, 0xf}], 0x2000032, &(0x7f0000001600)={[{@subvolid}, {@nodatacow}, {@max_inline={'max_inline', 0x3d, [0x6b, 0x78, 0x34, 0x35]}}, {@enospc_debug}, {}, {@compress_force_algo={'compress-force', 0x3d, 'lzo'}}], [{@context={'context', 0x3d, 'staff_u'}}, {@fsname={'fsname', 0x3d, '/dev/vcsa#\x00'}}]})

0x03 核心代码

本部分将对 HEALER 的部分代码进行简要分析。本部分代码以 2022/02/23 2efbb44 为样例。

功能性代码

minimize

工具接口位置 healer/tools/minimize/src/main.rs

1
2
3
4
5
6
7
8
9
// healer/tools/minimize/src/main.rs: line 1
use healer_core::gen::{self, minimize};

// line 61
minimize(&target, &mut p, idx, |p, idx| {
println!("> current prog:\n{}", p.display(&target));
println!("current idx: {}", idx);
read_line().to_ascii_lowercase().trim() == "y"
});

代码实现位置 healer/healer_core/src/gen/mod.rs

实现上和文章中提到的一致,通过从最后一个系统调用往前枚举(line 11) 然后逐一移除前面的系统调用(line 19) ,判断是否发生覆盖率改变来确定系统调用是否可以移除(line 20)。

fixup (line 27) 定义在 healer/healer_core/src/mutation/mod.rs: 182 用于修正移除之后的一些地址值和重定向等。

/// Fixup the addr of ptr value, re-order res id and re-collect generated res and used res of calls.

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
// healer/healer_core/src/gen/mod.rs: line 194
pub fn minimize(
target: &Target,
p: &mut Prog,
mut idx: usize,
mut pred: impl FnMut(&Prog, usize) -> bool,
) -> usize {
debug_assert!(!p.calls.is_empty());
debug_assert!(idx < p.calls.len());

for i in (0..p.calls.len()).rev() {
if i == idx {
continue;
}
let mut new_idx = idx;
if i < idx {
new_idx -= 1;
}
let new_p = p.remove_call(i);
if !pred(&new_p, new_idx) {
continue;
}
*p = new_p;
idx = new_idx;
}

fixup(target, p.calls_mut());

// TODO Add args minimization

idx
}

generate

mod.rs

代码实现在 healer/healer_core/src/gen/mod.rs

该代码定义了和生成相关的功能和函数。

next_prog_len

该函数负责给出下一个 prog 中的系统调用的个数,即序列的长度。

获取上一次设定的长度作为当前 prog 的长度(line 4),然后获取 prog 的长度范围之后,尝试设置下一个 prog 的长度为比当前长 1,如果其长度超过了 prog 的范围则设置下一个 prog 长度为允许的最短长度。

即每次获取的长度在 prog 允许的范围中进行循环遍历。

1
2
3
4
5
6
7
8
9
10
11
12
13
// healer/healer_core/src/gen/mod.rs: line 63
fn next_prog_len() -> usize {
NEXT_PROG_LEN.with(|next_len| {
let len = next_len.get();
let r = prog_len_range();
let mut new_len = len + 1;
if new_len >= r.end {
new_len = FAVORED_MIN_PROG_LEN
};
next_len.set(new_len);
len
})
}

prog 允许的长度范围为手动指定,硬编码在代码中的(line 2)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// healer/healer_core/src/gen/mod.rs: line 37
pub const FAVORED_MIN_PROG_LEN: usize = 16;
pub const FAVORED_MAX_PROG_LEN: usize = 25;

thread_local! {
static NEXT_PROG_LEN: Cell<usize> = Cell::new(FAVORED_MIN_PROG_LEN);
static PROG_LEN_RANGE: Cell<(usize, usize)> = Cell::new((FAVORED_MIN_PROG_LEN, FAVORED_MAX_PROG_LEN));
}

//...
// healer/healer_core/src/gen/mod.rs: line 52
/// Get current prog length range
#[inline]
pub fn prog_len_range() -> Range<usize> {
PROG_LEN_RANGE.with(|r| {
let v = r.get();
Range {
start: v.0,
end: v.1,
}
})
}

Generate Prog

负责从学习到的关系中不断随机选择一个系统调用(line 18)插入到当前上下文的输入序列中(line 50),直到系统调用数量达到允许的最大值(line 7)。

而在函数 gen_one_call 中负责对一个实际的系统调用进行构造和设计。通过传入的系统调用序号(sid)来选择这个系统调用(line 29),然后获取其对应的参数(line 30),然后枚举其所有的参数,并将对应的参数设置好初始值(line 31)。

代码实现位置 healer/healer_core/src/gen/mod.rs: line 76

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
// healer/healer_core/src/gen/mod.rs: line 76
/// Generate prog based on `target` and `relation`.
pub fn gen_prog(target: &Target, relation: &RelationWrapper, rng: &mut RngType) -> Prog {
let mut ctx = Context::new(target, relation);
let len = next_prog_len();
debug_info!("prog len: {}", len);
while ctx.calls().len() < len {
gen_call(&mut ctx, rng);
}
debug_info!("Context:\n{}", ctx);
fixup(target, &mut ctx.calls);
ctx.to_prog()
}

/// Add a syscall to `context`.
#[inline]
pub fn gen_call(ctx: &mut Context, rng: &mut RngType) {
let sid = select(ctx, rng);
gen_one_call(ctx, rng, sid)
}

//...

// healer/healer_core/src/gen/mod.rs: line 119
/// Generate syscall `sid` to `context`.
pub fn gen_one_call(ctx: &mut Context, rng: &mut RngType, sid: SyscallId) {
push_builder(sid);
debug_info!("generating: {}", ctx.target().syscall_of(sid));
let syscall = ctx.target().syscall_of(sid);
let mut args = Vec::with_capacity(syscall.params().len());
for param in syscall.params() {
args.push(gen_ty_value(
ctx,
rng,
param.ty(),
param.dir().unwrap_or(Dir::In),
));
}
if need_calculate_len() {
calculate_len_args(ctx.target(), syscall.params(), &mut args);
len_calculated();
}
let ret = syscall.ret().map(|ty| {
assert!(!ty.optional());
gen_ty_value(ctx, rng, ty, Dir::Out)
});

let mut builder = pop_builder();
builder.args(args).ret(ret);
ctx.append_call(builder.build());
}

gen_ty_value

该函数是一个负责根据对应的类型生成相应的值的函数接口,负责根据传入的类型调用其他模块中实现的函数来处理数据。

代码实现位置 healer/healer_core/src/gen/mod.rs: line 165

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
// healer/healer_core/src/gen/mod.rs: line 146
pub type Generator = fn(&mut Context, &mut RngType, &Type, Dir) -> Value;
pub const GENERATOR: [Generator; 15] = [
gen_res,
gen_const,
gen_int,
gen_flags,
gen_len,
gen_proc,
gen_csum,
gen_vma,
gen_buffer_blob,
gen_buffer_string,
gen_buffer_filename,
gen_array,
gen_ptr,
gen_struct,
gen_union,
];

// healer/healer_core/src/gen/mod.rs: line 165
pub fn gen_ty_value(ctx: &mut Context, rng: &mut RngType, ty: &Type, dir: Dir) -> Value {
use TypeKind::*;

if dir == Dir::Out && matches!(ty.kind(), Const | Int | Flags | Proc | Vma) {
ty.default_value(dir)
} else if ty.optional() && rng.gen_ratio(1, 5) {
if let Some(ty) = ty.as_res() {
let v = ty.special_vals().choose(rng).copied().unwrap_or(0);
return ResValue::new_null(ty.id(), dir, v).into();
}
ty.default_value(dir)
} else {
GENERATOR[ty.kind() as usize](ctx, rng, ty, dir)
}
}

147 行的 GENERATOR 则是对其他模块实现的函数进行封装的函数数组,对应各种类型的实现都在同目录下(healer/healer_core/src/gen/),这里不再赘述。

mutation

代码实现位置 healer/healer_core/src/mutation/ ,该文件夹中多个文件分别对不同场景下的变异进行了实现。

mod.rs

该代码实现在 healer/healer_core/src/mutation/mod.rs

这个代码中定义了函数 mutate (healer/healer_core/src/mutation/mod.rs: 29) 。

在这个函数中定义了4种变异操作:insert_calls , mutate_call_args , splice , remove_call (line 11)。

具体的变异规则为:

  1. 最多尝试连续变异 128次,同时必须满足下面条件之一(line 21):
    • 没有变异成功过
    • 或者一个系统调用都没有(有可能一直在移除,把系统调用移除完了那也不行
    • 或者满足一个实现设定的概率会继续变异
  2. 进行变异时,按照设定的权重从四种变异方式中选择一种(line 22)。
  3. 如果变异之后系统调用数量太多了,超过了当前设定的长度,就会直接移除多余的系统调用。
  4. 最后进行一些收尾工作,释放空间、修复参数等。

根据设定的权重从四种变异中选择一种。

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
// healer/healer_core/src/mutation/mod.rs: line 29
/// Muate input prog `p`
pub fn mutate(
target: &Target,
relation: &RelationWrapper,
corpus: &CorpusWrapper,
rng: &mut RngType,
p: &mut Prog,
) -> bool {
type MutateOperation = fn(&mut Context, &CorpusWrapper, &mut RngType) -> bool;
const OPERATIONS: [MutateOperation; 4] = [insert_calls, mutate_call_args, splice, remove_call];
const WEIGHTS: [u64; 4] = [400, 980, 999, 1000];

let calls = std::mem::take(&mut p.calls);
let mut ctx = Context::new(target, relation);
restore_partial_ctx(&mut ctx, &calls);
ctx.calls = calls;

let mut mutated = false;
let mut tries = 0;
while tries < 128 && (!mutated || ctx.calls.is_empty() || rng.gen_ratio(1, 2)) {
let idx = choose_weighted(rng, &WEIGHTS);
debug_info!("using strategy-{}", idx);
mutated = OPERATIONS[idx](&mut ctx, corpus, rng);

if ctx.calls.len() >= prog_len_range().end {
remove_extra_calls(&mut ctx);
}

// clear mem usage, fixup later
ctx.mem_allocator.restore();
// clear newly added res
ctx.res_ids.clear();
ctx.res_kinds.clear();

tries += 1;
}

if mutated {
fixup(ctx.target, &mut ctx.calls);
}
*p = ctx.to_prog();

mutated
}
seq.rs

该代码实现在 healer/healer_core/src/mutation/seq.rs

实现了三种变异操作: insert_calls(line 37) , splice(line 16) , remove_call(line 59)

insert_calls

负责随机插入一些系统调用进去。

  1. 如果系统调用的长度已经超过了当前要的系统调用长度则直接退出插入变异,变异失败(line 4)。
  2. 在已有的系统调用中随机选择一个位置(line 8)。
  3. 然后调用 gen_one_call 生成变异后的系统调用序列。

代码实现在: healer/healer_core/src/mutation/seq.rs: line 37

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// healer/healer_core/src/mutation/seq.rs: line 37
/// Insert calls to random location of ctx's calls.
pub fn insert_calls(ctx: &mut Context, _corpus: &CorpusWrapper, rng: &mut RngType) -> bool {
if ctx.calls.len() > prog_len_range().end {
return false;
}

let idx = rng.gen_range(0..=ctx.calls.len());
restore_res_ctx(ctx, idx); // restore the resource information before call `idx`
let sid = select_call_to(ctx, rng, idx);
debug_info!(
"insert_calls: inserting {} to location {}",
ctx.target.syscall_of(sid).name(),
idx
);
let mut calls_backup = std::mem::take(&mut ctx.calls);
gen_one_call(ctx, rng, sid);
let new_calls = std::mem::take(&mut ctx.calls);
debug_info!("insert_calls: {} call(s) inserted", new_calls.len());
calls_backup.splice(idx..idx, new_calls);
ctx.calls = calls_backup;
true
}

splice

负责从 corpus 中随机选择一个,然后进行分割后与当前的系统调用进行拼接。

  1. 如果当前没有系统调用或者系统调用长度超过了最大长度或者种子库为空,则这种拼接变异失败(line 4)。
  2. 然后随机从种子库中选择一个(line 8),然后随机选择一个位置(line 13)进行拼接(line 19)。

代码实现在: healer/healer_core/src/mutation/seq.rs: line 16

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// healer/healer_core/src/mutation/seq.rs: line 16
/// Select a prog from `corpus` and splice it with calls in the `ctx` randomly.
pub fn splice(ctx: &mut Context, corpus: &CorpusWrapper, rng: &mut RngType) -> bool {
if ctx.calls.is_empty() || ctx.calls.len() > prog_len_range().end || corpus.is_empty() {
return false;
}

let p = corpus.select_one(rng).unwrap();
let mut calls = p.calls;
// mapping resource id of `calls`, continue with current `ctx.next_res_id`
mapping_res_id(ctx, &mut calls);
restore_partial_ctx(ctx, &calls);
let idx = rng.gen_range(0..=ctx.calls.len());
debug_info!(
"splice: splicing {} call(s) to location {}",
calls.len(),
idx
);
ctx.calls.splice(idx..idx, calls);
true
}

remove_call

该部分负责将从系统调用序列中随机一个位置移除后面的系统调用。

  1. 如果当前一个系统调用都没有,则直接结束,变异失败 (line 3)。
  2. 否则就从系统调用序列中随机选择一个位置(line 7),然后移除后面的系统调用(line 11)。

代码实现在: healer/healer_core/src/mutation/seq.rs: line 59

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// healer/healer_core/src/mutation/seq.rs: line 59
pub fn remove_call(ctx: &mut Context, _corpus: &CorpusWrapper, rng: &mut RngType) -> bool {
if ctx.calls.is_empty() {
return false;
}

let idx = rng.gen_range(0..ctx.calls.len());
let calls = std::mem::take(&mut ctx.calls);
let mut p = Prog::new(calls);
debug_info!("remove_call: removing call-{}", idx);
p.remove_call_inplace(idx);
ctx.calls = p.calls;
true
}
call.rs

该代码实现在 healer/healer_core/src/mutation/call.rs

实现了变异操作: mutate_call_args(line 25)

mutate_call_args

该部分负责选择一个系统调用,然后随机变异其参数。

  1. 首先如果系统调用序列为空,则直接变异失败(line 4)。
  2. 然后调用 do_mutate_call_args 尝试判断是否有可进行随机变异的参数(line 8)。
  3. 否则变异失败,所以的系统调用都没有可以变异的参数。(line 10)

代码实现在: healer/healer_core/src/mutation/call.rs: line 25

1
2
3
4
5
6
7
8
9
10
11
12
13
// healer/healer_core/src/mutation/call.rs: line 25
/// Select a call and mutate its args randomly.
pub fn mutate_call_args(ctx: &mut Context, _corpus: &CorpusWrapper, rng: &mut RngType) -> bool {
if ctx.calls.is_empty() {
return false;
}
if let Some(idx) = select_call(ctx, rng) {
do_mutate_call_args(ctx, rng, idx)
} else {
debug_info!("mutate_call_args: all calls do not have mutable parameters");
false
}
}

do_mutate_call_args

该部分是真正对某个系统调用进行参数变异的函数实现。

  1. 最多尝试变异 128 次,同时要么还没有变异成功,要么以一个设定的概率进行再变异(line 8)。
  2. 然后会随机去选择这个系统调用的随机一个参数,选择失败则变异失败(line 14)。
  3. 如果当前这个参数是 const 则没法变异,于是 continue 等下重新挑选一个(line 19)。
  4. 然后记录一些用于 backup 的东西,开始对选中的值调用 mutate_value 进行变异(line 26)。
  5. 如果变异完发现系统调用序列不为空才对上下文进行覆写(line 29)。

代码实现在: healer/healer_core/src/mutation/call.rs: line 38

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
// healer/healer_core/src/mutation/call.rs: line 38
/// Mutate the `idx` call in `ctx` based on value prio.
fn do_mutate_call_args(ctx: &mut Context, rng: &mut RngType, mut idx: usize) -> bool {
let mut tries = 0;
let mut mutated = false;
let mut visited = HashSet::new();

while tries < 128 && (!mutated || rng.gen_ratio(1, 2)) {
// restore
ctx.res_ids.clear();
ctx.res_kinds.clear();
ctx.mem_allocator.restore();

let arg = if let Some(arg) = select_arg(ctx, rng, idx) {
arg
} else {
return false;
};
if !visited.insert(arg as *const _) && rng.gen_ratio(9, 10) {
continue;
}

restore_res_ctx(ctx, idx);
record_call_res(&ctx.calls[idx]);
let mut calls_backup = std::mem::take(&mut ctx.calls);
mutated = mutate_value(ctx, rng, arg);
restore_call_res(&mut calls_backup[idx]);

if !ctx.calls.is_empty() {
let new_calls = std::mem::take(&mut ctx.calls);
let new_idx = idx + new_calls.len();
calls_backup.splice(idx..idx, new_calls);
idx = new_idx;
}
ctx.calls = calls_backup;

tries += 1;
}

if mutated {
calculate_len(ctx.target, &mut ctx.calls[idx]);
}

mutated
}

mutate_value

这个函数只是一个接口,负责根据传入的参数的类型选择对应的变异函数来执行。

1
2
3
4
5
6
// healer/healer_core/src/mutation/call.rs: line 171
/// Mutate the given value
pub fn mutate_value(ctx: &mut Context, rng: &mut RngType, val: &mut Value) -> bool {
let ty = val.ty(ctx.target);
TYPE_MUTATORS[ty.kind() as usize](ctx, rng, val)
}

而在这个文件的上前方则定义了一个函数数组用于被调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// healer/healer_core/src/mutation/call.rs: line 151
type TypeMutator = fn(&mut Context, &mut RngType, &mut Value) -> bool;
const TYPE_MUTATORS: [TypeMutator; 15] = [
mutate_res,
mutate_const,
mutate_int,
mutate_flags,
mutate_len,
mutate_proc,
mutate_csum,
mutate_vma,
mutate_buffer_blob,
mutate_buffer_string,
mutate_buffer_filename,
mutate_array,
mutate_ptr,
mutate_struct,
mutate_union,
];

这些函数的实现都可以在同文件夹中找到(healer/healer_core/src/mutation/),这里不再一一列举。

例如函数 mutate_ptr 实现位置为 healer/healer_core/src/mutation/ptr.rs: line 13

ty

代码实现位置为: healer/healer_core/src/ty/

该部分实现的是从 Syzlang 类型系统的 AST 相关的代码。

healer/healer_core/src/ty/mod.rs 开头有如下注释:

1
2
3
4
5
6
7
// healer/healer_core/src/ty/mod.rs: line 1
//! AST of Syzlang type system
//!
//! Each type in syzlang has an corresponding type, while the top level `Type` wraps all of them.
//! Every type is constructed via builder pattern.
//! Everything is immutable so that value and type are always consistent.

我估计是处理 Syzlang 的模板来获取各种参数类型等用于引导后续生成系统调用构建参数、返回值等。

relation

代码实现位置: healer/healer_core/src/relation.rs

该部分是本文计算系统调用之间是否存在影响关系的相关代码。

使用了一个结构体来维护每个系统调用能影响的系统调用和能被那些系统调用影响。

1
2
3
4
5
6
7
8
// healer/healer_core/src/relation.rs: line 50
/// Influence relations between syscalls.
#[derive(Debug, Clone)]
pub struct Relation {
influence: HashMap<SyscallId, Vec<SyscallId>>,
influence_by: HashMap<SyscallId, Vec<SyscallId>>,
n: usize,
}

接口 Relation 提供了相关的方法,并且代码中含有丰富的注释,所以应该是非常容易看懂的。

构造函数会通过目标初始化相关的信息确定当前系统调用能够影响和被影响的系统调用。

初始时,Static Learning 将会根据 Syzlang 描述所提供的信息来初始化 Relation Table。其中,参数类型和返回值类型对静态分析至关重要。

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
// healer/healer_core/src/relation.rs: line 58

/// Create initial relations based on syscall type information.
pub fn new(target: &Target) -> Self {
let influence: HashMap<SyscallId, Vec<SyscallId>> = target
.enabled_syscalls()
.iter()
.map(|syscall| (syscall.id(), Vec::new()))
.collect();
let influence_by = influence.clone();
let mut r = Relation {
influence,
influence_by,
n: 0,
};

for i in target.enabled_syscalls().iter().map(|s| s.id()) {
for j in target.enabled_syscalls().iter().map(|s| s.id()) {
if i != j && Self::calculate_influence(target, i, j) {
r.push_ordered(i, j);
}
}
}

r
}

calculate_influence

该函数用于计算两个系统调用是否有影响关系,实现位置在 healer/healer_core/src/relation.rs: line 93

动态学习可以使用 syzlang 无法表达的信息来更好的更新和细化关系表,以便于生成更高质量的测试用例。

如何判断影响关系已经写在注释中了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// healer/healer_core/src/relation.rs: line 93
/// Calculate if syscall `a` can influence the execution of syscall `b` based on
/// input/output resources.
///
/// Syscall `a` can influcen syscall `b` when any resource output by `a` is subtype
/// of resources input by `b`. For example, syscall `a` outputs resource `sock`, syscall
/// `b` takes resource `fd` as input, then `a` can influence `b`, because `sock` is
/// subtype of `fd`. In contrast, if `b` takes `sock_ax25` as input, then the above
/// conlusion maybe wrong (return false), because `sock` is not subtype of `sock_ax25` and
/// the output resource of `a` maybe useless for `b`. For the latter case, the relation
/// should be judged with dynamic method.
pub fn calculate_influence(target: &Target, a: SyscallId, b: SyscallId) -> bool {
let output_res_a = target.syscall_output_res(a);
let input_res_b = target.syscall_input_res(b);

!output_res_a.is_empty()
&& !input_res_b.is_empty()
&& input_res_b.iter().any(|input_res| {
output_res_a
.iter()
.any(|output_res| target.res_sub_tys(input_res).contains(output_res))
})
}

try_update

现在尝试通过移除来确定两个系统调用之间的关系,实现位置在 healer/healer_core/src/relation.rs: line 115

如何尝试移除已经写在注释中了。

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
// healer/healer_core/src/relation.rs: line 115
/// Detect relations by removing calls dynamically.
///
/// The algorithm removes call before call `idx `of `p` and calls the callback
/// `changed` to verify if the removal changed the feedback of adjacent call.
/// For example, for prog [open, read], the algorithm removes `open` first and calls `changed`
/// with the index of `open` (0 in this case) and the `new_prog`. The index of `open` equals to
/// the index of `read` in the new `prog` and the callback `changed` should judge the feedback
/// changes of the `index` call after the execution of `new_prog`. Finally, `try_update` returns
/// the number of detected new relations.
pub fn try_update<T>(&mut self, p: &Prog, idx: usize, mut pred: T) -> bool
where
T: FnMut(&Prog, usize) -> bool, // fn(new_prog: &Prog, index: usize) -> bool
{
let mut found_new = false;
if idx == 0 {
return found_new;
}
let a = &p.calls[idx - 1];
let b = &p.calls[idx];
if !self.influence(a.sid(), b.sid()) {
let new_p = p.remove_call(idx - 1);
if pred(&new_p, idx - 1) {
self.push_ordered(a.sid(), b.sid());
found_new = true;
}
}

found_new
}

fuzz 流程代码

healer/healer_fuzzer 中实现了 healer 其 fuzzer 的部分。

main

fuzzer 的主入口文件是 healer/headeler_fuzzer/src/main.rs 在这个主程序中实现了 main() 函数,该文件主要负责从终端中获取与 fuzz 过程相关的参数,一些可选的参数则通过使用默认值来填充。在所有参数填充到 config 中之后,在 line 129 处通过调用函数 boot(config) 启动了 fuzz 。(boot 的实现位置为: healer/healer_fuzzer/src/lib.rs: line 57

boot

进入该函数后会对传入的 config 进行合法性检测,检测通过后,则会打印 “HEALER” 的 banner。

1
2
3
4
5
// healer/healer_fuzzer/src/lib.rs: line 57
pub fn boot(mut config: Config) -> anyhow::Result<()> {
config.check().context("config error")?;
println!("{}", HEALER);
// ...

0x04 参考链接

Healer

【Linux学习笔记】debootstrap的使用技巧