模糊测试工具AFL源码浅析

打印 上一主题 下一主题

主题 801|帖子 801|积分 2403

前言

AFL是一款著名的模糊测试的工具,最近在阅读AFL源码,记录一下,方便以后查阅。
环境


  • 项目:AFL
  • 编译项目:将编译的优化选项关闭,即改写成-O0
[img=720,514.601226993865]https://m-1254331109.cos.ap-guangzhou.myqcloud.com/202210251527686.png[/img]
afl-gcc.c

使用gdb加载afl-gcc,并使用set arg -o test test.c设置参数
[img=720,392.3562753036437]https://m-1254331109.cos.ap-guangzhou.myqcloud.com/202210251527688.png[/img]
find_as函数


  • find_as函数首先会通过AFL_PATH环境变量的值从而获得AFL对应的路径
  • 若上述环境变量不存在则获取当前afl-gcc所在的文件路径
  • 判断该路径下的as文件是否具有可执行权限
  1. u8 *afl_path = getenv("AFL_PATH");
  2. ...
  3. if (afl_path) {
  4.     tmp = alloc_printf("%s/as", afl_path); //将AFL所在路径与字符as进行拼接
  5.     if (!access(tmp, X_OK)) { //函数用来判断指定的文件或目录是否有可执行权限,若指定方式有效则返回0,否则返回-1
  6.       as_path = afl_path;
  7.       ck_free(tmp);
  8.       return;
  9.     }
  10.     ck_free(tmp);
  11.   }
  12.   slash = strrchr(argv0, '/'); //在参数argv0所指向的字符串中搜索最后一次出现字符'/'
  13.   if (slash) {
  14.     u8 *dir;
  15.     *slash = 0;
  16.     dir = ck_strdup(argv0);
  17.     *slash = '/';
  18.     tmp = alloc_printf("%s/afl-as", dir); //将当前AFL所在的路径跟afl-as进行拼接
  19.     if (!access(tmp, X_OK)) {
  20.       as_path = dir;
  21.       ck_free(tmp);
  22.       return;
  23.     }
  24. ...
复制代码
 
【----帮助网安学习,以下所有学习资料免费领!加vx:yj009991,备注 “博客园” 获取!】
 ① 网安学习成长路径思维导图
 ② 60+网安经典常用工具包
 ③ 100+SRC漏洞分析报告
 ④ 150+网安攻防实战技术电子书
 ⑤ 最权威CISSP 认证考试指南+题库
 ⑥ 超1800页CTF实战技巧手册
 ⑦ 最新网安大厂面试题合集(含答案)
 ⑧ APP客户端安全检测指南(安卓+IOS)
edit_params函数


  • edit_params函数实际就是准备需要传入编译器的参数,如编译器的类型gcc或clang
  • 其次就是是否需要开启保护如canary等
  • 最后就是判断是否开启内存泄漏探测的工具,如ASAN,该工具是针对C/C++ 的快速内存错误检测工具
  1.   ...
  2.   cc_params = ck_alloc((argc + 128) * sizeof(u8*));
  3.   name = strrchr(argv[0], '/'); //获取可执行文件名称
  4.   if (!name) name = argv[0]; else name++; /*跳过路径符'/' */
  5.   if (!strncmp(name, "afl-clang", 9)) { //判断编译器是否为clang
  6.       ...
  7.   }
  8.   else {
  9.     if (!strcmp(name, "afl-g++")) {
  10.       u8* alt_cxx = getenv("AFL_CXX");
  11.       cc_params[0] = alt_cxx ? alt_cxx : (u8*)"g++";
  12.     } else if (!strcmp(name, "afl-gcj")) {
  13.       u8* alt_cc = getenv("AFL_GCJ");
  14.       cc_params[0] = alt_cc ? alt_cc : (u8*)"gcj";
  15.     } else {
  16.       u8* alt_cc = getenv("AFL_CC");
  17.       cc_params[0] = alt_cc ? alt_cc : (u8*)"gcc"; //如环境变量没写入AFL_CC则默认使用gcc
  18.     }
  19.   }
  20.   while (--argc) {
  21.     u8* cur = *(++argv); //读取下一个参数
  22.     if (!strncmp(cur, "-B", 2)) { //若参数是-B
  23.       if (!be_quiet) WARNF("-B is already set, overriding"); //用于设置编译器的搜索路径
  24.       if (!cur[2] && argc > 1) { argc--; argv++; }//继续读取下一个参数
  25.       continue;
  26.     }
  27.     if (!strcmp(cur, "-integrated-as")) continue;
  28.     if (!strcmp(cur, "-pipe")) continue;
  29. #if defined(__FreeBSD__) && defined(__x86_64__)
  30.     if (!strcmp(cur, "-m32")) m32_set = 1;
  31. #endif
  32.     if (!strcmp(cur, "-fsanitize=address") ||
  33.         !strcmp(cur, "-fsanitize=memory")) asan_set = 1; //内存访问的错误
  34.     if (strstr(cur, "FORTIFY_SOURCE")) fortify_set = 1;//缓冲区溢出问题的检查
  35.     cc_params[cc_par_cnt++] = cur; //cc_params用于存放的参数
  36.   }
  37.   cc_params[cc_par_cnt++] = "-B"; //参数-B
  38.   cc_params[cc_par_cnt++] = as_path; //afl-as的路径
  39.   if (clang_mode)
  40.     cc_params[cc_par_cnt++] = "-no-integrated-as";
  41.   if (getenv("AFL_HARDEN")) {
  42.     cc_params[cc_par_cnt++] = "-fstack-protector-all"; //canary保护
  43.     if (!fortify_set)
  44.       cc_params[cc_par_cnt++] = "-D_FORTIFY_SOURCE=2";
  45.   }
  46.   if (asan_set) {
  47.     /* Pass this on to afl-as to adjust map density. */
  48.     setenv("AFL_USE_ASAN", "1", 1);
  49.   } else if (getenv("AFL_USE_ASAN")) {
  50.     if (getenv("AFL_USE_MSAN"))
  51.       FATAL("ASAN and MSAN are mutually exclusive");
  52.     if (getenv("AFL_HARDEN"))
  53.       FATAL("ASAN and AFL_HARDEN are mutually exclusive");
  54.     cc_params[cc_par_cnt++] = "-U_FORTIFY_SOURCE";
  55.     cc_params[cc_par_cnt++] = "-fsanitize=address";
  56.   } else if (getenv("AFL_USE_MSAN")) {
  57.     if (getenv("AFL_USE_ASAN"))
  58.       FATAL("ASAN and MSAN are mutually exclusive");
  59.     if (getenv("AFL_HARDEN"))
  60.       FATAL("MSAN and AFL_HARDEN are mutually exclusive");
  61.     cc_params[cc_par_cnt++] = "-U_FORTIFY_SOURCE";
  62.     cc_params[cc_par_cnt++] = "-fsanitize=memory";
  63.   }
  64.   ...
  65.       cc_params[cc_par_cnt++] = "-g";
  66.   ...
  67.     cc_params[cc_par_cnt++] = "-O3";
  68.     cc_params[cc_par_cnt++] = "-funroll-loops";
  69.     /* Two indicators that you're building for fuzzing; one of them is
  70.        AFL-specific, the other is shared with libfuzzer. */
  71.     cc_params[cc_par_cnt++] = "-D__AFL_COMPILER=1";
  72.     cc_params[cc_par_cnt++] = "-DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION=1";
  73.   }
  74.   if (getenv("AFL_NO_BUILTIN")) {
  75.     cc_params[cc_par_cnt++] = "-fno-builtin-strcmp";
  76.     cc_params[cc_par_cnt++] = "-fno-builtin-strncmp";
  77.     cc_params[cc_par_cnt++] = "-fno-builtin-strcasecmp";
  78.     cc_params[cc_par_cnt++] = "-fno-builtin-strncasecmp";
  79.     cc_params[cc_par_cnt++] = "-fno-builtin-memcmp";
  80.     cc_params[cc_par_cnt++] = "-fno-builtin-strstr";
  81.     cc_params[cc_par_cnt++] = "-fno-builtin-strcasestr";
  82.   }
  83.   cc_params[cc_par_cnt] = NULL;
  84. }
