avatar

Catalog
AFL源码分析02:afl-as.c

前言

前一篇分析了afl-gcc,它相当于gcc的一个wrapper,最后会调用实际的gcc,并编辑参数指定汇编器afl-as;本篇则分析(普通)插桩过程中的另一个wrapper:afl-as,他是对GNU as的一个wrapper,会编辑好参数并插桩后,调用实际的as进行汇编操作。

afl-as.c源码分析

关键(全局)变量

c
1
2
3
4
5
6
7
8
9
10
static u8** as_params;          /* Parameters passed to the real 'as', 传递给as的参数数组 */
static u8* input_file; /* Originally specified input file 指定的输入文件 */
static u8* modified_file; /* Instrumented file for the real 'as' 经过afl-as进行插桩处理后的文件 */
static u8 be_quiet, /* Quiet mode (no stderr output) */
clang_mode, /* Running in clang mode? */
pass_thru, /* Just pass data through? */
just_version, /* Just show version? */
sanitizer; /* Using ASAN/MSAN? */
static u32 inst_ratio = 100, /* Instrumentation probability (%) 插桩覆盖率 */
as_par_cnt = 1; /* Number of params to 'as' 用于计算传给as_params数组的参数个数 */

main

c
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
/* 主函数入口点 */
int main(int argc, char** argv) {

s32 pid;
u32 rand_seed;
int status;

/* 获取环境变量AFL_LAST_RATIO,该环境变量主要控制检测每个分支的概率,
取值为0~100%,设置为0时则只检测函数入口的跳转,不检测函数分支的跳转 */
u8* inst_ratio_str = getenv("AFL_INST_RATIO");

struct timeval tv;
struct timezone tz;

clang_mode = !!getenv(CLANG_ENV_VAR);

if (isatty(2) && !getenv("AFL_QUIET")) {
SAYF(cCYA "afl-as " cBRI VERSION cRST " by \n");

} else be_quiet = 1;

if (argc < 2) {
SAYF("\n"
"This is a helper application for afl-fuzz. It is a wrapper around GNU 'as',\n"
"executed by the toolchain whenever using afl-gcc or afl-clang. You probably\n"
"don't want to run this program directly.\n\n"

"Rarely, when dealing with extremely complex projects, it may be advisable to\n"
"set AFL_INST_RATIO to a value less than 100 in order to reduce the odds of\n"
"instrumenting every discovered branch.\n\n");
exit(1);
}

/* 通过gettimeofday获取时区和时间,然后设置srandom用的随机种子并进行初始化 */
gettimeofday(&tv, &tz);
rand_seed = tv.tv_sec ^ tv.tv_usec ^ getpid();
srandom(rand_seed);

/* 生成实际要执行的as的参数,作用类似afl-gcc.c中的edit_params */
edit_params(argc, argv);

/* 1.检测inst_ratio_str的值是否在规定范围内
2.设置环境变量AS_LOOP_ENV_VAR的值 */
if (inst_ratio_str) {
if (sscanf(inst_ratio_str, "%u", &inst_ratio) != 1 || inst_ratio > 100)
FATAL("Bad value of AFL_INST_RATIO (must be between 0 and 100)");
}
if (getenv(AS_LOOP_ENV_VAR))
FATAL("Endless loop when calling 'as' (remove '.' from your PATH)");
setenv(AS_LOOP_ENV_VAR, "1", 1);

/* 在使用ASAN或MSAN的情况下(即AFL_USE_ASAN/AFL_USE_MASN中有一个值为1):
1.设置sanitizer的值为1(表明使用了ASAN或MSAN)
2.令inst_ratio除以3,即降低插桩的概率(这是因为AFL无法在插桩的时候识别出
ASAN specific branches,所以会插入很多无意义的桩,为了降低这种概率,粗
暴的将整个插桩的概率都除以3) */
if (getenv("AFL_USE_ASAN") || getenv("AFL_USE_MSAN")) {
sanitizer = 1;
inst_ratio /= 3;
}

/* 如果just_version(只显示版本)的值为0
那么调用add_instrumentation进行实际的插桩工作 */
if (!just_version) add_instrumentation();

/* fork出一个子进程去执行调用实际的汇编器as完成汇编操作
fork函数被调用一次,但返回两次:
子进程的返回值是0
父进程的返回值是子进程的Pid */
if (!(pid = fork())) {
execvp(as_params[0], (char**)as_params);
FATAL("Oops, failed to execute '%s' - check your PATH", as_params[0]);
}

if (pid < 0) PFATAL("fork() failed");

/* 等待子进程结束 */
if (waitpid(pid, &status, 0) <= 0) PFATAL("waitpid() failed");

/* 读取环境变量AFL_KEEP_ASSEMBLY的值
若不存在该环境变量,则调用unlink移除modified_file(已插桩完成的文件)
设置该环境变量主要是为了防止afl-as删掉插桩后的汇编文件,设置为1则会保留插桩后的汇编文件
前面fork一个子进程进行插桩,是为了在执行完实际的as之后,能够unlink掉modified_file */
if (!getenv("AFL_KEEP_ASSEMBLY")) unlink(modified_file);

exit(WEXITSTATUS(status));

}

