syzkaller 源码阅读笔记2(syz-manager)

2022/05/14 程序分析技术 共 11619 字,约 34 分钟

1. 介绍

syz-manager 功能:主要负责各种工作的启动(HTTP、RPC、dashboard等等)、调用fuzz以及repro的生成。

fuzz命令$ ./syz-manager -config=my.cfg

示例qemu.cfgsyzkaller\pkg\mgrconfig\testdata 目录下

{
	"target": "linux/amd64",
	"http": "myhost.com:56741",      // 显示正在运行的 syz-manager 进程信息的URL 
	"workdir": "/syzkaller/workdir", // syz-manager 进程的工作目录的位置
	"kernel_obj": "/linux/",   		 // 包含目标文件的目录,例如linux中的vmlinux
	"image": "./testdata/wheezy.img",// qemu实例的磁盘镜像文件的位置
	"syzkaller": "./testdata/syzkaller", // syzkaller的位置,syz-manager将在bin子目录中查找二进制文件
	"disable_syscalls": ["keyctl", "add_key", "request_key"], // 禁用的系统调用列表
	"suppressions": ["some known bug"],  // 已知错误的正则表达式列表
	"procs": 4, 					 // 每个VM中的并行测试进程数,一般是4或8
	"type": "qemu", 				 // 要使用的虚拟机类型,例如qemu
	"vm": { 						 // 特定VM类型相关的参数
		"count": 16, 								// 并行运行的VM数
		"cpu": 2, 									// 要在VM中模拟的CPU数
		"mem": 2048, 								// VM的内存大小,以MB为单位
		"kernel": "/linux/arch/x86/boot/bzImage", 	// 要测试的内核的bzImage文件的位置
		"initrd": "linux/initrd"
	}
}

其他config参数说明

  • email_addrs:第一次出现bug时接收通知的电子邮件地址,只支持 Mailx
  • sshkey:用于与虚拟机通信的SSH密钥的位置
  • sandbox:沙盒模式,支持以下模式
    • none:默认设置,不做任何特殊的事情
    • setuid:冒充用户nobody(65534)
    • namespace:使用命名空间删除权限(内核需要设置 CONFIG_NAMESPACESCONFIG_UTS_NSCONFIG_USER_NSCONFIG_PID_NSCONFIG_NET_NS 构建)
  • enable_syscalls:测试的系统调用列表
  • disable_syscalls:禁用的系统调用列表

debug参数和bench参数:debug参数将VM所有输出打印到console帮助我们排查使用中出现的错误;bench参数定期将执行的统计信息写入我们指定的文件。

var (
	flagConfig = flag.String("config", "", "configuration file")
	flagDebug  = flag.Bool("debug", false, "dump all VM output to console")
	flagBench  = flag.String("bench", "", "write execution statistics into this file periodically")
)

代码总结

  • main():开启日志缓存,加载 config 文件,调用 RunManager()
  • RunManager():新开线程,定期记录VM状态、crash数量等信息,最后调用 vmLoop()
  • vmLoop():将VM实例分为两个部分,一部分用于进行crash复现,另一部分用于进行fuzz。
    • crash复现:提取出触发crash的C代码。
      • ctx.extractProg() —— 提取出触发 crash 的程序;
      • ctx.minimizeProg() —— 若成功复现,则调用 prog.Minimize(),简化所有的调用和参数;
      • ctx.extractC() —— 生成C代码,编译成二进制文件,执行并检查是否crash;
      • ctx.simplifyProg() —— 进一步简化。在 repro.go 中定义了 progSimplifies 数组作为简化规则,依次使用每一条规则后,如果crash还能被触发, 再调用 extractC(res) 尝试提取 C repro;
      • ctx.simplifyC() —— 对提取出的C程序进行简化。 跟上面的 ctx.simplifyProg(res) 差不多,就是规则使用了 cSimplifies 数组;
    • 启动fuzz:将 syz-fuzzer / syz-executor 拷贝到VM中,构造好命令,调用FuzzerCmd() 启动 syz-fuzzer

2. main()

位置syz-manager/manager.go

功能:开启日志缓存,加载 config 文件,调用 RunManager()

func main() {
	if prog.GitRevision == "" {
		log.Fatalf("bad syz-manager build: build with make, run bin/syz-manager")
	}
	flag.Parse()
	log.EnableLogCaching(1000, 1<<20)           // [1] 开启日志缓存,日志不超过1000行或1^29字节
	cfg, err := mgrconfig.LoadFile(*flagConfig) // [2] 加载 config 文件
	if err != nil {
		log.Fatalf("%v", err)
	}
	RunManager(cfg)
}

3. RunManager()

功能: 新开线程,定期记录VM状态、crash数量等信息,最后调用 vmLoop()

说明

  • [1] —— 调用 vm/vm.go: Create() 创建 vmPool。一个 vmPool 可用于创建多个独立的VM,vm.go 对不同的虚拟化方案提供了统一的接口,这里会调用 qemu.go: Ctor() 函数,主要检查了一些参数。
  • [2] —— 新开线程,定期记录VM状态、crash数量等信息。
  • [3] —— 如果设置了 bench 参数,还要在指定的文件中记录一些信息。
  • [5] —— 主要调用 vmLoop()
func RunManager(cfg *mgrconfig.Config) {
	var vmPool *vm.Pool
	// Type "none" is a special case for debugging/development when manager
	// does not start any VMs, but instead you start them manually
	// and start syz-fuzzer there.
	if cfg.Type != "none" { // 将type指定为none是在调试/开发中用的,这样manager就不会启动VM而是需要手动启动
		var err error
		vmPool, err = vm.Create(cfg, *flagDebug) // [1] 创建 vmPool
		if err != nil {...
		}
	}

	crashdir := filepath.Join(cfg.Workdir, "crashes")
	osutil.MkdirAll(crashdir)

	reporter, err := report.NewReporter(cfg)
	if err != nil {...
	}

	mgr := &Manager{...
	}

	mgr.preloadCorpus()
	mgr.initStats() // Initializes prometheus variables.
	mgr.initHTTP()  // Creates HTTP server.
	mgr.collectUsedFiles()

	// Create RPC server for fuzzers.
	mgr.serv, err = startRPCServer(mgr)
	if err != nil {...
	}

	if cfg.DashboardAddr != "" {...
	}

	go func() { // [2] 新开线程,定期记录VM状态、crash数量等信息
		for lastTime := time.Now(); ; {
			time.Sleep(10 * time.Second)
			now := time.Now()
			diff := now.Sub(lastTime)
			lastTime = now
			mgr.mu.Lock()
			if mgr.firstConnect.IsZero() {
				mgr.mu.Unlock()
				continue
			}
			mgr.fuzzingTime += diff * time.Duration(atomic.LoadUint32(&mgr.numFuzzing))
			executed := mgr.stats.execTotal.get()
			crashes := mgr.stats.crashes.get()
			corpusCover := mgr.stats.corpusCover.get()
			corpusSignal := mgr.stats.corpusSignal.get()
			maxSignal := mgr.stats.maxSignal.get()
			mgr.mu.Unlock()
			numReproducing := atomic.LoadUint32(&mgr.numReproducing)
			numFuzzing := atomic.LoadUint32(&mgr.numFuzzing)

			log.Logf(0, "VMs %v, executed %v, cover %v, signal %v/%v, crashes %v, repro %v",
				numFuzzing, executed, corpusCover, corpusSignal, maxSignal, crashes, numReproducing)
		}
	}()

	if *flagBench != "" { // [3] 如果设置了 bench 参数,还要在指定的文件中记录一些信息
		...
     	mgr.minimizeCorpus() // [4]
        ...
	}

	if mgr.dash != nil {...
	}

	osutil.HandleInterrupts(vm.Shutdown)
	if mgr.vmPool == nil {...
	}
	mgr.vmLoop() // [5] 主要调用 vmLoop()
}

4. vmLoop()

功能:将VM实例分为两个部分,一部分用于进行crash复现,另一部分用于进行fuzz。

