WinAFL

发布者:Ox9A82
发布于:2017-09-06 16:20

winafl

标签(空格分隔): fuzz


构成

afl-fuzz.c 主模块
读取文件
维护testcase queue
进行mutate fuzz_one
评估代码覆盖率 执行遗传算法
更新界面 show_stats

winafl.c 注入dll
循环调用fuzz目标
更新覆盖率位图
注册事件回调函数
在模块加载回调中对模块进行插桩
使用pre_fuzz_handler和post_fuzz_handler进行循环fuzz
基本块\边界 覆盖率模式


(一)变异部分

queue_entry代表每一个测试用例的struct

//测试用例队列
struct queue_entry {

  u8* fname;                          /* File name for the test case      */
  u32 len;                            /* Input length                     */

  u8  cal_failed,                     /* Calibration failed?              */
      trim_done,                      /* Trimmed?                         */
      was_fuzzed,                     /* Had any fuzzing done yet?        */
      passed_det,                     /* Deterministic stages passed?     */
      has_new_cov,                    /* Triggers new coverage?           */
      var_behavior,                   /* Variable behavior?               */
      favored,                        /* Currently favored?               */
      fs_redundant;                   /* Marked as redundant in the fs?   */

  u32 bitmap_size,                    /* Number of bits set in bitmap     */
      exec_cksum;                     /* Checksum of the execution trace  */

  u64 exec_us,                        /* Execution time (us)              */
      handicap,                       /* Number of queue cycles behind    */
      depth;                          /* Path depth                       */

  u8* trace_mini;                     /* Trace bytes, if kept             */
  u32 tc_ref;                         /* Trace bytes ref count            */

  struct queue_entry *next,           /* Next element, if any             */
                     *next_100;       /* 100 elements ahead               */

};

fuzz_one函数

afl-fuzz.c的主要函数是fuzz_one(char** argv),对测试文件的变形操作也通过这个函数完成,在fuzz_one函数中会调用trim_case(char** argv, struct queue_entry* q, u8* in_buf)函数,先对测试用例进行裁剪,测试用例太大会影响测试速度,这是肯定的,所以理论上说测试用例越小那么全部fuzzing完成的时间就越短,那么怎么裁剪才不会影响fuzzing的结果呢,afl-fuzz.c靠如下手段完成。
首先,Winafl会建立一个64KB的共享内存,用于存储被fuzzing的应用程序的执行路径,如:A -> B -> C -> D -> E (tuples: AB, BC, CD, DE) ,当程序路径发生改变时,如变成A -> B -> D -> C -> E (tuples: AB, BD, DC, CE),Winafl会更新共享内存,发现新的执行路径更有助于发现代码的漏洞,因为大多数安全漏洞经常是一些没有预料到的状态转移,而不是因为没有覆盖那一块代码。
trim_case函数会用测试用例的总长度除以16,并删除这个长度的数据交给被fuzzing的应用程序打开并记录执行路径到共享内存中,执行完成后trim_case函数对这块共享内存进行hash计算,如果这块共享内存没有发生变化,那么说明没有发现新的执行路径,也同时说明被裁剪的这段数据对整个测试结果影响不大,于是对以后的测试用例都删除掉这段内容,这样,测试用例就会减小到一个合理的大小而并不会影响fuzzing结果。
在fuzz_one函数里,接着就会对测试用例文件做大量的变形操作,包括以下几种类型的变形:bit flips、byte flips、arithmetics、known ints、dictionary、havoc。

1.bit flips
2.byte flips
3.arithmetics
4.known ints
5.dictionary
6.havoc

bit flips

#define FLIP_BIT(_ar, _b) do { \
u8* _arf = (u8*)(_ar); \
u32 _bf = (_b); \
_arf[(_bf) >> 3] ^= (128 >> ((_bf) & 7)); \
} while (0)

byte flips

在byte flips模式下,对测试用例文件的内容进行按每8、16、32位的长度进行顺序,变形时使用语句:out_buf[stage_cur] ^= 0xFF;、(u16)(out_buf + i) ^= 0xFFFF;、(u32)(out_buf + i) ^= 0xFFFFFFFF;来进行变形,并把变形后的测试用例交给common_fuzz_stuff函数进行后续处理。

arithmetics

在arithmetics模式下,
8位的运算运用变形语句:
out_buf[i] = orig + j;
16位的运算运用变形语句:
(u16)(out_buf + i) = orig – j;
32位的运算运用变形语句:
(u32)(out_buf + i) = orig + j;
并把变形后的测试用例交给common_fuzz_stuff函数进行后续处理。

known

在known ints模式下
8位的运算运用变形语句:
out_buf[i] = interesting_8[j];
16位的运算运用变形语句:
(u16)(out_buf + i) = interesting_16[j];
32位的运算运用变形语句:
(u32)(out_buf + i) = interesting_32[j];
并把变形后的测试用例交给common_fuzz_stuff函数进行后续处理。其中interesting相关定义如下:

