本文最后更新于:星期二, 八月 2日 2022, 9:32 晚上
灰盒变异的模糊测试。
普通fuzzing:从0开始构造测试用例
突变fuzzing:从seed开始构造测试用例
灰盒fuzzing:有引导的突变fuzzing
背景
上回书说到,AFL是突变fuzzer,通过把种子字符串进行些微修改,得到变异体;此外AFL还会将一个种子的前半部分与另一个种子的后半部分连接,形成变异体。
AFL也是个灰盒fuzzer,这是由于AFL需要使用程序内部信息(即覆盖率)。AFL不是白盒,因为AFL没有对程序进行约束求解、程序分析之类的,只是简单获取了一个覆盖率。如果生成的样本能够提升覆盖率,那么就将这个样本添加进种子队列以供下次突变使用(这就意味着突变体有重复突变的可能)。
AFL计算覆盖率的方法,是通过在每个分支的跳转指令后执行一段标记代码。这样就可以做到,监控每个输入导致的激活分支,以及每个分支被激活的大概频率。注入代码这个环节通常在编译时完成。对于Python,可以在未经处理的情况下执行覆盖率信息收集。
突变算法和种子
引入Mutator类。Mutator类是个过程类,包装了对输入inp的突变方法。
class Mutator(object):
def __init__(self):
self.mutators = [
self.delete_random_character,
self.insert_random_character,
self.flip_random_character
def insert_random_character(self,s):
"""Returns s with a random character inserted"""
pos = random.randint(0, len(s))
random_character = chr(random.randrange(32, 127))
return s[:pos] + random_character + s[pos:]
def delete_random_character(self,s):
"""Returns s with a random character deleted"""
if s == "":
return self.insert_random_character(s)
pos = random.randint(0, len(s) - 1)
return s[:pos] + s[pos + 1:]
def flip_random_character(self,s):
"""Returns s with a random bit flipped in a random position"""
if s == "":
return self.insert_random_character(s)
pos = random.randint(0, len(s) - 1)
c = s[pos]
bit = 1 << random.randint(0, 6)
new_c = chr(ord(c) ^ bit)
return s[:pos] + new_c + s[pos + 1:]
def mutate(self, inp):
"""Return s with a random mutation applied"""
mutator = random.choice(self.mutators)
return mutator(inp)
使用Mutator是只需实例化Mutator,然后调用mutate()方法即可。
Mutator().mutate("good")
'cood'
精力分配(Power Schedule)
模糊测试是一种执行很慢的测试方法。既然并不是每个测试用例种子都值得分配同样的精力,那么试图发现那些更令人感兴趣的种子就是理所当然的选择了。
我们把一个种子从种群中被选中的可能性称为种子的能量(energy)。我们希望优先突变和执行那些更有希望发现待测程序错误的种子,不希望在无进步的种子身上浪费精力。
决定种子能量分配的算法称为“功率表”(Power Schedule)。AFL的功率表会将更多的能量分配给那些长度较短、执行速度较快、覆盖率增加较多的种子。
由此,每个种子需要额外维护其能量。构建Seed类如下:
class Seed(object):
def __init__(self, data):
"""Set seed data"""
self.data = data
def __str__(self):
"""Returns data as string representation of the seed"""
return self.data
__repr__ = __str__
下面是功率表PowerSchedule类的定义:
class PowerSchedule(object):
def assignEnergy(self, population):
"""Assigns each seed the same energy"""
for seed in population:
seed.energy = 1
def normalizedEnergy(self, population):
"""Normalize energy"""
energy = list(map(lambda seed: seed.energy, population))
sum_energy = sum(energy) # Add up all values in energy
norm_energy = list(map(lambda nrg: nrg/sum_energy, energy))
return norm_energy
def choose(self, population):
"""Choose weighted by normalized energy."""
import numpy as np
self.assignEnergy(population)
norm_energy = self.normalizedEnergy(population)
seed = np.random.choice(population, p=norm_energy)
return seed
灰盒fuzzing与黑盒fuzzing的比较
首先定义不使用coverage的黑盒fuzzer,MutationFuzzer 类:
class MutationFuzzer(Fuzzer):
def __init__(self, seeds, mutator, schedule):
self.seeds = seeds
self.mutator = mutator
self.schedule = schedule
self.inputs = []
self.reset()
def reset(self):
"""Reset the initial population and seed index"""
self.population = list(map(lambda x: Seed(x), self.seeds))
self.seed_index = 0
def create_candidate(self):
"""Returns an input generated by fuzzing a seed in the population"""
seed = self.schedule.choose(self.population)
# Stacking: Apply multiple mutations to generate the candidate
candidate = seed.data
trials = min(len(candidate), 1 << random.randint(1,5))
for i in range(trials):
candidate = self.mutator.mutate(candidate)
return candidate
def fuzz(self):
"""Returns first each seed once and then generates new inputs"""
if self.seed_index < len(self.seeds):
# Still seeding
self.inp = self.seeds[self.seed_index]
self.seed_index += 1
else:
# Mutating
self.inp = self.create_candidate()
self.inputs.append(self.inp)
return self.inp
MutationFuzzer 是由一组初始种子、一个突变器和一个功率表构成的。在整个模糊化过程中,它维护着一个名为population的种子语料库。create_candidate对某个种子执行多次突变,fuzz先试图返回正常种子,随后返回突变种子。
population_coverage 是预先定义好的覆盖率计算库,返回(all_coverage,cumulative_coverage)。其中all_coverage是所有输入所覆盖的语句集,cumulative_coverage是随着执行输入数量的增加而覆盖的语句数量。
下面是GrayBox fuzzing的实现:
class GreyboxFuzzer(MutationFuzzer):
def reset(self):
"""Reset the initial population, seed index, coverage information"""
super().reset()
self.coverages_seen = set()
self.population = [] # population is filled during greybox fuzzing
def run(self, runner):
"""Run function(inp) while tracking coverage.
If we reach new coverage,
add inp to population and its coverage to population_coverage
"""
result, outcome = super().run(runner)
new_coverage = frozenset(runner.coverage())
if new_coverage not in self.coverages_seen:
# We have new coverage
seed = Seed(self.inp)
seed.coverage = runner.coverage()
self.coverages_seen.add(new_coverage)
self.population.append(seed)
return (result, outcome)
经过计算,分别得到覆盖率变化趋势blackbox_coverage和greybox_coverage,可视化如下:
可以看到,灰盒fuzzing的覆盖率增长明显比黑盒要好。
增强后的灰盒fuzzer
通过修改功率表PowerSchedule,使那些能激活不寻常的path的input具有更高的energy。不寻常的path指的是激活次数比较小。
有多种方法计算一个种子的能量。上述的要求形式化为具体定义即为
其中$s$是种子
$p(s)$为$s$激活的path
$f(p)$返回path激活的次数
$a$是给定的超参数
$e(s)$是种子$s$被分配的能量
下面是按照此思想设置的PowerSchedule:
class AFLFastSchedule(PowerSchedule):
def __init__(self, exponent):
self.exponent = exponent
def assignEnergy(self, population):
"""Assign exponential energy inversely proportional to path frequency"""
for seed in population:
seed.energy = 1 / (self.path_frequency[getPathID(seed.coverage)] ** self.exponent)
改进的灰盒Fuzzer:
class CountingGreyboxFuzzer(GreyboxFuzzer):
def reset(self):
"""Reset path frequency"""
super().reset()
self.schedule.path_frequency = {}
def run(self, runner):
"""Inform scheduler about path frequency"""
result, outcome = super().run(runner)
path_id = getPathID(runner.coverage())
if not path_id in self.schedule.path_frequency:
self.schedule.path_frequency[path_id] = 1
else:
self.schedule.path_frequency[path_id] += 1
return(result, outcome)
覆盖率变化如图所示
总结
三篇Fuzzing文章到此为止。
Fuzzing 的方法是通过大量生成input,来找出被测程序的错误的方法。
Fuzzing的关键点之一在于input生成方法,其二在于input的排序方法,其三在于软件内部信息的获取和应用。
如果input完全是自己构建的,那么这种方法称之为generational fuzzing
如果input是通过原始种子略微修改后得到的,那么这种fuzzing为Mutational fuzzing。
如果Mutator可以经过一定的程序信息的引导,那么这叫做GrayBox Fuzzing,比如覆盖率引导的模糊测试
如果Seed经过Power Schedule的精力分配,随后Mutator根据Seed的精力大小排序,那么这种方法称之为Boosted GrayBox Fuzzing
使用到的类:
Runner:待测程序的基类
Fuzzer:模糊测试器的基类
Seed:测试用例种子的基类
PowerSchedule:功率表的基类
本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!