您现在的位置: smartcar > smartcar性能 > 正文 > 正文

技术推荐AFL二三事源码分析

  • 来源:本站原创
  • 时间:2021/11/11 10:42:16
北京青春痘最好的医院 http://baidianfeng.39.net/a_yqhg/210111/8578752.html

前言

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

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

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

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

宏观

首先在宏观上看一下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.关键变量

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

staticu8*as_path;/*PathtotheAFLaswrapper,AFL的as的路径*/staticu8**cc_params;/*ParameterspassedtotherealCC,CC实际使用的编译器参数*/staticu32cc_par_cnt=1;/*Paramcount,includingargv0,参数计数*/staticu8be_quiet,/*Quietmode,静默模式*/clang_mode;/*Invokedasafl-clang*?,是否使用afl-clang*模式*/#数据类型说明#typedefuint8_tu8;#typedefuint16_tu16;#typedefuint32_tu32;

2.main函数

main函数全部逻辑如下:

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

find_as(argv[0]):查找使用的汇编器

edit_params(argc,argv):处理传入的编译参数,将确定好的参数放入cc_params[]数组

调用execvp(cc_params[0],(cahr**)cc_params)执行afl-gcc

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

3.find_as函数

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

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

首先检查环境变量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,然后进行相应的处理。

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

调用ch_alloc()为cc_params分配大小为(argc+)*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,设置-Bas_path;

如果为clang_mode,则设置-no-integrated-as;

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

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

2.源码

1.关键变量

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

staticu8**as_params;/*Parameterspassedtotherealas,传递给as的参数*/staticu8*input_file;/*Originallyspecifiedinputfile,输入文件*/staticu8*modified_file;/*Instrumentedfilefortherealas,as进行插桩处理的文件*/staticu8be_quiet,/*Quietmode(nostderroutput),静默模式,没有标准输出*/clang_mode,/*Runninginclangmode?是否运行在clang模式*/pass_thru,/*Justpassdatathrough?只通过数据*/just_version,/*Justshowversion?只显示版本*/sanitizer;/*UsingASAN/MSAN是否使用ASAN/MSAN*/staticu32inst_ratio=,/*Instrumentationprobability(%)插桩覆盖率*/as_par_cnt=1;/*Numberofparamstoas传递给as的参数数量初始值*/

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

2.main函数

main函数全部逻辑如下:

首先获取环境变量AFL_INST_RATIO,赋值给inst_ratio_str,该环境变量主要控制检测每个分支的概率,取值为0到%,设置为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(inti=0;isizeof(as_params);i++){peinrf("as_params[%d]:%s\n",i,as_params[i]);}

在插桩完成后,会生成.s文件,内容如下(具体的文件位置与设置的环境变量相关):

3.add_instrumentation函数

add_instrumentation函数负责处理输入文件,生成modified_file,将instrumentation插入所有适当的位置。其整体控制流程如下:

整体逻辑看上去有点复杂,但是关键内容并不算很多。在main函数中调用完edit_params()函数完成as_params参数数组的处理后,进入到该函数。

判断input_file是否为空,如果不为空则尝试打开文件获取fd赋值给inf,失败则抛出异常;input_file为空则inf设置为标准输入;

打开modified_file,获取fd赋值给outfd,失败返回异常;进一步验证该文件是否可写,不可写返回异常;

while循环读取inf指向文件的每一行到line数组,每行最多MAX_LINE=个字节(含末尾的‘\0’),从line数组里将读取到的内容写入到outf指向的文件,然后进入到真正的插桩逻辑。这里需要注意的是,插桩只向.text段插入,:

首先跳过标签、宏、注释;

这里结合部分关键代码进行解释。需要注意的是,变量instr_ok本质上是一个flag,用于表示是否位于.text段。变量设置为1,表示位于.text中,如果不为1,则表示不再。于是,如果instr_ok为1,就会在分支处执行插桩逻辑,否则就不插桩。

首先判断读入的行是否以‘\t’开头,本质上是在匹配.s文件中声明的段,然后判断line[1]是否为.:

if(line[0]==\tline[1]==.){/*OpenBSDputsjumptablesdirectlyinlinewiththecode,whichisabitannoying.Theyuseaspecificformatofp2aligndirectivesaroundthem,soweusethatasasignal.*/if(!clang_modeinstr_ok!strncmp(line+2,"p2align",8)isdigit(line[10])line[11]==\n)skip_next_label=1;if(!strncmp(line+2,"text\n",5)

!strncmp(line+2,"section\t.text",13)

!strncmp(line+2,"section\t__TEXT,__text",21)

!strncmp(line+2,"section__TEXT,__text",21)){instr_ok=1;continue;}if(!strncmp(line+2,"section\t",8)

!strncmp(line+2,"section",8)

!strncmp(line+2,"bss\n",4)

!strncmp(line+2,"data\n",5)){instr_ok=0;continue;}}\t开头,且line[1]==.,检查是否为p2align指令,如果是,则设置skip_next_label=1;

尝试匹配"text\n""section\t.text""section\t__TEXT,__text""section__TEXT,__text"其中任意一个,匹配成功,设置instr_ok=1,表示位于.text段中,continue跳出,进行下一次遍历;

尝试匹配"section\t""section""bss\n""data\n"其中任意一个,匹配成功,设置instr_ok=0,表位于其他段中,continue跳出,进行下一次遍历;

接下来通过几个if判断,来设置一些标志信息,包括off-flavorassembly,Intel/ATT的块处理方式、ad-hoc__asm__块的处理方式等;

/*Detectoff-flavorassembly(rare,happensingdb).Whenthisisencountered,wesetskip_csectuntiltheoppositedirectiveisseen,andwedonotinstrument.*/if(strstr(line,".code")){if(strstr(line,".code32"))skip_csect=use_64bit;if(strstr(line,".code64"))skip_csect=!use_64bit;}/*Detectsyntaxchanges,ascouldhappenwithhand-writtenassembly.SkipIntelblocks,resumeinstrumentationwhenbacktoATT.*/if(strstr(line,".intel_syntax"))skip_intel=1;if(strstr(line,".att_syntax"))skip_intel=0;/*Detectandskipad-hoc__asm__blocks,likewiseskippingthem.*/if(line[0]==#

line[1]==#){if(strstr(line,"#APP"))skip_app=1;if(strstr(line,"#NO_APP"))skip_app=0;}

AFL在插桩时重点


本文编辑:佚名
转载请注明出地址  http://www.smartcarf.com/smartcarxn/8776.html

热点文章

  • 没有任何图片文章
  • 没有热点文章
推荐文章

  • 没有任何图片文章
  • 没有推荐文章

Copyright © 2012-2020 smartcar版权所有



现在时间: