AFL 源码精读笔记 - Fuzzer 部分

  本文是笔者阅读AFL源码的核心代码部分阅读笔记,主要内容为AFL fuzzer结构和流程等,不包含插桩内容。由于另一部分笔记直接写入代码注释,可能部分内容不是很完善,但笔者自以为将基本所有AFL流程要点记录在笔记中,欢迎各位交流。

AFL-showmap

  • 解析各个参数,详见Default说明

    • -b 隐藏选项,以二进制格式输出结果(类似outdir/queue/fuzz_bitmap
    • -t nn+ sets time_out to 2, tolerate but skip queue entries that time out
  • setup_shm 配置共享内存

    • 通过环境变量SHM_ENV_VAR 传递共享内存的地址(十进制数据)
    • atexit注册共享内存移除函数,将在退出时调用(与fini_array不是同一个结构,后者是静态写入ELF中,前者是动态注册,但是都在同一套体系中_dl_fini
  • setup_signal_handlers

    • 通过设置sigaction,改写信号捕获处理例程
    • SIGHUP SIGINT SIGTERM -> handle_stop_sig kill child proc, stop_soon = 1
    • SIGALRM -> handle_timeout kill child proc child_timed_out = 1
  • set_up_environment

    • ASAN_OPTIONS
    • MSAN_OPTIONS
    • if AFL_PRELOAD -> LD_PRELOAD DYLD_INSERT_LIBRARIES
  • find_binary 在 PATH 或当前路径下寻找目标文件

    • 如果文件路径不含/,则被认为是PATH下的文件,会在每一个PATH下搜索
    • 当前路径下文件需要加./
  • run_target Main Routine

    • Set Memory Barrier
    • fork child process
    • add memory limit with setrlimit
    • if -c option not set, don’t keep core dumps
    • setsid 脱离父进程的进程组、打开的终端、Session ID
    • execve execute the target binary
    • set timer for child proc
    • classify_counts 预处理 trace_bits,将其分为几个bucket(详见 [Technical Whitepaper](#Coverage measurements))
    • write_results按照是否为 binary_mode 将trace_bits输出

AFL-fuzz

  • 获取各项参数

  • 检查CPU状态(check_crash_handling检查核心转储位置等)

  • read_testcases读取测试用例,并将其归入输入pending queue中

    • 可以通过未清理的文件out_dir/.state/deterministic_done/testcase_name,恢复之前完成的确定性测试部分

    • add_to_queue 将 testcase 加入队列

        q->fname        = fname;		# origin input testcase name
        q->len          = len;		# testcase length
        q->depth        = cur_depth + 1;	# queue depth of the testcase
        q->passed_det   = passed_det;		# whether passed deterministic fuzzing
      
      • queue 含有 next_100 成员,用于快速跳转(仅有!(index % 100)含有)
  • load_auto Load automatically generated extras

  • pivot_inputs Create hard links for input test cases in the output directory

    • 将输入的testcases映射到输出queue中,并修改为对应标准文件名

    • 原输入文件名为id:%06u,orig:%s

  • detect_file_args 将参数中@@换为输入testcases (out_dir/.cur_input)

  • setup_stdio_file 如果没有设置@@,则打开新文件out_fd作为输入

  • check_binary 查找binary,判断是否为shell,判断是否被插桩

    • 通过查找__AFL_SHM_ID (用于forkserver查找shared memory)判断插桩
    • 通过查找__msan_init判断是否启用Address Sanitizer
    • 通过查找Persistent Sig (SIG_AFL_PERSISTENT)判断是否启用Persistent Mode
    • 通过查找Defer Sig (SIG_AFL_DEFERRED_FORKSRV)判断是否启用Deferred Mode
  • 如果处于qemu mode,则还需要获取qemu参数(参数附加于目标文件名后(–))

  • perform_dry_run Perform dry run of all test cases to confirm that the app is working as expected

    • 遍历队列,取出fname,读取文件到内存中

    • calibrate_case 校准testcase

      • 执行queue中所有testcase,并对对应测试结果进行处理/反馈
        • new stage: calibration
        • stage_max: 阶段轮数 3 or 8
        • init_forkserver - child
          • 使用两个管道st_pipe ctl_pipe, 传递状态控制命令
            • 管道0号为接收,1为发送
          • 重定向输出和错误输出到null
          • 如果使用out_file,则重定向输入到null,否则,重定向到out_fd(见setup_stdio_file
          • set LD_BIND_NOW,避免LD_BIND_LAZY延迟绑定机制降低fork后运行效率
          • execve执行目标程序,目标程序会进入fork_server,完成初始化操作
        • init_forkserver - fuzzer
          • status读取forkserver状态(hello message),确认forkserver启动
        • write_to_testcase 将数据(testcase)由内存写入输入文件,供target使用
        • run_target 同show map (此过程中已完成trace_bit预处理)
        • 通过hash32计算校验和cksum,通过校验和判断map是否变化
          • 如果之前几轮已经计算过trace_bits,并且两者的某位不同,则此边为可变边,执行路径不完全与输入相关。并且,延长循环次数,避免遗漏可变边。
          • 若第一次执行,则将本次结果更新为first_trace
    • 得到返回值res,判断错误类型

      • 如果没有增加的路径/边,则警告用户,此testcase无用
  • has_new_bits 检查当前执行路径是否有新的tuple,更新virgin bits

    • 采用部分循环展开加速运算

      virgin位图会初始化为全1
      *current为true且*current & *virgin为true说明当前用例执行到了一些边且在该执行情况在virgin位图中还没有出现过,
      *virgin &= ~*current;语句将表示当前执行情况的位在virgin位图中标出,下次在出现相同的执行情况时if语句将判断为false,即没有出现新的情况
      	举个例子,某个边初始为1111 1111,一个用例执行到该边4次,在trace_bits位图中表示为0000 1000。
      ~*current为1111 0111,与*virgin进行与操作后为1111 0111,此时*virgin被赋值为1111 0111。
      下次再有一个用例执行到该边,执行次数为4,5,6,7时,经过classify_counts函数计数后在trace_bits位图中为0000 1000,与*virgin即1111 0111做与操作为false
      	因此一个边的某个执行次数被触发过,下一次再有用例触发同一个计数桶中的执行次数时,将不被视为新的情况
      
    • 返回值共有三种情况:

      • 0: 没有发现任何新边
      • 1: 有已到达边的新触发bucket
      • 2: 含有以前从未到达的边
  • 如果发现测试中bitmap不同,则出现variable edge(不定边,在相同输入下执行情况不同,与随机等有关),标记该bit,并将

  • 为后续操作进行提前性能数据统计

  • update_bitmap_score 找到新边后,需要评估当前路径是否为"favorable",即维护一个能触发全部当前总bitmap的最少的边集合,并使开销尽量小

    • 以上第一个条件并没有被严格执行
    • 使用speed * size作为fav_factor,比较选择更小的,作为当前bit的关联path
    • 后续cull_queue对path数量做了缩减,但不是最优解

Main loop

  • cull_queue see above

  • sync_fuzzers sync between parallel fuzzers

  • fuzz_one 主程序,从序列中取出一个testcase (queue_cur)运行fuzz

    • 根据pending_favored是否有favored testcase正在等待,以及当前状态信息,选择是否跳过当前testcase
    • 将testcase映射到内存中
    • calibration (如果之前的测试失败,就重做)
    • trime_case修剪新的testcase,以减少确定性测试时的循环
      • 使用二分长度,不断缩小删去的长度
      • 以删去长度为分隔,将testcase分为几段(不足也算一段),依次遍历删除一段
      • 如果删除后对bitmap的checksum没有影响(得到相同checksum),则认为是相同的执行路径,删除有效
      • 如果进行过trim,则会标记为need_write,在全部trim结束后进行回写,并重新评估分数
    • calculate_score 计算testcase的性能分数,以执行时间和bitmap大小作为评分因素
      • 较晚发现的path可以获得handicap增益,在初期获得更多运行时间
      • 更深的testcase被认为能获得更多发现,所以具有分数增益
    • 如果已经执行,或者跳过执行确定性测试阶段,则直接跳转到havoc_stage,开始非确定性测试
    • 每个mutate后(见[下一节](#Mutation Method)),都会进行common_fuzz_stuff,运行变异后的testcase,并评估其是否有价值,变异获得的有价值的testcase会入列,等待后续操作

Mutation Method

ordered by position in code

前四种过程没有随机性,称为deterministic fuzzing,只有非dumb mode,以及main fuzzer使用。

  • bitflip,位翻转
    • 依次翻转每一位;同时翻转连续的2 bit/4 bit;按字节翻转连续的1 byte/2 byte/4 byte
    • on 8 bit flap stage,we use effictive map to identify effectless bytes (no effect on exec path even fully flipped),如果一个testcase的90%(EFF_MAX_PERC)都是有效的,则认为其全部有效,使其全部进行后续测试,否则将会跳过后续时间更长的deterministic fuzz环节(arithmetic)
    • 判断语义token:如果翻转一段bytes中的任何一个bit,都会产生同样的结果,则大概率这一段是语义检查的token
    • 由经验,在变换最低位时进行检查,效果最好,因为路径不太会产生明显变化
    • 检查时,如果发现行为变化,并且前段行为满足extra长度,则尝试加入auto extra(maybe_add_auto
      • interesting 数据互不相同,extras按照len排序(升序),auto extras 按照use count 降序排列
  • arithmetic,算数运算
    • INC/DEC 对char/short/int依次进行+/-数学运算(在一定范围内)
    • 会受到effective map的影响,自动跳过某些字节
    • 对大端序和小端序均有计算
  • interest,将一些特殊内容写入输入文件中(特殊内容预定义硬编码,与extra不同)
    • 将每个byte/word/dword替换为interesting关键字
  • dictionary,将自动生成或用户提供的token替换/插入到源文件中
    • 包含over insert两种模式(替换/插入)
    • 由于user extra按长度排列,不需要恢复原数据,因为一定会被覆盖
    • 会跳过超出最大extra范围的、放不下的、长度内都是effectless的

后两种过程具有随机性,所有fuzzer均会使用:

  • havoc “浩劫”,将原文件大量变异
    • 轮数受到perf_score,即testcase执行效率的影响,以及是否完成确定性测试,但通常都很大,在千量级
    • 按照随机数随机进行不同的动作(确定性变异+随机参数),通过随机数选择变异操作方式
  • splice “铰接”,将两个文件拼合为一个文件
    • 完整一轮执行完后,如果还没有发现,就使用它(来自文档,从逻辑上看,即使有发现,也会运行这一部分)
    • locate_diffs 比骄傲两个buffer,返回第一个和最后一个不同的字节偏移,用于确定合并位置
    • 在第一个和最后一个不同的字符之间接入
    • 合并后返回havoc

stage_name 中的标号a/b,表示perform len/step,即进行操作的位数/迭代步长

你可能感兴趣的:(Fuzzing,linux,安全性测试,模糊测试)