首页
社区
课程
招聘
[翻译]写一个简单的fuzzer-part 2
2020-5-8 23:50 7805

[翻译]写一个简单的fuzzer-part 2

2020-5-8 23:50
7805

前言

在本系列的上一篇英文版)中,我们实现了一个非常简单的fuzzer,因为我们使用的是随机的变异策略且没有任何反馈信息来证明我们对原始文件所做的变异是有效的(之后我们会对此进行优化),所以这个fuzzer很难胜任对复杂目标的fuzz。另外就是上一篇中编写的代码并不完整(主要是只有功能代码,并没有调用代码的过程),在这一篇中我们依然只有功能代码,但是为了能帮助新手理解整个工程,我已将所以的代码放到GitHub上供大家学习。

代码优化

随着目标越来越复杂,我们早期的fuzzer代码显然是不够用的,所以我将在不改变原有代码功能的情况下对代码的三个部分进行优化。第一个就是支持全局配置文件和运行时的部分信息提示:

config = {
  'file': 'mutated.jpg', # 要fuzz的程序的名称
  'target': '',     # 要fuzz的程序的位置
  'corpus': '',     # 原始数据样本
  'rounds': 100000,  # fuzz迭代的次数
  'seed': None,       # 随机种子
}
 
def main():
  parser = argparse.ArgumentParser()
  parser.add_argument("-t", "--target", help = "target program", 
      required=True)
  parser.add_argument("-c", "--corpus", help = "corpus of files",
      required=True)
  parser.add_argument("-r", "--rounds", help = "number of rounds", 
      required=False)
  parser.add_argument("-s", "--seed", help = "seed for PRNG", 
      required=False)
  create_config(parser.parse_args())

我们不必将过多的硬编码、路径、名称和值也打印出来。

 

第二就是让fuzzer能访问单个文件或文件目录:

def get_corpus(path):
  corpus = []
 
  if os.path.isfile(path):
    with open(path, "rb") as fh:
      corpus.append(bytearray(fh.read()))
  elif os.path.isdir(path):
    for file in os.listdir(path):
      if os.path.isfile(file):
        with open(file, "rb") as fh:
          corpus.append(bytearray(fh.read()))
 
  return corpus

这项优化能使fuzzer直接使用整个文件库,能帮助我们实现之后的对覆盖率的优化(这项优化现在是没用的)

 

第三就是对ptrace事件的一些优化:

def execute_fuzz(dbg, data, counter):
  cmd = [config['target'], config['file']]
  pid = debugger.child.createChild(cmd, no_stdout=True, env=None)
  proc = dbg.addProcess(pid, True)
  proc.cont()
 
  event = dbg.waitProcessEvent()
  
  if event.signum == signal.SIGSEGV:
    proc.detach()
    with open("../crashes/crash.{}.jpg".format(counter), "wb+") as fh:
      fh.write(data)
  else:
    proc.detach()

上述这三种优化并没有直接改变fuzzer的功能;接下来我们将对fuzzer本身的功能进行优化

 

随机策略优化

我们的fuzzer的变异策略是依靠随机化来完成的,但是我们对随机化的了解并不多,假如我想复现之前的fuzz的运行结果,如何实现呢? 接下来简单介绍一下Python-random的随机策略。

如果仔细阅读关于random库的文档,不难发现它是基于伪随机数生成器(Pseudo Random Number Generator, PRNG)实现的,如果使用这种生成器生成加密密钥的话,是很不安全的;而且如果我们想获取或者设置生成器的状态的话,就必须将其反序列化后生成的文件放在磁盘上,并且每次加载文件的时候就必须再次反序列化(贼麻烦)。

然而,针对PRNG的特性(对于相同的种子,生成的初始状态也是相同的),有一个更简单的方法

# 修改PRNG的种子
  if config['seed']:
    initial_seed = config['seed']
  else:
    initial_seed = os.urandom(24)
    
  random.seed(initial_seed)
  print("Initial seed: {}".format(base64.b64encode(initial_seed).decode('utf-8')))

当fuzzer启动时就会随机获取24个字节并将其作为种子,这么做的好处就是如果想要恢复状态只需要传递运行时的种子值,此方法的缺点是fuzzer一旦开始就无法暂停(只能等运行结束或者中途kill),但是如果循环时能有反馈信息的话,这个缺点也就不重要了

 

关于crash的优化

上一篇最后运行fuzzer的时候出发了七千多个crash,事实上这七千多个crash是无法进行手工分析的,为了能让手工分析更容易,我们需要优化crash(只记录一次,而不是每次都记录)。我们在execute_fuzz()函数里进行优化,在捕获SIGSEGV信号时,将导致崩溃的数据与指令指针的值关联起来:

 if event.signum == signal.SIGSEGV:
    crash_ip = proc.getInstrPointer()
    if crash_ip not in crashes:
      crashes[crash_ip] = data
    proc.detach()
  else:
    proc.detach()

在测试时这项优化的效果并不明显,因为ASLR的原因,fuzzer依然记录了很多相同的crash,只是触发的地址不同而已。有三种不同的方法可以解决这个问题,第一是禁用整个系统的ASLR(这方法不长久);第二是只禁用进程的ASLR,这种方法是可行的,但是十分困难,需要在创建和设置子进程之前调用Personality(0x88)这个syscall并设置为ADDR_NO_RANDOMIZE,Python的ptrace库并不支持这么做(我不知道别的库支不支持);所以我想尝试第三种方法,获取程序的内存映射,用基址+偏移的方法算出真正的地址(绕过ASLR),

def absolute_address(ip, mappings):
  for mapping in mappings:
    if ip in mapping:
      return ip-mapping.start
 
def execute_fuzz(dbg, data, counter):
  cmd = [config['target'], config['file']]
  pid = debugger.child.createChild(cmd, no_stdout=True, env=None)
  proc = dbg.addProcess(pid, True)
  proc.cont()
 
  event = dbg.waitProcessEvent()
  
  if event.signum == signal.SIGSEGV:
    crash_ip = absolute_address(proc.getInstrPointer(), proc.readMappings())
    if crash_ip not in crashes:
      crashes[crash_ip] = data
    proc.detach()
  else:
    proc.detach()

优化后的运行结果是十万次迭代只产生了8个crash,用binary ninja(或者其他的反汇编工具)加载这些crash就会发现这些偏移行不指向具体的指令,而是指向了.text段。这种方法有一个问题,我们只保留了一个变异后的数据样本,这个样本可以让任何导致seg fault的指令都触发一个crash,这样的变异策略可以得到一些优秀的数据样本(这些样本以不同的方式触发crash)。事实上在调试程序的时候能使用的数据样本很少,所以,如果有一种迭代方法可以触发足够的crash,我们就能准确的判断出这到底是不是bug。

 

下一步要做的事情

可能有很多人期待这篇里能讲到一些很好的优化方法,结果很失望(因为这些优化很辣鸡),但是在本系列的下一篇中我将做一些简单的有关于覆盖率的优化,覆盖率优化完成以后就可以尝试使用遗传算法选择和编译我们的样本,覆盖程序中更多的代码并触发更多新的crash(下一篇中至少要完成这两部分的优化)


译者言

翻译结果为译者阅读后理解的内容,因为译者水平有限,翻译如有不妥还请各位大佬指出

原文链接:

https://carstein.github.io/2020/04/25/writing-simple-fuzzer-2.html


[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

收藏
点赞1
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回