这篇记录 llvm mode 中的 afl-llvm-pass.so.cc 文件和 afl-llvm-rt.o.c 文件,以及整体流程的简述。
对llvm这部分理解还比较浅。
可以阅读大佬的这篇文章:https://eternalsakura13.com/2020/08/23/afl/#more,讲的很透彻。
afl-llvm-pass.so.cc
using namespace llvm;
namespace {
// 在命名空间中定义一个继承自ModulePass的类AFLCoverage。ModulePass是最通用的一个父类,继承该类表明此Pass将整个程序作为一个单元
// 因此选择继承不同类型的Pass父类就是从不同的粒度对程序进行处理。这里选择继承ModulePass就是对整个程序都进行处理。
class AFLCoverage : public ModulePass {
public:
static char ID; // LLVM识别Pass的标识符
AFLCoverage() : ModulePass(ID) { }
bool runOnModule(Module &M) override;
// StringRef getPassName() const override {
// return "American Fuzzy Lop Instrumentation";
// }
};
}
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 */
// 打印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.
获取指向 SHM 区域和上一个块ID的全局变量指针。 注意 __afl_prev_loc 是线程本地的
*/
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) // 遍历Module中的每个
for (auto &BB : F) { // 遍历Function中的每个BasicBlock
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); // 随机生成当前块的ID
ConstantInt *CurLoc = ConstantInt::get(Int32Ty, cur_loc);
/* Load prev_loc */
// 加载前驱块的ID
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 */
// 更新位图,将对应位置加1,代表边的执行次数加1
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 */
// 更新全局变量AFLPrevLoc的值为 当前基本块的ID右移一位
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;
}
// 注册Pass
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);
使用下面两条指令可以分别得到插桩前和插桩后的IR文件
clang -emit-llvm -S test.c -o a.ll # a.ll插桩前的IR
afl-clang-fast -emit-llvm -S test.c -o b.ll # b.ll插桩后的IR
afl-llvm-rt.o.c
根据README文件可知,该文件主要提供了三个额外的功能
deferred instrumentation
AFL会尝试通过仅执行一次目标二进制文件来优化性能。它会暂停控制流,然后复制该“主”进程以持续提供fuzzer的目标。该功能在某些情况下可以减少操作系统、链接与libc内部执行程序的成本。
要想使用这个功能,需要在合适的位置插入如下代码。
#ifdef __AFL_HAVE_MANUAL_CONTROL __AFL_INIT(); #endif
需要注意的是,不能在如下几个位置插入上述代码
- 任何重要线程或子进程的创建 - 因为 forkserver 无法轻松克隆它们。
- 通过 setitimer() 或等效调用初始化计时器。
- 临时文件、network sockets、偏移敏感文件描述符和类似的共享状态资源的创建——但前提是它们的状态会有意义地影响以后程序的行为。
- 对模糊册数输入的任何访问,包括读取有关其大小的元数据。
然后 afl-clang-fast 重新编译即可。需要注意的是afl-gcc 和 afl-clang 是没有这个功能的。 这很容易理解,因为关于
__AFL_HAVE_MANUAL_CONTROL
和__AFL_INIT()
的定义是在 afl-clang-fast 在处理参数时添加的,再交由真正的编译器处理。回顾上一篇打印出来的afl-clang-fast处理后的参数
其中__AFL_INIT()展开后大概就是这样。摘自https://eternalsakura13.com/2020/08/23/afl/#more
#define __AFL_INIT() \ do { \ static char *_A; \ _A = (char*)"##SIG_AFL_DEFER_FORKSRV##"; \ __afl_manual_init(); \ } while (0)
可以看到调用了
__afl_manual_init
,该函数在未初始化的情况下调用__afl_map_shm
和__afl_start_forkserver
设置共享内存和初始化forkserver
1. __afl_map_shm 设置共享内存
- 获取环境变量 SHM_ENV_VAR 赋值给
id_str
- 如果环境变量存在
- 转换为整数,赋值给
shm_id
- 将共享内存映射到当前进程的地址空间,
__afl_area_ptr
指向该地址 - 如果
__afl_area_ptr
= -1,表示映射失败,退出程序 - 将共享内存的第一个位置置1。源码注释解释道:在位图中写入一些内容,这样即使 AFL_INST_RATIO 较低,我们的父进程也不会放弃我们
- 转换为整数,赋值给
2. __afl_start_forkserver
child_stopped
= 0- 向状态管道中写入4字节,通知fuzzer,fork server准备完毕。
tmp
无论为何值,只要能正确写入或是读取四个字节就可以。 - 进入while循环
- 从控制管道读取父进程传来的命令
- 如果
child_stopped
为 1并且was_killed
不为0,表示如果子进程处于暂停状态,并且被kill了,则等待子进程彻底结束- 置
child_stopped
= 0 - 等待子进程结束
- 置
- 如果
child_stopped
为 0- fork出一个子进程
- 子进程关闭状态管道和控制管道,return恢复执行
- 如果
child_stopped
为1,这是对于persistent mode的特殊处理,此时子进程还活着,只是被暂停了,所以可以通过kill(child_pid, SIGCONT)
来简单的重启,然后设置child_stopped
为0。 - 将PID发送给Fuzzer
- 等待子进程结束,注意这里在persistent mode时,会设置waitpid的第三个参数为WUNTRACED,代表若子进程进入暂停状态,则该函数马上返回
- WIFSTOPPED(status)如果子进程的结束状态为暂停,置
child_stopped
= 1 - 将子进程的结束状态发送给Fuzzer
persistent mode
该功能也仅适用于 afl-clang-fast
persistent mode的功能是将已经完成fuzz的一个testcase的子进程进行复用,从而节省计算机开销,但是需要注意的是每次fuzz过程都会改变一些进程或线程的状态变量,因此,在复用这个fuzz子进程的时候需要将这些变量恢复成初始状态,否则会导致下一次fuzz过程的不准确。所以状态初始化的工作只对第一个循环做。之后的初始化工作都交给父进程。
程序的基本结构
while (__AFL_LOOP(1000)) { /* Read input data. */ /* Call library code to be fuzzed. */ /* Reset state. */ }
循环次数为1000是因为这最大限度地减少了内存泄漏和类似故障的影响,更高的值会增加“打嗝”的可能性,从而没有了性能优势。
__AFL_LOOP
也是在 afl-clang-fast 在处理参数时添加的#define __AFL_LOOP() \ do { \ static char *_B; \ _B = (char*)"##SIG_AFL_PERSISTENT##"; \ __afl_persistent_loop(); \ }while (0)
3. __afl_persistent_loop
first_pass
= 1- 如果
first_pass
为 1,即表示第一次执行- 如果在persistent模式下
- 共享内存清空为0
__afl_area_ptr[0]
= 1,应该跟上面__afl_map_shm中最后一步的作用一样__afl_prev_loc
= 0,前一个基本块ID置0
cycle_cnt
置为max_cnt
first_pass
置为0- return 1
- 如果在persistent模式下
- 如果不是第一次执行
- 如果在persistent模式下
cycle_cnt
减1 如果大于0- 发出SIGSTOP信号,暂停当前进程
__afl_area_ptr[0]
= 1__afl_prev_loc
= 0- return 1
- 如果
cycle_cnt
减1 如果等于 0__afl_area_ptr
指向一个虚拟的区域
4. __afl_auto_init
需要注意的是 被 _attribute_ constructor 修饰的函数将在main执行之前自动运行
- 读取环境变量PERSIST_ENV_VAR的值,赋值给
is_persistent
,表示以persistent模式fuzz - 如果存在环境变量 DEFER_ENV_VAR ,直接返回。这代表
__afl_auto_init
和deferred instrumentation不通用,因为deferred instrumentation会自己选择合适的时机,手动init,不需要用这个函数来init,所以这个函数只在没有手动init的时候会自动init。 - 调用
__afl_manual_init
,该函数在未初始化的情况下调用__afl_map_shm
和__afl_start_forkserver
设置共享内存和初始化fork server
trace-pc-guard mode
如果想尝试一下这个功能,则需要执行这条代码来重新编译afl-clang-fast,从而在执行afl-clang-fast的时候传入
-fsanitize-coverage=trace-pc-guard
参数,来开启这个功能,和之前我们的插桩不同,开启了这个功能之后,我们不再是仅仅只对每个基本块插桩,而是对每条edge都进行了插桩。AFL_TRACE_PC=1 make clean all
5. __sanitizer_cov_trace_pc_guard
__sanitizer_cov_trace_pc_guard
这个函数将在每个edge调用,该函数利用函数参数guard指针所指向的uint32值来确定共享内存上所对应的地址。
每个edge上都有应该有其不同的guard值
6. __sanitizer_cov_trace_pc_guard_init
inst_ratio
插桩概率为100- 如果
start
等于stop
或是,直接return - 获取环境变量"AFL_INST_RATIO"的值赋给
x
- 如果环境变量存在,转换为整数赋值给
inst_ratio
- 如果
inst_ratio
的值不合法,抛出异常 - 遍历
start
至stop
,设置为[1, MAP_SIZE)中的一个随机数,为0则表示对这个edge不进行插桩
整体流程简述
- 使用
afl-clang-fast test.c -o a.out
编译test.c
文件后 afl-clang-fast
会处理参数,这期间会将afl-llvm-pass.so
添加到参数当中- llvm根据
afl-llvm-pass.so
中的代码对文件进行插桩 - forkserver 是何时启动的呢?注意
afl-clang-fast
最后还将afl-llvm-rt.o
添加到参数当中 - 在
afl-llvm-rt.o
当中有这么个__attribute__((constructor(CONST_PRIO))) void __afl_auto_init(void)
函数,会在main函数前自动运行 - 于是就调用了
__afl_manual_init()
__afl_manual_init()
有调用了__afl_map_shm()
和__afl_start_forkserver()
就将共享内存映射到当前进程的地址空间中了,以及启动了fork server- 之后,就正常fuzz了