说明

  • 变量说明:reproQueue —— 保存crash,可通过 len(reproQueue) != 0 判断当前是否有等待复现的crash;

  • [3]:可以复现且有剩余的 instances,则复现crash;
  • [4]:没有可复现的但是有剩余的 instances,则进行fuzz;
func (mgr *Manager) vmLoop() {
    ...
			canRepro := func() bool { // [2] 判断当前是否有等待复现的crash
			return phase >= phaseTriagedHub && len(reproQueue) != 0 &&
				(int(atomic.LoadUint32(&mgr.numReproducing))+1)*instancesPerRepro <= maxReproVMs
		}

		if shutdown != nil {
			for canRepro() { // [3] 可以复现且有剩余的 instances, 则复现crash
				vmIndexes := instances.Take(instancesPerRepro) // [3-1] 取 instancesPerRepro 个 (默认4) VM, 对crash进行复现
				if vmIndexes == nil {
					break
				}
				last := len(reproQueue) - 1
				crash := reproQueue[last]
				reproQueue[last] = nil
				reproQueue = reproQueue[:last]
				atomic.AddUint32(&mgr.numReproducing, 1)
				log.Logf(1, "loop: starting repro of '%v' on instances %+v", crash.Title, vmIndexes)
				go func() {
					reproDone <- mgr.runRepro(crash, vmIndexes, instances.Put) // [3-2] crash 复现 runRepro() -> repro.Run() -> ctx.repro() !!!
				}()
			}
			for !canRepro() { // [4] 没有可复现的但是有剩余的 instances, 则进行fuzz
				idx := instances.TakeOne() // [4-1] 取 1 个 VM, 运行新的实例
				if idx == nil {
					break
				}
				log.Logf(1, "loop: starting instance %v", *idx)
				go func() {
					crash, err := mgr.runInstance(*idx) // [4-2] 启动fuzz, 监控信息并返回Report对象 runInstance() -> runInstanceInner() -> FuzzerCmd() & MonitorExecution()  !!!
					runDone <- &RunResult{*idx, crash, err}
				}()
			}
		}
    ...
}

4-1 crash复现

调用链vmLoop() -> mgr.runRepro() -> repro.Run() -> ctx.repro() (重点函数)

位置pkg/repro/repro.go: (*context).repro()

功能:crash 复现,提取出触发crash的C代码。

说明

  • [2] ctx.extractProg() —— 提取出触发 crash 的程序;
  • [3] ctx.minimizeProg() —— 若成功复现,则调用 prog.Minimize(),简化所有的调用和参数;
  • [4] ctx.extractC() —— 生成C代码,编译成二进制文件,执行并检查是否crash;
  • [5] ctx.simplifyProg() —— 进一步简化。在 repro.go 中定义了 progSimplifies 数组作为简化规则,依次使用每一条规则后,如果crash还能被触发, 再调用 extractC(res) 尝试提取 C repro;
  • [6] ctx.simplifyC() —— 对提取出的C程序进行简化。 跟上面的 ctx.simplifyProg(res) 差不多,就是规则使用了 cSimplifies 数组;
  • [5][6] 简化的是复现crash时设置的一些选项,比如线程、并发、沙盒等等。简化选项分别保存在 progSimplifiescSimplifies 数组中。
// pkg/repro/repro.go: (*context).repro()
func (ctx *context) repro(entries []*prog.LogEntry, crashStart int) (*Result, error) {
    ...
	res, err := ctx.extractProg(entries) // [2] 提取出触发 crash 的程序  !!!
	...
	res, err = ctx.minimizeProg(res) // [3] 若成功复现, 则调用prog.Minimize(), 简化所有的调用和参数 !!!
	...
	// Try extracting C repro without simplifying options first.
	res, err = ctx.extractC(res) // [4] 生成C代码,编译成二进制文件,执行并检查是否crash,若crash则赋值 res.CRepro = crashed !!!
	...
	// Simplify options and try extracting C repro.
	if !res.CRepro {
		res, err = ctx.simplifyProg(res) // [5] !!! 进一步简化。在 repro.go 中定义了 progSimplifies 数组作为简化规则,依次使用每一条规则后,如果crash还能被触发, 再调用 extractC(res) 尝试提取 C repro
		...
	}
	// Simplify C related options.
	if res.CRepro {
		res, err = ctx.simplifyC(res) // [6] 对提取出的C程序进行简化。 跟上面的ctx.simplifyProg(res)差不多,就是规则使用了cSimplifies数组。[5][6] 简化的是复现crash时设置的一些选项,比如线程、并发、沙盒等等。
		...
	}
	return res, nil
}

