感谢大佬们的文章:
https://bbs.pediy.com/thread-265973.htm
https://eternalsakura13.com/2020/08/23/afl/#more
这篇记录 afl-as.c 的代码
afl-as.c
首先,需要知道的是我们平常在用gcc时,其实是经过了预处理、编译、汇编、链接这几个阶段的。汇编就是将汇编代码转换为二进制代码的过程,这需要一个汇编器来完成,Linux常用的汇编器是as。
插桩过程:先调用afl-gcc,这是对gcc的一个简单封装,其中有一项工作是查找AFL文件夹下的as,这个as是个假的as,是指向afl-as的符号链接(软连接),并且-B 指定优先执行这个假的as。所以经过预处理和编译两个阶段后,在汇编阶段实际上是执行了afl-as(as的封装),在afl-as中封装了插桩功能,在执行真正的as命令前会将插桩好的汇编程序传给真正的as。
1. main主函数
-
获取环境变量 AFL_INST_RATIO ,赋值给
inst_ratio_str
,该变量表示插桩密度 -
时间和时区结构体
-
获取__AFL_CLANG_MODE环境变量并归一化 ,赋值给
clang_mode
-
如果没有AFL_QUIET 环境变量,打印信息
-
有AFL_QUIET环境变量, 置 be_quiet = 1
-
如果传入参数少于2,打印信息并退出
-
获取时间和时区
-
设置随即种子
rand_seed
,秒为单位的时间异或毫秒为单位的时间再异或进程ID -
调用
edit_params
函数进行参数处理 -
打印出参数来看一下
// 输入 afl-gcc test.c -o a.out // 输出 arg0: as arg1: --64 arg2: -o arg3: /tmp/cc1Twew.o arg4: /tmp/.afl-33240-1653576534.s arg5: (null) arg6: (null) arg7: (null)
-
如果
inst_ratio_str
不为0- 如果输入格式不正确,或是大于100,抛出异常。就是检测
inst_ratio_str
的值是否合法范围内
- 如果输入格式不正确,或是大于100,抛出异常。就是检测
-
如果获取到环境变量 __AFL_AS_LOOPCHECK,抛出异常。去掉传入参数中的 .
-
设置环境变量 __AFL_AS_LOOPCHECK 为1
-
读取环境变量AFL_USE_ASAN和AFL_USE_MSAN的值,如果其中有一个为1
- sanitizer = 1
- inst_ratio 除以 3,该变量表示插桩密度,通常情况下是100%。这是因为在进行ASAN的编译时,AFL无法识别出ASAN特定的分支,导致插入很多无意义的桩代码,所以直接暴力地将插桩概率/3
-
调用
add_instrumentation
函数进行插桩 -
fork一个子进程调用execvp,进行编译。这其实是因为我们的execvp执行的时候,会用
as_params[0]
来完全替换掉当前进程空间中的程序,如果不通过子进程来执行实际的as,那么后续就无法在执行完实际的as之后,还能unlink掉modified_file -
如果pid < 0 ,fork失败
-
调用
waitpid
等待子进程结束,status中存放导致子进程返回的状态信息 -
如果没有 AFL_KEEP_ASSEMBLY 环境变量,删除文件
2. edit_params() 处理参数
- 获取环境变量 TMPDIR ,赋值给
tmp_dir
,获取环境变量 AFL_AS ,赋值给afl_as
- 没有环境变量 TMPDIR,查看是否有环境变量 TEMP,没有则查看环境变量 TMP,还没有就将
tmp_dir
赋值为 “/tmp” - 为
cc_params
分配空间 - 如果afl_as不为空,就设置
as_params[0]
为afl_as
,否则设置为as
- as_params[argc] = 0; 最后一个参数设置为0(没看懂)
- 进入循环,从argv[1]遍历到
argv[argc-2]
,放入cc_params
数组中。如果传入的参数中有 --64,置use_64bit
= 1,如果传入的参数中有 --32置use_64bit
= 0。 input_file
为 argv的最后一个参数、- 如果
input_file
的第一个字符为 ‘-’- 如果
input_file
为”-version“- 置
just_version
为1 - modified_file = input_file
- 跳转到wrap_things_up
- 置
- 如果
input_file
不是”-version“,并且 - 后面还有其他值,则抛出异常,告知用户使用错误 - 否则
input_file = NULL
- 如果
- 如果
input_file
的第一个字符不为 ‘-’- 如果
input_file
的前几个字符与tmp_dir 不相同,并且input_file
的前9个字符与 /var/tmp/ 不相同,并且input_file
的前5个字符与 /tmp/ 不相同- 置
pass_thru
为1
- 置
- 如果
modified_file
置为 tmp_dir/afl-getpid()-(u32)time(NULL),注意这里只是给了个文件名,真正的文件会在add_instrumentation 中生成。cc_params
最后一个参数为modified_file- 设置结束标志
3. add_instrumentation 处理输入文件并插桩
- 如果
input_file
不为null- 只读方式打开
input_file
,fd赋值给inf
,如果打开失败,抛出异常
- 只读方式打开
- 如果
input_file
为null,inf
设置为标准输入 - 创建modified_file文件,fd赋值给
outfd
,如果创建失败,抛出异常 - 进一步验证该文件是否可写,不可写抛出异常
- 进入while循环,读取 inf 指向文件的每一行到 line 数组,每行最多 MAX_LINE = 8192个字节(含末尾的‘\0’)
- 如果
pass_thru
为0,并且skip_intel
为0,并且skip_app
为0,并且instr_ok
不为0,并且instrument_next
不为0,并且以’\t’ + 字母 开头(instr_ok
是一个flag,是否位于text段)- 向
outf
指向的文件(即modified_file
)中写入trampoline_fmt_64
桩代码并插入随机数 instrument_next
置0ins_lines
插桩行数加一
- 向
- 将原有汇编代码写入
outf
- 如果
pass_thru
为1,直接进入下一次循环 - 如果以 '\t.'开头
- 如果
clang_mode
为0,且instr_ok
不为0,且 \t. 后面跟的是 p2align 指令,且 p2align 后跟了一个数字,且后又跟了一个 ‘\n’(检查是否为 p2align 指令)- 则置
skip_next_label
为1
- 则置
- 如果line的值为
\t.[text\n|section\t.text|section\t__TEXT,__text|section __TEXT,__text]...
其中之一,则设置instr_ok
为1,直接continue(表示位于.text段,指令段。具体可阅读https://www.cnblogs.com/skullboyer/p/10979704.html) - 如果不是上面的几种情况,且line的值为
\t.[section\t|section |bss\n|data\n]...
,则设置instr_ok
为0,直接continue(表示不位于.text段,不执行插桩)
- 如果
- 如果line中包含 “.code”
- 如果包含的是 “.code32”,
skip_csect
置1,不插桩 - 如果包含的是 “.code64”,
skip_csect
置0。插桩
- 如果包含的是 “.code32”,
- 如果包含".intel_syntax"即intel汇编(一般为window),
skip_intel
置1,不插桩 - 如果包含".att_syntax"即att汇编(一般为Linux),
skip_intel
置0,插桩 - 如果line中第一个字符或第二个字符为 ‘#’(跳过ad-hoc __asm__块)
- 如果是"#APP",
skip_app
置1,不插桩 - 如果是"#NO_APP",
skip_app
置0,插桩
- 如果是"#APP",
- 如果
skip_intel
不为0,或skip_app
不为0,或skip_ceset
不为0,或instr_ok
为0,或第一个字符为’#‘,或第一个字符为’ ’- continue
- 如果第一个字符为 ‘\t’
- 如果第二个字符为 ‘j’,并且第三个字符不为 ‘m’(这是在判断是否是跳转指令,但是好像不包括无条件跳转jmp),且随机数小于
inst_ratio
插桩密度- 调用 fprintf 将桩代码以及随机数(块的标识符,应该会产生碰撞,即多个块的标识符相同)写入代码段中
ins_lines
插桩计数加1
- 否则,continue
- 如果第二个字符为 ‘j’,并且第三个字符不为 ‘m’(这是在判断是否是跳转指令,但是好像不包括无条件跳转jmp),且随机数小于
- 如果line中有’:’
- 如果第一个字符为’.’
- 如果第三个字符是数字(.L0,GCC的分支标志) 或者 clang模式并且第二到第四个字符为"LBB"(.LBB,clang的分支标志) 并且 随机数小于
inst_ratio
插桩密度- 如果
skip_next_label
不为0, 置instrument_next
= 1,表示要进行插桩 - 否则
skip_next_label
置0
- 如果
- 如果第三个字符是数字(.L0,GCC的分支标志) 或者 clang模式并且第二到第四个字符为"LBB"(.LBB,clang的分支标志) 并且 随机数小于
- 否则,即如果第一个字符不为’.'(说明是函数标志)
- 置
instrument_next
= 1
- 置
- 如果第一个字符为’.’
- 如果
- 如果
ins_lines
不为0- 将
main_payload_64
插入到代码最后
- 将
- 关闭资源
- 如果不是Quite 模式
- 如果没有插入任何桩代码,警告
- 否则,打印插桩信息
注:在while循环中总共有两处插桩的代码,while循环开头的是deferred mode (延迟模式)下的插桩代码,用于.Lfunc_begin0这样的插桩处理。另一处是正常逻辑的插桩代码。这两处插桩代码只是简单地将 保存寄存器状态、插入随机数、调用、恢复寄存器状态 这些操作插入到每个块中。可以注意到循环结束后,插入了main_payload_64
代码,这部分代码中定义了要调用的函数等更多操作。