首页
论坛
课程
招聘
[翻译]写一个简单的fuzzer – part 1
2020-5-8 01:16 9283

[翻译]写一个简单的fuzzer – part 1

2020-5-8 01:16
9283

前言

因为新冠病毒的原因我们已经在家待了很久了,趁此机会我们可以在家搞点事情,也有很多人是这么想的,很多人在Twitter上发了文章,即便如此,我还是准备开一个系列,写一个简单的fuzzer,事实上我认为每一个从事信息安全的技术人员都应该写一个自己的fuzzer,倒不是说真的去用自己写的fuzzer挖洞(考虑到已经有人写了很多更高效的fuzzer),而是去了解fuzzer的工作原理和机制;事实上像我这么想的人并不在少数,之前有个叫h0mbre的铁子写了一篇“像老八一样去fuzzing”后来又写了一篇“超越老八的fuzzer”,我在拜读他的fuzzer code的同时还发现了一些改进fuzzer的方法,在开始我自创简易fuzzer之前我强烈建议你们先去看一下h0mbre的文章 :p


说明

首先说明一下我们将用Python写一个最简单的fuzzer,但是这个fuzzer是不可能用于实际的模糊测试的,真正的fuzzer最好要用更接近底层的高级语言来写,比如C/C++和Rust(但是几乎没有人用Python写),这是因为像Python这样的解释性语言执行的时候要比C/C++慢很多。其次,我们会使用h0mbre老铁在他的文章里的目标库文件:exif,这个库文件是多年之前写的并且写的很好,所以并不会一下子爆出很多crash,这导致的结果就是我们会不断地思考是不是我们写的代码不够好,或者是模糊处理的辣鸡,如此思考下来会让我们迅速的进步。


正言

一开始fuzzer是很简单的:只要不断的创建随机数据并喂给程序看它有没有crash,然后不断地变换随机策略再次喂给程序,循环往复…… 所以本质上fuzzer就是不断的做着同样的事情,并且期待着不同的结果(就想孤岛危机里的那个CG一样)。在开始写代码之前我们需要记住一件事情,那就是fuzzer永不崩溃!!!就像海森堡定理、薛定谔的喵还有兽人永不为奴一样!!!

我认为每个fuzzer都至少得有两个组件 – 变异引擎和执行引擎(“像老八一样fuzzing”这篇文章里也提到了这一点):

def main():
  if len(sys.argv) < 2:
    print('Usage: {} <valid_jpg>'.format(sys.argv[0]))
  else:
    filename = sys.argv[1]
    orig_data = get_bytes(filename)
    dbg = debugger.PtraceDebugger()
 
    counter = 0
    while counter < 100000:
      data = orig_data[:]
      mutated_data = mutate(data)
      create_new(mutated_data)
      execute_fuzz(dbg, mutated_data, counter)
 
      if counter % 100 == 0:
        print('Counter: {}\r'.format(counter),file=sys.stderr, end='')
     
      counter += 1 
 
    dbg.quit()

main函数里的代码主要分为两部分:初始化阶段和fuzz循环

在理想情况下每次模糊测试都应该有且只有一次初始化,所以吃性能的操作都可以放在初始化里(必要组件的初始化、读取配置文件等等),事实上我们只读取原始文件样本(稍后会介绍到)并设置调试器(稍后也会介绍到)

Fuzz循环是一段可能会被执行数万甚至数十万次的代码段,因此我们会尽量简化这段代码,在这个例子中,我们的循环做了两件重要的事情 – 修改数据并运行目标程序,代码中可以去除一些多余的东西(比如计数器),但是我们先不考虑这个。

 

变异引擎

变异引擎是我们的fuzzer代码中最重要的组件之一,也是能改进的最多的地方(特别是性能方面的改进);另外就是应该选用一个有效的数据样本开始fuzzing,并对这个数据样本进行修改,如果选择随机数据的话,fuzzer会产生许多垃圾数据,因为前两个字节与期望值不匹配。

在这个例子中,我们先从一个已知有效的JPEG文件开始:canon_40D.jpg(该文件已包含有效的exif数据),我们通过get_bytes()函数读取它,让后将其转换为一个字节数组,并提供给下面的函数:

def mutate(data):
  flips = int((len(data)-4) * FLIP_RATIO)
  flip_indexes = random.choices(range(2, (len(data) - 6)), k=flips)
 
  methods = [0,1]
  
  for idx in flip_indexes:
    method = random.choice(methods)
 
    if method == 0:
      data[idx] = bit_flip(data[idx])
    else:
      magic(data, idx)
 
  return data