复制代码
通过edit_params函数后
[img=720,301.72062904717853]https://m-1254331109.cos.ap-guangzhou.myqcloud.com/202210251527689.png[/img]
可以传递给编译器的参数增加了-B . -g -O3 -funroll-loops -D__AFL_COMPILER=1 -DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION=1这几项
main函数


  • 首先调用isatty函数判断描述符是否为终端机以及是否为静默模式,即不打印任何信息,SAYF即输出函数用于输出提示字符
  • 接着通过find_as函数搜索as文件所在的路径
  • 接着通过edit_params函数编辑获取需要传入编译器的参数
  • 最后通过execvp函数启动gcc或其他编译器
  1.   /*
  2.     isatty函数用于判断文件描述词是否是为终端机
  3.     获取AFL_QUIET的环境变量
  4.   */
  5.   if (isatty(2) && !getenv("AFL_QUIET")) { //判断是否静默模式
  6.     /*
  7.       #ifdef MESSAGES_TO_STDOUT
  8.       #  define SAYF(x...)    printf(x)
  9.       #else
  10.       #  define SAYF(x...)    fprintf(stderr, x)
  11.       #endif
  12.     */
  13.     SAYF(cCYA "afl-cc " cBRI VERSION cRST " by <lcamtuf@google.com>\n");
  14.   } else be_quiet = 1;
  15.   if (argc < 2) { //参数个数小于两个
  16.     SAYF("\n"
  17.          "This is a helper application for afl-fuzz. It serves as a drop-in replacement\n"
  18.          "for gcc or clang, letting you recompile third-party code with the required\n"
  19.          "runtime instrumentation. A common use pattern would be one of the following:\n\n"
  20.          "  CC=%s/afl-gcc ./configure\n"
  21.          "  CXX=%s/afl-g++ ./configure\n\n"
  22.          "You can specify custom next-stage toolchain via AFL_CC, AFL_CXX, and AFL_AS.\n"
  23.          "Setting AFL_HARDEN enables hardening optimizations in the compiled code.\n\n",
  24.          BIN_PATH, BIN_PATH);
  25.     exit(1);
  26.   }
  27.   find_as(argv[0]); //用于寻找as所在路径
  28.   edit_params(argc, argv);//用于获取编译参数
  29.   execvp(cc_params[0], (char**)cc_params);//启动gcc或其他编译器
