书接上回:AFL源码阅读笔记(一)—— gcc 普通插桩
上一篇文章中我们分析了传统编译器(gcc、clang)背景下进行插桩,整体而言比较粗暴,思路是碰到可插桩的情况,通过 trampoline 跳到插桩代码(在 afl-as.h 中),将相应的汇编代码插入。LLVM(low-level virtual machine)作为先进的编译器套件,在它的基础上可以做更多有想象力的工作。
建议:使用 ubuntu 18.04 或 ubuntu 20.04 作为 llvm 开发环境。
这部分源码在 llvm_mode 文件夹下,包含三个代码文件,分别是 afl-clang-fast.c,afl-llvm-pass.so.cc,afl-llvm-rt.o.c。
Pass 直译可以理解为 “趟”,在这一趟处理中,可以对 LLVM IR 进行如修改、插入等操作。具体见 我的另一篇博客 。
这个文件和 afl-gcc.c 类似,起到的是包装类的作用,只不过包装的是 Clang。文件导入了四个自己写的头文件,config.h、types.h、debug.h 和 alloc-inl.h,作用如下:
config.h
,配置类头文件,包括如版本号、超时时间、记录 unique crash 的最大数量等通用设定,还有一些作为 interesting 的特殊值如边界值、零值等。最后部分是开发者可能不想关注的参数(Really exotic stuff you probably don’t want to touch)。types.h
,对一些数据结构的重命名,如 uint8_t 取名为 u8;对一些数据结构的新定义。debug.h
,用于调试的各种宏及函数定义。如 OKF(“…”) 输出成功信息,高级的printf。alloc-inl.h
,做内存相关的检查,它的设计不是为了抵抗恶意攻击,而是提供健壮方便的方式检测 use-after-free,off-by-one writes,stale pointers 等内存问题。首先,导入 llvm 相关的开发包
#include "llvm/ADT/Statistic.h" // ADT = Advanced Data Type,llvm 为业务逻辑定义的高性能抽象类型
#include "llvm/IR/IRBuilder.h" // IR,中间表示
#include "llvm/IR/LegacyPassManager" // 采用 legacy 方式编辑 pass
#include "llvm/IR/Module.h"
#include "llvm/Support/Debug.h"
#include "llvm/Transforms/IPO/PassManagerBuilder.h"
using namespace llvm;
然后,在匿名命名空间中声明 pass。匿名命名空间是 C++ 的特性,指的是没有名字的命名空间,用处如下:
(1)匿名命名空间里的内容只能被当前代码文件调用,不能被外部引用;
(2)匿名命名空间中声明的变量和全局变量相同,声明的函数和添加了 static 关键字的函数相同。
namespace {
// 继承 ModulePass,作用范围是整个程序
class AFLCoverage: public ModulePass {
public:
static char ID; // pass的ID
AFLCoverage() : ModulePass(ID) { } // 显式调用父类构造函数
bool runOnModule(Module &M) override;
};
}
runOnModule 函数末尾声明为 override,意味着它将重写从 ModulePass 继承来的同名函数。所谓 runOnModule 就是以模块为单位进行处理,LLVM 会按一次一个模块交给该函数处理。在类外初始化静态成员变量 ID,这个 ID 是 pass 的标识符,用于 LLVM 识别 pass;runOnModule 也在类外实现,这部分实现 pass 的具体内容。AFL 的 pass 只实现一个功能,就是记录覆盖率。
char AFLCoverage::ID = 0;
bool AFLCoverage::runOnModule(Module &M) { ... }
实现 runOnModule 后,注册 pass 到 PassManager 中,每个 pass 彼此独立,由 PM 统一注册和调度,更加模块化。
static void registerAFLPass(const PassManagerBuilder &,
legacy::PassManagerBase &PM) {
PM.add(new AFLCoverage());
}
最后需要注册到 RegisterStandardPasses 结构体中,代码采用类似类的构造函数的方式初始化。
例如,对于 C++ 中结构体 Stu
struct Stu {
Stu(int a, int b) { this->a = a; this->b = b; }
int a; int b;
};
定义了构造函数,就可以采用 Stu s(1, 2); 的方式初始化结构体。
static RegisterStandardPasses RegisterAFLPass(
PassManagerBuilder::EP_ModuleOptimizerEarly, registerAFLPass);
static RegisterStandardPasses RegisterAFLPass0(
PassManagerBuilder::EP_EnabledOnOptLevel0, registerAFLPass);
(1)首先是获取线程上下文和声明 8 位和 32 位整型类型实例。
llvm.org:LLVMContext 是在线程上下文中使用 LLVM 的一个重要类。它(opaquely)拥有并管理 LLVM 的核心全局数据,包括类型和常量唯一表(constant uniquing tables)。LLVM 本身不提供锁保证,因此可以小心地(carefully)为每个线程提供一个上下文。
LLVMContext &C = M.getContext();
IntegerType *Int8Ty = IntegerType::getInt8Ty(C);
IntegerType *Int32Ty = IntegerType::getInt32Ty(C);
(2)然后是检查 AFL_QUIET,在 afl/docs/env_variables.txt 中对其定义是
(3)接着设置插桩比例(密度)
/* Decide instrumentation ratio */
// 插桩率必须在 1 到 100 间
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)");
}
}
(4)定义两个全局变量,AFLMapPtr 和 AFLPrevLoc。前者是指向共享内存的指针,后者记录前一个基本块(已右移一位)的编号。SHM = SHared Memory。
llvm.org 中 GlobalVariable 的构造函数声明为:
GlobalVariable (Module &M, Type *Ty, bool isConstant, LinkageTypes Linkage, Constant *Initializer, const Twine &Name="", GlobalVariable *InsertBefore=nullptr, ThreadLocalMode=NotThreadLocal, std::optional< unsigned > AddressSpace=std::nullopt, bool isExternallyInitialized=false)
/* 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);
(5)插桩所有代码。对于 Module(整个程序)中每个函数的每个基本块,先获取其第一条指令的迭代器。然后使用迭代器创建一个 IRBuilder 的实例,通过该实例就可以方便地创建 IR 指令(create IR instructions),并将这些指令插在迭代器所在位置。
llvm.org 中对 IRBuilder 的说明是,IRBuilder 提供了一个统一的API,用于创建指令并将它们插入到基本块中:要么在 BasicBlock 的末尾,要么在块中的特定迭代器位置。⚠️ Note:IRBuilder 未对所有 LLVM 指令提供支持。为了访问额外的指令属性,在指令被创建后直接访问它们,如 LoadInst::setVolatile() 直接使用 setVolatile。
int inst_blocks = 0; // 基本块计数器
for (auto &F : M) // Module 中的所有函数
for (auto &BB : F) { // 一个函数中的所有基本块
BasicBlock::iterator IP = BB.getFirstInsertionPt();
IRBuilder<> IRB(&(*IP));
然后生成当前基本块编号。AFL_R 在 types.h 中定义,# define AFL_R(x) (random() % (x))
,即产生一个 x 以内的随机数。代码中的 x 是 MAP_SIZE,在 config.h 中定义,默认值为 64 KB(216)。
/* Make up cur_loc */
unsigned int cur_loc = AFL_R(MAP_SIZE);
ConstantInt *CurLoc = ConstantInt::get(Int32Ty, cur_loc);
接着加载前一个基本块的编号。在 LLVM 中,无论是全局变量还是局部变量都是指针类型,因此都需要 CreateLoad() 来获取值,CreateStore() 来赋值,两者构成对内存的读写指令,通过 CreateZExt() 完成相应的类型转换。setMetadata 视为调试信息???
/* Load prev_loc */
LoadInst *PrevLoc = IRB.CreateLoad(AFLPrevLoc);
PrevLoc->setMetadata(M.getMDKindID("nosanitize"), MDNode::get(C, None));
Value *PrevLocCasted = IRB.CreateZExt(PrevLoc, IRB.getInt32Ty());
接着加载共享内存,并根据当前基本块编号和前驱基本块编号计算出 key 值,更新 bitmap。CreateLoad 取出全局变量共享内存指针 AFLMapPtr,CreateGEP = GetElementPtr,CreateXor() 对当前 BB 编号值和前驱 BB 编号值做异或,得到一个 异或结果在共享内存中的地址 的指针 MapPtrIdx 。
所谓 bitmap,就是以一个 bit 位作为 key,key 对应的 value 是基本块间的边覆盖情况(如该边的命中次数)。
更新 bitmap 时,读到 MapPtrIdx 地址的值,就是边的命中次数,记为 Counter。通过 CreateAdd() 加 1 的方式使命中次数加 1,再通过 CreateStore() 将新的命中次数写回到 MapPtrIdx 地址上。
/* 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));;
当前基本块右移一位,作为下一次计算的前驱基本块,基本块计数器自增 1。
/* 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++;
(6)此时已退出(5)中的两个 for 嵌套循环,进行一些收尾工作,即如何打印信息。
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);
}
这一部分是与 runtime 相关的源码,它为 LLVM mode 提供了三种特殊功能。
使用该功能需要设置 USE_TRACE_PC
宏为 1,本质上是在编译时传入参数,使 CFLAGS
和 CXXFLAGS
中存在 -fsanitize-coverage=trace-pc-guard
参数。开启该功能后,AFL 将不再只是对每个基本块插桩,而是对每条 Edge 也进行插桩。
以下是 Makefile 中的代码,
ifdef AFL_TRACE_PC
CFLAGS += -DUSE_TRACE_PC=1
endif
相当于在头文件中加上宏定义
#define USE_TRACE_PC 1
而在 afl-clang-fast.c
中有对 USE_TRACE_PC
宏的处理,可见是在 cc_params
数组中加入了参数。
#ifdef USE_TRACE_PC
cc_params[cc_par_cnt++] = "-fsanitize-coverage=trace-pc-guard";
cc_params[cc_par_cnt++] = alloc_printf("%s/afl-llvm-pass.so", obj_path);
#endif /* ^USE_TRACE_PC */
以上是它与其他文件的交互,该部分功能的实现主要涉及以下两个函数:
void __sanitizer_cov_trace_pc_guard(uint32_t* guard) {
__afl_area_ptr[*guard]++;
}
__sanitizer_cov_trace_pc_guard
函数会在每条边上被回调,它利用函数参数 guard
指针所指向的 uint32
值来确定共享内存上的对应地址。__afl_area_ptr
是共享内存地址,默认为 0,此处 ++
置其为 1 表示该边已被覆盖。
void __sanitizer_cov_trace_pc_guard_init(uint32_t* start, uint32_t* stop) {
u32 inst_ratio = 100; // 默认插桩概率为 100%
u8* x; // 用于存放 AFL_INST_RATIO 中定义的插桩概率
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();
}
/* 确保范围中的第一位肯定被设置,值为 MAP_SIZE -1 内的随机数再加一
设置完成后,指针向后移动一位 */
*(start++) = R(MAP_SIZE - 1) + 1;
while (start < stop) {
if (R(100) < inst_ratio) *start = R(MAP_SIZE - 1) + 1; // 插桩一个随机 edge ID
else *start = 0; // 不插桩
start++; // 指针向后移动一位
}
}
__sanitizer_cov_trace_pc_guard_init
函数填充插桩 ID(instrumentation ID),0 表示未被插桩的 bit 位。如果程序中的 edge 数较多,而 MAP_SIZE
不够大,就有可能重复。在设置 edge ID 值时需要加一,是因为可能存在 R(MAP_SIZE - 1) == 0
的情况,但插桩 ID 不可以为 0。在该函数中,我们可以打印输出一下 stop - start
的值,就代表了 LLVM 发现的程序中的 edge 总数。