简介

前面学习了 afl-as ,但是可以发现前面的插桩技术十分原始且神奇,现在更为通用的插桩方式是通过 llvm pass 进行插桩,对应的也就是 afl-clang-fast 。

LLVM

llvm 的大体框架如下:

Untitled.png

Clang 是 LLVM 项目的一个子项目,它是 LLVM 架构下的 C/C++/Objective-C 的编译器,是 LLVM 前端的一部分。相较于GCC,具备编译速度快、占用内存少、模块化设计、诊断信息可读性强、设计清晰简单等优点。

以 Clang 为例,从源码到机器码的流程如下:

Untitled 1.png

其中的 LLVM Pass 便是用户自定义的处理 IR 的过程,可以进行优化、插桩、分析等功能。

AFL中的afl-clang-fast

AFL 中的 llvm mode 可以实现编译器级别的插桩,主要包含以下三个文件:

  • afl-clang-fast.c :本质上是 clang 的 wrapper ,和 afl-gcc 类似
  • afl-llvm-pass.so.cc :通过 afl-clang-fast 调用 clang 时将 pass 插入 LLVM 中
  • afl-llvm-rt.o.c :重写了 afl-as.h 中的插桩代码(main_payload)

总结一下就是通过 llvm pass 记录信息,将每个基本块都插入探针

afl-clang-fast.c

main

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
/* Main entry point */

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

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

#ifdef USE_TRACE_PC
SAYF(cCYA "afl-clang-fast [tpcg] " cBRI VERSION cRST " by <lszekeres@google.com>\n");
#else
SAYF(cCYA "afl-clang-fast " cBRI VERSION cRST " by <lszekeres@google.com>\n");
#endif /* ^USE_TRACE_PC */

}

if (argc < 2) {

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

" CC=%s/afl-clang-fast ./configure\n"
" CXX=%s/afl-clang-fast++ ./configure\n\n"

"In contrast to the traditional afl-clang tool, this version is implemented as\n"
"an LLVM pass and tends to offer improved performance with slow programs.\n\n"

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

exit(1);

}

#ifndef __ANDROID__
find_obj(argv[0]);
#endif

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_obj :如果是安卓平台,则查找运行时的 lib
  • edit_params :修改 params
  • execvp :和 afl-gcc 类似

find_obj

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
/* Try to find the runtime libraries. If that fails, abort. */

static void find_obj(u8* argv0) {

u8 *afl_path = getenv("AFL_PATH");
u8 *slash, *tmp;

if (afl_path) {

tmp = alloc_printf("%s/afl-llvm-rt.o", afl_path);

if (!access(tmp, R_OK)) {
obj_path = afl_path;
ck_free(tmp);
return;
}

ck_free(tmp);

}

slash = strrchr(argv0, '/');

if (slash) {

u8 *dir;

*slash = 0;
dir = ck_strdup(argv0);
*slash = '/';

tmp = alloc_printf("%s/afl-llvm-rt.o", dir);

if (!access(tmp, R_OK)) {
obj_path = dir;
ck_free(tmp);
return;
}

ck_free(tmp);
ck_free(dir);

}

if (!access(AFL_PATH "/afl-llvm-rt.o", R_OK)) {
obj_path = AFL_PATH;
return;
}

FATAL("Unable to find 'afl-llvm-rt.o' or 'afl-llvm-pass.so'. Please set AFL_PATH");

}
  • 查看是否有环境变量 AFL_PATH 并赋值给 afl_path ,如果有则在 afl_path 下寻找 afl-llvm-rt.o 并返回
  • 通过 argv0 的 “/” 分割出前面的路径,并在该路径下寻找 afl-llvm-rt.o 并返回
  • 最后在 AFL_PATH 路径下寻找 afl-llvm-rt.o 并返回
  • 都找不到则抛出异常

edit_params

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
/* Copy argv to cc_params, making the necessary edits. */

static void edit_params(u32 argc, char** argv) {

u8 fortify_set = 0, asan_set = 0, x_set = 0, bit_mode = 0;
u8 *name;

cc_params = ck_alloc((argc + 128) * sizeof(u8*));

name = strrchr(argv[0], '/');
if (!name) name = argv[0]; else name++;

if (!strcmp(name, "afl-clang-fast++")) {
u8* alt_cxx = getenv("AFL_CXX");
cc_params[0] = alt_cxx ? alt_cxx : (u8*)"clang++";
} else {
u8* alt_cc = getenv("AFL_CC");
cc_params[0] = alt_cc ? alt_cc : (u8*)"clang";
}

/* There are two ways to compile afl-clang-fast. In the traditional mode, we
use afl-llvm-pass.so to inject instrumentation. In the experimental
'trace-pc-guard' mode, we use native LLVM instrumentation callbacks
instead. The latter is a very recent addition - see:

http://clang.llvm.org/docs/SanitizerCoverage.html#tracing-pcs-with-guards */

#ifdef USE_TRACE_PC
cc_params[cc_par_cnt++] = "-fsanitize-coverage=trace-pc-guard";
#ifndef __ANDROID__
cc_params[cc_par_cnt++] = "-mllvm";
cc_params[cc_par_cnt++] = "-sanitizer-coverage-block-threshold=0";
#endif
#else
cc_params[cc_par_cnt++] = "-Xclang";
cc_params[cc_par_cnt++] = "-load";
cc_params[cc_par_cnt++] = "-Xclang";
cc_params[cc_par_cnt++] = alloc_printf("%s/afl-llvm-pass.so", obj_path);
#endif /* ^USE_TRACE_PC */

cc_params[cc_par_cnt++] = "-Qunused-arguments";

while (--argc) {
u8* cur = *(++argv);

if (!strcmp(cur, "-m32")) bit_mode = 32;
if (!strcmp(cur, "armv7a-linux-androideabi")) bit_mode = 32;
if (!strcmp(cur, "-m64")) bit_mode = 64;

if (!strcmp(cur, "-x")) x_set = 1;

if (!strcmp(cur, "-fsanitize=address") ||
!strcmp(cur, "-fsanitize=memory")) asan_set = 1;

if (strstr(cur, "FORTIFY_SOURCE")) fortify_set = 1;

if (!strcmp(cur, "-Wl,-z,defs") ||
!strcmp(cur, "-Wl,--no-undefined")) continue;

cc_params[cc_par_cnt++] = cur;

}

if (getenv("AFL_HARDEN")) {

cc_params[cc_par_cnt++] = "-fstack-protector-all";

if (!fortify_set)
cc_params[cc_par_cnt++] = "-D_FORTIFY_SOURCE=2";

}

if (!asan_set) {

if (getenv("AFL_USE_ASAN")) {

if (getenv("AFL_USE_MSAN"))
FATAL("ASAN and MSAN are mutually exclusive");

if (getenv("AFL_HARDEN"))
FATAL("ASAN and AFL_HARDEN are mutually exclusive");

cc_params[cc_par_cnt++] = "-U_FORTIFY_SOURCE";
cc_params[cc_par_cnt++] = "-fsanitize=address";

} else if (getenv("AFL_USE_MSAN")) {

if (getenv("AFL_USE_ASAN"))
FATAL("ASAN and MSAN are mutually exclusive");

if (getenv("AFL_HARDEN"))
FATAL("MSAN and AFL_HARDEN are mutually exclusive");

cc_params[cc_par_cnt++] = "-U_FORTIFY_SOURCE";
cc_params[cc_par_cnt++] = "-fsanitize=memory";

}

}

#ifdef USE_TRACE_PC

if (getenv("AFL_INST_RATIO"))
FATAL("AFL_INST_RATIO not available at compile time with 'trace-pc'.");

#endif /* USE_TRACE_PC */

if (!getenv("AFL_DONT_OPTIMIZE")) {

cc_params[cc_par_cnt++] = "-g";
cc_params[cc_par_cnt++] = "-O3";
cc_params[cc_par_cnt++] = "-funroll-loops";

}

if (getenv("AFL_NO_BUILTIN")) {

cc_params[cc_par_cnt++] = "-fno-builtin-strcmp";
cc_params[cc_par_cnt++] = "-fno-builtin-strncmp";
cc_params[cc_par_cnt++] = "-fno-builtin-strcasecmp";
cc_params[cc_par_cnt++] = "-fno-builtin-strncasecmp";
cc_params[cc_par_cnt++] = "-fno-builtin-memcmp";

}

cc_params[cc_par_cnt++] = "-D__AFL_HAVE_MANUAL_CONTROL=1";
cc_params[cc_par_cnt++] = "-D__AFL_COMPILER=1";
cc_params[cc_par_cnt++] = "-DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION=1";

/* When the user tries to use persistent or deferred forkserver modes by
appending a single line to the program, we want to reliably inject a
signature into the binary (to be picked up by afl-fuzz) and we want
to call a function from the runtime .o file. This is unnecessarily
painful for three reasons:

1) We need to convince the compiler not to optimize out the signature.
This is done with __attribute__((used)).

2) We need to convince the linker, when called with -Wl,--gc-sections,
not to do the same. This is done by forcing an assignment to a
'volatile' pointer.

3) We need to declare __afl_persistent_loop() in the global namespace,
but doing this within a method in a class is hard - :: and extern "C"
are forbidden and __attribute__((alias(...))) doesn't work. Hence the
__asm__ aliasing trick.

*/

cc_params[cc_par_cnt++] = "-D__AFL_LOOP(_A)="
"({ static volatile char *_B __attribute__((used)); "
" _B = (char*)\"" PERSIST_SIG "\"; "
#ifdef __APPLE__
"__attribute__((visibility(\"default\"))) "
"int _L(unsigned int) __asm__(\"___afl_persistent_loop\"); "
#else
"__attribute__((visibility(\"default\"))) "
"int _L(unsigned int) __asm__(\"__afl_persistent_loop\"); "
#endif /* ^__APPLE__ */
"_L(_A); })";

cc_params[cc_par_cnt++] = "-D__AFL_INIT()="
"do { static volatile char *_A __attribute__((used)); "
" _A = (char*)\"" DEFER_SIG "\"; "
#ifdef __APPLE__
"__attribute__((visibility(\"default\"))) "
"void _I(void) __asm__(\"___afl_manual_init\"); "
#else
"__attribute__((visibility(\"default\"))) "
"void _I(void) __asm__(\"__afl_manual_init\"); "
#endif /* ^__APPLE__ */
"_I(); } while (0)";

if (x_set) {
cc_params[cc_par_cnt++] = "-x";
cc_params[cc_par_cnt++] = "none";
}

#ifndef __ANDROID__
switch (bit_mode) {

case 0:
cc_params[cc_par_cnt++] = alloc_printf("%s/afl-llvm-rt.o", obj_path);
break;

case 32:
cc_params[cc_par_cnt++] = alloc_printf("%s/afl-llvm-rt-32.o", obj_path);

if (access(cc_params[cc_par_cnt - 1], R_OK))
FATAL("-m32 is not supported by your compiler");

break;

case 64:
cc_params[cc_par_cnt++] = alloc_printf("%s/afl-llvm-rt-64.o", obj_path);

if (access(cc_params[cc_par_cnt - 1], R_OK))
FATAL("-m64 is not supported by your compiler");

break;

}
#endif

cc_params[cc_par_cnt] = NULL;

}
  • 为 cc_params 开辟 (argc + 128) * sizeof(u8*) 大小的空间
  • 通过分割 argv[0] 判断传入的是啥
    • 如果是 afl-clang-fast++ ,则将 cc_params[0] 设置为环境变量 AFL_CXX 或 clang++
    • 否则就是 afl-clang-fast ,则将 cc_params[0] 设置为环境变量 AFL_CC 或 clang
  • 如果定义了宏 USE_TRACE_PC ,则添加 -fsanitize-coverage=trace-pc-guard
    • 如果不是安卓平台,则添加 -mllvm -sanitizer-coverage-block-threshold=0
  • 否则添加 -Xclang -load -Xclang “obj_path/afl-llvm-pass.so”
  • 最后添加 -Qunused-arguments
    • 这里分别对应了两种模式
      • 传统模式(使用 afl-llvm-pass.so 注入来进行插桩)
      • trace-pc-guard 模式
  • 通过 argc 遍历 argv 并进行设置
    • -m32 和 armv7a-linux-androideabi 设置 bit_mode 为 32
    • -m64 设置 bit_mode 为 64
    • -x 设置 x_set 为 1
    • -fsanitize=address 或 -fsanitize=memory 设置 asan_set 为 1
    • FORTIFY_SOURCE 设置 fortify_set 为 1
    • 忽略 -Wl,-z,defs 和 -Wl,–no-undefined
  • 如果有环境变量 AFL_HARDEN ,添加 -fstack-protector-all
    • 如果没有设置 fortify_set ,则添加 -D_FORTIFY_SOURCE=2
  • 如果没有设置 asan_set
    • 如果有环境变量 AFL_USE_ASAN
      • 如果有环境变量 AFL_USE_MSAN 或 AFL_HARDEN ,抛出错误
      • 添加 -U_FORTIFY_SOURCE -fsanitize=address
    • 没有环境变量 AFL_USE_ASAN,而有环境变量 AFL_USE_MSAN
      • 如果有环境变量 AFL_USE_ASAN 或 AFL_HARDEN ,抛出错误
      • 添加 -U_FORTIFY_SOURCE -fsanitize=memory
  • 如果有宏定义 USE_TRACE_PC
    • 如果有环境变量 AFL_INST_RATIO ,抛出错误
  • 如果没有环境变量 AFL_DONT_OPTIMIZE
    • 添加 -g -O3 -funroll-loops
  • 如果有环境变量 AFL_NO_BUILTIN
    • 添加 -fno-builtin-strcmp -fno-builtin-strncmp -fno-builtin-strcasecmp -fno-builtin-strncasecmp -fno-builtin-memcmp
  • 添加 -D__AFL_HAVE_MANUAL_CONTROL=1 -D__AFL_COMPILER=1 -DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION=1
  • 添加宏定义
    • D__AFL_LOOP(_A) :对应 ___afl_persistent_loop
    • D__AFL_INIT() :对应 ___afl_manual_init
  • 如果设置了 x_set ,添加 -x none
  • 如果是安卓平台,判断 bit_mode 的值
    • 如果为 0 ,则添加 obj_path/afl-llvm-rt.o
    • 如果为 32 ,则添加 obj_path/afl-llvm-rt-32.o
    • 如果为 64 ,则添加 obj_path/afl-llvm-rt-64.o

