欢迎来到编写fuzzer系列的第三部分,我收到了一些读者的来信,大家对模糊测试技术非常感兴趣并且想要了解更多的技术细节,在这里我非常感谢大家的支持与鼓励。
在本篇中我将致力于优化我在上一篇中写好的fuzzer,为了能让更多的人更好的理解文章中的内容,我将接下来的内容分为两篇,在这篇文章中我们将讨论覆盖率的跟踪与实现,下一篇文章中我们将讨论这些优化如何指导fuzzer并使其有更好的返回结果。
让我们先从一些理论基础开始讲起:“覆盖率引导fuzzing(greybox fuzzing),使用程序检测跟踪已经喂给fuzz目标的样本数据并检测变异后的样本的覆盖率,fuzzing引擎使用检测跟踪后的信息来判断那种变异策略可以使覆盖率最大化”正如链接文档解释的那样,这种技术适用于对非结构化数据具有适当容忍度的确定性目标,比如第一篇的jpeg解析器;但是要注意,像编程语言这样的高度结构化输入,随机变异很可能无法生成有效的样本,因此fuzzer的运行深度会受到影响。可惜的是有关于覆盖率跟踪这样庞大的实验,我懂得并不多,正因如此才有必要学习并亲自编写fuzzer;最后,关于这次的代码可以再GitHub上找到。
为了能让大多数人都能理解文章中的内容,我使用了Python,但是因为Python执行效率的原因,最后我将放弃Python并用rust重写所有内容。我准备写一些简单的代码来了解我的fuzzer每秒能执行多少次迭代并从中计算出fuzzer的性能,但当我看到中间的某个地方有一个自定义的线程类,并在固定的时间间隔生成其他对象,这意味着“简单”的代码变成了“如何在线程之间传值”这种复杂的问题,于是我果断使用了另一种方法来实现性能的计算:
start_time = time.time() ... # FUZZ LOOP HERE ... x = counter / (time.time()-start_time) print('-> {:.0f} exec/sec'.format(x))
我使用运行的迭代书除以运行时间这种简单的方法来计算性能,目前这已经够用了,在下一阶段我将以不同的方式实现它,特别是能进行连续的模糊测试时。
如果你真的对如何配置程序感兴趣(改变性能),有两篇Python模块的文章可以帮到你——profile和memory_profiler。
我有一些关于测试程序执行时所花费的成本的想法,我之前略过了一个重要的概念——覆盖率粒度,我们可以跟踪已执行的函数、块甚至指令,目前我的fuzzer的粒度保持在函数级,但我决定挑战将粒度上升到块级。我依然要使用ptrace来达到目的,我最初的想法是在代码执行过程中检查指令,这样不论程序做了什么操作、调用了哪些函数我都能准确的了解,但是这个方法有严重的缺陷,没办法了解覆盖了多少程序(覆盖率无法计算),另一个就是因为每条指令都要检查,所以执行速度从原来的300exec/sec降到了1exec/sec。显然这种方法行不通。AFL的做法是函数插桩,但是我并不想直接深入编译器内部,所以我开始考虑ptrace
我们的fuzzer已经能够处理信号了,再添加一些代码来处理SIGTRAP也没什么问题,所以我决定在每个函数开始处设置断点,但是理想很丰满,现实很骨感
生成函数列表
第一个问题就是在哪里下断点,这只需要反汇编程序,找到以标识的函数起始地址就可,我写了个binary ninja的自动下断脚本:
def main(): parser = argparse.ArgumentParser() parser.add_argument('-b', '--binary', help = 'binary to analyze', required=True) args = parser.parse_args() bv = bn.BinaryViewType.get_view_of_file(args.binary) # select appropriate segment for s in bv.segments: if s.executable: base = s.start for func in bv.functions: # filter out the list of functions if func.symbol.type == bn.SymbolType.ImportedFunctionSymbol: continue if func.name in skip_func: continue print('0x{:x}'.format(func.start - base))
你可能很好奇为什么不在fuzzer运行时下断,在无头模式(”headless mode”译者没有使用过binary ninja,不知道这个模式的正确名称。。。)下工作是需要付费的,并不是每个读者都有这个付费能力,考虑到新人的感受,我设计了一种工具可以直接输出一个地址列表供fuzzer使用,你可以使用objdump或者awk/sed生成地址列表,喜欢挑(作)战(死)的,也可以使用Radare2。
关于脚本,有两点需要解释一下,计算下断位置的地址,首先我们需要得到.text段(代码所在位置)的偏移,在二进制文件运行时.text段会被加载到一个虚拟地址中,所以不要直接把地址写死,但是如果将偏移放到正确的内存区域的开始位置时,就能在函数开始处直接下断。
之前我们提到了跟踪所有函数,但是我们并不需要跟踪libc的函数以及一些设置函数,下面是要跳过的函数列表:
skip_func = ['__libc_csu_init', '__libc_csu_fini', '_fini', '__do_global_dtors_aux', '_start', '_init']
事实上我是看到了一些同事的文章中提到了函数筛选,所以我才做的,这里需要跳过多少函数要取决于你的目标是什么。
Fuzzer加载了断点列表后可以实现断点插入和信号处理了,这是完整的函数:
def execute_fuzz(dbg, data, counter, bpmap): trace = [] cmd = [config['target'], config['file']] pid = debugger.child.createChild(cmd, no_stdout=True, env=None) proc = dbg.addProcess(pid, True) base = get_base(proc.readMappings()) # Inser breakpoints for tracing if bpmap: for offset in bpmap: proc.createBreakpoint(base + offset) while True: proc.cont() event = dbg.waitProcessEvent() if event.signum == signal.SIGSEGV: # getInstrPointer() always returns instruction + 1 crash_ip = proc.getInstrPointer() - base - 1 if crash_ip not in crashes: crashes[crash_ip] = data proc.detach() break elif event.signum == signal.SIGTRAP: trace.append(proc.getInstrPointer() - base - 1) elif isinstance(event, debugger.ProcessExit): proc.detach() break else: print(event) # Program terminated return trace
关于这个函数我们只讨论重要的部分,要想在函数下断,我们需要知道代码段的基地址,可以通过执行下面的get_base()函数(CV大法好)获得:
def get_base(vmmap): for m in vmmap: if 'x' in m.permissions and m.pathname.endswith(os.path.basename(config['target'])): return m.start
此函数会加载所有的内存映射,并试图找到一个路径名与目标匹配的映射,这种方法也可以帮助我们跟踪crash时解决ASLR
接下来我们将简单的遍历函数偏移列表(bpmap),通过基址+偏移的方式算出函数地址后下断:
if bpmap: for offset in bpmap: proc.createBreakpoint(base + offset)
当调试器遇到断点时,会产生一个SIGTRAP信号,我们需要对这个信号做额外的处理:
elif event.signum == signal.SIGTRAP: trace.append(proc.getInstrPointer() - base - 1)
我们的覆盖跟踪相当简单,我们只记录了运行时执行的函数,稍后我将规范化跟踪,事实上这些跟踪代码在现在的用处并不大,在下一篇中我将很好的利用它。
还有一些更好的方法,比如像@5aelo那样:一次性手工插入断点;这种方法可以在执行循环时节省时间,但是可能会出现一些问题,比如:当程序在阴影空间手动命中断点时,我必须要步过。最重要的是,我不确定在实现反馈信息之前粒度是否足够,稍后我会测试一下,如果粒度不够,我们将实现一个块级的解决方案。
糟糕的是我们的工具从300exec/sec降到了50exec/sec,性能慢了六倍的同时crash并没有更精确。但正如之前承诺的那样,我将尝试完全改变变异策略以提高性能,通过遗传算法来决定哪些变异后的样本要丢弃,哪些要保留并进一步变异,希望能挖出新的bug
因译者水平有限,翻译如有出错请指出 :)
请问看雪的翻译小组还缺人吗??
原文链接:https://carstein.github.io/2020/05/02/writing-simple-fuzzer-3.html