首页 > 编程语言 >AFL源码分析(一)

AFL源码分析(一)

时间:2022-11-26 12:00:14浏览次数:73  
标签:AFL 分析 skip clang __ 源码 params && line

AFL源码分析(一)

文章首发于:ChaMd5公众号 ​​https://mp.weixin.qq.com/s/E-D_M25xv5gIpRa6k8xOvw​

a.alf-gcc.c

1.find_as

这个函数的功能是获取使用的汇编器。首先获取环境变量​​AFL_PATH​​​,如果这个环境变量存在的话,接着把他和​​/as​​​拼接,并判断次路径下的​​as​​​文件是否存在。如果存在,就使得​​as_path = afl_path = getenv("AFL_PATH")​​​。如果不存在就通过第二种方式尝试获取其路径。首先判断是否存在​​/​​​,并把最后一个​​/​​​之后的路径清空,之后为其前面的路径分配空间,并与​​/afl-as​​​拼接后判断这个文件是否存在,如果存在,则使得​​as_path = dir = ck_strdup(argv0)​​。如果这两种方式都不能找到相应路径,即会爆出异常。

2.edit_params

这个函数的主要功能是对编译所用到的参数进行编辑。先为​​cc_params​​​分配一大块内存空间,然后尝试获取​​argv[0]​​​的最后一个​​/​​​的位置,如果存在就把它后面的内容设为​​name​​​,否则​​name=argv[0]​​​。之后判断我们预期的编译是不是​​afl-clang​​​模式,如果是的话就设置​​clang_mode = 1​​​,设置环境变量​​CLANG_ENV_VAR​​​为 1,并添加相应的编译参数。如果不是​​clang​​​模式,则判断​​name​​​是否等于​​afl-g++,afl-gcj​​​等选项,并添加相应的参数。接着从​​argv[1]​​​开始遍历编译选项,会跳过​​-B -integrated-as -pipe​​​这三个选项,因为​​edit_params​​​会自动添加这三个编译选项。最后​​cc_params[cc_par_cnt] = NULL​​标志结束对选项的编辑。

3.main

int main(int argc, char** argv) {

if (isatty(2) && !getenv("AFL_QUIET")) {

SAYF(cCYA "afl-cc " cBRI VERSION cRST " by <[email protected]>\n");

} else be_quiet = 1;

if (argc < 2) {

SAYF("\n"
"This is a helper application for afl-fuzz. It serves as a drop-in replacement\n"
"for gcc or clang, letting you recompile third-party code with the required\n"
"runtime instrumentation. A common use pattern would be one of the following:\n\n"

" CC=%s/afl-gcc ./configure\n"
" CXX=%s/afl-g++ ./configure\n\n"

"You can specify custom next-stage toolchain via AFL_CC, AFL_CXX, and AFL_AS.\n"
"Setting AFL_HARDEN enables hardening optimizations in the compiled code.\n\n",
BIN_PATH, BIN_PATH);

exit(1);

}

find_as(argv[0]);

edit_params(argc, argv);

execvp(cc_params[0], (char**)cc_params);

FATAL("Oops, failed to execute '%s' - check your PATH", cc_params[0]);

return 0;

}

总体来说就是先调用​​find_as(argv[0])​​​获取使用的汇编器,再调用​​edit_params(argc, argv)​​​对编译选项进行编辑,再通过​​execvp​​​去进行编译。总的来说​​alf-gcc​​​是对​​gcc或clang​​​的一个​​wrapper​​​。而其中强制加上的​​-B as_path​​​实际上是给其指定汇编器,也就是我们下面会提到的​​afl-as​​​。实际的插桩也就是在​​afl-as​​里进行插桩的。

b.afl-as

1.edit_params

