0x00 写在前面
本文主要对 Syzkaller 的学习过程进行记录,其中不少内容都是来自个人对源码的研读。由于是目的导向性的,所以整个文本都以某种功能为需求,然后再找相关代码的实现进行记录的,因此可能并不适合新学者进行系统性的学习。
由于是初学者写的笔记,对 golang
也不是特别熟悉,基本就是能看懂,能简单改改的地步,所以可能有些谬误之处。
0x01 coverage 检测方法
该部分来自对 coverage 的整理和翻译。
Syzkaller 使用 kcov 来收集内核的覆盖率信息。kcov 将会探索每个被执行的基本块的地址,然后 Syzkaller 在运行时会使用 binutils
(objdump
, nm
, addr2line
, readelf
) 中的工具来映射这些地址为源代码中的函数和 lines
。
Binutils
readelf
使用 readelf
来获取虚拟内存偏移。具体使用的命令是:
1 | readelf -SW kernel_image |
-S
: 列举 kernel image 文件中的所有 section headers。-W
: 每一行单独输出每个 section 的 header 。
输出的数据大约是下面这个样子:
1 | There are 59 section headers, starting at offset 0x3825258: |
Syzkaller 中的 executor
会将 PC 中的值截断保存为 uint32
的值,而后发送给 syz-manager
。syz-manager
将会使用 section header 中的信息来恢复计算偏移。Syzkaller 中只考虑 Type
为 PROGBITS
的 section header (这个字段是啥含义?为啥只考虑这个部分)。Address
字段表示一个 section 在内存中的虚拟地址。要求所有的 PROGBITS
的 section 具有相同的 32 位高地址 (0xffff ffff
),这 32 位将用于进行偏移恢复。
Reporting coverage data
MakeReportGenerator
这个玩意创建了一个数据库对象来进行报告。它需要目标数据,以及有关源文件和构建目录位置的信息。而构建数据库的第一步是从目标二进制文件中提取函数数据。
nm
nm
是用于处理 kernel 中每个函数的地址和大小的。
1 | nm -Ptx kernel_image |
-P
- 使用便携的输出格式(标准输出-tx
- 以 hex 的格式写数字
输出数据大概是下面的样子:
1 | tracepoint_module_nb d ffffffff84509580 0000000000000018 |
- 第一列是符号的名字。
- 第二列是它的类型(
text section
,data section
,debugging symbol
,undefined
,zero-init section
, etc) - 第三列是它 hex 形式的符号值。
- 第四列是它的大小。在 Syzkaller 中大小总是四舍五入到 16。
对于覆盖率报告, Syzkaller 只关注于代码段(code section
),所以会对 nm
的输出进行过滤,只要类型是 t
or T
的符号。最终的结果是一个使用符号作为键,使用符号的起始和终止地址为值的字典。这个数据将用于覆盖数据到符号(函数)的映射。需要此步骤来查明是否调用了某些函数。
Object Dump and Symbolize
为了给 Syzkaller 提供必要的信息用于捕获覆盖率信息,需要对编译器进行插桩,让其在编译阶段在每个基本块生成的时候,插入 __sanitizer_cov_trace_pc
调用。该调用可以作为一个锚点指令,返回覆盖的代码的行数(backtrack the covered code lines)。
objdump
objdump
被用于处理内核镜像中每个调用 __sanitizer_cov_trace_pc
时的 PC 的值。(这时的 PC 值就是下一个要执行的指令的地址,而这个又是我们给每个基本块中插入的,那这样不就捕获到了每个被执行的基本块的地址了吗?)这些 PC 值表示内核映像中内置的所有代码。将 kcov 导出的 PC 值与这些值进行比较以确定覆盖率。(我不明白,不相信kcov?还是不相信自己的插桩?这个做法还会有什么错的情况吗?)
内核 image 可以使用下面的命令反汇编。
1 | objdump -d --no-show-raw-insn kernel_image |
-d
- 反汇编可执行代码块-no-show-raw-insn
- 防止在符号反汇编的同时打印十六进制
部分输出大概长这个样子
1 | ... |
从这个输出来看,覆盖率 trace 调用时被标记来判别可执行区块的起始地址的。(从这个输出覆盖跟踪调用被识别以确定可执行块地址的开始)。(From this output coverage trace calls are identified to determine the start of the executable block addresses:)(我的理解是,应该是说上面这一堆中,我们应该关注的有用的信息是下面这两行)
1 | ffffffff81000f41: callq ffffffff81382a00 <__sanitizer_cov_trace_pc> |
addr2line
addr2line
被用于映射 kcov 发现的 PC 值,然后被 objdump
进一步处理成源码文件和行。
1 | addr2line -afip -e kernel_image |
-afip
- 显示地址、函数名字和 unwind ilined functions 和适合人类读。-e
- 是用于指定可执行文件,而不是使用默认值a.out
。
addr2line
从标准输入中读取 16 进制地址,然后在标准输出打印每个地址的文件名、函数和行号。
使用示例,其中 >>
表示询问, <<
表示 addr2line
的响应:
1 | >> ffffffff8148ba08 |
但是我实际实测发现输出的都是 ?? 这样的样子,我还以为是没有符号表,网上查了查,好像是这个命令只接受相对偏移而不是一个绝对地址,应该减去起始地址。但是我还是只能整出来这些 ??
最终的目标是构建一个 frames
的哈希表,键是 PC 值,值是一个 frame array
由下列的信息组成:
PC
- 64 位的 program counter 的值,与键一样。Func
- 这个 frame 属于的函数的名字File
- 文件名(函数或者 frame 所属的)Line
- PC 映射所对应的一个文件中的行号Inline
- 布尔值 inlining 信息。由于 inlining 的存在,可能会有很多个 frame 能够被连接到同一个 PC 值上。
Create report
一旦 frames 和函数地址范围的数据库构建成功后,下一步就是去确定程序(program)的覆盖率。每个 program 在这里就是一系列 PC 值。此时每个函数的地址范围都是已知的,所以很容易通过简单迭代比较 PC 中的值和函数的范围来判断到底是哪个函数被调用了。此外基于以 PC 值为键的 frame 哈希表中,覆盖率信息被聚集到了源文件上。这些被称为 coveredPCs
。覆盖率结果并不是基于行的而是基于基本块的。最终的结果是保存在如下的文件结构中。
lines
- 文件中被覆盖的行。(lines coverd in the filetotalPCs
- 这个文件中总共被标注的 PCcoveredPCS
- 这个 program 执行过程中被执行了的 PCtotalInline
- 被映射到 inlined frames 上的 PC 的数量coveredInline
- 在这个 program 执行的过程中被映射到 inlined frames 上的 PC
问题:不是很懂这个文件开始保存的时机或者说这些 PC 拿到时候的时机。
0x02 go func & feature
对一些 go 的语法和函数进行一个解释, syzkaller 中关于随机相关的定义在 prog/rand.go:line 561 。
go 疑似函数命名必须驼峰;不用的变量,例如遍历字典时需要使用 _ 不然就会报错
r.Intn(n)
每次调用 r.Intn(n)
是重新随机执行,返回一个 [0, n)
中的整数。参考自: link .
1 | // Intn, Int31n, and Int63n limit their output to be < n. |
r.oneOf(n)
每次调用 r.oneOf(n)
是重新随机执行,返回一个 [0, n)
中的整数是否是 0
,即 以 1/n
的概率返回 true
。link .
1 | // 显然这个就是以 1/n 的概率返回 true , n 越大 概率越小 |
r.nOutOf(n, oufOf)
每次调用 r.nOutOf(n, outOf)
是重新执行,以 n/outOf
的概率返回 true
。 link .
1 | // r.nOutOf(n, outOf) 以 n/outOf 的概率返回 true |
r. biasedRand(n, k)
每次调用 biasedRand(n, k)
返回一个 [1,n)
中的值,但是返回 n-1
的概率是 0
的 k
倍。
1 | // biasedRand returns a random int in range [0..n), |
Sort.Search(int, func)
该函数 Search uses binary search to find and return the smallest index i in [0, n) at which f(i) is true,
1 | func Search(n int, f func(int) bool) int |
switch
实测 go
的 switch
会自动 break
。
1 | package main |
运行之后会只打印 1
而不会有后面的值。
1 | $ go run test.go |
0x03 mutation 规则
对 Syzkaller 的具体变异规则的简单分析。
变异模式
其实就是以一定的概率选择下面的变异模式:
squashAny
随机选择
p
中一个复杂指针并将其参数压缩为ANY
。随后,如果ANY
包含blob
,则变异一个随机blob
。我感觉就是挑一个复杂的指针变异其中的某一个参数即可。
- 如果
p
中维护的复杂指针数组 (complexPtrs
) 是空的,直接退出,变异失败。 - 否则随机在其中选择一个成为
ptr
,如果不幸选中的ptr
所属的call
不可变异,则直接退出,变异失败。 - 如果当前
ptr.arg
没有任何指针,则主动调用squashPtr
将其压缩成指针。 - 然后对
ptr
中的数据参数和相应基址分别压缩到两个数组中。如果发现最后blob
数组为空,直接退出,变异失败。 - 然后需要在对
blob
进行变异前进行分析,不然可能因为这个变异引入越界,导致分析过程越界报错。 - 然后对
blob
中随机选择一个参数来进行数据变异,最后返回变异成功。
- 如果
splice
如果当前种子库为空、或当前
p
一个call
都没有、或当前p
拥有足够数量的call
,则直接退出,变异失败。从当前种子库中随机选择一个
p0
,然后在当前p
中随机选一个位置idx
。将
p0
插入到p
中idx
前面,组合成一个超级大的p1
,p1=p[:idx]+p0+p[idx:]
。然后将
p1
中的call
从末尾开始一个一个进行移除,直到p1
中的call
的数量刚好够ncall
。
感觉有可以优化的空间,如果太长了可以直接计算填满所缺的长度,然后切片就好了啊,为啥要无脑拼接,然后一一移除。
insertCall
如果程序已经有不少于
ncall
个call
了,则直接退出,变异失败。在现有的
p
中随机选择一个位置(偏向于现有p
的末尾,末尾的概率是开头的5
倍),在其前方插入一个新生成的call
(通过调用generateCall
来生成)。若插入之后
p
中call
的数量超过了ncall
,则会主动调用RemoveCall
移除刚才选中的那个call
。(感觉怎么都不会走到这一步呀,前面明明只有小于时才会插入一个call
那最多也是等于,怎会大于呢?
mutateArg
随机选择一个
call
的参数进行变异。- 如果当前
p
一个参数都没有,直接退出,变异失败。 - 基于
call
的参数的复杂性来选择一个call
,这里似乎没有随机,总会选到当前p
中参数最复杂的call
,不同类型的参数会有一个计算规则,总之是可以计算得到一个浮点数的,然后最后加起来排序即可。如果所有的call
都没有参数,那么直接退出,变异失败。 - 如果不幸选中的这个
call
是不能进行变异的,直接退出,变异失败。 - 然后开启一个循环,退出条件 (依据类型进行参数变异成功 and 1/3 的概率)
- 调用 analyze 根据选择表、种子库、
p
、选中的call
进行分析得到状态s
。 - 然后调用
(*Target)mutateArg
真实进行参数变异,根据参数的类型选择实现设计好的变异策略。然后还会维护好上下文中的基地址不能变。 (*Target)mutateArg
变异得到一串calls
。- 然后将变异得到的
calls
插入到前面选中的call
前面。如果插入后过长了(大于ncall
),就开始对一个一个去移除p
中的call
直到等于ncall
。 - 然后判断 一些不应该出现的异常情况后,返回变异成功。
- 如果当前
removeCall
- 如果当前
p
是空的,则直接退出,变异失败。 - 否则从
p
中随机选择一个idx
,然后主动调用RemoveCall
移除。
- 如果当前
具体变异过程
在源码 /prog/mutation.go#L26 可以找到和变异相关的函数。
当调用函数 Mutate
后会传入相关的一些必要的参数对当前的prog进行变异,该部分描述的是对一个prog进行的最外层的粗粒度的5种变异手法,这5种变异中除了mutateArg
和 squashAny
的选择有一定的设计外,其余的都是简单的随机法则 ,只保证不会少于0个,不会多于 ncall个和不会出现黑名单中的syscall。
1 | // Mutate program p. |
mutateArg 规则
当选择 mutateArg
模式时,会随机变异一个系统调用的参数。具体实施时:
- 如果当前 prog 长度为 0 ,直接返回变异失败。
- 根据当前 prog 中的系统调用的参数的复杂性计算一个优先级,然后基于优先级随机选择一个系统调用,此时选择不会忽略特殊系统调用。某个参数的优先级计算规则是根据 参数变异优先级 进行。
- 某一个系统调用的某一个参数的优先数据与其类型相关,有一个类型优先级计算规则。
- 某一个系统调用的优先级理论上等于其所有参数的优先级之和。但是会在扫描到其含有某些特殊类型的参数的某些条件时提前终止对后续参数的扫描,从而提前得到当前系统调用的优先级。
- 当扫描完,得知所有的系统调用都没有参数时,返回 -1,使得上层变异返回变异失败。
- 否则基于 prog 中每个系统调用的优先级随机返回一个系统调用下标。
- 然后开始迭代变异,退出条件为变异成功并且1/3的概率。
- 每次迭代重新收集选中的系统调用 c 的参数信息。
- 然后对当前 prog 中的状态信息进行分析,但是会忽略c及其之后的系统调用导致的状态中的 resources 的改变。
- 基于 c 的参数的优先级随机选择一个参数进行参数变异。参数变异则是根据 变异规则按类型 的变异规则进行 。
- 如果变异失败则回到迭代开始处。
- 否则变异成功则将变异新生成的 calls 插入到原 prog 中 c 之前的位置,并将插入后超过最大长度限制的 syscall 从末尾处移除。
- 验证移除之后的prog中长度必须有效,且 idx 对应的位置还应该是 c,不然就 panic。(如果变异生成的 calls 实际上只有一个 call 的话,只有可能被选中的 c 在原 prog 中位于最后并且 prog 长度达到了最长,才会导致原有的 c 被移除,然后才会报错这个才对。
squashAny 规则
// Picks a random complex pointer and squashes its arguments into an ANY.
// Subsequently, if the ANY contains blobs, mutates a random blob.
当选择 squashAny
模式时,会随机选择一个复杂指针,然后将其参数压缩成 ANY,随后,如果 ANY 包含了多个 blob,则随机选择一个 blob 来进行变异。
复杂指针来源于定义在 prog/any.go
中的 prog.complexPtrs()
该函数会遍历当前 prog
的每一个系统调用的每一个参数,然后依据一定的规则判定某个参数是否是复杂指针。
例如下面的这些就不是复杂指针:
- 参数返回值为空。
- 参数方向不是 in。
- 指针类型。
下面的类型是复杂指针:
- 参数本身就是一个指针类型并且指向任何数组。
- union类型并且field大于5。
- 有长度的结构体类型。
参数变异优先级
在 Syzkaller(f325deb0) 版本中,mutation.go 中的函数 getMutationPrio
对变异优先级有如下的注释。
// TODO: find a way to estimate optimal priority values.
// Assign a priority for each type. The boolean is the reference type and it has
// the minimum priority, since it has only two possible values.
首先定义了三个全局变量,用于描述优先级的范围
1 | const ( |
整型,不会停止迭代
- 对于没有数值范围的或者范围不超过 256 的整型而言,其底层数据类型的位数越多优先级越高(最多64位)。
bitSize+0.1*maxPriority
。 - 对于有范围的,则根据范围大小而定优先级。
- 范围为 0 优先级就是 0, 范围是 1 优先级就是
minPriority
。 - 范围不超过 15 的,假设认为是与 FlagsType 相当的,因为大多数 syscall 的可能的 flag 是小于15的范围的。所以将会去尝试所有的值。具体做法,优先级与范围大小成比例。在阈值之后,优先级是恒定的。
min(size/3, 0.9)*maxPriority
。 - 范围不超过 256 的,才会被认定有范围,因为这个是一个字节最多能表示的范围。优先级恒定
maxPriority
。
- 范围为 0 优先级就是 0, 范围是 1 优先级就是
- 对于没有数值范围的或者范围不超过 256 的整型而言,其底层数据类型的位数越多优先级越高(最多64位)。
结构体类型
- 如果这个结构体是应该被忽略的或者不特殊,则返回不变异
dontMutate
,并不停止迭代。 - 否则则返回最高优先级,且停止迭代。
- 如果这个结构体是应该被忽略的或者不特殊,则返回不变异
枚举类型(
UnionType
)- 如果这个类型应该被忽略或者(不特殊并且只有一个 field) 则返回不变异
dontMutate
,并不停止迭代。 - 否则如果只是不特殊但是又多个field,则希望变异枚举类型本身和当前选项的值。返回最高优先级,并不停止迭代。
- 否则就是特殊的,那就直接返回最高优先级,同时停止迭代。
- 如果这个类型应该被忽略或者(不特殊并且只有一个 field) 则返回不变异
flag类型(FlagsType),不会停止迭代
与前面小范围整型一样,根据flag的大小正相关优先级。返回
min(size/3, 0.9)*maxPriority
,但是如果有BitMask
则会再加一个0.1*maxPriority
。指针类型(PtrType),不会停止迭代
- 如果是特殊的指针,则返回不变异。(TODO,认为应该进行变异,但是还没有相应的代码)
- 否则返回
0.3*maxPriority
。
常量类型(ConstType),返回不变异,不会停止迭代。
CsumType,自定义类型?
- 返回不变异,不会停止迭代。
ProcType?
- 返回
0.5*maxPriority
,不会停止迭代。
- 返回
资源类型(ResourceType)
- 返回
0.5*maxPriority
,不会停止迭代。
- 返回
虚拟页?(VmaType)
- 返回
0.5*maxPriority
,不会停止迭代。
- 返回
长度类型(LenType)
- 返回
0.5*maxPriority
,不会停止迭代,(因为根据描述,变异这个类型只会导致不正确的结果。
- 返回
缓冲区类型(BufferType)
- 如果缓冲区方向是出(剩下的缓冲期方向还有入和出入两种 in&inout)或者变量没长度? 返回不变异,不会停止迭代。
- 如果是字符串型缓冲区并且值长度为1,则通常是常量,尤其是文件名。返回不变异,不会停止迭代。
- 如果是压缩的缓冲区(例如压缩的镜像),则应该优先变异,直接返回最高优先级,不会停止迭代。
- 否则返回
0.8*maxPriority
,不会停止迭代。
数组类型(ArrayType)
- 如果开始范围等于结束范围并且它的种类是数组范围长度,则返回不变异,不会停止迭代。
- 否则返回最大优先级,不会停止迭代。
变异规则按类型
mutateInt
对需要变异的整数进行小范围的变化,增加或者减少 4 以内,或者随机翻转一位。
1 | func mutateInt(r *randGen, a *ConstArg, t *IntType) uint64 { |
mutateAlignedInt
对需要进行对齐的整型进行变异,主要就是在允许的范围内,变异这个数值对齐的位置。即想像成分页数据,则变异的是页码,而对应的页内位置不变。
1 | func mutateAlignedInt(r *randGen, a *ConstArg, t *IntType) uint64 { |
(t *IntType) mutate
真正对整型数据进行变异。一半的概率直接重新生成,一半的概率根据是否需要对齐进行变异。
1 | func (t *IntType) mutate(r *randGen, s *state, arg Arg, ctx ArgCtx) (calls []*Call, retry, preserve bool) { |
(t *FlagsType) mutate
略微有点复杂,但是源代码中写了注释,还是简单解释了一下为啥是这样一些概率和设定。
1 | func (t *FlagsType) mutate(r *randGen, s *state, arg Arg, ctx ArgCtx) (calls []*Call, retry, preserve bool) { |
(t *LenType) mutate
(t ResourceType/\VmaType/*ProcType) mutate
直接重新生成
(t *BufferType) mutate
数据变异函数
在 mutation.go 中的函数数组 mutateDataFuncs
定义了一系列数据变异的函数。这些变异函数统一在一个变异数组 mutateDataFuncs
中。
这个数组通过函数 mutateData
进行变异调度,以一个循环每次随机选择一个变异函数,然后当变异成功并且1/3的概率会停止迭代变异。
这个函数只在两个地方被调用了:
(t *BufferType) mutate
的类型为BufferBlobRand, BufferBlobRange
和BufferString
时;squashAny
中。
1 | func mutateData(r *randGen, data []byte, minLen, maxLen uint64) []byte { |
具体变异措施包括下面的函数,这些函数一般都会变异成功,毕竟只是对 bytes 进行操作,除非数据越界超过了预定义的最大或者最小范围:
1 | //Flip bit in byte. |
syz-fuzzer
工作流程大概是这个样子
在 syzkaller/syz-fuzzer
文件夹下面是关于在被 fuzz
机器中执行组件的代码,这个部分是具体负责:
syz-fuzzer
进程则运行在大概率会不稳定的虚拟机上(被测试的内核上),该进程会对模糊测试进程的输入生成(generation)、变异(mutation)和最小化(minimization)等进行引导。同时会将触发了新的覆盖率的输入数据通过 RPC 返回给syz-manager
,它还会临时启动并将需要运行的输入(program)发送给syz-executor
进程执行。
在这个代码中,一个 proc
实际是一个在虚拟机中跑的实例,负责自己的一系列 fuzz
事情。
// Proc represents a single fuzzing process (executor).
newProc
就是生成一个新的 fuzz
进程,其中会配置一些相关的参数和变量,然后返回一个 proc
实例。
1 | func newProc(fuzzer *Fuzzer, pid int) (*Proc, error) |
loop
就是一个 fuzz
实例的具体主循环部分了,这是一个死循环,一直进行持续的 fuzz
直到有退出信号。正常情况下,生成和变异的比例大约是 1:99
。prog.RecommendedCalls
是用于指示生成和变异的 prog
中的 ncall
即指示系统调用数量。定义在 syz-fuzzer/proc.go:line 62 。
对该部分代码进行再次仔细的分析,会发现当 fuzzer 的工作队列不为空时将不会执行下面的生成和变异,即并不会去通过fuzzer快照生成一个新的fuzzer,整个循环到这里就结束了。
1 | // proc.go |
对于 fuzzerSnapshot.chooseProgram
定义在 syz-fuzzer/fuzzer.go:line 517 。会根据当前 fuzzer
中的一个参数值 sumPrios
(猜测这个参数与当前的种子数有关),获取一个小于这个参数的随机数 randVal
,然后顺序查找种子库中的第一个优先级大于等于 randVal
种子进行返回。
1 | func (fuzzer *FuzzerSnapshot) chooseProgram(r *rand.Rand) *prog.Prog { |
对于 proc.fuzzer.target.Generate
定义在 prog/generation.go:line 12 。
1 | // Generate generates a random program with ncalls calls. |
而对于其中的 r.generateCall
定义在 prog/rand.go:line 561 。
1 | func (r *randGen) generateCall(s *state, p *Prog, insertionPoint int) []*Call { |
对于 executeAndCollide
定义在 syz-fuzzer/proc.go:line 283 。到这里我就没有继续跟了,有空再来跟。
1 | func (proc *Proc) executeAndCollide(execOpts *ipc.ExecOpts, p *prog.Prog, flags ProgTypes, stat Stat) { |
调用 proc.executeRaw()
后,反正是进入了 ipc.Env.Exec
来进行执行的。
ipc
该部分组件定义在 ipc/ ,其中 ipc.Env.Exec
定义在 ip c.go:line 255 。
1 | // Exec starts executor binary to execute program p and returns information about the execution: |
覆盖收集
triageInput
定义在 syz-fuzzer/proc.go:line 102 ,在这里测算覆盖率,进行重执行,进行最小化操作,将输入添加到种子库中,发送该种子信息给 syz-manager
。
实际测算覆盖率时,其实是每个输入覆盖的PC的数量。
1 | func (proc *Proc) triageInput(item *WorkTriage) { |
0x04 cov web 的查看
Syzkaller 专门提供了web页面的覆盖率查看功能。具体实现大致在 /syzkaller/pkg/cover
中。web 页面的实现主要看文件 /syzkaller/pkg/cover/html.go
。
DoHTML
1 | // /syzkaller/pkg/cover/html.go line:27 |
processDir
1 | // /syzkaller/pkg/cover/html.go line:740 |
实例分析
覆盖率分析
Syzkaller 提供了web接口进行查看,同时也提供了命令行工具进行查看。(docs/coverage.md)
这里对web-interface进行解释。
首先是左侧边栏中的数据。
- 左侧表示内核中的文件夹和文件。
- 中间有两个百分数表示当前对这个文件/文件夹的覆盖率。
- 括号外面的是:当前覆盖到的PC数量除以整个文件或者文件夹中的所有的PC数得到的百分比,也就是绝对覆盖率。
- 括号里面是:当前覆盖到的PC数量除以当前文件或者文件夹中所有有被覆盖到的函数的总PC数量得到的百分比。反映的就是到达的函数中,这些函数有多大的比例已经被完全覆盖了。
- 右侧两个数表示当前文件/文件夹的PC总数量。
- 括号外面是这个文件/文件夹中的所有的PC的总数量。
- 括号里面是,当前有到达过的函数的PC的总数量。所以随着fuzz过程继续,这个值应该会持续变大。
然后是右侧内容框中的数据。
- 左侧表示函数的名字。
- 中间表示这个函数覆盖的PC数除以该函数的PC总数,即该函数的覆盖率。
- 右侧这个数字表示这个函数所拥有的PC总数。
- 最下面的
SUMMARY
则是表示一个平均,即当前这个文件中所有的到达的PC数除以当前文件中有被覆盖到的函数的PC总数的和得到的覆盖率。(即上一个括号中的比例。 - 最下面右侧统计的则是,当前文件中所有的有到达的函数拥有的PC总数。
源码分析与查看
而当点击文件名时,会出来这个文件对应的源码。web-interface 有解释。
全黑的行表示这一行对应的所有的PC值都被完全覆盖了。左侧会有一个数字,表明有多少个prog已触发执行与该行关联的 PC 值。点击该数字会展示最后一个执行的prog是啥。
橙色则是表示这一行相关的PC值没有完全被执行覆盖。左侧的数字含义相同。
赤红色表示 weak-uncovered,意思是这一行所示的函数或者符合根本不可能到达。但可能会因为变异优化符号表缺失或者函数修改内嵌等方式有误。
红色表示理论上可以到但是没有被覆盖到的单行?
Line is uncovered. Function (symbol) this line is in is executed and one of the PC values associated to this line. Example below shows how single line which is not covered is shown.
灰色表示没有被插桩的。PC关联过去的行根本没有被插桩或者该源码行根本没有生成任何code。
0x05 执行 prog
在Syzkaller报告了某个 bug
之后,可以自己再执行该输入。
教程文档在(docs/executing_syzkaller_programs.md)
关于执行和判别触发 crash
的输入文件 (docs/eproducing_crashes.md)将很有用。
基本思路是,用qemu将目标内核拖起来,然后将必要的文件发送到目标系统中,然后执行。
首先将目标系统拖起来,这里和编译Syzkaller时一样,直接qemu拖起来即可。
然后使用命令将必要的文件发送进去。
-P 10021
是拖起来的待测试系统的端口-i
指定的是和目标系统ssh
通信的认证文件,一般是bullseye.id_rsa
syz-execprog
就是读取 prog 文件,然后调用 executor 进行执行的程序。syz-executor
就是在目标系统中解析我们给定的prog并进行执行的可执行文件program
就是给出的想执行的 prog,疑似就是repro.prog
中的数据即可。1
scp -P 10021 -i bullseye.img.key bin/linux_amd64/syz-execprog bin/linux_amd64/syz-executor program root@localhost:
执行命令可能会报错:
Offending ECDSA key in /home/th1nk5t4ti0n/.ssh/known_hosts:18
根据错误下面的提示,会让执行一个删除命令,根据这个删除命令,把文件中冲突的已知主机的key删除即可。然后在目标系统中执行下面的命令即可。
其中一些参数可以根据repro.prog
中的值进行设定。1
./syz-execprog -repeat=0 -procs=8 program
eproducing_crashes.md 还讲述了怎么从多个里面找到某个、尝试最小化和使用prog2c工具。
0x06 Crash
link .
Once syzkaller detected a kernel crash in one of the VMs, it will automatically start the process of reproducing this crash (unless you specified
"reproduce": false
in the config). By default it will use 4 VMs to reproduce the crash and then minimize the program that caused it. This may stop the fuzzing, since all of the VMs might be busy reproducing detected crashes.
0x07 实际编译测试
Linux-6.22
1 | sudo make defconfig |
0x08 一些过程命令脚本
syz_install.sh
我在实验过程中的一些实用性脚本
1 | # go |
0x09 参考链接
主要参考自 Syzkaller 官方仓库,一些即时的参考链接已经在文中给出。