BruceFan's Blog

Stay hungry, stay foolish

0%

Syzkaller原理和源码分析

syzkaller运行流程结构如下图所示,红色标签表示需要配置的选项:

syz-manager用来启动、监控、和重启多个虚拟机实例,并在虚拟机里启动一个syz-fuzzer进程。它负责持久化corpus和存储crash。

syz-fuzzer在要测试的内核虚拟机上运行,syz-fuzzer指导fuzz进程(产生输入、变异、精简等)并通过RPC方式发送触发新路径的输入返回给syz-manager进程。它也会启动一个暂态syz-executor进程。
每个syz-executor进程执行一个输入(一套syscalls)。如:

1
2
3
4
mmap(&(0x7f000000000),(0x1000), 0x3, 0x32, -1, 0)
r0 = open(&(0x7f0000000000))="./file0", 0x3, 0x9)
read(r0, &(0x7f0000000000), 42)
close(r0)

syz-fuzzer进程接收输入来执行,并将结果返回。它被设计的尽可能简单(为了不干扰fuzz进程),用C++实现,编译为静态二进制,用共享内存通信。

源码分析

先从启动syzkaller的命令行工具syz-manager的源码开始分析,syz-manager的源码位于syz-manager/manager.go文件,首先是一个Manager结构体,里面包含了fuzz过程中的重要信息,如配置信息、虚拟机信息、测试目标信息等,具体内容后面会分析到。
接下来是syz-manager运行的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
func main() {
if sys.GitRevision == "" {
log.Fatalf("Bad syz-manager build. Build with make, run bin/syz-manager.")
}
flag.Parse() // 对命令行参数进行解析
log.EnableLogCaching(1000, 1<<20)
cfg, err := mgrconfig.LoadFile(*flagConfig) // flagConfig是命令行解析出来的配置文件,通过mgrconfig的LoadFile读取到cfg中
if err != nil {
log.Fatalf("%v", err)
}
target, err := prog.GetTarget(cfg.TargetOS, cfg.TargetArch) // 根据配置文件里的系统和架构获取目标信息,包括系统调用等,保存在Target结构体中
if err != nil {
log.Fatalf("%v", err)
}
sysTarget := targets.Get(cfg.TargetOS, cfg.TargetArch)
if sysTarget == nil {
log.Fatalf("unsupported OS/arch: %v/%v", cfg.TargetOS, cfg.TargetArch)
}
syscalls, err := mgrconfig.ParseEnabledSyscalls(target, cfg.EnabledSyscalls, cfg.DisabledSyscalls) // 可以去掉一些不感兴趣的syscall
if err != nil {
log.Fatalf("%v", err)
}
RunManager(cfg, target, sysTarget, syscalls)
}

pkg/mgrconfig里的config.go里定义了Config结构体,用来保存配置文件里的信息,load.go文件里定义了读取配置文件的方法。
prog/target.go中定义了Target结构体,GetTarget方法中用到了targets变量,targets变量在RegisterTarget方法中初始化,在RegisterTarget中添加debug.PrintStack(),发现RegisterTarget位于栈底,不知道是哪里调用了它。其实是在manager.go文件开头,import了sys包,在sys/sys.go文件中,import ( _ “**/sys/linux/gen)导入包前的下划线表示这个包里所有文件的init方法都会被执行,sys/linux/gen/里有386.go、amd64.go、arm64.go等,这些文件里都有init方法,init里调用了RegisterTarget方法,初始化了各个目标平台的target信息。GetTarget最后用sync.Once的Do方法确保target的初始化在整个程序(多线程环境)中只执行一次,内部通过互斥锁实现。
sysTarget的用处还没有细看。
接下来是RunManager方法,RunManager实现了启动虚拟机、http服务、rpc服务和log fuzz进程等操作。

1
2
3
4
5
6
7
8
9
func RunManager(cfg *mgrconfig.Config, target *prog.Target, sysTarget *targets.Target, syscalls []int) {
var vmPool *vm.Pool
if cfg.Type != "none" {
var err error
vmPool, err = vm.Create(cfg, *flagDebug) // 首先根据cfg中的虚拟机类型(Type如qemu)对vmPool进行初始化
if err != nil {
log.Fatalf("%v", err)
}
}

在vm/vm.go文件中,还是用import (_ “**/vm/qemu”)的方法,调用了所有导入文件里的init方法,qemu的init方法:

1
2
3
func init() {
vmimpl.Register("qemu", ctor, true)
}

调用了vm/vmimpl/vmimpl.go的Register方法:

1
2
3
4
5
6
func Register(typ string, ctor ctorFunc, allowsOvercommit bool) {
Types[typ] = Type{
Ctor: ctor,
Overcommit: allowsOvercommit,
}
}

再看vm/vm.go的Create方法:

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 Create(cfg *mgrconfig.Config, debug bool) (*Pool, error) {
typ, ok := vmimpl.Types[cfg.Type] // 取出vmimpl中cfg.Type对应的虚拟机类型,如qemu
if !ok {
return nil, fmt.Errorf("unknown instance type '%v'", cfg.Type)
}
env := &vmimpl.Env{
Name: cfg.Name,
OS: cfg.TargetOS,
Arch: cfg.TargetVMArch,
Workdir: cfg.Workdir,
Image: cfg.Image,
SSHKey: cfg.SSHKey,
SSHUser: cfg.SSHUser,
Debug: debug,
Config: cfg.VM,
}
impl, err := typ.Ctor(env) // 用取出的Type类型构建vmimpl.Pool,vmimpl.Pool是interface,在这里typ.Ctor返回的是实现了这个接口的具体的Pool,如qemu的Pool
if err != nil {
return nil, err
}
return &Pool{ // 将Pool返回给RunManager的vmPool变量
impl: impl, // 这里的impl已经是qemu的Pool了
workdir: env.Workdir,
}, nil
}

再回到RunManager,进行mgr := &Manager创建mgr管理信息,initHTTP()创建HTTP服务器,startRPCServer(mgr)为fuzzer创建RPC服务器。接下来的go func() { for log },并发执行一个匿名函数,不停log fuzz进度。go加上方法表示并发执行这个方法。最后RunManager执行一个mgr.vmLoop()方法,vmLoop()方法会调用一个runInstance方法,runInstance调用mgr.vmPool.Create(index),这里是vm/vm.go的Create(),这个Create()会调用vmPool的impl的Create(workdir, index)方法,这里也就是qemu的Create(workdir, index)方法,qemu的Create方法会创建sshkey,并调用ctor方法,ctor方法会调用boot方法,boot方法会执行启动qemu的命令。vm.go的Create方法会返回启动虚拟机的实例,接下来runInstance方法会通过ssh拷贝fuzzerBin和executorBin到虚拟机实例,然后执行fuzzer二进制文件,并监控虚拟机的执行过程。

reference
How syzkaller works