syzkaller 源码阅读笔记3(syz-fuzzer)

2022/05/15 程序分析技术 共 21093 字,约 61 分钟

syz-fuzzer 功能:进行 fuzzing 的主程序,根据文件系统/sys/kernel/debug/kcov获取内核代码覆盖率,并且生成新的变异数据,然后将输入传给 syz-executor。

代码总结:见 main() 函数。

1. main()

位置syz-fuzzer/fuzzer.go: main()

说明

  • [1] —— RPC远程调用 -> syz-manager/rpc.go:Check() -> machineChecked() -> loadCorpus() 将 db 中所有的语料库加载到 mgr.candidates
  • [2] poll() —— 更新 fuzzer.corpus 语料库以及 fuzzer.workQueue 队列;
    • RPC远程调用 -> syz-manager/rpc.go:Poll() -> candidateBatch() 取得 mgr.candidates 并存入 r.Candidates;从 r.Candidates 提取出程序, 并加入到 fuzzer.workQueue
    • 调用 addInputToCorpus() 更新 fuzzer.corpus
  • [3] BuildChoiceTable() —— 生成 prios[X][Y] 优先级, 预测在包含系统调用X的程序中添加系统调用Y是否能得到新的覆盖
    • 如果 syscall X 和 syscall Y 都接受参数 fd[sock], 把它们放在一起就更有可能出现新的覆盖;
    • 单个程序中,两个syscall一起出现的频率越高,越有可能出现新的覆盖。
  • [4] loop() —— fuzz的核心函数,如果有剩余的Procs,就开启新的线程执行此函数。 生成新的程序和变异都在这里进行。
    • [1]: 遍历 fuzzer.workQueue 队列,取到item中,对三种不同类型的item,分别调用不同的函数进行处理;
    • [2] triageInput() —— WorkTriage:第一次执行时,检查是否产生了新的覆盖,有新覆盖的话则 Minimize 并添加到语料库中。
    • [3] execute() —— WorkCandidate:交给 executor 执行,若产生新的 syscall,则加入到 fuzzer.workQueue 队列。
    • [4] smashInput() —— WorkSmash:对刚加入到语料库中的程序,采用syscall中的比较操作数,来对参数进行变异,以通过程序的分支判断,到达程序更深处。
    • [5] Generate() —— 如果 corpus 为空, 则调用 Generate() 随机生成新的程序(加入新的syscall);
    • [6] Mutate() —— 调用 Mutate() 进行变异(对现有的syscall进行变异)。
  • [5] pollLoop() —— 循环等待,如果程序需要新的语料库,就调用 poll() 生成新的数据。
func main() {
    ...
    manager, err := rpctype.NewRPCClient(*flagManager, timeouts.Scale) // 初始化RPC (远程过程调用协议), 后面就通过 manager RPC 远程调用 /syz-manager/rpc.go 中的函数
    ...
    if err := manager.Call("Manager.Connect", a, r); err != nil { // RPC -> Connect() 连接
		log.Fatalf("failed to connect to manager: %v ", err)
	}
    if err := manager.Call("Manager.Check", r.CheckResult, nil); err != nil { // [1] RPC远程调用 -> syz-manager/rpc.go:Check() -> machineChecked() -> loadCorpus() 将 db 中所有的语料库加载到 mgr.candidates
			log.Fatalf("Manager.Check call failed: %v", err)
		}
    ...
    for needCandidates, more := true, true; more; needCandidates = false {
		more = fuzzer.poll(needCandidates, nil) // [2] 更新 fuzzer.corpus 语料库以及 fuzzer.workQueue 队列 !!!
		// This loop lead to "no output" in qemu emulation, tell manager we are not dead.
		log.Logf(0, "fetching corpus: %v, signal %v/%v (executing program)",
			len(fuzzer.corpus), len(fuzzer.corpusSignal), len(fuzzer.maxSignal))
	}
    ...
    fuzzer.choiceTable = target.BuildChoiceTable(fuzzer.corpus, calls) // [3] 生成 prios[X][Y] 优先级, 预测在包含系统调用X的程序中添加系统调用Y是否能得到新的覆盖 !!!
    ...
    log.Logf(0, "starting %v fuzzer processes", *flagProcs)
	for pid := 0; pid < *flagProcs; pid++ { // flagProcs —— 表示每个VM中的并行测试进程数 (来自config文件中的procs参数)
		proc, err := newProc(fuzzer, pid)
		if err != nil {
			log.Fatalf("failed to create proc: %v", err)
		}
		fuzzer.procs = append(fuzzer.procs, proc)
		go proc.loop() // [4] fuzz的核心函数,如果有剩余的Procs,就开启新的线程执行此函数。 生成新的程序和变异都在这里进行。 !!!
	}

	fuzzer.pollLoop() // [5] 循环等待,如果程序需要新的语料库,就调用poll()生成新的数据。 !!!
}

2. poll()

