AFLNET-aflfuzz.c main函数分析

总体流程在这里插入图片描述

1:采集网络流量,本地监听采集;
2:报文序列解析:使用特定协议的消息结构,以正确的顺序从pcap中提取单个请求,只获取客户端的请求数据包,不要响应数据,从而生成消息序列的初始语料库。
3:状态学习机:接收服务器响应并将新发现的状态和转换方式添加进协议状态机IPSM。AFLNET 读取响应报文并提取协议指定的状态码,确定当前的执行状态。而在代码中,引导机制的实现则利用哈希表形式,出现新的状态序列哈希值,则认为当前的测试用例是值得关注的。
在这里插入图片描图片描述
图5

4:目标状态选择器:选择下一个状态,一开始随机在IPSM的所有状态中选取,fuzz一段时间后,根据已有数据决定哪些状态fuzz权重更大,如选取更少被遍历的状态。
5:序列选择器:目标状态被选择后,该组件从种子库哈希表中随机选取能够遍历到到状态 s 的报文序列。AFLNET 将种子库做为一个队列实体,队列实体结构保存了种子的相关信息。此外,AFLNET 维护了一个状态库,该状态库由两部分组成:1)一个状态实体列表,保存相关状态信息的一个数据结构;2)一个哈希表,该表将状态标识符映射到执行与状态标识符对应状态的队列列表。该组件利用哈希表随机选取能够遍历到状态 s 的报文序列。
6:序列变异器:使用 AFL 的 fuzz_one 函数中的变异,同时结合协议感知的变异。这个模块在序列选取器选取报文序列后,把报文对话 M 拆分成三个部分,M1 是报文中遍历到状态 s 的报文前缀,他保证在 M1 后能到达状态 s。M2 是变异部分,M2 包含可在 M1 之后执行的所有消息,但仍处在状态 s 中。在 M2 实施 AFL 的字节翻转、插入、删除等变异操作。变异之后生成报文序列。如果生成的序列 M` 能够得到新的响应状态码或者带来程序新的覆盖率就认为是值得关注的,然后存入种子库。M3是M1\M2以外的部分。
在这里插入图片描述

总体流程为首先,请求序列分析器解析pcap 文件以生成单个序列(如图 3所示),并将其保存到语料库 C C C中。同时,状态机基于响应代码学习构造初始IPSM;这个初始的 IPSM 包含黑色的节点和转换(图4)。假设目标状态选择器选择状态331 (USER foo OK)作为目标状态,序列选择器将从序列语料库 C C C中随机选择一个序列,该语料库 C C C此时只包含一个序列。然后,序列突变器标识序列前缀(“USER foo” request)(M1),候选子序列(“PASS foo” request)(M2),和其余子序列为后缀(M3)。通过使用复合变异器突变候选子序列,序列突变器可能生成错误的密码请求(“PASS bar”)导致错误状态(530 Not logged in)。在这个错误的密码之后,它重播后缀(例如“MKD demo”, “CWD demo”),导致状态 530 的循环,因为在成功验证之前不允许所有这些命令。最后,发送“QUIT”请求,服务器退出。由于生成的测试序列(如图 5 所示)涵盖了新的状态和状态转换(在图 4 中以红色突出显示),因此会将其添加到语料库 C C C和IPSM中。
其流程可以描述如下:程序的输入是数据包文件,程序内部的parser会对数据包进行分析,Parser随协议不同而采用不同设计,目的在于解析出Request请求,这些请求将组合为一个个序列,作为序列数据集。数据集中的序列会通过Mutator变异后发往Server,Server对request返回response,response数据包将被程序捕获,提取其中的状态码,判断该状态码位于FSM的何处,若原本的模型中不存在该状态码,则FSM中会增加一个结点代表该状态。程序中的State Selector将会分析程序当前的FSM模型,从中选出较少遍历/未曾遍历的状态(这是通过一个优先级机制实现的),传递给sequence selector,使之选择出可以触发该状态的request序列。当然,一开始由于缺乏统计信息,State的选取是随机化的

开始读源码啦

其实网上更多资料是只讲AFLNET新增的代码,对于原本AFL的代码并不介绍,对新手不太友好,本节按照从头到尾开始顺一遍,也是只讲功能、逻辑,之后如果需要更进一步的学习,会继续更新某些函数的工作机制。

初始化(8812-9146)

8812-8834设置随机数种子、文件路径

  s32 opt;
  u64 prev_queued = 0;
  u32 sync_interval_cnt = 0, seek_to;
  u8  *extras_dir = 0;
  u8  mem_limit_given = 0;
  u8  exit_1 = !!getenv("AFL_BENCH_JUST_ONE");
  //char** use_argv;

  struct timeval tv;
  struct timezone tz;

  SAYF(cCYA "afl-fuzz " cBRI VERSION cRST " by <lcamtuf@google.com>\n");

  doc_path = access(DOC_PATH, F_OK) ? "docs" : DOC_PATH;

  gettimeofday(&tv, &tz);
  srandom(tv.tv_sec ^ tv.tv_usec ^ getpid());

8835-9131参数设置
就是while和switch,不贴代码了;
9132-9142:AFLNET参数检查,配置网络信息和选择使用的协议

//AFLNet - Check for required arguments
  if (!use_net) FATAL("Please specify network information of the server under test (e.g., tcp://127.0.0.1/8554)");

  if (!protocol_selected) FATAL("Please specify the protocol to be tested using the -P option");

  if (netns_name) {
    if (check_ep_capability(CAP_SYS_ADMIN, argv[0]) != 0)
      FATAL("Could not run the server under test in a \"%s\" network namespace "
            "without CAP_SYS_ADMIN capability.\n You can set it by invoking "
            "afl-fuzz with sudo or by \"$ setcap cap_sys_admin+ep /path/to/afl-fuzz\".", netns_name);
  }

9144:信号处理,输入的处理和屏幕大小变化的响应
在这里插入图片描述

9145:ASAN_OPTIONS和MASN_OPTIONS内容合法性检查

  setup_signal_handlers();
  check_asan_opts();

模式设置和系统检查(9147-9194)

1:fix_up_sync 函数(不太懂主从模式什么意思)
如果通过 -M或者-S指定了 sync_id,则更新 out_dir 和 sync_dir 的值:设置 sync_dir 的值为 out_dir,设置 out_dir 的值为out_dir/sync_id。

  //9147
  if (sync_id) fix_up_sync();/*设置同步目录的路径和标志位*/

  if (!strcmp(in_dir, out_dir))
    FATAL("Input and output directories can't be the same");

2:dumb模式
处理完主从模式之后,判断是否是dumb模式
3:save_cmdline 函数
copy当前命令行参数,保存,主要是把参数全部转移到堆上存储,并且存储指针在全局变量orig_cmdline中。
4:fix_up_banner函数
设置use_banner变量的值,主要用于后面UI图形化展示中的标题,流程如下,以调试为例:

  • 首先通过strchr函数获得name最后一个/
    • 如果有存在/ 则设置后面的内容为trim 即 xxxx/fuzz_png 则trim为 fuzz_png
    • 如果不存在 / 则直接设置trim为name,即 若被fuzz的程序路径为 fuzz_png,则直接设置trim为fuzz_png
  • 将use_banner设置为trim,如果长度过长,则格式化,取前40个字符,

5:check_if_tty函数检查程序是否是tty终端运行
首先检查是否有AFL_NO_UI,如果有则return,然后设置not_no_tty=1
然后通过ioctl获得当前windows tty的size
4: check_if_tty 函数
检查是否在tty终端上面运行:读取环境变量 AFL_NO_UI ,如果存在,设置 not_on_tty 为1,并返回;通过 ioctl 读取window size,如果报错为 ENOTTY,表示当前不在一个tty终端运行,设置 not_on_tty。
5: 几个CPU检查相关的函数
get_core_count(void)get_core_count():获取核心数量
check_crash_handling():确保核心转储不会进入程序,/proc/sys/kernel/core_pattern指定了发生崩溃的时候如何处理崩溃
check_cpu_governor():检查CPU管理者,使得cpu可以处于高效的运行状态
bind_to_free_cpu:该函数定义在宏内,如果定义了HAVE_AFFINITY,那么执行该函数,该函数的作用是绑定当前进程到free的cpu上。

处理输入输出文件、目标文件(9195-9217)

1:setup_post():如果定义了AFL_POST_LIBRARY环境变量,那么使用dlopen函数打开该动态链接库,然后使用alsym定位该库中afl_postprocess函数的位置,最后对该函数进行一次尝试调用(此时出错比后面出错开销小),该函数在后续的每一次fuzz中,都会先调用,相当于实在这里给用户留下了一个hook,可以自定义fuzz前的操作.
2:setup_shm:用来配置共享内存和virgin_bits之类的变量的,通过 trace_bits 和 virgin_bits 两个 bitmap 来分别记录当前的 tuple 信息及整体 tuple 信息,其中 trace_bits 位于共享内存上,便于进行进程间通信。通过 virgin_tmout 和 virgin_crash 两个 bitmap 来记录 fuzz 过程中出现的所有目标程序超时以及崩溃的 tuple 信息,具体如下:

  • 如果in_bitmap为空,使用memset初始化virgin_bits为255(0xff)
  • 初始化virgin_tmout和virgin_crash为255
  • 调用shmeget函数获得一片共享内存,内存的标识存储在shm_id中
    • 函数原型int shmget(key_t key, size_t size, int shmflg);
    • 第一个参数,程序需要提供一个参数key,这个key为非0整数,它有效的为共享内存段命名,shmget函数返回一个和key相关的共享内存标识符号,这个符号被后续的shmat利用
    • 第二个参数标识需要的共享内存大小
    • 第三个参数是flag,标识权限,这里的是IPC_CREAT | IPC_EXCL | 0600
    • IPC_CREAT — 如果共享内存不存在,则创建一个共享内存,否则打开
    • IPC_EXCL – 只有在共享内存不存在的时候,新的共享内存才建立,否则,报错
    • 0600是一种权限表示方法,每一位表示一种类型的权限,第一个表示八进制,第二位表示6=4+2为拥有者的权限为读写,第三位表示同组无权限,第四位表示其他人无权限
  • 注册atexit handler为remove_shm
    • atexit是一个注册函数,被注册用来最后删除共享内存
    • remove_shm函数是shmctl(shm_id, IPC_RMID, NULL);的wrapper函数。
      • 第二个参数IPC_RMID 作为command意思是删除共享内存片段,目标是第一个参数
    • 把生成的shm_id赋值给shm_str
  • 如果没有设置dumb模式,则把环境变量设置为SHM_ENV_VAR设置为shm_str(即为id),然后释放shm_str
  • 调用shmat函数,返回一片内存空间的地址,该地址即为共享空间的地址

这里对返回的trace_bits的理解如下:

  • 首先,返回的地址是共享内存在这个程序链接的地址,创建共享空间后,该进程还不能直接使用,需要使用函数shmat允许该进程使用(链接进该进程的空间)
  • 其次按照注释解释 trace_bits 是用做SHM with instrumentation bitmap

3:init_count_class16:是一个技巧函数,记录大概路径,避免由于循环次数不一样而被记录为不同路径。需要重点理解两个变量数组count_class_lookup8[256]和count_class_lookup16[65536];count_class_lookup8[256]用来实现记录到达这个状态的次数,count_class_lookup16是因为AFL在后面实际进行规整的时候,是一次读两个字节去处理的,为了提高效率,这只是出于效率的考量,实际效果还是上面这种效果,count_class_lookup16的高八位代表某一状态的到达次数、低八位代表另一状态的到达次数,同时可以构成两个二元组,指示状态转变,高八位和低八位是0-255的数值,或许代表之前如第一部分所说的状态哈希。
同时,这样处理之后,对分支执行次数就会有一个简单的归类。例如,如果对某个测试用例处理时,分支A执行了32次;对另外一个测试用例,分支A执行了3次,那么AFL就会认为这两次的代码覆盖是相同的。当然,这样的简单分类肯定不能区分所有的情况,不过在某种程度上,处理了一些因为循环次数的微小区别,而误判为不同执行结果的情况。
以上对这个函数的理解不一定对,有很多猜测,欢迎指正。
4:setup_ipsm():初始化状态机,初始化graphviz图和khs_ipsm_paths、khms_states两个哈希表,若收到服务器的状态序列为interesting,则保存该kl_message。遍历该response的状态序列来更新状态机ipsm,将状态序列的状态和转移关系添加到graphviz图,同时更新两个哈希表。
5:setup_dirs_fds():该函数主要是对输入和输出的目录做了处理,函数流程如下:

  • 如果sync_id存在,且创建sync_dir文件夹,设置该文件夹权限为0700(拥有用户可读可写可执行)
    • 报错,且报错的原因不是文件夹已经存在,抛出异常
  • mkdir(out_dir, 0700),创建输出文件夹
    • 如果创建失败,错误为已经存在则调用maybe_delete_out_dir函数删除已有的所有数据,如果错误不是已经存在则报错
    • 创建成功,则判断in_place_resume是否为1,如果为1则报错
    • 部位1则用只读的方式打开out_dir,然后返回句柄给out_dir_fd。
    • 如果没有定义宏__sun则判断out_dir是否打开且能够调用flock函数上锁,二者只要一个返回True即报错
  • 接着一系列的文件创建,queue,queue/.stat,queue/.state/deterministic_done/等等
    其中包含一些文件打开的操作,这些文件夹的内容不太重要,后续分析crash的时候再去了解比这里方便。

6:read_testcasese()读取所有测试用例

  • 判断in_dir/queue是否可以存在,如果存在则设置in_dir为in_dir/queue。
  • 使用scandir和alphasort函数进行文件扫描,获得in_dir中的文件个数,nl_cnt = scandir(in_dir, &nl, NULL, alphasort);,扫描的结果存储在nl中,nl使用数组存储结果,返回的nl_cnt为个数(这个个数都把…包含了进去
  • 如果有效文件个数nl_cnt小于0,即为没有输入,则报错
  • 如果设置了shuffle_queue,且nl_cnt大于1,则执行shuffle_ptrs((void **)nl, nl_cnt);,作用是对nl数组中的内容进行重新排序
  • 通过for循环进行遍历,遍历所有的testcases
    • 利用stat文件的一些属性过滤掉一些没用的文件,一般是readme.txt …也可以加入自定义
    • 剩下的文件首先判断size,是否大于 1024 * 1024也就是1M,超出则报错,这里规定了最大的testcase只能是1M,也可自己更改
    • 判断in_dir/.state/deterministic_done/testcase这个文件是否可以访问,如果可以则设置passed_det为1
      • 这是为了在resum 扫面的时候使用,如果这个entry已经结束了deterministic fuzzing,在恢复异常终止的扫描时,我们不想重复deterministic fuzzing,因为这将毫无意义,而且可能非常耗时(来自注释)
      • 为了理解这个注释,需要知道fuzz的几个阶段:
        在AFL的fuzzing过程中,维护了一个 testcase 队列 queue ,每次把队列里的文件取出来之后,对其进行变异,下面就先粗略讲一下各个阶段的变异是怎样的。
        bitflip:按位翻转,每次都是比特位级别的操作,从 1bit 到 32bit ,从文件头到文件尾,会产生一些有意思的额外重要数据信息;
        arithmetic:与位翻转不同的是,从 8bit 级别开始,而且每次进行的是加减操作,而不是翻转;
        interest:把一些有意思的东西“interesting values”对文件内容进行替换;
        dictionary:用户提供的字典里有token,用来替换要进行变异的文件内容,如果用户没提供就使用 bitflip 自动生成的 token;
        havoc:进行很大程度的杂乱破坏,规则很多,基本上换完就是面目全非的新文件了;
        splice:通过将两个文件按一定规则进行拼接,得到一个效果不同的新文件;
        其中bitflip、arithmetic、interest、dictionary 是 deterministic过程(确定性过程),是dumb mode(-d) 和主 fuzzer(-M) 会进行的操作。而havoc、splice是随机过程,是所有fuzz mod都会采取的策略。
        为了避免无意义的变异,节约资源,所以deterministic fuzz不会重复执行。在后面的循环中,havoc和splice这类随机过程会反复执行。
        所以前面的pass_det设置为1,就是为了后面不重复执行deterministic fuzz这个过程。
    • add_to_queue(fn, st.st_size, passed_det);
    • 如果queued_paths为0,则代表输入文件夹为0,抛出异常
    • last_path_time = 0; queued_at_start = queued_paths;

7:6中用到add_to_queue:afl里面维护了一个testcase的队列queue,add_to_queue函数就是将新的testcase添加到queue中;max_depth为queue队列中元素个数,会随着扫描到的testcases的增加而增加;queue采用的维护方法是头插法;queue记录的是总的testcase数量,max_depth记录的是每一个队列里的数量,这两个变量有区别;每100个testcase作为一个队列管理。
pending_not_fuzzed记录的是待fuzz的testcase数量。
8:load_auto()是加载自动生成的额外内容。
函数使用循环自动load生成的字典。

  • 遍历循环从i等于0到USE_AUTO_EXTRAS(默认50)
    • 只读模式打开alloc_printf(“%s/.state/auto_extras/auto_%06u”, in_dir, i)
      • 打开失败则抛出错误
      • 成功则使用read函数从该文件中读出最多MAX_AUTO_EXTRA+1个字节到tmp数组里面,默认MAX_AUTO_EXTRA为32,读出的长度保存在len中
    • 如果满足条件if (len >= MIN_AUTO_EXTRA && len <= MAX_AUTO_EXTRA) 则调用maybe_add_auto(tmp,len)函数,否则结束
      大致意思就是从目标文件中读出一些内容,如果读出的大小在规定的范围内则调用maybe_add_auto函数

9:maybe_add_auto 重点函数,用于添加token

  • 如果用户设置了MAX_AUTO_EXTRAS或者USE_AUTO_EXTRAS为0,则直接返回。
  • 循环遍历i从1到len,mem[0]和mem[i]异或,如果相同,则结束循环。
  • 如果len的长度为2,就和interesting_16数组里的元素比较,如果和其中某一个相同,就直接return。
  • 如果len的长度为4,就和interesting_32数组里的元素比较,如果和其中某一个相同,就直接return。
  • 将tmp和现有的extras数组里的元素比较,利用extras数组里保存的元素是按照size大小,从小到大排序这个特性,来优化代码(具体体现在两个循环,第一个找到相同大小,第二个对相同大小的进行寻找)。
    • 遍历extras数组,比较memcmp_nocase(extras[i].data, mem, len),如果有一个相同,就直接return。
  • 通过筛选,设置auto_changed为1
  • 遍历a_extras数组,比较memcmp_nocase(a_extras[i].data, mem, len),如果相同,就将其hit_cnt值加一,这是代表在语料中被use的次数,然后跳转到sort_a_extras
  • sort_a_extras用了两个快排,对a_extras进行排序。第一个是根据使用次数进行降序排序。第二个是对根据size进行排序。
    • 排序完毕之后进行的操作是:首先判断a_extras_cnt是否小于MAX_AUTO_EXTRAS,如果小于,则表示a_extras数组没有被填满,所以此时直接将候选token加入到a_extra数组里。
    • 如果a_extras_cnt大于MAX_AUTO_EXTRAS,则从a_extras数组的后半部分里面随机选择一个元素,用候选token替换,也就是更改a_extras[i].data,a_extras[i].len。然后将a_extras[i].hit_cnt设置为0.

10:pivot_inputs()在输出目录中为输入的测试用例创建硬链接

  • 创建queue_entry结构体,初始化为queue(输入队列)
  • 循环遍历所有输入
    • 通过strrchr得到文件名字,例如/123.pdf,然后通过后面rsl++,将rsl编程123.pdf,起到一个去除/的作用
    • 根据rsl(文件名)操作
      • 如果文件名字满足:1)rsl的前三个字符必须是id:;2)将id:后面的内容赋值给orig_id,如rsl是id:123123123123,则orig_id为123123123123。3)比较orig_id和id是否相等,如果源文件名字和我们记录的id号是一样的,那么就使用原来的文件名就行了,这样做是为了方便恢复模糊测试现场。如果满足上述三个条件,则设置resuming_fuzz=1,然后将nfn赋值为outdir/queue/rsl。
      • 否则,对对文件进行规格化命名,nfn=out_dir/queue/id:xxxxxxx,orig:use_name。如1.png命名为"/home/tamako/Desktop/FUZZ/AFL_debug/work_dir/fuzz_out/queue/id:000000,orig:1.png"
    • 将nfn赋值给q->name
    • 如果q的passed_det为1,assed_det这个变量,是为了标识是否经过了deterministic fuzz这个过程,如果该标识符为1,就表示该testcase是经历过确定性变异,然后调用mark_as_det_done函数。mark_as_det_done函数的主要作用就是将该testcase文件放入deterministic_done(out_dir/queue/.state/deterministic_done/use_name)文件夹,这样做是为了将经历过deterministic fuzz过程单独放置,节省处理时间。如果不存在就创建这个文件,然后设置q的passed_det为1。这里的use_name就是orig:后面的字符串
  • 如果in_place_resume为1,执行nuke_resume_dir
    这个函数的作用大致是删除一些用于恢复会话的临时目录,out_dir下面的id:前缀的文件,可能是resuming fuzz中的某种设定,恢复之后删除硬链接

11:load_extras(区别于load_auto)
load_auto是加载自动生成的extras。load_extras这个函数则是加载自己指定的extras。如果你指定了extras_dir,则会从这个目录里加载extras,并按size排序。
12:find_timeout
这是个超时处理函数,可以通过-t设置超时时间,但是如果在没有设置-t的情况下resuming session,也能够通过这个而函数迅速确定超过时间。
如果指定了resuming_fuzz即从输出目录当中恢复模糊测试状态,会从之前的模糊测试状态fuzz_stats文件中计算time out的值,保存在exec_tmout中,如此一来在没有指定-t 的情况下resuming session的时候,则能够迅速的确定超过时间,而不是让系统一次又一次的自动调整。

  • 如果 resuming_fuzz为0,则直接return
  • 如果设置了in_place_resume,设置fn为 out_dir/fuzzer_stats
  • 没有设置in_place_resume则fn=in_dir/…/fuzzer_stats
  • 以只读方式打开目标文件,然后读入tmp数组,最后在tmp文件流里面检索exec_timeout
  • 没有检索到则退出
  • 检索到了则判断该值的大小,如果大于4,则设置为exec_tmout的值
  • 小于4则退出
  • 设置timeout_given为3
    13:detect_file_args,检查参数中是否有@@,如果有则替换为out_dir/.cur_input。
    14:setup_stdio_file:当用户不指定输出目录的时候,会自动设置输出目录,不过我们一般会自己设置输出目录
    15:check_binary,对文件路径进行检查,检查文件是否存在且不是一个shell脚本,检查方式是检查文件内容,文件内容的开头是不是#!第二个作用是检查ELF头以及程序是否被插桩编译。

fuzz试运行(9218-9229)

先获取fuzz开始时间,检查是不是在qemu模式下,然后
1:perform_dry_run (关键函数):将input文件夹里的所有testcase作为输入,生成初始化的queue和bitmap,仅对初始输入执行且只执行一次,以确认应用程序按照预期。

  • 读取AFL_SKIP_CRASHES环境变量,存储值到skip_crashes,设置cal_failures为0
  • 遍历queue(输入队列)
    • 打开q->fname即打开测试目标,然后通过read读取到use_mem中
    • res = calibrate_case(argv, q, use_mem, 0, 1) 该函数内是主要的的run过程,用于校准testcase,是fuzz的主要运行函数,res是返回的执行结果
      • 首先,如果dumb_mode!=1或者forkserver没有启动,则调用init_forkserver来fork一个进程
        • 父进程通过fork创建一个进程之后,父进程和子进程就同时从fork处开始分别往下执行,就像从这里开始世界线分叉了。
          在这里插入图片描述
          在这里插入图片描述

        • init_forkserver这个函数:父进程在fork出一个子进程之后,子进程满足if(!forksrv_pid),所以子进程进入if语句块。而父进程不满足if(!forksrv_pid),所以父进程会直接跳过这个if语句块。1)调用execv()函数执行某一个程序时,如果运行的程序不结束,execv()是不会return的。所以可以看到,只有在程序结束,子进程才会exit(0),然后通过一个EXEC_FAIL_SIG信号,告诉父进程目标程序执行失败;2)父进程则直接跳到了下面的位置,等待子进程创建的server上线,如果收到了四个字节的"hello"消息,说明server已经上线,则父进程return,结束init_forkserver函数。

      • 结束init_forkserver函数之后,首先判断这个testcase是不是第一次被运行。如果是第一次运行这个testcase,则拷贝trace_bits到first_trace里。然后调用get_cur_time_us函数,得到开始时间。然后进入进入一个循环,如果不是第一次执行这个testcase且stage_cur%stats_update_freq == 0.则会调用show_stats函数,刷新展示界面,可以通过调整stats_update_freq来调整刷新频率。
        • 然后调用wirte_to_testcase函数,这个函数的作用是使用ckwirte的方法,把testcase的内容写入out/.cur_input中。但是因为AFLnet通过网络传输testcase,所以不需要这个函数。AFLnet传输testcase的函数叫send_over_network,后面遇到再分析。
        • run_target:这个函数的主要作用是执行目标应用程序,监控超时,返回状态信息,然后利用调用的程序更新trace_bits[].init_forserver这个函数已经通过fork子进程,启动了目标程序,但是1)如果在dumb mode(相当于黑盒)或者no_forkserver下运行的话,在这里就通过run_target函数启动目标程序。2)如果不是dumb_mode,或者之前已经fork过一个子进程用来执行目标程序了,则通过管道来进行父进程和子进程之间的通信。目的是告诉父进程子进程的存在,然后获取子进程进程号,设置超时时间,调用setitimer函数等待子进程终止。
          • 通过send_over_network函数发送数据到目标,来造成目标服务器进入各种状态。后面我们会分析是怎样通过send_over_network函数来传输数据的。不同的是,由于dumb_mode模式或者no_forkserver无法得知目标服务器的状态,所以这里需要通过一个waitpid函数来获取子进程状态,从而确认目标服务器是否存活。如果不是dump_mode,则可以直接利用read函数,通过读管道读出目标状态。
          • 通过WIFSTOPPED函数判断子进程是否已经结束。然后获取当前时间作为运行结束时间,与之前的开始时间做减法,得到运行时长。然后total_execs自增1。total_execs是这个过程执行的次数。
          • 然后用classify_counts函数,对trace_bits共享内存记录的路径执行次数进行分类,是一个对稀疏位图进行优化的操作
          • 从下面可以看出根据目标服务器的状态,会返回三种类型:FAULT_CRASH,FAULT_ERROR,FAULT_NONE.其中:FAULT_CRASH和FAULT_ERROR状态都表示当前运行的testcase造成了目标服务器崩溃。FAULT_NONE表示当前testcase的运行时间超过了用户定义的超时时间的情况,也就是说这个testcase在我们定义的超时时间内并没有造成目标服务器崩溃。
          • run_target函数进行fuzzer与目标服务器之间的通信,通过这个函数将testcase发送到服务器,然后判断服务器状态。
        • send_over_network函数,传输变异之后的testcase。aflnet与afl的区别也主要在于这个函数。
          • 首先是一些参数的设置,比如目标服务器的IP地址和端口号,超时设置,轮询间隔。
          • 创建并初始化消息存放的缓冲区
          • 创建TCP/UDP的sockets
      • 然后是update_bitmap_score函数,在每一次run_target函数结束之后执行,主要是对于当前的testcases造成的程序执行路径进行评估。
        • 在进入这个函数之前,需要收集testcase的各种状态参数,形成一个结构体。
        • bitmap_size的计算,是通过count_bytes函数进行计算的,count_bytes函数是通过计算位图中设置的字节数,主要作用是更新状态或者检查新的路径。
        • 不同的程序执行路径会触发位图中不同的位,我们要用尽可能少的时间和尽可能少的testcase,覆盖尽可能多的位。每一条执行路径的“有利”程度,就是指这个程序执行路径执行时间是否短,是否覆盖到更多的位。
        • 当我们遇到一条新路径时,我们调用它以查看该路径是否比任何现有路径看起来更“有利”。“有利”的目的是拥有一组最小的路径来触发到目前为止在位图中看到的所有位,并专注于模糊它们,而牺牲其余位。该过程的第一步是为位图中的每个字节维护一个 top_rated[] 条目的列表。如果没有先前的竞争者,或者竞争者具有较小的唯一状态计数,或者它具有更有利的速度 x 大小因子,我们将赢得该插槽。
        • 实现方式:
          • 首先,定义一个fav_factor,它的值等于当前执行testcase队列的执行时间与testcases数目的乘积。
          • 设置一个数组top_rated[i],表示trace_bits中第i个字节的最“有利”路径。
          • 然后循环遍历trace_bits中的每一个字节,如果trace_bits[i]为0,表示这个位置还没有被覆盖到,如果trace_bits[i]为1,则表示该路径已经被覆盖到了,这个时候检查对应位置的top_rated,进行下面的比较:
            • 首先,如果不存在top_rated,说明当前testcase队列就是最优路径,这个时候直接跳过if语句,执行top_rated[i]=q,将最优路径设置为当前路径。
            • 如果存在top_rated:则首先比较当前testcase队列和top_rated的状态数(unique_state_count),如果当前testcase队列造成的独特状态数小于top_rated造成的独特状态数,则跳过该次循环,进行下一次循环。
            • 然后进行fav_factor的比较,如果当前testcase队列的fav_factor小于top_rated,则当前testcase队列获胜,
            • 当前testcase队列获胜之后,会减少top_rated的引用次数(top_rated[i]->tc-ref),如果top_rated的引用次数本来就为0,则删除当前top_rated.
            • 如果当前的test case队列获胜,则将当前testcase队列设为top_rated[i].然后将他的引用次数tc_ref加1。
              如果该队列条目还没有最小化的trace_bits数组,则是哟个minimize_bits函数分配和填充一个数组。
              最后设置变量score_changed为1,表示位图已经更新。
  • update_state_aware_variables,在试运行模式下更新状态机
  • 如果没有设置stop_soon则直接返回
  • 根据不同的res 有不同的处理
    • res == crash_mode || res == FAULT_NOBITS
    • FAULT_NONE
      • 首先检查q是不是头节点,如果是,则执行check_map_coverage,检查trace_bit,即检查bit map,统计其中被标记的个数,如果小于100则直接返回,检查数组后半段,如果有值则直接返回,抛出警告;
      • 如果是crash mode,则抛出异常,但是文件不会结束。
    • FAULT_TMOUT
      • 如果指定了-t参数,则timeout_given值为2,抛出警告WARNF(“Test case results in a timeout (skipping)”);,并设置q的cal_failed为CAL_CHANCES,cal_failures计数器加一。
    • FAULT_CRASH
      • 如果没有指定mem_limit,则可能抛出建议增加内存的建议,但不管指定了还是没有,都会抛出异常FATAL(“Test case ‘%s’ results in a crash”, fn);
    • FAULT_ERROR
      • 抛出异常Unable to execute target application
    • FAULT_NOINST
      • 这个样例运行没有出现任何路径信息,抛出异常No instrumentation detected
    • FAULT_NOBITS
      • 如果这个样例有出现路径信息,但是没有任何新路径,抛出警告WARNF(“No new instrumentation output, test case may be useless.”),认为这是无用路径。useless_at_start计数器加一

2:cull_queue()函数,精简队列,遍历top_rated中的每一个queue。然后寻找能够发现新的edge的queue,并标记为favored。这样在下一轮fuzz的过程中,这些能够发现新edge的queue就有更多执行fuzz的机会
3:show_init_stats:在处理输入目录的末尾显示统计信息,以及一堆警告,以及几个硬编码的常量。

Fuzz主循环(9231-)

1:find_start_position函数的作用是找到一个queue入口的testcase,也就是一个testcase队列的第一个testcase。然后返回值传给seek_to,后面会用到。
2:write_stats_file函数是更新状态的函数。
3:save_auto函数是更新token的函数,保存token的目录为"%s/queue/.state/auto_extras/auto_%06u", out_dir, i
4:有一段调试用的代码,用来暂停程序

  /* Woop woop woop */
  if (!not_on_tty) {
    sleep(4);
    start_time += 4000;
    if (stop_soon) goto stop_fuzzing;
  }

5:使用状态机指导fuzz:if (state_aware_mode),可以通过在命令中增加-E选项开启

  • 如果服务器状态数为0,即服务器没有返回相应报文,报错退出
if (state_ids_count == 0) {      
   PFATAL("No server states have been detected. Server responses are likely empty!");
 }
  • fuzz主循环
    • 在主循环中首先初始化selected_seed为NULL,然后判断种子是否未选择以及选择的种子的region数量是否为0,满足任意一个则进入一个用于选择状态的循环
      • 调用choose_target_state选取目标状态(这里的state_selection_algo是一个枚举变量,表示选择哪种状态选择策略,默认是轮询选择)
      • 基于已选择的状态调用cull_queue精简队列
      • 更新一个状态被选取过的次数(这里用到的是khash)
      • 调用choose_seed选择种子,其中seed_selection_algo也是一个枚举变量,表示种子选择策略
      • 如何选择状态和种子,下一次再看吧,累了,想毁灭啦
    • 如果selected_seed不为NULL,就要在queue里寻找这个种子并将其设置为queue_cur,调整queue_cur到selected_seed
    • 接下来调用fuzz_one函数进行一轮fuzz(fuzz_one函数之后再细看)
    • 然后根据三个值来判断是否要调用sync_fuzzers(作用是读取其他fuzz的queue中的testcase,然后保存到自己的queue中)
      • 其中skipped_fuzz表示是否开启了IGNORE_FINDS模式,如果开启则fuzzer只会使用原始种子文件进行fuzz
      • stop_soon则是由用户的ctrl+c操作进行设置,表示用户是否要停止fuzzing
      • sync_id表示fuzzer 的id
      • 如果三个条件都满足,则将sync_interval_cnt加1模5并判断是否为0,为0则调用sync_fuzzers。即在一切正常的情况下每SYNC_INTERVAL(默认为5)次调用一次sync_fuzzers。

6:不利用状态机指导的fuzz循环

  • 其实主体差不多,只讲区别
  • seek_to我们前面说过了,是通过find_start_position函数得到的,目的是为了找到queue的开始testcase。下面通过一个循环,使queue_cur移动到了queue的首部,也就是一个queue的开始。
  • 然后如果循环了一整个queue之后还是没有新的发现,则需要重组策略-sync_fuzzers这个函数。
  • 这里注意到一点,此处代码与利用状态机fuzz那部分,多了两句:
      queue_cur = queue_cur->next;
      current_entry++;
  • 原因在于状态感知模式会在开头通过选择状态和种子来确定本次fuzzing所用的queue是什么,而此处是顺序往下走的

7:之后就是展示和退出操作啦,如果用到之后再看吧。逐渐看的崩溃,感觉还有好多没有讲清楚。

参考资料

https://blog.csdn.net/github_53542847/article/details/123863974
https://blog.csdn.net/github_53542847/article/details/125876034
https://bbs.kanxue.com/thread-276306.htm
https://blog.csdn.net/m0_50819561/article/details/129326221
https://blog.csdn.net/github_53542847/article/details/123863974
https://blog.csdn.net/von_Neumann_/article/details/128078090
http://www.hackdig.com/12/hack-854647.htm
https://forum.butian.net/index.php/share/2092
一并致谢,同时如果本文有错误或需要补充,欢迎在评论区指正,尽力变得更完整、准确、易懂。之后会陆续更新细节。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值