复制代码
 
大致流程图
[img=720,237.33333333333334]https://m-1254331109.cos.ap-guangzhou.myqcloud.com/202210251527690.png[/img]
afl-gcc可以看作是劫持了gcc的一个程序,从而修改as的路径(为了后续的插桩做准备),并且添加所有fuzzing所需要的参数再传入实际的编译器中去(这里以gcc作为例子)
afl-as.c

edit_params函数

afl-as.c的edit_params函数比较简单

  • 首先是确定as文件所在的路径,若没有设置环境变量则直接使用as作为汇编器所在路径的参数
  • 其次是检测.s文件是否在临时目录下,这里我做了测试如果.s不在临时目录则无法插桩成功
  • 最后随机生成文件名,将该文件作为插桩后的文件并作为传输传入汇编器
  1.   u8 *tmp_dir = getenv("TMPDIR"), *afl_as = getenv("AFL_AS"); //afl-as的地址
  2.   ...
  3.   as_params = ck_alloc((argc + 32) * sizeof(u8*)); //给参数分配空间
  4.   as_params[0] = afl_as ? afl_as : (u8*)"as";
  5.   as_params[argc] = 0; //截断符
  6.   ...
  7.   //用于记录文件是64位还是32位
  8.   for (i = 1; i < argc - 1; i++) {
  9.     if (!strcmp(argv[i], "--64")) use_64bit = 1;
  10.     else if (!strcmp(argv[i], "--32")) use_64bit = 0;  
  11.   ...
  12.     if (strncmp(input_file, tmp_dir, strlen(tmp_dir)) &&
  13.         strncmp(input_file, "/var/tmp/", 9) &&
  14.         strncmp(input_file, "/tmp/", 5)) pass_thru = 1; //汇编文件需要放在临时目录下,否则后续无法对文件进行插桩
  15.   }
  16.   modified_file = alloc_printf("%s/.afl-%u-%u.s", tmp_dir, getpid(),
  17.                                (u32)time(NULL)); //随机生成文件名,作为插桩的目标文件
  18.   ...
  19.   as_params[as_par_cnt++] = modified_file; //将待修改的文件名作为汇编器的参数
  20.   as_params[as_par_cnt]   = NULL;
复制代码
add_instrumentation函数