功能:更新 fuzzer.corpus 语料库以及 fuzzer.workQueue 队列。

说明

  • [1] —— RPC远程调用 -> syz-manager/rpc.go:Poll() -> candidateBatch() 取得 mgr.candidates 并存入 r.Candidates
  • [2] addInputToCorpus() —— 更新 fuzzer.corpus
  • [3] addCandidateInput() —— 从 r.Candidates 提取出程序, 并加入到 fuzzer.workQueue
func (fuzzer *Fuzzer) poll(needCandidates bool, stats map[string]uint64) bool {
	a := &rpctype.PollArgs{
		Name:           fuzzer.name,
		NeedCandidates: needCandidates,
		MaxSignal:      fuzzer.grabNewSignal().Serialize(),
		Stats:          stats,
	}
	r := &rpctype.PollRes{}
	if err := fuzzer.manager.Call("Manager.Poll", a, r); err != nil { // [1] RPC远程调用 -> syz-manager/rpc.go:Poll() -> candidateBatch() 取得 mgr.candidates 并存入 r.Candidates
		log.Fatalf("Manager.Poll call failed: %v", err)
	}
	maxSignal := r.MaxSignal.Deserialize() // 获得最大的信号量
	log.Logf(1, "poll: candidates=%v inputs=%v signal=%v",
		len(r.Candidates), len(r.NewInputs), maxSignal.Len())
	fuzzer.addMaxSignal(maxSignal)    // 调用了 fuzzer.maxSignal.Merge(sign) 实现此功能: 对已经存在的sign比较优先级,对没有的sign直接添加
	for _, inp := range r.NewInputs { // 更新 corpusSignal 和 maxSignal
		fuzzer.addInputFromAnotherFuzzer(inp) // [2] -> addInputToCorpus() 更新 fuzzer.corpus
	}
	for _, candidate := range r.Candidates {
		fuzzer.addCandidateInput(candidate) // [3] 从 r.Candidates 提取出程序, 并加入到 fuzzer.workQueue
	}
	if needCandidates && len(r.Candidates) == 0 && atomic.LoadUint32(&fuzzer.triagedCandidates) == 0 {
		atomic.StoreUint32(&fuzzer.triagedCandidates, 1) // 如果需要Candidates,并且Candidates长度为0,并且triagedCandidates==0,就把triagedCandidates置为1
	}
	return len(r.NewInputs) != 0 || len(r.Candidates) != 0 || maxSignal.Len() != 0 // NewInputs、Candidates、maxSignal有一个不为空,就返回true
}

3. BuildChoiceTable()

功能生成 prios[X][Y] 优先级,预测在包含系统调用X的程序中添加系统调用Y是否能得到新的覆盖

说明:返回值 ChoiceTable 会被存入 fuzzer.ChoiceTable,包含 run[X][Y] 表和 enabledCalls 可用的 syscall。

  • [1]:把 enabled 数组赋值到 enabledCalls, 并按ID大小进行排序
  • [2] CalculatePriorities() —— 根据 剩下的 corpus 计算 prios[X][Y] 优先级;
    • calcStaticPriorities() 静态组件 —— 对参数类型进行分析。例如,如果 syscall X 和 syscall Y 都接受参数 fd[sock], 把它们放在一起就更有可能出现新的覆盖。
    • calcDynamicPrio() 动态组件 —— 基于语料库中单个程序中两个系统调用一起出现的频率。例如,如果 socket()connect() 在程序中经常一起出现, 就把这对 syscalls 赋予更高优先级。
    • 静态和动态计算出来的优先级相乘,即为最终的优先级。
  • [3]:根据 prios[X][Y] 计算表 run。对系统调用 i/j来说, run[i][j] 的值是之前 run[i][x] (x<j) 的和加上 prios[i][j], 所以对 run[x] 来说是从小到大排好序的。
func (target *Target) BuildChoiceTable(corpus []*Prog, enabled map[*Syscall]bool) *ChoiceTable {
	...
    var enabledCalls []*Syscall // [1] 把可用syscall的数组 enabled 赋值到 enabledCalls, 并按ID大小进行排序
	for c := range enabled {
		enabledCalls = append(enabledCalls, c)
	}
    ...
    prios := target.CalculatePriorities(corpus)  // [2] 根据 剩下的 corpus 计算 prios[X][Y] 优先级 !!!!!
	run := make([][]int32, len(target.Syscalls)) // [3] 基于上一步计算的 prios 和启用的 syscall, 计算出表 run。对系统调用 i/j来说, run[i][j] 的值是之前 run[i][x] (x<j) 的和加上 prios[i][j], 所以对run[x]来说是从小到大排好序的。
	for i := range run {
		if !enabled[target.Syscalls[i]] {
			continue
		}
		run[i] = make([]int32, len(target.Syscalls))
		var sum int32
		for j := range run[i] {
			if enabled[target.Syscalls[j]] {
				sum += prios[i][j]
			}
			run[i][j] = sum
		}
	}
	return &ChoiceTable{target, run, enabledCalls} // 返回&ChoiceTable
}