整体的流程和 afl-gcc 中的 edit_params 类似,有很多相同的地方

execvp

最后的 execvp 则是将替换过后的执行运行,和 afl-gcc 一样,这里不再赘述

afl-llvm-pass.so.cc

该文件实现了插桩的 llvm pass

AFLCoverage

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
namespace {

class AFLCoverage : public ModulePass {

public:

static char ID;
AFLCoverage() : ModulePass(ID) { }

bool runOnModule(Module &M) override;

// StringRef getPassName() const override {
// return "American Fuzzy Lop Instrumentation";
// }

};

}

实现了 runOnModule

register pass

1
2
3
4
5
6
7
8
9
10
11
12
static void registerAFLPass(const PassManagerBuilder &,
legacy::PassManagerBase &PM) {

PM.add(new AFLCoverage());

}

static RegisterStandardPasses RegisterAFLPass(
PassManagerBuilder::EP_ModuleOptimizerEarly, registerAFLPass);

static RegisterStandardPasses RegisterAFLPass0(
PassManagerBuilder::EP_EnabledOnOptLevel0, registerAFLPass);

向 PassManager 中注册了新的 AFLPass

runOnModule

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
char AFLCoverage::ID = 0;

bool AFLCoverage::runOnModule(Module &M) {

LLVMContext &C = M.getContext();

IntegerType *Int8Ty = IntegerType::getInt8Ty(C);
IntegerType *Int32Ty = IntegerType::getInt32Ty(C);

/* Show a banner */

char be_quiet = 0;

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

SAYF(cCYA "afl-llvm-pass " cBRI VERSION cRST " by <lszekeres@google.com>\n");

} else be_quiet = 1;

/* Decide instrumentation ratio */

char* inst_ratio_str = getenv("AFL_INST_RATIO");
unsigned int inst_ratio = 100;

if (inst_ratio_str) {

if (sscanf(inst_ratio_str, "%u", &inst_ratio) != 1 || !inst_ratio ||
inst_ratio > 100)
FATAL("Bad value of AFL_INST_RATIO (must be between 1 and 100)");

}

/* Get globals for the SHM region and the previous location. Note that
__afl_prev_loc is thread-local. */

GlobalVariable *AFLMapPtr =
new GlobalVariable(M, PointerType::get(Int8Ty, 0), false,
GlobalValue::ExternalLinkage, 0, "__afl_area_ptr");

GlobalVariable *AFLPrevLoc = new GlobalVariable(
M, Int32Ty, false, GlobalValue::ExternalLinkage, 0, "__afl_prev_loc",
0, GlobalVariable::GeneralDynamicTLSModel, 0, false);

/* Instrument all the things! */

int inst_blocks = 0;

for (auto &F : M)
for (auto &BB : F) {

BasicBlock::iterator IP = BB.getFirstInsertionPt();
IRBuilder<> IRB(&(*IP));

if (AFL_R(100) >= inst_ratio) continue;

/* Make up cur_loc */

unsigned int cur_loc = AFL_R(MAP_SIZE);

ConstantInt *CurLoc = ConstantInt::get(Int32Ty, cur_loc);

/* Load prev_loc */

LoadInst *PrevLoc = IRB.CreateLoad(AFLPrevLoc);
PrevLoc->setMetadata(M.getMDKindID("nosanitize"), MDNode::get(C, None));
Value *PrevLocCasted = IRB.CreateZExt(PrevLoc, IRB.getInt32Ty());

/* Load SHM pointer */

LoadInst *MapPtr = IRB.CreateLoad(AFLMapPtr);
MapPtr->setMetadata(M.getMDKindID("nosanitize"), MDNode::get(C, None));
Value *MapPtrIdx =
IRB.CreateGEP(MapPtr, IRB.CreateXor(PrevLocCasted, CurLoc));

/* Update bitmap */

LoadInst *Counter = IRB.CreateLoad(MapPtrIdx);
Counter->setMetadata(M.getMDKindID("nosanitize"), MDNode::get(C, None));
Value *Incr = IRB.CreateAdd(Counter, ConstantInt::get(Int8Ty, 1));
IRB.CreateStore(Incr, MapPtrIdx)
->setMetadata(M.getMDKindID("nosanitize"), MDNode::get(C, None));

/* Set prev_loc to cur_loc >> 1 */

StoreInst *Store =
IRB.CreateStore(ConstantInt::get(Int32Ty, cur_loc >> 1), AFLPrevLoc);
Store->setMetadata(M.getMDKindID("nosanitize"), MDNode::get(C, None));

inst_blocks++;

}

