1. 介绍
syz-manager 功能:主要负责各种工作的启动(HTTP、RPC、dashboard等等)、调用fuzz以及repro的生成。
fuzz命令:$ ./syz-manager -config=my.cfg
示例qemu.cfg:syzkaller\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时接收通知的电子邮件地址,只支持 Mailxsshkey
:用于与虚拟机通信的SSH密钥的位置sandbox
:沙盒模式,支持以下模式none
:默认设置,不做任何特殊的事情setuid
:冒充用户nobody(65534)namespace
:使用命名空间删除权限(内核需要设置CONFIG_NAMESPACES
,CONFIG_UTS_NS
,CONFIG_USER_NS
,CONFIG_PID_NS
和CONFIG_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
。
- crash复现:提取出触发crash的C代码。
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时设置的一些选项,比如线程、并发、沙盒等等。简化选项分别保存在progSimplifies
和cSimplifies
数组中。
// 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
}
参考
[原创]syzkaller源码分析(一) syz-manager.go
文档信息
- 本文作者:bsauce
- 本文链接:https://bsauce.github.io/2022/05/14/syzkaller2/
- 版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)