AFL二三事

您所在的位置:网站首页 gcc插桩函数 AFL二三事

AFL二三事

2024-06-30 20:53| 来源: 网络整理| 查看: 265

AFL二三事——源码分析 前言

AFL,全称“American Fuzzy Lop”,是由安全研究员Michal Zalewski开发的一款基于覆盖引导(Coverage-guided)的模糊测试工具,它通过记录输入样本的代码覆盖率(代码执行路径的覆盖情况),以此进行反馈,对输入样本进行调整以提高覆盖率,从而提升发现漏洞的可能性。AFL可以针对有源码和无源码的程序进行模糊测试,其设计思想和实现方案在模糊测试领域具有十分重要的意义。

深入分析AFL源码,对理解AFL的设计理念和其中用到的技巧有着巨大的帮助,对于后期进行定制化Fuzzer开发也具有深刻的指导意义。所以,阅读AFL源码是学习AFL必不可少的一个关键步骤。

(注:需要强调的是,本文的主要目的是协助fuzz爱好者阅读AFL的源码,所以需要在了解AFL基本工作流程和原理的前提下进行阅读,本文并不会在原理侧做过多说明。)

当别人都要快的时候,你要慢下来。

宏观

首先在宏观上看一下AFL的源码结构:

MetricsTreemap-AFL

主要的代码在 afl-fuzz.c 文件中,然后是几个独立模块的实现代码,llvm_mode 和 qemu_mode 的代码量大致相当,所以分析的重点应该还是在AFL的根目录下的几个核心功能的实现上,尤其是 afl-fuzz.c,属于核心中的重点。

各个模块的主要功能和作用的简要说明:

插桩模块

afl-as.h, afl-as.c, afl-gcc.c:普通插桩模式,针对源码插桩,编译器可以使用gcc, clang;llvm_mode:llvm 插桩模式,针对源码插桩,编译器使用clang;qemu_mode:qemu 插桩模式,针对二进制文件插桩。

fuzzer 模块

afl-fuzz.c:fuzzer 实现的核心代码,AFL 的主体。

其他辅助模块

afl-analyze:对测试用例进行分析,通过分析给定的用例,确定是否可以发现用例中有意义的字段;afl-plot:生成测试任务的状态图;afl-tmin:对测试用例进行最小化;afl-cmin:对语料库进行精简操作;afl-showmap:对单个测试用例进行执行路径跟踪;afl-whatsup:各并行例程fuzzing结果统计;afl-gotcpu:查看当前CPU状态。

部分头文件说明

alloc-inl.h:定义带检测功能的内存分配和释放操作;config.h:定义配置信息;debug.h:与提示信息相关的宏定义;hash.h:哈希函数的实现定义;types.h:部分类型及宏的定义。 一、AFL的插桩——普通插桩 (一) 、AFL 的 gcc —— afl-gcc.c 1. 概述

afl-gcc 是GCC 或 clang 的一个wrapper(封装),常规的使用方法是在调用 ./configure 时通过 CC 将路径传递给 afl-gcc 或 afl-clang。(对于 C++ 代码,则使用 CXX 并将其指向 afl-g++ / afl-clang++。)afl-clang, afl-clang++, afl-g++ 均为指向 afl-gcc 的一个符号链接。

afl-gcc 的主要作用是实现对于关键节点的代码插桩,属于汇编级,从而记录程序执行路径之类的关键信息,对程序的运行情况进行反馈。

2. 源码 1. 关键变量

在开始函数代码分析前,首先要明确几个关键变量:

static u8* as_path; /* Path to the AFL 'as' wrapper,AFL的as的路径 */ static u8** cc_params; /* Parameters passed to the real CC,CC实际使用的编译器参数 */ static u32 cc_par_cnt = 1; /* Param count, including argv0 ,参数计数 */ static u8 be_quiet, /* Quiet mode,静默模式 */ clang_mode; /* Invoked as afl-clang*? ,是否使用afl-clang*模式 */ # 数据类型说明 # typedef uint8_t u8; # typedef uint16_t u16; # typedef uint32_t u32; 2. main函数

main 函数全部逻辑如下:

image-20210825105002088

其中主要有如下三个函数的调用:

find_as(argv[0]) :查找使用的汇编器edit_params(argc, argv):处理传入的编译参数,将确定好的参数放入 cc_params[] 数组调用 execvp(cc_params[0], (cahr**)cc_params) 执行 afl-gcc

20210825115404

这里添加了部分代码打印出传入的参数 arg[0] - arg[7] ,其中一部分是我们指定的参数,另外一部分是自动添加的编译选项。

3. find_as 函数

函数的核心作用:寻找 afl-as

函数内部大概的流程如下(软件自动生成,控制流程图存在误差,但关键逻辑没有问题):

image-20210825145618543

首先检查环境变量 AFL_PATH ,如果存在直接赋值给 afl_path ,然后检查 afl_path/as 文件是否可以访问,如果可以,as_path = afl_path。如果不存在环境变量 AFL_PATH ,检查 argv[0] (如“/Users/v4ler1an/AFL/afl-gcc”)中是否存在 “/” ,如果存在则取最后“/” 前面的字符串作为 dir,然后检查 dir/afl-as 是否可以访问,如果可以,将 as_path = dir 。以上两种方式都失败,抛出异常。 4. edit_params 函数

核心作用:将 argv 拷贝到 u8 **cc_params,然后进行相应的处理。

函数内部的大概流程如下:

image-20210825150938293

调用 ch_alloc() 为 cc_params 分配大小为 (argc + 128) * 8 的内存(u8的类型为1byte无符号整数)

检查 argv[0] 中是否存在/,如果不存在则 name = argv[0],如果存在则一直找到最后一个/,并将其后面的字符串赋值给 name

对比 name和固定字符串afl-clang:

若相同,设置clang_mode = 1,设置环境变量CLANG_ENV_VAR为1

对比name和固定字符串afl-clang++:: 若相同,则获取环境变量AFL_CXX的值,如果存在,则将该值赋值给cc_params[0],否则将afl-clang++赋值给cc_params[0]。这里的cc_params为保存编译参数的数组;若不相同,则获取环境变量AFL_CC的值,如果存在,则将该值赋值给cc_params[0],否则将afl-clang赋值给cc_params[0]。

如果不相同,并且是Apple平台,会进入 #ifdef __APPLE__。在Apple平台下,开始对 name 进行对比,并通过 cc_params[0] = getenv("") 对cc_params[0]进行赋值;如果是非Apple平台,对比 name 和 固定字符串afl-g++(此处忽略对Java环境的处理过程):

若相同,则获取环境变量AFL_CXX的值,如果存在,则将该值赋值给cc_params[0],否则将g++赋值给cc_params[0];

若不相同,则获取环境变量AFL_CC的值,如果存在,则将该值赋值给cc_params[0],否则将gcc赋值给cc_params[0]。

进入 while 循环,遍历从argv[1]开始的argv参数:

如果扫描到 -B ,-B选项用于设置编译器的搜索路径,直接跳过。(因为在这之前已经处理过as_path了);

如果扫描到 -integrated-as,跳过;

如果扫描到 -pipe,跳过;

如果扫描到 -fsanitize=address 和 -fsanitize=memory 告诉 gcc 检查内存访问的错误,比如数组越界之类,设置 asan_set = 1;

如果扫描到 FORTIFY_SOURCE ,设置 fortify_set = 1 。FORTIFY_SOURCE 主要进行缓冲区溢出问题的检查,检查的常见函数有memcpy, mempcpy, memmove, memset, strcpy, stpcpy, strncpy, strcat, strncat, sprintf, vsprintf, snprintf, gets 等;

对 cc_params 进行赋值:cc_params[cc_par_cnt++] = cur;

跳出 while 循环,设置其他参数:

取出前面计算出的 as_path ,设置 -B as_path ;

如果为 clang_mode ,则设置-no-integrated-as; 3. 如果存在环境变量 AFL_HARDEN,则设置-fstack-protector-all。且如果没有设置 fortify_set ,追加 -D_FORTIFY_SOURCE=2 ;

sanitizer相关,通过多个if进行判断:

如果 asan_set 在前面被设置为1,则设置环境变量 AFL_USE_ASAN 为1;

如果 asan_set 不为1且,存在 AFL_USE_ASAN 环境变量,则设置-U_FORTIFY_SOURCE -fsanitize=address;