/* Say something nice. */

if (!be_quiet) {

if (!inst_blocks) WARNF("No instrumentation targets found.");
else OKF("Instrumented %u locations (%s mode, ratio %u%%).",
inst_blocks, getenv("AFL_HARDEN") ? "hardened" :
((getenv("AFL_USE_ASAN") || getenv("AFL_USE_MSAN")) ?
"ASAN/MSAN" : "non-hardened"), inst_ratio);

}

return true;

}
  • 获取 LLVMContext 进程上下文
  • 检测环境变量 AFL_INST_RATIO
    • 如果有则设置为 inst_ratio_str 并转为数字赋值给 inst_ratio 。检测 inst_ratio 是否在 0 到 100 内,如果不是则抛出异常
    • 否则设置 inst_ratio 为默认值 100
  • 获取共享内存 SHM 区域的指针和上一基本块的随机 ID
  • 开始插桩
    • 遍历每一个基本块(BB),如果 AFL_R(100) 大于等于 inst_ratio 则 continue
    • 给当前基本块的 ID (CurLoc)赋值为随机值 AFL_R(MAP_SIZE)
    • 插入 Load 指令,获取前一个基本块的 ID 并赋值给 PrevLocCasted
    • 插入 Load 指令,获取共享内存区域的指针 MapPtr
    • 使用 CreateGEP 获取 PrevLocCasted 和 CurLoc 异或的 MapPtrIdx
    • 插入 Load 指令,获取 MapPtrIdx 并使用 Store 指令将共享内存的指定索引处内存加一
    • 插入 Store 指令,更新上一个块的 ID (AFLPrevLoc)为 cur_loc >> 1
    • 将插桩计数器 inst_blocks 加一
  • 如果不是静默模式,则打印信息

整个过程和 afl-as 中的流程十分类似,不同的在于这里使用 llvm 的 pass ,使得插桩更加科学。

afl-llvm-rt.o.c

该文件实现了三个 llvm mode 中的主要功能:

  • deferred instrumentation
  • persistent mode
  • trace-pc-guard mode

全局定义

1
2
3
4
5
6
7
8
9
10
11
12
/* Globals needed by the injected instrumentation. The __afl_area_initial region
is used for instrumentation output before __afl_map_shm() has a chance to run.
It will end up as .comm, so it shouldn't be too wasteful. */

