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 中运行的测试程序

架构图如下:

syzkaller 的进程结构

在本文中,我将先介绍 syz-extract 和 syz-sysgen 的源码。

在本系列源码阅读笔记中,所有涉及到的 arch 和 platform 均为 x86_64 linux,不再另行说明。

syzkaller git checkout: 3a9d0024ba818c5b37058d9ac6fdfc0ddfa78be6

checkout Date: Fri Nov 19 13:06:38 2021 +0100

二、syz-extract

用途:解析并获取 syzlang 文件中的常量所对应的具体整型,并将结果存放至 xxx.txt.const 文件中。

1. main

syz-extract main 函数位于 sys/syz-extract/extract.go 中。

首先,syz-extract 将会尝试解析传入的参数:

1
2
3
4
5
// Kiprey: in Function `main` 
flag.Parse()
if *flagBuild && *flagBuildDir != "" {
tool.Failf("-build and -builddir is an invalid combination")
}

其参数列表如下:

1
2
3
4
5
6
7
8
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)")
)

之后是调用 archFileList 函数,解析传入的参数,并生成对应的返回值。

其中

  • OS 为操作系统字符串
  • archArray 为待生成的 arch 字符串数组
  • files 为待分析的 syzlang 文件名 字符串数组
1
2
3
4
5
// Kiprey: in Function `main` 
OS, archArray, files, err := archFileList(*flagOS, *flagArch, flag.Args())
if err != nil {
tool.Fail(err)
}

接下来,便是尝试获取 OS 所对应的 Extractor 结构体;如果 OS 不存在则肯定取不到,直接报错:

1
2
3
4
5
// Kiprey: in Function `main` 
extractor := extractors[OS]
if extractor == nil {
tool.Failf("unknown os: %v", OS)
}

extractors 数组如下所示,该数组为不同的 OS 实例化了不同的 Extractor 类。其中 linux OS 所对应的 Extractor 实例(即那三个函数的实现)位于 sys/syz-extract/linux.go 中:

三个函数的实现我们稍后再看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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), // sys/syz-extract/linux.go
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 结构体数组

1
2
3
4
5
6
7
8
9
// Kiprey: in function `main`
arches, err := createArches(OS, archArray, files)
if err != nil {
tool.Fail(err)
}
if *flagSourceDir == "" {
tool.Fail(fmt.Errorf("provide path to kernel checkout via -sourcedir " +
"flag (or make extract SOURCEDIR)"))
}

准备工作已经做的差不多了,接下来让 extractor 执行初始化操作:

1
2
3
4
// Kiprey: in function main
if err := extractor.prepare(*flagSourceDir, *flagBuild, arches); err != nil {
tool.Fail(err)
}

这一步实际上会调用到 sys/syz-extract/linux.go 中的 prepare 函数:

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
// Kiprey: in sys/syz-extract/linux.go
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 中执行。

1
2
3
4
5
6
7
8
9
jobC := make(chan interface{}, len(archArray)*len(files))
// 将 arch 结构体放置进 jobC 管道中
for _, arch := range arches {
jobC <- arch
}

for p := 0; p < runtime.GOMAXPROCS(0); p++ {
go worker(extractor, jobC)
}

worker 启动后,main 函数就需要等待 worker 处理完成后才能保存处理结果至文件中,这就涉及到了线程协同。注意到代码中有 <-arch.done<-f.done 语句,这两个语句会一直阻塞等待管道,直到其传来信息。若 worker 函数中对管道执行 close 操作,则被关闭的管道将不再等待,继续向下执行。因此这里 syz-extract 就利用了管道来完成线程协同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Kiprey: in function `main`
constFiles := make(map[string]*compiler.ConstFile)
for _, file := range files {
constFiles[file] = compiler.NewConstFile()
}
for _, arch := range arches {
fmt.Printf("generating %v/%v...\n", arch.target.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
}
constFiles[f.name].AddArch(f.arch.target.Arch, f.consts, f.undeclared)
}
}

后面的代码内容便是将生成结果保存进 .const 文件中,没有其他有意思的东西了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Kiprey: in function `main`
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)
}

2. archFileList

archFileList 函数用于解析传入的参数信息,代码量非常短。

首先,调用者需要将 OS 字符串arch 字符串,以及存放 syzlang 文件路径的字符串数组传入该函数:

1
2
func archFileList(os, arch string, files []string) 
(string, []string, []string, error)

之后,archFileList 会对 android 设置一些特殊的字段,然后切割参数字符串 arch,并将切割后的结果全保存进字符串数组 arches 中。若没有指定 arches 参数,则添加全部的 arch 进 arches 数组中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Kiprey: in archFileList Function
// Note: this is linux-specific and should be part of Extractor and moved to linux.go.
android := false
if os == "android" {
android = true
os = targets.Linux
}
var arches []string
if arch != "" {
arches = strings.Split(arch, ",")
} else {
for arch := range targets.List[os] {
arches = append(arches, arch)
}
if android {
arches = []string{targets.I386, targets.AMD64, targets.ARM, targets.ARM64}
}
sort.Strings(arches)
}

其中,targets.List 是一个 map 映射(即 sys/targets/targets.go 中的 List 变量),这上面存放了很多关于不同 OS 以及这些 OS 在特定 arch 下的信息,以下是一个精简后的代码片段:

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
// nolint: lll
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",
},
...
},
...
}

不过在 for arch := range targets.List[os] 的过程中,只会取出这些 map 的 key 值,即一系列的架构字符串,因此最后 archs 数据中存放的值如下:

image-20220309090646115

接下来我们回到函数 archFileList 中:

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
// Kiprey: in archFileList Function
if len(files) == 0 {
matches, err := filepath.Glob(filepath.Join("sys", os, "*.txt"))
if err != nil || len(matches) == 0 {
return "", nil, nil, fmt.Errorf("failed to find sys files: %v", err)
}
manualFiles := map[string]bool{
// Not upstream, generated on https://github.com/multipath-tcp/mptcp_net-next
"vnet_mptcp.txt": true,
// Was in linux-next, but then was removed, fate is unknown.
"dev_watch_queue.txt": true,
// Not upstream, generated on:
// https://chromium.googlesource.com/chromiumos/third_party/kernel d2a8a1eb8b86
"dev_bifrost.txt": true,
// ION support was removed from kernel.
// We plan to leave the descriptions for some time as is and later remove them.
"dev_ion.txt": true,
// Not upstream, generated on unknown tree.
"dev_img_rogue.txt": true,
}
androidFiles := map[string]bool{
"dev_tlk_device.txt": true,
// This was generated on:
// https://source.codeaurora.org/quic/la/kernel/msm-4.9 msm-4.9
"dev_video4linux.txt": true,
// This was generated on:
// https://chromium.googlesource.com/chromiumos/third_party/kernel 3a36438201f3
"fs_incfs.txt": true,
}
for _, f := range matches {
f = filepath.Base(f)
if manualFiles[f] || os == targets.Linux && android != androidFiles[f] {
continue
}
files = append(files, f)
}
sort.Strings(files)
}

若传入的参数 files 为空,则 syz-extract 将尝试自动添加文件进入。在这一部分代码中:

1
2
3
4
matches, err := filepath.Glob(filepath.Join("sys", os, "*.txt"))
if err != nil || len(matches) == 0 {
return "", nil, nil, fmt.Errorf("failed to find sys files: %v", err)
}