3-1 calcStaticPriorities()

功能:静态组件,两个系统调用接受相同的参数,则更有可能出现新的覆盖。

说明

  • [1] calcResourceUsage() —— 创建hash表 map[string]map[int]weights - uses,
    • uses hash 表中,key 是 string 类型,表示某种资源;int 表示系统调用的 id;weights 表示权重。
    • 资源就表示各个函数的参数,类型可以是 Vma / Ptr / Buffer 等,不同的类型有不同的权重,比如Vma是5,Ptr是10 。
    • 调用 noteUsage() 将权重存入 uses hash 表,同一种资源同一个系统调用,只会记录最大的值。
  • [2]:编译 uses hash 表,计算 prios[X][Y] —— prios[w0.call][w1.call] += w0.inout*w1.in*3/2 + w0.inout*w1.inout ,如果c0产生的资源被c1使用,则优先级更高(乘了 3/2)。
  • [3] normalizePrio() —— 标准化处理,使优先级的值落在区间 [prioLow,prioHigh] 内。默认 10-1000
  • [4]:把 prios[c0][c0] 这种自己调用自己的情况赋予一个较高的优先级,但也不能太高 (prioHigh * 9 / 10)
func (target *Target) calcStaticPriorities() [][]int32 {
	uses := target.calcResourceUsage() // [1] 创建hash表, key是string类型, 表示某种资源; value 也是hash表, 对应 (id, value), 系统调用id和value权重。 !!!
	// 资源是通过遍历函数参数得到的, 比如可以是 Vma, Ptr, Buffer 等等。每种类型的权重是不同的, 比如Vma是5,Ptr是10。 !!!
	// noteUsage() 同一种资源同一个系统调用,只会记录最大的值
	prios := make([][]int32, len(target.Syscalls))
	for i := range prios {
		prios[i] = make([]int32, len(target.Syscalls))
	}
	for _, weights := range uses { // [2] 对uses的weights进行双重遍历,跳过自身(在[4]单独处理),设置prios的值
		for _, w0 := range weights {
			for _, w1 := range weights {
				if w0.call == w1.call {
					// Self-priority is assigned below.
					continue
				}
				prios[w0.call][w1.call] += w0.inout*w1.in*3/2 + w0.inout*w1.inout // 设置prios的值, 优先级的值是基于参数方向的,c0产生资源、c1来使用会有更高的优先级
			}
		}
	}
	normalizePrio(prios) // [3] 对prios进行规范化处理,使优先级的值落在区间[prioLow,prioHigh]内。默认10-1000
	for c0, pp := range prios { // [4] 把prios[c0][c0]这种自己调用自己的情况赋予一个较高的优先级,但也不能太高 (prioHigh * 9 / 10)。
		pp[c0] = prioHigh * 9 / 10
	}
	return prios
}

3-2 calcDynamicPrio()

功能:动态组件,单个程序中,两个syscall一起出现的频率越高,越有可能出现新的覆盖。

说明

  • [1]:统计一对syscall 一起出现在程序中的次数;
  • [2] normalizePrio() —— 标准化处理。
func (target *Target) calcDynamicPrio(corpus []*Prog) [][]int32 {
	prios := make([][]int32, len(target.Syscalls))
	for i := range prios {
		prios[i] = make([]int32, len(target.Syscalls))
	}
	for _, p := range corpus { // [1] 如果语料库中一对系统调用一起出现在程序中,则计数加 1
		for idx0, c0 := range p.Calls {
			for _, c1 := range p.Calls[idx0+1:] {
				prios[c0.Meta.ID][c1.Meta.ID]++
			}
		}
	}
	normalizePrio(prios) // [2] 规范化
	return prios
}

4. loop()

位置/syz-fuzzer/proc.go: loop()

功能:fuzz的核心函数,如果有剩余的Procs,就开启新的线程执行此函数。 生成新的程序和变异都在这里进行。

说明

  • [1]: 遍历 fuzzer.workQueue 队列,取到item中,对三种不同类型的item,分别调用不同的函数进行处理;
  • [2] triageInput() —— WorkTriage:第一次执行时,检查是否产生了新的覆盖,有新覆盖的话则 Minimize 并添加到语料库中。
  • [3] execute() —— WorkCandidate:交给 executor 执行,若产生新的 syscall,则加入到 fuzzer.workQueue 队列。
  • [4] smashInput() —— WorkSmash:对刚加入到语料库中的程序,采用syscall中的比较操作数,来对参数进行变异,以通过程序的分支判断,到达程序更深处。
  • [5] Generate() —— 如果 corpus 为空, 则调用 Generate() 随机生成新的程序 (加入新的syscall);
  • [6] Mutate() —— 调用 Mutate() 进行变异(对现有的syscall进行变异)。
