afl-gcc小叙
核心函数
find_as
这个函数用来寻找afl-as
的位置。
- 它首先检查是否存在AFL_PATH这个环境变量,如果存在就赋值给afl_path,然后检查
afl_path/as
这个文件是否可以访问,如果可以访问,就将afl_path设置为as_path。 - 如果不存在AFL_PATH这个环境变量,则检查argv0,例如(”/Users/sakura/gitsource/AFL/cmake-build-debug/afl-gcc”)中是否存在’/‘,如果有就找到最后一个’/‘所在的位置,并取其前面的字符串作为dir,然后检查
dir/afl-as
这个文件是否可以访问,如果可以访问,就将dir设置为as_path - 如果上述两种方式都失败,则抛出异常。
edit_params
这个函数主要是将argv拷贝到u8 **cc_params
中,并做必要的编辑。
- 它首先通过ck_alloc来为cc_params分配内存,分配的长度为
(argc+128)*8
,相当大的内存了。 - 然后检查argv[0]里有没有’/‘,如果没有就赋值’argv[0]’到name,如果有就找到最后一个’/‘所在的位置,然后跳过这个’/‘,将后面的字符串赋值给name。
- 将name和
afl-clang
比较- 如果相同,则设置clang_mode为1,然后设置环境变量CLANG_ENV_VAR为1。
- 然后将name和
afl-clang++
比较- 如果相同,则获取环境变量
AFL_CXX
的值,如果该值存在,则将cc_params[0]设置为该值,如果不存在,就设置为clang++ - 如果不相同,则获取环境变量
AFL_CC
的值,如果该值存在,则将cc_params[0]设置为该值,如果不存在,就设置为clang
- 如果相同,则获取环境变量
- 然后将name和
- 如果不相同,则将name和
afl-g++
比较- 如果相同,则获取环境变量
AFL_CXX
的值,如果该值存在,则将cc_params[0]设置为该值,如果不存在,就设置为g++ - 如果不相同,则获取环境变量
AFL_CC
的值,如果该值存在,则将cc_params[0]设置为该值,如果不存在,就设置为gcc
- 如果相同,则获取环境变量
- 如果相同,则设置clang_mode为1,然后设置环境变量CLANG_ENV_VAR为1。
- 然后遍历从argv[1]开始的argv参数
- 跳过
-B/integrated-as/-pipe
- 如果存在
-fsanitize=address
或者-fsanitize=memory
,就设置asan_set为1; - 如果存在
FORTIFY_SOURCE
,则设置fortify_set为1 cc_params[cc_par_cnt++] = cur
;
- 跳过
- 然后开始设置其他的cc_params参数
- 取之前计算出来的
as_path
,然后设置-B as_path
- 如果是clang_mode,则设置
-no-integrated-as
- 如果存在AFL_HARDEN环境变量,则设置
-fstack-protector-all
- sanitizer
- 如果asan_set在上面被设置为1,则使
AFL_USE_ASAN
环境变量为1 - 如果存在AFL_USE_ASAN环境变量,则设置
-fsanitize=address
- 如果存在AFL_USE_MSAN环境变量,则设置
-fsanitize=memory
,但不能同时还指定AFL_HARDEN
或者AFL_USE_ASAN
,因为这样运行时速度过慢。
- 如果asan_set在上面被设置为1,则使
- 如果不存在AFL_DONT_OPTIMIZE环境变量,则设置
-g -O3 -funroll-loops -D__AFL_COMPILER=1 -DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION=1
- 如果存在AFL_NO_BUILTIN环境变量,则设置
-fno-builtin-strcmp
等
- 取之前计算出来的
- 最后
cc_params[cc_par_cnt] = NULL;
终止对cc_params的编辑
main函数
实际上看到这里,我们就知道afl-gcc就是找到as所在的位置,将其加入搜索路径,然后设置必要的gcc参数和一些宏,然后调用gcc进行实际的编译,仅仅只是一层wrapper
1 | /* Main entry point */ |
输出如下
1 | sakura@sakuradeMacBook-Pro:~/gitsource/AFL/cmake-build-debug$ ./afl-gcc ../test-instr.c -o test |
afl-as小叙
核心函数
edit_params
检查并修改参数以传递给as
。请注意,文件名始终是GCC传递的最后一个参数,因此我们利用这个特性使代码保持简单。
主要是设置变量as_params的值,以及use_64bit/modified_file的值。
- 首先为as_params分配空间,大小为
(argc+32)*8
u8 *tmp_dir
- 依次检查是否存在TMPDIR/TEMP/TMP环境变量,如果存在就设置,如果都不存在就设置tmp_dir为”/tmp”
u8 *afl_as
- 读取AFL_AS环境变量,如果存在就设置为afl_as的值
- 因为apple的一些原因,所以如果我们定义了
__APPLE__
宏,且当前是在clang_mode且没有设置AFL_AS环境变量,就设置use_clang_as为1,并设置afl_as为AFL_CC/AFL_CXX/clang中的一种。
- 如果afl_as不为空,就设置
as_params[0]
为afl_as
,否则设置为as
- 设置
as_params[argc]
为0,as_par_cnt初始值为1。 - 然后遍历从argv[1]开始,到
argv[argc-1]
(也就是最后一个参数)之前的argv参数- 如果存在
--64
,设置use_64bit为1,如果存在--32
,设置use_64bit为0;如果是apple,则如果存在-arch x86_64
,设置use_64bit为1,并跳过-q
和-Q
选项 as_params[as_par_cnt++] = argv[i]
;设置as_params的值为argv对应的参数值
- 如果存在
- 然后开始设置其他的as_params参数
- 如果use_clang_as为1,则设置
-c -x assembler
选项 - 读取
argv[argc - 1]
的值,赋给input_file的值,也就是传递的最后一个参数的值作为input_file - 比较input_file和tmp_dir/
/var/tmp/
//tmp/
的前strlen(tmp_dir)/9/5个字节是否相同,如果不相同,就设置pass_thru为1 - 设置modified_file的值为
alloc_printf("%s/.afl-%u-%u.s", tmp_dir, getpid(),(u32) time(NULL));
,简单的说就是tmp_dir/.afl-pid-time.s
这样的字符串。 - 设置
as_params[as_par_cnt++] = modified_file
as_params[as_par_cnt] = NULL;
- 如果use_clang_as为1,则设置
add_instrumentation
处理输入文件,生成modified_file,将instrumentation插入所有适当的位置。
- 如果input_file不为空,则尝试打开这个文件,如果打开失败就抛出异常,如果为空,则读取标准输入,最终获取FILE* 指针inf
- 然后打开modified_file对应的临时文件,并获取其句柄outfd,再根据句柄通过fdopen函数拿到FILE*指针outf
- 通过fgets从inf中逐行读取内容保存到line数组里,每行最多读取的字节数是MAX_LINE(8192),这个值包括’\0’,所以实际读取的有内容的字节数是MAX_LINE-1个字节。从line数组里将读取的内容写入到outf对应的文件里。
接下来是真正有趣的部分,首先我们要确定的是,我们只在.text部分进行插桩,但因为这部分涉及到多平台以及优化后的汇编文件格式,这里我只会描述最核心的逻辑
核心逻辑如下,我抽取了最重要的代码出来。
1 | ^func: - function entry point (always instrumented) |
1 | while (fgets(line, MAX_LINE, inf)) { |
检查
instr_ok && instrument_next && line[0] == '\t' && isalpha(line[1])
即判断instrument_next和instr_ok是否都为1,以及line是否以\t
开始,且line[1]
是否是字母- 如果都满足,则设置
instrument_next = 0
,并向outf中写入trampoline_fmt
,并将插桩计数器ins_lines
加一。 - 这其实是因为我们想要插入instrumentation trampoline到所有的标签,宏,注释之后。
- 如果都满足,则设置
首先要设置instr_ok的值,这个值其实是一个flag,只有这个值被设置为1,才代表我们在
.text
部分,否则就不在。于是如果instr_ok为1,就会在分支处执行插桩逻辑,否则就不插桩。- 如果line的值为
\t.[text\n|section\t.text|section\t__TEXT,__text|section __TEXT,__text]...
其中之一,则设置instr_ok为1,然后跳转到while循环首部,去读取下一行的数据到line数组里。 - 如果不是上面的几种情况,且line的值为
\t.[section\t|section |bss\n|data\n]...
,则设置instr_ok为0,并跳转到while循环首部,去读取下一行的数据到line数组里。
- 如果line的值为
插桩
^\tjnz foo
条件跳转指令- 如果line的值为
\tj[!m]...
,且R(100) < inst_ratio
,R(100)会返回一个100以内的随机数,inst_ratio是我们之前设置的插桩密度,默认为100,如果设置了asan之类的就会默认设置成30左右。 fprintf(outf, use_64bit ? trampoline_fmt_64 : trampoline_fmt_32, R(MAP_SIZE));
根据use_64bit来判断向outfd里写入trampoline_fmt_64还是trampoline_fmt_32。define R(x) (random() % (x))
,可以看到R(x)是创建的随机数除以x取余,所以可能产生碰撞- 这里的R(x)实际上是用来区分每个桩的,也就是是一个标识。后文会再说明。
- 将插桩计数器
ins_lines
加一。
- 如果line的值为
首先检查该行中是否存在
:
,然后检查是否以.开始
- 如果以
.
开始,则代表想要插桩^.L0:
或者^.LBB0_0:
这样的branch label,即style jump destination- 然后检查
line[2]
是否为数字 或者 如果是在clang_mode下,比较从line[1]开始的三个字节是否为LBB. 前述所得结果和R(100) < inst_ratio)
相与。- 如果结果为真,则设置
instrument_next = 1
- 如果结果为真,则设置
- 然后检查
- 否则代表这是一个function,插桩
^func:
function entry point- 直接设置
instrument_next = 1
- 直接设置
- 如果以
如果插桩计数器ins_lines不为0,就在完全拷贝input_file之后,依据架构,像outf中写入main_payload_64或者main_payload_32,然后关闭这两个文件
至此我们可以看出afl的插桩相当简单粗暴,就是通过汇编的前导命令来判断这是否是一个分支或者函数,然后插入instrumentation trampoline。
关于instrumentation trampoline,后文叙述
main函数
最后我们回来看一下main函数
- 读取环境变量AFL_INST_RATIO的值,设置为inst_ratio_str
- 设置srandom的随机种子为
rand_seed = tv.tv_sec ^ tv.tv_usec ^ getpid();
- 设置环境变量AS_LOOP_ENV_VAR的值为1
- 读取环境变量AFL_USE_ASAN和AFL_USE_MSAN的值,如果其中有一个为1,则设置sanitizer为1,且将inst_ratio除3。
- 这是因为AFL无法在插桩的时候识别出ASAN specific branches,所以会插入很多无意义的桩,为了降低这种概率,粗暴的将整个插桩的概率都除以3
- edit_params(argc, argv)
- add_instrumentation()
- fork出一个子进程,让子进程来执行
execvp(as_params[0], (char **) as_params);
- 这其实是因为我们的execvp执行的时候,会用
as_params[0]
来完全替换掉当前进程空间中的程序,如果不通过子进程来执行实际的as,那么后续就无法在执行完实际的as之后,还能unlink掉modified_file - exec系列函数
- fork出的子进程和父进程
- 这其实是因为我们的execvp执行的时候,会用
waitpid(pid, &status, 0)
等待子进程结束- 读取环境变量AFL_KEEP_ASSEMBLY的值,如果没有设置这个环境变量,就unlink掉modified_file。
稍微打印一下参数
1 | for (int i = 0; i < sizeof(as_params); i++) { |
afl-fast-clang中叙
因为AFL对于上述通过afl-gcc
来插桩这种做法已经属于不建议,并提供了更好的工具afl-clang-fast,通过llvm pass来插桩。
clang wrapper
afl-clang-fast.c
这个文件其实是clang的一层wrapper,和之前的afl-gcc
一样,只是定义了一些宏,和传递了一些参数给真正的clang。
我们还是依次来看一下核心函数。
find_obj
- 获取环境变量
AFL_PATH
的值,如果存在,就去读取AFL_PATH/afl-llvm-rt.o
是否可以访问,如果可以就设置这个目录为obj_path
,然后直接返回 - 如果没有设置这个环境变量,就检查arg0中是否存在
/
,例如我们可能是通过/home/sakura/AFL/afl-clang-fast
去调用afl-clang-fast的,所以它此时就认为最后一个/
之前的/home/sakura/AFL
是AFL的根目录,然后读取其下的afl-llvm-rt.o
文件,看是否能够访问,如果可以就设置这个目录为obj_path
,然后直接返回。 - 最后如果上面两种都找不到,因为默认的AFL的MakeFile在编译的时候,会定义一个名为
AFL_PATH
的宏,其指向/usr/local/lib/afl
,会到这里找是否存在afl-llvm-rt.o
,如果存在设置obj_path
并直接返回。 - 如果上述三种方式都找不到,那么就会抛出异常
Unable to find 'afl-llvm-rt.o' or 'afl-llvm-pass.so'. Please set AFL_PATH
edit_params
- 首先根据我们执行的是
afl-clang-fast
还是afl-clang-fast++
来决定cc_params[0]
的值是clang++还是clang。- 如果执行的是
afl-clang-fast++
,读取环境变量AFL_CXX
,如果存在,就将其值设置为cc_params[0]
,如果不存在,就直接设置成clang++
- 如果执行的是
afl-clang-fast
,读取环境变量AFL_CC
,如果存在,就将其值设置为cc_params[0]
,如果不存在,就直接设置成clang
- 如果执行的是
- 默认情况下,我们通过
afl-llvm-pass.so
来注入instrumentation,但是现在也支持trace-pc-guard
模式,可以参考llvm的文档 - 然后如果定义了
USE_TRACE_PC
宏,就将-fsanitize-coverage=trace-pc-guard -mllvm -sanitizer-coverage-block-threshold=0
添加到参数里 - 如果没有定义,就依次将
-Xclang -load -Xclang obj_path/afl-llvm-pass.so -Qunused-arguments
- 依次读取我们传给
afl-clang-fast
的参数,并添加到cc_params里,不过这里会做一些检查和设置。- 如果传入参数里有
-m32
或者armv7a-linux-androideabi
,就设置bit_mode
为32 - 如果传入参数里有
-m64
,就设置bit_mode
为64 - 如果传入参数里有
-x
,就设置x_set
为1 - 如果传入参数里有
-fsanitize=address
或者-fsanitize=memory
,就设置asan_set为1 - 如果传入参数里有
-Wl,-z,defs
或者-Wl,--no-undefined
,就直接pass掉,不传给clang。
- 如果传入参数里有
- 读取环境变量
AFL_HARDEN
,如果存在,就在cc_params里添加-fstack-protector-all
- 如果参数里没有
-fsanitize=address/memory
,即asan_set是0,就读取环境变量AFL_USE_ASAN
,如果存在就添加-fsanitize=address
到cc_params里,环境变量AFL_USE_MSAN
同理 - 如果定义了
USE_TRACE_PC
宏,就检查是否存在环境变量AFL_INST_RATIO
,如果存在就抛出异常AFL_INST_RATIO not available at compile time with 'trace-pc'.
- 读取环境变量
AFL_DONT_OPTIMIZE
,如果不存在就添加-g -O3 -funroll-loops
到参数里 - 读取环境变量
AFL_NO_BUILTIN
,如果存在就添加-fno-builtin-strcmp
等。 - 添加参数
-D__AFL_HAVE_MANUAL_CONTROL=1 -D__AFL_COMPILER=1 -DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION=1
,定义一些宏 - 这里定义了如下两个宏
__AFL_LOOP
,__AFL_INIT()
,宏展开是类似这样的,为简化我去掉了和编译器优化相关的东西。
1 |
|
- 如果x_set为1,则添加参数
-x none
- 根据
bit_mode
的值选择afl-llvm-rt
- 如果为0,即没有
-m32
和-m64
选项,就向参数里添加obj_path/afl-llvm-rt.o
- 如果为32,添加
obj_path/afl-llvm-rt-32.o
- 如果为64,添加
obj_path/afl-llvm-rt-64.o
- 如果为0,即没有
main
- 寻找obj_path路径
- 编辑参数cc_params
- 替换进程空间,执行要调用的clang和为其传递参数
execvp(cc_params[0], (char**)cc_params);
afl-llvm-pass
关于llvm不懂的可以看CSCD70,顺便可以学一下优化,这里放一下我之前抽空做的笔记, 以及这篇文章可以列为查询和参考.
afl-llvm-pass里只有一个Transform pass AFLCoverage,其继承自ModulePass,所以我们主要分析一下它的runOnModule
函数,这里简单的介绍一下llvm里的一些层次关系,粗略理解就是Module相当于你的程序,里面包含所有Function和全局变量,而Function里包含所有BasicBlock和函数参数,BasicBlock里包含所有Instruction,Instruction包含Opcode和Operands。注册pass
这些都是向PassManager来注册新的pass,每个pass彼此独立,通过PM统一注册和调度,更加模块化。1
2
3
4
5
6
7
8
9
10
11
12static void registerAFLPass(const PassManagerBuilder &,
legacy::PassManagerBase &PM) {
PM.add(new AFLCoverage());
}
static RegisterStandardPasses RegisterAFLPass(
PassManagerBuilder::EP_ModuleOptimizerEarly, registerAFLPass);
static RegisterStandardPasses RegisterAFLPass0(
PassManagerBuilder::EP_EnabledOnOptLevel0, registerAFLPass);
具体的可以参考定义,我摘取了必要的代码和注释,请仔细阅读。
简单的理解就是当我创建了一个类RegisterStandardPasses之后,就会调用它的构造函数,然后调用PassManagerBuilder::addGlobalExtension
,这是一个静态函数,这个函数会创建一个tuple保存Ty和Fn还有一个id,并将其添加到一个静态全局vector里,以供PassManagerBuilder在需要的时候,将其添加到PM里。
而这个添加的时机就是ExtensionPointTy
来指定的。
1 | /// Registers a function for adding a standard set of passes. This should be |
runOnModule
- 通过getContext来获取LLVMContext,其保存了整个程序里分配的类型和常量信息。
- 通过这个Context来获取type实例Int8Ty和Int32Ty
- Type是所有type类的一个超类。每个Value都有一个Type,所以这经常被用于寻找指定类型的Value。Type不能直接实例化,只能通过其子类实例化。某些基本类型(VoidType、LabelType、FloatType和DoubleType)有隐藏的子类。之所以隐藏它们,是因为除了Type类提供的功能之外,它们没有提供任何有用的功能,除了将它们与Type的其他子类区分开来之外。所有其他类型都是DerivedType的子类。Types可以被命名,但这不是必需的。一个给定Type在任何时候都只存在一个实例。这允许使用Type实例的地址相等来执行type相等。也就是说,给定两个Type*值,如果指针相同,则types相同。
- 读取环境变量AFL_INST_RATIO给变量inst_ratio,其值默认为100,这个值代表一个插桩概率,本来应该每个分支都必定插桩,而这是一个随机的概率决定是否要在这个分支插桩。
- 获取全局变量中指向共享内存的指针,以及上一个基础块的编号
1
2
3
4
5
6
7GlobalVariable *AFLMapPtr =
new GlobalVariable(M, PointerType::get(Int8Ty, 0), false,
GlobalValue::ExternalLinkage, 0, "__afl_area_ptr");
GlobalVariable *AFLPrevLoc = new GlobalVariable(
M, Int32Ty, false, GlobalValue::ExternalLinkage, 0, "__afl_prev_loc",
0, GlobalVariable::GeneralDynamicTLSModel, 0, false); - 遍历每个基本块,找到此基本块中适合插入instrument的位置,后续通过初始化IRBuilder的一个实例进行插入。
1
2BasicBlock::iterator IP = BB.getFirstInsertionPt();
IRBuilder<> IRB(&(*IP)); - 随机创建一个当前基本块的编号,并通过插入load指令来获取前一个基本块的编号。
1
2
3
4
5unsigned int cur_loc = AFL_R(MAP_SIZE);
ConstantInt *CurLoc = ConstantInt::get(Int32Ty, cur_loc);
LoadInst *PrevLoc = IRB.CreateLoad(AFLPrevLoc);
PrevLoc->setMetadata(M.getMDKindID("nosanitize"), MDNode::get(C, None));
Value *PrevLocCasted = IRB.CreateZExt(PrevLoc, IRB.getInt32Ty()); - 通过插入load指令来获取共享内存的地址,并通过CreateGEP函数来获取共享内存里指定index的地址,这个index通过cur_loc和prev_loc取xor计算得到。
1
2
3
4LoadInst *MapPtr = IRB.CreateLoad(AFLMapPtr);
MapPtr->setMetadata(M.getMDKindID("nosanitize"), MDNode::get(C, None));
Value *MapPtrIdx =
IRB.CreateGEP(MapPtr, IRB.CreateXor(PrevLocCasted, CurLoc)); - 通过插入load指令来读取对应index地址的值,并通过插入add指令来将其加一,然后通过创建store指令将新值写入,更新共享内存。
1
2
3
4
5LoadInst *Counter = IRB.CreateLoad(MapPtrIdx);
Counter->setMetadata(M.getMDKindID("nosanitize"), MDNode::get(C, None));
Value *Incr = IRB.CreateAdd(Counter, ConstantInt::get(Int8Ty, 1));
IRB.CreateStore(Incr, MapPtrIdx)
->setMetadata(M.getMDKindID("nosanitize"), MDNode::get(C, None)); - 将当前cur_loc的值右移一位,然后通过插入store指令,更新
__afl_prev_loc
的值。1
2StoreInst *Store = IRB.CreateStore(ConstantInt::get(Int32Ty, cur_loc >> 1), AFLPrevLoc);
Store->setMetadata(M.getMDKindID("nosanitize"), MDNode::get(C, None)); - 总结
总的来说就是通过遍历每个基本块,向其中插入实现了如下伪代码功能的instruction ir来进行插桩。看一个例子1
2
3cur_location = <COMPILE_TIME_RANDOM>;
shared_mem[cur_location ^ prev_location]++;
prev_location = cur_location >> 1; - 源程序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int main(int argc, char** argv) {
char buf[8];
if (read(0, buf, 8) < 1) {
printf("Hum?\n");
exit(1);
}
if (buf[0] == '0')
printf("Looks like a zero to me!\n");
else
printf("A non-zero value? How quaint!\n");
exit(0);
} - 插桩前的ir
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; ModuleID = 'nopt_test-instr.ll'
source_filename = "test-instr.c"
target datalayout = "e-m:o-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-apple-macosx10.15.0"
@.str = private unnamed_addr constant [6 x i8] c"Hum?\0A\00", align 1
@.str.1 = private unnamed_addr constant [26 x i8] c"Looks like a zero to me!\0A\00", align 1
@.str.2 = private unnamed_addr constant [31 x i8] c"A non-zero value? How quaint!\0A\00", align 1
; Function Attrs: noinline nounwind ssp uwtable
define i32 @main(i32 %0, i8** %1) #0 {
%3 = alloca [8 x i8], align 1
%4 = getelementptr inbounds [8 x i8], [8 x i8]* %3, i64 0, i64 0
%5 = call i64 @"\01_read"(i32 0, i8* %4, i64 8)
%6 = icmp slt i64 %5, 1
br i1 %6, label %7, label %9
7: ; preds = %2
%8 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([6 x i8], [6 x i8]* @.str, i64 0, i64 0))
call void @exit(i32 1) #3
unreachable
9: ; preds = %2
%10 = getelementptr inbounds [8 x i8], [8 x i8]* %3, i64 0, i64 0
%11 = load i8, i8* %10, align 1
%12 = sext i8 %11 to i32
%13 = icmp eq i32 %12, 48
br i1 %13, label %14, label %16
14: ; preds = %9
%15 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([26 x i8], [26 x i8]* @.str.1, i64 0, i64 0))
br label %18
16: ; preds = %9
%17 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([31 x i8], [31 x i8]* @.str.2, i64 0, i64 0))
br label %18
18: ; preds = %16, %14
call void @exit(i32 0) #3
unreachable
}
declare i64 @"\01_read"(i32, i8*, i64) #1
declare i32 @printf(i8*, ...) #1
; Function Attrs: noreturn
declare void @exit(i32) #2
attributes #0 = { noinline nounwind ssp uwtable "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "frame-pointer"="all" "less-precise-fpmad"="false" "min-legal-vector-width"="0" "no-infs-fp-math"="false" "no-jump-tables"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="penryn" "target-features"="+cx16,+cx8,+fxsr,+mmx,+sahf,+sse,+sse2,+sse3,+sse4.1,+ssse3,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }
attributes #1 = { "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "frame-pointer"="all" "less-precise-fpmad"="false" "no-infs-fp-math"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="penryn" "target-features"="+cx16,+cx8,+fxsr,+mmx,+sahf,+sse,+sse2,+sse3,+sse4.1,+ssse3,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }
attributes #2 = { noreturn "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "frame-pointer"="all" "less-precise-fpmad"="false" "no-infs-fp-math"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="penryn" "target-features"="+cx16,+cx8,+fxsr,+mmx,+sahf,+sse,+sse2,+sse3,+sse4.1,+ssse3,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }
attributes #3 = { noreturn }
!llvm.module.flags = !{!0, !1}
!llvm.ident = !{!2}
!0 = !{i32 1, !"wchar_size", i32 4}
!1 = !{i32 7, !"PIC Level", i32 2}
!2 = !{!"clang version 10.0.0 "} - 插桩后的ir
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
107
108
109
110
111; ModuleID = 'm2r_nopt_test-instr.ll'
source_filename = "test-instr.c"
target datalayout = "e-m:o-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-apple-macosx10.15.0"
@.str = private unnamed_addr constant [6 x i8] c"Hum?\0A\00", align 1
@.str.1 = private unnamed_addr constant [26 x i8] c"Looks like a zero to me!\0A\00", align 1
@.str.2 = private unnamed_addr constant [31 x i8] c"A non-zero value? How quaint!\0A\00", align 1
@__afl_area_ptr = external global i8*
@__afl_prev_loc = external thread_local global i32
; Function Attrs: noinline nounwind ssp uwtable
define i32 @main(i32 %0, i8** %1) #0 {
%3 = load i32, i32* @__afl_prev_loc, !nosanitize !3
%4 = load i8*, i8** @__afl_area_ptr, !nosanitize !3
%5 = xor i32 %3, 17767
%6 = getelementptr i8, i8* %4, i32 %5
%7 = load i8, i8* %6, !nosanitize !3
%8 = add i8 %7, 1
store i8 %8, i8* %6, !nosanitize !3
store i32 8883, i32* @__afl_prev_loc, !nosanitize !3
%9 = alloca [8 x i8], align 1
%10 = getelementptr inbounds [8 x i8], [8 x i8]* %9, i64 0, i64 0
%11 = call i64 @"\01_read"(i32 0, i8* %10, i64 8)
%12 = icmp slt i64 %11, 1
br i1 %12, label %13, label %21
13: ; preds = %2
%14 = load i32, i32* @__afl_prev_loc, !nosanitize !3
%15 = load i8*, i8** @__afl_area_ptr, !nosanitize !3
%16 = xor i32 %14, 9158
%17 = getelementptr i8, i8* %15, i32 %16
%18 = load i8, i8* %17, !nosanitize !3
%19 = add i8 %18, 1
store i8 %19, i8* %17, !nosanitize !3
store i32 4579, i32* @__afl_prev_loc, !nosanitize !3
%20 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([6 x i8], [6 x i8]* @.str, i64 0, i64 0))
call void @exit(i32 1) #3
unreachable
21: ; preds = %2
%22 = load i32, i32* @__afl_prev_loc, !nosanitize !3
%23 = load i8*, i8** @__afl_area_ptr, !nosanitize !3
%24 = xor i32 %22, 39017
%25 = getelementptr i8, i8* %23, i32 %24
%26 = load i8, i8* %25, !nosanitize !3
%27 = add i8 %26, 1
store i8 %27, i8* %25, !nosanitize !3
store i32 19508, i32* @__afl_prev_loc, !nosanitize !3
%28 = getelementptr inbounds [8 x i8], [8 x i8]* %9, i64 0, i64 0
%29 = load i8, i8* %28, align 1
%30 = sext i8 %29 to i32
%31 = icmp eq i32 %30, 48
br i1 %31, label %32, label %40
32: ; preds = %21
%33 = load i32, i32* @__afl_prev_loc, !nosanitize !3
%34 = load i8*, i8** @__afl_area_ptr, !nosanitize !3
%35 = xor i32 %33, 18547
%36 = getelementptr i8, i8* %34, i32 %35
%37 = load i8, i8* %36, !nosanitize !3
%38 = add i8 %37, 1
store i8 %38, i8* %36, !nosanitize !3
store i32 9273, i32* @__afl_prev_loc, !nosanitize !3
%39 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([26 x i8], [26 x i8]* @.str.1, i64 0, i64 0))
br label %48
40: ; preds = %21
%41 = load i32, i32* @__afl_prev_loc, !nosanitize !3
%42 = load i8*, i8** @__afl_area_ptr, !nosanitize !3
%43 = xor i32 %41, 56401
%44 = getelementptr i8, i8* %42, i32 %43
%45 = load i8, i8* %44, !nosanitize !3
%46 = add i8 %45, 1
store i8 %46, i8* %44, !nosanitize !3
store i32 28200, i32* @__afl_prev_loc, !nosanitize !3
%47 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([31 x i8], [31 x i8]* @.str.2, i64 0, i64 0))
br label %48
48: ; preds = %40, %32
%49 = load i32, i32* @__afl_prev_loc, !nosanitize !3
%50 = load i8*, i8** @__afl_area_ptr, !nosanitize !3
%51 = xor i32 %49, 23807
%52 = getelementptr i8, i8* %50, i32 %51
%53 = load i8, i8* %52, !nosanitize !3
%54 = add i8 %53, 1
store i8 %54, i8* %52, !nosanitize !3
store i32 11903, i32* @__afl_prev_loc, !nosanitize !3
call void @exit(i32 0) #3
unreachable
}
declare i64 @"\01_read"(i32, i8*, i64) #1
declare i32 @printf(i8*, ...) #1
; Function Attrs: noreturn
declare void @exit(i32) #2
attributes #0 = { noinline nounwind ssp uwtable "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "frame-pointer"="all" "less-precise-fpmad"="false" "min-legal-vector-width"="0" "no-infs-fp-math"="false" "no-jump-tables"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="penryn" "target-features"="+cx16,+cx8,+fxsr,+mmx,+sahf,+sse,+sse2,+sse3,+sse4.1,+ssse3,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }
attributes #1 = { "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "frame-pointer"="all" "less-precise-fpmad"="false" "no-infs-fp-math"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="penryn" "target-features"="+cx16,+cx8,+fxsr,+mmx,+sahf,+sse,+sse2,+sse3,+sse4.1,+ssse3,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }
attributes #2 = { noreturn "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "frame-pointer"="all" "less-precise-fpmad"="false" "no-infs-fp-math"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="penryn" "target-features"="+cx16,+cx8,+fxsr,+mmx,+sahf,+sse,+sse2,+sse3,+sse4.1,+ssse3,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }
attributes #3 = { noreturn }
!llvm.module.flags = !{!0, !1}
!llvm.ident = !{!2}
!0 = !{i32 1, !"wchar_size", i32 4}
!1 = !{i32 7, !"PIC Level", i32 2}
!2 = !{!"clang version 10.0.0 "}
!3 = !{}
afl-llvm-rt
AFL LLVM_Mode中存在着三个特殊的功能。这三个功能的源码位于afl-llvm-rt.o.c中。
deferred instrumentation
AFL会尝试通过仅执行一次目标二进制文件来优化性能。它会暂停控制流,然后复制该“主”进程以持续提供fuzzer的目标。该功能在某些情况下可以减少操作系统、链接与libc内部执行程序的成本。
选好位置后,将下述代码添加到该位置上,之后使用afl-clang-fast重新编译代码即可
1 | #ifdef __AFL_HAVE_MANUAL_CONTROL |
__AFL_INIT()
内部调用__afl_manual_init
函数。该函数的源代码如下
1 | void __afl_manual_init(void) { |
如果还没有被初始化,就初始化共享内存,然后开始执行forkserver,然后设置init_done为1。
__afl_map_shm
就是简单的通过读取环境变量SHM_ENV_VAR
来获取共享内存,然后将地址赋值给__afl_area_ptr
。否则,默认的__afl_area_ptr
指向的是一个数组。
__afl_start_forkserver
的逻辑稍微复杂,分条叙述
- 首先设置
child_stopped
为0,然后通过FORKSRV_FD + 1
向状态管道写入4个字节,告知AFL fuzz已经准备好了。 - 然后进入fuzz loop循环
- 通过read从控制管道
FORKSRV_FD
读取4个字节,如果当前管道中没有内容,就会堵塞在这里,如果读到了,就代表AFL命令我们fork server去执行一次fuzz - 如果
child_stopped
为0,则直接fork出一个子进程去进行fuzz- 然后此时对于子进程就会关闭和控制管道和状态管道相关的fd,然后return跳出fuzz loop,恢复正常执行。
- 如果
child_stopped
为1,这是对于persistent mode的特殊处理,此时子进程还活着,只是被暂停了,所以可以通过kill(child_pid, SIGCONT)
来简单的重启,然后设置child_stopped
为0。 - 然后fork server向状态管道
FORKSRV_FD + 1
写入子进程的pid,然后等待子进程结束,注意这里对于persistent mode,我们会设置waitpid的第三个参数为WUNTRACED,代表若子进程进入暂停状态,则马上返回。 - WIFSTOPPED(status)宏确定返回值是否对应于一个暂停子进程,因为在persistent mode里子进程会通过SIGSTOP信号来暂停自己,并以此指示运行成功,所以在这种情况下,我们需要再进行一次fuzz,就只需要和上面一样,通过SIGCONT信号来唤醒子进程继续执行即可,不需要再进行一次fuzz。
- 设置
child_stopped
为1。
- 设置
- 当子进程结束以后,向状态管道
FORKSRV_FD + 1
写入4个字节,通知AFL这次target执行结束了。
- 通过read从控制管道
persistent mode
上面我们其实已经介绍过persistent mode的一些特点了,那就是它并不是通过fork出子进程去进行fuzz的,而是认为当前我们正在fuzz的API是无状态的,当API重置后,一个长期活跃的进程就可以被重复使用,这样可以消除重复执行fork函数以及OS相关所需要的开销。
所以的使用方法如下:
1 | while (__AFL_LOOP(1000)) { |
循环次数不能设置过大,因为较小的循环次数可以将内存泄漏和类似故障的影响降到最低。所以循环次数设置成1000是个不错的选择。
接下来我们来解读一下源码,首先介绍一个__attribute__ constructor
,demo如下,代表被此修饰的函数将在main执行之前自动运行
1 | __attribute__((constructor(1))) void before_main1(){ |
llvm mode里有一个函数__attribute__((constructor(CONST_PRIO))) void __afl_auto_init(void)
,其逻辑如下
- 读取环境变量PERSIST_ENV_VAR的值,设置给is_persistent
- 读取环境变量DEFER_ENV_VAR的值,如果为1,就直接返回,这代表
__afl_auto_init
和deferred instrumentation不通用,这其实道理也很简单,因为deferred instrumentation会自己选择合适的时机,手动init,不需要用这个函数来init,所以这个函数只在没有手动init的时候会自动init。 - 执行
__afl_manual_init
函数,其含义见上文。
宏定义__AFL_LOOP
内部调用__afl_persistent_loop
函数。__afl_persistent_loop(unsigned int max_cnt)
的逻辑如下
- 如果是第一次执行loop
- 如果is_persistent为1
- 清空
__afl_area_ptr
,设置__afl_area_ptr[0]
为1,__afl_prev_loc
为0
- 清空
- 设置cycle_cnt的值为传入的max_cnt参数,然后直接返回1
- 如果is_persistent为1
- 如果不是第一次执行loop
- 如果cycle_cnt减一(代表需要执行的循环次数减一)后大于0
- 发出信号
SIGSTOP
来让当前进程暂停 - 设置
__afl_area_ptr[0]
为1,__afl_prev_loc
为0,然后直接返回1
- 发出信号
- 如果cycle_cnt为0
- 设置
__afl_area_ptr
指向一个无关数组__afl_area_initial
。
- 设置
- 如果cycle_cnt减一(代表需要执行的循环次数减一)后大于0
我们将这些联系在一起,重新梳理一遍
假设我们是这么使用的:
1 | while (__AFL_LOOP(1000)) { |
- 首先在main函数之前读取共享内容,然后以当前进程为fork server,去和AFL fuzz通信。
- 当AFL fuzz通知进行一次fuzz,由于此时child_stopped为0,则fork server先fork出一个子进程。
- 这个子进程会很快执行到
__AFL_LOOP
包围的代码,因为是第一次执行loop,所以会先清空__afl_area_ptr
和设置__afl_prev_loc
为0,并向共享内存的第一个元素写一个值,然后设置循环次数1000,随后返回1,此时while(__AFL_LOOP)
满足条件,于是执行一次fuzzAPI。 - 然后因为是while循环,会再次进入
__AFL_LOOP
里,此时将循环次数减一,变成999,然后发出信号SIGSTOP
来让当前进程暂停,因为我们设置了WUNTRACED,所以waitpid函数就会返回,fork server将继续执行。 - fork server在收到
SIGSTOP
信号后就知道fuzzAPI已经被成功执行结束了,就设置child_stopped为1,并告知AFL fuzz - 然后当AFL fuzz通知再进行一次fuzz的时候,fork server将不再需要去fork出一个新的子进程去进行fuzz,只需要恢复之前的子进程继续执行,并设置child_stopped为0
- 因为我们是相当于重新执行一次程序,所以将
__afl_prev_loc
设置为0,并向共享内存的第一个元素写一个值,随后直接返回1,此时while(__AFL_LOOP)
满足条件,于是执行一次fuzzAPI,然后因为是while循环,会再次进入__AFL_LOOP
里,再次减少一次循环次数变成998,并发出信号暂停。 - 上述过程重复执行,直到第1000次执行时,先恢复执行,然后返回1,然后执行一次fuzzAPI,然后因为是while循环,会再次进入
__AFL_LOOP
里,再次减少一次循环次数变成0,此时循环次数cnt已经被减到0,就不会再发出信号暂停子进程,而是设置__afl_area_ptr
指向一个无关数组__afl_area_initial
,随后将子进程执行到结束。- 这是因为程序依然会向后执行并触发到instrument,这会向
__afl_area_ptr
里写值,但是此时我们其实并没有执行fuzzAPI
,我们并不想向共享内存里写值,于是将其指向一个无关数组,随意写值。同理,在deferred instrumentation模式里,在执行__afl_manual_init
之前,也是向无关数组里写值,因为我们将fork点手动设置,就代表在这个fork点之前的path我们并不关心。 - 重新整理一下上面的逻辑
- loop第一次执行的时候,会初始化,然后返回1,执行一次fuzzAPI,然后cnt会减到999,然后抛出信号暂停子进程。
- loop第二次执行的时候,恢复执行,清空一些值,然后返回1,执行一次fuzzAPI,然后cnt会减到998,然后抛出信号暂停子进程。
- loop第1000次执行的时候,恢复执行,清空一些值,然后返回1,执行一次fuzzAPI,然后cnt会减到0,然后就设置指向无关数组,返回0,while循环结束,程序也将执行结束。
- 这是因为程序依然会向后执行并触发到instrument,这会向
- 此时fork server将不再收到SIGSTOP信号,于是child_stopped仍为0。
- 所以当AFL fuzz通知fork server再进行一次fuzz的时候,由于此时child_stopped为0,则fork server会先fork出一个子进程,然后后续过程和之前一样了。
- 有点绕,可以结合代码再读一下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25int __afl_persistent_loop(unsigned int max_cnt) {
static u8 first_pass = 1;
static u32 cycle_cnt;
if (first_pass) {
if (is_persistent) {
memset(__afl_area_ptr, 0, MAP_SIZE);
__afl_area_ptr[0] = 1;
__afl_prev_loc = 0;
}
cycle_cnt = max_cnt;
first_pass = 0;
return 1;
}
if (is_persistent) {
if (--cycle_cnt) {
raise(SIGSTOP);
__afl_area_ptr[0] = 1;
__afl_prev_loc = 0;
return 1;
} else {
__afl_area_ptr = __afl_area_initial;
}
}
return 0;
}
trace-pc-guard mode
要使用这个功能,需要先通过AFL_TRACE_PC=1
来定义DUSE_TRACE_PC宏,从而在执行afl-clang-fast的时候传入-fsanitize-coverage=trace-pc-guard
参数,来开启这个功能,和之前我们的插桩不同,开启了这个功能之后,我们不再是仅仅只对每个基本块插桩,而是对每条edge都进行了插桩。
1 | ifdef AFL_TRACE_PC |
__sanitizer_cov_trace_pc_guard
这个函数将在每个edge调用,该函数利用函数参数guard指针所指向的uint32值来确定共享内存上所对应的地址。
每个edge上都有应该有其不同(但其实可能相同,原因下述)的guard值
1 | void __sanitizer_cov_trace_pc_guard(uint32_t* guard) { |
而这个guard指针的初始化在__sanitizer_cov_trace_pc_guard_init
函数里,llvm会设置guard其首末分别为start和stop。
它会从第一个guard开始向后遍历,设置guard指向的值,这个值是通过R(MAP_SIZE)
设置的,定义如下,所以如果我们的edge足够多,而MAP_SIZE
不够大,就有可能重复,而这个加一是因为我们会把0当成一个特殊的值,其代表对这个edge不进行插桩。
这个init其实很有趣,我们可以打印输出一下stop-start
的值,就代表了llvm发现的程序里总计的edge数。
1 |
|
afl-fuzz长叙
初始配置
setup_signal_handlers
注册必要的信号处理函数
- Linux进程间通信(一):信号 signal()、sigaction()
- SIGHUP/SIGINT/SIGTERM
- hangup/interrupt/software termination signal from kill
- 主要是”stop”的处理函数
- handle_stop_sig
- 设置stop_soon为1
- 如果child_pid存在,向其发送SIGKILL终止信号,从而被系统杀死。
- 如果forksrv_pid存在,向其发送SIGKILL终止信号
- SIGALRM
- alarm clock
- 处理超时的情况
- handle_timeout
- 如果child_pid>0,则设置child_timed_out为1,并kill掉child_pid
- 如果child_pid==-1,且forksrv_pid>0,则设置child_timed_out为1,并kill掉forksrv_pid
- SIGWINCH
- Window resize
- 处理窗口大小的变化信号
- handle_resize
- 设置clear_screen=1
- SIGUSR1
- user defined signal 1,这个是留给用户自定义的信号
- 这里定义成skip request (SIGUSR1)
- handle_skipreq
- 设置skip_requested=1
- SIGTSTP/SIGPIPE
- stop signal from tty/write on a pipe with no one to read it
- 不关心的一些信号
- SIG_IGN
check_asan_opts
check asan选项
- 读取环境变量ASAN_OPTIONS和MSAN_OPTIONS,做一些检查
fix_up_sync
如果通过-M或者-S指定了sync_id,则更新out_dir和sync_dir的值
- 设置sync_dir的值为out_dir
- 设置out_dir的值为
out_dir/sync_id
save_cmdline
拷贝当前的命令行参数
1 | 00 ff 00 ff 55 00 00 00 buf-> 2f 55 73 65 72 73 2f 73 │ ····U···/Users/s │ |
fix_up_banner
修剪并且创建一个运行横幅
check_if_tty
检查是否在tty终端上面运行。
- 读取环境变量AFL_NO_UI的值,如果为真,则设置not_on_tty为1,并返回
ioctl(1, TIOCGWINSZ, &ws)
通过ioctl来读取window size,如果报错为ENOTTY,则代表当前不在一个tty终端运行,设置not_on_tty
get_core_count
计数logical CPU cores
check_crash_handling
check_cpu_governor
setup_post
setup_shm
配置共享内存和virgin_bits
- Linux进程间通信(六):共享内存 shmget()、shmat()、shmdt()、shmctl()
- 如果in_bitmap为空,则通过memset初始化数组virgin_bits[MAP_SIZE]的每个元素的值为’255’(\xff)
- 通过memset设置virgin_tmout[MAP_SIZE]和virgin_crash[MAP_SIZE]的每个元素的值为’255’(\xff)
- 调用shmget分配一块共享内存,
shmget(IPC_PRIVATE, MAP_SIZE, IPC_CREAT | IPC_EXCL | 0600);
,将返回的共享内存标识符保存到shm_id里。int shmget(key_t key, size_t size, int shmflg);
- 第一个参数,程序需要提供一个参数key(非0整数),它有效地为共享内存段命名,shmget()函数成功时返回一个与key相关的共享内存标识符(非负整数),用于后续的共享内存函数。调用失败返回-1.
- 这里shm_id取值是IPC_PRIVATE,所以函数shmget()将创建一块新的共享内存
- 第二个参数,size以字节为单位指定需要共享的内存容量
- 这里取值为MAP_SIZE
- 第三个参数,shmflg是权限标志
- IPC_CREAT 如果共享内存不存在,则创建一个共享内存,否则打开操作。
- IPC_EXCL 只有在共享内存不存在的时候,新的共享内存才建立,否则就产生错误。
- 421分别表示,读写执行3种权限。 比如,上面的6=4+2,表示读+写。
- 0600 每一位表示一种类型的权限,比如,第一位是表示八进制,第二位表示拥有者的权限为读写,第三位表示同组无权限,第四位表示他人无权限。
- 注册atexit handler为remove_shm
- remove_shm
- shmctl(shm_id, IPC_RMID, NULL);
- 第一个参数,shm_id是shmget()函数返回的共享内存标识符。
- 第二个参数,command是要采取的操作,它可以取下面的三个值
- IPC_RMID:删除共享内存段
- 第三个参数,buf是一个结构指针
- shmctl(shm_id, IPC_RMID, NULL);
- remove_shm
- 使用alloc_printf(“%d”, shm_id)来创建一个字符串shm_str
1
00 ff 00 ff 06 00 00 00 shm_str->36 35 35 33 38 00 f0 f0 │ ········65538··· │
- 如果不是dumb_mode,则设置环境变量SHM_ENV_VAR的值为shm_str
trace_bits = shmat(shm_id, NULL, 0);
- trace_bits是用做
SHM with instrumentation bitmap
- 第一次创建完共享内存时,它还不能被任何进程访问,所以通过shmat来启动对该共享内存的访问,并把共享内存连接到当前进程的地址空间。
void *shmat(int shm_id, const void *shm_addr, int shmflg)
- 第一个参数,shm_id是由shmget()函数返回的共享内存标识。
- 第二个参数,shm_addr指定共享内存连接到当前进程中的地址位置,通常为空,表示让系统来选择共享内存的地址。
- 第三个参数,shm_flg是一组标志位,通常为0。
- 调用成功时返回一个指向共享内存第一个字节的指针,如果调用失败返回-1.
init_count_class16
这其实是因为trace_bits是用一个字节来记录是否到达这个路径,和这个路径被命中了多少次的,而这个次数在0-255之间,但比如一个循环,它循环5次和循环6次可能是完全一样的效果,为了避免被当成不同的路径,或者说尽可能减少因为命中次数导致的区别。
在每次去计算是否发现了新路径之前,先把这个路径命中数进行规整,比如把命中5次和6次都统一认为是命中了8次,见下面这个。而为什么又需要用一个1
2
3
4
5
6
7
8
9
10
11static const u8 count_class_lookup8[256] = {
[0] = 0,
[1] = 1,
[2] = 2,
[3] = 4,
[4 ... 7] = 8,
[8 ... 15] = 16,
[16 ... 31] = 32,
[32 ... 127] = 64,
[128 ... 255] = 128
};count_class_lookup16
呢,是因为AFL在后面实际进行规整的时候,是一次读两个字节去处理的,为了提高效率,这只是出于效率的考量,实际效果还是上面这种效果。
初始化u16 count_class_lookup16[65536]
1
2
3
4
5
6
7
8
9
10EXP_ST void init_count_class16(void) {
u32 b1, b2;
for (b1 = 0; b1 < 256; b1++)
for (b2 = 0; b2 < 256; b2++)
count_class_lookup16[(b1 << 8) + b2] =
(count_class_lookup8[b1] << 8) |
count_class_lookup8[b2];
}
- trace_bits是用做
setup_dirs_fds
准备输出文件夹和fd
- 如果sync_id存在,且创建sync_dir文件夹,设置权限为0700(读写执行)
- 如果报错,且errno不是EEXIST,则抛出异常。
- 创建out_dir,设置权限为0700(读写执行)
- 如果报错,且errno不是EEXIST,则抛出异常。
- maybe_delete_out_dir
- 如果创建成功
- 如果设置了in_place_resume,就抛出异常”Resume attempted but old output directory not found”
out_dir_fd = open(out_dir, O_RDONLY)
以只读模式打开这个文件,并返回文件句柄out_dir_fd- 如果没有定义宏
__sun
- 如果打开out_dir失败,或者为out_dir通过flock建立互斥锁定失败,就抛出异常”Unable to flock() output directory.”
- 如果报错,且errno不是EEXIST,则抛出异常。
- 建立queue文件夹
- 创建
out_dir/queue
文件夹,设置权限为0700- 创建
out_dir/queue/.state/
,设置权限为0700,该文件夹主要保存用于session resume和related tasks的queue metadata- 创建
out_dir/queue/.state/deterministic_done/
,设置权限为0700,该文件夹标记过去经历过deterministic fuzzing的queue entries。 - 创建
out_dir/queue/.state/auto_extras/
,设置权限为0700,Directory with the auto-selected dictionary entries. - 创建
out_dir/queue/.state/redundant_edges/
,设置权限为0700,保存当前被认为是多余的路径集合 - 创建
out_dir/queue/.state/variable_behavior/
,设置权限为0700,The set of paths showing variable behavior.
- 创建
- 创建
- 创建
- 如果sync_id存在
- 创建
out_dir/.synced/
,设置权限为0700,同步文件夹,用于跟踪cooperating fuzzers.
- 创建
- 建立crashes文件夹
- 创建
out_dir/crashes
文件夹,设置权限为0700,用于记录crashes
- 创建
- 建立hangs文件夹
- 创建
out_dir/hangs
文件夹,设置权限为0700,用于记录hangs
- 创建
- 通常有用的文件描述符
dev_null_fd = open("/dev/null", O_RDWR);
以读写模式打开/dev/null
dev_urandom_fd = open("/dev/urandom", O_RDONLY);
,以只读模式打开/dev/urandom
- 建立Gnuplot输出文件夹
fd = open(tmp, O_WRONLY | O_CREAT | O_EXCL, 0600);
以只写方式打开out_dir/plot_data
文件,如果文件不存在,就创建,并获取句柄plot_file = fdopen(fd, "w");
根据句柄得到FILE* plot_file- 向其中写入
# unix_time, cycles_done, cur_path, paths_total, pending_total, pending_favs, map_size, unique_crashes, unique_hangs, max_depth, execs_per_sec\n
read_testcases
从输入文件夹中读取所有文件,然后将它们排队进行测试。
- 尝试访问
in_dir/queue
文件夹,如果存在就重新设置in_dir为in_dir/queue
- Auto-detect non-in-place resumption attempts.
- 扫描in_dir,并将结果保存在
struct dirent **nl
里- 不使用readdir,因为测试用例的顺序将随机地有些变化,并且将难以控制。
- 遍历nl,nl[i]->d_name的值为input文件夹下的文件名字符串
u8 *fn = alloc_printf("%s/%s", in_dir, nl[i]->d_name);
u8 *dfn = alloc_printf("%s/.state/deterministic_done/%s", in_dir, nl[i]->d_name);
- 如果
shuffle_queue
的值为真,且nl_cnt大于1,则shuffle_ptrs((void **) nl, nl_cnt)
,字面意思上就是重排nl里的指针的位置。 - 通过文件属性过滤掉
.
和..
这样的regular文件,并检查文件大小,如果文件大小大于MAX_FILE,默认是1024*1024字节,即1M - 通过access检查
in_dir/.state/deterministic_done/nl[i]->d_name
是否存在,这应该是为了用在resume恢复扫描使用- 如果存在就设置passed_det为1
- 这个检查是用来判断是否这个entry已完成deterministic fuzzing。在恢复异常终止的扫描时,我们不想重复deterministic fuzzing,因为这将毫无意义,而且可能非常耗时
- add_to_queue(fn, st.st_size, passed_det);
- 如果queued_paths为0,则代表输入文件夹为0,抛出异常
- 设置last_path_time为0
- queued_at_start的值设置为queued_paths
- Total number of initial inputs
add_to_queue(u8 *fname, u32 len, u8 passed_det)
- queue_entry是一个链表数据结构
- 先通过calloc动态分配一个queue_entry结构体,并初始化其fname为文件名fn,len为文件大小,depth为cur_depth + 1,passed_det为传递进来的passed_det
1
2
3
4q->fname = fname;
q->len = len;
q->depth = cur_depth + 1;
q->passed_det = passed_det; - 如果
q->depth > max_depth
,则设置max_depth为q->depth - 如果queue_top不为空,则设置
queue_top->next为q,queue_top = q;
,否则q_prev100 = queue = queue_top = q;
1
2
3static struct queue_entry *queue, /* Fuzzing queue (linked list) */
*queue_top, /* Top of the list */
*q_prev100; /* Previous 100 marker */ - queue计数器queued_paths和待fuzz的样例计数器pending_not_fuzzed加一
- cycles_wo_finds设置为0
- Cycles without any new paths
- 如果
queued_paths % 100
得到0,则设置q_prev100->next_100 = q; q_prev100 = q;
- 设置last_path_time为当前时间。
load_auto
load自动生成的提取出来的词典token
- 遍历循环从i等于0到USE_AUTO_EXTRAS,默认50
- 以只读模式尝试打开文件名为
alloc_printf("%s/.state/auto_extras/auto_%06u", in_dir, i)
的文件 - 如果打开失败,则结束
- 如果打开成功,则从fd读取最多
MAX_AUTO_EXTRA+1
个字节到tmp数组里,默认MAX_AUTO_EXTRA为32,这是单个auto extra文件的最大大小,读取出的长度保存到len里。 - maybe_add_auto(tmp, len);
};
- 以只读模式尝试打开文件名为
maybe_add_auto(u8 *mem, u32 len)
- 如果用户设置了MAX_AUTO_EXTRAS或者USE_AUTO_EXTRAS为0,则直接返回。
- 循环遍历i从1到len,将tmp[0]和mem[i]异或,如果相同,则结束循环。
- 如果结束时i=0,即tmp[0]和tmp[1]就相同,就直接返回。这里我推断tmp应该是从小到大排序的字节流。
- 如果len的长度为2,就和interesting_16数组里的元素比较,如果和其中某一个相同,就直接return。
- 如果len的长度为4,就和interesting_32数组里的元素比较,如果和其中某一个相同,就直接return。
- 将tmp和现有的extras数组里的元素比较,利用extras数组里保存的元素是按照size大小,从小到大排序这个特性,来优化代码。
- 遍历extras数组,比较
memcmp_nocase(extras[i].data, mem, len)
,如果有一个相同,就直接return。 - static struct extra_data extras; / Extra tokens to fuzz with */
- 遍历extras数组,比较
- 设置auto_changed为1
- 遍历a_extras数组,比较
memcmp_nocase(a_extras[i].data, mem, len)
,如果相同,就将其hit_cnt值加一,这是代表在语料中被use的次数,然后跳转到sort_a_extras
- static struct extra_data a_extras; / Automatically selected extras */
1
2
3
4struct extra_data {
u8 *data; /* Dictionary token data */
u32 len; /* Dictionary token length */
u32 hit_cnt; /* Use count in the corpus */
- static struct extra_data a_extras; / Automatically selected extras */
- 此时我们可能在处理一个不在之前的任何a_extras或者extras数组里的新entry了,处理逻辑是
- 先比较a_extras_cnt和MAX_AUTO_EXTRAS,如果小于就代表a_extras数组没有填满,直接拷贝tmp和len,来构造出一个新项,加入到a_extras数组里
- 否则的话,就从a_extras数组的后半部分里,随机替换掉一个元素的a_extras[i].data为ck_memdup(mem, len),并将len设置为len,hit_cnt设置为0。
pivot_inputs
逻辑上说这个函数就是为inputdir里的testcase,在output dir里创建hard link
- 初始化id=0
- 依次遍历queue里的queue_entry
- 在q->fname里找到最后一个’/‘所在的位置,如果找不到,则
rsl = q->fname
,否则rsl指向’/‘后的第一个字符,其实也就是最后一个/
后面的字符串 - 将rsl的前三个字节和
id_
进行比较- 如果相等,则设置
resuming_fuzz
为1,然后做一些恢复操作,不叙述。 - 如果不相等
- 在rsl里寻找
,orig:
子串,如果找到了,将use_name指向该子串的冒号后的名字;如果没找到,就另use_name = rsl
nfn = alloc_printf("%s/queue/id:%06u,orig:%s", out_dir, id, use_name);
- 尝试创建从input file到
alloc_printf("%s/queue/id:%06u,orig:%s", out_dir, id, use_name)
的硬链接
- 在rsl里寻找
- 如果相等,则设置
- 修改q的fname指向这个硬链接
- 如果q的passed_det为1,则mark_as_det_done(q),这主要是对应上面的resuming_fuzz的情况。
- mark_as_det_done简单的说就是打开
out_dir/queue/.state/deterministic_done/use_name
这个文件,如果不存在就创建这个文件,然后设置q的passed_det为1。 - 这里的
use_name就是orig:后面的字符串
- mark_as_det_done简单的说就是打开
- 在q->fname里找到最后一个’/‘所在的位置,如果找不到,则
- 如果设置了in_place_resume为1,则nuke_resume_dir()
- nuke_resume_dir()
- 删除
out_dir/_resume/.state/deterministic_done
文件夹下所有id:
前缀的文件 - 删除
out_dir/_resume/.state/auto_extras
文件夹下所有auto_
前缀的文件 - 删除
out_dir/_resume/.state/redundant_edges
文件夹下所有id:
前缀的文件 - 删除
out_dir/_resume/.state/variable_behavior
文件夹下所有id:
前缀的文件 - 删除文件夹
out_dir/_resume/.state
- 删除
out_dir/_resume
文件夹下所有id:
前缀的文件 - 如果全部删除成功就正常返回,如果有某一个删除失败就抛出异常。
- 删除
- nuke_resume_dir()
load_extras
如果定义了extras_dir,则从extras_dir读取extras到extras数组里,并按size排序。
find_timeout
如果timeout_given没有被设置,则进入find_timeout
这个想法是,在不指定-t的情况下resuming sessions时,我们不希望一遍又一遍地自动调整超时时间,以防止超时值因随机波动而增长
- 如果resuming_fuzz为0,则直接return
- 如果in_place_resume为1,则
fn = alloc_printf("%s/fuzzer_stats", out_dir);
,否则fn = alloc_printf("%s/../fuzzer_stats", in_dir);
- 以只读方式打开fd,读取内容到tmp[4096]里,并在里面搜索”exec_timeout : “,如果搜索不到就直接返回,如果搜索到了,就读取这个timeout的数值,如果大于4就设置为exec_tmout的值。
- EXP_ST u32 exec_tmout = EXEC_TIMEOUT; /* Configurable exec timeout (ms) */
- timeout_given = 3;
- timeout_given, /* Specific timeout given? */
detect_file_args
这个函数其实就是识别参数里面有没有@@
,如果有就替换为out_dir/.cur_input
,如果没有就返回
setup_stdio_file
如果out_file为NULL,如果没有使用-f,就删除原本的out_dir/.cur_input
,创建一个新的out_dir/.cur_input
,保存其文件描述符在out_fd中
check_binary
check指定路径处要执行的程序是否存在,且它不能是一个shell script
perform_dry_run
执行所有的测试用例,以检查是否按预期工作
- 读取环境变量AFL_SKIP_CRASHES到skip_crashes,设置cal_failures为0
- 遍历queue
- 打开q->fname,并读取到分配的内存use_mem里
- res = calibrate_case(argv, q, use_mem, 0, 1);
- 校准测试用例,见下文
- 如果stop_soon被置为1,就直接return
- 如果res的结果为crash_mode或者FAULT_NOBITS
- 打印
SAYF("len = %u, map size = %u, exec speed = %llu us\n", q->len, q->bitmap_size, q->exec_us);
- 打印
- 依据res的结果查看是哪种错误并进行判断。一共有以下几种错误类型
- FAULT_NONE
- 如果q是头结点,即第一个测试用例,则
check_map_coverage
,用以评估map coverage- 计数trace_bits发现的路径数,如果小于100,就直接返回
- 在trace_bits的数组后半段,如果有值就直接返回。
- 抛出警告
WARNF("Recompile binary with newer version of afl to improve coverage!")
- 如果是crash_mode,则抛出异常,
FATAL("Test case '%s' does *NOT* crash", fn);
,该文件不崩溃
- 如果q是头结点,即第一个测试用例,则
- FAULT_TMOUT
- 如果指定了-t参数,则timeout_given值为2
- 抛出警告
WARNF("Test case results in a timeout (skipping)");
,并设置q的cal_failed为CAL_CHANCES,cal_failures计数器加一。
- 抛出警告
- 如果指定了-t参数,则timeout_given值为2
- FAULT_CRASH
- 如果没有指定mem_limit,则可能抛出建议增加内存的建议
- 但不管指定了还是没有,都会抛出异常
FATAL("Test case '%s' results in a crash", fn);
- FAULT_ERROR
- 抛出异常
Unable to execute target application
- 抛出异常
- FAULT_NOINST
- 这个样例运行没有出现任何路径信息,抛出异常
No instrumentation detected
- 这个样例运行没有出现任何路径信息,抛出异常
- FAULT_NOBITS
- 如果这个样例有出现路径信息,但是没有任何新路径,抛出警告
WARNF("No new instrumentation output, test case may be useless.")
,认为这是无用路径。useless_at_start计数器加一
- 如果这个样例有出现路径信息,但是没有任何新路径,抛出警告
- FAULT_NONE
- 如果这个样例q的var_behavior为真,则代表它多次运行,同样的输入条件下,却出现不同的覆盖信息。
- 抛出警告
WARNF("Instrumentation output varies across runs.");
,代表这个样例的路径输出可变
- 抛出警告
- 然后读取下一个queue,继续测试,直到结束。
1
2
3
4
5
6
7enum {
/* 00 */ FAULT_NONE,
/* 01 */ FAULT_TMOUT,
/* 02 */ FAULT_CRASH,
/* 03 */ FAULT_ERROR,
/* 04 */ FAULT_NOINST,
/* 05 */ FAULT_NOBITS };
u8 calibrate_case(char **argv, struct queue_entry *q, u8 *use_mem, u32 handicap, u8 from_queue)
这个函数评估input文件夹下的case,来发现这些testcase的行为是否异常;以及在发现新的路径时,用以评估这个新发现的testcase的行为是否是可变(这里的可变是指多次执行这个case,发现的路径不同)等等
- 这个函数的参数为
char **argv, struct queue_entry *q, u8 *use_mem, u32 handicap, u8 from_queue
- 创建first_trace[MAP_SIZE]
- 如果q->exec_cksum为0,代表这是这个case第一次运行,即来自input文件夹下,所以将first_run置为1。
- 保存原有的stage_cur、stage_max、stage_name
- 设置use_tmout为exec_tmout,如果from_queue是0或者resuming_fuzz被置为1,即代表不来自于queue中或者在resuming sessions的时候,则use_tmout的值被设置的更大。
- q->cal_failed++
- 设置stage_name为”calibration”,以及根据是否fast_cal为1,来设置stage_max的值为3还是CAL_CYCLES(默认为8),含义是每个新测试用例(以及显示出可变行为的测试用例)的校准周期数,也就是说这个stage要执行几次的意思。
- 如果当前不是以dumb mode运行,且no_forkserver(禁用forkserver)为0,且forksrv_pid为0,则init_forkserver(argv)启动fork server,见后文。
- 如果这个queue不是来自input文件夹,而是评估新case,则此时
q->exec_cksum
不为空,拷贝trace_bits到first_trace里,然后计算has_new_bits
的值,赋值给new_bits。 - 开始执行calibration stage,共执行stage_max轮
- 如果这个queue不是来自input文件夹,而是评估新case,且第一轮calibration stage执行结束时,刷新一次展示界面
show_stats
,用来展示这次执行的结果,此后不再展示。 - write_to_testcase(use_mem, q->len)
- 将从
q->fname
中读取的内容写入到.cur_input
中
- 将从
u8 run_target(argv, use_tmout)
,结果保存在fault中- 如果这是
calibration stage
第一次运行,且不在dumb_mode,且共享内存里没有任何路径(即没有任何byte被置位),设置fault为FAULT_NOINST
,然后goto abort_calibration。- 计算共享内存里有多少字节被置位了,通过count_bytes函数
u32 count_bytes(u8 *mem)
- 计算共享内存里有多少字节被置位了,通过count_bytes函数
- 计算
hash32(trace_bits, MAP_SIZE, HASH_CONST)
的结果,其值为一个32位uint值,保存到cksum中 - 如果
q->exec_cksum
不等于cksum,即代表这是第一次运行,或者在相同的参数下,每次执行,cksum却不同,是一个路径可变的queuehnb = has_new_bits(virgin_bits)
- 如果hnb大于new_bits,设置new_bits的值为hnb
- 如果
q->exec_cksum
不等于0,即代表这是判断是否是可变queue- i从0到MAP_SIZE遍历,如果first_trace[i]不等于trace_bits[i],代表发现了可变queue,且var_bytes为空,则将该字节设置为1,并将stage_max设置为
CAL_CYCLES_LONG
,即需要执行40次。 - 将var_detected设置为1
- i从0到MAP_SIZE遍历,如果first_trace[i]不等于trace_bits[i],代表发现了可变queue,且var_bytes为空,则将该字节设置为1,并将stage_max设置为
- 否则,即q->exec_cksum等于0,即代表这是第一次执行这个queue
- 设置q->exec_cksum的值为之前计算出来的本次执行的cksum
- 拷贝trace_bits到first_trace中。
- 如果这个queue不是来自input文件夹,而是评估新case,且第一轮calibration stage执行结束时,刷新一次展示界面
- 保存所有轮次总的执行时间,加到total_cal_us里,总的执行轮次,加到total_cal_cycles里
- 计算出一些统计信息,包括
- 计算出单次执行时间的平均值保存到q->exec_us里
- 将最后一次执行所覆盖到的路径数保存到q->bitmap_size里
q->handicap = handicap;
q->cal_failed = 0;
- total_bitmap_size里加上这个queue所覆盖到的路径数
- total_bitmap_entries++
- update_bitmap_score(struct queue_entry *q)
- 如果fault为
FAULT_NONE
,且该queue是第一次执行,且不属于dumb_mode,而且new_bits为0,代表在这个样例所有轮次的执行里,都没有发现任何新路径和出现异常,设置fault为FAULT_NOBITS
- 如果new_bits为2,且
q->has_new_cov
为0,设置其值为1,并将queued_with_cov加一,代表有一个queue发现了新路径。 - 如果这个queue是可变路径,即var_detected为1,则计算var_bytes里被置位的tuple个数,保存到var_byte_count里,代表这些tuple具有可变的行为。
- 将这个queue标记为一个variable
mark_as_variable(struct queue_entry *q)
- 创建符号链接
out_dir/queue/.state/variable_behavior/fname
- 设置queue的var_behavior为1
- 创建符号链接
- 计数variable behavior的计数器
queued_variable
的值加一
- 恢复之前的stage值
- 如果不是第一次运行这个queue,展示
show_stats
- 返回fault的值
init_forkserver
建立管道st_pipe和ctl_pipe,在父子进程之间,是通过管道进行通信,一个用于传递状态,另一个用于传递命令。
- 在继续往下读之前需要仔细阅读这篇文章
- Linux 的进程间通信:管道
fork出一个子进程,fork函数执行完毕后,如果创建新进程成功,则出现两个进程,一个是子进程,一个是父进程。在子进程中,fork函数返回0,在父进程中,fork返回新创建子进程的进程ID。我们可以通过fork返回的值来判断当前进程是子进程还是父进程。
- forksrv_pid = fork()
子进程和父进程都会向下执行,我们通过pid来使它们执行不同的代码
if(!forksrv_pid)
- 以下都是子进程要执行的代码
- 在继续向下读之前,需要仔细阅读这篇文章
- 进程间通信管道进阶篇:linux下dup/dup2函数的用法
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假设进程A拥有一个已打开的文件描述符fd3,它的状态如下
进程A的文件描述符表(before dup2)
------------
fd0 0 | p0
------------
fd1 1 | p1 -------------> 文件表1 ---------> vnode1
------------
fd2 2 | p2
------------
fd3 3 | p3 -------------> 文件表2 ---------> vnode2
------------
... ...
... ...
------------
经下面调用:
n_fd = dup2(fd3, STDOUT_FILENO);后进程状态如下:
进程A的文件描述符表(after dup2)
------------
fd0 0 | p0
------------
n_fd 1 | p1 ------------
------------ \
fd2 2 | p2 \
------------ _\|
fd3 3 | p3 -------------> 文件表2 ---------> vnode2
在学习dup2时总是碰到“重定向”一词,上图完成的就是一个“从标准输出到文件的重定向”,经过dup2后进程A的任何目标为STDOUT_FILENO的I/O操作如printf等,其数据都将流入fd3所对应的文件中。下面是一个例子程序:
int main() {
int fd3;
fd3 = open("testdup2.dat", 0666);
if (fd < 0) {
printf("open error\n");
exit(-1);
}
if (dup2(fd3, STDOUT_FILENO) < 0) {
printf("err in dup2\n");
}
printf(TESTSTR);
return 0;
}
其结果就是你在testdup2.dat中看到"Hello dup2"。
- 进程间通信管道进阶篇:linux下dup/dup2函数的用法
- 重定向文件描述符1和2到dev_null_fd,如果指定了out_file,则文件描述符0重定向到dev_null_fd,否则重定向到out_fd。
dup2(dev_null_fd, 1);
dup2(dev_null_fd, 2);
- 重定向FORKSRV_FD到ctl_pipe[0],重定向FORKSRV_FD + 1到st_pipe[1]
- 子进程只能读取命令
- 子进程只能发送(“写出”)状态
- 关闭子进程里的一些文件描述符
1
2
3
4
5
6
7
8
9close(ctl_pipe[0]);
close(ctl_pipe[1]);
close(st_pipe[0]);
close(st_pipe[1]);
close(out_dir_fd);
close(dev_null_fd);
close(dev_urandom_fd);
close(fileno(plot_file)); - 读取环境变量LD_BIND_LAZY,如果没有设置,则设置环境变量LD_BIND_NOW为1
- 设置环境变量ASAN_OPTIONS为
"abort_on_error=1:" "detect_leaks=0:" "symbolize=0:" "allocator_may_return_null=1"
,同理设置MSAN_OPTIONS execv(target_path, argv)
带参数执行target,这个函数除非出错不然不会返回。- execv会替换掉原有的进程空间为target_path代表的程序,所以相当于后续就是去执行target_path,这个程序结束的话,子进程就结束。
- 而在这里非常特殊,第一个target会进入
__afl_maybe_log
里的__afl_fork_wait_loop
,并充当fork server,在整个Fuzz的过程中,它都不会结束,每次要Fuzz一次target,都会从这个fork server fork出来一个子进程去fuzz。
- 使用一个独特的bitmaps EXEC_FAIL_SIG(0xfee1dead)写入trace_bits,来告诉父进程执行失败,并结束子进程。
- 在继续向下读之前,需要仔细阅读这篇文章
- 以下都是子进程要执行的代码
以下都是父进程要执行的代码
- 关闭不需要的endpoints
1
2
3
4
5
6// 关闭不是需要的endpoints
close(ctl_pipe[0]);
close(st_pipe[1]);
fsrv_ctl_fd = ctl_pipe[1];//父进程只能发送("写出")命令
fsrv_st_fd = st_pipe[0];//父进程只能读取状态 - 等待fork server启动,但是不能等太久。(所以在调试时要注意这个…)
- 从管道里读取4个字节到status里,如果读取成功,则代表fork server成功启动,就结束这个函数并返回。
- 如果超时,就抛出异常。
- 关闭不需要的endpoints
后续是一些子进程启动失败的异常处理逻辑,暂时不叙。
has_new_bits(u8 *virgin_map)
- 检查有没有新路径或者某个路径的执行次数有所不同。
- 初始化current和virgin为trace_bits和virgin_map的u64首元素地址,设置ret的值为0
- 8个字节一组,每次从trace_bits,也就是共享内存里取出8个字节
- 如果current不为0,且
current & virgin
不为0,即代表current发现了新路径或者某条路径的执行次数和之前有所不同- 如果ret当前小于2
- 取current的首字节地址为cur,virgin的首字节地址为vir
- i的范围是0-7,比较
cur[i] && vir[i] == 0xff
,如果有一个为真,则设置ret为2- 这代表发现了之前没有出现过的tuple
- 注意==的优先级比&&要高,所以先判断vir[i]是否是0xff,即之前从未被覆盖到,然后再和cur[i]进行逻辑与
- 否则设置ret为1
- 这代表仅仅只是改变了某个tuple的hit-count
*virgin &= ~*current
- 如果ret当前小于2
- current和virgin移动到下一组8个字节,直到MAPSIZE全被遍历完。
- 如果current不为0,且
- 如果传入给has_new_bits的参数
virgin_map
是virgin_bits
,且ret不为0,就设置bitmap_changed为1- virgin_bits保存还没有被Fuzz覆盖到的byte,其初始值每位全被置位1,然后每次按字节置位。
- 返回ret的值。
u32 count_bytes(u8 *mem)
- 初始化计数器ret的值为0,循环读取mem里的值,每次读取4个字节到u32变量v中
- 如果v为0,则代表这四个字节都是0,直接跳过,进入下一次循环
- 如果v不为0,则依次计算
v & FF(0),v & FF(1),v & FF(2),v&FF(3)
的结果,如果不为0,则计数器ret加一。- #define FF(_b) (0xff << ((_b) << 3))
(_b) << 3)
即_b * 8
- 即
0x000000ff
左移(_b * 8)
位 - 最终结果可以是
0x000000ff
,0x0000ff00
,0x00ff0000
,0xff000000
其中之一
- #define FF(_b) (0xff << ((_b) << 3))
u8 run_target(char **argv, u32 timeout)
- 先清空trace_bits[MAP_SIZE],将其全置为0,也就是清空共享内存。
- 如果dumb_mode等于1,且no_forkserver,则直接fork出一个子进程,然后让子进程execv去执行target,如果execv执行失败,则向trace_bits写入EXEC_FAIL_SIG
- 否则,就向控制管道写入
prev_timed_out
的值,命令Fork server开始fork出一个子进程进行fuzz,然后从状态管道读取fork server返回的fork出的子进程的ID到child_pid
- 无论实际执行的是上面两种的哪一种,在执行target期间,都设置计数器为timeout,如果超时,就杀死正在执行的子进程,并设置child_timed_out为1;
- 计算target执行时间exec_ms,并将total_execs这个执行次数计数器加一。
- 等待target执行结束,如果是dumb_mode,target执行结束的状态码将直接保存到status中,如果不是dumb_mode,则从状态管道中读取target执行结束的状态码。
- classify_counts((u64 *) trace_bits)
- 具体地,target是将每个分支的执行次数用1个byte来储存,而fuzzer则进一步把这个执行次数归入到buckets中,举个例子,如果某分支执行了1次,那么落入第2个bucket,其计数byte仍为1;如果某分支执行了4次,那么落入第5个bucket,其计数byte将变为8,等等。
- 这样处理之后,对分支执行次数就会有一个简单的归类。例如,如果对某个测试用例处理时,分支A执行了32次;对另外一个测试用例,分支A执行了33次,那么AFL就会认为这两次的代码覆盖是相同的。当然,这样的简单分类肯定不能区分所有的情况,不过在某种程度上,处理了一些因为循环次数的微小区别,而误判为不同执行结果的情况.
- 设置prev_timed_out的值为child_timed_out。
- 接着依据status的值,向调用者返回结果。
WIFSIGNALED(status)
若为异常结束子进程返回的状态,则为真WTERMSIG(status)
取得子进程因信号而中止的信号代码- 如果child_timed_out为1,且状态码为
SIGKILL
,则返回FAULT_TMOUT
- 否则返回
FAULT_CRASH
- 如果child_timed_out为1,且状态码为
- 如果是dumb_mode,且trace_bits为EXEC_FAIL_SIG,就返回
FAULT_ERROR
- 如果
timeout
小于等于exec_tmout
,且slowest_exec_ms
小于exec_ms
,设置slowest_exec_ms
等于exec_ms
- 返回
FAULT_NONE
classify_counts(u64 *mem)
- 8个字节一组去循环读入,直到遍历完整个mem
- 每次取两个字节
u16 *mem16 = (u16 *) mem
- i从0到3,计算
mem16[i]
的值,在count_class_lookup16[mem16[i]]
里找到对应的取值,并赋值给mem16[i]
- 每次取两个字节
update_bitmap_score(struct queue_entry *q)
每当我们发现一个新的路径,都会调用这个函数来判断其是不是更加地favorable,这个favorable的意思是说是否包含最小的路径集合来遍历到所有bitmap中的位,我们专注于这些集合而忽略其他的。
- 首先计算出这个case的fav_factor,计算方法是
q->exec_us * q->len
即执行时间和样例大小的乘积,以这两个指标来衡量权重。 - 遍历trace_bits数组,如果该字节的值不为0,则代表这是已经被覆盖到的path
- 然后检查对应于这个path的top_rated是否存在
static struct queue_entry *top_rated[MAP_SIZE]; /* Top entries for bitmap bytes */
- 如果存在,就比较
fav_factor > top_rated[i]->exec_us * top_rated[i]->len
,即比较执行时间和样例大小的乘积,哪个更小。- 如果
top_rated[i]
的更小,则代表top_rated[i]
的更优,不做任何处理,继续遍历下一个path。 - 如果q更小,就将
top_rated[i]
原先对应的queue entry的tc_ref字段减一,并将其trace_mini字段置为空。 u8 *trace_mini; /* Trace bytes, if kept */
u32 tc_ref; /* Trace bytes ref count */
- 如果
- 然后设置
top_rated[i]
为q,即当前case,然后将其tc_ref的值加一 - 如果
q->trace_mini
为空,则将trace_bits经过minimize_bits压缩,然后存到trace_mini字段里 - 设置score_changed为1.
- 然后检查对应于这个path的top_rated是否存在
void minimize_bits(u8 *dst, u8 *src)
将trace_bits压缩为较小的位图。
简单的理解就是把原本是包括了是否覆盖到和覆盖了多少次的byte,压缩成是否覆盖到的bit。
在看这个函数和下一个函数cull_queue之前,建议把经典算法系列之(一) - BitMap [数据的压缩存储]读完。
1 | static void minimize_bits(u8 *dst, u8 *src) { |
虽然dst是一个bitmap,但是实际上在这里我们还是用一个byte数组来操作它,所以就首先得做byte->bit的映射,比如说将src的前0-7个字节映射到dst的第一个字节(0-7位)
1 | >>> 0>>3 |
然后如果src里该字节的值不为0,i此时就代表这个字节的index索引,其与0000 0111
相与,最终的结果都只在0-7之间,这样我们就可以知道这个index在0-7之间对应的具体的bit是哪一个,最后通过或运算将该位置位。
1 | >>> 0&7 |
cull_queue
精简队列
- 如果score_changed为0,即top_rated没有变化,或者dumb_mode,就直接返回
- 设置score_changed的值为0
- 创建u8 temp_v数组,大小为
MAP_SIZE除8
,并将其初始值设置为0xff,其每位如果为1就代表还没有被覆盖到,如果为0就代表以及被覆盖到了。 - 设置queued_favored为0,pending_favored为0
- 开始遍历queue队列,设置其favored的值都为0
- 将i从0到MAP_SIZE迭代,这个迭代其实就是筛选出一组queue entry,它们就能够覆盖到所有现在已经覆盖到的路径,而且这个case集合里的case要更小更快,这并不是最优算法,只能算是贪婪算法。
- 这又是个不好懂的位运算,
temp_v[i >> 3] & (1 << (i & 7))
与上面的差不多,中间的或运算改成了与,是为了检查该位是不是0,即判断该path对应的bit有没有被置位。1
2
3for (i = 0; i < MAP_SIZE; i++)
if (top_rated[i] && (temp_v[i >> 3] & (1 << (i & 7)))) {
... - 如果
top_rated[i]
有值,且该path在temp_v里被置位- 就从temp_v中清除掉所有
top_rated[i]
覆盖到的path,将对应的bit置为0 - 设置
top_rated[i]->favored
为1,queued_favored计数器加一 - 如果
top_rated[i]
的was_fuzzed字段是0,代表其还没有fuzz过,则将pending_favored计数器加一
- 就从temp_v中清除掉所有
- 遍历queue队列
- mark_as_redundant(q, !q->favored)
- 也就是说,如果不是favored的case,就被标记成redundant_edges
- mark_as_redundant(q, !q->favored)
- 这又是个不好懂的位运算,
mark_as_redundant(struct queue_entry *q, u8 state)
- 如果
state和q->fs_redundant
相等,就直接返回 - 设置
q->fs_redundant
的值为state, - 如果state为1
- 尝试创建
out_dir/queue/.state/redundant_edges/fname
- 尝试创建
- 如果state为0
- 尝试删除
out_dir/queue/.state/redundant_edges/fname
- 尝试删除
show_init_stats
在处理输入目录的末尾显示统计信息,以及一堆警告,以及几个硬编码的常量。
- 依据之前从calibrate_case里得到的total_cal_us和total_cal_cycles,计算出单轮执行的时间avg_us,如果大于10000,就警告
"The target binary is pretty slow! See %s/perf_tips.txt."
- 如果avg_us大于50000,设置havoc_div为10 /* 0-19 execs/sec */
- 大于20000,设置havoc_div为5 /* 20-49 execs/sec */
- 如果大于10000,设置havoc_div为2 /* 50-100 execs/sec */
- 如果不是resuming session,则对queue的大小和个数超限提出警告,且如果useless_at_start不为0,就警告有可以精简的样本。
- 如果timeout_given为0,则根据avg_us来计算出exec_tmout,注意这里avg_us的单位是微秒,而exec_tmout单位是毫秒,所以需要除以1000
- avg_us > 50000
- exec_tmout = avg_us * 2 / 1000
- avg_us > 10000
- exec_tmout = avg_us * 3 / 1000
- exec_tmout = avg_us * 5 / 1000
- 然后在上面计算出来的exec_tmout和所有样例中执行时间最长的样例进行比较,取最大值赋给exec_tmout
- 如果exec_tmout大于EXEC_TIMEOUT,就设置exec_tmout = EXEC_TIMEOUT
- EXEC_TIMEOUT的值为1秒,即最大超时时间是1秒
- 打印出
"No -t option specified, so I'll use exec timeout of %u ms.", exec_tmout
- 设置timeout_given为1
- avg_us > 50000
- 如果timeout_give不为0,且为3,代表这是resuming session,直接打印
"Applying timeout settings from resumed session (%u ms).", exec_tmout
,此时的timeout_give是我们从历史记录里读取出的。 - 如果是dumb_mode且没有设置环境变量AFL_HANG_TMOUT
- 设置hang_tmout为EXEC_TIMEOUT和
exec_tmout * 2 + 100
中的最小值
- 设置hang_tmout为EXEC_TIMEOUT和
All set and ready to roll!
find_start_position
resume时,请尝试查找要从其开始的队列位置,这仅在resume时以及当我们可以找到原始的fuzzer_stats时才有意义.
- 如果不是resuming_fuzz,就直接返回
- 如果是in_place_resume,就打开
out_dir/fuzzer_stats
文件,否则打开in_dir/../fuzzer_stats
文件 - 读这个文件的内容到tmp[4096]中,找到
cur_path
,并设置为ret的值,如果大于queued_paths就设置ret为0,返回ret。
void write_stats_file(double bitmap_cvg, double stability, double eps)
更新统计信息文件以进行无人值守的监视
- 创建文件
out_dir/fuzzer_stats
- 写入统计信息
- start_time
- fuzz运行的开始时间,start_time / 1000
- last_update
- 当前时间
- fuzzer_pid
- 获取当前pid
- cycles_done
queue_cycle
在queue_cur
为空,即执行到当前队列尾的时候才增加1,所以这代表queue队列被完全变异一次的次数。
- execs_done
- total_execs,target的总的执行次数,每次
run_target
的时候会增加1
- total_execs,target的总的执行次数,每次
- execs_per_sec
- 每秒执行的次数
- paths_total
- queued_paths在每次
add_to_queue
的时候会增加1,代表queue里的样例总数
- queued_paths在每次
- paths_favored
- queued_favored,有价值的路径总数
- paths_found
- queued_discovered在每次
common_fuzz_stuff
去执行一次fuzz时,发现新的interesting case的时候会增加1,代表在fuzz运行期间发现的新queue entry。
- queued_discovered在每次
- paths_imported
- queued_imported是master-slave模式下,如果sync过来的case是interesting的,就增加1
- max_depth
- 最大路径深度
- cur_path
- current_entry一般情况下代表的是正在执行的queue entry的整数ID,queue首节点的ID是0
- pending_favs
- pending_favored 等待fuzz的favored paths数
- pending_total
- pending_not_fuzzed 在queue中等待fuzz的case数
- variable_paths
- queued_variable在
calibrate_case
去评估一个新的test case的时候,如果发现这个case的路径是可变的,则将这个计数器加一,代表发现了一个可变case
- queued_variable在
- stability
- bitmap_cvg
- unique_crashes
- unique_crashes这是在
save_if_interesting
时,如果fault是FAULT_CRASH,就将unique_crashes计数器加一
- unique_crashes这是在
- unique_hangs
- unique_hangs这是在
save_if_interesting
时,如果fault是FAULT_TMOUT,且exec_tmout小于hang_tmout,就以hang_tmout为超时时间再执行一次,如果还超时,就让hang计数器加一。
- unique_hangs这是在
- last_path
- 在
add_to_queue
里将一个新case加入queue时,就设置一次last_path_time为当前时间,last_path_time / 1000
- 在
- last_crash
- 同上,在unique_crashes加一的时候,last_crash也更新时间,
last_crash_time / 1000
- 同上,在unique_crashes加一的时候,last_crash也更新时间,
- last_hang
- 同上,在unique_hangs加一的时候,last_hang也更新时间,
last_hang_time / 1000
- 同上,在unique_hangs加一的时候,last_hang也更新时间,
- execs_since_crash
- total_execs - last_crash_execs,这里last_crash_execs是在上一次crash的时候的总计执行了多少次
- exec_tmout
- 配置好的超时时间,有三种可能的配置方式,见上文
1 | fprintf(f, "start_time : %llu\n" |
- 统计子进程的资源用量并写入。
save_auto
保存自动生成的extras
- 如果auto_changed为0,则直接返回
- 如果不为0,就设置为0,然后创建名为
alloc_printf("%s/queue/.state/auto_extras/auto_%06u", out_dir, i);
的文件,并写入a_extras的内容。
Fuzz执行
主循环
- 首先精简队列
cull_queue
- 然后如果
queue_cur
为空,代表所有queue都被执行完一轮- 设置queue_cycle计数器加一,即代表所有queue被完整执行了多少轮。
- 设置current_entry为0,和queue_cur为queue首元素,开始新一轮fuzz。
- 如果是resume fuzz情况,则先检查seek_to是否为空,如果不为空,就从seek_to指定的queue项开始执行。
- 刷新展示界面
show_stats
- 如果在一轮执行之后的queue里的case数,和执行之前一样,代表在完整的一轮执行里都没有发现任何一个新的case
- 如果use_splicing为1,就设置cycles_wo_finds计数器加1
- 否则,设置use_splicing为1,代表我们接下来要通过splice重组queue里的case。
- 执行
skipped_fuzz = fuzz_one(use_argv)
来对queue_cur进行一次测试- 注意fuzz_one并不一定真的执行当前queue_cur,它是有一定策略的,如果不执行,就直接返回1,否则返回0
- 如果skipped_fuzz为0,且存在sync_id
- sync_interval_cnt计数器加一,如果其结果是SYNC_INTERVAL(默认是5)的倍数,就进行一次sync
queue_cur = queue_cur->next;current_entry++;
,开始测试下一个queue
fuzz_one
如果
pending_favored
不为0,则对于queue_cur被fuzz过或者不是favored的,有99%的几率直接返回1。如果
pending_favored
为0且queued_paths(即queue里的case总数)大于10- 如果queue_cycle大于1且queue_cur没有被fuzz过,则有75%的概率直接返回1
- 如果queue_cur被fuzz过,否则有95%的概率直接返回1
设置len为
queue_cur->len
打开该case对应的文件,并通过mmap映射到内存里,地址赋值给
in_buf
和orig_in
分配len大小的内存,并初始化为全0,然后将地址赋值给out_buf
CALIBRATION阶段
假如当前项有校准错误,并且校准错误次数小于3次,那么就用calibrate_case再次校准。
TRIMMING阶段
如果该case没有trim过,
- 调用函数
trim_case(argv, queue_cur, in_buf)
进行trim(修剪) - 设置queue_cur的trim_done为1
- 重新读取一次
queue_cur->len
到len中
- 调用函数
将in_buf拷贝len个字节到out_buf中
PERFORMANCE SCORE阶段
perf_score =
calculate_score(queue_cur)
如果skip_deterministic为1,或者queue_cur被fuzz过,或者queue_cur的passed_det为1,则跳转去havoc_stage阶段
设置doing_det为1
SIMPLE BITFLIP (+dictionary construction)阶段
下面这个宏很有意思
1
2
3
4
5
u8* _arf = (u8*)(_ar); \
u32 _bf = (_b); \
_arf[(_bf) >> 3] ^= (128 >> ((_bf) & 7)); \
} while (0)设置stage_name为
bitflip 1/1
,_ar的取值是out_buf,而_bf的取值在[0: len << 3)
所以用_bf & 7
能够得到0,1,2...7 0,1,2...7
这样的取值一共len组,然后(_bf) >> 3
又将[0: len<<3)映射回了[0: len),对应到buf里的每个byte,如图:
所以在从0-len*8
的遍历过程中会通过亦或运算,依次将每个位翻转,然后执行一次common_fuzz_stuff
,然后再翻转回来。1
2
3
4
5
6
7
8
9stage_max = len << 3;
for (stage_cur = 0; stage_cur < stage_max; stage_cur++)
{
FLIP_BIT(out_buf, stage_cur);
if (common_fuzz_stuff(argv, out_buf, len)) goto abandon_entry;
FLIP_BIT(out_buf, stage_cur);
}在进行bitflip 1/1变异时,对于每个byte的最低位(least significant bit)翻转还进行了额外的处理:如果连续多个bytes的最低位被翻转后,程序的执行路径都未变化,而且与原始执行路径不一致,那么就把这一段连续的bytes判断是一条token。
比如对于SQL的SELECT *
,如果SELECT
被破坏,则肯定和正确的路径不一致,而被破坏之后的路径却肯定是一样的,比如AELECT
和SBLECT
,显然都是无意义的,而只有不破坏token,才有可能出现和原始执行路径一样的结果,所以AFL在这里就是在猜解关键字token。token默认最小是3,最大是32,每次发现新token时,通过
maybe_add_auto
添加到a_extras
数组里。stage_finds[STAGE_FLIP1]
的值加上在整个FLIP_BIT中新发现的路径和Crash总和stage_cycles[STAGE_FLIP1]
的值加上在整个FLIP_BIT中执行的target次数stage_max
设置stage_name为
bitflip 2/1
,原理和之前一样,只是这次是连续翻转相邻的两位。1
2
3
4
5
6
7
8
9
10
11stage_max = (len << 3) - 1;
for (stage_cur = 0; stage_cur < stage_max; stage_cur++)
{
FLIP_BIT(out_buf, stage_cur);
FLIP_BIT(out_buf, stage_cur + 1);
if (common_fuzz_stuff(argv, out_buf, len)) goto abandon_entry;
FLIP_BIT(out_buf, stage_cur);
FLIP_BIT(out_buf, stage_cur + 1);
}然后保存结果到
stage_finds[STAGE_FLIP2]和stage_cycles[STAGE_FLIP2]
里。同理,设置stage_name为
bitflip 4/1
,翻转连续的四位并记录。生成effector map
- 在进行bitflip 8/8变异时,AFL还生成了一个非常重要的信息:effector map。这个effector map几乎贯穿了整个deterministic fuzzing的始终。
- 具体地,在对每个byte进行翻转时,如果其造成执行路径与原始路径不一致,就将该byte在effector map中标记为1,即“有效”的,否则标记为0,即“无效”的。
- 这样做的逻辑是:如果一个byte完全翻转,都无法带来执行路径的变化,那么这个byte很有可能是属于”data”,而非”metadata”(例如size, flag等),对整个fuzzing的意义不大。所以,在随后的一些变异中,会参考effector map,跳过那些“无效”的byte,从而节省了执行资源。
- 由此,通过极小的开销(没有增加额外的执行次数),AFL又一次对文件格式进行了启发式的判断。看到这里,不得不叹服于AFL实现上的精妙。
- 不过,在某些情况下并不会检测有效字符。第一种情况就是dumb mode或者从fuzzer,此时文件所有的字符都有可能被变异。第二、第三种情况与文件本身有关:
设置stage_name为
bitflip 8/8
,以字节为单位,直接通过和0xff
亦或运算去翻转整个字节的位,然后执行一次,并记录。设置stage_name为
bitflip 16/8
,设置stage_max
为len - 1
,以字为单位和0xffff
进行亦或运算,去翻转相邻的两个字节(即一个字的)的位。- 这里要注意在翻转之前会先检查eff_map里对应于这两个字节的标志是否为0,如果为0,则这两个字节是无效的数据,stage_max减一,然后开始变异下一个字。
- common_fuzz_stuff执行变异后的结果,然后还原。
同理,设置stage_name为
bitflip 32/8
,然后设置stage_max
为len - 3
,以双字为单位,直接通过和0xffffffff
亦或运算去相邻四个字节的位,然后执行一次,并记录。- 在每次翻转之前会检查eff_map里对应于这四个字节的标志是否为0,如果是0,则这两个字节是无效的数据,stage_max减一,然后开始变异下一组双字。
ARITHMETIC INC/DEC
在bitflip变异全部进行完成后,便进入下一个阶段:arithmetic。与bitflip类似的是,arithmetic根据目标大小的不同,也分为了多个子阶段:
arith 8/8,每次对8个bit进行加减运算,按照每8个bit的步长从头开始,即对文件的每个byte进行整数加减变异
arith 16/8,每次对16个bit进行加减运算,按照每8个bit的步长从头开始,即对文件的每个word进行整数加减变异
arith 32/8,每次对32个bit进行加减运算,按照每8个bit的步长从头开始,即对文件的每个dword进行整数加减变异
加减变异的上限,在config.h中的宏ARITH_MAX定义,默认为35。所以,对目标整数会进行+1, +2, …, +35, -1, -2, …, -35的变异。特别地,由于整数存在大端序和小端序两种表示方式,AFL会贴心地对这两种整数表示方式都进行变异。
此外,AFL还会智能地跳过某些arithmetic变异。第一种情况就是前面提到的effector map:如果一个整数的所有bytes都被判断为“无效”,那么就跳过对整数的变异。第二种情况是之前bitflip已经生成过的变异:如果加/减某个数后,其效果与之前的某种bitflip相同,那么这次变异肯定在上一个阶段已经执行过了,此次便不会再执行。
INTERESTING VALUES
下一个阶段是interest,具体可分为:
interest 8/8,每次对8个bit进替换,按照每8个bit的步长从头开始,即对文件的每个byte进行替换
interest 16/8,每次对16个bit进替换,按照每8个bit的步长从头开始,即对文件的每个word进行替换
interest 32/8,每次对32个bit进替换,按照每8个bit的步长从头开始,即对文件的每个dword进行替换
而用于替换的”interesting values”,是AFL预设的一些比较特殊的数,这些数的定义在config.h文件中
1
2
3static s8 interesting_8[] = { INTERESTING_8 };
static s16 interesting_16[] = { INTERESTING_8, INTERESTING_16 };
static s32 interesting_32[] = { INTERESTING_8, INTERESTING_16, INTERESTING_32 };与之前类似,effector map仍然会用于判断是否需要变异;此外,如果某个interesting value,是可以通过bitflip或者arithmetic变异达到,那么这样的重复性变异也是会跳过的。
DICTIONARY STUFF
进入到这个阶段,就接近deterministic fuzzing的尾声了。具体有以下子阶段:user extras(over),从头开始,将用户提供的tokens依次替换到原文件中,stage_max为
extras_cnt * len
user extras(insert),从头开始,将用户提供的tokens依次插入到原文件中,stage_max为
extras_cnt * len
auto extras(over),从头开始,将自动检测的tokens依次替换到原文件中,stage_max为
MIN(a_extras_cnt, USE_AUTO_EXTRAS) * len
其中,用户提供的tokens,是在词典文件中设置并通过-x选项指定的,如果没有则跳过相应的子阶段。
RANDOM HAVOC
对于非dumb mode的主fuzzer来说,完成了上述deterministic fuzzing后,便进入了充满随机性的这一阶段;对于dumb mode或者从fuzzer来说,则是直接从这一阶段开始。
havoc,顾名思义,是充满了各种随机生成的变异,是对原文件的“大破坏”。具体来说,havoc包含了对原文件的多轮变异,每一轮都是将多种方式组合(stacked)而成:
随机选取某个bit进行翻转
随机选取某个byte,将其设置为随机的interesting value
随机选取某个word,并随机选取大、小端序,将其设置为随机的interesting value
随机选取某个dword,并随机选取大、小端序,将其设置为随机的interesting value
随机选取某个byte,对其减去一个随机数
随机选取某个byte,对其加上一个随机数
随机选取某个word,并随机选取大、小端序,对其减去一个随机数
随机选取某个word,并随机选取大、小端序,对其加上一个随机数
随机选取某个dword,并随机选取大、小端序,对其减去一个随机数
随机选取某个dword,并随机选取大、小端序,对其加上一个随机数
随机选取某个byte,将其设置为随机数
随机删除一段bytes
随机选取一个位置,插入一段随机长度的内容,其中75%的概率是插入原文中随机位置的内容,25%的概率是插入一段随机选取的数
随机选取一个位置,替换为一段随机长度的内容,其中75%的概率是替换成原文中随机位置的内容,25%的概率是替换成一段随机选取的数
随机选取一个位置,用随机选取的token(用户提供的或自动生成的)替换
随机选取一个位置,用随机选取的token(用户提供的或自动生成的)插入
怎么样,看完上面这么多的“随机”,有没有觉得晕?还没完,AFL会生成一个随机数,作为变异组合的数量,并根据这个数量,每次从上面那些方式中随机选取一个(可以参考高中数学的有放回摸球),依次作用到文件上。如此这般丧心病狂的变异,原文件就大概率面目全非了,而这么多的随机性,也就成了fuzzing过程中的不可控因素,即所谓的“看天吃饭”了。
splice
设置ret_val的值为0
如果queue_cur通过了评估,且was_fuzzed字段是0,就设置
queue_cur->was_fuzzed
为1,然后pending_not_fuzzed计数器减一如果queue_cur是favored, pending_favored计数器减一。
sync_fuzzers(char **argv)
这个函数其实就是读取其他sync文件夹下的queue文件,然后保存到自己的queue里。
打开
sync_dir
文件夹while循环读取该文件夹下的目录和文件
while ((sd_ent = readdir(sd)))
- 跳过
.
开头的文件和sync_id
即我们自己的输出文件夹 - 读取
out_dir/.synced/sd_ent->d_name
文件即id_fd
里的前4个字节到min_accept
里,设置next_min_accept
为min_accept
,这个值代表之前从这个文件夹里读取到的最后一个queue的id。 - 设置stage_name为
sprintf(stage_tmp, "sync %u", ++sync_cnt);
,设置stage_cur为0,stage_max为0 - 循环读取
sync_dir/sd_ent->d_name/queue
文件夹里的目录和文件- 同样跳过
.
开头的文件和标识小于min_accept的文件,因为这些文件应该已经被sync过了。 - 如果标识
syncing_case
大于等于next_min_accept,就设置next_min_accept为syncing_case + 1
- 开始同步这个case
- 如果case大小为0或者大于MAX_FILE(默认是1M),就不进行sync。
- 否则mmap这个文件到内存mem里,然后
write_to_testcase(mem, st.st_size)
,并run_target,然后通过save_if_interesting来决定是否要导入这个文件到自己的queue里,如果发现了新的path,就导入。- 设置syncing_party的值为
sd_ent->d_name
- 如果save_if_interesting返回1,queued_imported计数器就加1
- 设置syncing_party的值为
- stage_cur计数器加一,如果stage_cur是stats_update_freq的倍数,就刷新一次展示界面。
- 同样跳过
- 向id_fd写入当前的
next_min_accept
值
- 跳过
总结来说,这个函数就是先读取有哪些fuzzer文件夹,然后读取其他fuzzer文件夹下的queue文件夹里的case,并依次执行,如果发现了新path,就保存到自己的queue文件夹里,而且将最后一个sync的case id写入到
.synced/其他fuzzer文件夹名
文件里,以避免重复运行。
trim_case(char **argv, struct queue_entry *q, u8 *in_buf)
- 如果这个case的大小len小于5字节,就直接返回
- 设置stage_name的值为tmp,在bytes_trim_in的值里加上len,bytes_trim_in代表被trim过的字节数
- 计算len_p2,其值是大于等于q->len的第一个2的幂次。(eg.如果len是5704,那么len_p2就是8192)
- 取
len_p2的1/16
为remove_len,这是起始步长。 - 进入while循环,终止条件是remove_len小于终止步长
len_p2的1/1024
,每轮循环步长会除2.- 设置remove_pos的值为remove_len
- 读入
"trim %s/%s", DI(remove_len), DI(remove_len)
到tmp中, 即stage_name = “trim 512/512” - 设置stage_cur为0,stage_max为
q->len / remove_len
- 进入while循环,
remove_pos < q->len
,即每次前进remove_len个步长,直到整个文件都被遍历完为止。- 由in_buf中remove_pos处开始,向后跳过remove_len个字节,写入到
.cur_input
里,然后运行一次fault = run_target
,trim_execs计数器加一 - 由所得trace_bits计算出一个cksum,和
q->exec_cksum
比较- 如果相等
- 从
q->len
中减去remove_len个字节,并由此重新计算出一个len_p2
,这里注意一下while (remove_len >= MAX(len_p2 / TRIM_END_STEPS, TRIM_MIN_BYTES))
- 将
in_buf+remove_pos+remove_len
到最后的字节,前移到in_buf+remove_pos
处,等于删除了remove_pos向后的remove_len个字节。 - 如果needs_write为0,则设置其为1,并保存当前trace_bits到clean_trace中。
- 从
- 如果不相等
- remove_pos加上remove_len,即前移remove_len个字节。注意,如果相等,就无需前移
- 如果相等
- 注意trim过程可能比较慢,所以每执行stats_update_freq次,就刷新一次显示界面
show_stats
- stage_cur加一
- 由in_buf中remove_pos处开始,向后跳过remove_len个字节,写入到
- 如果needs_write为1
- 删除原来的q->fname,创建一个新的q->fname,将in_buf里的内容写入,然后用clean_trace恢复trace_bits的值。
- 进行一次update_bitmap_score
- 返回fault
u32 calculate_score(struct queue_entry *q)
根据queue entry的执行速度、覆盖到的path数和路径深度来评估出一个得分,这个得分perf_score在后面havoc的时候使用。
前面的没什么好说的,这里的q->depth
解释一下,它在每次add_to_queue的时候,会设置为cur_depth+1
,而cur_depth是一个全局变量,一开始的初始值为0。
- 处理输入时
- 在read_testcases的时候会调用add_to_queue,此时所有的input case的queue depth都会被设置为1。
- fuzz_one时
- 然后在后面fuzz_one的时候,会先设置cur_depth为当前queue的depth,然后这个queue经过mutate之后调用save_if_interesting,如果是interesting case,就会被add_to_queue,此时就建立起了queue之间的关联关系,所以由当前queue变异加入的新queue,深度都在当前queue的基础上再增加。
u8 common_fuzz_stuff(char **argv, u8 *out_buf, u32 len)
简单的说就是写入文件并执行,然后处理结果,如果出现错误,就返回1.
- 如果定义了
post_handler
,就通过out_buf = post_handler(out_buf, &len)
处理一下out_buf,如果out_buf或者len有一个为0,则直接返回0- 这里其实很有价值,尤其是如果需要对变异完的queue,做一层wrapper再写入的时候。
- write_to_testcase(out_buf, len)
- fault = run_target(argv, exec_tmout)
- 如果fault是FAULT_TMOUT
- 如果
subseq_tmouts++ > TMOUT_LIMIT
(默认250),就将cur_skipped_paths加一,直接返回1 - subseq_tmout是连续超时数
- 如果
- 否则设置subseq_tmouts为0
- 如果skip_requested为1
- 设置skip_requested为0,然后将cur_skipped_paths加一,直接返回1
- queued_discovered += save_if_interesting(argv, out_buf, len, fault),即如果发现了新的路径才会加一。
- 如果stage_cur除以stats_update_freq余数是0,或者其加一等于stage_max,就更新展示界面
show_stats
- 返回0
void write_to_testcase(void *mem, u32 len)
将从mem
中读取len
个字节,写入到.cur_input
中
u8 save_if_interesting(char **argv, void *mem, u32 len, u8 fault)
检查这个case的执行结果是否是interesting的,决定是否保存或跳过。如果保存了这个case,则返回1,否则返回0
以下分析不包括crash_mode,暂时略过以简洁
- 设置keeping等于0
hnb = has_new_bits(virgin_bits)
,如果没有新的path发现或者path命中次数相同,就直接返回0- 否则,将case保存到
fn = alloc_printf("%s/queue/id:%06u,%s", out_dir, queued_paths, describe_op(hnb))
文件里 add_to_queue(fn, len, 0);
将其添加到队列里- 如果hnb的值是2,代表发现了新path,设置刚刚加入到队列里的queue的has_new_cov字段为1,即
queue_top->has_new_cov = 1
,然后queued_with_cov计数器加一 - 保存hash到其exec_cksum
- 评估这个queue,
calibrate_case(argv, queue_top, mem, queue_cycle - 1, 0)
- 设置keeping值为1.
- 根据fault结果进入不同的分支
- FAULT_TMOUT
- 设置total_tmouts计数器加一
- 如果unique_hangs的个数超过能保存的最大数量
KEEP_UNIQUE_HANG
,就直接返回keeping的值 - 如果不是dumb mode,就
simplify_trace((u64 *) trace_bits)
进行规整。 - 如果没有发现新的超时路径,就直接返回keeping
- 否则,代表发现了新的超时路径,unique_tmouts计数器加一
- 如果hang_tmout大于exec_tmout,则以hang_tmout为timeout,重新执行一次runt_target
- 如果结果为
FAULT_CRASH
,就跳转到keep_as_crash - 如果结果不是
FAULT_TMOUT
,就返回keeping,否则就使unique_hangs
计数器加一,然后更新last_hang_time的值,并保存到alloc_printf("%s/hangs/id:%06llu,%s", out_dir, unique_hangs, describe_op(0))
文件。
- 如果结果为
- FAULT_CRASH
- total_crashes计数器加一
- 如果unique_crashes大于能保存的最大数量
KEEP_UNIQUE_CRASH
即5000,就直接返回keeping的值 - 同理,如果不是dumb mode,就
simplify_trace((u64 *) trace_bits)
进行规整 - 如果没有发现新的crash路径,就直接返回keeping
- 否则,代表发现了新的crash路径,unique_crashes计数器加一,并将结果保存到
alloc_printf("%s/crashes/id:%06llu,sig:%02u,%s", out_dir,unique_crashes, kill_signal, describe_op(0))
文件。 - 更新last_crash_time和last_crash_execs
- FAULT_ERROR
- 抛出异常
- 对于其他情况,直接返回keeping
- FAULT_TMOUT
simplify_trace(u64 *mem)
- 按8个字节为一组循环读入,直到完全读取完mem
- 如果mem不为空
- i从0-7,
mem8[i] = simplify_lookup[mem8[i]]
,代表规整该路径的命中次数到指令值,这个路径如果没有命中,就设置为1,如果命中了,就设置为128,即二进制的1000 0000
- i从0-7,
- 否则设置mem为
0x0101010101010101ULL
,即代表这8个字节代表的path都没有命中,每个字节的值被置为1。1
2
3
4
5
6static const u8 simplify_lookup[256] = {
[0] = 1,
[1 ... 255] = 128
};
- 如果mem不为空
通信和覆盖率信息的记录
关键变量和常量
1 | .bss:000000000060208F unk_60208F db ? ; ; DATA XREF: deregister_tm_clones↑o |
- __afl_area_ptr
- 存储共享内存的首地址
- __afl_prev_loc
- 存储上一个位置,即上一次R(MAP_SIZE)生成的随机数的值
- __afl_fork_pid
- 存储fork出来的子进程的pid
- __afl_temp
- 临时buffer
- _AFL_SHM_ENV
- 申请的共享内存的shm_id被设置为环境变量
__AFL_SHM_ID
的值,所以通过这个环境变量来获取shm_id,然后进一步得到共享内存。
- 申请的共享内存的shm_id被设置为环境变量
trampoline_fmt_64
1 | .text:00000000004009C0 lea rsp, [rsp-98h] |
插入的trampoline_fmt_64只有在mov rcx, xxx
这里不同,其xxx的取值就是随机数R(MAP_SIZE),以此来标识与区分每个分支点,然后传入__afl_maybe_log
作为第二个参数调用这个函数。
__afl_maybe_log
直接看汇编,还是很好理解的
- 首先检查
_afl_area_ptr
是否为0,即是否共享内存已经被设置了。换句话说,只有第一个__afl_maybe_log会执行这个if里的代码- 如果
_afl_area_ptr
为0,即共享内存还没被设置,则判断_afl_setup_failure
是否为真,如果为真,则代表setup失败,直接返回。- 读取
_afl_global_area_ptr
的值- 如果不为0,则赋值给
_afl_area_ptr
- 否则
- 首先读取环境变量
__AFL_SHM_ID
,默认是个字符串,atoi转一下,得到shm_id,然后通过shmat启动对该共享内存的访问,并把共享内存连接到当前进程的地址空间,将得到的地址,保存到_afl_area_ptr
和_afl_global_area_ptr
中。 - 然后通过
FORKSRV_FD+1
即199这个文件描述符,向状态管道中写入4个字节的值,用来告知afl fuzz,fork server成功启动,等待下一步指示。 - 进入
__afl_fork_wait_loop
循环,从FORKSRV
即198中读取字节到_afl_temp
,直到读取到4个字节,这代表afl fuzz命令我们新建进程执行一次测试。- fork出子进程,原来的父进程充当fork server来和fuzz进行通信,而子进程则继续执行target。
- 父进程即fork server将子进程的pid写入到状态管道,告知fuzz。
- 然后父进程即fork server等待子进程结束,并保存其执行结果到
_afl_temp
中,然后将子进程的执行结果,从_afl_temp
写入到状态管道,告知fuzz。 - 父进程不断轮询
__afl_fork_wait_loop
循环,不断从控制管道读取,直到fuzz端命令fork server进行新一轮测试。
- 首先读取环境变量
- 如果不为0,则赋值给
- 读取
- 如果
- 如果
_afl_area_ptr
不为0,即共享内存已经被设置好了。那么就跳过上面的if,只执行__afl_store
逻辑,伪代码如下:- 简单的说,就是将上一个桩点的值(prev_location)和当前桩点的值(
R(MAP_SIZE)
)异或,取值后,使得共享内存里对应的槽的值加一,然后将prev_location设置为cur_location >> 1;
- 因此,AFL为每个代码块生成一个随机数,作为其“位置”的记录;随后,对分支处的”源位置“和”目标位置“进行异或,并将异或的结果作为该分支的key,保存每个分支的执行次数。用于保存执行次数的实际上是一个哈希表,大小为MAP_SIZE=64K,当然会存在碰撞的问题;但根据AFL文档中的介绍,对于不是很复杂的目标,碰撞概率还是可以接受的。
1
2
3cur_location = <COMPILE_TIME_RANDOM>;
shared_mem[cur_location ^ prev_location]++;
prev_location = cur_location >> 1; - 另外,比较有意思的是,AFL需要将cur_location右移1位后,再保存到prev_location中。官方文档中解释了这样做的原因。假设target中存在A->A和B->B这样两个跳转,如果不右移,那么这两个分支对应的异或后的key都是0,从而无法区分;另一个例子是A->B和B->A,如果不右移,这两个分支对应的异或后的key也是相同的。
- 简单的说,就是将上一个桩点的值(prev_location)和当前桩点的值(
1 | char __usercall _afl_maybe_log@<al>(char a1@<of>, __int64 a2@<rcx>, __int64 a3@<xmm0>, __int64 a4@<xmm1>, __int64 a5@<xmm2>, __int64 a6@<xmm3>, __int64 a7@<xmm4>, __int64 a8@<xmm5>, __int64 a9@<xmm6>, __int64 a10@<xmm7>, __int64 a11@<xmm8>, __int64 a12@<xmm9>, __int64 a13@<xmm10>, __int64 a14@<xmm11>, __int64 a15@<xmm12>, __int64 a16@<xmm13>, __int64 a17@<xmm14>, __int64 a18@<xmm15>) |
其他
strrchr
char *strrchr(const char *str, int c)
在参数 str 所指向的字符串中搜索最后一次出现字符 c(一个无符号字符)的位置。
strlen
unsigned int strlen (char *s)
用来计算指定的字符串s的长度,不包括结束字符”\0”。- 注意:strlen() 函数计算的是字符串的实际长度,遇到第一个’\0’结束。如果你只定义没有给它赋初值,这个结果是不定的,它会从首地址一直找下去,直到遇到’\0’停止。而sizeof返回的是变量声明后所占的内存数,不是实际长度,此外sizeof不是函数,仅仅是一个操作符,strlen()是函数。
DFL_ck_strdup
- Create a buffer with a copy of a string. Returns NULL for NULL inputs.
size = strlen((char*)str) + 1;
1
2
3
4ALLOC_MAGIC_C1-> 00 ff 00 ff size-> 2e 00 00 00 ret-> 2f 55 73 65 72 73 2f 73 │ ····.···/Users/s │
61 6b 75 72 61 2f 67 69 74 73 6f 75 72 63 65 2f │ akura/gitsource/ │
41 46 4c 2f 63 6d 61 6b 65 2d 62 75 69 6c 64 2d │ AFL/cmake-build- │
64 65 62 75 67 00 ALLOC_MAGIC_C2-> f0 00 00 00 00 00 00 00 00 00 │ debug··········· │
snprintf()
int snprintf(char *str, int n, char * format [, argument, ...]);
函数用于将格式化的数据写入字符串- str为要写入的字符串;n为要写入的字符的最大数目,超过n会被截断;format为格式化字符串,与printf()函数相同;argument为变量。
- http://brg-liuwei.github.io/tech/2014/09/29/snprintf.html
- 重点理解snprintf函数的返回值,不是实际打印出来的长度,而是实际应该打印的长度。
- https://forcemz.net/cxx/2019/04/29/StringFormattingTalk/
- snprintf的可能的一种实现,及其可能存在的安全问题。
DFL_ck_alloc
- Allocate a buffer, returning zeroed memory.
- DFL_ck_alloc_nozero
1
2
3
400 ff 00 ff 35 00 00 00 00 00 00 00 00 00 00 00 │ ····5··········· │
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 │ ················ │
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 │ ················ │
00 00 00 00 00 00 00 00 00 00 00 00 00 f0 00 00 │ ················ │
- DFL_ck_alloc_nozero
- Allocate a buffer, returning zeroed memory.
alloc_printf
- User-facing macro to sprintf() to a dynamically allocated buffer
- ck_alloc:分配内存
- snprintf:写入格式化字符串
1
2
3
400 ff 00 ff 35 00 00 00 2f 55 73 65 72 73 2f 73 │ ····5···/Users/s │
61 6b 75 72 61 2f 67 69 74 73 6f 75 72 63 65 2f │ akura/gitsource/ │
41 46 4c 2f 63 6d 61 6b 65 2d 62 75 69 6c 64 2d │ AFL/cmake-build- │
64 65 62 75 67 2f 61 66 6c 2d 61 73 00 f0 00 00 │ debug/afl-as···· │
- User-facing macro to sprintf() to a dynamically allocated buffer
access
int access(const char * pathname, int mode)
检查调用进程是否可以对指定的文件执行某种操作。- 成功执行时,返回0。失败返回-1,errno被设为以下的某个值
strncmp
int strncmp ( const char * str1, const char * str2, size_t n );
若str1与str2的前n个字符相同,则返回0;若s1大于s2,则返回大于0的值;若s1 若小于s2,则返回小于0的值。strcmp
int strcmp(const char *s1, const char *s2);
若参数s1 和s2 字符串相同则返回0。s1 若大于s2 则返回大于0 的值。s1 若小于s2 则返回小于0 的值。strstr
char *strstr(const char *haystack, const char *needle)
在字符串 haystack 中查找第一次出现字符串 needle 的位置,不包含终止符 ‘\0’。- 返回该函数返回在 haystack 中第一次出现 needle 字符串的位置,如果未找到则返回 null。
gettimeofday
int gettimeofday(struct timeval *tv, struct timezone *tz)
gettimeofday()会把目前的时间用tv结构体返回,当地时区的信息则放到tz所指的结构中。- timeval
1
2
3
4
5_STRUCT_TIMEVAL
{
__darwin_time_t tv_sec; /* seconds */
__darwin_suseconds_t tv_usec; /* and microseconds */
};
srandom
- 设置随机种子,注意只需要设置一次即可
- 1、生产随机数需要种子(Seed),且如果种子固定,random()每次运行生成的随机数(其实是伪随机数)也是固定的;因为返回的随机数是根据稳定的算法得出的稳定结果序列,并且Seed就是这个算法开始计算的第一个值。
- 2、srandom()可以设定种子,比如srandom(0) 、srandom(1)等等。如果srandom设定了一个固定的种子,那么random得出的随机数就是固定的;
如果程序运行时通过srandom(time(NULL))设定种子为随机的,那么random()每次生成的随机数就是非固定的了。
open
- open函数的简要介绍
- open函数返回值
- 调用成功时返回一个文件描述符fd,调用失败时返回-1,并修改errno
fdopen
FILE * fdopen(int fildes, const char * mode);
fdopen()会将参数fildes 的文件描述词, 转换为对应的文件指针后返回.- 参数mode 字符串则代表着文件指针的流形态, 此形态必须和原先文件描述词读写模式相同. 关于mode字符串格式请参考fopen().
- 返回值:转换成功时返回指向该流的文件指针. 失败则返回NULL, 并把错误代码存在errno中.
fgets
char *fgets(char *str, int size, FILE *stream)
从指定的流 stream 读取一行,并把它存储在 str 所指向的字符串内。当读取 (size-1) 个字符时,或者读取到换行符时,或者到达文件末尾时,它会停止,具体视情况而定。- string为一个字符数组,用来保存读取到的字符。
- size为要读取的字符的个数。如果该行字符数大于size-1,则读到size-1个字符时结束,并在最后补充’\0’;如果该行字符数小于等于size-1,则读取所有字符,并在最后补充’\0’。即,每次最多读取size-1个字符。
- stream为文件流指针。
- 【返回值】读取成功,返回读取到的字符串,即string;失败或读到文件结尾返回NULL。因此我们不能直接通过fgets()的返回值来判断函数是否是出错而终止的,应该借助feof()函数或者ferror()函数来判断。
fopen
FILE * fopen(const char * path, const char * mode);
打开一个文件并返回文件指针- fopen参数详解
atexit
int atexit (void (*function) (void));
atexit()用来设置一个程序正常结束前调用的函数. 当程序通过调用exit()或从main中返回时, 参数function所指定的函数会先被调用, 然后才真正由exit()结束程序.- 如果执行成功则返回0, 否则返回-1, 失败原因存于errno 中.
mkdir
int mkdir(const char *pathname, mode_t mode);
mkdir()函数以mode方式创建一个以pathname为名字的目录,mode定义所创建目录的权限- 返回值: 0:目录创建成功 -1:创建失败
flock
int flock(int fd,int operation);
flock()会依参数operation所指定的方式对参数fd所指的文件做各种锁定或解除锁定的动作。此函数只能锁定整个文件,无法锁定文件的某一区域。- LOCK_SH 建立共享锁定。多个进程可同时对同一个文件作共享锁定。
- LOCK_EX 建立互斥锁定。一个文件同时只有一个互斥锁定。
- LOCK_UN 解除文件锁定状态。
- LOCK_NB 无法建立锁定时,此操作可不被阻断,马上返回进程。通常与LOCK_SH或LOCK_EX 做OR(|)组合。
- 单一文件无法同时建立共享锁定和互斥锁定,而当使用dup()或fork()时文件描述词不会继承此种锁定。
- 返回值 返回0表示成功,若有错误则返回-1,错误代码存于errno。
scandir
int scandir(const char *dir,struct dirent **namelist,int (*filter)(const void *b),int ( * compare )( const struct dirent **, const struct dirent ** ) );
int alphasort(const void *a, const void *b);
int versionsort(const void *a, const void *b);
- 函数scandir扫描dir目录下(不包括子目录)满足filter过滤模式的文件,返回的结果是compare函数经过排序的,并保存在namelist中。注意namelist的元素是通过malloc动态分配内存的,所以在使用时要注意释放内存。alphasort和versionsort是使用到的两种排序的函数。
- 当函数成功执行时返回找到匹配模式文件的个数,如果失败将返回-1。
lstat
int lstat (const char * file_name, struct stat * buf);
- 函数说明:lstat()与stat()作用完全相同, 都是取得参数file_name 所指的文件状态, 其差别在于, 当文件为符号连接时, lstat()会返回该link 本身的状态. 详细内容请参考stat().
- 返回值:执行成功则返回0, 失败返回-1, 错误代码存于errno.
read
size_t read(int fd, void * buf, size_t count);
read()会把参数fd所指的文件传送count个字节到buf指针所指的内存中. 若参数count为0, 则read()不会有作用并返回0.- 返回值为实际读取到的字节数, 如果返回0, 表示已到达文件尾或是无可读取的数据,此外文件读写位置会随读取到的字节移动.
- 如果顺利,read()会返回实际读到的字节数, 最好能将返回值与参数count作比较, 若返回的字节数比要求读取的字节数少, 则有可能读到了文件尾
- 当有错误发生时则返回-1, 错误代码存入errno中, 而文件读写位置则无法预期.
sscanf
int sscanf(const char *str, const char *format, ...)
从字符串读取格式化输入。- 如果成功,该函数返回成功匹配和赋值的个数。如果到达文件末尾或发生读错误,则返回EOF。
- 例子
1
2
3
4
5
6strcpy( dtm, "Saturday March 25 1989" );
sscanf( dtm, "%s %s %d %d", weekday, month, &day, &year );
printf("%s %d, %d = %s\n", month, day, year, weekday )
...
March 25, 1989 = Saturday
link
int link (const char * oldpath, const char * newpath);
- link()以参数newpath指定的名称来建立一个新的连接(硬连接)到参数oldpath所指定的已存在文件. 如果参数newpath指定的名称为一已存在的文件则不会建立连接.
- 返回值:成功则返回0, 失败返回-1, 错误原因存于errno.
rmdir
int rmdir(const char *pathname);
rmdir函数用于删除一个空目录。getcwd
char * getcwd(char * buf, size_t size);
getcwd()会将当前的工作目录绝对路径复制到参数buf所指的内存空间,参数size为buf的空间大小。unlink
int unlink(const char * pathname)
unlink()会删除参数pathname 指定的文件. 如果该文件名为最后连接点, 但有其他进程打开了此文件, 则在所有关于此文件的文件描述词皆关闭后才会删除. 如果参数pathname 为一符号连接, 则此连接会被删除。pipe
int pipe(int fd[2])
创建一个简单的管道,若成功则为数组fd分配两个文件描述符,其中fd[0]用于读取管道,fd[1]用于写入管道- 若成功则返回零,否则返回-1,错误原因存于errno中。
- 管道,顾名思义,当我们希望将两个进程的数据连接起来的时候就可以使用它,从而将一个进程的输出数据作为另一个进程的输入数据达到通信交流的目的
setsid
子进程从父进程继承了:SessionID、进程组ID和打开的终端。子进程如果要脱离这些,代码中可通过调用setsid来实现。而命令行或脚本中可以通过使用命令setsid来运行程序实现。setsid帮助一个进程脱离从父进程继承而来的已打开的终端、隶属进程组和隶属的会话。
dup2
int dup2(int oldfd,int newfd);
- 复制一个现存的文件描述符。当调用dup函数时,内核在进程中创建一个新的文件描述符,此描述符是当前可用文件描述符的最小数值,这个文件描述符指向oldfd所拥有的文件表项。dup2和dup的区别就是可以用newfd参数指定新描述符的数值,如果newfd已经打开,则先将其关闭。如果newfd等于oldfd,则dup2返回newfd, 而不关闭它。
- dup2函数返回的新文件描述符同样与参数oldfd共享同一文件表项。
waitpid
pid_t waitpid(pid_t pid, int * status, int options);
waitpid()会暂时停止目前进程的执行, 直到有信号来到或子进程结束. 如果在调用wait()时子进程已经结束, 则wait()会立即返回子进程结束状态值. 子进程的结束状态值会由参数status返回, 而子进程的进程识别码也会一块返回. 如果不在意结束状态值, 则参数status可以设成NULL. 参数pid为欲等待的子进程识别码。- 返回值:如果执行成功则返回子进程识别码(PID), 如果有错误发生则返回-1. 失败原因存于errno中.
setitimer
mmap
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset)
该函数主要用途有三个:- 将普通文件映射到内存中,通常在需要对文件进行频繁读写时使用,用内存读写取代I/O读写,以获得较高的性能;
- addr
- 指向欲映射的内存起始地址,通常设为NULL,代表让系统自动选定地址,映射成功后返回该地址。
- length
- 代表将文件中多大的部分映射到内存。
- prot
- PROT_EXEC 映射区域可被执行
- PROT_READ 映射区域可被读取
- PROT_WRITE 映射区域可被写入
- PROT_NONE 映射区域不能存取
sprintf
int sprintf(char *string, char *format [,argument,...]);
- 把格式化的数据写入某个字符串中,即发送格式化输出到string所指向的字符串
ftruncate
int ftruncate(int fd, off_t length)
ftruncate()会将参数fd指定的文件大小改为参数length指定的大小。参数fd为已打开的文件描述词,而且必须是以写入模式打开的文件。如果原来的文件件大小比参数length大,则超过的部分会被删去lseek
off_t lseek(int fildes, off_t offset, int whence);
每一个已打开的文件都有一个读写位置, 当打开文件时通常其读写位置是指向文件开头, 若是以附加的方式打开文件(如O_APPEND), 则读写位置会指向文件尾. 当read()或write()时, 读写位置会随之增加,lseek()便是用来控制该文件的读写位置. 参数fildes 为已打开的文件描述词, 参数offset 为根据参数whence来移动读写位置的位移数.- 参数 whence 为下列其中一种:
- SEEK_SET 参数offset 即为新的读写位置.
- SEEK_CUR 以目前的读写位置往后增加offset 个位移量.
- SEEK_END 将读写位置指向文件尾后再增加offset 个位移量. 当whence 值为SEEK_CUR 或
- SEEK_END 时, 参数offet 允许负值的出现.
- 下列是特别的使用方式:
1) 欲将读写位置移到文件开头时:lseek(int fildes, 0, SEEK_SET);
2) 欲将读写位置移到文件尾时:lseek(int fildes, 0, SEEK_END);
3) 想要取得目前文件位置时:lseek(int fildes, 0, SEEK_CUR); - 返回值:当调用成功时则返回目前的读写位置, 也就是距离文件开头多少个字节. 若有错误则返回-1, errno 会存放错误代码.
readdir
- readdir()返回参数dir 目录流的下个目录进入点
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
main()
{
DIR * dir;
struct dirent * ptr;
int i;
dir = opendir("/etc/rc.d");
while((ptr = readdir(dir)) != NULL)
{
printf("d_name : %s\n", ptr->d_name);
}
closedir(dir);
}
执行:
d_name : .
d_name : ..
d_name : init.d
d_name : rc0.d
d_name : rc1.d
d_name : rc2.d
d_name : rc3.d
d_name : rc4.d
d_name : rc5.d
d_name : rc6.d
d_name : rc
d_name : rc.local
d_name : rc.sysinit
- readdir()返回参数dir 目录流的下个目录进入点
参考资料
https://forcemz.net/cxx/2019/04/29/StringFormattingTalk/
http://rk700.github.io/2017/12/28/afl-internals/
http://rk700.github.io/2018/01/04/afl-mutations/
other
也public到了安全客,可以在我的个人主页查看
https://www.anquanke.com/member/133369欢迎加入我的知识星球天问之路,可以获取带目录的pdf版,以及有什么问题可以提问我。