syz-extract 将尝试解析路径 sys/linux/*.txt 路径,并将解析结果存放进 matches 数组中:

image-20220309090909852

之后,在下面的代码中,跳过人工添加的文件,以及 android 不允许添加的文件(androidFiles 映射中 value 为 false 的条目),最后为结果数组做个顺序排序:

1
2
3
4
5
6
7
8
9
// Kiprey: in archFileList Function
for _, f := range matches {
f = filepath.Base(f)
if manualFiles[f] || os == targets.Linux && android != androidFiles[f] {
continue
}
files = append(files, f)
}
sort.Strings(files)

函数结束,结果返回:

1
2
// Kiprey: in archFileList Function
return os, arches, files, nil

3. createArches

该函数用于生成与参数对应的 Arch 结构体数组。该函数内容较少,因此笔记以注释形式内嵌在函数中:

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
func createArches(OS string, archArray, files []string) ([]*Arch, error) {
var arches []*Arch
// 遍历 archArray 结构体
for _, archStr := range archArray {
// 尝试确定 buid 文件夹路径
buildDir := ""
if *flagBuild {
dir, err := ioutil.TempDir("", "syzkaller-kernel-build")
if err != nil {
return nil, fmt.Errorf("failed to create temp dir: %v", err)
}
buildDir = dir
} else if *flagBuildDir != "" {
buildDir = *flagBuildDir
} else {
buildDir = *flagSourceDir
}
// 获取 targets.List 中对应与 OS 和 arch 的 `Target` 结构体
target := targets.Get(OS, archStr)
if target == nil {
return nil, fmt.Errorf("unknown arch: %v", archStr)
}
// 创建 arch 结构体
arch := &Arch{
// 存放特定 OS 特定 arch 的一些信息
target: target,
// kernel source 路径
sourceDir: *flagSourceDir,
// kernel source header 路径
includeDirs: *flagIncludes,
// build 路径
buildDir: buildDir,
// bool 值,是否需要重新生成架构指定的 kernel header
build: *flagBuild,
// 管道,用于 go routine 间通信。当 arch 分析完成后,将会向该管道通知
done: make(chan bool),
}
// 将 syzlang 文件名数组添加进 arch 结构体中
for _, f := range files {
arch.files = append(arch.files, &File{
arch: arch,
name: f,
// 管道,用于 go routine 间通信。当 file 分析完成后,将会向该管道通知
done: make(chan bool),
})
}
// 将新创建的 arch 结构体放置进 arches 数组中
arches = append(arches, arch)
}
return arches, nil
}

4. worker

worker 用于执行真正的解析变量工作:

1
func worker(extractor Extractor, jobC chan interface{})

对于管道 jobC 中的元素来说,初始时在 main 函数放进去的肯定是 Arch 结构体:

image-20220309095730698

因此初始时 worker 内部的 switch 将检测到传入的变量类型为 Arch 结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Kiprey: in function `worker`
for job := range jobC {
// 为 j 赋值为 jobC 管道中的对象,初始时为 Arch
switch j := job.(type) {
// 最开始的时候肯定会走入这个分支
case *Arch:
// 执行 processArch,生成 const 信息
infos, err := processArch(extractor, j)
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
}
}
case *File:
j.consts, j.undeclared, j.err = processFile(extractor, j.arch, j)
close(j.done)
}
}

注意到变量 j 就是从 jobC 中取出来的 Arch 结构体,因此在 processArch 操作完成后,worker 函数会分别从 infos 映射中遍历取出对应文件的信息,并将其填充至 arch 结构体files 结构体数组内的各个元素字段里:

image-20220309111211911

最后执行 jobC <- f 操作,将这个 File 结构体放入 jobC 管道中。

由于 worker 函数是会循环读取 jobC 内数据,因此 worker 函数接下来便会取出刚刚新放入的 File 结构体,执行 processFile 函数。在 processFile 中,syz-extract 将会获取各个 const 变量(例如 O_RDWR)所对应的整型值(例如2)。

worker 函数中还有一个关键点需要注意,当 processXXX 函数执行完成后,worker 函数接下来都会执行 close(j.done) ,将通信管道关闭。这样做的目的是为了通知 main goroutine “某部分工作已经完成”。这个操作有点类似于使用信号量来保证线程同步。

5. processArch

processArch 的作用是,处理传入的 Extractor 和 Arch 结构体,生成 const 信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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
}

其中,compiler.ExtractConsts 只是一个简单的 wrapper 函数,获取编译 syzlang 结果中的 fileConsts 字段:

image-20220309104824331

字段 res.fileConsts 包含了 syzlang 文件名与其用到的常量数组的映射,以及其所 include 的头文件数组的映射;这些东西都将会用到获取 consts 对应的具体整数操作中。

extractor.prepareArch 函数在 linux.go 中,做的操作主要是定义了几个头文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
"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 返回给调用者:

image-20220309110405501

6. processFile

processFile 函数只是 extractor.processFile 的 wrapper,主要是做了一些 check 操作:

1
2
3
4
5
6
7
8
9
10
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)
}

实际用于查找 const 值的操作位于 extractor.processFile

1
func (*linux) processFile(arch *Arch, info *compiler.ConstInfo) (map[string]uint64, map[string]bool, error)

在 linux.go 中,processFile 初始时先过滤掉不满足条件的情况:

1
2
3
4
5
6
7
8
9
// Kiprey: in function processFile of sys/syz-extract/linux.go
if strings.HasSuffix(info.File, "_kvm.txt") &&
(arch.target.Arch == targets.ARM || arch.target.Arch == targets.RiscV64) {
// Hack: KVM is not supported on ARM anymore. We may want some more official support
// for marking descriptions arch-specific, but so far this combination is the only
// one. For riscv64, KVM is not supported yet but might be in the future.
// Note: syz-sysgen also ignores this file for arm and riscv64.
return nil, nil, nil
}

之后,生成编译代码模板所要用到的 gcc 编译参数:

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
// Kiprey: in function processFile of sys/syz-extract/linux.go
headerArch := arch.target.KernelHeaderArch
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)
}
}

参数有亿点点多:

image-20220309113521638

在准备好参数之后,processFile 还准备了 extract 参数,以及待使用的 CC 编译器,之后执行更加核心的 extract 函数,生成出 res 映射和 undeclared 集合:

1
2
3
4
5
6
7
8
9
10
11
// Kiprey: in function processFile of sys/syz-extract/linux.go
params := &extractParams{
AddSource: "#include <asm/unistd.h>",
ExtractFromELF: true,
TargetEndian: arch.target.HostEndian,
}
cc := arch.target.CCompiler
res, undeclared, err := extract(info, cc, args, params)
if err != nil {
return nil, nil, err
}

image-20220309113727970

其中,res 是 const 字符串与整型的映射;undeclared 是未声明 const 字符串与 bool 值的映射,通常这里的 bool 值都为 true:

undeclared 所对应的常量将在 .const 文件中标明其值为 ???

例如:

1
2
O_RDWR = 2
MyConst = ???

image-20220309132346585

执行完成 extract 函数后,如果当前架构为 32 位,则 syz-extract 需要使用 mmap2 来替换 mmap,以避免一些可能的错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if arch.target.PtrSize == 4 {
// 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)
}
}

替换完成后将结果返回:

1
return res, undeclared, nil

以上内容便是 extractor.processFile 的源码解释,接下来我们深入一下 extract 函数。

7. extract

函数代码位于 sys/syz-extract/fetch.go

该函数调用编译器来编译代码模板,并根据编译出的二进制文件来获取 consts 常量整数。若编译过程出错,则会尝试自动纠错。

函数声明:

1
2
func extract(info *compiler.ConstInfo, cc string, args []string, params *extractParams) 
map[string]uint64, map[string]bool, error)

其中参数 Info 便是单个文件存放 const 数据的结构体,cc 是编译器名称字符串,args 是编译器执行参数,params 是用于 extract 执行过程用的选项:

image-20220309133255996

初始时,extract 函数声明一系列的 map:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Kiprey: in function `extract`
data := &CompileData{
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
}

接下来便是尝试将 consts 常量字符串与模板C代码结合,并编译结合后的代码,形成一个可执行文件。编译操作由 compile 函数完成,其返回结果分别为编译出的可执行文件路径;编译器标准输出信息;编译器标准错误信息:

1
2
3
4
5
6
7
8
9
// Kiprey: in function `extract`
for {
bin1, out, err := compile(cc, args, data)
if err == nil {
bin = bin1
break
}
...
}

我们先深入进 compile 函数看看,该函数非常的简单,因此将笔记内联进代码中:

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

执行至该函数入口时,其参数示例如下:

image-20220309134856818

现在我们看看是什么样的代码模板:

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
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 字符串所对应的数值是多少。

回到 extract 函数,由于编写 syzlang 时极易出问题,因此 syz-extract 需要尝试自动纠错:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// Kiprey: in function `extract`
for {
bin1, out, err := compile(cc, args, data)
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{
`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)
// 如果匹配到了,则将出问题的常量取出至 undeclared 中
for _, match := range matches {
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)
}
// 重置编译用的 consts 数组
data.Values = nil
// 将出错的 consts 剔除,并将剩余没出错的 consts 存入编译用的 consts 数组
for _, v := range info.Consts {
if undeclared[v] {
continue
}
data.Values = append(data.Values, v)
}
// 这部分代码没咋看懂,因为 data.Includes 没有被重置,没必要重复添加
data.Includes = nil
for _, v := range info.Includes {
// missingIncludes 没有初始化,因此是个一直为空的变量
if missingIncludes[v] {
continue
}
data.Includes = append(data.Includes, v)
}
}

之后便是从编译出的二进制文件中读取数值,解析并返回:

注意:虽然 syz-extract 立即对编译出的二进制文件执行 remove 操作,但由于 syz-extract 仍然持有该文件的文件描述符,因此该文件将不会立即被删除,而是等到 syz-extract 释放了该文件的文件描述符后才会被删除。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 将新编译出的二进制文件删除
defer os.Remove(bin)

var flagVals []uint64
var err error
if data.ExtractFromELF {
flagVals, err = extractFromELF(bin, params.TargetEndian)
} else {
flagVals, err = extractFromExecutable(bin)
}
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

操作二进制文件的代码主要是这几行:

1
2
3
4
5
if data.ExtractFromELF {
flagVals, err = extractFromELF(bin, params.TargetEndian)
} else {
flagVals, err = extractFromExecutable(bin)
}

若 ExtractFromELF 字段为 false,则 sys-extract 会走下面这个分支,执行函数 extractFromExecutable。该函数将实际执行目标程序,解析其输出并转换为整型数组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func extractFromExecutable(binFile string) ([]uint64, error) {
out, err := osutil.Command(binFile).CombinedOutput()
if err != nil {
return nil, fmt.Errorf("failed to run flags binary: %v\n%s", err, out)
}
if len(out) == 0 {
return nil, nil
}
var vals []uint64
for _, val := range strings.Split(string(out), " ") {
n, err := strconv.ParseUint(val, 10, 64)
if err != nil {
return nil, fmt.Errorf("failed to parse value: %v (%v)", err, val)
}
vals = append(vals, n)
}
return vals, nil
}

但由于 OS 为 Linux 时,其 ExtractFromELF 标志为 true,因此会执行 extractFromELF 函数。在该函数中, syz-extract 将不会实际执行程序,而是从 ELF 文件中一个名为 syz_extract_data 的 section 中读取常量值

而且也执行不起来,因为先前手动不让二进制文件执行 link 操作,还没 main 函数。

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
func extractFromELF(binFile string, targetEndian binary.ByteOrder) ([]uint64, error) {
f, err := os.Open(binFile)
if err != nil {
return nil, err
}
ef, err := elf.NewFile(f)
if err != nil {
return nil, err
}
for _, sec := range ef.Sections {
if sec.Name != "syz_extract_data" {
continue
}
data, err := ioutil.ReadAll(sec.Open())
if err != nil {
return nil, err
}
vals := make([]uint64, len(data)/8)
if err := binary.Read(bytes.NewReader(data), targetEndian, &vals); err != nil {
return nil, err
}
return vals, nil
}
return nil, fmt.Errorf("did not find syz_extract_data section")
}

这样做的目的貌似是为了提高常量读取速度,因为读取文件远比执行程序来的快。

8. 小结

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,是为了能达到边进行常量提取,边将先前已有的提取结果存放进文件中,这样做是为了提高效率,加快常量提取的速度。

调试用的 vscode launch.json 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"version": "0.2.0",
"configurations": [
{
"name": "syzextractLaunch",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${fileDirname}",
"env": {},
"cwd": "/usr/class/syzkaller",
"args": ["-sourcedir", "/usr/class/linux", "-arch", "amd64"]
}
]
}

三、syz-sysgen

代码位于 sys/syz-sysgen/sysgen.go 中。

syz-gen 用于解析人工编写的 syzlang 代码文件,并将其 syzlang 内部定义的 syscall 类型信息转换成后续 syzkaller 能够使用的数据结构。

在理解了 syz-extract 的代码后,syz-sysgen 的代码相对来说也比较好理解,接下来我们先从 main 函数开始看起。

1. main

首先是将所有 OS 的类型都取出来,并且创建了用于存储结果的结构体:

1
2
3
4
5
6
7
8
9
10
// Kiprey:in Function main
defer tool.Init()()

var OSList []string
for OS := range targets.List {
OSList = append(OSList, OS)
}
sort.Strings(OSList)

data := &ExecutorData{}

其中第一行的 golang defer 关键字表示,defer 后面的函数将在整个函数正常返回时被执行。由于 tool.Init() 涉及到命令行中 CPU/Mem 分析,不在我们的考虑范畴,因此忽略不看。

完成这段代码的执行后,其变量情况如下图所示:

image-20220309165347651

紧接着便是一个 for 循环,遍历 OSList 中的每个 OS 字符串,并解析其中的 syzlang 代码。我将这个 for 循环分为了上中下三个部分:

  • 首先是第一部分:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // Kiprey:in Function main
    for _, OS := range OSList {
    descriptions := ast.ParseGlob(filepath.Join(*srcDir, "sys", OS, "*.txt"), nil)
    if descriptions == nil {
    os.Exit(1)
    }
    constFile := compiler.DeserializeConstFile(filepath.Join(*srcDir, "sys", OS, "*.const"), nil)
    if constFile == nil {
    os.Exit(1)
    }
    osutil.MkdirAll(filepath.Join(*outDir, "sys", OS, "gen"))

    var archs []string
    for arch := range targets.List[OS] {
    archs = append(archs, arch)
    }
    sort.Strings(archs)

    ...
    }

    这部分内容较为简单,将当前遍历到的 OS 所对应的 sys/<os>/*.txtsys/<os>/*.const文件,分别解析成 AST 树 (ast.Description 类型) 和 ConstFile 结构体。之后创建 sys/<os>/gen 文件夹,整个 syz-sysgen 的输出将存放在该文件夹下:

    image-20220309170145753

    之后还是收集当前 OS 所对应的全部 arch 字符串集合,并做一次排序操作。

  • 其次是第二部分:

    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
    // Kiprey:in Function main
    for _, OS := range OSList {
    ...

    var jobs []*Job
    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
    wg.Add(len(jobs))

    for _, job := range jobs {
    job := job
    go func() {
    defer wg.Done()
    processJob(job, descriptions, constFile)
    }()
    }
    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 关键头文件代码所用。

  • 最后是第三部分:

    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
    // Kiprey:in Function main
    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 设置一些字段:

1
2
3
4
5
6
7
8
9
10
11
12
attrs := reflect.TypeOf(prog.SyscallAttrs{})
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),
})
})

这部分代码乍看上去可能不太能理解,但仔细一看就能发现,它只是分别将 prog.SyscallAttrsprog.CallProps 这两个结构体对应的字段名存了起来。俩结构体声明如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 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
}

// 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"`
}

实际保存进变量 data 中的内容如下:

image-20220309231414961

通过对上面源码的分析,我发现貌似 syz-sysgen 将整个 prog.SyscallAttrs 结构体的字段名和每个 syscall 所对应的数据,全都转换成了普通字符串型和整型。看上去这像是要用这些数据来填充 C 语言模板?我们接下来再来看看 writeExecutorSyscalls 函数,看看这里面具体是做了什么。

writeExecutorSyscalls 函数源码分析位于下文,这里不再赘述。

2. processJob

processJob 函数的主要功能是:编译传入的 syzlang AST,分析其中的 syscall 类型信息等,并反序列化为一个 golang 语法源码。

传入 processJob 的参数 job,其结构体声明如下所示:

1
2
3
4
5
6
7
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 字符串->整型映射表:

1
2
3
4
5
6
// Kiprey: in function `processJob`
eh := func(pos ast.Pos, msg string) {
job.Errors = append(job.Errors, fmt.Sprintf("%v: %v\n", pos, msg))
}
consts := constFile.Arch(job.Target.Arch)
top := descriptions

image-20220309171903363

之后,对于一些 Linux OS 需要特殊处理的架构,syz-sysgen 设置了过滤器,过滤掉那些文件名中带有 _kvm.txt 后缀的 syzlang,那些 syzlang 将不参与处理;并且将那些不支持的条目将会存放进 job.Unsupported 中,接下来的操作将跳过这些条目:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Kiprey: in function `processJob`
if job.Target.OS == targets.Linux && (job.Target.Arch == targets.ARM || job.Target.Arch == targets.RiscV64) {
// Hack: KVM is not supported on ARM anymore. On riscv64 it
// is not supported yet but might be in the future.
// Note: syz-extract also ignores this file for arm and riscv64.
top = descriptions.Filter(func(n ast.Node) bool {
pos, typ, name := n.Info()
if !strings.HasSuffix(pos.File, "_kvm.txt") {
return true
}
switch n.(type) {
case *ast.Resource, *ast.Struct, *ast.Call, *ast.TypeDef:
// Mimic what pkg/compiler would do with unsupported entries.
// This is required to keep the unsupported diagnostic below working
// for kvm entries, otherwise it will not think that kvm entries
// are not supported on all architectures.
job.Unsupported[typ+" "+name] = true
}
return false
})
}

除了这些 Linux OS 需要过滤的架构以外,syz-sysgen 还需要过滤掉自己开发者人员测试用的 testOS:

1
2
3
4
5
// Kiprey: in function `processJob`
if job.Target.OS == targets.TestOS {
constInfo := compiler.ExtractConsts(top, job.Target, eh)
compiler.FabricateSyscallConsts(job.Target, constInfo, consts)
}

其中,targets.TestOS 所对应的字符串为 test

接下来,syz-sysgen 需要分析 AST 信息,对 syzlang 进行编译:

1
2
3
4
5
6
7
8
// Kiprey: in function `processJob`
prog := compiler.Compile(top, consts, job.Target, eh)
if prog == nil {
return
}
for what := range prog.Unsupported {
job.Unsupported[what] = true
}

返回的 Prog 结构体声明如下:

1
2
3
4
5
6
7
8
9
10
11
12
// Kiprey: in function `processJob`

// 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
}

编译操作和先前 syz-extract 类似,不同的是这次提供了 consts 信息,因此会执行完整的编译过程,分析 syzlang 代码中描述的全部 syscall 参数类型信息。返回的 Prog 结构体中:

  • 字段 fileConsts 为空
  • 涉及到的类型信息保存在了 Resource 和 Types 字段
  • syscall 的描述则存放在 Syscalls 字段中。

之后便是将分析结果,序列化为 go 语言源代码,留待后续 syz-fuzzer 所使用;序列化后的 golang 代码存放至 sys/<OS>/gen/<arch>.go,例如 sys/linux/gen/amd64.goloc: ~11w):

1
2
3
4
5
6
7
8
// Kiprey: in function `processJob`
sysFile := filepath.Join(*outDir, "sys", job.Target.OS, "gen", job.Target.Arch+".go")
out := new(bytes.Buffer)
// generate 执行 golang 序列化操作
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())

我们来看看生成出的 golang 代码是什么样的(以 /sys/linux/gen/amd64.go 为例):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
// 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"

其中,init 函数用于将当前这个 linux amd64 的 target,注册进 targets 数组中以供后续 syz-fuzzer 取出使用。

1
2
3
4
5
6
7
8
9
10
11
var targets = make(map[string]*Target)

func RegisterTarget(target *Target, types []Type, initArch func(target *Target)) {
key := target.OS + "/" + target.Arch
if targets[key] != nil {
panic(fmt.Sprintf("duplicate target %v", key))
}
target.initArch = initArch
target.types = types
targets[key] = target
}

amd64.go 内部还声明了多个数组,其中:

  • resources_amd64 数组:存放着每个 syzlang 代码中声明的 resource 变量
  • syscalls_amd64 数组:存放着每个 syscall 所对应的名称、调用号,以及各个参数的名称和类型。
  • types_amd64 数组:每个类型的具体信息,例如数组、结构体类型信息等等
  • consts_amd64:存放 consts 字符串与整型的映射关系
  • revision_amd64:amd64.go 源码的哈希值

回到 generateExecutorSyscall 函数,该函数最后便是调用 generateExecutorSyscalls 函数来创建 Executor 的 syscall 信息,并将其返回给上层调用者(即 main 函数):

1
2
3
4
5
6
// Kiprey: in function `processJob`
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-exexcutor 的 C 代码。

3. generateExecutorSyscalls

该函数的作用是,为生成 syz-executor 准备相关的 syscall 数据,因此起名神似 生成(generate) executor 的 syscall 数据

初始时,generateExecutorSyscalls 函数创建了一个 ArchData 结构体,这个结构体将一层层返回给 main 函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
data := ArchData{
Revision: rev,
GOARCH: target.Arch,
PageSize: target.PageSize,
NumPages: target.NumPages,
DataOffset: target.DataOffset,
}
if target.ExecutorUsesForkServer {
data.ForkServer = 1
}
if target.ExecutorUsesShmem {
data.Shmem = 1
}

如果目标 OS & arch 所对应的 target 结构体,设置了对 ForkServer 和 Shmem(共享内存)的支持,则在 data 中将这两个字段设置为 true,这样 syz-executor 便可以使用这两个技术加速 fuzz 过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 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
}

接下来便是一个遍历 syscalls 数组中的各个 Syscall 类型结构体的 for 循环。这个 for 循环虽然看上去一眼难以看懂,但实际上,它只是将变量 c 中结构体 SyscallAttrs 里的各个字段取出,并将其依次存放至整型数组 attrVals,然后再使用生成的 attrVals 数组进一步生成 SyscallData 结构体:

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
for _, c := range syscalls {
var attrVals []uint64
attrs := reflect.ValueOf(c.Attrs)
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
}
}
data.Calls = append(data.Calls, newSyscallData(target, c, attrVals[:last+1]))
}
sort.Slice(data.Calls, func(i, j int) bool {
return data.Calls[i].Name < data.Calls[j].Name
})
return data

以下是 data 变量中所存放信息的一个示例:

image-20220309214932071

结构体 SyscallAttrs 定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 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
}

以上图所示,由于当前遍历的 SyscallAttrs 结构体(也就是变量 attrs)的值全为默认值0,因此取出来的 Attrs 数组中各元素也为 0:

image-20220309215426959

该 for 循环会一次次的将遍历到的 syscall 对应的 SyscallData 添加进data.Calls,其中 newSyscallData 函数所生成的 SyscallData 结构体定义如下:

1
2
3
4
5
6
7
8
// 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 数据数组
}

待整个 for 循环完成后,generateExecutorSyscall 函数将会把上面所生成的 data.Calls 数组进行排序,并返回 data 变量。

4. writeExecutorSyscalls

作用:该函数将生成 syz-executor 所使用的 C 代码头文件。

通读一下代码可以很容易的发现,该函数将会尝试填充两个 C 代码模板,并将填充后的 C 代码输出至 executor/defs.hexecutor/syscalls.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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)
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()
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 代码模板如下:

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
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}}
#endif
{{end}}
#endif
{{end}}
`))

代码模板看上去有点难以理解,因为其中混杂着 C 宏定义与模板描述,因此不妨从 executor/defs.h 中直接看看生成好的代码:

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

可以看到, syz-sysgen 会将把先前 generateExecutorSyscalls 函数中所生成的 ArchData 结构体数据,导出至 executor/defs.h 文件中,供后续编译 syz-executor 所使用。syz-sysgen 将所有OS所有架构所对应的 ArchData 数据全部导出至一个文件中,并使用宏定义来选择启用哪一部分的数据。

另一个代码模板 syscallsTempl 的内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// nolint: lll
var syscallsTempl = template.Must(template.New("").Parse(`// AUTOGENERATED FILE
// clang-format off
{{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 示例:

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
...
#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
...

可以看到,executor/syscalls.h 下会存放着各个 syzlang 中所声明的 syscall 名与 syscall调用号的映射关系,以及可能有的 SyscallData。同时,也是使用宏定义来控制使用哪个OS哪个Arch下的 syscalls 映射关系

再贴一下 SyscallData 结构体定义:

1
2
3
4
5
6
7
type SyscallData struct {
Name string
CallName string
NR int32
NeedCall bool
Attrs []uint64
}

5. 小结

当执行完 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 的编译运行做准备。

调试用的 vscode launch.json 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"version": "0.2.0",
"configurations": [
{
"name": "syzgenLaunch",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${fileDirname}",
"env": {},
"cwd": "/usr/class/syzkaller",
"args": ["-src", "/usr/class/syzkaller", "-out", "/tmp"]
}
]
}
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2020-2022 Kiprey
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~