edit_params

c
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
112
113
114
115
116
117
118
/* 调整传递给实际汇编器as的参数,由于filename永远被GCC作为最后一个传递的参数
利用这个特性可以使代码保持简单 */
static void edit_params(int argc, char** argv) {

/* 首先获取环境变量TMPDIR和AFL_AS的值
分别用来设置tmp_dir和afl_as的值 */
u8 *tmp_dir = getenv("TMPDIR"), *afl_as = getenv("AFL_AS");
u32 i;


/* 为了解决MacOS X上的过时的as无法正常工作的问题,需要在没有运行afl-as的情况下,
使用'clang -c'而不是'as -q'来编译汇编文件
这里当处于clang_mode时,且没有没有设置AFL_AS环境变量,
就设置use_clang_as的值为1,再将afl_as的值设置为AFL_CC/AFL_CXX/clang中的一种 */
#ifdef __APPLE__
u8 use_clang_as = 0;

if (clang_mode && !afl_as) {
use_clang_as = 1;

afl_as = getenv("AFL_CC");
if (!afl_as) afl_as = getenv("AFL_CXX");
if (!afl_as) afl_as = "clang";
}
#endif /* __APPLE__ */


/* 当环境变量TMPDIR没有设置时,GCC也会使用环境变量TMP和TEMP
所以都需要检查一下。如果一直为空,就将tmp_dir设置为'/tmp' */
if (!tmp_dir) tmp_dir = getenv("TEMP");
if (!tmp_dir) tmp_dir = getenv("TMP");
if (!tmp_dir) tmp_dir = "/tmp";

/* 1.为as的参数数组开辟空间,大小(argc+32)*8
2.若afl_as存在,则设置as_params数组第一个参数为afl_as的值,否则设置为'as'
3.设置as_params数组的截断位置 */
as_params = ck_alloc((argc + 32) * sizeof(u8*));
as_params[0] = afl_as ? afl_as : (u8*)"as";
as_params[argc] = 0;

/* 从argv[1]开始遍历(注意初始化全局变量时as_par_cnt设置为1),
如果遇到参数为'--64',则设置use_64bit的值
如果遇到参数为'--32',则清除use_64bit的值 */
for (i = 1; i < argc - 1; i++) {
if (!strcmp(argv[i], "--64")) use_64bit = 1;
else if (!strcmp(argv[i], "--32")) use_64bit = 0;


/* 如果是Apple系统:
1.对于use_64bit的设置和清除操作会有一点点不同
2.需要去掉那些特定于Xcode中的前端汇编器的选项(即参数'-q'和'-Q') */
#ifdef __APPLE__
if (!strcmp(argv[i], "-arch") && i + 1 < argc) {
if (!strcmp(argv[i + 1], "x86_64")) use_64bit = 1;
else if (!strcmp(argv[i + 1], "i386"))
FATAL("Sorry, 32-bit Apple platforms are not supported.");
}

if (clang_mode && (!strcmp(argv[i], "-q") || !strcmp(argv[i], "-Q")))
continue;
#endif /* __APPLE__ */


as_params[as_par_cnt++] = argv[i];
}


/* 如果是Apple系统,当调用clang作为前端汇编器时,最好添加上参数'-c -x assembler' */
#ifdef __APPLE__
if (use_clang_as) {
as_params[as_par_cnt++] = "-c";
as_params[as_par_cnt++] = "-x";
as_params[as_par_cnt++] = "assembler";
}
#endif /* __APPLE__ */


/* 下面开始设置as_params数组中的其它参数
首先从GCC传进来的最后一个参数拿到input_file */
input_file = argv[argc - 1];

/* 判断input_file的第一个参数是不是字符'-'开头
如果是'-'开头,判断第一个参数是不是-version
如果是-version:
1.设置just_version的值为1
2.不对input_file进行修改直接赋给modified_file
3.跳转到wrap_things_up执行 */
if (input_file[0] == '-') {
if (!strcmp(input_file + 1, "-version")) {
just_version = 1;
modified_file = input_file;
goto wrap_things_up;
}

if (input_file[1]) FATAL("Incorrect use (not called through afl-gcc?)");
else input_file = NULL;

} else {

/* 分别判断input_file和tmp_dir、/var/tmp的前9个字节、/tmp的前5个字节是否相同,
如果不相同,则设置pass_thru的值为1(这里检查是否为一次尝试编译程序时的标准调用) */
if (strncmp(input_file, tmp_dir, strlen(tmp_dir)) &&
strncmp(input_file, "/var/tmp/", 9) &&
strncmp(input_file, "/tmp/", 5)) pass_thru = 1;
}

/* 将modified_file设置为诸如tmp_dir/.afl-pid-time.s形式的字符串 */
modified_file = alloc_printf("%s/.afl-%u-%u.s", tmp_dir, getpid(),
(u32)time(NULL));

/* 进入warp_things_up后,会向as_params添加参数modified_file
之后用NULL截断数组,完成对as_params的编辑 */
wrap_things_up:

as_params[as_par_cnt++] = modified_file;
as_params[as_par_cnt] = NULL;

}

