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许可证)