这个函数的主要功能是编辑汇编器所用到的参数。首先获取环境变量​​TMPDIR​​​和​​AFL_AS​​​。接着根据是否是​​clang​​​模式并且​​afl_as​​​是否为空,去判断是否要重新获取​​afl_as​​​的值,直到其不为空。接着获取​​tmp_dir​​​的值,直到其不为空。下面就是给​​as_params​​​分配一大块空间,并开始对参数进行编辑。首先先设置​​as_params[0]​​​,也即汇编器,一般来说这里都是​​as​​​。接着从​​argv[1]​​​遍历到​​argv[argc-1]​​​,看是否存在​​--64​​​,如果存在​​--64​​​就使得​​use_64bit = 1​​​。如果定义了​​__APPLE__​​​,那么如果存在​​-arch x86_64​​​就使得​​use_64bit = 1​​​。并且其会忽略​​-q或者-Q​​​选项。其余选项参数都会依此加到​​as_params[as_par_cnt++]​​​中。如果定义了​​__APPLE__​​​,接下来会判断是否是​​clang​​​模式,如果是那么添加​​-c -x assembler​​​的选项。紧接着把​​argv[argc - 1]​​​赋给 ​​input_file​​​,即最后一个参数的值为​​input_file​​​的值。下面会判断​​input_file​​​,是否与​​--version​​​相等,如果相等,标志​​just_version=1​​​,可能是代表查询版本。如果不等那么将​​input_file​​​与 ​​tmp_dir、/var/tmp/、/tmp/​​​进行比较,如果都不相同,则设置​​pass_thru = 1​​​。并通过格式化字符串设置​​modified_file = tmp_dir/.afl-getpid()-(u32)time(NULL).s​​​。最后设置​​as_params[as_par_cnt++] = modified_file​​​,并结束对​​as_params​​的编辑。

2.add_instrumentation

这个函数就是进行插桩的关键函数了。首先判断文件是否存在并且可读,不满足就抛出异常。然后打开​​modified_file​​​里的临时文件,获得其句柄​​outfd​​,再通过句柄拿到文件对应的指针。

if (input_file) {                                                         // 判断文件是否存在并可读

inf = fopen(input_file, "r");
if (!inf) PFATAL("Unable to read '%s'", input_file);

} else inf = stdin; // 文件不存在,则标准输入作为 input_file

outfd = open(modified_file, O_WRONLY | O_EXCL | O_CREAT, 0600); // 打开这个临时文件

if (outfd < 0) PFATAL("Unable to write to '%s'", modified_file);

outf = fdopen(outfd, "w");

if (!outf) PFATAL("fdopen() failed");

接下来就是插桩的关键部分了。