编辑afl-as.c,在main函数中,调用edit_params之后添加如下代码,打印as_params来查看实际执行的命令

c
1
2
3
for(int i = 0; i < as_par_cnt; i++) {
printf("\targ%d: %s\n", i, as_params[i]);
}

然后执行

shell
1
2
$ make
$ sudo make install

即可查看打印的参数

可以看到,在执行afl-gcc的时候,会调用到实际的gcc,并在之后进一步调用了afl-as,最后afl-as也会去调用实际的as完成整个汇编的过程。其中在afl-as阶段,主要完成的就是插桩部分的操作,这些工作交由add_instrumentation函数完成。

add_instrumentation

c
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
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
/* 输入原始文件,插桩到适当位置,生成插桩后的文件 */
static void add_instrumentation(void) {

static u8 line[MAX_LINE];

FILE* inf;
FILE* outf;
s32 outfd;
u32 ins_lines = 0;

u8 instr_ok = 0, skip_csect = 0, skip_next_label = 0,
skip_intel = 0, skip_app = 0, instrument_next = 0;


#ifdef __APPLE__
u8* colon_pos;
#endif /* __APPLE__ */


/* 如果input_file不为空,则尝试打开这个文件,如果打开失败就抛出异常
如果input_file为空,则从标准输入读取这个文件 */
if (input_file) {
inf = fopen(input_file, "r");
if (!inf) PFATAL("Unable to read '%s'", input_file);

} else inf = stdin;

/* 创建modified_file文件(这里O_EXCL是确保此次调用创建了这个文件)
然后调用fdopen,将modified_file的文件指针保存在outf中 */
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");

/* 逐行读取输入文件的内容,并保存到line数组里,每行最多读取MAX_LINE-1个字节(去掉最后的'\0')
再从line数组里将读取的内容写入到outf指向的文件里 */
while (fgets(line, MAX_LINE, inf)) {

/* 检查pass_thru、skip_intel、skip_app、skip_csect是值是否都为0
检查instr_ok、instrument_next的值是否都为1
检查line是否以\t开始,line[1]是否为字母
这么做,是为了跳过标签,宏,注释 */
if (!pass_thru && !skip_intel && !skip_app && !skip_csect && instr_ok &&
instrument_next && line[0] == '\t' && isalpha(line[1])) {

/* 如果以上条件都满足,则对以defered mode进行插桩的位置开始插桩处理:
1.进行插桩,将trampoline_fmt写入outf,也就是modified_file
2.设置instrument_next的值为0
3.插桩计数器ins_lines的值加1 */
fprintf(outf, use_64bit ? trampoline_fmt_64 : trampoline_fmt_32,
R(MAP_SIZE));

instrument_next = 0;
ins_lines++;
}

/* 将原本的指令写入到outf;如果是pass_thru模式下,就回到循环开始处读取下一行数据 */
fputs(line, outf);
if (pass_thru) continue;

/* 这里可以认为是插桩程序真正的开始位置:
1.AFL只关心对.text节的插桩,根据对处理文件的追踪,会相应的
设置instr_ok的值(该值被设置时代表此时位于.text节)
2.OpenBSD系统会将跳转表直接放在代码中。跳转表附近会有特定格
式的p2align指令,可以将其作为信号 */

/* 如果line以'\t.'开头,则进行如下判断:
如果满足如下条件:
1.clang_mode值为0
2.instr_ok值为1(位于.text节)
3.line=='\t.p2align '
4.line[10]是数字
5.line[11]=='\n'
则设置skip_next_label的值为1
如果line的值为'\t.text\n'或'\t.section\t.text'或'\t.section\t__TEXT,__text'或'\t.section __TEXT,__text'
则设置instr_ok的值为1,并跳回循环开始处,读取下一行的数据到line里
如果line的值为'\t.section\t' '\t.section' '\t.bss' '\t.data'
则设置instr_ok的值为0,并跳回循环开始处,读取下一行的数据到line里 */
if (line[0] == '\t' && line[1] == '.') {
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) ||
!strncmp(line + 2, "section\t.text", 13) ||
!strncmp(line + 2, "section\t__TEXT,__text", 21) ||
!strncmp(line + 2, "section __TEXT,__text", 21)) {
instr_ok = 1;
continue;
}

if (!strncmp(line + 2, "section\t", 8) ||
!strncmp(line + 2, "section ", 8) ||
!strncmp(line + 2, "bss\n", 4) ||
!strncmp(line + 2, "data\n", 5)) {
instr_ok = 0;
continue;
}

}