func (proc *Proc) loop() {
	generatePeriod := 100
	if proc.fuzzer.config.Flags&ipc.FlagSignal == 0 {
		// If we don't have real coverage signal, generate programs more frequently
		// because fallback signal is weak.
		generatePeriod = 2 // 在下面的循环中, 当 (i % generatePeriod == 0) 时调用 Generate() 来生成新的prog, 所以generatePeriod的值越小, 生成的频率越高
	}
	for i := 0; ; i++ {
		item := proc.fuzzer.workQueue.dequeue() // [1] 遍历 fuzzer.workQueue 队列, 取到item中, 对三种不同类型的item,分别调用不同的函数进行处理
		if item != nil {
			switch item := item.(type) {
			case *WorkTriage: // WorkTriage: 第一次执行时,检查是否产生了新的覆盖,有新覆盖的话则 Minimize 并添加到语料库中。
				proc.triageInput(item) // [2] !!!
			case *WorkCandidate: // WorkCandidate: 来自hub的程序,所以现在不知道它是否对当前的fuzzer有效。proc处理它们的方式跟本地生成或变异出的程序相同。
				proc.execute(proc.execOpts, item.p, item.flags, StatCandidate) // [3] !!! 依次调用 proc.execute->proc.executeRaw->proc.env.Exec->env.cmd.exec 将数据传给executor执行, 若产生新的 syscall,则加入到 fuzzer.workQueue 队列
			case *WorkSmash: // WorkSmash: 刚加入到语料库中的程序。hint变异: 先调用execute()执行原始程序, 收集比较操作数; 调用 MutateWithHints(), 用比较操作数来替换参数,进行变异,执行变异的程序,检查是否有新覆盖
				proc.smashInput(item) // [4] !!!
			default:
				log.Fatalf("unknown work type: %#v", item)
			}
			continue
		}

		ct := proc.fuzzer.choiceTable                                 // ct用来存储 prios[X][Y] 优先级
		fuzzerSnapshot := proc.fuzzer.snapshot()                      // 保存快照
		if len(fuzzerSnapshot.corpus) == 0 || i%generatePeriod == 0 { // 如果 corpus 的长度为0或者到了i%generatePeriod == 0的计数
			// Generate a new prog.
			p := proc.fuzzer.target.Generate(proc.rnd, prog.RecommendedCalls, ct) // [5] 如果 corpus 为空, 则调用 Generate() 随机生成新的程序 (加入新的syscall) !!!
			log.Logf(1, "#%v: generated", proc.pid)
			proc.executeAndCollide(proc.execOpts, p, ProgNormal, StatGenerate)
		} else {
			// Mutate an existing prog.
			p := fuzzerSnapshot.chooseProgram(proc.rnd).Clone()
			p.Mutate(proc.rnd, prog.RecommendedCalls, ct, fuzzerSnapshot.corpus) // [6] 调用Mutate()进行变异 (对现有的syscall进行变异) !!!
			log.Logf(1, "#%v: mutated", proc.pid)
			proc.executeAndCollide(proc.execOpts, p, ProgNormal, StatFuzz)
		}
	}
}

4-1 triageInput()WorkTriage

位置/syz-fuzzer/proc.go: triageInput()

功能:第一次执行时,检查是否产生了新的覆盖,有新覆盖的话则 Minimize 并添加到语料库中。

说明

  • [1]:检查item中是否存在新的 signal, 如果不存在, 直接返回;
  • [2] executeRaw() —— 执行 item.p 程序获得执行信息 info;
  • [3] getSignalAndCover() —— 获得信号量信息 thisSignal 和覆盖率信息 thisCover
  • [4] Minimize() —— 调用 Minimize() 对程序和call进行 Minimize;
  • [5] Serialize() —— 序列化生成程序并生成hash;
  • [6] sendInputToManager() —— RPC 调用 Manager.NewInput() 将新的覆盖、信号等数据发送给 syz-manager;
  • [7] addInputToCorpus() —— 保存到语料库中。

4-2 execute()WorkCandidate

位置/syz-fuzzer/proc.go: execute()

功能:依次调用 proc.execute() -> proc.executeRaw() -> proc.env.Exec() -> env.cmd.exec() 将数据传给executor执行,若产生新的 syscall,则加入到 fuzzer.workQueue 队列。

说明

  • [1] executeRaw() —— 执行 item.p 程序获得执行信息 info;
  • [2] checkNewSignal() —— 检查有没有生成新的call;
  • [3] enqueueCallTriage() —— 把新的call加入到 fuzzer.workQueue 队列中。