前两行代码负责选择要修改文件中多少字节以及哪些字节,如果FLIP_RATIO被设置成了1%,我们就会修改jpg文件80个不同的位置,我们会在这些位置填充任意值,往后在我们不断的优化和改进下我们就会知道哪些值是更好的,这样就可以优化我们的fuzzer,另外jpg的文件格式导致文件头和尾是不可以修改的。

在主循环里可以用两种方法修改文件的值——位翻转(xor)或者直接给一个有效的值,我们首先进行位翻转(因为简单):

def bit_flip(byte):
  return byte ^ random.choice([1, 2, 4, 8, 16, 32, 64, 128])

我们从数组中随机选择一个值并进行异或(xor)操作。

直接给一个有效的值会稍微复杂一些,但简而言之我们会用一个接近整数类型的最大或最小值,这些值很有可能会因为一些没有检查正负号的算术操作而出发bug:

MAGIC_VALS = [
  [0xFF],
  [0x7F],
  [0x00],
  [0xFF, 0xFF], # 0xFFFF
  [0x00, 0x00], # 0x0000
  [0xFF, 0xFF, 0xFF, 0xFF], # 0xFFFFFFFF
  [0x00, 0x00, 0x00, 0x00], # 0x80000000
  [0x00, 0x00, 0x00, 0x80], # 0x80000000
  [0x00, 0x00, 0x00, 0x40], # 0x40000000
  [0xFF, 0xFF, 0xFF, 0x7F], # 0x7FFFFFFF
]
 
def magic(data, idx):
  picked_magic = random.choice(MAGIC_VALS)
 
  offset = 0
  for m in picked_magic:
    data[idx + offset] = m
    offset += 1

有两种方法可以修改这些值,手动拆分为字节(小端序硬编码)或者直接二进制位移(这种方法更智能),目前我决定选择第一种(因为我懒……),事实上变异策略不止这两种(只是现在只实现了两种),并且这两种变异策略的缺点是变异后的文件长度永远不变(这个我们之后会慢慢优化)。

 

程序执行

变异了数据以后就可以开始fuzzing测试程序是否会崩溃了,可以使用像execv()、Popen()和run()这样的函数来测试库文件,然后看是否会有seg fault或者返回地址错误,我不太喜欢这种方法(我更喜欢结构化输出),所以我决定在ptrace下运行程序:

def execute_fuzz(dbg, data, counter):
  cmd = ['exif/exif', 'data/mutated.jpg']
  pid = debugger.child.createChild(cmd, no_stdout=True, env=None)
  proc = dbg.addProcess(pid, True)
  proc.cont()
 
  try:
    sig = dbg.waitSignals()
  except:
    return
  
  if sig.signum == signal.SIGSEGV:
    proc.detach()
    with open("crashes/crash.{}.jpg".format(counter), "wb+") as fh:
      fh.write(data)

这是我第一次使用Python的ptrace,所以可能会报错。Ptrace的基本原理是在调试器下创建启动一个子进程并监视,如果收到了一个SIGSEGV之类的型号,我们就可以用一个唯一的随机名称保存崩溃文件以便进一步分析,如果程序执行完了也没有发出任何信号,那么等待信号只会引发一个异常,我们只需停止运行即可。

 

下一步要做的事情

我们的fuzzer在经过十万次的迭代后得到了7810个crash(只用了6分39秒),但是因为变异策略和覆盖率的问题,这些crash有很多是一样的,本系列的下一篇将介绍如何确保crash的唯一性以及变异策略的优化。

目前为止我们仅仅是靠运气在fuzzing(修改的字节可能是对的也可能不对),对于简单的目标确实可以这么做,但更复杂的目标就不行了,在第三篇中我将尝试做一些基本的代码覆盖率测量,以查看实例文件给出的更改是否能让我们的fuzzer覆盖更多的代码。


译者言

这是我第一次翻译文章,还请各位大佬轻喷

原文链接:https://carstein.github.io/2020/04/18/writing-simple-fuzzer-1.html


[看雪官方培训] Unicorn Trace还原Ollvm算法!《安卓高级研修班》2021年秋季班火热招生!!

最后于 2020-5-8 01:21 被pureGavin编辑 ,原因: 添加超链接
收藏
点赞1
打赏
分享
最新回复 (2)
雪    币: 0
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
wx_Aditi 活跃值 2020-7-5 02:39
2
0
Canon lbp1210 driver for windows 7 64bit. Plz help
雪    币: 7225
活跃值: 活跃值 (6165)
能力值: ( LV12,RANK:210 )
在线值:
发帖
回帖
粉丝
pureGavin 活跃值 2 2020-7-5 10:48
3
0
wx_Aditi Canon lbp1210 driver for windows 7 64bit. Plz help
what's that supposed mean?? you want me to do what??
游客
登录 | 注册 方可回帖
返回