(1)extractProg()

位置pkg/repro/repro.go

功能:提取出触发 crash 的程序。

说明:按照时间从短到长, 从后向前, 从单个到多个的顺序复现crash。

  • [1]:在所有程序 (用 entries 数组存放) 中提取出每个proc所执行的最后一个程序;
  • [2]:将程序按倒序存放到 lastEntries (通常最后一个程序就是触发crash的程序);
  • [3]:不同类型的漏洞漏洞需要不同的复现时间, 复杂crash耗时长(eg, race);
  • [4] extractProgSingle() —— 倒序执行单个程序, 若触发crash则返回;
  • [5] extractProgBisect() —— 若单个程序无法触发crash, 则采用二分查找的方法找出哪几个程序一起触发crash。先调用 bisectProgs() 进行分组,看哪一组可以触发crash。 !!!
  • 返回值是能触发crash的单个program或者能触发crash的programs的组合。

    func (ctx *context) extractProg(entries []*prog.LogEntry) (*Result, error) {
      ...
      for _, idx := range procs { // [1] 在所有程序 (用entries数组存放) 中提取出每个proc所执行的最后一个程序
          indices = append(indices, idx)
      }
      sort.Ints(indices)
      var lastEntries []*prog.LogEntry
      for i := len(indices) - 1; i >= 0; i-- { // [2] 将程序按倒序存放到 lastEntries (通常最后一个程序就是触发crash的程序)
          lastEntries = append(lastEntries, entries[indices[i]])
      }
      for _, timeout := range ctx.testTimeouts { // [3] 不同类型的漏洞漏洞需要不同的复现时间, 复杂crash耗时长(eg, race)
          // Execute each program separately to detect simple crashes caused by a single program.
          // Programs are executed in reverse order, usually the last program is the guilty one.
          res, err := ctx.extractProgSingle(lastEntries, timeout) // [4] 倒序执行单个程序, 若触发crash则返回
          if err != nil {
              return nil, err
          }
          if res != nil {
              ctx.reproLogf(3, "found reproducer with %d syscalls", len(res.Prog.Calls))
              return res, nil
          }
    
          // Don't try bisecting if there's only one entry.
          if len(entries) == 1 {
              continue
          }
          // [5] 若单个程序无法触发crash, 则采用二分查找的方法找出哪几个程序一起触发crash。先调用bisectProgs()进行分组,看哪一组可以触发crash。 !!!
          // Execute all programs and bisect the log to find multiple guilty programs.
          res, err = ctx.extractProgBisect(entries, timeout)
          ...
    }
    

(2)Minimize()

调用链ctx.minimizeProg() -> prog.Minimize()(重点函数)

位置prog/minimization.go: Minimize()

功能:简化所有的调用和参数。

说明

  • [1] sanitizeFix() —— 有些系统调用需要做一些特殊的处理;
  • [2] removeCalls() —— 尝试逐个移除系统调用;
  • [3] :去除系统调用的无关参数;
  • [4] ctx.do() —— 根据不同的参数类型调用不同的minimize函数。
    • func (typ *PtrType) minimize() —— 如果参数是指针类型的,把指针或者指针指向的内容置空;
    • func (typ *ArrayType) minimize() —— 如果参数是数组类型的,尝试一个一个移除数组中的元素;
func Minimize(p0 *Prog, callIndex0 int, crash bool, pred0 func(*Prog, int) bool) (*Prog, int) {
	pred := func(p *Prog, callIndex int) bool {
		p.sanitizeFix() // [1] 有些系统调用需要做一些特殊的处理 !!!
		p.debugValidate()
		return pred0(p, callIndex)
	}
	...
	// Try to remove all calls except the last one one-by-one.
	p0, callIndex0 = removeCalls(p0, callIndex0, crash, pred) // [2] 尝试逐个移除系统调用

	// Try to reset all call props to their default values.
	p0 = resetCallProps(p0, callIndex0, pred)

	// Try to minimize individual calls.
	for i := 0; i < len(p0.Calls); i++ { // [3] 去除系统调用的无关参数
		ctx := &minimizeArgsCtx{
			target:     p0.Target,
			p0:         &p0,
			callIndex0: callIndex0,
			crash:      crash,
			pred:       pred,
			triedPaths: make(map[string]bool),
		}
	again:
		ctx.p = p0.Clone()
		ctx.call = ctx.p.Calls[i]
		for j, field := range ctx.call.Meta.Args {
			if ctx.do(ctx.call.Args[j], field.Name, "") { // [4] 在do函数中,根据不同的参数类型调用不同的minimize函数 !!!
				goto again
			}
		}
		p0 = minimizeCallProps(p0, i, callIndex0, pred)
	}
	...
	return p0, callIndex0
}

(3)extractC()

调用链ctx.extractC() -> ctx.testCProg() -> inst.RunCProg() -> csource.Write() & csource.BuildNoWarn() & inst.runBinary()

位置pkg/instance/execprog.go: (*ExecProgInstance).RunCProg()

功能:生成C代码,编译成二进制文件,执行并检查是否crash。

说明:调用 csource.Write() 生成C代码; csource.BuildNoWarn() 编译出可执行文件; inst.runBinary() 执行二进制文件。

4-2 启动fuzz

调用链vmLoop() -> mgr.runInstance() -> mgr.runInstanceInner()

位置syz-manager/manager.go: (*Manager).runInstanceInner()

功能:负责启动 syz-fuzzer

说明

  • [1]:将 syz-fuzzer 复制到VM中;

  • [2]:将 syz-executor 复制到VM中;

  • [3] FuzzerCmd() —— 构造好命令,通过ssh执行 syz-fuzzer

    # fuzz命令示例
    /syz-fuzzer -executor=/syz-executor -name=vm-0 -arch=amd64 -manager=10.0.2.10:33185 -procs=1 -leak=false -cover=true -sandbox=none -debug=true -v=100
    
  • [4] MonitorExecution() —— 监控, 检测输出中的内核oops信息、丢失连接、挂起等等。

func (mgr *Manager) runInstanceInner(index int, instanceName string) (*report.Report, []byte, error) {
    ...
	fuzzerBin, err := inst.Copy(mgr.cfg.FuzzerBin) // [1] 将 syz-fuzzer 复制到VM中
	if err != nil {
		return nil, nil, fmt.Errorf("failed to copy binary: %v", err)
	}

	// If ExecutorBin is provided, it means that syz-executor is already in the image,
	// so no need to copy it.
	executorBin := mgr.sysTarget.ExecutorBin
	if executorBin == "" {
		executorBin, err = inst.Copy(mgr.cfg.ExecutorBin) // [2] 将 syz-executor 复制到VM中
		...
	}
    ...
	// Run the fuzzer binary.
	start := time.Now()
	atomic.AddUint32(&mgr.numFuzzing, 1)
	defer atomic.AddUint32(&mgr.numFuzzing, ^uint32(0))

	args := &instance.FuzzerCmdArgs{...
	}
	cmd := instance.FuzzerCmd(args) // [3] 调用 FuzzerCmd() 通过ssh执行 syz-fuzzer   !!!
	outc, errc, err := inst.Run(mgr.cfg.Timeouts.VMRunningTime, mgr.vmStop, cmd)
	if err != nil {
		return nil, nil, fmt.Errorf("failed to run fuzzer: %v", err)
	}

	var vmInfo []byte
	rep := inst.MonitorExecution(outc, errc, mgr.reporter, vm.ExitTimeout) // [4] 监控, 检测输出中的内核oops信息、丢失连接、挂起等等。
	...
	return rep, vmInfo, nil
}

参考

内核漏洞挖掘技术系列(4)——syzkaller(3)

[原创]syzkaller源码分析(一) syz-manager.go

文档信息

Search

    Table of Contents