Fuzz之再见端倪

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]; // [rsp+0h] [rbp-10h] BYREF
unsigned __int64 v5; // [rsp+8h] [rbp-8h]

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; // rax
__int64 v4; // rcx
unsigned __int64 v5; // rbx
unsigned __int64 v6; // rbp
__int64 v7; // rdx
__int64 *v9; // rdi
char v10; // r11
char *v11; // rdi
unsigned __int64 v12; // rdx
__int64 v13; // rsi
char v14; // cl
char v15; // r13
__int64 v16; // rax
unsigned __int64 v17; // [rsp+0h] [rbp-120h]
__int64 v18; // [rsp+90h] [rbp-90h]
__int64 v19[17]; // [rsp+98h] [rbp-88h] BYREF

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() //启动fork设置执行环境并创建管道

execv(target_path, argv); //启动程序

父进程(ALF)和子进程(带测试函数的目标程序)通过创建的管道通信。

1
2
3
//afl-fuzz.c init_forkserver()内2083行
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)//判断是否为空,是否是第一次进入
{
//forkserver初始化的逻辑里面
//然后通过管道和afl-fuzz进行交互,管道id 199

//通过199管道通知afl-fuzz,forkserver已经准备完毕
if(write(199,&afl_tmp,4) == 4)
{
while 1 //forkserver
{
//收到afl-fuzz指令fork新进程
if(read(198,,&afl_tmp,4) != 4)
break;//如果198管道关闭,forkserver退出
v14 = fork();
if(v14 < 0 )
break;
if(!v14)
goto afl_fork_resume;
afl_fork_pid = v14
write(199,&afl_fork_pid,4)//向afl-fuzz传递进程pid
v13 = afl_fork_pid
v15 = waitpid(afl_fork_pid,afl-tmp,0)//父进程forkserver获得子进程pid,并通过waitpid等待子进程退出
if(v15<=0)
break;
write(199,afl_temp,4);//通过保存退出信息,并且传递给afl-fuzz
}
}

}
else
{
//这里就是进入到覆盖信息反馈的位置
//主要就是计算路径hash,并且反馈
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(random=0x1000) 和 B(random=0x2000) 两个基本块

// 有右移的情况:
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
// A→B 和 B→A 索引相同 ✗(无法区分)

第一次启动示例:

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 到覆盖图

Fuzz之再见端倪
https://zlsf-zl.github.io/2026/04/01/Fuzz之再见端倪/
作者
ZLSF
发布于
2026年4月1日
许可协议