func (proc *Proc) execute(execOpts *ipc.ExecOpts, p *prog.Prog, flags ProgTypes, stat Stat) *ipc.ProgInfo {
	info := proc.executeRaw(execOpts, p, stat) // [1] 执行 item.p 程序获得执行信息 info !!!
	if info == nil {
		return nil
	}
	calls, extra := proc.fuzzer.checkNewSignal(p, info) // [2] 检查有没有生成新的call
	for _, callIndex := range calls {
		proc.enqueueCallTriage(p, flags, callIndex, info.Calls[callIndex]) // [3] 把新的call加入到 fuzzer.workQueue 队列中
	}
	if extra {
		proc.enqueueCallTriage(p, flags, -1, info.Extra)
	}
	return info
}

4-3 smashInput()WorkSmash

位置/syz-fuzzer/proc.go: smashInput()

功能:对刚加入到语料库中的程序,采用syscall中的比较操作数,来对参数进行变异,以通过程序的分支判断,到达程序更深处。

说明

  • [1] :在测试过程中注入错误, 再调用 executeRaw() 执行;
  • [2] executeHintSeed() —— 采用比较操作数进行变异,称为hint变异策略
    • 先调用 execute() 执行原始程序,收集比较操作数;
    • 调用 MutateWithHints() ,用比较操作数来替换参数,进行变异,执行变异的程序,检查是否有新覆盖。
  • [3] snapshot() —— 保存一个快照;
  • [4]:循环100次,调用 Mutate() 进行变异,再调用 execute() 执行变异后的程序。
func (proc *Proc) smashInput(item *WorkSmash) {
	if proc.fuzzer.faultInjectionEnabled && item.call != -1 {
		proc.failCall(item.p, item.call) // [1] 在测试过程中注入错误, 再调用executeRaw()执行
	}
	if proc.fuzzer.comparisonTracingEnabled && item.call != -1 {
		proc.executeHintSeed(item.p, item.call) // [2] hint变异: 先调用execute()执行原始程序, 收集比较操作数; 调用 MutateWithHints(), 用比较操作数来替换参数,进行变异,执行变异的程序,检查是否有新覆盖。 !!!
	}
	fuzzerSnapshot := proc.fuzzer.snapshot() // [3] 保存一个快照
	for i := 0; i < 100; i++ {               // [4] 循环100次,调用 Mutate() 进行变异,再调用 execute() 执行变异后的程序
		p := item.p.Clone()
		p.Mutate(proc.rnd, prog.RecommendedCalls, proc.fuzzer.choiceTable, fuzzerSnapshot.corpus) // [5] !!!
		log.Logf(1, "#%v: smash mutated", proc.pid)
		proc.executeAndCollide(proc.execOpts, p, ProgNormal, StatSmash) // [6] !!!
	}
}

(1)hint变异介绍

主要函数:由 /syz-fuzzer/proc.go: executeHintSeed() 完成。

hint 变异:一个hint是一个元组,它由一个指向syscall的一个参数的指针一个value组成,该值应该赋值给该参数(syzkaller中称之为replacer)。核心原理就是利用程序中的比较操作数,来对参数进行变异,以通过程序的分支判断,到达程序更深处hint 工作流程:第1步调用 proc.execute(),后3步调用 MutateWithHints()

  • 1、Fuzzer启动一个程序(这个程序被称为 hint seed)并且收集这个程序中每一个syscall的比较数据(通过kcov的 KCOV_MODE_TRACE_CMP 模式来收集比较的数据)。
  • 2、下一步Fuzzer尝试把获得的比较操作数与输入的参数值进行匹配。
  • 3、对于每一对匹配成功的值,fuzzer用保存的值来替换对应的指针,来对程序进行变异。
  • 4、如果能获得一个有效的程序,就用fuzzer启动程序,检查有没有新的覆盖情况生成。
func (proc *Proc) executeHintSeed(p *prog.Prog, call int) {
	log.Logf(1, "#%v: collecting comparisons", proc.pid)
	// First execute the original program to dump comparisons from KCOV.
	info := proc.execute(proc.execOptsComps, p, ProgNormal, StatSeed) // [1] 先执行原始程序, 通过KCOV收集syscall中的比较操作数
	if info == nil {
		return
	}
	// 再对初始程序的每一个可以匹配成功的系统调用参数和比较操作数进行变异。执行每一次变异后的程序, 检查是否出现新的覆盖。
	// 参数2 info.Calls[call].Comps —— 每个syscall的比较操作数, 数据类型 map[uint64]map[uint64]bool
	// 参数3 传入的是一个func,用来执行程序,后面将使用到的几个函数也会把func作为参数
	p.MutateWithHints(call, info.Calls[call].Comps, func(p *prog.Prog) { // [2] !!!
		log.Logf(1, "#%v: executing comparison hint", proc.pid)
		proc.execute(proc.execOpts, p, ProgNormal, StatHint)
	})
}

参数 info.Calls[call].Comps 示例:对原本的比较值进行整理,对于每一个match,以前面的值为key,后面的值 + true 为 value。

    // Example: for comparisons {(op1, op2), (op1, op3), (op1, op4), (op2, op1)}
    // this map will store the following:
    // m = {
    //        op1: {map[op2]: true, map[op3]: true, map[op4]: true},
    //        op2: {map[op1]: true}
    // }