/* 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. */
/* 检测off-flavor编码(很少见,仅出现在gdb中),遇到此类情况时,将对skip_csect
进行设置,并且不进行插桩操作 */
if (strstr(line, ".code")) {
if (strstr(line, ".code32")) skip_csect = use_64bit;
if (strstr(line, ".code64")) skip_csect = !use_64bit;
}

/* 检查语法变化,通常只发生在硬编码写入的情况,跳过Intel语法的指令块,当回到
AT&T指令块时恢复插桩 */
if (strstr(line, ".intel_syntax")) skip_intel = 1;
if (strstr(line, ".att_syntax")) skip_intel = 0;

/* 检测并跳过临时的__asm__块,即不对内联汇编进行插桩 */
if (line[0] == '#' || line[1] == '#') {
if (strstr(line, "#APP")) skip_app = 1;
if (strstr(line, "#NO_APP")) skip_app = 0;
}

/* 如果出现以下的情况,则回到循环开始处,读取下一行的内容到line:
1.设置了skip_intel或skip_app或skip_csect的值
2.instr_ok的值为0
3.line[0]的值为'#'或' ' */
if (skip_intel || skip_app || skip_csect || !instr_ok ||
line[0] == '#' || line[0] == ' ')
continue;

/* 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.

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). */

/* #define R(x) (random() % (x)) --> 定义在types.h中
这里的随机性使得可以唯一定位程序运行的位置(不考虑碰撞的情况下)

如果line以'\tj[!m]'开头,且R(100)小于插桩密度inst_ratio,则
1.进行插桩,将trampoline_fmt写入outf,也就是modified_file
2.插桩计数器ins_lines的值加1 */
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,
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.
以下是对一些标签(label)进行评估,有一些标签可能是一些分支的目的地,需要单独评判 */

