-
-
[原创]syzkaller源码分析(二) syz-fuzzer.go
-
2021-6-23 09:48 8819
-
这一篇主要来说syz.fuzzer的内容,它主要负责fuzz的部分,但是由于篇幅原因,Mutate()和Generate()以及executeHintSeed()会放到下一篇中。
写的目的为了删繁就简,只列举了比较重要的函数和主要流程,一些具体的实现要结合源码看。源码的地址 https://github.com/google/syzkaller
syz-manager.go的解析在 https://bbs.pediy.com/thread-268152.htm
参考 https://xz.aliyun.com/t/5223
md文件会附在最后。
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 | fuzzer.go func func main() { / / 做一些检查以及初始化的工作 获取架构信息和系统信息 初始化ipc / / ipc:Inter - Process Communication 进程间通信 初始化rpc 解析参数 检查参数 / / 更新corpus以及candidate队列,下面会提到 for needCandidates, more : = true, true; more; needCandidates = false { more = fuzzer.poll(needCandidates, nil) } / / call - to - call的优先级,下面会提到 fuzzer.choiceTable = target.BuildChoiceTable(fuzzer.corpus, calls) / / 最主要的函数之一,用来生成program或者变异已经存在的program,下面会提到 / / flagProcs用来表示number of parallel test processes for pid : = 0 ; pid < * flagProcs; pid + + { proc, err : = newProc(fuzzer, pid) fuzzer.procs = append(fuzzer.procs, proc) / / 最主要的函数之一,如果有剩余的Procs,就开启新的协程执行此函数 / / fuzz的核心,生成新的prog和变异都在这里进行。 / / 下面会提到 go proc.loop() } / / 循环等待,如果程序需要新的语料库,就调用poll()生成新的数据。就在下面。 fuzzer.pollLoop() } / / 主要功能就是:循环等待,如果程序需要新的语料库,就调用poll()生成新的数据。 func (fuzzer * Fuzzer) pollLoop() { ticker : = time.NewTicker( 3 * time.Second * fuzzer.timeouts.Scale).C for { ... if poll || time.Since(lastPoll) > 10 * time.Second * fuzzer.timeouts.Scale { log() } if poll || time.Since(lastPoll) > 10 * time.Second * fuzzer.timeouts.Scale { 进行判断 为环境变量赋值 fuzzer.poll(needCandidates, stats) 更新参数 } } } |
重点函数 poll(needCandidates, nil)
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 | / / 更新corpus以及candidate队列 func (fuzzer * Fuzzer) poll(needCandidates bool , stats map [string]uint64) bool { / / 启动Manager.Poll,不过没找到对应的代码在哪里,猜测可能类似linux的poll(),用来处理队列信息 fuzzer.manager.Call( "Manager.Poll" , a, r) / / 用来获得最大的信号量 / / 调用了fuzzer.maxSignal.Merge(sign)实现此功能:对已经存在的sign比较优先级,对没有的sign直接添加 fuzzer.addMaxSignal(maxSignal) for _, inp : = range r.NewInputs { / / 更新corpusSignal和maxSignal / / 主要调用了fuzzer.addInputToCorpus(p, sign, sig),这个函数又调用了Merge(sign) fuzzer.addInputFromAnotherFuzzer(inp) } for _, candidate : = range r.Candidates { / / 使candidate进入队列workQueue / / 先反序列化获得Prog,再根据candidate.Minimized、candidate.Smashed的值对flags对应的位进行置位 fuzzer.addCandidateInput(candidate) } / / 如果需要Candidates,并且Candidates长度为 0 ,并且triagedCandidates = = 0 ,就把triagedCandidates置为 1 if needCandidates && len (r.Candidates) = = 0 && atomic.LoadUint32(&fuzzer.triagedCandidates) = = 0 { atomic.StoreUint32(&fuzzer.triagedCandidates, 1 ) } / / NewInputs、Candidates、maxSignal有一个不为空,就返回true return len (r.NewInputs) ! = 0 || len (r.Candidates) ! = 0 || maxSignal. Len () ! = 0 } |
重点函数 BuildChoiceTable(fuzzer.corpus, calls)
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 | / / 用来获取prios[X][Y]的优先级,prios[X][Y]表示是否在一个包含X的program中插入一个call Y会出现新的覆盖率 / * 函数的返回值是 * ChoiceTable,作者对ChoiceTable的原注释如下: ChooseTable allows to do a weighted choice of a syscall for a given syscall based on call - to - call priorities and a set of enabled syscalls. call - to - call priorities就是上面解释的prios[X][Y]; enabled syscalls 是指的在运行的架构系统中可以使用的syscall,代码对disable syscall进行了筛选 * / func (target * Target) BuildChoiceTable(corpus [] * Prog, enabled map [ * Syscall] bool ) * ChoiceTable { / / 如果enabled = = nil,把target.Syscalls依次赋值给enabled,并置为true if enabled = = nil { enabled = make( map [ * Syscall] bool ) for _, c : = range target.Syscalls { enabled[c] = true } } / / 删除enabled是不可用(Disabled)的元素 for call : = range enabled { if call.Attrs.Disabled { delete(enabled, call) } } / / 把enabled数组赋值到enabledCalls(省略部分一);并按 ID 大小进行排序;省略部分二对corpus再进行检查,看有没有不可用的disabled syscall ...(省略部分一) sort. Slice (enabledCalls, func(i, j int ) bool { return enabledCalls[i]. ID < enabledCalls[j]. ID }) ...(省略部分二) / / * * * * * * * * * * * * * * * * * 计算优先级 * * * * * * * * * * * * * * * * / / 重点函数,下面会提 prios : = target.CalculatePriorities(corpus) 把优先级赋值到run[][]的每一个元素 / / 返回&ChoiceTable return &ChoiceTable{target, run, enabledCalls} } |
重点函数 CalculatePriorities(corpus []*Prog)
在BuildChoiceTable()中被调用
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 | / / 用来计算优先级 / / 先把函数的原始注释粘上 / / Calulation of call - to - call priorities. / / For a given pair of calls X and Y, the priority is our guess as to whether / / additional of call Y into a program containing call X is likely to give / / new coverage or not . / / The current algorithm has two components: static and dynamic. / / The static component is based on analysis of argument types. For example, / / if call X and call Y both accept fd[sock], then they are more likely to give / / new coverage together. / / The dynamic component is based on frequency of occurrence of a particular / / pair of syscalls in a single program in corpus. For example, if socket and / / connect frequently occur in programs together, we give higher priority to / / this pair of syscalls. / / Note: the current implementation is very basic, there is no theory behind any / / constants. / * 对上面内容做一下大致的解释: 这个函数用来计算call - to - call的优先级。 call - to - call的优先级用prios[X][Y]来表示,对于一对给定的calls X和Y,优先级是指 在一个包含X的program中,加入call Y后能不能生成新的覆盖情况。 这个算法有两个部分:静态部分和动态部分,也调用了两个对应的函数, calcStaticPriorities()和calcDynamicPrio(corpus)。 静态部分calcStaticPriorities()是对参数类型进行分析。举个例子,如果call X和call Y 都接受参数fd[sock],把它们放在一起就更有可能出现新的覆盖情况。 动态部分calcDynamicPrio(corpus)是指在语料库中的单个程序中一对特定的syscalls出现的 频率。举个例子,如果socket和connect在programs中经常出现,就把这一对syscalls给予更高的优 先级。 * / func (target * Target) CalculatePriorities(corpus [] * Prog) [][]int32 { / * calcStaticPriorities()通过静态的参数分析设置优先级。 具体实现一定要结合代码一起看,这里为了留核心的东西就不再另贴一个函数了。 代码在syzkaller / prog / prio.go 41 行左右。 第一步,调用calcStaticPriorities()对不同的 Type 设置不同的权值weight,返回uses map [string] map [ int ]weights; 对uses的weights进行双重遍历,跳过自身(在第三步单独处理),设置prios的值,值为: prios[w0.call][w1.call] + = w0.inout * w1. in * 3 / 2 + w0.inout * w1.inout 对这个值源码也有相关的注释,优先级的值是基于参数方向的,c0产生资源、c1来使用会有更高的优先级: / / The static priority is assigned based on the direction of arguments. A higher priority will be / / assigned when c0 is a call that produces a resource and c1 a call that uses that resource. 第二步,调用normalizePrio(prios)对prios进行normalize处理,使优先级的值落在区间[prioLow,prioHigh]内。默认 10 - 1000 第三步,把prios[c0][c0]这种自己调用自己的情况赋予一个较高的优先级,但也不能太高(prioHigh * 9 / 10 )。 * / static : = target.calcStaticPriorities() if len (corpus) ! = 0 { / / calcDynamicPrio(corpus)用来动态处理优先级,这个就简单一点了。 / / 先统计在每个prog中两个call一起出现的次数,再进行normalize就可以了。 dynamic : = target.calcDynamicPrio(corpus) for i, prios : = range dynamic { dst : = static[i] for j, p : = range prios { / / 把静态和动态得到的优先级统计到一起。 dst[j] = dst[j] * p / prioHigh } } } return static } |
重点函数loop()
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 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 | 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. / / 在下面的循环中,当i % generatePeriod = = 0 时调用Generate()来生成新的prog, / / 所以generatePeriod的值越小,生成的频率越高。 generatePeriod = 2 } for i : = 0 ; ; i + + { / / fuzzer队列依次出队返回到item中,下面用switch来判断item的类型, / / 对三种不同类型的item,分别调用不同的函数进行处理。 item : = proc.fuzzer.workQueue.dequeue() if item ! = nil { switch item : = item.( type ) { / * * WorkTriage的原注释如下: / / WorkTriage are programs for which we noticed potential new coverage during / / first execution. But we are not sure yet if the coverage is real or not . / / During triage we understand if these programs in fact give new coverage, / / and if yes, minimize them and add to corpus. WorkTriage是在第一次运行时有可能出现新的覆盖的程序。但是不确定是否真的存在新的覆盖率。 在triage的过程中去了解这些程序是否真正有了新的覆盖率。如果有了新的覆盖率,进行minimize并把 它加入到语料库中。 * / case * WorkTriage: / * 函数triageInput(item)的大致流程如下: 首先,通过调用signalPrio()、FromRaw()、corpusSignalDiff()来看 * WorkTriage类型 的item中是否存在新的signal。如果不存在,直接返回。 下一步,进入 for 循环执行signalRuns次(默认为 3 次)。调用executeRaw()执行item的program 获得执行信息info。用reexecutionSuccess()来判断是否执行成功,不成功就返回或者再次执行。 再调用getSignalAndCover()获得信号量信息thisSignal和覆盖率信息thisCover。再调用 Intersection()返回thisSignal和newSignal共有且thisSignal中优先级高的signal,放在 newSignal中。最后调用Merge()把新的覆盖情况添加到原来的覆盖情况inputCover中。 下一步,如果item.flags&ProgMinimized = = 0 成立,即需要进行Minimize,就调用Minimize() 对程序和call进行Minimize。 最后,序列化、 hash ,保存到语料库中。 * / proc.triageInput(item) / * * WorkCandidate的原注释如下: / / WorkCandidate are programs from hub. / / We don't know yet if they are useful for this fuzzer or not . / / A proc handles them the same way as locally generated / mutated programs. WorkCandidate是来自hub的程序,所以现在不知道它是否对当前的fuzzer有效。 进程处理它们的方式跟本地生成或变异出的程序相同。 * / case * WorkCandidate: / * 首先,调用executeRaw()执行item.p返回对应的info; 然后,调用checkNewSignal()看有没有生成新的call; 最后,调用enqueueCallTriage()把新的call加入到队列中。 * / proc.execute(proc.execOpts, item.p, item.flags, StatCandidate) / * * WorkSmash的原注释如下: / / WorkSmash are programs just added to corpus. / / During smashing these programs receive a one - time special attention / / (emit faults, collect comparison hints, etc). WorkSmash是刚加入到语料库中的程序。在smashing过程中这类程序会进行一些特殊的处理。 * / case * WorkSmash: / * 第一步,如果faultInjectionEnabled = = true,就调用failCall()。failCall()用来 在测试过程中注入错误(inject a fault in this execution)。先通过opts.Flags | = ipc.FlagInjectFault 对相应的位置置位,再调用executeRaw()执行。 第二步,如果comparisonTracingEnabled = = true,就调用executeHintSeed()。这个 方法先调用execute()执行原始程序,再调用MutateWithHints()执行变异的程序,看有没有 出现新的覆盖情况。注意这里的变异和Mutate()生成的变异不一样,会放在下一篇说。 第三步,调用snapshot()保存一个快照。 第四步,进行 100 次循环,调用Mutate()进行大变异,再调用execute()执行变异后的程序。 Mutate()和Generate()会在下一篇单独说。 * / proc.smashInput(item) default: log.Fatalf( "unknown work type: %#v" , item) } continue } / / ct用来存储call - to - call的优先级,上面有详细的介绍 ct : = proc.fuzzer.choiceTable / / 保存快照 fuzzerSnapshot : = proc.fuzzer.snapshot() / / 如果corpus的长度为 0 或者到了i % generatePeriod = = 0 的计数时, / / 调用Generate()生成新的程序。Mutate()和Generate()会在下一篇单独说。 if len (fuzzerSnapshot.corpus) = = 0 || i % generatePeriod = = 0 { / / Generate a new prog. p : = proc.fuzzer.target.Generate(proc.rnd, prog.RecommendedCalls, ct) log.Logf( 1 , "#%v: generated" , proc.pid) proc.execute(proc.execOpts, p, ProgNormal, StatGenerate) } else { / / 调用Mutate()进行变异。Mutate()和Generate()会在下一篇单独说。 / / Mutate an existing prog. p : = fuzzerSnapshot.chooseProgram(proc.rnd).Clone() p.Mutate(proc.rnd, prog.RecommendedCalls, ct, fuzzerSnapshot.corpus) log.Logf( 1 , "#%v: mutated" , proc.pid) proc.execute(proc.execOpts, p, ProgNormal, StatFuzz) } } } |
赞赏
他的文章
谁下载
看原图