static s8 interesting_8[] = { INTERESTING_8 };
static s16 interesting_16[] = { INTERESTING_8, INTERESTING_16 };
static s32 interesting_32[] = { INTERESTING_8, INTERESTING_16, INTERESTING_32 };

dictionary

在dictionary模式下,允许用户使用字典,这样可以用字典文件对测试用例的相关部分进行替换并可以对一些有格式的文本、协议进行fuzz,如html、js、php等。
如果使用字典,参数是-x,后面可以是目录或者是一个字典文件,如:-x dict\xml.dict、-x dict,装载字典的函数是load_extras,如果参数是一个文件,那么用load_extras_file打开并格式化,用于对一些文本文件处理程序进行fuzz,如php、sql等、如果是一个目录,那么load_extras函数把所有文件内容读到extras数组,对二进制文件处理程序进行fuzz,如doc、xls等,此时对字典文件的大小限定为小于128B,但这个版本的load_extras函数写的有问题,没办法使用,但自己可以简单的修改下,如把load_extras_file(dir, &min_len, &max_len, dict_level);goto check_and_sort;这2条语句上移,不进行多余的判断,直接执行load_extras_file函数,至少能用字典跑一些带格式的文本文件,文本格式文件的一些字典可以参考testcases的例子。

havoc

在havoc模式下,通过如下表达式UR(15 + ((extras_cnt + a_extras_cnt) ? 2 : 0))计算出一个值,并通过switch…case进行不同的匹配变形。

other

char *dynamorio_dir;
cmd = alloc_printf
    (
      "%s\\drrun.exe -pidfile %s -no_follow_children -c winafl.dll %s -fuzzer_id %s -- %s",
      dynamorio_dir, 
      pidfile, 
      client_params, 
      fuzzer_id, 
      target_cmd
    );

执行dynamorio进程

destroy_target_process

进行插桩


(二)捕获部分

afl-fuzz进行变形后,调用DynamoRIO的对目标程序进行instrumentation,监视目标程序的运行,如果目标程序crash,那么复制变形后的测试用例到out\crashes目录下,如果目标程序无响应,那么复制变形后的测试用例到out\hangs目录下。
这里负责运行DynamoRIO的函数是run_target函数。

run_target

run_target首先检查目标进程是否存在,如果存在终止进程。
并通过drconfig.exe -nudge_pid %d 0 1命令将一个参数为1的nudge事件送到客户端回调,既目标fuzz程序的参数为1。
然后通过 drrun.exe -pidfile %s -no_follow_children -c winafl.dll %s — %s
命令启动目标程序,并注入winafl.dll到目标程序进程,之后的操作可以通过查看winafl.c文件进行分析,winafl.c的入口函数为dr_client_main函数,先继续一些必要的初始化工作后,首先通过options_init函数处理winafl的相关命令行参数,包括coverage_module、target_module、target_offset等。
接着挂载各种回调函数:event_exit,退出事件回调;onexception,异常回调;instrument_bb_coverage,instrument_edge_coverage,两种模式instrumentation的回调;event_module_load,event_module_unload模块装载、卸载回调。
在event_module_load函数中,可以看到winafl通过drwrap_wrap(to_wrap, pre_fuzz_handler, post_fuzz_handler);
完成在给定偏移(offset)的指向函数进入时运行pre_fuzz_handler函数,退出时运行post_fuzz_handler函数。

moudle设置

coverage_module可以有多个,target_module只有一个。
target_offset指定的是相对于target_module基地址的偏移。
coverage_module和target_module必须是目标被fuzz程序所调用的模块或其自身,确定的办法可以通过-debug参数完成,如运行如下命令行:

C:\test\DynamoRIO\bin32\drrun.exe 
-c winafl.dll 
-debug 
-target_module test_gdiplus.exe 
-target_offset 0x1650 
-fuzz_iterations 10 
-nargs 2 
— test_gdiplus.exe input.bmp

winafl会在当前目录下生成log文件,文件名类似afl.test_gdiplus.exe.13280.0000.proc.log,内容如下图所示:

原理

如果一遍遍运行再结束程序,fuzzing的效率将非常低,所以我们可以将函数进程启动,然后找到一个会打开并完全关闭输入文件的函数,通过操作pc寄存器,使得程序反复执行该函数,当然每次执行前需要将我们的输入文件mutate以下,这样我们就节省了创建进程,杀死进程的时间了,每秒可以测试上百个输入文件。而操作pc寄存器是得靠PIN工具(也就是instrumentation工具)来完成,这就是dynamoRIO的作用。另外,如果强行改变pc寄存器,而不去管内存呀寄存器的状态,是很可能造成程序崩溃的,所以必须要在我们的目标函数被调用之前进行现场的保存和恢复,这就是pre_fuzz_handler和post_fuzz_handler的作用,这两个函数其实就是使用dynamoRIO注册了这两个hook函数,分别在目标函数执行前和执行后执行。