while (fgets(line, MAX_LINE, inf)) {                                      // 逐行从inf读取文件到line数组里

/* In some cases, we want to defer writing the instrumentation trampoline
until after all the labels, macros, comments, etc. If we're in this
mode, and if the line starts with a tab followed by a character, dump
the trampoline now. */

if (!pass_thru && !skip_intel && !skip_app && !skip_csect && instr_ok && // 判断是否满足插桩条件
instrument_next && line[0] == '\t' && isalpha(line[1])) {

fprintf(outf, use_64bit ? trampoline_fmt_64 : trampoline_fmt_32, // 根据use_64bit插入相应的插桩代码
R(MAP_SIZE));

instrument_next = 0;
ins_lines++;

}

首先是一个大​​while​​​循环,通过​​fgets​​​逐行从​​inf​​​读取文件到​​line​​​数组里,最多​​MAX_LINE​​​也即​​8192​​​字节。并且通过几个标记的值来判断是否要插入相应的代码。并且根据​​use_64bit​​​的值来确定插入的是​​trampoline_fmt_64​​​还是​​trampoline_fmt_32​​。

fputs(line, outf);                                                       // 把 line 写到 modified_file 里

if (pass_thru) continue;

/* All right, this is where the actual fun begins. For one, we only want to // 通过注释可以知道我们只对.text进行插桩
instrument the .text section. So, let's keep track of that in processed // 通过 instr_ok 来标记是否在 .text 段
files - and let's set instr_ok accordingly. */

if (line[0] == '\t' && line[1] == '.') {

/* OpenBSD puts jump tables directly inline with the code, which is
a bit annoying. They use a specific format of p2align directives
around them, so we use that as a signal. */

if (!clang_mode && instr_ok && !strncmp(line + 2, "p2align ", 8) &&
isdigit(line[10]) && line[11] == '\n') skip_next_label = 1;

if (!strncmp(line + 2, "text\n", 5) || // 如果 line 的值为 \t.text\n
!strncmp(line + 2, "section\t.text", 13) || // 或 \t.section\t.text
!strncmp(line + 2, "section\t__TEXT,__text", 21) || // 或 \t.section\t__TEXT,__text
!strncmp(line + 2, "section __TEXT,__text", 21)) { // 或 \t.section __TEXT,__text
instr_ok = 1; // 设置 instr_ok = 1,并跳转到开头读取下一行内容
continue;
}

if (!strncmp(line + 2, "section\t", 8) || // 如果 line 的值为 \t.section\t
!strncmp(line + 2, "section ", 8) || // 或 \t.section
!strncmp(line + 2, "bss\n", 4) || // 或 \tbss\n
!strncmp(line + 2, "data\n", 5)) { // 或 \tdata\n
instr_ok = 0; // 设置 instr_ok = 0,并跳转到开头读取下一行内容
continue;
}

}

我们会把​​line​​​里的值写道​​outf(即modified_file)​​​里。根据官方给的注释可以知道我们只期望对​​text段​​​进行插桩,并且通过设置​​instr_ok​​​来标记是否是​​text​​​段。如果​​line+2​​​匹配到​​\t.text\n、\t.section\t.text​​​等就设置​​instr_ok=1​​​,如果​​line+2​​​匹配到​​\t.section\t、\t.section​​​等就设置​​instr_ok=0​​。并跳过下面的代码,直接跳到循环的开头读取下一行的内容。

// 接下来设置一些其他的标志
/* Detect off-flavor assembly (rare, happens in gdb). When this is
encountered, we set skip_csect until the opposite directive is
seen, and we do not instrument. */

if (strstr(line, ".code")) { // 判断 off-flavor

if (strstr(line, ".code32")) skip_csect = use_64bit;
if (strstr(line, ".code64")) skip_csect = !use_64bit;

}

/* Detect syntax changes, as could happen with hand-written assembly.
Skip Intel blocks, resume instrumentation when back to AT&T. */

if (strstr(line, ".intel_syntax")) skip_intel = 1; // 跳过 Intel汇编的插桩
if (strstr(line, ".att_syntax")) skip_intel = 0;

/* Detect and skip ad-hoc __asm__ blocks, likewise skipping them. */

if (line[0] == '#' || line[1] == '#') { // 跳过 ad-hoc __asm__(内联汇编) 的插桩

if (strstr(line, "#APP")) skip_app = 1;
if (strstr(line, "#NO_APP")) skip_app = 0;

}

在往下就是设置一些其他的标志来判断是否跳过插桩。主要是跳过​​与设置架构不同的架构的汇编​​​,跳过​​Intel汇编​​​,跳过​​内联汇编​​的插桩。

/* If we're in the right mood for instrumenting, check for function
names or conditional labels. This is a bit messy, but in essence,
we want to catch:

^main: - function entry point (always instrumented)
^.L0: - GCC branch label
^.LBB0_0: - clang branch label (but only in clang mode)
^\tjnz foo - conditional branches

...but not:

^# BB#0: - clang comments
^ # BB#0: - ditto
^.Ltmp0: - clang non-branch labels
^.LC0 - GCC non-branch labels
^.LBB0_0: - ditto (when in GCC mode)
^\tjmp foo - non-conditional jumps

Additionally, clang and GCC on MacOS X follow a different convention
with no leading dots on labels, hence the weird maze of #ifdefs
later on.

*/

if (skip_intel || skip_app || skip_csect || !instr_ok ||
line[0] == '#' || line[0] == ' ') continue;

/* Conditional branch instruction (jnz, etc). We append the instrumentation
right after the branch (to instrument the not-taken path) and at the
branch destination label (handled later on). */

if (line[0] == '\t') {

if (line[1] == 'j' && line[2] != 'm' && R(100) < inst_ratio) {

fprintf(outf, use_64bit ? trampoline_fmt_64 : trampoline_fmt_32, // 通过 use_64bit,判断写入trampoline_fmt_64还是trampoline_fmt_32
R(MAP_SIZE));

ins_lines++;

}

continue;

}

/* Label of some sort. This may be a branch destination, but we need to
tread carefully and account for several different formatting
conventions. */

#ifdef __APPLE__

/* Apple: L<whatever><digit>: */

if ((colon_pos = strstr(line, ":"))) {

if (line[0] == 'L' && isdigit(*(colon_pos - 1))) {

#else

/* Everybody else: .L<whatever>: */

if (strstr(line, ":")) { // 检查 line 里是否有 :

if (line[0] == '.') { // 判断 line 是否以 . 开始

#endif /* __APPLE__ */

/* .L0: or LBB0_0: style jump destination */

#ifdef __APPLE__

/* Apple: L<num> / LBB<num> */

if ((isdigit(line[1]) || (clang_mode && !strncmp(line, "LBB", 3)))
&& R(100) < inst_ratio) {

#else

/* Apple: .L<num> / .LBB<num> */

if ((isdigit(line[2]) || (clang_mode && !strncmp(line + 1, "LBB", 3))) // 如果 line[2] 是数字,或者在 clang 模式下,line = .LBB
&& R(100) < inst_ratio) {

#endif /* __APPLE__ */

/* An optimization is possible here by adding the code only if the
label is mentioned in the code in contexts other than call / jmp.
That said, this complicates the code by requiring two-pass
processing (messy with stdin), and results in a speed gain
typically under 10%, because compilers are generally pretty good
about not generating spurious intra-function jumps.

We use deferred output chiefly to avoid disrupting
.Lfunc_begin0-style exception handling calculations (a problem on
MacOS X). */

if (!skip_next_label) instrument_next = 1; else skip_next_label = 0; // 如果 skip_next_label == 0

}

} else { // 否则就是函数(function),给 function 直接设置 instrument_next = 1

/* Function label (always instrumented, deferred mode). */

instrument_next = 1;

}

}

}

接下来是对其他的标志进行设置,可以从注释中看出我们想对​​main、.L0、.LBB0_0(clang mode)、\tjnz foo或者function​​​等地方设置​​instrument_next = 1​​。其他部分看我对源码加的注释。

循环结束后,接下来如果​​ins_lines​​​不为空,那么通过​​use_64bit​​​,判断向 outf 里写入​​main_payload_64​​​还是​​main_payload_32​​。并且关闭两个文件。

3.main

主函数就比较简单了。首先获取环境变量​​AFL_INST_RATIO​​​,并检测其是否合法(在0-100之间)。通过当前时间和进程号来获取并设置​​srandom​​​的随机种子。获取环境变量​​AS_LOOP_ENV_VAR​​,如果存在就抛出异常。

调用​​edit_params​​​设置相关参数。获取环境变量​​AFL_USE_ASAN​​​和​​AFL_USE_MSAN​​​,如果有一个存在就设置 ​​sanitizer = 1,inst_ratio /= 3​​​,这是因为在进行ASAN的编译时,AFL无法识别出ASAN特定的分支,导致插入很多无意义的桩代码,所以直接暴力地将插桩概率/3。最后​​fork​​​出一个子进程,执行 ​​execvp(as_params[0], (char**)as_params)​​。

作者:{狒猩橙}


标签:AFL,分析,skip,clang,__,源码,params,&&,line
From: https://blog.51cto.com/u_15786923/5888809

相关文章