u8 __afl_area_initial[MAP_SIZE];
u8* __afl_area_ptr = __afl_area_initial;

__thread u32 __afl_prev_loc;

/* Running in persistent mode? */

static u8 is_persistent;

正确的初始化程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#define DEFER_ENV_VAR       "__AFL_DEFER_FORKSRV"
#define PERSIST_ENV_VAR "__AFL_PERSISTENT"

/* Proper initialization routine. */

__attribute__((constructor(CONST_PRIO))) void __afl_auto_init(void) {

is_persistent = !!getenv(PERSIST_ENV_VAR);

if (getenv(DEFER_ENV_VAR)) return;

__afl_manual_init();

}
  • 读取环境变量 __AFL_PERSISTENT 并赋值给 is_persistent
  • 如果环境变量中没有 __AFL_DEFER_FORKSRV 则返回,否则执行 __afl_manual_init 进行初始化

deferred instrumentation

AFL 会尝试通过只执行一次目标二进制文件来提升性能,在 main()之前暂停程序,然后克隆“主”进程获得一个稳定的可进行持续fuzz的目标。简言之,避免目标二进制文件的多次、重复的完整运行,而是采取了一种类似快照的机制。我们之前所查看的 afl-as 中也是这样的模式。

首先我们需要寻找稳定的克隆位置,并添加如下代码:

1
2
3
#ifdef __AFL_HAVE_MANUAL_CONTROL
__AFL_INIT();
#endif

其中 __AFL_INIT() 也就是 afl-clang-fast.c 中定义的宏定义:

1
2
3
4
5
6
7
8
9
10
11
cc_params[cc_par_cnt++] = "-D__AFL_INIT()="
"do { static volatile char *_A __attribute__((used)); "
" _A = (char*)\"" DEFER_SIG "\"; "
#ifdef __APPLE__
"__attribute__((visibility(\"default\"))) "
"void _I(void) __asm__(\"___afl_manual_init\"); "
#else
"__attribute__((visibility(\"default\"))) "
"void _I(void) __asm__(\"__afl_manual_init\"); "
#endif /* ^__APPLE__ */
"_I(); } while (0)";

会走到 __afl_manual_init 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* This one can be called from user code when deferred forkserver mode
is enabled. */

void __afl_manual_init(void) {

static u8 init_done;

if (!init_done) {

__afl_map_shm();
__afl_start_forkserver();
init_done = 1;

}

}

__afl_map_shm 函数会初始化共享内存:

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

static void __afl_map_shm(void) {

u8 *id_str = getenv(SHM_ENV_VAR);

/* If we're running under AFL, attach to the appropriate region, replacing the
early-stage __afl_area_initial region that is needed to allow some really
hacky .init code to work correctly in projects such as OpenSSL. */

if (id_str) {

u32 shm_id = atoi(id_str);

__afl_area_ptr = shmat(shm_id, NULL, 0);

/* Whooooops. */

if (__afl_area_ptr == (void *)-1) _exit(1);

/* Write something into the bitmap so that even with low AFL_INST_RATIO,
our parent doesn't give up on us. */

__afl_area_ptr[0] = 1;

}

}
  • 获取环境变量 SHM_ENV_VAR 并赋值给 id_str ,转为数字 shm_id 并使用 (shm_id, NULL, 0) 申请共享内存,将结果赋值给 __afl_area_ptr
  • 如果上述步骤出错则推出,否则将 __afl_area_ptr[0] 置为 1
    • 该步骤是为了防止在低 AFL_INST_RATIO 的情况下父进程也不会停止本进程

__afl_start_forkserver 函数会启动 fork server :

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
/* Fork server logic. */