(2)hint变异示例

主要示例

// (1) 
// 源码:
// Models the following code:
// void f(u64 qw) {
//      u8 b = (u8) qw
//      u16 w = (u16) qw
//      u32 dw = (u32) qw
//      if (b == 0xab) {...}
//      if (w == 0xcdcd) {...}
//      if (dw == 0xefefefef) {...}
//      if (qw == 0x0101010101010101) {...}
//  }; f(0x1234567890abcdef);
// CompMap
CompMap{
    0xef:               uint64Set{0xab: true},
    0xcdef:             uint64Set{0xcdcd: true},
    0x90abcdef:         uint64Set{0xefefefef: true},
    0x1234567890abcdef: uint64Set{0x0101010101010101: true},
},
// results:
uint64Set{
    0x1234567890abcdab: true,
    0x1234567890abcdcd: true,
    0x12345678efefefef: true,
    0x0101010101010101: true,
},

// (2)
// 源码:
// void f(i32 dw) {
//      i64 qw = (i32) dw;
//      if (qw == -2) {...};
// }; f(-1);
// CompMap:
CompMap{0xffffffffffffffff: uint64Set{0xfffffffffffffffe: true}},
// results:
uint64Set{0xfffffffe: true},

shrinkExpand() 位数截断与扩展:对整数类型的数据进行截断和扩展,对位数进行统一。注意,在进行变窄的操作时忽略变宽的情况;变宽时直接变宽到 int64

  • 截断:如果调用 f(0x1234),在比较时用的 0xab0x34 进行比较,但是进行替换的时候,却没有可以与0x1234 进行匹配的值。但是如果只匹配缩减的值(0xab0x34),就可以进行替换了,用0x12ab来替换0x1234。 但是,如果把 if (y == 0xab) {...} 替换为 (y == 0xdeadbeef) {...},我们就放弃比较,因为这时我们要拓宽数据,但是我们很难得到有效的值。

    // Motivation for shrink:
    // void f(u16 x) {
    //        u8 y = (u8)x;
    //        if (y == 0xab) {...}
    // }
    
  • 扩展:如果调用 f(-1),这时 x=0xff,但是在比较时需要用0xffff0xfffe进行比较。如果用原始的0xff 就不能得到有效的匹配,因此我们需要进行拓宽并进行检查。

    // Motivation for expand:
    // void f(i8 x) {
    //        i16 y = (i16)x;
    //        if (y == -2) {...}
    // }
    

syzkaller/prog/hints_test.go 中其他示例

// (1) shrink-16-test 从16位到8位时,高8位用原数的高8位补足
 // Models the following code:
 // void f(u16 w) {
 //        u8 b = (u8) w;
 //        if (b == 0xab) {...}
 //        if (w == 0xcdcd) {...}
 //  }; f(0x1234);
// comps
    comps: CompMap{
        0x34:   compSet(0xab),
        0x1234: compSet(0xcdcd),
    }
// res
	res: []uint64{0x12ab, 0xcdcd}

// (2) shrink-32-test 对应的位置替换,不足的用原数补足
 // Models the following code:
 // void f(u32 dw) {
 //        u8 b = (u8) dw
 //        i16 w = (i16) dw
 //        if (b == 0xab) {...}
 //        if (w == 0xcdcd) {...}
 //        if (dw == 0xefefefef) {...}
 //  }; f(0x12345678);
 // comps
    comps: CompMap{
        0x78:       compSet(0xab),
        0x5678:     compSet(0xcdcd),
        0x12345678: compSet(0xefefefef),
     }
// res
	res: []uint64{0x123456ab, 0x1234cdcd, 0xefefefef}

// (3) shrink-with-a-wider-replacer-test1 这个例子中if判断不可能为真,就不需要生成新的hint了,直接返回nil
 // Models the following code:
 // void f(i16 w) {
 //        i8 b = (i8) w;
 //        i16 other = 0xabab;
 //        if (b == other) {...}
 //  }; f(0x1234);
// comps
	comps: CompMap{0x34: compSet(0x1bab)}
// res
	res:   nil

// (4) extend-32-test  再看一下扩展的情况,源代码中的扩展只给了输入为-1的情况,猜测可能是每一个拓展的位都有相同的值。其他情况就舍弃了。
 // Models the following code:
 // void f(i32 dw) {
 //        i64 qw = (i32) dw;
 //        if (qw == -2) {...};
 // }; f(-1);
 in:    0xffffffff,
 comps: CompMap{0xffffffffffffffff: compSet(0xfffffffffffffffe)},
 res:   []uint64{0xfffffffe},

// (5) extend-with-a-wider-replacer-test  匹配不到,直接返回nil
 // Models the following code:
 // void f(i8 b) {
 //        i16 w = (i16) b;
 //        if (w == (i16) 0xfeff) {...};
 // }; f(-1);
 in:    0xff,
 comps: CompMap{0xffffffffffffffff: compSet(0xfffffffffffffeff)},
 res:   nil,