首先,想要用winafl fuzz软件,目标软件必须支持命令行,能从命令行读取输入文件。
其次,in目录中的test_case不能为使得程序crash的输入,也就是不能用poc作为test_case,不然fuzzer在第一次迭代就crash,根本不会继续往下执行。
再次,作为target函数的函数有几点要求,一是必须进行一次完整的文件打开和关闭操作,如果没有文件关闭操作,winafl的mutator无法生成新的输入文件(覆盖旧的输入文件),会造成winafl强行kill进程,这样fuzzing效率非常低,而且并不是我们期待的做法。原文中提供的函数0x532a0仅有打开文件操作,没有关闭文件操作,是无法进行正确地fuzzing的。二是目标函数的参数如果有太复杂的指针,fuzzing有可能无法进行,这是因为在pre_fuzz_handler中无法聪明地恢复指针所指向的数据造成的(只能恢复指针本身的值),这种情况下有可能在第二次迭代中(也就是强行修改了pc寄存器后)造成崩溃。

参数

afl-fuzz.exe命令行格式如下:

afl-fuzz [afl options] — [instrumentation options] — target_cmd_line

afl-fuzz.exe参数[afl options]

-i 用于记录输入样本
-o 用于保存输出数据
-t 每次测试的超时时间 

-D DynamoRIO的路径
-Y enable the static instrumentation mode

-f location read by the fuzzed program (stdin)
-d quick & dirty mode (skips deterministic steps)
-x fuzzing字典(see README)

-target_offset 是要测试函数的偏移

winafl.dll参数[instrumentation options]

-coverage_module fuzzing对象程序会调用到的模块
-target_module   目标模块 就是target_offset所在的模块
-target_offset   偏移地址 就是会被instrumentation的偏移
-target_method   与上面相同 存在符号的时候可以使用
-fuzz_iterations 对象程序重启一次内运行目标函数的最大迭代数
-debug – debug模式
-nargs 放在结尾指示参数数目

test.exe @@ //其中的@@代指-i的输入文件

target_cmd_line参数就是要fuzzing对象的启动程序

测试样例


afl-fuzz.exe 
-i in 
-o out 
-D C:\test\DynamoRIO\bin32 
-t 20000 
-- 
-coverage_module gdiplus.dll 
-coverage_module WindowsCodecs.dll 
-fuzz_iterations 5000 
-target_module test_gdiplus.exe 
-target_offset 0x1650 
-nargs 2 
--
test_gdiplus.exe @@

测试

使用drrun.exe对winafl.dll进行插桩测试

drrun.exe 
-c winafl.dll 
-debug 
-target_module test.exe 
-target_offset 0x1010 
-fuzz_iterations 10 
-nargs 2 
-- 
test.exe in\file.txt
                   WinAFL 1.11 based on AFL 2.43b (test.exe)

+- process timing -------------------------------------+- overall results ----+
|        run time : 0 days, 0 hrs, 0 min, 46 sec       |  cycles done : 0     |
|   last new path : 0 days, 0 hrs, 0 min, 31 sec       |  total paths : 2     |
| last uniq crash : none seen yet                      | uniq crashes : 0     |
|  last uniq hang : none seen yet                      |   uniq hangs : 0     |
+- cycle progress --------------------+- map coverage -+----------------------+
|  now processing : 0 (0.00%)         |    map density : 0.02% / 0.02%        |
| paths timed out : 0 (0.00%)         | count coverage : 1.00 bits/tuple      |
+- stage progress --------------------+ findings in depth --------------------+
|  now trying : interest 16\8         | favored paths : 1 (50.00%)            |
| stage execs : 693/1779 (38.95%)     |  new edges on : 2 (100.00%)           |
| total execs : 5096                  | total crashes : 0 (0 unique)          |
|  exec speed : 116.0/sec             |  total tmouts : 0 (0 unique)          |
+- fuzzing strategy yields -----------+---------------+- path geometry -------+
|   bit flips : 0/400, 0/399, 0/397                   |    levels : 2         |
|  byte flips : 0/50, 0/49, 0/47                      |   pending : 2         |
| arithmetics : 1/2800, 0/0, 0/0                      |  pend fav : 1         |
|  known ints : 0/200, 0/0, 0/0                       | own finds : 1         |
|  dictionary : 0/0, 0/0, 0/0                         |  imported : n/a       |
|       havoc : 0/0, 0/0                              | stability : 93.75%    |
|        trim : 0.00%/12, 0.00%                       +-----------------------+
^C----------------------------------------------------+

声明:该文观点仅代表作者本人,转载请注明来自看雪