static void __afl_start_forkserver(void) {

static u8 tmp[4];
s32 child_pid;

u8 child_stopped = 0;

/* Phone home and tell the parent that we're OK. If parent isn't there,
assume we're not running in forkserver mode and just execute program. */

if (write(FORKSRV_FD + 1, tmp, 4) != 4) return;

while (1) {

u32 was_killed;
int status;

/* Wait for parent by reading from the pipe. Abort if read fails. */

if (read(FORKSRV_FD, &was_killed, 4) != 4) _exit(1);

/* If we stopped the child in persistent mode, but there was a race
condition and afl-fuzz already issued SIGKILL, write off the old
process. */

if (child_stopped && was_killed) {
child_stopped = 0;
if (waitpid(child_pid, &status, 0) < 0) _exit(1);
}

if (!child_stopped) {

/* Once woken up, create a clone of our process. */

child_pid = fork();
if (child_pid < 0) _exit(1);

/* In child process: close fds, resume execution. */

if (!child_pid) {

close(FORKSRV_FD);
close(FORKSRV_FD + 1);
return;

}

} else {

/* Special handling for persistent mode: if the child is alive but
currently stopped, simply restart it with SIGCONT. */

kill(child_pid, SIGCONT);
child_stopped = 0;

}

/* In parent process: write PID to pipe, then wait for child. */

if (write(FORKSRV_FD + 1, &child_pid, 4) != 4) _exit(1);

if (waitpid(child_pid, &status, is_persistent ? WUNTRACED : 0) < 0)
_exit(1);

/* In persistent mode, the child stops itself with SIGSTOP to indicate
a successful run. In this case, we want to wake it up without forking
again. */

if (WIFSTOPPED(status)) child_stopped = 1;

/* Relay wait status to pipe, then loop back. */

if (write(FORKSRV_FD + 1, &status, 4) != 4) _exit(1);

}

}
  • 写入四字节到状态管道 199 ,告诉父进程准备完毕
  • 进行循环
    • 等待父进程,从控制管道 198 读取四字节内容,如果读取失败则抛出异常
    • 如果 child_stopped 为 0 ,则 fork 出一个子进程,fork 失败则退出。否则关闭控制管道和状态管道并跳出循环(接下来的循环操作都是父进程的操作)
    • 如果 child_stopped 为 1 ,且处于 persistent mode 下,此时子进程还存活,只是被暂停了,通过 kill(child_pid, SIGCONT) 来重启并设置 child_stopped 为 0
    • 写入四字节到状态管道 199 ,并等待子进程,将状态赋值给 status
    • 使用 WIFSTOPPED 宏确定 status 是否对应于一个暂停的子进程,在 persistent mode 下子进程会通过 SIGSTOP 来暂停自己并指示运行成功。所以设置 child_stopped 为 1 (这样就会进入上面的唤醒流程)
    • 子进程运行结束后,写入四字节到状态管道 199 告知 AFL 本次运行结束,开始下一次循环

该过程类似 afl-as 中的 main_payload ,不同之处在于加了 persistent mode 部分的逻辑

persistent mode

相比于 deferred instrumentation ,persistent mode 并没有通过 fork 子进程来进行 fuzz 。一些库中的 API 是无状态的,可以在执行不同输入的时候进行重置,那么使用长期存活的进程来测试多个用例,以此来减少 fork 所带来的开销。

我们需要添加如下代码:

1
2
3
4
5
6
7
8
9
while (__AFL_LOOP(1000)) {

/* Read input data. */
/* Call library code to be fuzzed. */
/* Reset state. */

}

/* Exit normally */

主要的步骤是进行一个循环:

  • 读取输入数据
  • 调用库代码进行 fuzz
  • 重置状态

循环的次数也就是 AFL 从头开始重新启动的最大迭代次数,官方建议数值为 1000,太大了可能出现很多意想不到的问题。

其中 __AFL_LOOP() 也就是 afl-clang-fast.c 中定义的宏定义:

1
2
3
4
5
6
7
8
9
10
11
cc_params[cc_par_cnt++] = "-D__AFL_LOOP(_A)="
"({ static volatile char *_B __attribute__((used)); "
" _B = (char*)\"" PERSIST_SIG "\"; "
#ifdef __APPLE__
"__attribute__((visibility(\"default\"))) "
"int _L(unsigned int) __asm__(\"___afl_persistent_loop\"); "
#else
"__attribute__((visibility(\"default\"))) "
"int _L(unsigned int) __asm__(\"__afl_persistent_loop\"); "
#endif /* ^__APPLE__ */
"_L(_A); })";

会走到 ___afl_persistent_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
43
44
45
46
47
48
49
50
51
52
53
/* A simplified persistent mode handler, used as explained in README.llvm. */

