7

syzkaller 源码阅读笔记-1

 2 years ago
source link: https://kiprey.github.io/2022/03/syzkaller-1/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

syzkaller 源码阅读笔记-1

2022-03-15

字数统计: 11.4k

  |   阅读时长≈ 55 分钟

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 将会尝试解析传入的参数:

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

其参数列表如下:

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 文件名 字符串数组
// Kiprey: in Function `main` 
OS, archArray, files, err := archFileList(*flagOS, *flagArch, flag.Args())
if err != nil {
tool.Fail(err)
}

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

// 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 中:

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

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 结构体数组

// 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 执行初始化操作:

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

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

// 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 中执行。

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 就利用了管道来完成线程协同。

// 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 文件中,没有其他有意思的东西了:

// 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 文件路径的字符串数组传入该函数:

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

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

// 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 下的信息,以下是一个精简后的代码片段:

// 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 中:

// 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 将尝试自动添加文件进入。在这一部分代码中:

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 的条目),最后为结果数组做个顺序排序:

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

函数结束,结果返回:

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

3. createArches

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

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 用于执行真正的解析变量工作:

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

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

image-20220309095730698

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

// 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 信息。

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 中,做的操作主要是定义了几个头文件:

"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 操作:

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

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

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

// 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 编译参数:

// 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 集合:

// 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 文件中标明其值为 ???

O_RDWR = 2
MyConst = ???

image-20220309132346585

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

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

替换完成后将结果返回:

return res, undeclared, nil

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

7. extract

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

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

函数声明:

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:

// 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 函数完成,其返回结果分别为编译出的可执行文件路径;编译器标准输出信息;编译器标准错误信息:

// Kiprey: in function `extract`
for {
bin1, out, err := compile(cc, args, data)
if err == nil {
bin = bin1
break
}
...
}

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

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

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

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 需要尝试自动纠错:

// 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 释放了该文件的文件描述符后才会被删除。

// 将新编译出的二进制文件删除
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

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

if data.ExtractFromELF {
flagVals, err = extractFromELF(bin, params.TargetEndian)
} else {
flagVals, err = extractFromExecutable(bin)
}

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

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 函数。

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

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

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 文件:

{
"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 的类型都取出来,并且创建了用于存储结果的结构体:

// 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 循环分为了上中下三个部分:

  • 首先是第一部分:

    // 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 字符串集合,并做一次排序操作。

  • 其次是第二部分:

    // 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 关键头文件代码所用。

  • 最后是第三部分:

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

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 这两个结构体对应的字段名存了起来。俩结构体声明如下:

// 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,其结构体声明如下所示:

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 字符串->整型映射表:

// 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 中,接下来的操作将跳过这些条目:

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

// 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 进行编译:

// 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 结构体声明如下:

// 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):

// 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 为例):

// 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 取出使用。

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 函数):

// 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 函数。

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 过程。

// 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 结构体:

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 定义如下:

// 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 结构体定义如下:

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

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 代码模板如下:

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 中直接看看生成好的代码:

// 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 的内容如下:

// 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 示例:

...
#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 结构体定义:

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

调试用的 vscode launch.json 文件:

{
"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"]
}
]
}

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK