Syzkaller学习笔记
Syzkaller 安装
先安装软件
sudo apt-get install debootstrap
sudo apt install qemu-kvm
sudo apt-get install subversion
sudo apt-get install git
sudo apt-get install make
sudo apt-get install qemu
sudo apt install libssl-dev libelf-dev
sudo apt-get install flex bison libc6-dev libc6-dev-i386 linux-libc-dev linux-libc-dev:i386 libgmp3-dev libmpfr-dev libmpc-dev
apt-get install g++
apt-get install build-essential
apt install gcc
安装go
add-apt-repository ppa:longsleep/golang-backports
apt-get update
sudo apt-get install golang-go
//go的版本为1.19
然后设置goproxy
go env -w GOPROXY=https://mirrors.aliyun.com/goproxy/,direct
然后go get源代码
go get -u -d github.com/google/syzkaller/prog
进入后进行编译
发现报错,
dmesg | egrep -i -B100 'killed process'
执行命令 发现 OOM-Killer
重新分配,16G,编译成功
当然也可以 建立swap分区
https://studygolang.com/articles/11781?fr=sidebar
编译完成
文件系统
我们新建一个 image文件夹,下载create-image.sh 但是
https://raw.githubusercontent.com/google/syzkaller/master/tools/create-image.sh
网络问题,我们手动下载
# 安装debootstrap
sudo apt install debootstrap
# 下载脚本
wget https://raw.githubusercontent.com/google/syzkaller/master/tools/create-image.sh -O create-image.sh
# 添加可执行权限
chmod +x create-image.sh
# 使用清华源,不然慢死了
sed -i -e 's~sudo debootstrap .*~\0 https://mirrors.tuna.tsinghua.edu.cn/debian/~' create-image.sh
# 制作镜像,1024MB
./create-image.sh -s 1024
执行会有报错
由于windows系统下换行符为 \r\n,linux下换行符为 \n,所以导致在windows下编写的文件会比linux下多回车符号 \r。
只需要去掉多余的 \r 回车符 即可。操作办法可以用sed命令进行全局替换
sed 's/\r//' -i gen_cert.sh
内核
https://mirrors.edge.kernel.org/pub/linux/kernel
手动下载 或者wget
# 先采用默认配置
make defconfig
# 启用kvm
make kvmconfig
# Syzkaller需要启用一些调试功能
echo '
CONFIG_KCOV=y
CONFIG_DEBUG_INFO=y
CONFIG_KASAN=y
CONFIG_KASAN_INLINE=y
CONFIG_CONFIGFS_FS=y
CONFIG_SECURITYFS=y' >> .config
# 再次对新引入的配置采用默认值
make olddefconfig
使用qemu进入测试,成功
root 密码空
qemu-system-x86_64 -m 1G \
-enable-kvm \
-drive file=/home/test/go/src/github.com/google/syzkaller/image/stretch.img,format=raw \
-kernel ./linux-4.4.146/arch/x86/boot/bzImage \
-append root=/dev/sda
我们在syzkaller 中生成我们的cfg文件
{
"target": "linux/amd64",
"http": "0.0.0.0:8080",
"workdir": "/home/test/go/src/github.com/google/syzkaller/bin/workdir",
"kernel_obj": "/home/test/桌面/cheche/kernel/linux-4.4.146/",
"image": "../image/stretch.img",
"sshkey": "../image/stretch.id_rsa",
"syzkaller": "/home/test/go/src/github.com/google/syzkaller",
"enable_syscalls": ["chmod"],
"procs": 1,
"type": "qemu",
"vm": {
"count": 1,
"kernel": "/home/test/桌面/cheche/kernel/linux-4.4.146/arch/x86/boot/bzImage",
"cpu": 1,
"mem": 1024
}
}
./syz-manager -config 4.14.cfg -vv 10
config 字段的解释
https://github.com/google/syzkaller/blob/master/pkg/mgrconfig/config.go
Android common kernel
https://android.googlesource.com/kernel/common/
挂代理 git 所有后 才能进行查看
git log --all | grep 搜索
git log查看的不全
我们搜索到之后
精确下载 某个版本
proxychains git clone -b ASB-2018-08-05_4.4 https://android.googlesource.com/kernel/common
安装多次之后终于下载成功
参考文献
赛兹卡勒/setup_ubuntu-host_qemu-vm_x86-64-kernel.md at 大师 ·谷歌/Syzkaller ·GitHub
syzkaller/setup.md at master · google/syzkaller · GitHub
https://bbs.kanxue.com/thread-265405.htm#%E5%B0%9D%E8%AF%95%E4%BB%8E0%E5%88%B01%E5%BC%80%E5%A7%8B%E4%BD%BF%E7%94%A8syzkaller%E8%BF%9B%E8%A1%8Clinux%E5%86%85%E6%A0%B8%E6%BC%8F%E6%B4%9E%E6%8C%96%E6%8E%98
https://snappyjack.github.io/articles/2020-05/%E4%BD%BF%E7%94%A8Syzkaller%E8%BF%9B%E8%A1%8C%E5%86%85%E6%A0%B8fuzz
https://i-m.dev/posts/20200313-143737.html
https://blingblingxuanxuan.github.io/2019/10/26/syzkaller/
ARM syzkaller
http://wanjiabing.top/posts/zh/kerneldebug/syzkaller/
安卓模拟
https://www.owalle.com/2020/05/11/android-emulator/
http://pwn4.fun/2019/10/29/Syzkaller-Fuzz-Android-Kernel/
https://source.android.com/docs/core/tests/debug/kasan-kcov?hl=zh-cnzsy
syzkaller 源码阅读笔记-1
前言
syzkaller 是 google 开源的一款无监督覆盖率引导的 kernel fuzzer,支持包括 Linux、Windows 等操作系统的测试。
syzkaller 有很多个部件。其中:
- syz-extract:用于解析 syzlang 中的常量
- syz-sysgen:用于解析 syzlang,提取其中描述的 syscall 和参数类型,以及参数依赖关系
- syz-manager:用于启动与管理 syzkaller
- syz-fuzzer:实际在 VM 中运行的 fuzzer
- syz-executor:实际在 VM 中运行的测试程序
commit 14a312c837f1ebfece99a5cac64d37eba33654af
功能总结:编译系统调用模板的原理,可以理解成syzkaller实现了一种描述系统调用的小型的编程语言。
syz-extract
:根据 syzlang 文件从内核源文件中提取出使用的对应的宏、系统调用号等的值,生成.const
文件(例如,xxx.txt.const
)。syz-sysgen
:通过 syzlang 文件与 .const 文件进行,语法分析与语义分析,生成抽象语法树,最终生成供 syzkaller 使用的 golang 代码,分为如下四个步骤:- assignSyscallNumbers:分配系统调用号,检测不支持的系统调用并丢弃;
- patchConsts:将 AST 中的常量替换为对应的值;
- check:进行语义分析;
- genSyscalls:从 AST 生成 prog 对象。
syz-extract
用途:解析并获取 syzlang 文件中的常量所对应的具体整型,并将结果存放至 xxx.txt.const 文件中
syz-extract main 函数位于 sys/syz-extract/extract.go
中。
开头导入了一些包,我们 暂且不看
import (
"bytes"
"flag"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"runtime"
"sort"
"strings"
"github.com/google/syzkaller/pkg/ast"
"github.com/google/syzkaller/pkg/compiler"
"github.com/google/syzkaller/pkg/osutil"
"github.com/google/syzkaller/pkg/tool"
"github.com/google/syzkaller/sys/targets"
)
main
在main函数中
func main() {
flag.Parse()
if *flagBuild && *flagBuildDir != "" {
tool.Failf("-build and -builddir is an invalid combination")
}
syz-extract 会尝试解析传入的参数,也就是flag
直接定义了我们的参数 和他的提示信息以及默认值
var (
flagOS = flag.String("os", runtime.GOOS, "target OS")
flagBuild = flag.Bool("build", false, "regenerate arch-specific kernel headers")
flagSourceDir = flag.String("sourcedir", "", "path to kernel source checkout dir")
flagIncludes = flag.String("includedirs", "", "path to other kernel source include dirs separated by commas")
flagBuildDir = flag.String("builddir", "", "path to kernel build dir")
flagArch = flag.String("arch", "", "comma-separated list of arches to generate (all by default)")
)
-
flagOS:是一个字符串类型的变量,默认值是当前系统的操作系统(runtime.GOOS)。它定义了命令行参数 “os”,表示目标操作系统。
-
flagBuild:是一个布尔类型的变量,默认值是 false。它定义了命令行参数 “build”,表示是否重新生成特定架构的内核头文件。
-
flagSourceDir:是一个字符串类型的变量,默认值是空字符串。它定义了命令行参数 “sourcedir”,表示内核源代码的存储路径。
-
flagIncludes:是一个字符串类型的变量,默认值是空字符串。它定义了命令行参数 “includedirs”,表示其他内核源代码包含目录,多个目录用逗号隔开。
-
flagBuildDir:是一个字符串类型的变量,默认值是空字符串。它定义了命令行参数 “builddir”,表示内核生成的文件存储路径。
-
flagArch:是一个字符串类型的变量,默认值是空字符串。它定义了命令行参数 “arch”,表示需要生成的架构,多个架构用逗号隔开,如果不指定则生成所有架构。
这些值来自于go中的flag包
接下来,便是尝试获取 OS 所对应的 Extractor 结构体;如果 OS 不存在则肯定取不到,直接报错:
OS := *flagOS
extractor := extractors[OS]
if extractor == nil {
tool.Failf("unknown os: %v", OS)
}
extractors 数组如下所示,该数组为不同的 OS 实例化了不同的 Extractor 类。其中 linux OS 所对应的 Extractor 实例(即那三个函数的实现)位于 sys/syz-extract/linux.go
中:
type Extractor interface {
prepare(sourcedir string, build bool, arches []*Arch) error
prepareArch(arch *Arch) error
processFile(arch *Arch, info *compiler.ConstInfo) (map[string]uint64, map[string]bool, error)
}
var extractors = map[string]Extractor{
targets.Akaros: new(akaros),
targets.Linux: new(linux),
targets.FreeBSD: new(freebsd),
targets.Darwin: new(darwin),
targets.NetBSD: new(netbsd),
targets.OpenBSD: new(openbsd),
"android": new(linux),
targets.Fuchsia: new(fuchsia),
targets.Windows: new(windows),
targets.Trusty: new(trusty),
}
回到 main 函数,syz-extract 要用已有的 OS 字符串、archArray 字符串数组,以及 syzlang 文件名数组来生成出对应的 arches 结构体数组:
在旧的版本中,会有单另的archFileList用来生成arch字符串,file等
现在 合并在createArches中,archList函数之类的。
用已有的 OS 字符串、archArray 字符串数组(调用archList()
获得),以及 syzlang 文件名数组来生成出对应的 arches 结构体数组。
arches, nfiles, err := createArches(OS, archList(OS, *flagArch), flag.Args())
if err != nil {
tool.Fail(err)
}
if *flagSourceDir == "" {
tool.Fail(fmt.Errorf("provide path to kernel checkout via -sourcedir " +
"flag (or make extract SOURCEDIR)"))
}
- OS 为操作系统字符串
- archList结果为待生成的 arch 字符串数组
- nfiles 为待分析的 syzlang 文件名 字符串数组
准备工作已经做的差不多了,接下来让 extractor 执行初始化操作:
if err := extractor.prepare(*flagSourceDir, *flagBuild, arches); err != nil {
tool.Fail(err)
}
这一步实际上会调用到 sys/syz-extract/linux.go
中的 prepare
函数:
func (*linux) prepare(sourcedir string, build bool, arches []*Arch) error {
if build {
// Run 'make mrproper', otherwise out-of-tree build fails.
// However, it takes unreasonable amount of time,
// so first check few files and if they are missing hope for best.
for _, a := range arches {
arch := a.target.KernelArch
if osutil.IsExist(filepath.Join(sourcedir, ".config")) ||
osutil.IsExist(filepath.Join(sourcedir, "init/main.o")) ||
osutil.IsExist(filepath.Join(sourcedir, "include/config")) ||
osutil.IsExist(filepath.Join(sourcedir, "include/generated/compile.h")) ||
osutil.IsExist(filepath.Join(sourcedir, "arch", arch, "include", "generated")) {
fmt.Printf("make mrproper ARCH=%v\n", arch)
out, err := osutil.RunCmd(time.Hour, sourcedir, "make", "mrproper", "ARCH="+arch,
"-j", fmt.Sprint(runtime.NumCPU()))
if err != nil {
return fmt.Errorf("make mrproper failed: %v\n%s", err, out)
}
}
}
} else {
if len(arches) > 1 {
return fmt.Errorf("more than 1 arch is invalid without -build")
}
}
return nil
}
如果不指定重新生成 linux kernel header,那么只会做一些简单的检查。但如果指定重新生成了,则会尝试在 linux kernel src 上执行 make mrproper
。
回到 main 函数,接下来便是创建 go routine 通信管道和启动并行 worker:
go routine 是 go 的轻量级线程,其中关键字
go
后面的语句将被放进新的 go routine 中执行。
jobC := make(chan interface{}, len(arches)+nfiles)
for _, arch := range arches {
jobC <- arch
}
for p := 0; p < runtime.GOMAXPROCS(0); p++ {
go worker(extractor, jobC)
}
上面的代码创建了一个管道 jobC
,该管道的容量为所有架构数量加上文件数量。然后,它循环所有的架构,并将每个架构作为接口类型的值放入该管道中。
接着,它循环该代码在一个系统上可以同时运行的最大的处理数,并在每次循环中启动一个新的工作程序,并将 extractor
和 jobC
作为参数传递给该程序。这个工作程序的目的是从 jobC
管道中提取架构,并运行提取程序。
因此,通过创建多个工作程序,代码可以并行地处理所有架构。
总的来说,如果有多个架构,则启动多线程并发执行各自的 processArch()
/ processFile()
。
worker 启动后,main 函数就需要等待 worker 处理完成后才能保存处理结果至文件中,这就涉及到了线程协同。注意到代码中有 <-arch.done
和 <-f.done
语句,这两个语句会一直阻塞等待管道,直到其传来信息。若 worker 函数中对管道执行 close 操作,则被关闭的管道将不再等待,继续向下执行。因此这里 syz-extract 就利用了管道来完成线程协同。
constFiles := make(map[string]*compiler.ConstFile)
for _, arch := range arches {
fmt.Printf("generating %v/%v...\n", OS, arch.target.Arch)
<-arch.done
if arch.err != nil {
failed = true
fmt.Printf("%v\n", arch.err)
continue
}
for _, f := range arch.files {
<-f.done
if f.err != nil {
failed = true
fmt.Printf("%v: %v\n", f.name, f.err)
continue
}
if constFiles[f.name] == nil {
constFiles[f.name] = compiler.NewConstFile()
}
constFiles[f.name].AddArch(f.arch.target.Arch, f.consts, f.undeclared)
}
}
剩下的部分就是将生成结果保存在const文件中
for file, cf := range constFiles {
outname := filepath.Join("sys", OS, file+".const")
data := cf.Serialize()
if len(data) == 0 {
os.Remove(outname)
continue
}
if err := osutil.WriteFile(outname, data); err != nil {
tool.Failf("failed to write output file: %v", err)
}
}
if !failed && *flagArch == "" {
failed = checkUnsupportedCalls(arches)
}
for _, arch := range arches {
if arch.build {
os.RemoveAll(arch.buildDir)
}
}
if failed {
os.Exit(1)
}
main函数的主要逻辑如下:
-
首先,调用
flag.Parse()
来解析命令行参数,主要是OS,arch,syzlang文件名。 -
检查传入的参数是否合法:如果
flagBuild
和flagBuildDir
同时出现,输出错误信息;如果没有提供操作系统的类型,也输出错误信息。 -
获取参数
OS
的值,并通过extractors
字典来获取对应的提取器。如果没有对应的提取器,输出错误信息。 -
通过
createArches
函数生成需要处理的架构,并向jobC
channel 中添加需要处理的任务。见 createArches() -
sys/syz-extract/linux.go: prepare()
—— 初始化操作,如果设置了 build 参数,表示重新生成特定架构的内核头文件,先删除之前编译所生成的文件和配置文件; -
启动
GOMAXPROCS(0)
个工作协程,它们从jobC
channel 中读取任务并处理。 -
对每种arch架构,多线程并发执行
worker()
(边进行常量提取,边将先前已有的提取结果存放进文件中,提高效率),真正执行变量解析工作;—— 见1-4
processArch()sys/syz-extract/extract.go: processArch()
:处理传入的 Extractor 和 Arch 结构体,生成 const 信息。pkg/ast/parser.go: ParseGlob()
:将编写的txt文件解析成AST;pkg/compiler/consts.go: ExtractConsts()
:从每个syzlang文件中提取出const值;返回 syzlang 文件名与其用到的常量数组的映射;sys/syz-extract/linux.go: prepareArch()
:补全某些 arch 的 kernel src 可能会缺失的头文件;
sys/syz-extract/linux.go: processFile()
:编译生成可执行文件,并搜集常量;sys/syz-extract/fetch.go: extract()
:主要函数。
8.等待
worker()
多线程执行完成,结果保存到const
文件。
总体流程:
- 调用自定义 compiler 解析 syzlang 为 AST 森林,并依次提取每个 AST 树上的 consts 节点,然后将这些 consts 节点上的字符串放置进模板中,编译模板生成一个 ELF 或其他可执行文件;
- 分析 ELF 文件上的数据,或者尝试执行可执行文件来解析其输出,以获得各个 consts 字符串所对应的具体整型值;
- 将获取到的 consts 字符串与具体整型的映射关系,一个个序列化并填入
.const
文件中,这样便生成了对应于每个 syzlang 文件的.const
文件。
archList
获取架构 name list
功能:确定待分析的目标架构,如果指定了架构则直接返回,如果未指定架构则返回所有架构的架构name数组。注意所有架构的信息保存在 sys/targets/targets.go: targets.List
中。
参数:OS 字符串、arch 字符串。
代码如下
func archList(OS, arches string) []string {
if arches != "" {
return strings.Split(arches, ",")
}
var archArray []string
for arch := range targets.List[OS] {
archArray = append(archArray, arch)
}
sort.Strings(archArray)
return archArray
}
targets.List 如下
var List = map[string]map[string]*Target{
...
Linux: {
AMD64: {
PtrSize: 8,
PageSize: 4 << 10,
LittleEndian: true,
CFlags: []string{"-m64"},
Triple: "x86_64-linux-gnu",
KernelArch: "x86_64",
KernelHeaderArch: "x86",
NeedSyscallDefine: func(nr uint64) bool {
// Only generate defines for new syscalls
// (added after commit 8a1ab3155c2ac on 2012-10-04).
return nr >= 313
},
},
I386: {
VMArch: AMD64,
PtrSize: 4,
PageSize: 4 << 10,
Int64Alignment: 4,
LittleEndian: true,
CFlags: []string{"-m32"},
Triple: "x86_64-linux-gnu",
KernelArch: "i386",
KernelHeaderArch: "x86",
},
ARM64: {
PtrSize: 8,
PageSize: 4 << 10,
LittleEndian: true,
Triple: "aarch64-linux-gnu",
KernelArch: "arm64",
KernelHeaderArch: "arm64",
},
ARM: {
VMArch: ARM64,
PtrSize: 4,
PageSize: 4 << 10,
LittleEndian: true,
CFlags: []string{"-D__LINUX_ARM_ARCH__=6", "-march=armv6"},
Triple: "arm-linux-gnueabi",
KernelArch: "arm",
KernelHeaderArch: "arm",
},
...
}
createArches
功能:生成与参数对应的 Arch 结构体数组。
注:syzlang 可以用来写syscall模板
syzlang 是 syzkaller 中的一个组件,它提供了一种高级语言,用于描述系统调用和系统数据结构。这种语言称为 syzlang,并且它抽象了底层细节,方便描述复杂的系统调用和数据结构。它使用起来更方便,并且可以在不涉及技术细节的情况下描述系统调用。
func createArches(OS string, archArray, files []string) ([]*Arch, int, error) {
errBuf := new(bytes.Buffer)
//报错函数
eh := func(pos ast.Pos, msg string) {
fmt.Fprintf(errBuf, "%v: %v\n", pos, msg)
}
top := ast.ParseGlob(filepath.Join("sys", OS, "*.txt"), eh)
if top == nil {
return nil, 0, fmt.Errorf("%v", errBuf.String())
}
allFiles := compiler.FileList(top, OS, eh)
if allFiles == nil {
return nil, 0, fmt.Errorf("%v", errBuf.String())
}
nfiles := 0
var arches []*Arch
for _, archStr := range archArray { // [1] 遍历架构 name 数组
buildDir := "" // [2] 确定 build 文件夹路径
if *flagBuild {
dir, err := ioutil.TempDir("", "syzkaller-kernel-build")
if err != nil {
return nil, 0, fmt.Errorf("failed to create temp dir: %v", err)
}
buildDir = dir
} else if *flagBuildDir != "" {
buildDir = *flagBuildDir
} else {
buildDir = *flagSourceDir
}
target := targets.Get(OS, archStr) // [3] 获取 targets.List 中对应与 OS 和 arch 的 `Target` 结构体
if target == nil {
return nil, 0, fmt.Errorf("unknown arch: %v", archStr)
}
arch := &Arch{ // [4] 创建 arch 结构体
target: target, // 存放特定 OS 特定 arch 的一些信息
sourceDir: *flagSourceDir, // kernel source 路径
includeDirs: *flagIncludes, // kernel source header 路径
buildDir: buildDir, // build 路径
build: *flagBuild, // bool 值,是否需要重新生成架构指定的 kernel header
done: make(chan bool), // 管道,用于 go routine 间通信。当 arch 分析完成后,将会向该管道通知
}
archFiles := files
if len(archFiles) == 0 {
for file, meta := range allFiles {
if meta.NoExtract || !meta.SupportsArch(archStr) {
continue
}
archFiles = append(archFiles, file)
}
}
sort.Strings(archFiles)
for _, f := range archFiles { // [5] 将 syzlang 文件名数组添加进 arch 结构体中
arch.files = append(arch.files, &File{ //将文件的信息(通过 File 对象)附加到 "arch.files" 列表
arch: arch,
name: f,
done: make(chan bool),// 管道,用于 go routine 间通信。当 file 分析完成后,将会向该管道通知
})
}
arches = append(arches, arch)
nfiles += len(arch.files)
}
return arches, nfiles, nil
}
它叫做 “createArches”,接受三个参数:
- “OS” - 字符串类型,代表操作系统的名称。
- “archArray” - 字符串数组,代表你想要构建的架构。
- “files” - 字符串数组,代表要打包的文件。
它返回两个结果:
- []*Arch - 一个指针数组,代表创建的架构。
- int - 一个整数,代表打包后的文件数量。
- error - 错误信息,如果出现错误,则返回错误信息。
例如,你可以调用该函数如下:
os := "Linux"
archArray := []string{"x86", "x64"}
files := []string{"file1.txt", "file2.txt"}
arches, count, err := createArches(os, archArray, files)
if err != nil {
fmt.Println(err)
} else {
fmt.Println("Arches:", arches)
fmt.Println("Count:", count)
}
worker
功能:执行真正的变量解析工作。分别对Arch和 syzlang File 调用 processArch()
函数和 processFile()
函数处理。
参数: 传给 worker()
的 jobC
参数就是 Arch 结构体数组。所以在 worker()
函数中会进入 case *Arch
分支。
func worker(extractor Extractor, jobC chan interface{}) {
for job := range jobC {
switch j := job.(type) { // [1] j 赋值为 jobC 管道中的对象,初始时为 Arch 结构体
case *Arch:
infos, err := processArch(extractor, j) // [2] 执行 processArch(), 生成 const 信息
j.err = err
close(j.done)
if j.err == nil {
for _, f := range j.files {
f.info = infos[filepath.Join("sys", j.target.OS, f.name)]
jobC <- f // [3] processArch() 执行完后,从 infos 映射中遍历取出对应文件的信息,并将其填充至 arch 结构体中 files 结构体数组内的各个元素字段里; 将这个 File 结构体放入 jobC 管道中
} //"jobC <- f" 表示将一个 "f" 变量写入 "jobC" 通道。
}
case *File:
j.consts, j.undeclared, j.err = processFile(extractor, j.arch, j)
close(j.done)
}
}
}
该函数在一个 for 循环中不断读取 “jobC” 通道中的任务,并对其进行处理。每个任务是一个接口类型,该程序通过一个 switch 语句判断每个任务的具体类型。
如果任务的类型为 *Arch,则使用 “processArch” 函数处理该任务,并关闭 “j.done” 通道,如果 “j.err” 等于 nil,则对每个 “j.files” 中的文件再次进行处理并写入 “jobC” 通道。
如果任务的类型为 *File,则使用 “processFile” 函数处理该任务,并将处理结果写入 “j.consts”,“j.undeclared” 和 “j.err” 字段,然后关闭 “j.done” 通道。
这个代码是一个并行处理任务的示例,通过不断读取通道中的任务并处理,实现了并行的效果。
流程说明:由于 worker()
会循环读取 jobC 内数据,因此接下来便会取出刚刚新放入的 File 结构体,执行 processFile()
函数。在 processFile()
中,syz-extract 将会获取各个 const 变量(例如 O_RDWR)所对应的整型值(例如2)。
注意:worker()
中需注意,当 processFile()
执行完成后,worker 函数接下来都会执行 close(j.done)
,将通信管道关闭。这样做的是为了通知 main()
函数 goroutine “某部分工作已经完成”。这个操作有点类似于使用信号量来保证线程同步。
processArch
功能:processArch 的作用是,处理传入的 Extractor 和 Arch 结构体,生成 const 信息。
func processArch(extractor Extractor, arch *Arch) (map[string]*compiler.ConstInfo, error) {
errBuf := new(bytes.Buffer)
// 定义 error handler 函数
eh := func(pos ast.Pos, msg string) {
fmt.Fprintf(errBuf, "%v: %v\n", pos, msg)
}
// 解析 sys/linux/*.txt 的 syzlang 文件,形成一个 AST 数组
// 因此 top 变量就是 ast 森林的根节点
top := ast.ParseGlob(filepath.Join("sys", arch.target.OS, "*.txt"), eh)
if top == nil {
return nil, fmt.Errorf("%v", errBuf.String())
}
// 调用 compiler.ExtractConsts 获取每个 syzlang 文件中所对应的 const 信息
infos := compiler.ExtractConsts(top, arch.target, eh)
if infos == nil {
return nil, fmt.Errorf("%v", errBuf.String())
}
// 让 Extractor 为 arch 做些准备
if err := extractor.prepareArch(arch); err != nil {
return nil, err
}
return infos, nil //将获取到的consts infos 返回给调用者
}
-
调用
pkg/ast/parser.go: ParseGlob()
->pkg/ast/parser.go: Parse()
将编写的txt文件解析成AST。Parse()
->parseTopRecover()
解析出节点加入到top中,并且会在struct前后加上空行,移除重复的空行。parseTopRecover()
->parseTop()
根据标识符的类型调用不同的函数处理。
-
调用了库函数 pkg\compiler\const.go
compiler.ExtractConsts()
,主要调用pkg\compiler\compiler.goCompile()
提取出常量标识符。返回编译 syzlang 结果中的res.fileConsts
字段.- ExtractConsts() -> Compile()
createCompiler()
:在syscall_descriptions_syntax.md
中可以看到syzkaller内建的一些别名和模板,在createCompiler()
函数中对它们进行了初始化。typecheck()
:分别调用checkDirectives()
,checkNames()
,checkFields()
,checkTypedefs()
,checkTypes()
这五个函数进行一些检查。对于可能出现的错误可以对照consts_errors.txt,errors.txt和errors2.txt中给出的例子。extractConsts()
:返回提取const值所需的文本常量和其它信息的列表(负责提取目录/头文件/定义的name/系统调用名/call/struct/resource中的常量)。列表中的内容分别为常量(consts),定义(defines),包含头文件数组(includeArray),包含目录数组(incdirArray)。
其中,
compiler.ExtractConsts
只是一个简单的 wrapper 函数,获取编译 syzlang 结果中的 fileConsts 字段:func ExtractConsts(desc *ast.Description, target *targets.Target, eh ast.ErrorHandler) map[string]*ConstInfo { res := Compile(desc, nil, target, eh) if res == nil { return nil } return res.fileConsts }
- ExtractConsts() -> Compile()
字段 res.fileConsts 包含了 syzlang 文件名与其用到的常量数组的映射,以及其所 include 的头文件数组的映射;这些东西都将会用到获取 consts 对应的具体整数操作中。
而 extractor.prepareArch
函数在 linux.go
中,做的操作主要是定义了几个头文件:
"stdarg.h": `
#pragma once
#define va_list __builtin_va_list
#define va_start __builtin_va_start
#define va_end __builtin_va_end
#define va_arg __builtin_va_arg
#define va_copy __builtin_va_copy
#define __va_copy __builtin_va_copy
`,
"asm/a.out.h": "",
"asm/prctl.h": "",
"asm/mce.h": "",
"uapi/asm/msr.h": "",
因为某些 arch 的 kernel src 可能会缺失这些文件,需要自己手动补全。补全之后 extractor.prepareArch
会重新执行一次 linux kernel make 生成。
回到 processArch 函数,该函数最后会把先前获取到的 consts info 返回给调用者:
processFile
编译并搜集常量
功能:sys/syz-extract/extract.go: processFile()
只是封装了 sys/syz-extract/linux.go: processFile()
。查找const值(主要在 [3]
处调用 sys/syz-extract/fetch.go: extract()
函数)。
说明:最后生成的 res 映射和 undeclared 集合。res 是 const 字符串与整型的映射;undeclared 是未声明 const 字符串与 bool 值的映射,通常这里的 bool 值都为 true。
undeclared 所对应的常量将在 .const
文件中标明其值为 ???
,例如
O_RDWR = 2 MyConst = ???
type Extractor interface {
prepare(sourcedir string, build bool, arches []*Arch) error
prepareArch(arch *Arch) error
processFile(arch *Arch, info *compiler.ConstInfo) (map[string]uint64, map[string]bool, error)
}
func processFile(extractor Extractor, arch *Arch, file *File) (map[string]uint64, map[string]bool, error) {
inname := filepath.Join("sys", arch.target.OS, file.name)
if file.info == nil {
return nil, nil, fmt.Errorf("const info for input file %v is missing", inname)
}
if len(file.info.Consts) == 0 {
return nil, nil, nil
}
return extractor.processFile(arch, file.info)
}
//sys/syz-extract/linux.go: processFile()
func (*linux) processFile(arch *Arch, info *compiler.ConstInfo) (map[string]uint64, map[string]bool, error) {
headerArch := arch.target.KernelHeaderArch // [1] 生成编译代码模板所用到的 gcc 编译参数:args
sourceDir := arch.sourceDir
buildDir := arch.buildDir
args := []string{
// This makes the build completely hermetic, only kernel headers are used.
"-nostdinc",
"-w", "-fmessage-length=0",
"-O3", // required to get expected values for some __builtin_constant_p
"-I.",
"-D__KERNEL__",
"-DKBUILD_MODNAME=\"-\"",
"-I" + sourceDir + "/arch/" + headerArch + "/include",
"-I" + buildDir + "/arch/" + headerArch + "/include/generated/uapi",
"-I" + buildDir + "/arch/" + headerArch + "/include/generated",
"-I" + sourceDir + "/arch/" + headerArch + "/include/asm/mach-malta",
"-I" + sourceDir + "/arch/" + headerArch + "/include/asm/mach-generic",
"-I" + buildDir + "/include",
"-I" + sourceDir + "/include",
"-I" + sourceDir + "/arch/" + headerArch + "/include/uapi",
"-I" + buildDir + "/arch/" + headerArch + "/include/generated/uapi",
"-I" + sourceDir + "/include/uapi",
"-I" + buildDir + "/include/generated/uapi",
"-I" + sourceDir,
"-I" + sourceDir + "/include/linux",
"-I" + buildDir + "/syzkaller",
"-include", sourceDir + "/include/linux/kconfig.h",
}
args = append(args, arch.target.CFlags...)
for _, incdir := range info.Incdirs {
args = append(args, "-I"+sourceDir+"/"+incdir)
}
if arch.includeDirs != "" {
for _, dir := range strings.Split(arch.includeDirs, ",") {
args = append(args, "-I"+dir)
}
}
params := &extractParams{ // [2] 准备 extract 参数: params, 准备待使用的CC编译器
AddSource: "#include <asm/unistd.h>",
ExtractFromELF: true,
TargetEndian: arch.target.HostEndian,
}
cc := arch.target.CCompiler
res, undeclared, err := extract(info, cc, args, params) // [3] 执行核心函数 extract,生成 res 映射和 undeclared 集合
if err != nil {
return nil, nil, err
}
if arch.target.PtrSize == 4 { // [4] 若当前架构是32位, 则 syz-extract 需要使用 mmap2 来替换 mmap,以避免一些可能的错误
// mmap syscall on i386/arm is translated to old_mmap and has different signature.
// As a workaround fix it up to mmap2, which has signature that we expect.
// pkg/csource has the same hack.
const mmap = "__NR_mmap"
const mmap2 = "__NR_mmap2"
if res[mmap] != 0 || undeclared[mmap] {
if res[mmap2] == 0 {
return nil, nil, fmt.Errorf("%v is missing", mmap2)
}
res[mmap] = res[mmap2]
delete(undeclared, mmap)
}
}
return res, undeclared, nil // [5] 返回结果
}
核心代码extract 是这个
params := &extractParams{ // [2] 准备 extract 参数: params, 准备待使用的CC编译器
AddSource: "#include <asm/unistd.h>",
ExtractFromELF: true,
TargetEndian: arch.target.HostEndian,
}
cc := arch.target.CCompiler
res, undeclared, err := extract(info, cc, args, params) // [3] 执行核心函数 extract,生成 res 映射和 undeclared 集合
if err != nil {
return nil, nil, err
}
extract
编译并搜集常量
位置:sys/syz-extract/fetch.go
功能:调用编译器来编译代码模板,并根据编译出的二进制文件来获取 consts 常量整数。若编译过程出错,则会尝试自动纠错。
参数:Info 便是单个文件存放 const 数据的结构体,cc 是编译器名称字符串,args 是编译器执行参数,params 是用于 extract 执行过程用的选项。
func extract(info *compiler.ConstInfo, cc string, args []string, params *extractParams) (
map[string]uint64, map[string]bool, error) {
data := &CompileData{ // [1] 初始化: 声明一系列的 map
extractParams: params,
Defines: info.Defines,
Includes: info.Includes,
Values: info.Consts,
}
// 编译生成的程序路径
bin := ""
// 这个字段貌似没有用途,先行忽略
missingIncludes := make(map[string]bool)
// 未定义的 const,通常是自己定义的常量
undeclared := make(map[string]bool)
// 声明并初始化 valMap 中各个元素为 true
valMap := make(map[string]bool)
for _, val := range info.Consts {
valMap[val] = true
}
for {
// [2] 尝试将 consts 常量字符串与模板C代码结合,并编译结合后的代码,形成一个可执行文件
bin1, out, err := compile(cc, args, data) // [2-1] 编译操作, 返回结果分别为编译出的可执行文件路径 / 编译器标准输出信息 / 编译器标准错误信息
if err == nil {
bin = bin1
break
}
// Some consts and syscall numbers are not defined on some archs.
// Figure out from compiler output undefined consts,
// and try to compile again without them.
// May need to try multiple times because some severe errors terminate compilation.
tryAgain := false
for _, errMsg := range []string{ // [2-2] 遍历所有预先定义的错误信息,并使用正则表达式匹配
`error: [‘']([a-zA-Z0-9_]+)[’'] undeclared`,
`note: in expansion of macro [‘']([a-zA-Z0-9_]+)[’']`,
`note: expanded from macro [‘']([a-zA-Z0-9_]+)[’']`,
`error: use of undeclared identifier [‘']([a-zA-Z0-9_]+)[’']`,
} {
re := regexp.MustCompile(errMsg)
matches := re.FindAllSubmatch(out, -1)
for _, match := range matches { // [2-3] 如果匹配到了,则将出问题的常量存于 undeclared 中
val := string(match[1])
if valMap[val] && !undeclared[val] {
undeclared[val] = true
tryAgain = true
}
}
}
if !tryAgain {
return nil, nil, fmt.Errorf("failed to run compiler: %v %v\n%v\n%s",
cc, args, err, out)
}
data.Values = nil // 重置编译用的 consts 数组
for _, v := range info.Consts { // [2-4] 将出错的 consts 剔除,并将剩余没出错的 consts 存入编译用的 consts 数组
if undeclared[v] {
continue
}
data.Values = append(data.Values, v)
}
data.Includes = nil
for _, v := range info.Includes {
if missingIncludes[v] {
continue
}
data.Includes = append(data.Includes, v)
}
}
defer os.Remove(bin) // [3] 将新编译出的二进制文件删除
var flagVals []uint64
var err error
if data.ExtractFromELF { // [4] 从编译出的二进制文件中读取数值,解析并返回
flagVals, err = extractFromELF(bin, params.TargetEndian) // [4-1] OS 为 Linux 时, 走这个分支,不会实际执行程序,而是从 ELF 文件中一个名为 syz_extract_data 的 section 中读取常量值
} else {
flagVals, err = extractFromExecutable(bin) // 若 ExtractFromELF 字段为 false, 实际执行目标程序,解析其输出并转换为整型数组
}
if err != nil {
return nil, nil, err
}
if len(flagVals) != len(data.Values) {
return nil, nil, fmt.Errorf("fetched wrong number of values %v, want != %v",
len(flagVals), len(data.Values))
}
res := make(map[string]uint64)
for i, name := range data.Values {
res[name] = flagVals[i]
}
return res, undeclared, nil
}
因为上面提到了compile函数,我们进行查看
sys/syz-extract/fetch.go: compile()
功能:将 consts 常量字符串与模板C代码结合,并编译结合后的代码,形成一个可执行文件。
说明:模板C代码存于 srcTemplate
变量,该模板会将先前从 syzlang 收集到的 include、define 和 consts 字符串全部融合:
- 如果设置了 ExtractFromELF 标志位,则 consts 值将全部放置在一个名为 syz_extract_data 的 section 上
- 如果没有设置该标志位,则编译出来的程序在执行时将会依次打印 consts 值,以
%llu
的输出格式&使用空格来区分每个变量,输出至 stdout中。这样,sys-extract 就可以通过分析所编译程序的输出,来确定每个 consts 字符串所对应的数值是多少。
func compile(cc string, args []string, data *CompileData) (string, []byte, error) {
// 创建填充好后的 C 代码缓冲区
src := new(bytes.Buffer)
// 使用传入的 data 对代码模板 srcTemplate 进行填充
if err := srcTemplate.Execute(src, data); err != nil {
return "", nil, fmt.Errorf("failed to generate source: %v", err)
}
// 创建一个临时可执行文件路径
binFile, err := osutil.TempFile("syz-extract-bin")
if err != nil {
return "", nil, err
}
// 为编译器添加额外的参数
args = append(args, []string{
// -x c :指定代码语言为 C 语言
// - :指定代码从标准输入而不是从文件中读取
"-x", "c", "-",
// 指定文件输出的路径
"-o", binFile,
"-w",
}...)
if data.ExtractFromELF {
// gcc -c 参数:只编译但不链接
// 由于我们测试时使用的是 Linux,因此会进入该分支
args = append(args, "-c")
}
// 执行程序
cmd := osutil.Command(cc, args...)
// 将填充后的代码模板喂给 gcc 编译
cmd.Stdin = src
// 将 stdin 和 stdout 的输入糅合,使得他俩的输出完全一致
// 通俗的说就是让 stdin 和 stdout 都指向同一个管道
if out, err := cmd.CombinedOutput(); err != nil {
os.Remove(binFile)
return "", out, err
}
return binFile, nil, nil
}
执行至 compile的图
代码模板 如下
var srcTemplate = template.Must(template.New("").Parse(`
{{if not .ExtractFromELF}}
#define __asm__(...)
{{end}}
{{if .DefineGlibcUse}}
#ifndef __GLIBC_USE
# define __GLIBC_USE(X) 0
#endif
{{end}}
{{range $incl := $.Includes}}
#include <{{$incl}}>
{{end}}
{{range $name, $val := $.Defines}}
#ifndef {{$name}}
# define {{$name}} {{$val}}
#endif
{{end}}
{{.AddSource}}
{{if .DeclarePrintf}}
int printf(const char *format, ...);
{{end}}
{{if .ExtractFromELF}}
__attribute__((section("syz_extract_data")))
unsigned long long vals[] = {
{{range $val := $.Values}}(unsigned long long){{$val}},
{{end}}
};
{{else}}
int main() {
int i;
unsigned long long vals[] = {
{{range $val := $.Values}}(unsigned long long){{$val}},
{{end}}
};
for (i = 0; i < sizeof(vals)/sizeof(vals[0]); i++) {
if (i != 0)
printf(" ");
printf("%llu", vals[i]);
}
return 0;
}
{{end}}
`))
可以很容易的看出来,该模板会将先前从 syzlang 收集到的 include、define 和 consts 字符串全部融合:
- 如果设置了 ExtractFromELF 标志位,则 consts 值将全部放置在一个名为 syz_extract_data 的 section 上
- 如果没有设置该标志位,则编译出来的程序在执行时将会依次打印 consts 值,以
%llu
的输出格式&使用空格来区分每个变量,输出至 stdout中。这样,sys-extract 就可以通过分析所编译程序的输出,来确定每个 consts 字符串所对应的数值是多少。
checkUnsupportedCalls
func checkUnsupportedCalls(arches []*Arch) bool {
supported := make(map[string]bool)
unsupported := make(map[string]string)
for _, arch := range arches {
for _, f := range arch.files {
for name := range f.consts {
supported[name] = true
}
for name := range f.undeclared {
unsupported[name] = f.name
}
}
}
failed := false
for name, file := range unsupported {
if supported[name] {
continue
}
failed = true
fmt.Printf("%v: %v is unsupported on all arches (typo?)\n",
file, name)
}
return failed
}
- 首先,使用 make 函数创建两个 map,一个是 supported,一个是 unsupported。supported 用来存储已经支持的名称,unsupported 用来存储未支持的名称。
- 然后,对于 arches 中的每个架构,遍历该架构的所有文件,并对这些文件中的常量和未声明的变量进行处理。如果是常量,则将其名称添加到 supported 中;如果是未声明的变量,则将其名称和对应的文件名添加到 unsupported 中。
- 最后,对于 unsupported 中的每个未支持的变量,如果该变量的名称在 supported 中,则说明该变量是支持的;否则,打印出该变量不支持的错误信息。
最终,返回该函数是否有失败(failed)。如果 failed 为 true,则说明存在不支持的调用;否则,说明所有的调用都是支持的。
archList
func archList(OS, arches string) []string {
if arches != "" {
return strings.Split(arches, ",")
}
var archArray []string
for arch := range targets.List[OS] {
archArray = append(archArray, arch)
}
sort.Strings(archArray)
return archArray
}
archList 用来返回archArray 简单的拆分
小结
syz-extract 会调用自定义 compiler 解析 syzlang 为 ast 森林,并依次提取每个 ast 树上的 consts 节点,然后将这些 consts 节点上的字符串放置进模板中,编译模板生成一个 ELF 或其他可执行文件。
接下来 syz-extract 会分析 ELF 文件上的数据,或者尝试执行可执行文件来解析其输出,以获得各个 consts 字符串所对应的具体整型值。
最后 syz-extract 将获取到的 consts 字符串与具体整型的映射关系,一个个序列化并填入 .const
文件中,这样便生成了对应于每个 syzlang 文件的 .const 文件。
在 syz-extract 执行的整个过程中,syz-extract 另起一个 go routine 来执行 worker,是为了能达到边进行常量提取,边将先前已有的提取结果存放进文件中,这样做是为了提高效率,加快常量提取的速度。
syz-sysgen
位置:sys/syz-sysgen/sysgen.go
功能:解析人工编写的syzlang代码文件,并将syzlang内部定义的syscall类型信息转换成后续syzkaller能够使用的数据结构。简单地说,syz-sysgen 解析 syzlang 文件,并为 syz-fuzzer 和 syz-executor 的编译运行做准备。
main
func main() {
defer tool.Init()()
var OSList []string
for OS := range targets.List {
OSList = append(OSList, OS)
}
sort.Strings(OSList)
data := &ExecutorData{}
首先,使用 defer 关键字和 tool.Init() 函数在 main 函数结束之前初始化某些工具。
defer
关键字在 Go 语言中用于延迟函数的执行。当遇到defer
语句时,Go 程序会将该语句所在的函数的执行推迟到函数返回时再执行。举个例子,如果你有一个文件需要打开,并在程序执行完毕后关闭,你可以使用
defer
来做到这一点:f, err := os.Open("file.txt") if err != nil { log.Fatal(err) } defer f.Close() // 程序将在这里执行其他操作,而不是在这里关闭文件
这样做的优势在于,即使程序需要从多个不同的地方返回,您仍然可以确保文件将在最终关闭。
func Init() func() {
flagCPUProfile := flag.String("cpuprofile", "", "write CPU profile to this file")
flagMEMProfile := flag.String("memprofile", "", "write memory profile to this file")
if err := ParseFlags(flag.CommandLine, os.Args[1:]); err != nil {
Fail(err)
}
return installProfiling(*flagCPUProfile, *flagMEMProfile)
}
涉及到cpuprofile 和memprofile 暂时不看
根据ki 爷的图 执行到这里
紧接着便是一个 for 循环,遍历 OSList 中的每个 OS 字符串,并解析其中的 syzlang 代码。将这个 for 循环分为了上中下三个部分:
第一部分
for _, OS := range OSList { // [2] for 循环,遍历OSList中每个OS字符串,并解析其中的syzlang代码
descriptions := ast.ParseGlob(filepath.Join(*srcDir, "sys", OS, "*.txt"), nil)
if descriptions == nil { // [2-1] syzlang文件解析成AST数树
os.Exit(1)
}
constFile := compiler.DeserializeConstFile(filepath.Join(*srcDir, "sys", OS, "*.const"), nil)
if constFile == nil { // .const 文件解析成 ConstFile 结构体
os.Exit(1)
}
osutil.MkdirAll(filepath.Join(*outDir, "sys", OS, "gen")) // syz-sysgen 输出结果存放在本目录
var archs []string
for arch := range targets.List[OS] {
archs = append(archs, arch)
}
sort.Strings(archs)
...
这部分内容较为简单,将当前遍历到的 OS 所对应的 sys/<os>/*.txt
和 sys/<os>/*.const
文件,分别解析成 AST 树 (ast.Description 类型) 和 ConstFile 结构体。之后创建 sys/<os>/gen
文件夹,整个 syz-sysgen 的输出将存放在该文件夹下:
偷KI爷的图
之后还是收集当前 OS 所对应的全部 arch 字符串集合,并做一次排序操作。
第二部分
for _, OS := range OSList {
...
var jobs []*Job // [2-2] 为每个arch都创建1个Job结构体, 将其添加进数组jobs中, 并为数组执行排序操作
for _, arch := range archs {
jobs = append(jobs, &Job{
Target: targets.List[OS][arch],
Unsupported: make(map[string]bool),
})
}
sort.Slice(jobs, func(i, j int) bool {
return jobs[i].Target.Arch < jobs[j].Target.Arch
})
var wg sync.WaitGroup // sync.WaitGroup 结构体, 用于等待指定数量的 go routine 集合执行完成, 类似于信号量
wg.Add(len(jobs)) // wg.Add(): 增加内部计数器值; wg.Done(): 减小内部计数器值; wg.Wait():判断内部计数器值状态, 进而选择是否挂起等待
for _, job := range jobs { // 遍历 jobs 数组中每个 job, 创建 go routine 并行执行这些 job
job := job
go func() {
defer wg.Done()
processJob(job, descriptions, constFile) // processJob() 重要函数
}()
}
wg.Wait()
...
}
首先是为每个 arch 都创建了一个 Job 结构体,将其添加进数组 jobs中,并为数组执行排序操作,其中排序规则是自定义的。
接下来创建了一个 sync.WaitGroup
结构体,这个结构体用于等待指定数量的 go routine 集合执行完成。其内部原理有点类似于信号量,执行 wg.Add
函数以增加其内部计数器值,执行 wg.Done
函数以减小其内部计数器值,执行 wg.Wait
则判断内部计数器值状态,进而选择是否挂起等待。
其中最重要的是,syz-sysgen 依次遍历 jobs 数组中的每个 job,并创建 go routine 并行执行这些 job。函数 processJob 用于编译先前 parse 的 syzlang AST、分析其中的类型信息与依赖关系,并将其序列化为 golang 代码至 sys/<OS>/gen/<arch>.go
中,同时还将 syscall 属性相关的信息保存在 job.ArchData
中,供后续生成 sys-executor 关键头文件代码所用。
第三部分
for _, OS := range OSList {
...
var syscallArchs []ArchData
unsupported := make(map[string]int)
for _, job := range jobs {
if !job.OK {
fmt.Printf("compilation of %v/%v target failed:\n", job.Target.OS, job.Target.Arch)
for _, msg := range job.Errors {
fmt.Print(msg)
}
os.Exit(1)
}
syscallArchs = append(syscallArchs, job.ArchData)
for u := range job.Unsupported {
unsupported[u]++
}
}
data.OSes = append(data.OSes, OSData{
GOOS: OS,
Archs: syscallArchs,
})
for what, count := range unsupported {
if count == len(jobs) {
tool.Failf("%v is unsupported on all arches (typo?)", what)
}
}
}
第三部分没什么需要特别关注的,这部分主要是做了一些检查,并将先前 worker 里生成的 ArchData 提取进变量 data 中。
for 循环结束后吗,main 函数最后这部分的代码继续为变量 data 设置一些字段:
attrs := reflect.TypeOf(prog.SyscallAttrs{}) // [3] 分别将 prog.SyscallAttrs 和 prog.CallProps 这两个结构体对应的字段名存起来
for i := 0; i < attrs.NumField(); i++ {
data.CallAttrs = append(data.CallAttrs, prog.CppName(attrs.Field(i).Name))
}
props := prog.CallProps{}
props.ForeachProp(func(name, _ string, value reflect.Value) {
data.CallProps = append(data.CallProps, CallPropDescription{
Type: value.Kind().String(),
Name: prog.CppName(name),
})
})
writeExecutorSyscalls(data)
}
这部分代码只是分别将 prog.SyscallAttrs
和 prog.CallProps
这两个结构体对应的字段名存了起来。俩结构体声明如下:
// SyscallAttrs represents call attributes in syzlang.
//
// This structure is the source of truth for the all other parts of the system.
// pkg/compiler uses this structure to parse descriptions.
// syz-sysgen uses this structure to generate code for executor.
//
// Only `bool`s and `uint64`s are currently supported.
//
// See docs/syscall_descriptions_syntax.md for description of individual attributes.
type SyscallAttrs struct {
Disabled bool
Timeout uint64
ProgTimeout uint64
IgnoreReturn bool
BreaksReturns bool
NoGenerate bool
NoMinimize bool
}
prog\prog.go
// These properties are parsed and serialized according to the tag and the type
// of the corresponding fields.
// IMPORTANT: keep the exact values of "key" tag for existing props unchanged,
// otherwise the backwards compatibility would be broken.
type CallProps struct {
FailNth int `key:"fail_nth"`
Async bool `key:"async"`
Rerun int `key:"rerun"`
}
通过对上面源码的分析,我发现 syz-sysgen 将整个 prog.SyscallAttrs
结构体的字段名和每个 syscall 所对应的数据,全都转换成了普通字符串型和整型。看上去这像是要用这些数据来填充 C 语言模板?我们接下来再来看看 writeExecutorSyscalls 函数,看看这里面具体是做了什么。
writeExecutorSyscalls 函数源码分析位于下文,这里不再赘述。
processJob()
功能:编译传入的 syzlang AST,分析其中的 syscall 类型信息等,并反序列化为一个 golang 语法源码。
参数:传入的参数 job
,结构体声明如下:
type Job struct {
Target *targets.Target // 存放着一些关于特定 OS 特定 arch 的一些常量信息
OK bool
Errors []string // 保存报错信息的字符串集合,一条字符串表示一行报错信息
Unsupported map[string]bool // 存放不支持的 syscall 集合
ArchData ArchData // 存放待从 worker routine 返回给 main 函数的数据
}
首先,该函数会生成一个 error handler,用于输出错误信息;之后从 ConstFile 结构体中,取出对应 arch 的 consts 字符串->整型映射表:
eh := func(pos ast.Pos, msg string) { // [1] 生成一个 error handler, 用于输出错误信息;
job.Errors = append(job.Errors, fmt.Sprintf("%v: %v\n", pos, msg))
}
func processJob(job *Job, descriptions *ast.Description, constFile *compiler.ConstFile) {
eh := func(pos ast.Pos, msg string) { // [1] 生成一个 error handler, 用于输出错误信息;
job.Errors = append(job.Errors, fmt.Sprintf("%v: %v\n", pos, msg))
}
consts := constFile.Arch(job.Target.Arch) // [2] 从 constFile 结构体取出对应 arch 的 consts 字符串->整型 映射表
if job.Target.OS == targets.TestOS { // [3] 过滤掉自己开发人员测试用的 testOS (targets.TestOS 即为字符串 test)
constInfo := compiler.ExtractConsts(descriptions, job.Target, eh)
compiler.FabricateSyscallConsts(job.Target, constInfo, consts)
}
prog := compiler.Compile(descriptions, consts, job.Target, eh) // [4] 对 syzlang AST 进行编译, 继续分析 AST 信息。
if prog == nil { // 这次编译提供了consts信息,因此会执行完整的编译过程
return
}
for what := range prog.Unsupported {
job.Unsupported[what] = true
}
// [5] 将分析结果,序列化为go语言源代码,留待后续 syz-fuzzer 使用,代码存放在 sys/<OS>/gen/<arch>.go
sysFile := filepath.Join(*outDir, "sys", job.Target.OS, "gen", job.Target.Arch+".go")
out := new(bytes.Buffer)
generate(job.Target, prog, consts, out)
rev := hash.String(out.Bytes())
fmt.Fprintf(out, "const revision_%v = %q\n", job.Target.Arch, rev)
writeSource(sysFile, out.Bytes())
// [6] 调用 generateExecutorSyscalls 函数来创建 Executor 的 syscall 信息,并将其返回给 main 函数
job.ArchData = generateExecutorSyscalls(job.Target, prog.Syscalls, rev)
// Don't print warnings, they are printed in syz-check.
job.Errors = nil
job.OK = true
}
syz-sysgen 需要分析 AST 信息,对 syzlang 进行编译:
prog := compiler.Compile(descriptions, consts, job.Target, eh) // [4] 对 syzlang AST 进行编译, 继续分析 AST 信息。
if prog == nil { // 这次编译提供了consts信息,因此会执行完整的编译过程
return
}
for what := range prog.Unsupported {
job.Unsupported[what] = true
}
返回的 Prog 结构体声明如下:
// Prog is description compilation result.
type Prog struct {
Resources []*prog.ResourceDesc
Syscalls []*prog.Syscall
Types []prog.Type
// Set of unsupported syscalls/flags.
Unsupported map[string]bool
// Returned if consts was nil.
fileConsts map[string]*ConstInfo
}
[4]编译操作和先前 syz-extract 类似,不同的是这次提供了 consts 信息,因此会执行完整的编译过程,分析 syzlang 代码中描述的全部 syscall 参数类型信息。返回的 Prog 结构体中:
- 字段 fileConsts 为空
- 涉及到的类型信息保存在了 Resource 和 Types 字段
- syscall 的描述则存放在 Syscalls 字段中。
Compile()
除了调用 createCompiler()
函数和 typecheck()
函数,接下来首先调用的是assignSyscallNumbers()
/ patchConsts()
/ check()
函数。
assignSyscallNumbers()
函数分配系统调用号,检测不受支持的系统调用并丢弃。patchConsts()
函数将AST中的常量patch成对应的值。check()
函数对AST进行语义检查。genSyscalls()
主要是调用了genSyscall()
函数,然后按照系统调用名排序。genSyscall()
函数中调用genType()
函数生成返回值,调用genFieldArray()
函数生成每个参数。- 返回的
Prog
对象中调用genResources()
函数生成资源,generateTypes()
函数生成结构体的描述。
我们来看看生成出的 golang 代码是什么样的(以 /sys/linux/gen/amd64.go
为例):
说明:
- 开头的
init()
函数用于将当前这个 linux amd64 的 target,注册进targets
数组中以供后续 syz-fuzzer 取出使用。 - 其中声明了多个数组:
resources_amd64
数组:存放着每个 syzlang 代码中声明的 resource 变量syscalls_amd64
数组:存放着每个 syscall 所对应的名称、调用号,以及各个参数的名称和类型。types_amd64
数组:每个类型的具体信息,例如数组、结构体类型信息等等consts_amd64
:存放 consts 字符串与整型的映射关系revision_amd64
:amd64.go 源码的哈希值
// AUTOGENERATED FILE
// +build !codeanalysis
// +build !syz_target syz_target,syz_os_linux,syz_arch_amd64
package gen
import . "github.com/google/syzkaller/prog"
import . "github.com/google/syzkaller/sys/linux"
func init() {
RegisterTarget(&Target{OS: "linux", Arch: "amd64", Revision: revision_amd64, PtrSize: 8, PageSize: 4096, NumPages: 4096, DataOffset: 536870912, LittleEndian: true, ExecutorUsesShmem: true, Syscalls: syscalls_amd64, Resources: resources_amd64, Consts: consts_amd64}, types_amd64, InitTarget)
}
var resources_amd64 = []*ResourceDesc{
{Name:"ANYRES16",Kind:[]string{"ANYRES16"},Values:[]uint64{18446744073709551615,0}},
{Name:"ANYRES32",Kind:[]string{"ANYRES32"},Values:[]uint64{18446744073709551615,0}},
{Name:"ANYRES64",Kind:[]string{"ANYRES64"},Values:[]uint64{18446744073709551615,0}},
{Name:"IMG_DEV_VIRTADDR",Kind:[]string{"IMG_DEV_VIRTADDR"},Values:[]uint64{0}},
{Name:"IMG_HANDLE",Kind:[]string{"IMG_HANDLE"},Values:[]uint64{0}},
{Name:"assoc_id",Kind:[]string{"assoc_id"},Values:[]uint64{0}},
....
}
var syscalls_amd64 = []*Syscall{
{NR:43,Name:"accept",CallName:"accept",Args:[]Field{
{Name:"fd",Type:Ref(11199)},
{Name:"peer",Type:Ref(10021)},
{Name:"peerlen",Type:Ref(10305)},
},Ret:Ref(11199)},
{NR:43,Name:"accept$alg",CallName:"accept",Args:[]Field{
{Name:"fd",Type:Ref(11202)},
{Name:"peer",Type:Ref(4943)},
{Name:"peerlen",Type:Ref(4943)},
},Ret:Ref(11203)},
{NR:43,Name:"accept$ax25",CallName:"accept",Args:[]Field{
{Name:"fd",Type:Ref(11204)},
{Name:"peer",Type:Ref(10033)},
{Name:"peerlen",Type:Ref(10305)},
},Ret:Ref(11204)},
{NR:43,Name:"accept$inet",CallName:"accept",Args:[]Field{
{Name:"fd",Type:Ref(11223)},
{Name:"peer",Type:Ref(10025)},
{Name:"peerlen",Type:Ref(10305)},
},Ret:Ref(11223)},
....
}
var types_amd64 = []Type{
&ArrayType{TypeCommon:TypeCommon{TypeName:"array",TypeAlign:1,IsVarlen:true},Elem:Ref(17155)},
&ArrayType{TypeCommon:TypeCommon{TypeName:"array",TypeAlign:1,IsVarlen:true},Elem:Ref(14707),Kind:1,RangeEnd:32},
&ArrayType{TypeCommon:TypeCommon{TypeName:"array",TypeAlign:1,IsVarlen:true},Elem:Ref(14707),Kind:1,RangeEnd:8},
&ArrayType{TypeCommon:TypeCommon{TypeName:"array",TypeAlign:1,IsVarlen:true},Elem:Ref(14560)},
&ArrayType{TypeCommon:TypeCommon{TypeName:"array",TypeAlign:1,IsVarlen:true},Elem:Ref(14575)},
....
}
var consts_amd64 = []ConstValue{
{"ABS_CNT",64},
{"ABS_MAX",63},
{"ACL_EXECUTE",1},
{"ACL_GROUP",8},
{"ACL_GROUP_OBJ",4},
{"ACL_LINK",1},
....
}
const revision_amd64 = "e61403f96ca19fc071d8e9c946b2259a2804c68e"
generateExecutorSyscalls()
功能:为生成 syz-executor 准备相关的 syscall 数据,因此起名神似 生成(generate) executor 的 syscall 数据。具体来说,就是遍历 Syscall,将对应的 SyscallData
添加到 data.Calls
。
func generateExecutorSyscalls(target *targets.Target, syscalls []*prog.Syscall, rev string) ArchData {
data := ArchData{ // [1] 创建 ArchData结构体,该结构体最后会返回给 main()
Revision: rev,
GOARCH: target.Arch,
PageSize: target.PageSize,
NumPages: target.NumPages,
DataOffset: target.DataOffset,
}
if target.ExecutorUsesForkServer { // 若目标 OS & arch 对应的target结构体,设置了对 ForkServer 和 Shmem(共享内存)的支持, 则设置data中相应字段, 这样 syz-executor便能使用这两种技术加速fuzz
data.ForkServer = 1
}
if target.ExecutorUsesShmem {
data.Shmem = 1
}
defines := make(map[string]string)
for _, c := range syscalls { // [2] 遍历各个 Syscall 类型的结构体
var attrVals []uint64
attrs := reflect.ValueOf(c.Attrs) // 将变量 c 中结构体 SyscallAttrs 里的各个字段取出,并将其依次存放至整型数组 attrVals (bool值和整型值)
last := -1
for i := 0; i < attrs.NumField(); i++ {
attr := attrs.Field(i)
val := uint64(0)
switch attr.Type().Kind() {
case reflect.Bool:
if attr.Bool() {
val = 1
}
case reflect.Uint64:
val = attr.Uint()
default:
panic("unsupported syscall attribute type")
}
attrVals = append(attrVals, val)
if val != 0 {
last = i
}
} // 再使用生成的 attrVals 数组进一步生成 SyscallData 结构体
data.Calls = append(data.Calls, newSyscallData(target, c, attrVals[:last+1]))
// Some syscalls might not be present on the compiling machine, so we
// generate definitions for them.
if target.SyscallNumbers && !strings.HasPrefix(c.CallName, "syz_") &&
target.NeedSyscallDefine(c.NR) {
defines[target.SyscallPrefix+c.CallName] = fmt.Sprintf("%d", c.NR)
}
}
sort.Slice(data.Calls, func(i, j int) bool { // [3] 将生成的 data.Calls 数组进行排序,并返回 data 变量
return data.Calls[i].Name < data.Calls[j].Name
})
// Get a sorted list of definitions.
defineNames := []string{}
for key := range defines {
defineNames = append(defineNames, key)
}
sort.Strings(defineNames)
for _, key := range defineNames {
data.Defines = append(data.Defines, Define{key, defines[key]})
}
return data
}
reflect.ValueOf(c.Attrs) 在运行中获取c.Attrs 的值的意思嘛 反射
说明:
-
[2]
作用,遍历各个 Syscall 类型的结构体, 将变量 c 中结构体 SyscallAttrs 里的各个字段取出,并将其依次存放至整型数组 attrVals (bool值和整型值);再使用生成的 attrVals 数组进一步生成SyscallData
结构体 -
Syscall
结构体 ->SyscallAttrs
结构体type Syscall struct { ID int NR uint64 // kernel syscall number Name string CallName string MissingArgs int // number of trailing args that should be zero-filled Args []Field Ret Type Attrs SyscallAttrs inputResources []*ResourceDesc outputResources []*ResourceDesc } type SyscallAttrs struct { Disabled bool Timeout uint64 ProgTimeout uint64 IgnoreReturn bool BreaksReturns bool }
-
data.Calls
中SyscallData
结构体示例与说明:[0]:<main.SyscallData> Name: "accept" CallName: "accept" NR: 30 NeedCall: false // sys/syz-sysgen/sysgen.go type SyscallData struct { Name string // syzlang 中的调用名,例如 accept$inet CallName string // 实际的 syscall 调用名,例如 accept NR int32 // syscall 对应的调用号,例如 30 NeedCall bool // 一个用于后续的 syz-executor 源码生成的标志,后面会提到 Attrs []uint64 // 存放分析 syzlang 所生成的 SyscallAttrs 数据数组 }
writeExecutorSyscalls
功能:生成 syz-executor 所使用的 C 代码头文件写入 executor/defs.h
,将系统调用名和对应的系统调用号写入 executor\syscalls.h
文件。
func writeExecutorSyscalls(data *ExecutorData) {
osutil.MkdirAll(filepath.Join(*outDir, "executor"))
sort.Slice(data.OSes, func(i, j int) bool {
return data.OSes[i].GOOS < data.OSes[j].GOOS
})
buf := new(bytes.Buffer) // [1] 生成 defs.h 文件
if err := defsTempl.Execute(buf, data); err != nil {
tool.Failf("failed to execute defs template: %v", err)
}
writeFile(filepath.Join(*outDir, "executor", "defs.h"), buf.Bytes())
buf.Reset() // [2] 生成 syscalls.h 文件
if err := syscallsTempl.Execute(buf, data); err != nil {
tool.Failf("failed to execute syscalls template: %v", err)
}
writeFile(filepath.Join(*outDir, "executor", "syscalls.h"), buf.Bytes())
}
代码中提到 defsTempl 和 syscallsTempl模板如下
defsTempl
模板
说明:syz-sysgen 会将把先前 generateExecutorSyscalls
函数中所生成的 ArchData 结构体数据,导出至 executor/defs.h 文件中,供后续编译 syz-executor 所使用。syz-sysgen 将所有OS所有架构所对应的 ArchData 数据全部导出至一个文件中,并使用宏定义来选择启用哪一部分的数据。
模板如下:混杂着 C 宏定义与模板描述。
var defsTempl = template.Must(template.New("").Parse(`// AUTOGENERATED FILE
struct call_attrs_t { {{range $attr := $.CallAttrs}}
uint64_t {{$attr}};{{end}}
};
struct call_props_t { {{range $attr := $.CallProps}}
{{$attr.Type}} {{$attr.Name}};{{end}}
};
#define read_call_props_t(var, reader) { \{{range $attr := $.CallProps}}
(var).{{$attr.Name}} = ({{$attr.Type}})(reader); \{{end}}
}
{{range $os := $.OSes}}
#if GOOS_{{$os.GOOS}}
#define GOOS "{{$os.GOOS}}"
{{range $arch := $os.Archs}}
#if GOARCH_{{$arch.GOARCH}}
#define GOARCH "{{.GOARCH}}"
#define SYZ_REVISION "{{.Revision}}"
#define SYZ_EXECUTOR_USES_FORK_SERVER {{.ForkServer}}
#define SYZ_EXECUTOR_USES_SHMEM {{.Shmem}}
#define SYZ_PAGE_SIZE {{.PageSize}}
#define SYZ_NUM_PAGES {{.NumPages}}
#define SYZ_DATA_OFFSET {{.DataOffset}}
{{range $c := $arch.Defines}}#ifndef {{$c.Name}}
#define {{$c.Name}} {{$c.Value}}
#endif
{{end}}#endif
{{end}}
#endif
{{end}}
`))
executor/defs.h
示例:
// AUTOGENERATED FILE
struct call_attrs_t {
uint64_t disabled;
uint64_t timeout;
uint64_t prog_timeout;
uint64_t ignore_return;
uint64_t breaks_returns;
};
struct call_props_t {
int fail_nth;
};
#define read_call_props_t(var, reader) { \
(var).fail_nth = (int)(reader); \
}
#if GOOS_akaros
#define GOOS "akaros"
#if GOARCH_amd64
#define GOARCH "amd64"
#define SYZ_REVISION "361c8bb8e04aa58189bcdd153dc08078d629c0b5"
#define SYZ_EXECUTOR_USES_FORK_SERVER 1
#define SYZ_EXECUTOR_USES_SHMEM 0
#define SYZ_PAGE_SIZE 4096
#define SYZ_NUM_PAGES 4096
#define SYZ_DATA_OFFSET 536870912
#endif
#endif
...
#if GOOS_linux
#define GOOS "linux"
...
#if GOARCH_amd64
#define GOARCH "amd64"
#define SYZ_REVISION "e61403f96ca19fc071d8e9c946b2259a2804c68e"
#define SYZ_EXECUTOR_USES_FORK_SERVER 1
#define SYZ_EXECUTOR_USES_SHMEM 1
#define SYZ_PAGE_SIZE 4096
#define SYZ_NUM_PAGES 4096
#define SYZ_DATA_OFFSET 536870912
#endif
...
#endif
...
#if GOOS_windows
#define GOOS "windows"
#if GOARCH_amd64
#define GOARCH "amd64"
#define SYZ_REVISION "8967babc353ed00daaa6992068d3044bad9d29fa"
#define SYZ_EXECUTOR_USES_FORK_SERVER 0
#define SYZ_EXECUTOR_USES_SHMEM 0
#define SYZ_PAGE_SIZE 4096
#define SYZ_NUM_PAGES 4096
#define SYZ_DATA_OFFSET 536870912
#endif
#endif
syscallsTempl
模板
说明:executor/syscalls.h
下会存放着各个 syzlang 中所声明的 syscall 名与 syscall调用号的映射关系,以及可能有的 SyscallData。同时,也是使用宏定义来控制使用哪个OS哪个Arch下的 syscalls 映射关系。
模板如下:
// nolint: lll
var syscallsTempl = template.Must(template.New("").Parse(`// AUTOGENERATED FILE
// clang-format offz
{{range $os := $.OSes}}
#if GOOS_{{$os.GOOS}}
{{range $arch := $os.Archs}}
#if GOARCH_{{$arch.GOARCH}}
const call_t syscalls[] = {
{{range $c := $arch.Calls}} {"{{$c.Name}}", {{$c.NR}}{{if or $c.Attrs $c.NeedCall}}, { {{- range $attr := $c.Attrs}}{{$attr}}, {{end}}}{{end}}{{if $c.NeedCall}}, (syscall_t){{$c.CallName}}{{end}}},
{{end}}};
#endif
{{end}}
#endif
{{end}}
`))
executor/syscalls.h
示例:
...
#if GOOS_linux
...
#if GOARCH_amd64
const call_t syscalls[] = {
{"accept", 43},
{"accept$alg", 43},
{"accept$ax25", 43},
{"accept$inet", 43},
{"accept$inet6", 43},
{"accept$netrom", 43},
{"accept$nfc_llcp", 43},
....,
{"bind", 49},
{"bind$802154_dgram", 49},
{"bind$802154_raw", 49},
{"bind$alg", 49},
{"bind$ax25", 49},
{"bind$bt_hci", 49},
{"bind$bt_l2cap", 49},
....
{"prctl$PR_CAPBSET_DROP", 167, {0, 0, 0, 1, 1, }},
{"prctl$PR_CAPBSET_READ", 167, {0, 0, 0, 1, 1, }},
{"prctl$PR_CAP_AMBIENT", 167, {0, 0, 0, 1, 1, }},
....
}
#endif
...
#endif
...
syscallData 结构体
type SyscallData struct {
Name string
CallName string
NR int32
NeedCall bool
Attrs []uint64
}
小结
当执行完 syz-extractor 为每个 syslang 文件生成一个常量映射表 .const
文件后,syz-sysgen 便会利用常量映射表,来彻底的解析 syzlang 源码,获取到其中声明的类型信息与 syscall 参数依赖关系。
当这些信息全都收集完毕后,syz-sysgen 便会将这些数据全部序列化为 go 文件,以供后续 syz-fuzzer 所使用。除此之外,syz-sysgen 还会创建 executor/defs.h 和 executor/syscalls.h,将部分信息导出至 C 头文件,以供后续 syz-executor 编译使用。
简单地说,syz-sysgen 解析 syzlang 文件,并为 syz-fuzzer 和 syz-executor 的编译运行做准备。
参考
https://kiprey.github.io/2022/03/syzkaller-1/
https://bsauce.github.io/2022/05/13/syzkaller1/
https://47.99.84.243/fuzz/Syzkaller%20executor%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90/
https://github.com/google/syzkaller
syzkaller 源码阅读笔记-2
前言
我们上一篇分析了syz-extract 和 syz-sysgen
sysgen 用于解析syzlang,提取syscall 和参数
syz-extract 用于解析syzlang常量
本次主要分析syz-manager
syz-manager
syz-manager 的功能 是 负责 各种的启动,HTTP,RPC,dashboard,调用fuzz 以及repro(reproducible可重现性的测试结果)的生成
我们在开始的 时候都是
./syz-manager -config=4.14.cfg -vv 10
参数 我们可以见syzkaller安装的文章
{
"target": "linux/amd64", //目标架构
"http": "0.0.0.0:8080",//http的端口
"workdir": "/home/test/go/src/github.com/google/syzkaller/bin/workdir",//syz-namager的工作目录
"kernel_obj": "/home/test/桌面/cheche/kernel/linux-4.4.146/", //内核的目录,主要是去寻找vmlinux
"image": "../image/stretch.img",//文件系统镜像
"sshkey": "../image/stretch.id_rsa",//私钥
"syzkaller": "/home/test/go/src/github.com/google/syzkaller",//syzkaller 的目录
"disable_syscalls": ["keyctl", "add_key", "request_key"], // 禁用的系统调用列表
"enable_syscalls": ["chmod"],//syzkaller使用的系统调用列表
"suppressions": ["some known bug"], // 已知错误的正则表达式列表
"procs": 1, // 每个VM中的并行测试进程数,一般是4或8
"type": "qemu",// 要使用的虚拟机类型,例如qemu
"vm": {// 特定VM类型相关的参数
"count": 1,// 并行运行的VM数
"kernel": "/home/test/桌面/cheche/kernel/linux-4.4.146/arch/x86/boot/bzImage",// 要测试的内核的bzImage文件的位置
"cpu": 1,// 要在VM中模拟的CPU数
"mem": 1024// VM的内存大小,以MB为单位
}
}
有一些其他的参数
email_addrs
:第一次出现bug时接收通知的电子邮件地址,只支持 Mailxsshkey
:用于与虚拟机通信的SSH密钥的位置sandbox
:沙盒模式,支持以下模式none
:默认设置,不做任何特殊的事情setuid
:冒充用户nobody(65534)namespace
:使用命名空间删除权限(内核需要设置CONFIG_NAMESPACES
,CONFIG_UTS_NS
,CONFIG_USER_NS
,CONFIG_PID_NS
和CONFIG_NET_NS
构建)
debug 参数和 bench参数 debug参数将VM所有输出打印到console帮助我们排查使用中出现的错误;bench参数定期将执行的统计信息写入我们指定的文件。
var (
flagConfig = flag.String("config", "", "configuration file")
flagDebug = flag.Bool("debug", false, "dump all VM output to console")
flagBench = flag.String("bench", "", "write execution statistics into this file periodically")
)
main
syz-manager/manager.go
功能:开启日志缓存,加载config文件,调用Runmananger
func main() {
if prog.GitRevision == "" {
log.Fatalf("bad syz-manager build: build with make, run bin/syz-manager")
}
flag.Parse()
log.EnableLogCaching(1000, 1<<20)//开启日志缓存,提高性能,超过1000行会被覆盖,或者 1MB 超过丢弃
cfg, err := mgrconfig.LoadFile(*flagConfig)//加载 config文件
if err != nil {
log.Fatalf("%v", err)
}
RunManager(cfg)//接着分析
}
RunManager(cfg)
功能:新开线程,定期记录VM状态,crash数量等信息,最后调用vmloop()
func RunManager(cfg *mgrconfig.Config) {
var vmPool *vm.Pool
// Type "none" is a special case for debugging/development when manager
// does not start any VMs, but instead you start them manually
// and start syz-fuzzer there.
if cfg.Type != "none" {// 将type指定为none是在调试/开发中用的,这样manager就不会启动VM而是需要手动启动
var err error
vmPool, err = vm.Create(cfg, *flagDebug)//创建 vmPool,一个vmpool可用于创建多个独立的VM,vm.go 对不同的虚拟化方案提供了统一的接口,会调用qemu.go:Ctor 函数 主要检查一些参数
if err != nil {
log.Fatalf("%v", err)
}
}
//crashdir 很明白
crashdir := filepath.Join(cfg.Workdir, "crashes")
osutil.MkdirAll(crashdir)
//reporter导出
reporter, err := report.NewReporter(cfg)
if err != nil {
log.Fatalf("%v", err)
}
mgr := &Manager{
cfg: cfg,
vmPool: vmPool,
target: cfg.Target,
sysTarget: cfg.SysTarget,
reporter: reporter,
crashdir: crashdir,
startTime: time.Now(),
stats: &Stats{haveHub: cfg.HubClient != ""},
crashTypes: make(map[string]bool),
corpus: make(map[string]CorpusItem),
disabledHashes: make(map[string]struct{}),
memoryLeakFrames: make(map[string]bool),
dataRaceFrames: make(map[string]bool),
fresh: true,
vmStop: make(chan bool),
hubReproQueue: make(chan *Crash, 10),
needMoreRepros: make(chan chan bool),
reproRequest: make(chan chan map[string]bool),
usedFiles: make(map[string]time.Time),
saturatedCalls: make(map[string]bool),
}
mgr.preloadCorpus()
mgr.initStats() // Initializes prometheus variables.
mgr.initHTTP() // Creates HTTP server.
mgr.collectUsedFiles()
// Create RPC server for fuzzers.
mgr.serv, err = startRPCServer(mgr)
if err != nil {
log.Fatalf("failed to create rpc server: %v", err)
}
if cfg.DashboardAddr != "" {
mgr.dash, err = dashapi.New(cfg.DashboardClient, cfg.DashboardAddr, cfg.DashboardKey)
if err != nil {
log.Fatalf("failed to create dashapi connection: %v", err)
}
}
if !cfg.AssetStorage.IsEmpty() {
mgr.assetStorage, err = asset.StorageFromConfig(cfg.AssetStorage, mgr.dash)
if err != nil {
log.Fatalf("failed to init asset storage: %v", err)
}
}
go func() { // [2] 新开线程,定期记录VM状态、crash数量等信息
for lastTime := time.Now(); ; {
time.Sleep(10 * time.Second)
now := time.Now()
diff := now.Sub(lastTime)
lastTime = now
mgr.mu.Lock()
if mgr.firstConnect.IsZero() {
mgr.mu.Unlock()
continue
}
mgr.fuzzingTime += diff * time.Duration(atomic.LoadUint32(&mgr.numFuzzing))
executed := mgr.stats.execTotal.get()
crashes := mgr.stats.crashes.get()
corpusCover := mgr.stats.corpusCover.get()
corpusSignal := mgr.stats.corpusSignal.get()
maxSignal := mgr.stats.maxSignal.get()
mgr.mu.Unlock()
numReproducing := atomic.LoadUint32(&mgr.numReproducing)
numFuzzing := atomic.LoadUint32(&mgr.numFuzzing)
log.Logf(0, "VMs %v, executed %v, cover %v, signal %v/%v, crashes %v, repro %v",
numFuzzing, executed, corpusCover, corpusSignal, maxSignal, crashes, numReproducing)
}
}()
if *flagBench != "" { //如果设置了 bench 参数,还要在指定的文件中记录一些信息。
mgr.initBench()
}
if mgr.dash != nil {
go mgr.dashboardReporter()
}
osutil.HandleInterrupts(vm.Shutdown)
if mgr.vmPool == nil {
log.Logf(0, "no VMs started (type=none)")
log.Logf(0, "you are supposed to start syz-fuzzer manually as:")
log.Logf(0, "syz-fuzzer -manager=manager.ip:%v [other flags as necessary]", mgr.serv.port)
<-vm.Shutdown
return
}
mgr.vmLoop()// [5] 主要调用 vmLoop()
}
- 根据传入的配置文件
cfg
创建一个虚拟机池vmPool
(如果配置类型不是 “none”)。 - 创建用于处理崩溃报告的
reporter
,并创建与之关联的 崩溃存储目录crashdir
。 - 创建
Manager
对象,并初始化其各个字段,包括统计信息、语料库等。 - 调用
preloadCorpus()
函数预加载语料库文件。 - 初始化
prometheus
变量和HTTP
服务器。 - 创建用于处理
RPC
的服务器。 - 如果已指定仪表盘地址,则创建
dashapi
连接。 - 根据配置文件中的参数,初始化资产存储对象
vmLoop()
功能:
将VM实例分为两个部分,一部分用于进行crash复现,另一部分用于进行fuzz。
说明:
- 变量说明:
reproQueue
—— 保存crash,可通过len(reproQueue) != 0
判断当前是否有等待复现的crash; [3]
:可以复现且有剩余的 instances,则复现crash;[4]
:没有可复现的但是有剩余的 instances,则进行fuzz;
func (mgr *Manager) vmLoop() {
...
canRepro := func() bool { // [2] 判断当前是否有等待复现的crash
return phase >= phaseTriagedHub && len(reproQueue) != 0 &&
(int(atomic.LoadUint32(&mgr.numReproducing))+1)*instancesPerRepro <= maxReproVMs
}
if shutdown != nil {
for canRepro() { // [3] 可以复现且有剩余的 instances, 则复现crash
vmIndexes := instances.Take(instancesPerRepro)// [3-1] 取 instancesPerRepro 个 (默认4) VM, 对crash进行复现
if vmIndexes == nil {
break
}
last := len(reproQueue) - 1
crash := reproQueue[last]
reproQueue[last] = nil
reproQueue = reproQueue[:last]
atomic.AddUint32(&mgr.numReproducing, 1)
log.Logf(1, "loop: starting repro of '%v' on instances %+v", crash.Title, vmIndexes)
go func() {
reproDone <- mgr.runRepro(crash, vmIndexes, instances.Put)// [3-2] crash 复现 runRepro() -> repro.Run() -> ctx.repro() !!!
}()
}
for !canRepro() { // [4] 没有可复现的但是有剩余的 instances, 则进行fuzz
idx := instances.TakeOne()// [4-1] 取 1 个 VM, 运行新的实例
if idx == nil {
break
}
log.Logf(1, "loop: starting instance %v", *idx)
go func() {
crash, err := mgr.runInstance(*idx)// [4-2] 启动fuzz, 监控信息并返回Report对象 runInstance() -> runInstanceInner() -> FuzzerCmd() & MonitorExecution() !!!
runDone <- &RunResult{*idx, crash, err}
}()
}
}
var stopRequest chan bool
if !stopPending && canRepro() {
stopRequest = mgr.vmStop
}
//下面的部分是主循环的无限循环体
wait:
select {
case <-instances.Freed: //当读取到instances.Freed通道时,表示某个虚拟机已经空闲,可以用于下一个测试或重现任务
// An instance has been released.
case stopRequest <- true://当收到stopRequest通道中的信号时,表示需要停止所有正在进行的测试和重现,并尽快关闭所有虚拟机。
log.Logf(1, "loop: issued stop request")
stopPending = true
case res := <-runDone://当从runDone通道中读取到一个结果时,表示某个测试或重现任务已经完成,该函数将更新其状态,并将虚拟机标记为空闲状态。
log.Logf(1, "loop: instance %v finished, crash=%v", res.idx, res.crash != nil)
if res.err != nil && shutdown != nil {
log.Logf(0, "%v", res.err)
}
stopPending = false
instances.Put(res.idx)
// On shutdown qemu crashes with "qemu: terminating on signal 2",
// which we detect as "lost connection". Don't save that as crash.
if shutdown != nil && res.crash != nil {
needRepro := mgr.saveCrash(res.crash)
if needRepro {
log.Logf(1, "loop: add pending repro for '%v'", res.crash.Title)
pendingRepro[res.crash] = true
}
}
case res := <-reproDone://当从reproDone通道中读取到一个结果时,表示某个崩溃的重现已经完成,该函数将更新其状态,并将重现结果保存到数据库中。
atomic.AddUint32(&mgr.numReproducing, ^uint32(0))
crepro := false
title := ""
if res.repro != nil {
crepro = res.repro.CRepro
title = res.repro.Report.Title
}
log.Logf(1, "loop: repro on %+v finished '%v', repro=%v crepro=%v desc='%v'",
res.instances, res.report0.Title, res.repro != nil, crepro, title)
if res.err != nil {
log.Logf(0, "repro failed: %v", res.err)
}
delete(reproducing, res.report0.Title)
if res.repro == nil {
if !res.hub {
mgr.saveFailedRepro(res.report0, res.stats)
}
} else {
mgr.saveRepro(res)
}
case <-shutdown://当从shutdown通道中读取到一个信号时,表示需要关闭所有虚拟机并结束程序执行。
log.Logf(1, "loop: shutting down...")
shutdown = nil
case crash := <-mgr.hubReproQueue://当从mgr.hubReproQueue通道中读取到一个新的崩溃时,表示外部系统(例如fuzzing hub)发送了一个可重现的崩溃,该函数将把它添加到重现队列中。
log.Logf(1, "loop: get repro from hub")
pendingRepro[crash] = true
case reply := <-mgr.needMoreRepros://当从mgr.needMoreRepros通道中读取到一个请求时,表示需要检查是否还有待处理的崩溃。如果所有崩溃都已处理完毕,则回复true;否则回复false。
reply <- phase >= phaseTriagedHub &&
len(reproQueue)+len(pendingRepro)+len(reproducing) == 0
goto wait
case reply := <-mgr.reproRequest://当从mgr.reproRequest通道中读取到一个请求时,表示需要返回正在进行的重现列表。该函数将获取所有正在进行的重现,并将它们的标题保存在repros映射中返回。
repros := make(map[string]bool)
for title := range reproducing {
repros[title] = true
}
reply <- repros
goto wait
}
}
}
该函数是一个无限循环,是整个系统的核心。它使用了多种并发和同步机制,并通过监听各种事件和状态变化来调度虚拟机运行和崩溃重现。以下是对该函数的详细介绍:
- 首先,该函数使用一个资源池(instances)来管理可用实例的索引号码。资源池存储在结构体Manager中,并在函数开头初始化。资源池可以检测已释放的实例并通知正在等待实例的goroutine。
- 接着,该函数根据配置设置每个崩溃需要重现的虚拟机数量,以及可以使用的最大虚拟机数。因此,在代码中,有两个变量instancesPerRepro和maxReproVMs,其值都与可用的虚拟机数有关。如果实际可用的虚拟机数小于要求,则会相应地更改instancesPerRepro的值。
- 为了管理虚拟机的运行和崩溃的重现,该函数创建了三个通道:runDone、reproDone和hubReproQueue。这些通道分别用于处理运行结果、重现结果和从集线器接收重现请求。当处理结果时,该函数会根据需要保存崩溃并加入重现队列等待重现。
- 在函数的主循环中,它会不断检查当前状态,并根据需要调整虚拟机的运行和崩溃的重现。该函数使用多个映射,包括pendingRepro、reproducing和reproQueue,来跟踪正在处理的重现。如果发现新的重现请求,则将其添加到pendingRepro中。
- 如果当前还有空闲实例,则会启动实例以进行测试,并异步地等待运行结果。如果存在可以使用的虚拟机并且重现队列不为空,则会开始重现流程。在启动重现之前,该函数会检查当前是否已经有足够数量的虚拟机正在工作(instancesPerRepro决定),以免占用太多资源。
- 为了更好地控制整个系统的状态,该函数使用许多事件通道来等待相应的事件发生,例如资源池中某个实例被释放、需要停止正在执行的测试或重现以及从集线器接收重现请求等。在监听这些事件时,它使用goto语句来重新进入等待状态,直到特定的事件发生并满足某些条件时再继续执行。
crash 复现
调用链:vmLoop()
-> mgr.runRepro()
-> repro.Run()
-> ctx.repro()
(重点函数)
位置:pkg/repro/repro.go: (*context).repro()
功能:crash 复现,提取出触发crash的C代码。
runRepro 测试崩溃,调用repro.run
func (mgr *Manager) runRepro(crash *Crash, vmIndexes []int, putInstances func(...int)) *ReproResult {
features := mgr.checkResult.Features
res, stats, err := repro.Run(crash.Output, mgr.cfg, features, mgr.reporter, mgr.vmPool, vmIndexes)
...
pkg/repro/repro.go Run
func Run(crashLog []byte, cfg *mgrconfig.Config, features *host.Features, reporter *report.Reporter,
vmPool *vm.Pool, vmIndexes []int) (*Result, *Stats, error) {
....
res, err := ctx.repro(entries, crashStart)
if err != nil {
return nil, nil, err
}
if res != nil {
ctx.reproLogf(3, "repro crashed as (corrupted=%v):\n%s",
ctx.report.Corrupted, ctx.report.Report)
// Try to rerun the repro if the report is corrupted.
for attempts := 0; ctx.report.Corrupted && attempts < 3; attempts++ {
ctx.reproLogf(3, "report is corrupted, running repro again")
if res.CRepro {
_, err = ctx.testCProg(res.Prog, res.Duration, res.Opts)
} else {
_, err = ctx.testProg(res.Prog, res.Duration, res.Opts)
}
if err != nil {
return nil, nil, err
}
}
ctx.reproLogf(3, "final repro crashed as (corrupted=%v):\n%s",
ctx.report.Corrupted, ctx.report.Report)
res.Report = ctx.report
}
return res, ctx.stats, nil
}
这部分代码是程序中的一个函数 Run,它会运行 fuzzing 过程并尝试重现崩溃。具体来说:
首先,该函数会调用上下文对象的 repro 方法来尝试重现崩溃,并将得到的结果保存到 res 变量中。
如果重现成功,则 res 将包含相关的信息,例如重现用时、产生崩溃的程序等。
如果重现失败,或者重现后报告被标记为 "corrupted",则该函数将尝试重新运行重现多达三次,以确保报告数据正确。
最终,该函数将返回重现结果 res、统计数据 ctx.stats 和可能出现的错误。
ctx.repro这个函数
repro函数
[2]
ctx.extractProg()
—— 提取出触发 crash 的程序;[3]
ctx.minimizeProg()
—— 若成功复现,则调用prog.Minimize()
,简化所有的调用和参数;[4]
ctx.extractC()
—— 生成C代码,编译成二进制文件,执行并检查是否crash;[5]
ctx.simplifyProg()
—— 进一步简化。在 repro.go 中定义了progSimplifies
数组作为简化规则,依次使用每一条规则后,如果crash还能被触发, 再调用extractC(res)
尝试提取 C repro;[6]
ctx.simplifyC()
—— 对提取出的C程序进行简化。 跟上面的ctx.simplifyProg(res)
差不多,就是规则使用了cSimplifies
数组;[5][6]
简化的是复现crash时设置的一些选项,比如线程、并发、沙盒等等。简化选项分别保存在progSimplifies
和cSimplifies
数组中。
func (ctx *context) repro(entries []*prog.LogEntry, crashStart int) (*Result, error) {
// Cut programs that were executed after crash.
for i, ent := range entries {
if ent.Start > crashStart {
entries = entries[:i]
break
}
}
reproStart := time.Now()
defer func() {
ctx.reproLogf(3, "reproducing took %s", time.Since(reproStart))
}()
res, err := ctx.extractProg(entries)// [2] 提取出触发 crash 的程序 !!!
if err != nil {
return nil, err
}
if res == nil {
return nil, nil
}
defer func() {
if res != nil {
res.Opts.Repro = false
}
}()
res, err = ctx.minimizeProg(res) // [3] 若成功复现, 则调用prog.Minimize(), 简化所有的调用和参数 !!!
if err != nil {
return nil, err
}
// Try extracting C repro without simplifying options first.
res, err = ctx.extractC(res)// [4] 生成C代码,编译成二进制文件,执行并检查是否crash,若crash则赋值 res.CRepro = crashed !!!
if err != nil {
return nil, err
}
// Simplify options and try extracting C repro.
if !res.CRepro {
res, err = ctx.simplifyProg(res)//[5] !!! 进一步简化。在 repro.go 中定义了 progSimplifies 数组作为简化规则,依次使用每一条规则后,如果crash还能被触发, 再调用 extractC(res) 尝试提取 C repro
if err != nil {
return nil, err
}
}
// Simplify C related options.
if res.CRepro {
res, err = ctx.simplifyC(res)// [6] 对提取出的C程序进行简化。 跟上面的ctx.simplifyProg(res)差不多,就是规则使用了cSimplifies数组。[5][6] 简化的是复现crash时设置的一些选项,比如线程、并发、沙盒等等。
if err != nil {
return nil, err
}
}
return res, nil
}
这个函数是 fuzzing 过程中重现崩溃的核心部分,它会尝试从记录的日志中提取触发崩溃的程序,并进行简化以便更容易重现崩溃。具体来说:
- 首先,该函数会根据记录的日志,找到触发崩溃的程序所在的
entries
,并将后续的所有执行记录截断。 - 然后,该函数调用
extractProg
方法来提取出触发崩溃的程序,并将结果保存到res
变量中。 - 如果成功提取出程序,则继续调用
minimizeProg
方法对其进行简化,以便更容易重现崩溃。 - 接下来,该函数会尝试提取 C 语言的重现程序,不论之前是否已经有过简化操作。
- 如果之前提取出的程序不是 C 语言程序,则调用
simplifyProg
方法对选项进行简化。 - 如果程序是 C 语言程序,则调用
simplifyC
方法对 C 相关的选项进行简化。 - 最后,函数返回重现结果
res
,以及可能出现的错误。
extractProg()
位置:pkg/repro/repro.go
功能:提取出触发 crash 的程序。
说明:按照时间从短到长, 从后向前, 从单个到多个的顺序复现crash。
[1]
:在所有程序 (用entries
数组存放) 中提取出每个proc所执行的最后一个程序;[2]
:将程序按倒序存放到lastEntries
(通常最后一个程序就是触发crash的程序);[3]
:不同类型的漏洞漏洞需要不同的复现时间, 复杂crash耗时长(eg, race);[4]
extractProgSingle()
—— 倒序执行单个程序, 若触发crash则返回;[5]
extractProgBisect()
—— 若单个程序无法触发crash, 则采用二分查找的方法找出哪几个程序一起触发crash。先调用bisectProgs()
进行分组,看哪一组可以触发crash。 !!!- 返回值是能触发crash的单个program或者能触发crash的programs的组合。
func (ctx *context) extractProg(entries []*prog.LogEntry) (*Result, error) {
ctx.reproLogf(2, "extracting reproducer from %v programs", len(entries))
start := time.Now()
defer func() {
ctx.stats.ExtractProgTime = time.Since(start)
}()
// Extract last program on every proc.
procs := make(map[int]int)
for i, ent := range entries {
procs[ent.Proc] = i
}
var indices []int
for _, idx := range procs {// [1] 在所有程序 (用entries数组存放) 中提取出每个proc所执行的最后一个程序
indices = append(indices, idx)
}
sort.Ints(indices)
var lastEntries []*prog.LogEntry
for i := len(indices) - 1; i >= 0; i-- {// [2] 将程序按倒序存放到 lastEntries (通常最后一个程序就是触发crash的程序)
lastEntries = append(lastEntries, entries[indices[i]])
}
for _, timeout := range ctx.testTimeouts {// [3] 不同类型的漏洞漏洞需要不同的复现时间, 复杂crash耗时长(eg, race)
// Execute each program separately to detect simple crashes caused by a single program.
// Programs are executed in reverse order, usually the last program is the guilty one.
res, err := ctx.extractProgSingle(lastEntries, timeout)// [4] 倒序执行单个程序, 若触发crash则返回
if err != nil {
return nil, err
}
if res != nil {
ctx.reproLogf(3, "found reproducer with %d syscalls", len(res.Prog.Calls))
return res, nil
}
// Don't try bisecting if there's only one entry.
if len(entries) == 1 {
continue
}
// [5] 若单个程序无法触发crash, 则采用二分查找的方法找出哪几个程序一起触发crash。先调用bisectProgs()进行分组,看哪一组可以触发crash。 !!!
// Execute all programs and bisect the log to find multiple guilty programs.
res, err = ctx.extractProgBisect(entries, timeout)
if err != nil {
return nil, err
}
if res != nil {
ctx.reproLogf(3, "found reproducer with %d syscalls", len(res.Prog.Calls))
return res, nil
}
}
ctx.reproLogf(0, "failed to extract reproducer")
return nil, nil
}
这个函数是 repro
方法的一部分,它的作用是从执行日志中提取出触发崩溃的程序。具体来说:
- 首先,该函数会对所有执行记录进行处理,找到每个进程(
proc
)所执行的最后一个程序,并将这些程序保存到lastEntries
变量中。 - 接下来,该函数会尝试按照一定顺序执行
lastEntries
中的每个程序,并检查是否有任何简单的崩溃。如果找到了崩溃程序,则返回包含相应信息的Result
对象。 - 如果没有找到崩溃程序,且执行记录
entries
中包含多个程序,则尝试执行所有程序,并使用二分法(bisect)来检查哪些程序可能导致崩溃。如果找到崩溃程序,则同样返回相应信息的Result
对象。 - 最后,如果无论如何都无法找到崩溃程序,则返回
nil
。
需要注意的是,该函数在具体实现上使用了许多其他辅助函数和数据类型,如 extractProgSingle
和 extractProgBisect
等。
Minimize()
调用链:ctx.minimizeProg()
-> prog.Minimize()
(重点函数)
位置:prog/minimization.go: Minimize()
功能:简化所有的调用和参数。
说明:
[1]
sanitizeFix()
—— 有些系统调用需要做一些特殊的处理;[2]
removeCalls()
—— 尝试逐个移除系统调用;[3]
:去除系统调用的无关参数;[4]
ctx.do()
—— 根据不同的参数类型调用不同的minimize函数func (typ *PtrType) minimize()
—— 如果参数是指针类型的,把指针或者指针指向的内容置空;func (typ *ArrayType) minimize()
—— 如果参数是数组类型的,尝试一个一个移除数组中的元素;
// Minimize calls and arguments.
func (ctx *context) minimizeProg(res *Result) (*Result, error) {
res.Prog, _ = prog.Minimize(res.Prog, -1, true,
调用Minimize 也就是 prog/minimization.go: Minimize()
// Minimize minimizes program p into an equivalent program using the equivalence
// predicate pred. It iteratively generates simpler programs and asks pred
// whether it is equal to the original program or not. If it is equivalent then
// the simplification attempt is committed and the process continues.
func Minimize(p0 *Prog, callIndex0 int, crash bool, pred0 func(*Prog, int) bool) (*Prog, int) {
pred := func(p *Prog, callIndex int) bool {
p.sanitizeFix()// [1] 有些系统调用需要做一些特殊的处理 !!!
p.debugValidate()
return pred0(p, callIndex)
}
name0 := ""
if callIndex0 != -1 {
if callIndex0 < 0 || callIndex0 >= len(p0.Calls) {
panic("bad call index")
}
name0 = p0.Calls[callIndex0].Meta.Name
}
// Try to remove all calls except the last one one-by-one.
p0, callIndex0 = removeCalls(p0, callIndex0, crash, pred)// [2] 尝试逐个移除系统调用
// Try to reset all call props to their default values.
p0 = resetCallProps(p0, callIndex0, pred)
// Try to minimize individual calls.
for i := 0; i < len(p0.Calls); i++ {// [3] 去除系统调用的无关参数
if p0.Calls[i].Meta.Attrs.NoMinimize {
continue
}
ctx := &minimizeArgsCtx{
target: p0.Target,
p0: &p0,
callIndex0: callIndex0,
crash: crash,
pred: pred,
triedPaths: make(map[string]bool),
}
again:
ctx.p = p0.Clone()
ctx.call = ctx.p.Calls[i]
for j, field := range ctx.call.Meta.Args {
if ctx.do(ctx.call.Args[j], field.Name, "") {// [4] 在do函数中,根据不同的参数类型调用不同的minimize函数 !!!
goto again
}
}
p0 = minimizeCallProps(p0, i, callIndex0, pred)
}
if callIndex0 != -1 {
if callIndex0 < 0 || callIndex0 >= len(p0.Calls) || name0 != p0.Calls[callIndex0].Meta.Name {
panic(fmt.Sprintf("bad call index after minimization: ncalls=%v index=%v call=%v/%v",
len(p0.Calls), callIndex0, name0, p0.Calls[callIndex0].Meta.Name))
}
}
return p0, callIndex0
}
这个函数实现了程序的最小化,旨在将一个程序简化为与其等价的最小形式。具体来说:
- 首先,该函数尝试逐个删除所有调用,直到只剩下最后一个调用。在每次删除前,调用
pred
判断是否仍然等效,如果是,则继续删除操作。 - 接下来,该函数尝试将所有调用的属性重置为默认值,并再次调用
pred
判断是否仍然等效。 - 最后,该函数对每个调用进行单独的最小化操作。对于每个调用,它会将参数传递给
do
方法,并生成新的程序。如果pred
返回 true,则表示新程序仍然等效,此时重复上述过程,直到不能再进一步简化为止。
需要注意的是,该函数在具体实现上使用了许多其他辅助函数和数据类型,如 removeCalls
和 resetCallProps
等。
extractC()
调用链:ctx.extractC()
-> ctx.testCProg()
-> inst.RunCProg()
-> csource.Write()
& csource.BuildNoWarn()
& inst.runBinary()
位置:pkg/instance/execprog.go: (*ExecProgInstance).RunCProg()
功能:生成C代码,编译成二进制文件,执行并检查是否crash。
说明:调用 csource.Write()
生成C代码; csource.BuildNoWarn()
编译出可执行文件; inst.runBinary()
执行二进制文件。
func (inst *ExecProgInstance) RunCProg(p *prog.Prog, duration time.Duration,
opts csource.Options) (*RunResult, error) {
src, err := csource.Write(p, opts)
if err != nil {
return nil, err
}
inst.Logf(2, "testing compiled C program (duration=%v, %+v): %s", duration, opts, p)
return inst.RunCProgRaw(src, p.Target, duration)
}
func (inst *ExecProgInstance) RunCProgRaw(src []byte, target *prog.Target,
duration time.Duration) (*RunResult, error) {
bin, err := csource.BuildNoWarn(target, src)
if err != nil {
return nil, err
}
defer os.Remove(bin)
return inst.runBinary(bin, duration)
这两个函数是 ExecProgInstance
结构体的方法,用于执行经过编译的 C 程序。具体来说:
RunCProg
方法接收一个 Go 语言的程序p
,先将其转换为 C 语言源代码,并使用csource.Options
对象指定编译选项,最后调用RunCProgRaw
方法执行编译好的二进制文件。RunCProgRaw
方法接收一个二进制文件(已经通过csource.BuildNoWarn
编译好了),并在执行过程中限制运行时间不超过duration
。如果运行成功,则返回包含相应信息的RunResult
对象,否则返回错误信息。
需要注意的是,这两个方法都使用了 os.Remove
函数删除了生成的临时文件
simplifyProg()
调用链: simplifyProg() -> testProg() -> testProgs ->testWithInstance & RunSyzProg
位置:
// Simplify repro options (threaded, sandbox, etc).
func (ctx *context) simplifyProg(res *Result) (*Result, error) {
ctx.reproLogf(2, "simplifying guilty program options")
start := time.Now()
defer func() {
ctx.stats.SimplifyProgTime = time.Since(start)
}()
// Do further simplifications.
for _, simplify := range progSimplifies {
opts := res.Opts
if !simplify(&opts) || !checkOpts(&opts, ctx.timeouts, res.Duration) {
continue
}
crashed, err := ctx.testProg(res.Prog, res.Duration, opts)
if err != nil {
return nil, err
}
if !crashed {
continue
}
res.Opts = opts
// Simplification successful, try extracting C repro.
res, err = ctx.extractC(res)
if err != nil {
return nil, err
}
if res.CRepro {
return res, nil
}
}
return res, nil
}
这个函数实现了对程序参数的简化,以便更容易地重现导致崩溃或错误的场景。具体来说:
- 在执行过程中,该函数首先遍历
progSimplifies
列表中的每个函数,并尝试将结果应用于Result
对象的选项。 - 如果简化后的选项合法,则使用
ctx.testProg
函数测试是否可以在新的选项下触发相同的错误。如果测试成功,则将简化后的选项保存到res.Opts
中,并进一步尝试提取 C 语言重现文件。 - 如果最终提取出 C 重现文件成功,则返回包含此信息的
Result
对象;否则,该函数将继续尝试其他简化方法,直到所有方法都被尝试完毕为止。
需要注意的是,在执行过程中,该函数会记录运行时间、统计信息等,并在最后返回处理后的 Result
对象。
testProgs
func (ctx *context) testProgs(entries []*prog.LogEntry, duration time.Duration, opts csource.Options) (
crashed bool, err error) {
if len(entries) == 0 {
return false, fmt.Errorf("no programs to execute")
}
pstr := encodeEntries(entries)
program := entries[0].P.String()
if len(entries) > 1 {
program = "["
for i, entry := range entries {
program += fmt.Sprintf("%v", len(entry.P.Calls))
if i != len(entries)-1 {
program += ", "
}
}
program += "]"
}
ctx.reproLogf(2, "testing program (duration=%v, %+v): %s", duration, opts, program)
ctx.reproLogf(3, "detailed listing:\n%s", pstr)
//重点在下面
return ctx.testWithInstance(func(inst *instance.ExecProgInstance) (*instance.RunResult, error) {
return inst.RunSyzProg(pstr, duration, opts)
})
}
这个函数实际上是一个调用 ctx.testWithInstance
函数的简写方式,其中传递了一个匿名函数作为参数。具体来说:
- 匿名函数的定义与类型为
func(*instance.ExecProgInstance) (*instance.RunResult, error)
,即它接收一个*instance.ExecProgInstance
类型的参数,返回一个*instance.RunResult
类型的对象以及一个错误对象(如果有)。 - 在执行过程中,该匿名函数将调用
inst.RunSyzProg
方法来执行 Syzkaller 程序,并返回相应的结果和错误值。 - 该函数将该匿名函数作为参数传递给
ctx.testWithInstance
函数,然后将其返回值(即RunResult
对象和错误值)返回给调用者。
需要注意的是,ctx.testWithInstance
函数用于提供一个执行环境来运行程序,并在整个测试过程中跟踪统计信息。因此,这个函数的调用依赖于是否正确设置了 context
对象。
启动Fuzz
调用链:vmLoop()
-> mgr.runInstance()
-> mgr.runInstanceInner()
位置:syz-manager/manager.go: (*Manager).runInstanceInner()
功能:负责启动 syz-fuzzer
。
说明:
[1]
:将syz-fuzzer
复制到VM中;[2]
:将syz-executor
复制到VM中;[3]
FuzzerCmd()
—— 构造好命令,通过ssh执行syz-fuzzer
;
# fuzz命令示例
/syz-fuzzer -executor=/syz-executor -name=vm-0 -arch=amd64 -manager=10.0.2.10:33185 -procs=1 -leak=false -cover=true -sandbox=none -debug=true -v=100
[4]
MonitorExecution()
—— 监控, 检测输出中的内核oops信息、丢失连接、挂起等等。
func (mgr *Manager) runInstanceInner(index int, instanceName string) (*report.Report, []byte, error) {
inst, err := mgr.vmPool.Create(index)
if err != nil {
return nil, nil, fmt.Errorf("failed to create instance: %v", err)
}
defer inst.Close()
fwdAddr, err := inst.Forward(mgr.serv.port)
if err != nil {
return nil, nil, fmt.Errorf("failed to setup port forwarding: %v", err)
}
fuzzerBin, err := inst.Copy(mgr.cfg.FuzzerBin)// [1] 将 syz-fuzzer 复制到VM中
if err != nil {
return nil, nil, fmt.Errorf("failed to copy binary: %v", err)
}
// If ExecutorBin is provided, it means that syz-executor is already in the image,
// so no need to copy it.
executorBin := mgr.sysTarget.ExecutorBin
if executorBin == "" {
executorBin, err = inst.Copy(mgr.cfg.ExecutorBin)// [2] 将 syz-executor 复制到VM中
if err != nil {
return nil, nil, fmt.Errorf("failed to copy binary: %v", err)
}
}
fuzzerV := 0
procs := mgr.cfg.Procs
if *flagDebug {
fuzzerV = 100
procs = 1
}
// Run the fuzzer binary.
start := time.Now()
atomic.AddUint32(&mgr.numFuzzing, 1)
defer atomic.AddUint32(&mgr.numFuzzing, ^uint32(0))
args := &instance.FuzzerCmdArgs{
Fuzzer: fuzzerBin,
Executor: executorBin,
Name: instanceName,
OS: mgr.cfg.TargetOS,
Arch: mgr.cfg.TargetArch,
FwdAddr: fwdAddr,
Sandbox: mgr.cfg.Sandbox,
Procs: procs,
Verbosity: fuzzerV,
Cover: mgr.cfg.Cover,
Debug: *flagDebug,
Test: false,
Runtest: false,
Optional: &instance.OptionalFuzzerArgs{
Slowdown: mgr.cfg.Timeouts.Slowdown,
RawCover: mgr.cfg.RawCover,
SandboxArg: mgr.cfg.SandboxArg,
},
}
cmd := instance.FuzzerCmd(args)// [3] 调用 FuzzerCmd() 通过ssh执行 syz-fuzzer !!!
outc, errc, err := inst.Run(mgr.cfg.Timeouts.VMRunningTime, mgr.vmStop, cmd)
if err != nil {
return nil, nil, fmt.Errorf("failed to run fuzzer: %v", err)
}
var vmInfo []byte
rep := inst.MonitorExecution(outc, errc, mgr.reporter, vm.ExitTimeout)// [4] 监控, 检测输出中的内核oops信息、丢失连接、挂起等等。
if rep == nil {
// This is the only "OK" outcome.
log.Logf(0, "%s: running for %v, restarting", instanceName, time.Since(start))
} else {
vmInfo, err = inst.Info()
if err != nil {
vmInfo = []byte(fmt.Sprintf("error getting VM info: %v\n", err))
}
}
return rep, vmInfo, nil
}
参考
https://bsauce.github.io/2022/05/14/syzkaller2/
https://xz.aliyun.com/t/5154
https://bbs.kanxue.com/thread-268152.htm
https://github.com/google/syzkaller