由于之后的章节中我们会常常用到第二部分所提到的Fuzzer与Runner思路结构,因此创建一种易于重复使用、后期易于拓展的Fuzz框架结构就显得十分重要。为此我们引入Python中类的概念,来逐步封装之前所提到的功能,为后面的章节做准备。
创建Runner的类:
我么首先需要介绍的是Runner的概念:使用给定的输入来执行某些特定的程序,特定的程序通常是指要接受测试的某些程序或函数。
Runner本质上提供了一种run(input)方法:用于将input(字符串)传递给程序运行。run()会返回一对值(result,outcome),这里的result是run运行后结果的返回值,提供了run运行的细节供我们参考;outcome是将结果分为三类值:
Runner.PASS
—测试通过。运行产生正确的结果。
Runner.FAIL
—测试失败。运行产生不正确的结果。
Runner.UNRESOLVED
—测试既没有通过也没有失败。如果无法进行运行(例如输入无效),则会发生这种情况。
class Runner(object): # Test outcomes PASS = "PASS" FAIL = "FAIL" UNRESOLVED = "UNRESOLVED" def __init__(self): """Initialize""" pass def run(self, inp): """Run the runner with the given input""" return (inp, Runner.UNRESOLVED)
这里的Runner类是后续的一个基类,仅仅是一个基础的框架。后续要根据自己的需求继承该基类并用额外的函数重写。
这里举一个简单的例子:PrintRunner会打印出所有传递给自身的值,它就是基于Runner继承并修改的:
class PrintRunner(Runner): def run(self, inp): """Print the given input""" print(inp) return (inp, Runner.UNRESOLVED)
p = PrintRunner() (result, outcome) = p.run("Some input")
结果就是我们作为输入传递的字符串:
result:
'Some input'
但是到目前为止,我们仍无法对程序行为进行分类:
outcome:
'UNRESOLVED'
下面的ProgramRunner
类将输入传送到程序,并根据运行结果分类:
class ProgramRunner(Runner): def __init__(self, program): """Initialize. `program` is a program spec as passed to `subprocess.run()`""" self.program = program def run_process(self, inp=""): """Run the program with `inp` as input. Return result of `subprocess.run()`.""" return subprocess.run(self.program, input=inp, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True) def run(self, inp=""): """Run the program with `inp` as input. Return test outcome based on result of `subprocess.run()`.""" result = self.run_process(inp) if result.returncode == 0: outcome = self.PASS elif result.returncode < 0: outcome = self.FAIL else: outcome = self.UNRESOLVED return (result, outcome)
如果是针对二进制程序文件,可能还要小改一下:
class BinaryProgramRunner(ProgramRunner): def run_process(self, inp=""): """Run the program with `inp` as input. Return result of `subprocess.run()`.""" return subprocess.run(self.program, input=inp.encode(), stdout=subprocess.PIPE, stderr=subprocess.PIPE)
让我们演示一个ProgramRunner
使用cat
程序的实际例子:cat程序将其输入复制到其输出。
我们看到标准的调用cat
简单地完成了这项工作,其输出cat
与其输入相同:
cat = ProgramRunner(program="cat")
cat.run("hello")
输出:
(CompletedProcess(args='cat', returncode=0, stdout='hello', stderr=''), 'PASS')
是不是封装后使用起来更方便了。
创建Fuzzer的类:
现在我们来一起定义一个fuzzer类,fuzzer的主要作用其实就是生成数据并送至runner.
Fuzzer的基类提供了一些创建输入的核心方法,随后run()将这些数据传送至Runner并返回结果;runs()可以设定发送的次数(trials)
class Fuzzer(object): def __init__(self): pass def fuzz(self): """Return fuzz input""" return "" def run(self, runner=Runner()): """Run `runner` with fuzz input""" return runner.run(self.fuzz()) def runs(self, runner=PrintRunner(), trials=10): """Run `runner` with fuzz input, `trials` times""" # Note: the list comprehension below does not invoke self.run() for subclasses # return [self.run(runner) for i in range(trials)] outcomes = [] for i in range(trials): outcomes.append(self.run(runner)) return outcomes
默认情况下,Fuzzer
对象不执行任何操作。但在RandomFuzzer
中,子类实现了上述功能的fuzzer()
功能,并增加了一个附加参数min_length
以指定生成数据的最小长度。
class RandomFuzzer(Fuzzer): def __init__(self, min_length=10, max_length=100, char_start=32, char_range=32): """Produce strings of `min_length` to `max_length` characters in the range [`char_start`, `char_start` + `char_range`]""" self.min_length = min_length self.max_length = max_length self.char_start = char_start self.char_range = char_range def fuzz(self): string_length = random.randrange(self.min_length, self.max_length + 1) out = "" for i in range(0, string_length): out += chr(random.randrange(self.char_start, self.char_start + self.char_range)) return out
使用RandomFuzzer
,我们现在可以创建一个模糊器,在创建模糊器时只需配置一次即可。
random_fuzzer = RandomFuzzer(min_length=20, max_length=20) for i in range(10): print(random_fuzzer.fuzz())
'>23>33)(&"09.377.*3 *+:5 ? (?1$4<>!?3>.' 4+3/(3 (0%!>!(+9%,#$ /51$2964>;)2417<9"2& 907.. !7:&--"=$7',7* (5=5'.!*+&>")6%9)=,/ ?:&5) ";.0!=6>3+>)=, 6&,?:!#2))- ?:)=63'- ,)9#839%)?&(0<6("*;) 4?!(49+8=-'&499%?< '
下面我们仍然以cat应用程序为例,将这样生成的输入发送到我们先前定义的cat,验证是否
cat确实确实将其(模糊的)输入复制到其输出中。
for i in range(10): inp = random_fuzzer.fuzz() result, outcome = cat.run(inp) assert result.stdout == inp assert outcome == Runner.PASS
最后,将Fuzzer与Runner结合十分的普遍
random_fuzzer.run(cat)
(CompletedProcess(args='cat', returncode=0, stdout='?:+= % <1<6$:(>=:9)5', stderr=''),
'PASS')
使用runs()
,我们可以重复执行模糊测试多次,以获得结果列表。
random_fuzzer.runs(cat, 10)
[(CompletedProcess(args='cat', returncode=0, stdout='3976%%&+%6=(1)3&3:<9', stderr=''),
'PASS'),
(CompletedProcess(args='cat', returncode=0, stdout='33$#42$ 11=*%$20=<.-', stderr=''),
'PASS'),
(CompletedProcess(args='cat', returncode=0, stdout='"?<\'#8 </:*%9.--\'97!', stderr=''),
'PASS'),
(CompletedProcess(args='cat', returncode=0, stdout="/0-#(03/!#60'+6>&&72", stderr=''),
'PASS'),
(CompletedProcess(args='cat', returncode=0, stdout="=,+:,6'5:950+><3(*()", stderr=''),
'PASS'),
(CompletedProcess(args='cat', returncode=0, stdout=" 379+0?'%3137=2:4605", stderr=''),
'PASS'),
(CompletedProcess(args='cat', returncode=0, stdout="02>!$</'*81.#</22>+:", stderr=''),
'PASS'),
(CompletedProcess(args='cat', returncode=0, stdout="=-<'3-#88*%&*9< +1&&", stderr=''),
'PASS'),
(CompletedProcess(args='cat', returncode=0, stdout='2;;0=3&6=8&30&<-;?*;', stderr=''),
'PASS'),
(CompletedProcess(args='cat', returncode=0, stdout='/#05=*3($>::#7!0=12+', stderr=''),
'PASS')]
有了这些类,我们就可以创建更多更复杂的模糊器了。