首页
社区
课程
招聘
[原创]syzkaller源码分析(二) syz-fuzzer.go
2021-6-23 09:48 8819

[原创]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)
        }
    }
}

[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

上传的附件:
收藏
点赞3
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回