AFL-QBDI与AFL-Unicorn实战

  • A+
所属分类:安全开发





以上文章由作者【hackedbylh】的连载有赏投稿,共有五篇,本文为第五篇;也欢迎广大朋友继续投稿,详情可点击OSRC重金征集文稿!!!了解~~
温馨提示:建议投稿的朋友尽量用markdown格式,特别是包含大量代码的文章



概述

本节首先以 whatsapp 为例介绍如何使用 AFL-QBDI模式来测试 Android Native库,并介绍 AFL-Unicorn 模式的使用。

Fuzz Android Native库

为了能够Fuzz Android Native库,笔者基于QBDI框架为AFLplusplus新增一种Fuzz模式,代码和文档如下
https://github.com/vanhauser-thc/AFLplusplus/tree/master/qbdi_mode
qbdi_mode的工作方式是在template.cpp里面把目标库加载到内存,然后设置好被测函数需要的参数,最后调用目标函数。在这个过程里面会使用QBDI的API获取到目标程序的覆盖率信息并启动forkserver和AFL通信,从而实现Fuzz。

本节以Whatsapp为例,介绍如何Fuzz Android的Native库。在Whatsapp APK中的libwhatsapp.so里面的Java_com_whatsapp_Mp4Ops_mp4check函数是一个JNI函数
__int64 __fastcall Java_com_whatsapp_Mp4Ops_mp4check(JNIEnv_ *a1, __int64 a2, __int64 a3, char a4){// 转换UTF-8字符串到 C 字符串 fpath = (a1->functions->GetStringUTFChars)(a1, v5, 0LL); clock_gettime(1, &tp);
// 目标函数, 处理视频文件 v8 = sub_79C70(fpath, &tp, 2 * (v4 == 0), 0);
根据JNI函数的调用约定和Java层的参数信息可以发现该函数的参数类型大概为
a1: JNIEnva2: jobjecta3: 文件路径a4: 一个bool变量,为0或者1
Java_com_whatsapp_Mp4Ops_mp4check函数最终会把文件路径转换成C字符串并传入sub_79C70函数,后面再也没有用过这个文件路径,那么这个函数应该就是实际处理文件的函数,而且函数的参数比较简单,那么我们就可以直接去Fuzz这个函数,修改template.cpp在main函数里面获取到sub_79C70函数的地址
void *offset_func = dlsym(handle, "Java_com_whatsapp_Mp4Ops_mp4check");
if (NULL == offset_func) {printf("getprocaddress errorn");return 1; }
p_target_func = (target_func)((unsigned char *)offset_func + 0x45af0);printf("target function addr: %xn", p_target_func);
然后修改fuzz_func,构造好sub_79C70函数需要的参数,然后调用它。
QBDI_NOINLINE int fuzz_func() {
if (afl_setup()) { afl_forkserver(); }
unsigned long len = 0;char * data = read_file(FPATH, &len);
printf("In fuzz_funcn");struct timespec tp; clock_gettime(1, &tp); p_target_func(FPATH, &tp, 1);printf("execute p_target_func:%pn", p_target_func);exit(0);return 1;}
编译完成后要能够执行被测程序需要先把libQBDI.so放到安卓设备上并设置LD_LIBRARY_PATH为libQBDI.so和libwhatsapp.so所在的路径,因为libwhatsapp.so还依赖一些APK里面的库。
# find / -name libwhatsapp.so 2>o/data/app/com.whatsapp-wMSOMeRwydbzJJmi-G1wEw==/lib/x86_64/libwhatsapp.so# ls ./libQBDI.so./libQBDI.so# pwd/data/lsl# export LD_LIBRARY_PATH=/data/lsl:/data/app/com.whatsapp-wMSOMeRwydbzJJmi-G1wEw==/lib/x86_64/# ./loader /data/app/com.whatsapp-wMSOMeRwydbzJJmi-G1wEw==/lib/x86_64/libwhatsapp.so xxx.mp4target function addr: 7eaf9c70In fuzz_funcexecute p_target_func:0x702f7eaf9c70
确保程序能够正常执行后下面用AFL去Fuzz即可
./afl-fuzz -i mp4in/ -o mp4out -m 5000 -t 3000 -p exploit -- ./loader /data/app/com.whatsapp-wMSOMeRwydbzJJmi-G1wEw==/lib/x86_64/libwhatsapp.so @@
AFL-QBDI与AFL-Unicorn实战

基于unicorn的Fuzz

前言
unicorn 是一模拟执行框架,该框架可以模拟执行Arm, Arm64 (Armv8), M68K, Mips, Sparc,  X86等指令集的程序。unicorn基于qemu实现,与qemu不一样的是unicorn提供了丰富的接口,让用户可以控制并监控目标程序的执行。比如用户可以设置模拟环境中的寄存器的值,映射内存以及设置内存的值等,此外还可以通过注册事件回调函数的方式来监控代码的执行流程。

使用unicorn来进行Fuzz的场景主要是测试一些难以正常执行的程序中的一些关键的代码片段,比如IOT固件程序,内核及TrustZone中的程序。这些类型的程序难以在常规的Linux服务器中运行,于是我们可以通过unicorn来模拟执行程序中的处理数据的高风险函数,实现程序的局部Fuzz。

在AFLplusplus中已经集成了unicorn模式
https://github.com/vanhauser-thc/AFLplusplus/tree/master/unicorn_mode
下载AFLplusplus首先编译好afl-fuzz,然后进入unicorn_mode目录,运行build_unicorn_support.sh,就会下载并编译专门为AFL定制的unicorn。
$ cd unicorn_mode$ ./build_unicorn_support.sh
下面以几个例子为例来介绍如何使用unicorn来Fuzz程序。

x86 demoorn

本节的被测程序是 `persistent_target_x86_64` ,这是一个 x64 的 ELF可执行文件,待测函数是 main 函数
int __cdecl main(int argc, const char **argv, const char **envp){size_t v4; // raxsize_t v5; // [rsp+20h] [rbp-10h]char *s; // [rsp+28h] [rbp-8h]
if ( argc <= 1 )return -1; s = (char *)argv[1]; v5 = strlen(argv[1]);if ( v5 <= 0x13 )return -2;while ( 1 ) { v4 = v5--;if ( !v4 )break;if ( v5 <= 0x11 && v5 > 2 && v5 <= 0x11 ) s[v5] = s[v5 + 1]; // s 不可写 }if ( *s > 16 && *s <= 31 ) { s[1]; s[2]; }return 0;}
函数的主要逻辑是解析 argv[1],当输入的数据符合一定的格式时会修改 argv[1],为了演示这里设置argv[1]为不可写的内存,所以我们的目标是让AFL进入写 argv[1]的路径,进而导致crash。

下面看看Fuzz该函数的unicorn适配代码
int main(int argc, char **argv, char **envp) {bool tracing = false;// 首先获取输入文件的参数char *filename = argv[1];if (argc > 2 && !strcmp(argv[1], "-t")) { tracing = true; filename = argv[2]; }
// 创建一个unicorn实例, 架构是 x86(UC_ARCH_X86), 64位(UC_MODE_64) err = uc_open(UC_ARCH_X86, UC_MODE_64, &uc);if (err) {printf("Failed on uc_open() with error returned: %u (%s)n", err, uc_strerror(err));return -1; }
- 首先从命令行参数中获取输入文件的路径,后面程序会从文件中读取数据,然后将数据设置到unicorn虚拟机中交给目标代码去处理。
- 然后使用uc_open创建一个unicorn实例, 架构是 x86(UC_ARCH_X86), 64位(UC_MODE_64)。

紧接着把目标二进制程序(persistent_target_x86_64)映射到unicorn内存中
// 首先读取二进制程序到内存 off_t len = afl_mmap_file(BINARY_FILE, &file_contents);
// 在unicorn中映射内存 mem_map_checked(uc, BASE_ADDRESS, len, UC_PROT_ALL);
// 最后将文件的内容写入刚刚映射的内存if (uc_mem_write(uc, BASE_ADDRESS, file_contents, len) != UC_ERR_OK) { printf("Error writing to CODEn"); }
// 释放host的内存 munmap(file_contents, len);
然后设置RIP为main函数的地址,分配一块内存给栈使用,具体的方式是将分配到的内存的地址设置为 RSP。
uint64_t start_address = CODE_ADDRESS; // main 函数的地址uint64_t end_address = END_ADDRESS; // main函数的最后一条指令
// 设置 RIP 寄存器的值 uc_reg_write(uc, UC_X86_REG_RIP, &start_address); // address of entry point of main()
// 映射一块可读、可写的内存用来当作栈 mem_map_checked(uc, STACK_ADDRESS - STACK_SIZE, STACK_SIZE, UC_PROT_READ | UC_PROT_WRITE);// 将 RSP 设置为刚刚分配到的内存地址uint64_t stack_val = STACK_ADDRESS; uc_reg_write(uc, UC_X86_REG_RSP, &stack_val);
下面设置main函数的参数。
// 映射一块内存用来存放输入数据, INPUT_LOCATION 为 0x10000 mem_map_checked(uc, INPUT_LOCATION, INPUT_SIZE_MAX, UC_PROT_READ);
// 往INPUT_LOCATION+8处存放一个指针,用作argv[1] uc_mem_write(uc, INPUT_LOCATION + 8, "x16x00x01", 3);
// 设置 argv 为 INPUT_LOCATION, argc为2 uc_reg_write(uc, UC_X86_REG_RSI, &INPUT_LOCATION); // argv uc_reg_write(uc, UC_X86_REG_RDI, &EMULATED_ARGC); // argc == 2
代码逻辑如下

首先映射一块INPUT_SIZE_MAX大小的内存到INPUT_LOCATION(0x10000)地址,然后我们在INPUT_LOCATION出设置argv,其结构如下
INPUT_LOCATION + 0:NULL, argv[0]INPUT_LOCATION + 8: INPUT_LOCATION + 16, argv[1]INPUT_LOCATION + 16:存放 argv[1] 的数据
INPUT_LOCATION开始8字节为argv[0],mian函数中没有使用到,这里保持 NULL 不变。

INPUT_LOCATION + 8开始8字节为argv[1],保存的是 INPUT_LOCATION + 16 ,因此argv[1]的数据保存到INPUT_LOCATION + 16。

此时main函数还不能正常执行结束,因为目前函数中使用了_strlen这个库函数,而我们刚才只把可执行程序映射到了内存里面,所以这里执行 010116B 这条指令时会异常。
.text:0000000000101168 mov rdi, rax ; s.text:000000000010116B call _strlen.text:0000000000101170 mov [rbp+var_10], rax
本节的解决方案是使用 unicorn 的hook机制来 hook 这条指令,有我们来实现 strlen 这个函数调用。
// Add our strlen hook (for this specific testcase only)int strlen_hook_pos = BASE_ADDRESS + 0x116b;uc_hook strlen_hook;uc_hook_add(uc, &strlen_hook, UC_HOOK_CODE, hook_strlen, NULL, strlen_hook_pos, strlen_hook_pos);
hook_strlen的代码如下
static void hook_strlen(uc_engine *uc, uint64_t address, uint32_t size, void *user_data) { uint64_t addr; uint32_t count = 0; char c = 0;// 首先获取 strlen 的入参 uc_reg_read(uc, UC_X86_REG_RDI, &addr);
while(1) { // 读一个字节 uc_err ret = uc_mem_read(uc, addr, &c, 1);if(ret != UC_ERR_OK) {break; }// 搜索字符串的结尾if(c == 'x00') {break; }
addr++;count++; }
printf("strlen ret: %dn", count);
// 通过修改 RAX 来设置strlen的返回值 uc_reg_write(uc, UC_X86_REG_RAX, &count);
// 通过修改 RIP 来忽略 call _strlen 指令的执行 uint64_t next_addr = address + size; uc_reg_write(uc, UC_X86_REG_RIP, &next_addr);}
代码逻辑很简单,首先模拟strlen的工作流程来获取字符串的长度,然后通过修改RAX来设置strlen的返回值,最后通过修改RIP寄存器来忽略这条指令的执行。

最后使用uc_afl_fuzz来启动Fuzz
uc_afl_ret afl_ret = uc_afl_fuzz( uc, // unicorn实例 filename, // 输入文件,每一轮Fuzz时uc_afl_fuzz里面会读取文件内容到host的内存 place_input_callback, // 一个回调函数,用于把刚刚读入的文件内容设置成目标程序获取数据 &end_address, // 结束地址数组1, // 结束地址数组的长度 crash_handler, // 用于在crash时或者每次此时完成后调用false, // 如果为true每次测试结束都会调用该函数来判断是否发生了crash.1, // 指定执行多少次后才新fork进程,类比AFL的persistent模式,为1的话表示每次都fork新进程来测试NULL // additional data pointer );switch(afl_ret) {case UC_AFL_RET_ERROR:printf("Error starting to fuzz");return -3;break;case UC_AFL_RET_NO_AFL:printf("No AFL attached - We are done with a single run.");break;default:break; }
下面看看place_input_callback函数的实现,该函数在每次测试开始前调用,作用是将AFL生成的变异数据,按照被测函数的需求,喂给被测函数去处理。
static bool place_input_callback( uc_engine *uc, char *input, size_t input_len, uint32_t persistent_round, void *data){// 如果数据太长,返回false,表示忽略这个测试用例if (input_len < 1 || input_len >= INPUT_SIZE_MAX - INPUT_OFFSET) {return false; }
//设置寄存器状态,因为如果是persistent模式的话需要重复执行同一个函数,所以参数和 RIP, RSP需要设置 uc_reg_write(uc, UC_X86_REG_RIP, &CODE_ADDRESS); uc_reg_write(uc, UC_X86_REG_RSI, &INPUT_LOCATION); uc_reg_write(uc, UC_X86_REG_RDI, &EMULATED_ARGC); uc_reg_write(uc, UC_X86_REG_RSP, &STACK_ADDRESS);
// 设置末尾为 x00 input[input_len-1] = '';// 设置测试数据到 unicorn中 uc_mem_write(uc, INPUT_LOCATION + INPUT_OFFSET, input, input_len);
// 返回 true表示用例已经设置完毕,可以开始测试return true;}
编译的话将文件拷贝到 AFLplusplus的unicorn_mode/samples/c目录,然后执行
$ cd AFLplusplus/unicorn_mode/samples/c$ cc -w -I../../unicornafl/include -g -g -c harness.c$ cc -L../../unicornafl harness.o ../../unicornafl/libunicornafl.a -L../../unicornafl -lpthread -lm -g -lrt -o harness
编译完后使用afl-fuzz开始Fuzzing
~/AFLplusplus/afl-fuzz -U -i in -o out -- ./harness @@
AFL-QBDI与AFL-Unicorn实战

参考
https://github.com/vanhauser-thc/AFLplusplus/blob/master/unicorn_mode/samples/c/harness.c

mips with elf loader

上一节的被测函数中使用libc的strlen函数,由于我们没有把libc.so加载到内存,所以我们的处理方式是hook调用strlen函数的地方,由我们在host端模拟strlen的行为,然后设置函数返回值,最后跳过调用strlen的指令的执行。本节介绍另外一种处理方式,即自己实现一个elf loader,把elf文件和它的依赖库一起加载到内存中,并做好符号重定位,本节代码主要由uniFuzzer项目修改而来。
https://github.com/PAGalaxyLab/uniFuzzer
首先我们看看被测程序, main函数的逻辑比较简单,就是从文件中读取数据,然后传给 vuln 函数去处理。
AFL-QBDI与AFL-Unicorn实战
vuln函数模拟了堆溢出漏洞和栈溢出漏洞,函数入参就是一个内存指针。
AFL-QBDI与AFL-Unicorn实战
下面我们看看适配代码怎么写的
int main(int argc, char **argv) {char *target = getenv("UF_TARGET"); // 主程序的路径char *preload = getenv("UF_PRELOAD"); // preload的路径char *libPath = getenv("UF_LIBPATH"); // 库搜索路径,下面加载 target 会在libPath中搜索库
// 数据输入文件,用于接收 AFL 生成的变异数据char* filename = argv[1];
// 加载 target 和 preload,并且处理好符号重定位 uc = loadELF(target, preload, libPath);
loadELF加载好二进制后会返回一个 unicorn实例,接下来就是初始化函数运行需要内存状态和参数,最后使用开始测试。
// 初始化一些内存 uc_init_mem(uc);
uc_afl_ret afl_ret = uc_afl_fuzz( uc, // The unicorn instance we prepared filename, // Filename of the input to process. In AFL this is usually the '@@' placeholder, outside it's any input file. place_input_callback, // Callback that places the input (automatically loaded from the file at filename) in the unicorninstance &END, // Where to exit (this is an array)1, // Count of end addresses crash_handler, // Optional calback to run after each exectrue, // true, if the optional callback should be run also for non-crashes1000000, // For persistent mode: How many rounds to runNULL // additional data pointer );switch(afl_ret) {case UC_AFL_RET_ERROR:printf("Error starting to fuzzn");return -3;break;case UC_AFL_RET_NO_AFL:printf("No AFL attached - We are done with a single run.n");break;default:break; }
初始化内存的代码如下,uc_mem_map_ptr函数的作用是将host的内存和unicorn中的内存建立起映射关系,之后我们就可以直接操纵host的内存来实现修改unicorn中的内存。
int uc_init_mem(uc_engine *uc) {
// 分配内存用于 heap heapBase = mmap(NULL, HEAP_SIZE, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_32BIT, -1, 0);
if(uc_mem_map_ptr(uc, heapBase, HEAP_SIZE, UC_PROT_READ | UC_PROT_WRITE, heapBase) != UC_ERR_OK) {fprintf(stderr, "uc mapping heap failedn");return 1; }
// 分配内存用于栈 stackTop = mmap(NULL, STACK_SIZE, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_32BIT, -1, 0);if(uc_mem_map_ptr(uc, stackTop, STACK_SIZE, UC_PROT_READ | UC_PROT_WRITE, stackTop) != UC_ERR_OK) {fprintf(stderr, "uc mapping stack failedn");return 1; }
// 分配空间,用于存放 AFL 测试数据,最终会被目标函数处理 dataAddr = mmap(NULL, DATA_SIZE, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_32BIT, -1, 0);
if(uc_mem_map_ptr(uc, dataAddr, DATA_SIZE, UC_PROT_READ | UC_PROT_WRITE, dataAddr) != UC_ERR_OK) {fprintf(stderr, "uc mapping data failedn");return 1; }return 0;}
place_input_callback函数的逻辑也很简单,就是设置寄存器的值,然后将AFL传过来的测试数据设置到unicorn中。
static bool place_input_callback( uc_engine *uc, char *input, size_t input_len, uint32_t persistent_round, void *data){if(input_len < 0) {return false; }
if(input_len > 255) { input_len = 255; }
// 设置寄存器的值,包括栈寄存器SP, 参数寄存器A0,PC、RA、T9等uint32_t reg;
reg = stackTop+STACK_SIZE - 0x200; uc_reg_write(uc, UC_MIPS_REG_SP, &reg);
reg = dataAddr; uc_reg_write(uc, UC_MIPS_REG_A0, &reg);
reg = START; uc_reg_write(uc, UC_MIPS_REG_T9, &reg);
reg = RA; uc_reg_write(uc, UC_MIPS_REG_RA, &reg);
uc_reg_write(uc, UC_MIPS_REG_PC, &START);
// 设置 demo-libcpreload.so 里面的heapBoundaryGOT, 用于子实现的malloc使用 *heapBoundaryGOT = heapBase;
// 拷贝AFL的测试数据到 unicornmemcpy(dataAddr, input, input_len);
return true;}
编译命令
gcc -DUF_DEBUG test.c -w -I../../unicornafl/include -g -IelfLoader elfLoader/elfLoader.c elfLoader/arm.c elfLoader/i386.c elfLoader/mips.c elfLoader/dl-hash.c -L../../unicornafl -lpthread -g -lrt ../../unicornafl/libunicornafl.a -lm -o test
编译完执行
UF_TARGET=/home/hac425/AFLplusplus/unicorn_mode/samples/c/demo-vuln UF_PRELOAD=/home/hac425/AFLplusplus/unicorn_mode/samples/c/demo-libcpreload.so UF_LIBPATH=/usr/mipsel-linux-gnu/lib/ ./test init.txt
AFL的命令
UF_TARGET=/home/hac425/AFLplusplus/unicorn_mode/samples/c/demo-vuln UF_PRELOAD=/home/hac425/AFLplusplus/unicorn_mode/samples/c/demo-libcpreload.so UF_LIBPATH=/usr/mipsel-linux-gnu/lib/ ~/AFLplusplus/afl-fuzz -U -i in -o out -- ./test @@
AFL-QBDI与AFL-Unicorn实战
参考
http://galaxylab.com.cn/%e5%9f%ba%e4%ba%8eunicorn%e5%92%8clibfuzzer%e7%9a%84%e6%a8%a1%e6%8b%9f%e6%89%a7%e8%a1%8cfuzzing/

总结

本章介绍了各种Fuzz技术并介绍了一些优化Fuzz测试的方法,为了能够熟练使用各种Fuzz技术,读者需要多实践并且要多看看Fuzz工具的源代码。Fuzz测试并不是单纯的启动Fuzz工具让Fuzz工具执行就可以的,在Fuzz的过程中测试人员需要不断地查看Fuzz的状态,一般而言Fuzz的流程如下

1. 确定并分析Fuzz目标。
2. 初步运行Fuzz工具保证能够正常开始Fuzz。
3. 收集大量初始用例并对初始用例去重。
4. 用去重后的初始用例开始Fuzz。
5. 在Fuzz过程中当代码覆盖率长时间没有增长时,人工介入分析代码覆盖率,想办法提升代码覆盖率。
6. 对发现的Crash去重。

最后祝读者能够挖到更多的漏洞。

☆ END ☆


连载文章

Fuzz技术综述与文件Fuzz

协议Fuzz技术

AFL分析与实战

AFL分析与实战


最新动态

OSRC 2周年第二弹——第五次奖励升级

OPPO互联网DevSecOps实践


AFL-QBDI与AFL-Unicorn实战


发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: