看到网上大部分对于syzkaller的分析都是对于fuzz linux内核的流程的,尝试性的写一下其fuzz windows这样的闭源内核的流程。
先看一下docs/windows/readme.txt。给我们提供了一些有效信息在于
1 2 3 4 若要更新描述,请运行(假设 cl 交叉编译器在 PATH 中): syz-extract -os=windows syz-sysgen sys/windows/windows.txt 是通过 tools/syz-declextract 从 Windows 头文件自动提取的。
那么咱们追踪一下,在syz-extract文件传入os=windows时会发生什么。在sys/sys-extract/extract.go文件中有如下定义,将flagOS设置为runtime.GOOS或者用户传入的-os值
1 flagOS = flag.String("os", runtime.GOOS, "target OS")
那么上面的命令行传入后flagOS会被设置为windows。在下面的main函数中有
1 2 3 4 5 6 7 8 9 OS := *flagOS extractor := extractors[OS] if extractor == nil { tool.Failf("unknown os: %v" , OS) } arches, nfiles, err := createArches(OS, archList(OS, *flagArch), flag.Args()) if err != nil { tool.Fail(err) }
将OS设置为上文的flagOS,extractor是什么呢?在该文件的前文有定义
1 2 3 4 5 6 7 8 9 10 11 12 var extractors = map [string ]Extractor{ targets.Akaros: new (akaros), targets.Linux: new (linux), targets.FreeBSD: new (freebsd), targets.Darwin: new (darwin), targets.NetBSD: new (netbsd), targets.OpenBSD: new (openbsd), "android" : new (linux), targets.Fuchsia: new (fuchsia), targets.Windows: new (windows), targets.Trusty: new (trusty), }
相当于new(windows),windows这个结构体就定义在同一个文件夹的windows.go中,主要内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 type windows struct {}func (*windows) prepare(sourcedir string , build bool , arches []*Arch) error { return nil } func (*windows) prepareArch(arch *Arch) error { return nil } func (*windows) processFile(arch *Arch, info *compiler.ConstInfo) (map [string ]uint64 , map [string ]bool , error ) { params := &extractParams{ DeclarePrintf: true , TargetEndian: arch.target.HostEndian, } return extract(info, "cl" , nil , params) }
可以看到结构体中有三个函数,prepare,prepareArch都直接返回nil,主要就看最后这个processFile函数,接收两个参数,分别是系统架构信息和编译器的架构信息,返回三个参数,一个str->uint64的集合,一个str->bool的集合,一个error信息。而返回信息是通过extract函数得到的,具体的extract函数后文再说,先回到刚才的main函数中。
如果extractor无法识别,就报错。否则的话,调用createArches函数。该函数也在extract.go文件中。不过咱们先搞懂传入的到底是些个什么东西第一个参数OS不说了,第二个参数使用archList函数获取的,传入这个函数的两个参数分别是OS和archFlag,这个是用户输入的架构,那么现在来看archList函数
1 2 3 4 5 6 7 8 9 10 11 func archList (OS, arches string ) []string { if arches != "" { return strings.Split(arches, "," ) } var archArray []string for arch := range targets.List[OS] { archArray = append (archArray, arch) } sort.Strings(archArray) return archArray }
接着追一下targets.List,在sys/targets/targets.go中可以找到相关的定义,可以看到,对于windows只考虑AMD64这一种,其中包括了该架构下的指针大小,页大小,大小端序等等。
1 2 3 4 5 6 7 8 9 10 11 12 var List = map [string ]map [string ]*Target{ ... Windows: { AMD64: { PtrSize: 8 , PageSize: 4 << 10 , LittleEndian: true , }, }, ... }
那咱们就可以确定了,传入extract函数的第二个参数就是一个只有AMD64的字符串数组,第三个参数flag.Args则传入了未能解析的命令行参数。弄明白了参数就可以去分析createArches函数了。
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 func createArches (OS string , archArray, files []string ) ([]*Arch, int , error ) { errBuf := new (bytes.Buffer) eh := func (pos ast.Pos, msg string ) { fmt.Fprintf(errBuf, "%v: %v\n" , pos, msg) } top := ast.ParseGlob(filepath.Join("sys" , OS, "*.txt" ), eh) if top == nil { return nil , 0 , fmt.Errorf("%v" , errBuf.String()) } allFiles := compiler.FileList(top, OS, eh) if allFiles == nil { return nil , 0 , fmt.Errorf("%v" , errBuf.String()) } if len (files) == 0 { for file := range allFiles { files = append (files, file) } } nfiles := 0 var arches []*Arch for _, archStr := range archArray { buildDir := "" if *flagBuild { dir, err := os.MkdirTemp("" , "syzkaller-kernel-build" ) if err != nil { return nil , 0 , fmt.Errorf("failed to create temp dir: %w" , err) } buildDir = dir } else if *flagBuildDir != "" { buildDir = *flagBuildDir } else { buildDir = *flagSourceDir } target := targets.Get(OS, archStr) if target == nil { return nil , 0 , fmt.Errorf("unknown arch: %v" , archStr) } arch := &Arch{ target: target, sourceDir: *flagSourceDir, includeDirs: *flagIncludes, buildDir: buildDir, build: *flagBuild, done: make (chan bool ), } var archFiles []string for _, file := range files { meta, ok := allFiles[file] if !ok { return nil , 0 , fmt.Errorf("unknown file: %v" , file) } if meta.NoExtract || !meta.SupportsArch(archStr) { continue } archFiles = append (archFiles, file) } sort.Strings(archFiles) for _, f := range archFiles { arch.files = append (arch.files, &File{ arch: arch, name: f, done: make (chan bool ), }) } arches = append (arches, arch) nfiles += len (arch.files) } return arches, nfiles, nil }
总而言之这里返回了对应架构的结构体数组,文件数量,和错误情况。
接下来,调用了extractor类的prepare函数
1 2 3 if err := extractor.prepare(*flagSourceDir, *flagBuild, arches); err != nil { tool.Fail(err) }
主要就是定义了三个函数,windows这个target类实现了这些函数,不过根据前文,这个函数是直接返回nil的,不用看了,嘿嘿
1 2 3 4 5 6 7 jobC := make (chan interface {}, len (arches)+nfiles) for _, arch := range arches { jobC <- arch } for p := 0 ; p < runtime.GOMAXPROCS(0 ); p++ { go worker(extractor, jobC) }
这里要看一下worker函数的定义
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 func worker (extractor Extractor, jobC chan interface {}) { for job := range jobC { switch j := job.(type ) { case *Arch: infos, err := processArch(extractor, j) j.err = err close (j.done) if j.err == nil { for _, f := range j.files { f.info = infos[filepath.Join("sys" , j.target.OS, f.name)] jobC <- f } } case *File: j.consts, j.undeclared, j.err = processFile(extractor, j.arch, j) close (j.done) } } }
proc.go 我决定先不看extract.go了。先来看看核心内容吧。
先看看fuzz的主循环,代码在sys-fuzzer/proc.go中:
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 func (proc *Proc) loop() { generatePeriod := 100 if proc.fuzzer.config.Flags&ipc.FlagSignal == 0 { generatePeriod = 2 } for i := 0 ; ; i++ { item := proc.fuzzer.workQueue.dequeue() if item != nil { switch item := item.(type ) { case *WorkTriage: proc.triageInput(item) case *WorkCandidate: proc.execute(proc.execOpts, item.p, item.flags, StatCandidate) case *WorkSmash: proc.smashInput(item) default : log.SyzFatalf("unknown work type: %#v" , item) } continue } ct := proc.fuzzer.choiceTable fuzzerSnapshot := proc.fuzzer.snapshot() if len (fuzzerSnapshot.corpus) == 0 || i%generatePeriod == 0 { p := proc.fuzzer.target.Generate(proc.rnd, prog.RecommendedCalls, ct) log.Logf(1 , "#%v: generated" , proc.pid) proc.executeAndCollide(proc.execOpts, p, ProgNormal, StatGenerate) } else { p := fuzzerSnapshot.chooseProgram(proc.rnd).Clone() p.Mutate(proc.rnd, prog.RecommendedCalls, ct, proc.fuzzer.noMutate, fuzzerSnapshot.corpus) log.Logf(1 , "#%v: mutated" , proc.pid) proc.executeAndCollide(proc.execOpts, p, ProgNormal, StatFuzz) } } }
接下来具体看一下这里面的依赖,先看看WorkTriage这个类型。该定义在sys-fuzzer中的workqueue.go中
1 2 3 4 5 6 type WorkTriage struct { p *prog.Prog call int info ipc.CallInfo flags ProgTypes }
下面来看triageInput函数具体是怎么处理WorkTriage的。这个函数会对输入进行一个“分类”,确定测试样例的优先级
先计算一个信号优先级
1 prio := signalPrio(item.p, &item.info, item.call)
该函数在同文件夹下的signalPrio中实现
1 2 3 4 5 6 7 8 9 10 11 12 func signalPrio (p *prog.Prog, info *ipc.CallInfo, call int ) (prio uint8 ) { if call == -1 { return 0 } if info.Errno == 0 { prio |= 1 << 1 } if !p.Target.CallContainsAny(p.Calls[call]) { prio |= 1 << 0 } return }
很简短的代码,如果call为-1,那prio返回为0。应该该不存在-1的call,所以这里用-1来表示特殊情况。
1 2 3 4 5 type Prog struct { Target *Target Calls []*Call Comments []string }
在prog/prog.go的文件中可以找到这个定义。有三个元素,target、calls、和Comments。target的定义前面有说,不再赘述。主要看看这个call是怎么个事。定义就在下面
1 2 3 4 5 6 7 type Call struct { Meta *Syscall Args []Arg Ret *ResultArg Props CallProps Comment string }
这里面元素比较多了,先看syscall。定义在同目录下的types.go中
1 2 3 4 5 6 7 8 9 10 11 12 13 type Syscall struct { ID int NR uint64 Name string CallName string MissingArgs int Args []Field Ret Type Attrs SyscallAttrs inputResources []*ResourceDesc outputResources []*ResourceDesc }
含义都在变量名上写着了,不多说。接下来是Args。Arg的定义就在proc.go的下面
1 2 3 4 5 6 7 8 type Arg interface { Type() Type Dir() Dir Size() uint64 validate(ctx *validCtx) error serialize(ctx *serializer) }
一个接口,内中有五个方法。先看第一个,但是看之前先看第一个在实现中输入的类型ArgCommon
1 2 3 4 type ArgCommon struct { ref Ref dir Dir }
两个内容,一个Ref,一个Dir,Dir表示参数方向(输入还是输出)
接下来看type
1 2 3 4 5 6 func (arg ArgCommon) Type() Type { if arg.ref == 0 { panic ("broken type ref" ) } return typeRefs.Load().([]Type)[arg.ref] }
相关的定义如下:
1 2 3 4 var ( typeRefMu sync.Mutex typeRefs atomic.Value )
而上面ret中的load是go语言对atomic的一个自带方法。大概是能在多线程的情况下防止冲突。这句话可以简单的理解成返回arg.ref
而关于Dir的定义也是非常的简短,即返回arg.dir
接下来是size,设计的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 type ConstArg struct { ArgCommon Val uint64 } func MakeConstArg (t Type, dir Dir, v uint64 ) *ConstArg { return &ConstArg{ArgCommon: ArgCommon{ref: t.ref(), dir: dir}, Val: v} } func (arg *ConstArg) Size() uint64 { return arg.Type().Size() }
首先有个新的type:ConstArg。然后MakeConstArg这个函数负责建立一个ConstArg。Size()这个函数就相当于返回arg.ref.Size()