/* Apple: L:
对于Apple系统,检查是否存在line=='L::'的情况 */
#ifdef __APPLE__
if ((colon_pos = strstr(line, ":"))) {
if (line[0] == 'L' && isdigit(*(colon_pos - 1))) {

#else

/* Everybody else: .L: */
/* 对于Apple以外的系统,检查是否存在line=='.L:'的情况 */
if (strstr(line, ":")) {
if (line[0] == '.') {
#endif /* __APPLE__ */


/* .L0: or LBB0_0: style jump destination
以下是对形如'.L0'或者'LBB0_0'这样格式的分支标签进行筛选 */

/* Apple: L / LBB
对于Apple系统,筛选2种情况:
1.line=='L:' 并且 R(100) < inst_ratio
2.line=='LBB:' 并且 clang_mode==1 并且 R(100) < inst_ratio */
#ifdef __APPLE__
if ((isdigit(line[1]) || (clang_mode && !strncmp(line, "LBB", 3)))
&& R(100) < inst_ratio) {

#else

/* Everybody else: .L / .LBB
对于Apple以外的系统,筛选2种情况:
1.line=='.L:' 并且 R(100) < inst_ratio
2.line=='.LBB:' 并且 clang_mode==1 并且 R(100) < inst_ratio */
if ((isdigit(line[2]) || (clang_mode && !strncmp(line + 1, "LBB", 3)))
&& 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). */

/* 如果满足上述筛选条件:
如果skip_next_label值为0,就设置instrument_next值为1
否则,设置skip_next_label值为0 */
if (!skip_next_label)
instrument_next = 1;
else
skip_next_label = 0;
}
} else {

/* Function label (always instrumented, deferred mode).
如果未满足前面的筛选条件,说明这是一个函数入口点,
则设置instrument_next值为1,表明是defered mode */
instrument_next = 1;
}
}
}

/* 如果插桩计数器ins_lines的值不为0,就在input_file拷贝完成后,
向outf中写入main_payload,然后关闭input_file和modified_file */
if (ins_lines)
fputs(use_64bit ? main_payload_64 : main_payload_32, outf);

if (input_file) fclose(inf);
fclose(outf);

if (!be_quiet) {
if (!ins_lines) WARNF("No instrumentation targets found%s.",
pass_thru ? " (pass-thru mode)" : "");
else OKF("Instrumented %u locations (%s-bit, %s mode, ratio %u%%).",
ins_lines, use_64bit ? "64" : "32",
getenv("AFL_HARDEN") ? "hardened" :
(sanitizer ? "ASAN/MSAN" : "non-hardened"),
inst_ratio);
}
}

分析完add_instrumentation函数,可以了解到该函数会经过一系列判断,在代码段中的主要分支和条件分支中插入桩代码,并输入modified_file。这里,还有一部分的内容尚未了解,就是这些桩代码,是如何反馈出覆盖率的,桩代码到底实现了什么?要了解这些,还需要分析(普通)插桩流程涉及到的最后一个文件afl-as.h,我们将在下一篇中对此进行分析。

参考资料

  1. hollk:AFL源码分析之afl-as.c详细注释
  2. skr:sakuraのAFL源码全注释
  3. Seebug:AFL 二三事——源码分析
  4. AFL内部实现细节小记
  5. AFL:afl-as.c
  6. HICOOKIE:AFL-Learning
  7. 简书:AFL源码分析
Author: cataLoc
Link: http://cata1oc.github.io/2022/01/05/AFL%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%9002/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.
Donate
  • 微信
    微信
  • 支付寶
    支付寶