int __afl_persistent_loop(unsigned int max_cnt) {

static u8 first_pass = 1;
static u32 cycle_cnt;

if (first_pass) {

/* Make sure that every iteration of __AFL_LOOP() starts with a clean slate.
On subsequent calls, the parent will take care of that, but on the first
iteration, it's our job to erase any trace of whatever happened
before the loop. */

if (is_persistent) {

memset(__afl_area_ptr, 0, MAP_SIZE);
__afl_area_ptr[0] = 1;
__afl_prev_loc = 0;
}

cycle_cnt = max_cnt;
first_pass = 0;
return 1;

}

if (is_persistent) {

if (--cycle_cnt) {

raise(SIGSTOP);

__afl_area_ptr[0] = 1;
__afl_prev_loc = 0;

return 1;

} else {

/* When exiting __AFL_LOOP(), make sure that the subsequent code that
follows the loop is not traced. We do that by pivoting back to the
dummy output region. */

__afl_area_ptr = __afl_area_initial;

}

}

return 0;

}
  • 判断是否是第一次执行循环
    • 如果设置了 is_persistent
      • 清空 __afl_area_ptr
      • 将 __afl_area_ptr[0] 设为 1
      • 将 __afl_prev_loc 设为 0
    • 设置 cycle_cnt 为 max_cnt ,first_pass 为 1 并返回 1
  • 如果设置了 is_persistent
    • 递减 cycle_cnt ,如果结果不为 0
      • 发出 SIGSTOP 信号让程序暂停
      • 将 __afl_area_ptr[0] 设为 1
      • 将 __afl_prev_loc 设为 0
      • 返回 1
    • 否则将 __afl_area_ptr 置为 __afl_area_initial (无关数组)
  • 返回 0

trace-pc-guard mode

该功能需要设置宏 AFL_TRACE_PC=1 ,并且在使用 afl-clang-fast 时传入参数 -fsanitize-coverage=trace-pc-guard

该功能的主要特点是会在每个 edge 插入桩代码,也就是都会调用 __sanitizer_cov_trace_pc_guard 函数:

1
2
3
4
5
6
7
8
9
10
/* The following stuff deals with supporting -fsanitize-coverage=trace-pc-guard.
It remains non-operational in the traditional, plugin-backed LLVM mode.
For more info about 'trace-pc-guard', see README.llvm.

The first function (__sanitizer_cov_trace_pc_guard) is called back on every
edge (as opposed to every basic block). */

void __sanitizer_cov_trace_pc_guard(uint32_t* guard) {
__afl_area_ptr[*guard]++;
}

简单的对 __afl_area_ptr 对应位置出进行递增

其中 guard 的初始化位于函数 __sanitizer_cov_trace_pc_guard_init 中:

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
/* Init callback. Populates instrumentation IDs. Note that we're using
ID of 0 as a special value to indicate non-instrumented bits. That may
still touch the bitmap, but in a fairly harmless way. */

void __sanitizer_cov_trace_pc_guard_init(uint32_t* start, uint32_t* stop) {

u32 inst_ratio = 100;
u8* x;

if (start == stop || *start) return;

x = getenv("AFL_INST_RATIO");
if (x) inst_ratio = atoi(x);

if (!inst_ratio || inst_ratio > 100) {
fprintf(stderr, "[-] ERROR: Invalid AFL_INST_RATIO (must be 1-100).\n");
abort();
}

/* Make sure that the first element in the range is always set - we use that
to avoid duplicate calls (which can happen as an artifact of the underlying
implementation in LLVM). */

*(start++) = R(MAP_SIZE - 1) + 1;

while (start < stop) {

if (R(100) < inst_ratio) *start = R(MAP_SIZE - 1) + 1;
else *start = 0;

start++;

}

}
  • 从环境变量 AFL_INST_RATIO 中读取 inst_ratio ,且检查 inst_ratio 是否在 0 到 100 中间,不是的话抛出错误,inst_ratio 的默认值为 100
  • 对于 start 和 stop ,就是 guard 的起始和中止地
  • 从 start 开始依次获得随机值 R(100) ,并检查其是否小于 inst_ratio
    • 如果小于,则将当前的 guard 赋值为 R(MAP_SIZE - 1) + 1
    • 否则赋值为 0
    • 第一个 guard 总是 R(MAP_SIZE - 1) + 1

可以看的出 trace-pc-guard mode 的插桩逻辑也和之前分析的别的方法倒差不差。