4-4 Generate() 生成程序

位置/prog/generation.go: Generate()

功能:随机生成一个有ncalls个syscall的程序。

说明

  • [1] generateCall() —— 根据基准 syscall 和run表随机选择一个syscall,并生成具体的系统调用和相应参数
    • 调用 /prog/prio.go: choose() 给定基准syscall,根据run表随机选择一个系统调用(选择仍然是随机的,run表仅仅提供了有限的权重);
    • 继续根据系统调用号生成具体的系统调用和相应参数。依次调用 generateParticularCall() -> generateArgs() -> generateArg() -> generateArgImpl() -> generate()。根据参数的数据类型调用相应的 generate() 函数。
      • 例如指针类型,调用 func (a *PtrType) generate() (同样位于 /prog/prio.go 文件),随机生成一个特殊的值或者正常的值。特殊值可能是 0x0000000000000000 这样的空指针,0xffffffffffffffff 这样没有映射到的内核地址或者是 0x9999999999999999 这样不规范的地址;
      • 例如数组类型,调用 func (a *ArrayType) generate(),根据数组长度是否有指定的范围随机生成数组的长度,再根据数组的类型调用对应的 generateArg() 函数生成每个元素的值。
  • [2] analyze() -> analyzeImpl() —— 对 syscall 进行分析,对相应的类型做相应的处理;
  • [3]:超过 syscall 个数, 则移除多出的;
  • [4] sanitizeFix() —— 进行合法检测。
// 随机生成一个有ncalls个syscall的随机的程序。参数ct是一个可用的syscalls的集合,如果值为nil就表示所有的syscall都可用
func (target *Target) Generate(rs rand.Source, ncalls int, ct *ChoiceTable) *Prog {
	p := &Prog{ // 初始化
		Target: target,
	}
	r := newRand(target, rs)
	s := newState(target, ct, nil) // ct (含run表) 存到了 s.ct
	for len(p.Calls) < ncalls {
		calls := r.generateCall(s, p, len(p.Calls)) // [1] 根据基准 syscall 和run表随机选择一个syscall,并生成具体的系统调用和相应参数。 依次调用generateParticularCall()->generateArgs()->generateArg()->generateArgImpl()->generate()。 根据参数的数据类型调用相应的 generate() 函数 !!!
		for _, c := range calls {
			s.analyze(c) // [2] 对 syscall 进行分析,对相应的类型做相应的处理
			p.Calls = append(p.Calls, c)
		}
	}
	for len(p.Calls) > ncalls {
		p.RemoveCall(ncalls - 1) // [3] 超过 syscall 个数, 则移除多出的
	}
	p.sanitizeFix() // [4] 进行合法检测
	p.debugValidate()
	return p
}

4-5 Mutate() 变异

位置/prog/mutation.go: Mutate()

功能:进行变异操作,变异方法和 MutateWithHints() 完全不同。MutateWithHints() 是对能匹配的值进行替换,这里的变异和AFL的变异操作基本类似。

说明:5种变异方式,代码都位于 /prog/mutation.go

  • [1] squashAny() —— 对参数进行压缩,因为能够压缩参数的情况比较少,所以可能性最低;
  • [2] splice() —— 拼接,随机选择一个语料库外的程序p0,选一个随机数i,插入到程序 ctx.p 的第i条指令后面;
  • [3] insertCall() —— 在随机位置插入一个新的 syscall
  • [4] mutateArg() —— 对一个随机 syscall的参数进行变异。先判断参数的类型,对不同类型的参数使用不同的变异策略;
    • 例如,对于flag类型其实就是int类型,变异的方法包括加一个随机数,减一个随机数和异或一个随机数;
    • 例如,对于 struct/union 类型来说首先会检测是否含有 SpecialTypes 特殊的类型;
  • [5] removeCall() —— 随机移除一个 syscall
func (p *Prog) Mutate(rs rand.Source, ncalls int, ct *ChoiceTable, corpus []*Prog) {
	r := newRand(p.Target, rs)
	if ncalls < len(p.Calls) {
		ncalls = len(p.Calls)
	}
	ctx := &mutator{
		p:      p,
		r:      r,
		ncalls: ncalls,
		ct:     ct,
		corpus: corpus,
	}
	for stop, ok := false, false; !stop; stop = ok && len(p.Calls) != 0 && r.oneOf(3) { // 随机选择以下几种变异类型,都设置了一定的概率
		switch {
		case r.oneOf(5):
			// Not all calls have anything squashable,
			// so this has lower priority in reality.
			ok = ctx.squashAny() // [1] 对参数进行压缩,因为能够压缩参数的情况比较少,所以可能性最低;
		case r.nOutOf(1, 100):
			ok = ctx.splice() // [2] 拼接,随机选择一个语料库外的程序p0,选一个随机数i,插入到程序 ctx.p 的第i条指令后面;
		case r.nOutOf(20, 31):
			ok = ctx.insertCall() // [3] 在随机位置插入一个新的 syscall;
		case r.nOutOf(10, 11):
			ok = ctx.mutateArg() // [4] 对一个随机 syscall的参数进行变异。先判断参数的类型,对不同类型的参数使用不同的变异策略; !!!
		default:
			ok = ctx.removeCall() // [5] 随机移除一个 syscall
		}
	}
	p.sanitizeFix()
	p.debugValidate()
	if got := len(p.Calls); got < 1 || got > ncalls {
		panic(fmt.Sprintf("bad number of calls after mutation: %v, want [1, %v]", got, ncalls))
	}
}