如果不存在 AFL_USE_ASAN 环境变量,但存在 AFL_USE_MSAN 环境变量,则设置-fsanitize=memory(不能同时指定AFL_USE_ASAN或者AFL_USE_MSAN,也不能同时指定 AFL_USE_MSAN 和 AFL_HARDEN,因为这样运行时速度过慢;

如果不存在 AFL_DONT_OPTIMIZE 环境变量,则设置-g -O3 -funroll-loops -D__AFL_COMPILER=1 -DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION=1;如果存在 AFL_NO_BUILTIN 环境变量,则表示允许进行优化,设置-fno-builtin-strcmp -fno-builtin-strncmp -fno-builtin-strcasecmp -fno-builtin-strncasecmp -fno-builtin-memcmp -fno-builtin-strstr -fno-builtin-strcasestr。

最后补充cc_params[cc_par_cnt] = NULL;,cc_params 参数数组编辑完成。

###(二)、AFL的插桩 —— afl-as.c

1. 概述

afl-gcc 是 GNU as 的一个wrapper(封装),唯一目的是预处理由 GCC/clang 生成的汇编文件,并注入包含在 afl-as.h 中的插桩代码。 使用 afl-gcc / afl-clang 编译程序时,工具链会自动调用它。该wapper的目标并不是为了实现向 .s 或 asm 代码块中插入手写的代码。

experiment/clang_asm_normalize/ 中可以找到可能允许 clang 用户进行手动插入自定义代码的解决方案,GCC并不能实现该功能。

2. 源码 1. 关键变量

在开始函数代码分析前,首先要明确几个关键变量:

static u8** as_params; /* Parameters passed to the real 'as',传递给as的参数 */ static u8* input_file; /* Originally specified input file ,输入文件 */ static u8* modified_file; /* Instrumented file for the real 'as',as进行插桩处理的文件 */ static u8 be_quiet, /* Quiet mode (no stderr output) ,静默模式,没有标准输出 */ clang_mode, /* Running in clang mode? 是否运行在clang模式 */ pass_thru, /* Just pass data through? 只通过数据 */ just_version, /* Just show version? 只显示版本 */ sanitizer; /* Using ASAN / MSAN 是否使用ASAN/MSAN */ static u32 inst_ratio = 100, /* Instrumentation probability (%) 插桩覆盖率 */ as_par_cnt = 1; /* Number of params to 'as' 传递给as的参数数量初始值 */

注:如果在参数中没有指明 --m32 或 --m64 ,则默认使用在编译时使用的选项。

2. main函数

main 函数全部逻辑如下:

image-20210826112703339

首先获取环境变量 AFL_INST_RATIO ,赋值给 inst_ratio_str,该环境变量主要控制检测每个分支的概率,取值为0到100%,设置为0时则只检测函数入口的跳转,而不会检测函数分支的跳转;通过 gettimeofday(&tv,&tz);获取时区和时间,然后设置 srandom() 的随机种子 rand_seed = tv.tv_sec ^ tv.tv_usec ^ getpid();调用 edit_params(argc, argv) 函数进行参数处理;检测 inst_ratio_str 的值是否合法范围内,并设置环境变量 AFL_LOOP_ENV_VAR;读取环境变量``AFL_USE_ASAN和AFL_USE_MSAN的值,如果其中有一个为1,则设置sanitizer为1,且将inst_ratio`除3。这是因为在进行ASAN的编译时,AFL无法识别出ASAN特定的分支,导致插入很多无意义的桩代码,所以直接暴力地将插桩概率/3;调用 add_instrumentation() 函数,这是实际的插桩函数;fork 一个子进程来执行 execvp(as_params[0], (char**)as_params);。这里采用的是 fork 一个子进程的方式来执行插桩。这其实是因为我们的 execvp 执行的时候,会用 as_params[0] 来完全替换掉当前进程空间中的程序,如果不通过子进程来执行实际的 as,那么后续就无法在执行完实际的as之后,还能unlink掉modified_file;调用 waitpid(pid, &status, 0) 等待子进程执行结束;读取环境变量 AFL_KEEP_ASSEMBLY 的值,如果没有设置这个环境变量,就unlink掉 modified_file(已插完桩的文件)。设置该环境变量主要是为了防止 afl-as 删掉插桩后的汇编文件,设置为1则会保留插桩后的汇编文件。

可以通过在main函数中添加如下代码来打印实际执行的参数:

print("\n"); for (int i = 0; i


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3