add_instrumentation函数是插桩的关键函数

  • 首先是分别打开需要编译的文件以及存放插桩后的文件,并且对需要编译的文件逐行逐行进行扫描
  • 其次对于以下情况的代码块不进行插桩处理

    • pass_thru = 1,这里经调试发现只要.s文件存在于临时目录下pass_thru的值就会为0,pass_thru = 1的意思是只传递数据不进行插桩
    • skip_intel = 1即为跳过intel的汇编语法的代码
    • 不在.text段内
    • 在.text段但是不处于函数标签或者分支标签

  • trampoline_fmt_64与trampoline_fmt_32即为需要插桩的代码,并会记录总共插桩了几处
  • 若进行了插桩处理,那么则需要在文件末尾插入main_payload_64,是与afl进行fuzzing相关的函数
  1.    ...
  2.    if (input_file) { //需要编译的文件
  3.     inf = fopen(input_file, "r");
  4.     if (!inf) PFATAL("Unable to read '%s'", input_file);
  5.   } else inf = stdin;
  6.   outfd = open(modified_file, O_WRONLY | O_EXCL | O_CREAT, 0600); //打开存放插桩后的文件
  7.   if (outfd < 0) PFATAL("Unable to write to '%s'", modified_file);
  8.   outf = fdopen(outfd, "w");
  9.   if (!outf) PFATAL("fdopen() failed");  
  10.   while (fgets(line, MAX_LINE, inf)) { //对需要汇编的文件进行一行一行的扫描
  11.     /* In some cases, we want to defer writing the instrumentation trampoline
  12.        until after all the labels, macros, comments, etc. If we're in this
  13.        mode, and if the line starts with a tab followed by a character, dump
  14.        the trampoline now. */
  15.     //isalpha是一种函数:判断字符ch是否为英文字母
  16.     //#  define R(x) (random() % (x))
  17.     if (!pass_thru && !skip_intel && !skip_app && !skip_csect && instr_ok &&
  18.         instrument_next && line[0] == '\t' && isalpha(line[1])) {
  19.       fprintf(outf, use_64bit ? trampoline_fmt_64 : trampoline_fmt_32,
  20.               R(MAP_SIZE)); //将插桩代码写入改写文件中,trampoline_fmt_64为64位程序的插桩代码,trampoline_fmt_32为32位程序的插桩代码
  21.       instrument_next = 0;
  22.       ins_lines++; //总共插桩了多少处地方
  23.     }
  24.     ...
  25.     if (line[0] == '\t' && line[1] == '.') {
  26.       /* OpenBSD puts jump tables directly inline with the code, which is
  27.          a bit annoying. They use a specific format of p2align directives
  28.          around them, so we use that as a signal.
  29.         OpenBSD为一个类unix的操作系统
  30.        */
  31.       if (!clang_mode && instr_ok && !strncmp(line + 2, "p2align ", 8) &&
  32.           isdigit(line[10]) && line[11] == '\n') skip_next_label = 1; //跳转到下一个标签
  33.    
  34.       if (!strncmp(line + 2, "text\n", 5) ||
  35.           !strncmp(line + 2, "section\t.text", 13) ||
  36.           !strncmp(line + 2, "section\t__TEXT,__text", 21) ||
  37.           !strncmp(line + 2, "section __TEXT,__text", 21)) {
  38.         instr_ok = 1; //只要是text段就是我们应该插桩的段
  39.         continue;
  40.       }
  41.       if (!strncmp(line + 2, "section\t", 8) ||
  42.           !strncmp(line + 2, "section ", 8) ||
  43.           !strncmp(line + 2, "bss\n", 4) ||
  44.           !strncmp(line + 2, "data\n", 5)) {
  45.         instr_ok = 0; //不需要插桩的段
  46.         continue;
  47.       }
  48.     }   
  49.     ...
  50.     if (line[0] == '\t') {//检测jnz等分支指令
  51.       if (line[1] == 'j' && line[2] != 'm' && R(100) < inst_ratio) { //绝对跳转jmp不进行插桩处理
  52.         fprintf(outf, use_64bit ? trampoline_fmt_64 : trampoline_fmt_32,
  53.                 R(MAP_SIZE)); //给分支跳转指令进行插桩
  54.         ins_lines++; //插桩的指令数
  55.       }
  56.       continue; //插桩完直接跳过
  57.     }
  58.     ...
  59.     if (strstr(line, ":")) { //检测标签
  60.       if (line[0] == '.') {
  61.         /* Apple: .L<num> / .LBB<num> */
  62.   
  63.         if ((isdigit(line[2]) || (clang_mode && !strncmp(line + 1, "LBB", 3))) //分支标签
  64.             && R(100) < inst_ratio) {
  65.             
  66.           ...
  67.           if (!skip_next_label) instrument_next = 1; else skip_next_label = 0;//若该标签不需要跳转则记录下来,该标签需要插桩
  68.         }
  69.       } else { //函数标签
  70.         /* Function label (always instrumented, deferred mode). */
  71.         instrument_next = 1;//函数标签都需要进行插桩
  72.    
  73.       }
  74.     }
  75.   }
  76.     if (ins_lines)
  77.     fputs(use_64bit ? main_payload_64 : main_payload_32, outf); //若进行插桩处理则需要插入main_payload_64