(1)squashANY() 示例

说明:进行squash时,会调用mutateData()对数据进行变异,所涉及的规则在syzkaller/prog/mutations.gomutateDataFuncs 数组中,包括翻转字节、插入随机的字节、移除字节、添加一些字节、替换随机的字节、加减一个 int8/int16/int32/int64、把 int8/int16/int32/int64 设置为interesting的值。

// 原始程序
foo$any0(&(0x7f0000000000)=
	{0x11,
	0x11223344,
	0x2233,
	0x1122334455667788,
	{0x1, 0x7, 0x1, 0x1, 0x1bc, 0x4},
	[{@res32=0x0, @i8=0x44, "aabb"}, {@res64=0x1, @i32=0x11223344, "1122334455667788"}]
	})
// squashed 后
foo$any0(&(0x7f0000000000)=
	ANY=[@ANYBLOB="1100000044332211223300000000000088776655443322117d00bc11",
	@ANYRES32=0x0,
	@ANYBLOB="0000000044aabb00",
	@ANYRES64=0x1,
	@ANYBLOB="44332211112233445566778800000000"])

(2)splice() 示例

说明:随机选择一个语料库外的程序p0,选一个随机数i,插入到程序 ctx.p 的第i条指令后面,不改变程序 ctx.p 原有的指令顺序。

         ctx.p[0]       ctx.p[i] p0[0] <--插在这里--> p0结束 ctx.p[i+1]
            |-------------------|-------------------------|----------------------|

(3)insertCall() 示例

说明:在随机位置插入一个新的syscall。如果syscall的数目达到最大值ncalls就不要插入了。 在选取随机位置时调用了函数 biasedRand(n, k int),此函数会生成一个随机数 [0..n),但是获得 n-1 的概率是 0 的概率的k倍。 这个方法跟上面的方法不一样的是:1、这个是插入一个call,splice()是插入一个prog的所有calls;2、这个插入的call是新生成的call,splice()插入的是已经存在的prog的calls。

(4)mutateArg() 示例

说明:对一个随机call的参数进行变异。先判断参数的类型,对不同类型的参数使用不同的变异策略。

部分例子如下:prog/mutation_test.go

		// Change filename.
		{`
mutate5(&(0x7f0000001000)="2e2f66696c653000", 0x22c0)
mutate5(&(0x7f0000001000)="2e2f66696c653000", 0x22c0)
`, `
mutate5(&(0x7f0000001000)="2e2f66696c653000", 0x22c0)
mutate5(&(0x7f0000001000)="2e2f66696c653100", 0x22c0)
`},
		// Extend an array.
		{`
mutate3(&(0x7f0000000000)=[0x1, 0x1], 0x2)
`, `
mutate3(&(0x7f0000000000)=[0x1, 0x1, 0x1], 0x3)
`},
		// Mutate size from it's natural value.
		{`
mutate7(&(0x7f0000000000)='123', 0x3)
`, `
mutate7(&(0x7f0000000000)='123', 0x2)
`},
		// Mutate proc to the special value.
		{`
mutate8(0x2)
`, `
mutate8(0xffffffffffffffff)
`},
		// Remove calls and update args.   (mutator的)removeCall函数调用(Prog的)removeCall函数随机移除一个系统调用
		{`
r0 = mutate5(&(0x7f0000000000)="2e2f66696c653000", 0x0)
mutate0()
mutate6(r0, &(0x7f0000000000)="00", 0x1)
mutate1()
`, `
mutate0()
mutate6(0xffffffffffffffff, &(0x7f0000000000)="00", 0x1)
mutate1()
`},

5. executor

位置executor/executor.cc: main() 414

executor/common_linux.h 中的 do_sandbox_none() 函数为例,调用链 do_sandbox_none() -> loop() -> execute_one() -> schedule_call() -> thread_create() -> thread_start() -> worker_thread() -> execute_call() -> execute_syscall(),最后被执行并得到代码覆盖率等信息。

参考

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

[原创]syzkaller源码分析(二) syz-fuzzer.go

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

[原创]syzkaller源码分析(三) executeHintSeed() Mutate() Generate()

文档信息

Search

    Table of Contents