AFL源码阅读笔记(二)—— llvm_mode 和 pass 源码

书接上回:AFL源码阅读笔记(一)—— gcc 普通插桩

上一篇文章中我们分析了传统编译器(gcc、clang)背景下进行插桩,整体而言比较粗暴,思路是碰到可插桩的情况,通过 trampoline 跳到插桩代码(在 afl-as.h 中),将相应的汇编代码插入。LLVM(low-level virtual machine)作为先进的编译器套件,在它的基础上可以做更多有想象力的工作。
建议:使用 ubuntu 18.04 或 ubuntu 20.04 作为 llvm 开发环境。

三、llvm_mode 插桩

这部分源码在 llvm_mode 文件夹下,包含三个代码文件,分别是 afl-clang-fast.c,afl-llvm-pass.so.cc,afl-llvm-rt.o.c。

3.1 LLVM 及其 Pass

Pass 直译可以理解为 “趟”,在这一趟处理中,可以对 LLVM IR 进行如修改、插入等操作。具体见 我的另一篇博客 。

AFL源码阅读笔记(二)—— llvm_mode 和 pass 源码_第1张图片

3.2 afl-clang-fast.c — 把 clang 包起来

这个文件和 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 等内存问题。

3.3 afl-llvm-pass.so.cc ️ — 用于插桩的 Pass

首先,导入 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);

3.3.1 pass 中最重要的函数 runOnModule ️

(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 中对其定义是

  • 设置 AFL_QUIET 将在编译过程中不显示 afl-cc 和 afl-as 的横幅标语,以防你觉得它们令人分心。

(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);
}

3.4 afl-llvm-rt.o.c ️ — 提供三个特殊功能

这一部分是与 runtime 相关的源码,它为 LLVM mode 提供了三种特殊功能。

3.4.1 trace-pc-guard mode — 插桩 Edge

使用该功能需要设置 USE_TRACE_PC 宏为 1,本质上是在编译时传入参数,使 CFLAGSCXXFLAGS 中存在 -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 */

以上是它与其他文件的交互,该部分功能的实现主要涉及以下两个函数:

  1. __sanitizer_cov_trace_pc_guard()
  2. __sanitizer_cov_trace_pc_guard_init()
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 总数。

你可能感兴趣的:(AFL,llvm,afl,fuzzing)