AFL++漏洞挖洞原理与机制解析
AFL++会通过AFL-GCC在编译代码时在源代码中插入指定代码(插桩),以获得额外的信息从而确定本次输入是否是有趣的。
AFL-GCC会在跳转指令(如jz)这些位置插入afl_maybe_log的函数调用。常见于条件判断逻辑上。
实际上,如果你的程序只有一个分支,但AFL-GCC可不会这么温柔只插桩一两次,而是会将原本的分支判断条件细分,几乎每个输入字符都会被插桩。
源码:
1 2 3 4 5 6 7 8 9 10 11 12
| #include <stdio.h>
void fun(char *s) { printf("%s", s); }
int main() { char s[8]; fgets(s, 100, stdin); if(s[0] == 'x' && s[1] == 'a' && s[2] == 'j' && s[3] == 'k') fun(s); return 0; }
|
GCC编译后反编译:
1 2 3 4 5 6 7 8 9 10 11
| int __fastcall main(int argc, const char **argv, const char **envp) { char s[8]; unsigned __int64 v5;
v5 = __readfsqword(0x28u); fgets(s, 100, _bss_start); if ( s[0] == 'x' && s[1] == 'a' && s[2] == 'j' && s[3] == 'k' ) fun(s); return 0; }
|
AFL-GCC编译后反编译:
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
| int __fastcall main(int argc, const char **argv, const char **envp) { __int64 v3; __int64 v4; unsigned __int64 v5; unsigned __int64 v6; __int64 v7; __int64 *v9; char v10; char *v11; unsigned __int64 v12; __int64 v13; char v14; char v15; __int64 v16; unsigned __int64 v17; __int64 v18; __int64 v19[17];
v18 = v4; v19[0] = v3; _afl_maybe_log(argc, argv, envp, 53397LL); v5 = (unsigned __int64)v19; if ( _asan_option_detect_stack_use_after_return ) { v16 = __asan_stack_malloc_0(64LL, argv, _asan_option_detect_stack_use_after_return, v18); if ( v16 ) v5 = v16; } v6 = v5 >> 3; *(_QWORD *)v5 = 1102416563LL; *(_QWORD *)(v5 + 8) = "1 32 8 3 s:8"; *(_QWORD *)(v5 + 16) = main; *(_DWORD *)(v6 + 2147450880) = -235802127; *(_DWORD *)(v6 + 2147450884) = -202116352; v19[11] = __readfsqword(0x28u); if ( *(_BYTE *)(((unsigned __int64)&stdin >> 3) + 0x7FFF8000) ) __asan_report_load8(); __fgets_chk(v5 + 32, 8LL, 100LL, stdin); if ( *(char *)(((v5 + 32) >> 3) + 0x7FFF8000) < 0 ) { v9 = (__int64 *)(v5 + 32); __asan_report_load1(v5 + 32); goto LABEL_21; } if ( *(_BYTE *)(v5 + 32) != 'x' ) goto LABEL_5; v9 = (__int64 *)(v5 + 33); v10 = *(_BYTE *)(((v5 + 33) >> 3) + 0x7FFF8000); if ( v10 <= (char)((v5 + 33) & 7) && v10 ) { LABEL_21: __asan_report_load1(v9); goto LABEL_22; } _afl_maybe_log(v9, 8LL, v7, 61143LL); if ( *(_BYTE *)(v5 + 33) != 'a' ) goto LABEL_5; v11 = (char *)(v5 + 34); v12 = (v5 + 34) >> 3; v13 = ((_BYTE)v5 + 34) & 7; v14 = *(_BYTE *)(v12 + 0x7FFF8000); if ( v14 <= (char)v13 ) { if ( v14 ) { __asan_report_load1(v11); goto LABEL_28; } _afl_maybe_log(v11, v13, v12, 21126LL); v12 = (v5 + 34) >> 3; } v17 = v12; _afl_maybe_log(v11, v13, v12, 9750LL); if ( *(_BYTE *)(v5 + 34) != 'j' ) goto LABEL_5; v11 = (char *)(v5 + 35); v15 = *(_BYTE *)(((v5 + 35) >> 3) + 0x7FFF8000); if ( v15 <= (char)((v5 + 35) & 7) ) { if ( !v15 ) { _afl_maybe_log(v11, v13, v17, 18391LL); goto LABEL_18; } LABEL_28: __asan_report_load1(v11); JUMPOUT(0x1616LL); } LABEL_18: if ( *(_BYTE *)(v5 + 35) == 'k' ) fun((char *)(v5 + 32)); LABEL_5: if ( v19 != (__int64 *)v5 ) { LABEL_22: *(_QWORD *)v5 = 1172321806LL; *(_QWORD *)((v5 >> 3) + 0x7FFF8000) = 0xF5F5F5F5F5F5F5F5LL; **(_BYTE **)(v5 + 56) = 0; return 0; } *(_QWORD *)((v5 >> 3) + 0x7FFF8000) = 0LL; return 0; }
|
对此我们可以看见AFL-GCC明显将原本简单的一个if拆分成了多个if。
其中afl_maybe_log函数记录程序执行路径的覆盖信息,并将该信息通过共享内存反馈给AFL++模糊测试引擎。
每个被执行的路径会生成一个哈希值,该值映射到共享内存中的某个索引。
- afl_maybe_log从全局共享内存(由AFL++设置)中获取路径数据。
- 每条路径执行时生成唯一哈希值,由简单的逻辑运算和路径信息生成。
- 程序每执行一个路径,共享内存表对应的路径位置计数器会加一,记录该路径执行的频次。
共享内存 = 一个全局数组(记录路径的索引和路径执行的频次)
AFL-FUZZ通过分析共享内存中的信息发现哪些输入会触发新的路径。
唯一路径生成原理:将路径A -> B作为hash保存,如果存在路径A -> B -> C,那么将新增路径A -> C的hash。
其中每个路径点会存在一个唯一标识符,这是生成唯一路径hash的基础。哈希值的变化随着程序经过的点越来越多而不断累积,最终形成一个标志着程序完整执行路径的哈希值。
总的来说,AFL++会基于输入和产生出的路径关系来变异下一次输入,提升发现漏洞的概率。
AFL源码参考位置:https://github.com/google/AFL
AFL-FUZZ开启fork的执行路径(afl-fuzz.c):
1 2 3 4 5 6 7 8 9
| main() ↓ perform_dry_run() ↓ calibrate_case() ↓ init_forkserver() ↓ execv(target_path, argv);
|
父进程(ALF)和子进程(带测试函数的目标程序)通过创建的管道通信。
1 2 3
| if (dup2(ctl_pipe[0], FORKSRV_FD) < 0) PFATAL("dup2() failed"); if (dup2(st_pipe[1], FORKSRV_FD + 1) < 0) PFATAL("dup2() failed");
|
目标程序内部:
1 2 3
| __afl_maybe_log() ↓ __afl_fork_wait_loop
|
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
| void afl_maybe_log() { if(!afl_area_ptr) {
if(write(199,&afl_tmp,4) == 4) { while 1 { if(read(198,,&afl_tmp,4) != 4) break; v14 = fork(); if(v14 < 0 ) break; if(!v14) goto afl_fork_resume; afl_fork_pid = v14 write(199,&afl_fork_pid,4) v13 = afl_fork_pid v15 = waitpid(afl_fork_pid,afl-tmp,0) if(v15<=0) break; write(199,afl_temp,4); } }
} else { index = _afl_prev_loc ^ random_num; _afl_prev_loc ^= index; _afl_prev_loc = _afl_prev_loc >> 1; v8 = __CFADD__(((v6 + index))++, 1); (v6 + index) += v8; } }
|
初次进入函数时afl_area_ptr为0,此时fork初始化逻辑执行,afl_maybe_log通过获取环境变量__AFL_SHM_ID来访问共享内存的句柄。这部分共享内存用于记录路径覆盖信息。
如果afl_area_ptr为非0,证明fork已经初始化且开始执行,则此时应该进入路径信息记录。
- 当前路径的索引通过对前一个路径标识符(_afl_prev_loc)和当前随机数进行异或操作生成。之后,路径标识符通过右移操作进行更新,以避免循环路径生成相同的索引。
- 为了防止路径计数器溢出,使用
__CFADD__宏进行溢出检测。如果检测到溢出,v8的值会影响路径覆盖计数器,从而确保记录的准确性。
简单来说生成路径时会将当前块的随机数右移一位作为下一个块的afl_prev_loc,_afl_prev_loc初始值为0。
右移是为了区分 A -> B和 B -> A,例:
1 2 3 4 5 6 7 8 9 10 11
|
A → B: index = 0x1000 ^ 0x2000 = 0x3000, prev = 0x2000>>1 = 0x1000 B → A: index = 0x1000 ^ 0x1000 = 0x0000, prev = 0x1000>>1 = 0x0800
A → B: index = 0x1000 ^ 0x2000 = 0x3000, prev = 0x2000 B → A: index = 0x2000 ^ 0x1000 = 0x3000, prev = 0x1000
|
第一次启动示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| 程序启动: _afl_prev_loc = 0 (TLS自动初始化)
执行第一个基本块 (假设 random = 0x1234): index = 0 ^ 0x1234 = 0x1234 _afl_prev_loc ^= index → _afl_prev_loc = 0 ^ 0x1234 = 0x1234 _afl_prev_loc >>= 1 → _afl_prev_loc = 0x091A 记录索引 0x1234 到覆盖图
执行第二个基本块 (假设 random = 0x5678): index = 0x091A ^ 0x5678 = 0x4D62 _afl_prev_loc ^= index → _afl_prev_loc = 0x091A ^ 0x4D62 = 0x5678 _afl_prev_loc >>= 1 → _afl_prev_loc = 0x2B3C 记录索引 0x4D62 到覆盖图
|