复制代码
这里重点关注一下插桩的位置

  • 情况一:函数入口,例如main函数
函数标签处的插桩如下图所示,插桩的位置是函数第一条指令的上方进行插桩
[img=720,345.69948186528495]https://m-1254331109.cos.ap-guangzhou.myqcloud.com/202210251527691.png[/img]

  • 情况二:分支跳转,例如jle指令
扫描到分支跳转指令,则直接在跳转指令下方进行插桩处理,如下图所示
[img=720,341.59678345778286]https://m-1254331109.cos.ap-guangzhou.myqcloud.com/202210251527692.png[/img]

  • 情况三:.L标签
.L为本地标签,afl-as.c也会扫描该标签并进行插桩处理,可以看到跳转指令的目的地地址就是以.L,因此.L可以认为分支的起始位置,与函数标签一样,会在第一条指令上方进行插桩处理
[img=720,417.4755043227666]https://m-1254331109.cos.ap-guangzhou.myqcloud.com/202210251527693.png[/img]
main函数

main函数主要经过edit_params函数修改了传入汇编器的参数,并且对汇编文件进行插桩处理,最后使用execvp函数启动汇编器进行汇编处理
  1.   ...
  2.   gettimeofday(&tv, &tz);
  3.   rand_seed = tv.tv_sec ^ tv.tv_usec ^ getpid();//随机种子
  4.   srandom(rand_seed);//通过种子生成随机数
  5.   edit_params(argc, argv); //加载参数,并在/tmp/目录下生成临时的汇编文件
  6.   if (inst_ratio_str) {
  7.     if (sscanf(inst_ratio_str, "%u", &inst_ratio) != 1 || inst_ratio > 100)
  8.       FATAL("Bad value of AFL_INST_RATIO (must be between 0 and 100)");
  9.   }
  10.   if (getenv(AS_LOOP_ENV_VAR))
  11.     FATAL("Endless loop when calling 'as' (remove '.' from your PATH)");
  12.   setenv(AS_LOOP_ENV_VAR, "1", 1);
  13.   /* When compiling with ASAN, we don't have a particularly elegant way to skip
  14.      ASAN-specific branches. But we can probabilistically compensate for
  15.      that... */
  16.   if (getenv("AFL_USE_ASAN") || getenv("AFL_USE_MSAN")) {
  17.     sanitizer = 1;
  18.     inst_ratio /= 3;
  19.   }
  20.   if (!just_version) add_instrumentation();//对文件进行插桩处理
  21.   if (!(pid = fork())) {
  22.     execvp(as_params[0], (char**)as_params);//将插桩后的文件传入汇编器中
  23.     FATAL("Oops, failed to execute '%s' - check your PATH", as_params[0]);
  24. ...
复制代码
传入汇编器的参数情况
[img=720,150.69767441860466]https://m-1254331109.cos.ap-guangzhou.myqcloud.com/202210251529661.png[/img]
大致流程图
[img=720,382.22222222222223]https://m-1254331109.cos.ap-guangzhou.myqcloud.com/202210251527694.png[/img]
afl-as相当于劫持了as从而修改汇编的文件名以及对相应的汇编文件进行插桩处理
afl-as.h

该文件放置了插桩需要的代码如trampoline_fmt_64、trampoline_fmt_32、main_payload_64以及main_payload_32,这些代码结合fuzzing过程有关。
总结

afl-gcc与afl-as可以看作是劫持了编译器,将fuzzing相关的参数设置好并对编译文件进行相应的插桩后再调用实际的编译器。
更多靶场实验练习、网安学习资料,请点击这里>>
 

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
回复

使用道具 举报

0 个回复

正序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

立聪堂德州十三局店

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表