diff --git a/new/fuzzing-book-zh/1.md b/new/fuzzing-book-zh/1.md new file mode 100644 index 0000000000000000000000000000000000000000..99225c40938b6fbbe9d225bec3b58d1481a8c8b1 --- /dev/null +++ b/new/fuzzing-book-zh/1.md @@ -0,0 +1,3 @@ +# 生成软件测试 + +来源: [https://www.fuzzingbook.org/html/00_Table_of_Contents.html](https://www.fuzzingbook.org/html/00_Table_of_Contents.html) \ No newline at end of file diff --git a/new/fuzzing-book-zh/10.md b/new/fuzzing-book-zh/10.md new file mode 100644 index 0000000000000000000000000000000000000000..e1a7f948bc9834e43474627407c274507a7d3cd4 --- /dev/null +++ b/new/fuzzing-book-zh/10.md @@ -0,0 +1,1210 @@ +# 基于变异的模糊化 + +> 原文: [https://www.fuzzingbook.org/html/MutationFuzzer.html](https://www.fuzzingbook.org/html/MutationFuzzer.html) + +大多数[随机生成的输入](Fuzzer.html)在语法上都是*无效*,因此很快就被处理程序拒绝。 要行使输入处理之外的功能,我们必须增加获得有效输入的机会。 一种这样的方法就是所谓的*突变模糊测试*-也就是说,对现有输入进行小的更改,这些更改可能仍会保持输入有效,但会行使新的行为。 我们将展示如何创建此类突变,以及如何使用流行的AFL模糊器的中心概念来引导它们走向尚未发现的代码。 + +**前提条件** + +* 您应该知道基本的模糊测试是如何工作的。 例如,在[“模糊处理”](Fuzzer.html) 一章中。 + +## 内容提要 + +要使用本章中提供的代码来[,请编写](Importing.html) + +```py +>>> from [fuzzingbook.MutationFuzzer](MutationFuzzer.html) import + +``` + +然后利用以下功能。 + +本章介绍`MutationFuzzer`类,该类采用*种子输入*的列表,然后将其突变: + +```py +>>> seed_input = "http://www.google.com/search?q=fuzzing" +>>> mutation_fuzzer = MutationFuzzer(seed=[seed_input]) +>>> [mutation_fuzzer.fuzz() for i in range(10)] +['http://www.google.com/search?q=fuzzing', + 'http://www.g=oNogl.om/search?q=fuzzing/', + 'RttpX://w)ww.goo(gle.comq/sarc(q=fuzzng', + 'hdt8p://"wWw.goole.com/seDarb`*?q=fuzzing', + 'httop://www.CooglGe.om/s$arch?q=fuzzingY', + 'http://wwlw.google.c"om/secrch?yq=fuzzin', + 'hup://www.google.comC/search?q=fuzzing', + 'http://w7w.google.com/search?q=ufuzgzing', + 'http://www,google.com/sear4ch?.q=fuzzing', + 'http://w&ww.google.cKom/search7q=fuzzing'] + +``` + +`MutationCoverageFuzzer`保持输入的*种群*,然后对其进行演化以最大化覆盖范围。 + +```py +>>> mutation_fuzzer = MutationCoverageFuzzer(seed=[seed_input]) +>>> mutation_fuzzer.runs(http_runner, trials=10000) +>>> mutation_fuzzer.population[:5] +['http://www.google.com/search?q=fuzzing', + 'htTp://www.googld.cqom/searchq=fuzzIng', + 'htTp://www.gloogld.qom/|searchq=fuzzng', + 'htTp://www.googld.cqomo0searchq=fuzzIng', + 'htTp://www*goegld.cqoe/sa7#hq=fuzIng'] + +``` + +## 带有突变的 + +2013年11月,发布了 [American Fuzzy Lop](http://lcamtuf.coredump.cx/afl/) (AFL)的第一版。 从那时起,AFL已成为最成功的模糊测试工具之一,并具有许多特色,例如 [AFLFast](https://github.com/mboehme/aflfast) , [AFLGo](https://github.com/aflgo/aflgo) 和 [AFLSmart](https://github.com/aflsmart/aflsmart) (在 这本书)。 AFL使模糊测试成为自动漏洞检测的流行选择。 它是第一个证明可以在许多对安全性要求很高的实际应用程序中大规模自动检测漏洞的工具。 + +![American Fuzzy Lop Command Line User Interface](img/9b2f0f8d40bfc500d25dd91bdf3f77d3.jpg) + +
**Figure 1.** American Fuzzy Lop Command Line User Interface
+ +在本章中,我们将介绍突变模糊测试的基础。 接下来的下一章将进一步说明如何将模糊测试引导到特定的代码目标。 + +## 模糊化URL解析器 + +许多程序期望它们的输入以非常特定的格式输入,然后才能实际处理它们。 例如,考虑一个接受URL(Web地址)的程序。 该URL必须采用有效格式(即URL格式),以便程序能够对其进行处理。 当使用随机输入进行模糊测试时,我们实际产生有效URL的机会是多少? + +为了更深入地研究这个问题,让我们探索一下URL的组成。 URL由许多元素组成: + +```py +scheme://netloc/path?query#fragment +``` + +哪里 + +* `scheme`是要使用的协议,包括`http`,`https`,`ftp`,`file` ... +* `netloc`是要连接的主机的名称,例如`www.google.com` +* `path`是该主机上的路径,例如`search` +* `query`是键/值对的列表,例如`q=fuzzing` +* `fragment`是所检索文档中某个位置的标记,例如`#result` + +在Python中,我们可以使用`urlparse()`函数来解析URL并将其分解为各个部分。 + +```py +import [fuzzingbook_utils](https://github.com/uds-se/fuzzingbook/tree/master/notebooks/fuzzingbook_utils) + +``` + +```py +try: + from [urlparse](https://docs.python.org/3/library/urlparse.html) import urlparse # Python 2 +except ImportError: + from [urllib.parse](https://docs.python.org/3/library/urllib.parse.html) import urlparse # Python 3 + +urlparse("http://www.google.com/search?q=fuzzing") + +``` + +```py +ParseResult(scheme='http', netloc='www.google.com', path='/search', params='', query='q=fuzzing', fragment='') + +``` + +我们将看到结果如何将URL的各个部分编码为不同的属性。 + +现在让我们假设我们有一个使用URL作为输入的程序。 为简化起见,我们不会让它做太多事情。 我们只是让它检查传递的URL的有效性。 如果URL有效,则返回True;否则返回True。 否则,将引发异常。 + +```py +def http_program(url): + supported_schemes = ["http", "https"] + result = urlparse(url) + if result.scheme not in supported_schemes: + raise ValueError("Scheme must be one of " + repr(supported_schemes)) + if result.netloc == '': + raise ValueError("Host must be non-empty") + + # Do something with the URL + return True + +``` + +现在让我们开始模糊`http_program()`。 为了模糊起见,我们使用了所有可打印的ASCII字符,包括`:`,`/`和小写字母。 + +```py +from [Fuzzer](Fuzzer.html) import fuzzer + +``` + +```py +fuzzer(char_start=32, char_range=96) + +``` + +```py +'"N&+slk%h\x7fyp5o\'@[3(rW*M5W]tMFPU4\\P@tz%[X?uo\\1?b4T;1bDeYtHx #UJ5w}pMmPodJM,_' + +``` + +让我们尝试对1000个随机输入进行模糊测试,看看我们是否取得了一些成功。 + +```py +for i in range(1000): + try: + url = fuzzer() + result = http_program(url) + print("Success!") + except ValueError: + pass + +``` + +实际获得有效URL的机会是什么? 我们需要以`"http://"`或`"https://"`开头的字符串。 让我们先来看`"http://"`案例。 这是我们需要开始的七个非常具体的字符。 随机产生这七个字符(字符范围为96个不同字符)的机会为$ 1:96 ^ 7 $,或者 + +```py +96 ** 7 + +``` + +```py +75144747810816 + +``` + +产生`"https://"`前缀的几率甚至更糟,为$ 1:96 ^ 8 $: + +```py +96 ** 8 + +``` + +```py +7213895789838336 + +``` + +这给了我们完全的机会 + +```py +likelihood = 1 / (96 ** 7) + 1 / (96 ** 8) +likelihood + +``` + +```py +1.344627131107667e-14 + +``` + +这是我们产生有效的URL方案所需的运行次数(平均): + +```py +1 / likelihood + +``` + +```py +74370059689055.02 + +``` + +让我们测量一下`http_program()`的运行时间: + +```py +from [Timer](Timer.html) import Timer + +``` + +```py +trials = 1000 +with Timer() as t: + for i in range(trials): + try: + url = fuzzer() + result = http_program(url) + print("Success!") + except ValueError: + pass + +duration_per_run_in_seconds = t.elapsed_time() / trials +duration_per_run_in_seconds + +``` + +```py +5.87425309995524e-05 + +``` + +那非常快,不是吗? 不幸的是,我们有很多需要解决的问题。 + +```py +seconds_until_success = duration_per_run_in_seconds * (1 / likelihood) +seconds_until_success + +``` + +```py +4368685536.722877 + +``` + +转化为 + +```py +hours_until_success = seconds_until_success / 3600 +days_until_success = hours_until_success / 24 +years_until_success = days_until_success / 365.25 +years_until_success + +``` + +```py +138.4352909195527 + +``` + +即使我们并行处理很多事情,我们仍需要等待数月至数年。 这是为了使*成功运行一次*,这将使`http_program()`更加深入。 + +基本的模糊测试会很好地进行测试`urlparse()`,如果此解析函数中有错误,则很有可能将其发现。 但是,只要我们无法产生有效的输入,我们就无法达到任何更深层次的功能。 + +## 突变输入 + +从头开始生成随机字符串的替代方法是从给定的*有效*输入开始,然后随后*对其进行*变异。 在此上下文中,*突变*是一种简单的字符串操作-例如,插入(随机)字符,删除字符或在字符表示中翻转一位。 这被称为*突变模糊测试*-与之前讨论的*世代模糊测试*技术相反。 + +以下是一些可以帮助您入门的变体: + +```py +import [random](https://docs.python.org/3/library/random.html) + +``` + +```py +def delete_random_character(s): + """Returns s with a random character deleted""" + if s == "": + return s + + pos = random.randint(0, len(s) - 1) + # print("Deleting", repr(s[pos]), "at", pos) + return s[:pos] + s[pos + 1:] + +``` + +```py +seed_input = "A quick brown fox" +for i in range(10): + x = delete_random_character(seed_input) + print(repr(x)) + +``` + +```py +'A uick brown fox' +'A quic brown fox' +'A quick brown fo' +'A quic brown fox' +'A quick bown fox' +'A quick bown fox' +'A quick brown fx' +'A quick brown ox' +'A quick brow fox' +'A quic brown fox' + +``` + +```py +def insert_random_character(s): + """Returns s with a random character inserted""" + pos = random.randint(0, len(s)) + random_character = chr(random.randrange(32, 127)) + # print("Inserting", repr(random_character), "at", pos) + return s[:pos] + random_character + s[pos:] + +``` + +```py +for i in range(10): + print(repr(insert_random_character(seed_input))) + +``` + +```py +'A quick brvown fox' +'A quwick brown fox' +'A qBuick brown fox' +'A quick broSwn fox' +'A quick brown fvox' +'A quick brown 3fox' +'A quick brNown fox' +'A quick brow4n fox' +'A quick brown fox8' +'A equick brown fox' + +``` + +```py +def flip_random_character(s): + """Returns s with a random bit flipped in a random position""" + if s == "": + return s + + pos = random.randint(0, len(s) - 1) + c = s[pos] + bit = 1 << random.randint(0, 6) + new_c = chr(ord(c) ^ bit) + # print("Flipping", bit, "in", repr(c) + ", giving", repr(new_c)) + return s[:pos] + new_c + s[pos + 1:] + +``` + +```py +for i in range(10): + print(repr(flip_random_character(seed_input))) + +``` + +```py +'A quick bRown fox' +'A quici brown fox' +'A"quick brown fox' +'A quick brown$fox' +'A quick bpown fox' +'A quick brown!fox' +'A 1uick brown fox' +'@ quick brown fox' +'A quic+ brown fox' +'A quick bsown fox' + +``` + +现在让我们创建一个随机变异器,它随机选择要应用的变异: + +```py +def mutate(s): + """Return s with a random mutation applied""" + mutators = [ + delete_random_character, + insert_random_character, + flip_random_character + ] + mutator = random.choice(mutators) + # print(mutator) + return mutator(s) + +``` + +```py +for i in range(10): + print(repr(mutate("A quick brown fox"))) + +``` + +```py +'A qzuick brown fox' +' quick brown fox' +'A quick Brown fox' +'A qMuick brown fox' +'A qu_ick brown fox' +'A quick bXrown fox' +'A quick brown fx' +'A quick!brown fox' +'A! quick brown fox' +'A quick brownfox' + +``` + +现在的想法是*如果*我们有一些有效的输入作为开始,那么我们可以通过应用上述突变之一来创建更多输入候选。 要了解其工作原理,让我们回到URL。 + +## 突变网址 + +现在让我们回到我们的URL解析问题。 让我们创建一个函数`is_valid_url()`,该函数检查`http_program()`是否接受输入。 + +```py +def is_valid_url(url): + try: + result = http_program(url) + return True + except ValueError: + return False + +``` + +```py +assert is_valid_url("http://www.google.com/search?q=fuzzing") +assert not is_valid_url("xyzzy") + +``` + +现在让我们在给定的URL上应用`mutate()`函数,并查看我们获得了多少有效输入。 + +```py +seed_input = "http://www.google.com/search?q=fuzzing" +valid_inputs = set() +trials = 20 + +for i in range(trials): + inp = mutate(seed_input) + if is_valid_url(inp): + valid_inputs.add(inp) + +``` + +现在我们可以观察到,通过*突变*原始输入,我们得到了很大一部分有效输入: + +```py +len(valid_inputs) / trials + +``` + +```py +0.8 + +``` + +通过突变`http:`样本种子输入来产生`https:`前缀的几率是多少? 我们必须在正确的位置($ 1:l $)中插入($ 1:3 $)右字符`'s'`($ 1:96 $),其中$ l $是种子输入的长度。 这意味着平均而言,我们需要进行多次运行: + +```py +trials = 3 * 96 * len(seed_input) +trials + +``` + +```py +10944 + +``` + +我们实际上可以负担得起。 我们试试吧: + +```py +from [Timer](Timer.html) import Timer + +``` + +```py +trials = 0 +with Timer() as t: + while True: + trials += 1 + inp = mutate(seed_input) + if inp.startswith("https://"): + print( + "Success after", + trials, + "trials in", + t.elapsed_time(), + "seconds") + break + +``` + +```py +Success after 3656 trials in 0.010670263999600138 seconds + +``` + +当然,如果我们想获得一个`"ftp://"`前缀,我们将需要更多的突变和更多的运行–但是,最重要的是,我们需要应用*多个*突变。 + +## 多个突变 + +到目前为止,我们仅对示例字符串应用了一个突变。 但是,我们也可以应用*多个*突变,以进一步对其进行更改。 例如,如果我们对样本字符串应用20个突变,会发生什么情况? + +```py +seed_input = "http://www.google.com/search?q=fuzzing" +mutations = 50 + +``` + +```py +inp = seed_input +for i in range(mutations): + if i % 5 == 0: + print(i, "mutations:", repr(inp)) + inp = mutate(inp) + +``` + +```py +0 mutations: 'http://www.google.com/search?q=fuzzing' +5 mutations: 'http:/L/www.googlej.com/seaRchq=fuz:ing' +10 mutations: 'http:/L/www.ggoWglej.com/seaRchqfu:in' +15 mutations: 'http:/L/wwggoWglej.com/seaR3hqf,u:in' +20 mutations: 'htt://wwggoVgle"j.som/seaR3hqf,u:in' +25 mutations: 'htt://fwggoVgle"j.som/eaRd3hqf,u^:in' +30 mutations: 'htv://>fwggoVgle"j.qom/ea0Rd3hqf,u^:i' +35 mutations: 'htv://>fwggozVle"Bj.qom/eapRd[3hqf,u^:i' +40 mutations: 'htv://>fwgeo6zTle"Bj.\'qom/eapRd[3hqf,tu^:i' +45 mutations: 'htv://>fwgeo]6zTle"BjM.\'qom/eaR[3hqf,tu^:i' + +``` + +如您所见,原始种子输入几乎无法识别。 通过一次又一次地改变输入,我们得到了更高的输入多样性。 + +为了在单个程序包中实现这样的多个突变,让我们介绍一个`MutationFuzzer`类。 它需要种子(字符串列表)以及最小和最大数量的突变。 + +```py +from [Fuzzer](Fuzzer.html) import Fuzzer + +``` + +```py +class MutationFuzzer(Fuzzer): + def __init__(self, seed, min_mutations=2, max_mutations=10): + self.seed = seed + self.min_mutations = min_mutations + self.max_mutations = max_mutations + self.reset() + + def reset(self): + self.population = self.seed + self.seed_index = 0 + +``` + +接下来,让我们通过添加更多方法来进一步开发`MutationFuzzer`。 Python语言要求我们使用所有方法将整个方法定义为单个连续单元; 但是,我们想介绍一种方法。 为了避免这个问题,我们使用了一种特殊的技巧:每当我们想向某个类`C`引入新方法时,我们都会使用 + +```py +class C(C): + def new_method(self, args): + pass + +``` + +这似乎将`C`定义为其自身的子类,这没有任何意义-但实际上,它引入了一个新的`C`类作为*旧* `C`类的子类,然后进行了阴影处理 旧的`C`定义。 这使我们得到的是`C`类,并以`new_method()`作为方法,这正是我们想要的。 (不过,较早定义的`C`对象将保留较早的`C`定义,因此必须重新构建。) + +使用此技巧,我们现在可以添加一个`mutate()`方法,该方法实际调用上述`mutate()`函数。 当我们以后要扩展`MutationFuzzer`时,将`mutate()`作为方法很有用。 + +```py +class MutationFuzzer(MutationFuzzer): + def mutate(self, inp): + return mutate(inp) + +``` + +让我们回到我们的策略,在我们的人群中最大化覆盖率。 首先,让我们创建一个方法`create_candidate()`,该方法从当前种群(`self.population`)中随机选择一些输入,然后在`min_mutations`和`max_mutations`突变步骤之间应用,返回最终结果: + +```py +class MutationFuzzer(MutationFuzzer): + def create_candidate(self): + candidate = random.choice(self.population) + trials = random.randint(self.min_mutations, self.max_mutations) + for i in range(trials): + candidate = self.mutate(candidate) + return candidate + +``` + +`fuzz()`方法设置为首先采摘种子; 当这些消失后,我们进行变异: + +```py +class MutationFuzzer(MutationFuzzer): + def fuzz(self): + if self.seed_index < len(self.seed): + # Still seeding + self.inp = self.seed[self.seed_index] + self.seed_index += 1 + else: + # Mutating + self.inp = self.create_candidate() + return self.inp + +``` + +```py +seed_input = "http://www.google.com/search?q=fuzzing" +mutation_fuzzer = MutationFuzzer(seed=[seed_input]) +mutation_fuzzer.fuzz() + +``` + +```py +'http://www.google.com/search?q=fuzzing' + +``` + +```py +mutation_fuzzer.fuzz() + +``` + +```py +'http://www.gogl9ecom/earch?qfuzzing' + +``` + +```py +mutation_fuzzer.fuzz() + +``` + +```py +'htotq:/www.googleom/yseach?q=fzzijg' + +``` + +每次`fuzz()`的新调用,我们都会得到另一个应用了多个突变的变体。 但是,输入的多样性较高,会增加输入无效的风险。 成功的关键在于*指导*这些突变的想法,也就是说*保留那些特别有价值的突变。* + +## 覆盖率指导 + +为了涵盖尽可能多的功能,可以依赖*指定的*或*实现的*功能,如[“覆盖”](Coverage.html) 一章中所述。 现在,我们不会假设程序行为是规范的(尽管*肯定是*会很好!)。 但是,我们*将假定*存在,并且我们可以利用其结构来指导测试的产生。 + +由于测试总是执行手头的程序,因此人们总是可以收集有关其执行的信息-最少是决定测试是否通过所需的信息。 由于也经常测量覆盖率以确定测试质量,因此我们还假设我们可以检索测试运行的覆盖率。 问题是:*我们如何利用覆盖率来指导测试的产生?* + +一种特别成功的想法是在名为 [American Fuzzy lop](http://lcamtuf.coredump.cx/afl/) 或简称为 *AFL* 的流行模糊器中实现的。 就像上面的示例一样,AFL会开发成功的测试用例-但是对于AFL,``成功''表示*通过程序执行*找到了新的路径。 这样,AFL可以继续变异到目前为止已经找到新路径的输入。 如果输入找到另一条路径,则也会保留该路径。 + +让我们制定这样的策略。 我们首先介绍一个`Runner`类,该类捕获给定功能的覆盖范围。 首先,一个`FunctionRunner`类: + +```py +from [Fuzzer](Fuzzer.html) import Runner + +``` + +```py +class FunctionRunner(Runner): + def __init__(self, function): + """Initialize. `function` is a function to be executed""" + self.function = function + + def run_function(self, inp): + return self.function(inp) + + def run(self, inp): + try: + result = self.run_function(inp) + outcome = self.PASS + except Exception: + result = None + outcome = self.FAIL + + return result, outcome + +``` + +```py +http_runner = FunctionRunner(http_program) +http_runner.run("https://foo.bar/") + +``` + +```py +(True, 'PASS') + +``` + +现在,我们可以扩展`FunctionRunner`类,以便它也可以测量覆盖范围。 调用`run()`之后,`coverage()`方法返回上次运行时获得的覆盖率。 + +```py +from [Coverage](Coverage.html) import Coverage, population_coverage + +``` + +```py +class FunctionCoverageRunner(FunctionRunner): + def run_function(self, inp): + with Coverage() as cov: + try: + result = super().run_function(inp) + except Exception as exc: + self._coverage = cov.coverage() + raise exc + + self._coverage = cov.coverage() + return result + + def coverage(self): + return self._coverage + +``` + +```py +http_runner = FunctionCoverageRunner(http_program) +http_runner.run("https://foo.bar/") + +``` + +```py +(True, 'PASS') + +``` + +以下是前五个位置: + +```py +print(list(http_runner.coverage())[:5]) + +``` + +```py +[('urlparse', 375), ('__exit__', 25), ('_coerce_args', 115), ('http_program', 10), ('http_program', 3)] + +``` + +现在是主要班级。 我们维持人口和已经实现的一系列覆盖率(`coverages_seen`)。 `fuzz()`辅助函数接受输入并在其上运行给定的`function()`。 如果其覆盖范围是新的(即`coverages_seen`中没有),则将输入添加到`population`并将覆盖范围添加到`coverages_seen`。 + +```py +class MutationCoverageFuzzer(MutationFuzzer): + def reset(self): + super().reset() + self.coverages_seen = set() + # Now empty; we fill this with seed in the first fuzz runs + self.population = [] + + 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 outcome == Runner.PASS and new_coverage not in self.coverages_seen: + # We have new coverage + self.population.append(self.inp) + self.coverages_seen.add(new_coverage) + + return result + +``` + +现在让我们使用它: + +```py +seed_input = "http://www.google.com/search?q=fuzzing" +mutation_fuzzer = MutationCoverageFuzzer(seed=[seed_input]) +mutation_fuzzer.runs(http_runner, trials=10000) +mutation_fuzzer.population + +``` + +```py +['http://www.google.com/search?q=fuzzing', + 'http://www.goog.com/search;q=fuzzilng', + 'http://ww.6goog\x0eoomosearch;/q=f}zzilng', + 'http://uv.Lboo.comoseakrch;q=fuzilng', + 'http://ww.6goog\x0eo/mosarch;/q=f}z{il~g', + 'Http://www.g/ogle.com/earchq=fuzzing', + 'http://www.goog.com/lsearkh;q=fuzzilng', + 'http://www.Ifnole.com/searchq=fzzing', + 'Http://ww.g/ogle.com/earchq=furzing', + 'Http://wwQw.g/oGle.ug/m/ear#hq=uzzGing', + 'Http://ww.g/ogle.cnom?earchq=fZrzing', + 'http://www.goog.co#m/lsearkh;q=fuzilng', + 'Http://wwQw.g/oGle.ug/m/ear#hqg=uzzGvi~g', + 'http://RuV.Lboo.comose`krch;q=ftzil~g', + "http://www.googco#_mx/lsa'rkhq=fuzilng", + 'htTp://w.6goog\x0eo/mosarch;/q=f}z{il~g', + 'http://www.google.com/sea[arch?q=fuzzGing', + 'Http://wwQ?w.g/oGle6.ug/m/ekar#hq=uzzGing', + 'httP://uv.Lboo.comoseakrch;q=fzilng', + 'http://ww.6goog\x0eo/mosarch;/?q=fz{il~g', + 'htTp://w.6eoog\x0eo/mosarch;?p=f}z{il~w', + 'http://www.goog\x0eom/sea7rch;#q=fuezzi-mog', + 'httP://uv.L"Noo.omosekrch;q=fziln', + 'htTp://w.6go7opg\x0eo/mosayrch;/ #q=b}z{il~g', + 'Http://wwQ?w.g/%oGle6.ug//ekar!hq=uzzGinc', + 'htTp://wS.6eoog\x0e/msarcah;?p=f}z{i~7,', + 'http://www.goog\x0eo}/qa5ch;Y#q=fuezzi-mog', + 'http://ww.W=6goxog\x0eo/mosarXch;+?q=fz{il~g', + 'htTp://w.6Foo\x0en/mosarch;q=fK}z{il~g', + 'htTp://7.6Gogo\x0e/iowarcj;q=fK}z{Il~gX', + 'http://ww.62gogo/m%/saRch;/q=fl}ziil~g', + 'http://www.go?og\x0eo}/qac`P;Y#eq=fuezzik-mog', + 'http://www.go?g\x0ek}/qa`P;Y#q=fuezzik-moga', + 'htTp://Hw.6g7`g\x0eo/mosayrc)h;/K #q=j}zyil~g', + 'htTp://Hw.6g`g\x0eo/moseyrc)h;OK(#q=j}zyil~g', + 'http://*ww.W=goxog\x0e/2RmosarXch;+?Cq=f{i2l~g', + 'htTp://.6,gokg\x0eo~/mosrch;/?f\\}z{il~g', + 'htTp://Hw.6g`g\x0eo/}oseyrc)h;OK(#p=j}zyil~g', + 'htTp://w.A6go7evopg\nFo/mosayvch;?R#q=b}z{il~g', + 'htTp://w.Ago7e%vopgV\nzFo/oayv=ch;?R6#q=b}z{il~g', + 'Http://wwQ?w.g/oGle6.ug/m/ka2#hq9=uzzGing', + 'http://wHw.67`g\x0eo/mosaygrc)h;/K #q=j}zyil~g', + 'htTp://w.g&o\x0eo/mocarh;/q=b}Mx{iBl~g', + 'http://w.Ago7e%vopoV\nzIFo/oayv=ch;?R6#q=b}z{il~+g=', + 'htTp://.6O,gokg\x0eo~/mTmsrc;h;/?f\\}Ez{il~g', + 'http://ww%.6goog\x0eo/mosach;/?q=fz{il~g', + 'http://wHw.67`g\x0eo/mosaygc)h;/K #q#`=j}zyilg'] + +``` + +成功! 在我们的总体中,*的每个输入*现在都是有效的,并且覆盖范围不同,来自方案,路径,查询和片段的各种组合。 + +```py +all_coverage, cumulative_coverage = population_coverage( + mutation_fuzzer.population, http_program) + +``` + +```py +import [matplotlib.pyplot](https://docs.python.org/3/library/matplotlib.pyplot.html) as [plt](https://docs.python.org/3/library/plt.html) + +``` + +```py +plt.plot(cumulative_coverage) +plt.title('Coverage of urlparse() with random inputs') +plt.xlabel('# of inputs') +plt.ylabel('lines covered'); + +``` + +![](data:image/png;base64,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 +) + +这种策略的好处是,应用于较大的程序时,它将很乐于探索另一条路径–涵盖功能与功能之间的关系。 所需要的只是捕获覆盖范围的一种方法。 + +## 经验教训 + +* 随机生成的输入通常是无效的-因此主要行使输入处理功能。 +* 来自现有有效输入的变异具有较高的有效机会,因此可以行使输入处理以外的功能。 + +## 后续步骤 + +在有关[灰箱模糊](GreyboxFuzzer.html)的下一章中,我们进一步扩展了基于突变的测试的概念,其中包含*功率调度表*,可以为执行“不太可能”路径和种子运行的种子提供更多能量 与目标位置“更近”。 + +## 练习 + +### 练习1:对带有突变的CGI解码进行模糊处理 + +将上述*指导的基于*突变的模糊技术应用于[“封面”](Coverage.html) 一章的`cgi_decode()`。 在涵盖`+`,`%`(有效和无效)和常规字符的所有变体之前,您需要进行几次试验? + +```py +from [Coverage](Coverage.html) import cgi_decode + +``` + +```py +seed = ["Hello World"] +cgi_runner = FunctionCoverageRunner(cgi_decode) +m = MutationCoverageFuzzer(seed) +results = m.runs(cgi_runner, 10000) + +``` + +```py +m.population + +``` + +```py +['Hello World', 'he_<+llo(or libmath.h +/Applications/Xcode.app/Contents/Developer/usr/bin/make global.o +gcc -DHAVE_CONFIG_H -I. -I.. -I. -I./../h -g -O2 -Wall -funsigned-char --coverage -MT global.o -MD -MP -MF .deps/global.Tpo -c -o global.o global.c +mv -f .deps/global.Tpo .deps/global.Po +gcc -g -O2 -Wall -funsigned-char --coverage -o libmath.h -o fbc main.o bc.o scan.o execute.o load.o storage.o util.o warranty.o global.o ../lib/libbc.a -ll +./fbc -c ./libmath.b libmath.h +./fix-libmath_h +2655 +2793 +rm -f ./fbc ./global.o +gcc -DHAVE_CONFIG_H -I. -I.. -I. -I./../h -g -O2 -Wall -funsigned-char --coverage -MT global.o -MD -MP -MF .deps/global.Tpo -c -o global.o global.c +mv -f .deps/global.Tpo .deps/global.Po +gcc -g -O2 -Wall -funsigned-char --coverage -o bc main.o bc.o scan.o execute.o load.o storage.o util.o global.o warranty.o ../lib/libbc.a -ll +Making all in dc +gcc -DHAVE_CONFIG_H -I. -I.. -I./.. -I./../h -g -O2 -Wall -funsigned-char --coverage -MT dc.o -MD -MP -MF .deps/dc.Tpo -c -o dc.o dc.c +mv -f .deps/dc.Tpo .deps/dc.Po +gcc -DHAVE_CONFIG_H -I. -I.. -I./.. -I./../h -g -O2 -Wall -funsigned-char --coverage -MT misc.o -MD -MP -MF .deps/misc.Tpo -c -o misc.o misc.c +mv -f .deps/misc.Tpo .deps/misc.Po +gcc -DHAVE_CONFIG_H -I. -I.. -I./.. -I./../h -g -O2 -Wall -funsigned-char --coverage -MT eval.o -MD -MP -MF .deps/eval.Tpo -c -o eval.o eval.c +mv -f .deps/eval.Tpo .deps/eval.Po +gcc -DHAVE_CONFIG_H -I. -I.. -I./.. -I./../h -g -O2 -Wall -funsigned-char --coverage -MT stack.o -MD -MP -MF .deps/stack.Tpo -c -o stack.o stack.c +mv -f .deps/stack.Tpo .deps/stack.Po +gcc -DHAVE_CONFIG_H -I. -I.. -I./.. -I./../h -g -O2 -Wall -funsigned-char --coverage -MT array.o -MD -MP -MF .deps/array.Tpo -c -o array.o array.c +mv -f .deps/array.Tpo .deps/array.Po +gcc -DHAVE_CONFIG_H -I. -I.. -I./.. -I./../h -g -O2 -Wall -funsigned-char --coverage -MT numeric.o -MD -MP -MF .deps/numeric.Tpo -c -o numeric.o numeric.c +mv -f .deps/numeric.Tpo .deps/numeric.Po +gcc -DHAVE_CONFIG_H -I. -I.. -I./.. -I./../h -g -O2 -Wall -funsigned-char --coverage -MT string.o -MD -MP -MF .deps/string.Tpo -c -o string.o string.c +mv -f .deps/string.Tpo .deps/string.Po +gcc -g -O2 -Wall -funsigned-char --coverage -o dc dc.o misc.o eval.o stack.o array.o numeric.o string.o ../lib/libbc.a +Making all in doc +restore=: && backupdir=".am$$" && \ + am__cwd=`pwd` && CDPATH="${ZSH_VERSION+.}:" && cd . && \ + rm -rf $backupdir && mkdir $backupdir && \ + if (makeinfo --no-split --version) >/dev/null 2>&1; then \ + for f in bc.info bc.info-[0-9] bc.info-[0-9][0-9] bc.i[0-9] bc.i[0-9][0-9]; do \ + if test -f $f; then mv $f $backupdir; restore=mv; else :; fi; \ + done; \ + else :; fi && \ + cd "$am__cwd"; \ + if makeinfo --no-split -I . \ + -o bc.info bc.texi; \ + then \ + rc=0; \ + CDPATH="${ZSH_VERSION+.}:" && cd .; \ + else \ + rc=$?; \ + CDPATH="${ZSH_VERSION+.}:" && cd . && \ + $restore $backupdir/* `echo "./bc.info" | sed 's|[^/]*$||'`; \ + fi; \ + rm -rf $backupdir; exit $rc +restore=: && backupdir=".am$$" && \ + am__cwd=`pwd` && CDPATH="${ZSH_VERSION+.}:" && cd . && \ + rm -rf $backupdir && mkdir $backupdir && \ + if (makeinfo --no-split --version) >/dev/null 2>&1; then \ + for f in dc.info dc.info-[0-9] dc.info-[0-9][0-9] dc.i[0-9] dc.i[0-9][0-9]; do \ + if test -f $f; then mv $f $backupdir; restore=mv; else :; fi; \ + done; \ + else :; fi && \ + cd "$am__cwd"; \ + if makeinfo --no-split -I . \ + -o dc.info dc.texi; \ + then \ + rc=0; \ + CDPATH="${ZSH_VERSION+.}:" && cd .; \ + else \ + rc=$?; \ + CDPATH="${ZSH_VERSION+.}:" && cd . && \ + $restore $backupdir/* `echo "./dc.info" | sed 's|[^/]*$||'`; \ + fi; \ + rm -rf $backupdir; exit $rc +make[4]: Nothing to be done for `all-am'. + +``` + +文件`bc/bc`现在应该是可执行的... + +```py +!cd bc-1.07.1/bc; echo 2 + 2 | ./bc + +``` + +```py +4 + +``` + +...并且您应该能够运行`gcov`程序来检索覆盖率信息。 + +```py +!cd bc-1.07.1/bc; gcov main.c + +``` + +```py +File 'main.c' +Lines executed:51.69% of 118 +main.c:creating 'main.c.gcov' + +``` + +如[“覆盖率”一章](Coverage.html)中所述,文件 [bc-1.07.1 / bc / main.c.gcov](bc-1.07.1/bc/main.c.gcov) 现在保存`bc.c`的覆盖范围信息。 每行都以执行次数为前缀。 `#####`表示零倍; `-`表示不可执行的行。 + +像`FunctionCoverageRunner`一样,为`bc`解析GCOV文件并创建`coverage`集。 将其设置为`ProgramCoverageRunner`类,将使用一系列源文件(`bc.c`,`main.c`和`load.c`)构建该类以运行`gcov`。 + +完成后,请不要忘记清理: + +```py +!rm -fr bc-1.07.1 bc-1.07.1.tar.gz + +``` + +### 练习3 + +在[博客文章](https://lcamtuf.blogspot.com/2014/08/binary-fuzzing-strategies-what-works.html)中, *American Fuzzy Lop* (AFL)的作者,一个非常流行的基于变异的模糊器,讨论了各种变异算子的效率。 如上例所示,实施其中四个并评估其效率。 + +### 练习4 + +将新元素添加到候选列表时,AFL实际上并不比较*覆盖率*,但如果它行使新的*分支*则添加元素。 使用[“覆盖率”](Coverage.html) 一章的练习中的分支覆盖率,实施此“分支”策略,并将其与上述“覆盖率”策略进行比较。 + +### 练习5 + +设计并实现一个系统,该系统将从Web收集大量URL。 您可以使用这些样本获得更高的覆盖率吗? 如果将它们用作进一步突变的初始种群怎么办? \ No newline at end of file diff --git a/new/fuzzing-book-zh/11.md b/new/fuzzing-book-zh/11.md new file mode 100644 index 0000000000000000000000000000000000000000..342f5e53d59ba9501e4614a254549b83ca9dc756 --- /dev/null +++ b/new/fuzzing-book-zh/11.md @@ -0,0 +1,1340 @@ +# Greybox模糊测试 + +> 原文: [https://www.fuzzingbook.org/html/GreyboxFuzzer.html](https://www.fuzzingbook.org/html/GreyboxFuzzer.html) + +在[的前一章](MutationFuzzer.html)中,我们介绍了*基于突变的模糊化*,该技术通过将小的突变应用于给定输入来生成模糊输入。 在本章中,我们将说明*如何将*这些突变导向特定目标,例如覆盖范围。 本书中的算法源于流行的 [American Fuzzy Lop](http://lcamtuf.coredump.cx/afl/) (AFL)模糊测试器,尤其是其 [AFLFast](https://github.com/mboehme/aflfast) 和 [AFLGo](https://github.com/aflgo/aflgo) 口味。 我们将探索AFL背后的灰箱模糊测试算法,以及如何利用它来解决各种漏洞自动检测功能。 + +**前提条件** + +* 建议阅读有关基于[突变的模糊测试](MutationFuzzer.html)的介绍。 + +```py +import [fuzzingbook_utils](https://github.com/uds-se/fuzzingbook/tree/master/notebooks/fuzzingbook_utils) + +``` + +## Greybox模糊测试的成分 + +我们首先讨论突变测试和目标指导所需的最重要部分。 + +### 背景 + +AFL是基于*突变的模糊器*。 意思是,AFL通过稍微修改种子输入(即,突变)或通过将一个输入的前半部分与另一个输入的后半部分(即拼接)相结合来生成新的输入。 + +AFL还是*灰盒模糊器*(不是黑盒或白盒)。 意思是,AFL利用覆盖率反馈来学习如何更深入地了解该计划。 这并非完全是黑盒,因为AFL至少利用了*和某些*程序分析功能。 这也不是完全白盒,因为AFL并非建立在重量级程序分析或约束解决上。 取而代之的是,AFL使用轻量级程序工具收集有关生成的输入的(分支)覆盖率的一些信息。 如果生成的输入增加覆盖范围,则将其添加到种子语料库以进一步进行模糊测试。 + +为了检测程序,AFL在每条条件跳转指令之后立即插入一段代码。 当执行时,此所谓的蹦床会为锻炼的分支分配唯一的标识符,并递增与此分支关联的计数器。 为了提高效率,仅保留粗略的分支命中计数。 换句话说,对于每个输入,模糊器都知道哪个分支以及大概多久执行一次。 通常在编译时即在程序源代码被编译为可执行二进制文件时进行检测。 但是,可以使用诸如虚拟机(例如 [QEMU](https://github.com/mirrorer/afl/blob/master/qemu_mode) )或动态检测工具(例如 [Intel PinTool](https://github.com/vanhauser-thc/afl-pin) )之类的工具在非仪表二进制文件上运行AFL。 对于Python程序,我们无需任何检测即可收集覆盖率信息(请参阅[收集覆盖率](Coverage.html#Coverage-of-Basic-Fuzzing)的章节)。 + +### 突变体和种子 + +我们介绍了用于使种子变异的特定类。 + +```py +import [random](https://docs.python.org/3/library/random.html) +from [Coverage](Coverage.html) import Coverage, population_coverage + +``` + +首先,我们将介绍`Mutator`类。 给定种子输入`inp`,mutator返回`inp`的略微修改版本。 在灰盒子语法模糊的[一章中,我们扩展了此类,以考虑智能灰盒子模糊检测的输入语法。](GreyboxGrammarFuzzer.html) + +```py +class Mutator(object): + def __init__(self): + self.mutators = [ + self.delete_random_character, + self.insert_random_character, + self.flip_random_character + ] + +``` + +对于插入,我们在随机位置添加一个随机字符。 + +```py +class Mutator(Mutator): + 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:] + +``` + +对于删除,如果字符串非空,请选择一个随机位置并删除字符。 否则,请使用插入操作。 + +```py +class Mutator(Mutator): + 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:] + +``` + +对于替换,如果字符串非空,则选择一个随机位置并翻转字符中的随机位。 否则,请使用插入操作。 + +```py +class Mutator(Mutator): + 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:] + +``` + +主要方法是`mutate`,它从运算符列表中选择一个随机突变运算符。 + +```py +class Mutator(Mutator): + def mutate(self, inp): + """Return s with a random mutation applied""" + mutator = random.choice(self.mutators) + return mutator(inp) + +``` + +让我们尝试增变器。 您实际上可以与此类“单元”进行交互,并通过将本章加载为Jupyter笔记本来尝试其他输入。 打开后,使用“内核->重新启动&全部运行”来运行笔记本中的所有单元。 + +```py +Mutator().mutate("good") + +``` + +```py +'cood' + +``` + +### 电源表 + +现在我们介绍一个新概念; *功率表*。 功率计划表将宝贵的模糊时间分配到种群中的各个种子之间。 我们的目标是最大程度地花时间模糊那些(最进步的)种子,从而在更短的时间内提高覆盖率。 + +我们将从种群中选择种子的可能性称为种子的*能量*。 在整个测试过程中,我们希望优先考虑更有前途的种子。 简而言之,我们不想浪费能源消耗非渐进种子。 我们将确定种子能量的程序称为模糊器的*功率计划*。 例如,AFL的日程安排将更多的能量分配给较短的种子,执行速度更快的种子,并且产量覆盖率增加的频率更高。 + +首先,除了种子数据外,我们还需要附加一些信息到每个种子。 因此,我们定义以下`Seed`类。 + +```py +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__ + +``` + +以下实施的功率计划为每个种子分配相同的能量。 一旦种子进入种群,它就会像种群中的任何其他种子一样被频繁地起毛。 + +在Python中,我们可以将长的for循环压缩为更小的语句。 + +* `lambda x: ...`返回以`x`作为输入的函数。 Lambda允许快速定义未命名的函数。 +* `map(f, l)`返回一个列表,其中功能`f`应用于列表`l`中的每个元素。 +* `np.random.choice(l,p)`以`p[i]`中的概率返回元素`l[i]`。 + +```py +import [numpy](https://docs.python.org/3/library/numpy.html) as [np](https://docs.python.org/3/library/np.html) + +``` + +```py +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](https://docs.python.org/3/library/numpy.html) as [np](https://docs.python.org/3/library/np.html) + + self.assignEnergy(population) + norm_energy = self.normalizedEnergy(population) + seed = np.random.choice(population, p=norm_energy) + return seed + +``` + +让我们看看此功率调度是否随机地均匀选择种子。 我们要求时间表10k次从三颗种子(A,B,C)的种群中选择一颗种子,并跟踪我们看到每颗种子的次数。 我们应该看到每个种子大约3.3k次。 + +```py +population = [Seed("A"), Seed("B"), Seed("C")] +schedule = PowerSchedule() +hits = { + "A" : 0, + "B" : 0, + "C" : 0 +} + +for i in range(10000): + seed = schedule.choose(population) + hits[seed.data] += 1 + +hits + +``` + +```py +{'A': 3372, 'B': 3319, 'C': 3309} + +``` + +看起来不错。 每粒种子的选择率约为三分之一。 + +### 流道和示例程序 + +我们将从一个六行的小示例程序开始。 为了在执行期间收集覆盖率信息,我们从基于基于突变的[的模糊](MutationFuzzer.html#Guiding-by-Coverage)一章中导入了`FunctionCoverageRunner`类。 + +`FunctionCoverageRunner`构造函数使用Python `function`执行。 函数`run`接受输入,将其传递给Python `function`,并收集此执行的覆盖率信息。 函数`coverage()`为Python `function`中涵盖的每个语句返回元组`(function name, line number)`的列表。 + +```py +from [MutationFuzzer](MutationFuzzer.html) import FunctionCoverageRunner + +``` + +`crashme()`函数引发输入“ bad!”的异常。 让我们看看输入“ good”涵盖了哪些语句。 + +```py +def crashme (s): + if len(s) > 0 and s[0] == 'b': + if len(s) > 1 and s[1] == 'a': + if len(s) > 2 and s[2] == 'd': + if len(s) > 3 and s[3] == '!': + raise Exception() + +crashme_runner = FunctionCoverageRunner(crashme) +crashme_runner.run("good") +list(crashme_runner.coverage()) + +``` + +```py +[('__exit__', 25), ('crashme', 2), ('run_function', 7)] + +``` + +在`crashme`中,输入“ good”仅覆盖第2行中的if语句。分支条件`len(s) > 0 and s[0] == 'b'`的计算结果为False。 + +## 黑盒,灰盒和增强型灰盒模糊化 + +### 基于黑盒变异的模糊器 + +让我们将增幅器和功率计划集成到模糊器中。 我们将从黑匣子模糊器开始-它不会*而不是*利用任何覆盖率信息。 + +我们的`MutationFuzzer`类继承自 [Fuzzer](Fuzzer.html#Fuzzer-Classes) 类。 现在,我们只需要知道函数`fuzz`返回生成的输入,以及函数`runs`执行指定次数的`fuzz`。 对于我们的`MutationFuzzer`类,我们覆盖了`fuzz`函数。 + +```py +from [Fuzzer](Fuzzer.html) import Fuzzer + +``` + +`MutationFuzzer`由一组初始种子,一个变量和一个功率计划构成。 在整个模糊测试过程中,它维护着一个称为`population`的种子主体。 函数`fuzz`从初始种子返回未模糊的种子,或者对总体中的种子进行模糊处理的结果。 函数`create_candidate`处理后者。 它从总体中随机选择一个输入,并应用许多突变。 + +```py +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 + +``` + +好吧,让我们旋转一下变异模糊器。 给定一个种子,我们要求它生成三个输入。 + +```py +seed_input = "good" +mutation_fuzzer = MutationFuzzer([seed_input], Mutator(), PowerSchedule()) +print(mutation_fuzzer.fuzz()) +print(mutation_fuzzer.fuzz()) +print(mutation_fuzzer.fuzz()) + +``` + +```py +good +ooD +cW(ond + +``` + +让我们看看在n = 30k输入的广告活动中,基于变异的黑盒模糊器包含多少条语句。 + +模糊器功能`runs(crashme_runner, trials=n)`生成`n`输入,并通过`crashme_runner`在`crashme`功能上执行它们。 如前所述,`crashme_runner`还收集覆盖信息。 + +```py +import [time](https://docs.python.org/3/library/time.html) +n = 30000 + +``` + +```py +blackbox_fuzzer = MutationFuzzer([seed_input], Mutator(), PowerSchedule()) + +start = time.time() +blackbox_fuzzer.runs(FunctionCoverageRunner(crashme), trials=n) +end = time.time() + +"It took the blackbox mutation-based fuzzer %0.2f seconds to generate and execute %d inputs." % (end - start, n) + +``` + +```py +'It took the blackbox mutation-based fuzzer 1.41 seconds to generate and execute 30000 inputs.' + +``` + +为了衡量覆盖率,我们导入了 [Population_coverage](Coverage.html#Coverage-of-Basic-Fuzzing) 函数。 它接受一组输入和一个Python函数,在该函数上执行输入并收集coverage信息。 具体来说,它返回一个元组`(all_coverage, cumulative_coverage)`,其中`all_coverage`是所有输入覆盖的语句集,`cumulative_coverage`是随着执行的输入数量增加而覆盖的语句数。 我们只是对后者感兴趣,以便随着时间的推移绘制覆盖范围。 + +```py +from [Coverage](Coverage.html) import population_coverage + +``` + +我们从黑匣子模糊器中提取生成的输入,并随着输入数量的增加来测量覆盖率。 + +```py +_, blackbox_coverage = population_coverage(blackbox_fuzzer.inputs, crashme) +bb_max_coverage = max(blackbox_coverage) + +"The blackbox mutation-based fuzzer achieved a maximum coverage of %d statements." % bb_max_coverage + +``` + +```py +'The blackbox mutation-based fuzzer achieved a maximum coverage of 3 statements.' + +``` + +以下生成的输入增加了`crashme` [示例](#Runner-and-Sample-Program)的覆盖范围。 + +```py +[seed_input] + \ +[blackbox_fuzzer.inputs[idx] for idx in range(len(blackbox_coverage)) + if blackbox_coverage[idx] > blackbox_coverage[idx - 1] +] + +``` + +```py +['good', 'bgod'] + +``` + +***摘要*** 。 这是基于黑盒突变的模糊器的工作方式。 我们集成了*变异子*,以通过模糊提供的一组初始种子和*功率调度*来决定下一步选择哪个种子来生成输入。 + +### 基于Greybox变异的Fuzzer + +与黑箱模糊器相比,像 [AFL](http://lcamtuf.coredump.cx/afl/) *这样的灰箱模糊器确实会*利用覆盖信息。 具体来说,灰盒模糊器会在种子种群中添加生成的输入,从而增加代码覆盖率。 + +方法`run()`继承自 [Fuzzer](Fuzzer.html#Fuzzer-Classes) 类。 调用它可以生成并执行一个输入。 我们重写此功能可向`population`添加输入以增加覆盖范围。 灰盒模糊器属性`coverages_seen`维护先前已涵盖的语句集。 + +```py +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) + +``` + +让我们将灰盒模糊器旋转一下。 + +```py +seed_input = "good" +greybox_fuzzer = GreyboxFuzzer([seed_input], Mutator(), PowerSchedule()) + +start = time.time() +greybox_fuzzer.runs(FunctionCoverageRunner(crashme), trials=n) +end = time.time() + +"It took the greybox mutation-based fuzzer %0.2f seconds to generate and execute %d inputs." % (end - start, n) + +``` + +```py +'It took the greybox mutation-based fuzzer 1.67 seconds to generate and execute 30000 inputs.' + +``` + +在生成相同数量的测试输入后,灰盒模糊器会覆盖更多语句吗? + +```py +_, greybox_coverage = population_coverage(greybox_fuzzer.inputs, crashme) +gb_max_coverage = max(greybox_coverage) + +"Our greybox mutation-based fuzzer covers %d more statements" % (gb_max_coverage - bb_max_coverage) + +``` + +```py +'Our greybox mutation-based fuzzer covers 2 more statements' + +``` + +[示例](#Runner-and-Sample-Program)的种子种群现在包含以下种子。 + +```py +greybox_fuzzer.population + +``` + +```py +[good, bood, ba,oo`, bad + oo1] + +``` + +覆盖反馈确实有帮助。 这些新种子就像面包屑或里程碑一样,它们指导模糊器更快地进入更深的代码区域。 以下是一个简单的图表,显示了在我们的简单[示例](#Runner-and-Sample-Program)上两个模糊器随时间推移所实现的覆盖范围。 + +```py +%matplotlib inline + +``` + +```py +import [matplotlib.pyplot](https://docs.python.org/3/library/matplotlib.pyplot.html) as [plt](https://docs.python.org/3/library/plt.html) + +``` + +```py +line_bb, = plt.plot(blackbox_coverage, label="Blackbox") +line_gb, = plt.plot(greybox_coverage, label="Greybox") +plt.legend(handles=[line_bb, line_gb]) +plt.title('Coverage over time') +plt.xlabel('# of inputs') +plt.ylabel('lines covered'); + +``` + +![](data:image/png;base64,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ZrXLNClIakuSEG6LiHurL4+IjyJieTr9V6CtpC2zjMnMzGqX5dlHAm4C5kXEb2sp85W0HJL2SeNZkk1EkftX7lQwM6tRlmcfHQCcDMyRNDOd90ugB0BE3AB8GzhL0mrgU+CECJ8mZGbWVDJLChExmToGlIiIa4Frs4qh2s4aZTdmtmFWrVrFwoULWblyZVOHUpLat29Pt27daNu27Xqtn2VLoVlyR7NZ87Zw4UI6depEz549fai3niKCJUuWsHDhQrbffvv12kYZDXPhloJZKVi5ciVdunRxQlgPkujSpcsGtbLKKCkkAl+8ZtbcOSGsvw1978ouKZiZWe3KJylE3imp7lUwswJat25N79692WOPPejTpw/PPvssAG+++Sa77bbbem2zX79+TJs27UvzO3bsuEGxNrSy62g2M6vLxhtvzMyZyZn0f/vb3xg5ciRPPfVUE0fVOMooKVR1NPvOa2al4pIH5/LyO9VHx9kwu2yzKRcfVeMwazX66KOP6Ny585fmv/nmm5x88sl88sknAFx77bXsv//+AFx11VWMGzeOVq1accQRR3DllVfm1lu7di1Dhw6le/fu/OY3vwHgZz/7GU8++SSdO3fmjjvuoKKigpkzZ3LmmWeyYsUKdthhB0aPHk2nTp3o27cvV199Nf369WPkyJG0atWKyy67bEPeknWUz+EjM7Miffrpp/Tu3ZuddtqJYcOGceGFF36pzFZbbcWECROYMWMG48ePZ/jw4QA8/PDD3H///Tz//PPMmjWLX/ziF7l1Vq9ezYknnsjXvva1XEL45JNP6NOnDzNmzODggw/mkksuAeCUU05h1KhRzJ49m2984xtccskltGnThjFjxnDWWWcxYcIEHnnkES6++OIGrXv5tBR88ZpZyanPL/qGlH/4aMqUKZxyyim89NJL65RZtWoV5557LjNnzqR169a89tprADz22GMMHTqUTTbZBIAtttgit84ZZ5zBd77zHS644ILcvFatWnH88ccDcNJJJ3HssceybNkyli5dysEHHwzAqaeeynHHHQfArrvuysknn8xRRx3FlClTaNeuXYPWvexaCj4l1czqo2/fvixevJjqw/b/7ne/o2vXrsyaNYtp06bx+eefA8kFZLWdFrr//vvz5JNPFryOoJhTSufMmcPmm2/Oe++9V4+aFKfskoKZWX288sorrFmzhi5duqwzf9myZWy99da0atWKcePGsWbNGgD69+/P6NGjWbFiBQAffPBBbp3TTjuNgQMHctxxx7F69Wog6WO4++67Afjzn//MgQceyGabbUbnzp15+umnARg3blyu1XDvvfeyZMkSJk2axPDhw1m6dGmD1rd8Dh/lj5LatIGYWTNX1acAyS//sWPH0rp163XKnH322XzrW9/irrvu4pBDDqFDhw4AHH744cycOZPKykratWvHwIEDufzyy3Pr/fSnP2XZsmWcfPLJ3HbbbXTo0IG5c+ey1157sdlmmzF+/HgAxo4dm+to7tWrFzfffDOLFy/m/PPP5/HHH6d79+6ce+65jBgxgrFjxzZY3VVqg5JWVlZGTef61mnVp3DZV7hy1Qmcc+G1dGq/foNFmVm25s2bx84779zUYZS0mt5DSdMjorKudcvn8FH+ndfcqWBmVqPySQpmZlanMkoKpXWYzMysKZRRUki4o9nMrHblkxRKrEPdzKwplE9SSPniNTOz2pVdUjAzq8t7773H9773PXr16sVee+1F3759ue+++zZ4uxMnTmTQoEENEGF2yigp5J2S6l4FM6tFRDB48GAOOuggFixYwPTp07njjjtYuHDhOuWqrkhuacroimYzKzkPnw//mtOw2/zKN+CIK2td/MQTT9CuXTvOPPPM3LztttuOH/3oR4wZM4a//OUvrFy5kk8++YQnnniCq6++mjvvvJPPPvuMIUOGcMkll3DhhRey5ZZbMmLECAAuuOACunbtyu67785HH33EkCFDePXVVznooIP44x//SKtWrbj99tu5/PLLiQiOPPJIRo0axVtvvcVhhx3GlClT2GKLLTj44IO58MIL6d+/f8O+J3nKJym4o9nMijB37lz69OlT6/IpU6Ywe/ZstthiCx599FFef/11pk6dSkRw9NFHM2nSJE477TSOPfZYRowYwdq1a7njjjuYOnUqc+bMYerUqbz88stst912HH744dx7773sv//+nHfeeUyfPp3OnTvTv39/7r//fgYPHsx5553HmWeeyb777ssuu+ySaUKAckoKqQB3NJuVigK/6BvLOeecw+TJk2nXrh3nnHMO3/zmN3PDYT/66KM8+uij7LnnngAsX76c119/nYMOOoguXbrw4osv8t5777HnnnvmBtTbZ5996NWrFwDf/e53mTx5Mm3btqVfv35UVFQA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 +) + +***摘要*** 。 我们已经看到了灰匣子模糊器如何“发现”有趣的种子,这些种子可以带来更多的进步。 从输入`good`,我们的灰盒模糊器已经慢慢学习了如何生成输入`bad!`,该输入引发了异常。 现在,我们如何才能更快地做到这一点? + +***尝试*** 。 使用基于黑箱*的一代*模糊测试器,随着时间的推移将实现多少覆盖? 尝试绘制所有三个模糊器的覆盖范围。 您可以如下定义基于黑盒生成的模糊器。 + +```py +from [Fuzzer](Fuzzer.html) import RandomFuzzer +blackbox_gen_fuzzer = RandomFuzzer(min_length=4, max_length=4, char_start=32, char_range=96) + +``` + +您可以通过以Jupyter笔记本打开本章来执行自己的代码。 + +***读取*** 。 这是最成功的漏洞检测工具之一AFL的工作原理的高层视图。 如果您对技术细节感兴趣,请查看: [https://github.com/mirrorer/afl/blob/master/docs/technical_details.txt](https://github.com/mirrorer/afl/blob/master/docs/technical_details.txt) + +### 增强型Greybox Fuzzer + +我们增强的灰箱模糊器为有望实现更大覆盖率的种子分配了更多能量。 我们更改功率计划,使行使“非常规”路径的种子拥有更多能量。 对于*异常路径*,我们指的是生成的输入不经常使用的路径。 + +为了识别输入所执行的路径,我们利用了[迹线覆盖范围](WhenIsEnough.html#Trace-Coverage)中的功能`getPathID`。 + +```py +import [pickle](https://docs.python.org/3/library/pickle.html) # serializes an object by producing a byte array from all the information in the object +import [hashlib](https://docs.python.org/3/library/hashlib.html) # produces a 128-bit hash value from a byte array + +``` + +函数`getPathID`返回coverage集的唯一哈希。 + +```py +def getPathID(coverage): + """Returns a unique hash for the covered statements""" + pickled = pickle.dumps(coverage) + return hashlib.md5(pickled).hexdigest() + +``` + +有几种方法可以根据锻炼路径的异常程度分配能量。 在这种情况下,我们实现了指数功率调度,该算法计算种子$ s $的能量$ e(s)$,如下所示$$ e(s)= \ frac {1} {f(p(s))^ a } $$其中 + +* $ p(s)$返回$ s $行使的路径的ID, +* $ f(p)$返回生成的输入行使路径$ p $的次数,并且 +* $ a $是给定的指数。 + +```py +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) + +``` + +在灰箱模糊器中,跟踪每个路径$ p $行使$ f(p)$的次数,并更新电源计划。 + +```py +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) + +``` + +好的,让我们在简单的[示例](#Runner-and-Sample-Program)上运行增强型灰箱模糊器$ n = 10,000k次。 我们将指数功效表的指数设置为$ a = 5 $。 + +```py +n = 10000 +seed_input = "good" +fast_schedule = AFLFastSchedule(5) +fast_fuzzer = CountingGreyboxFuzzer([seed_input], Mutator(), fast_schedule) +start = time.time() +fast_fuzzer.runs(FunctionCoverageRunner(crashme), trials=n) +end = time.time() + +"It took the fuzzer w/ exponential schedule %0.2f seconds to generate and execute %d inputs." % (end - start, n) + +``` + +```py +'It took the fuzzer w/ exponential schedule 1.08 seconds to generate and execute 10000 inputs.' + +``` + +```py +x_axis = np.arange(len(fast_schedule.path_frequency)) +y_axis = list(fast_schedule.path_frequency.values()) + +plt.bar(x_axis, y_axis) +plt.xticks(x_axis) +plt.ylim(0, n) +#plt.yscale("log") +#plt.yticks([10,100,1000,10000]) +plt; + +``` + +![](data:image/png;base64,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 +) + +```py +print(" path id 'p' : path frequency 'f(p)'") +fast_schedule.path_frequency + +``` + +```py + path id 'p' : path frequency 'f(p)' + +``` + +```py +{'f2e66f5447cf94afc06f4aff3d7cf349': 5684, + '3e55dc120b76995e04fdeb76ef790af8': 2629, + 'e11bfcc84cfe5320d36a03abff4d8135': 1121, + 'ccab8793361783ff88e8cafc58ed178b': 442, + 'd5b7040bdd60ea92a98a0d715bdb7ced': 124} + +``` + +它与带有经典功率计划的灰盒模糊器相比如何? + +```py +seed_input = "good" +orig_schedule = PowerSchedule() +orig_fuzzer = CountingGreyboxFuzzer([seed_input], Mutator(), orig_schedule) +start = time.time() +orig_fuzzer.runs(FunctionCoverageRunner(crashme), trials=n) +end = time.time() + +"It took the fuzzer w/ original schedule %0.2f seconds to generate and execute %d inputs." % (end - start, n) + +``` + +```py +'It took the fuzzer w/ original schedule 0.76 seconds to generate and execute 10000 inputs.' + +``` + +```py +x_axis = np.arange(len(orig_schedule.path_frequency)) +y_axis = list(orig_schedule.path_frequency.values()) + +plt.bar(x_axis, y_axis) +plt.xticks(x_axis) +plt.ylim(0, n) +#plt.yscale("log") +#plt.yticks([10,100,1000,10000]) +plt; + +``` + +![](data:image/png;base64,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 +) + +```py +print(" path id 'p' : path frequency 'f(p)'") +orig_schedule.path_frequency + +``` + +```py + path id 'p' : path frequency 'f(p)' + +``` + +```py +{'f2e66f5447cf94afc06f4aff3d7cf349': 6799, + '3e55dc120b76995e04fdeb76ef790af8': 2164, + 'e11bfcc84cfe5320d36a03abff4d8135': 743, + 'ccab8793361783ff88e8cafc58ed178b': 294} + +``` + +指数功率调度可以消除“高频路径”的某些执行,并将它们添加到低频路径。 最不经常执行的路径要么根本不使用传统的功率计划表执行,要么不那么频繁地执行。 + +让我们看一下分配给发现的种子的能量。 + +```py +orig_energy = orig_schedule.normalizedEnergy(orig_fuzzer.population) + +for (seed, norm_energy) in zip(orig_fuzzer.population, orig_energy): + print("'%s', %0.5f, %s" % (getPathID(seed.coverage), norm_energy, repr(seed.data))) + +``` + +```py +'f2e66f5447cf94afc06f4aff3d7cf349', 0.25000, 'good' +'3e55dc120b76995e04fdeb76ef790af8', 0.25000, 'bgko?' +'e11bfcc84cfe5320d36a03abff4d8135', 0.25000, 'bao|oD?' +'ccab8793361783ff88e8cafc58ed178b', 0.25000, 'bado}oiD\x7f' + +``` + +```py +fast_energy = fast_schedule.normalizedEnergy(fast_fuzzer.population) + +for (seed, norm_energy) in zip(fast_fuzzer.population, fast_energy): + print("'%s', %0.5f, %s" % (getPathID(seed.coverage), norm_energy, repr(seed.data))) + +``` + +```py +'f2e66f5447cf94afc06f4aff3d7cf349', 0.00000, 'good' +'3e55dc120b76995e04fdeb76ef790af8', 0.00000, 'bood/' +'e11bfcc84cfe5320d36a03abff4d8135', 0.00002, 'ba^noD\x0f' +'ccab8793361783ff88e8cafc58ed178b', 0.00173, 'bad\x0f' +'d5b7040bdd60ea92a98a0d715bdb7ced', 0.99825, 'bad!\x0e' + +``` + +究竟。 我们新的指数式功率计划将最多能量分配给行使最低频率路径的种子。 + +让我们根据简单的[示例](#Runner-and-Sample-Program)随时间推移实现的覆盖率进行比较。 + +```py +_, orig_coverage = population_coverage(orig_fuzzer.inputs, crashme) +_, fast_coverage = population_coverage(fast_fuzzer.inputs, crashme) +line_orig, = plt.plot(orig_coverage, label="Original Greybox Fuzzer") +line_fast, = plt.plot(fast_coverage, label="Boosted Greybox Fuzzer") +plt.legend(handles=[line_orig, line_fast]) +plt.title('Coverage over time') +plt.xlabel('# of inputs') +plt.ylabel('lines covered'); + +``` + +![](data:image/png;base64,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O3bs4Nhjj+WZZ57hoIMOKjbW4cOHM2LECBo2bBiLtXfv3rHlTZo0oXfv3rzxxhsMGDCANm3acP3117NmzRoOOuggHn/8cRo2bEhOTg5LliyhatWqbNq0iZycHD7//HMguBIZOHAgmzZt4qmnnqJt27asX7+e3r178+WXX3LQQQcxZswYcnJyGDhwIIceeihDhgzh9ddfZ/jw4UyfPp0DDij9fLpz586MGDGC3Nxc1q5dS25uLsuWLaNPnz6xq5X//Oc/DBgwgPz8fKZMmQLAmjVr6NatG2PHjuXZZ5/lkUceYceOHZx++uk89thjZGVlUbNmTW6++WZef/117r//fjp27FhqPImKsvXRDDOTmeWYWavw9aqZjQ4TAmY20sxamFlLM2tnZjOjiicusqSv0auPXDpbuHAhp5122h7zateuTePGjVm6dCkAH374IePHj+ftt9/eo9xjjz3GwQcfzLx58/j973/PnDlzitzG559/zvXXX8/ChQupW7cuf/vb3wC45JJL+Ne//sUnn3zCiSeeyJNPPllqrK1bty6xTHZ2NjNmzOCKK66gb9++/OlPf2LOnDmMGDGC/v37U6tWLTp37swrr7wCwF//+ld++tOfxtrtf/fdd8ycOZPHHnsslnCGDh3Kqaeeyrx587j77rtjVUB/+MMfmDhxIu+88w4DBw5k7NixRSaEiRMnxqqPxo4dW2L8TzzxBHl5eUyePJl69erRq1cvhg0bRl5eHu+++y716tVjwIABLFq0iIkTJ/LBBx+Ql5dHVlYWEyZMiO3DySefzKxZs5KaECDaK4X0lMTqI/Mu8VwZlXRGHxUzK7JFSvz8rl27csghh+xVZsaMGQwaNAiAk08+mZycnCK30bRpU1q1agXAaaedxrJlywBYsGABt99+Oxs2bGDLli2cd955Ccc9f/58fv7zn7N582buvvtuevToARD7uWXLFmbOnMlll10W+8z27dsB6NOnD/fddx8XX3wxY8eO5fHHH4+VufLKKwHo1KkTmzZtYsOGDcyYMSOWyM4++2zWrVvHxo0bqVOnDo8//jidOnXiwQcfpFmzZkXG2qNHD0aOHJnwvm3bto3LLruMkSNHcvTRRwPB93HVVVdx0003cdpppzFy5EjmzJlDmzZtAPj+++857LDDgOAK6qc//WnC2yuLypcUksiS98iDc5Fp0aJF7IBXYNOmTaxYsYJmzZoxZ84catSoUeRnE63NrVatWmw6KyuL77//HoBevXrx8ssv07JlS8aNG8f06dNLjXXu3Ll06dKFU045hby8PAYMGBBbHxCLdffu3dStW5e8vLy91nPGGWewbNky3n33XfLz8zn55JNjywonSElF7mdBufnz51OvXj1WrVpVym9hT1WqVGH37t0Aez030K9fPy655BLOPffc2Lw77riDRo0axar5zIxf/OIX3HPPPXutOzs7m6ysrDLFk6hK2PdR8nss8uojl87OOecctm7dGrsBmp+fz69//Wt69epVYv0+QMeOHXnKjf3EAAAS+ElEQVT++ecB+PTTT5k/f36Ztr1582YaNGjAzp07Y1UfJbn11lv5zW9+w8qVK2Pz4hNCvNq1a9O0aVNeeOEFIDiIfvLJJ7HlPXv25Morr4wdZAtMnDgRCK6C6tSpQ506dejUqVMsvunTp3PooYdSu3Ztli9fzv3338/HH3/MtGnTmDWrqEetitakSZNYdVv8jftHH32UzZs3M3jw4Ni8qVOn8o9//INHHnkkNu+cc85h0qRJfPPNNwCsX7+e5cuXJ7z9fVUJk0LIx1NwlYQkXnrpJV544QWOO+44mjdvTnZ2NnfffXepn+3fvz9r1qwhJyeHe++9l5ycHOrUqZPwtv/v//6P008/na5du3LCCSeUWr579+4MHDiQ888/n5NOOokOHTqQlZVVbLXThAkTePLJJ2nZsiUtWrRg8uTJsWVXXXUV3377bay6qMDBBx9Mhw4d6NevX+wexx133MHs2bPJyclh8ODBjB8/HjPj2muvZcSIERx55JE8+eST9OnTJ+GnhX/zm98watQoOnTowNq1a2PzR4wYwfz582P3IEaPHs3999/PqlWraNu2La1atWLIkCGcdNJJ3HXXXXTr1o2cnBy6du3K6tWrE9r2/lCmjTOcm5tr+9XOePsWuKchdB0GZwzar1i2bN/FyUNf53fdT+SXnY7Zr3W5imvRokWceOKJqQ5jn+Tn57Nz506ys7P54osvOOecc1iyZAkHHnhgqkMr1aRJk5g8eTLPPPNMqkMpd0X9zUmaY2a5pX22Et5TSF71kQ+y4yq6rVu30qVLF3bu3ImZMWrUqIxICDfccAPTpk3j1VdfTXUoGacSJoWQVx85V6patWol/Qng8vCnP/0p1SFkrMp3TyHD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 +) + +不出所料,增强型灰箱模糊器(具有指数功率表)可以更快地实现覆盖。 + +***摘要*** 。 通过更频繁地对行使低频路径的种子进行模糊处理,我们可以以更有效的方式探索程序路径。 + +***尝试*** 。 您可以尝试其他指数以实现快速用电计划,或者完全改变电源消耗。 请注意,较大的指数可能导致浮点运算中的溢出和不精确,从而产生意外结果。 您可以通过以Jupyter笔记本打开本章来执行自己的代码。 + +***读取*** 。 您可以在论文“ [基于覆盖的Greybox Fuzzing as Markov Chain](https://mboehme.github.io/paper/CCS16.pdf) ” [[Böhme*等人*,2018\.](https://mboehme.github.io/paper/CCS16.pdf) ]中找到有关模糊器增强的更多信息。 在[ [http://github.com/mboehme/aflfast](http://github.com/mboehme/aflfast) ]处将其实施到AFL中。 + +### 复杂示例:XMLParser + +让我们在一个更现实的示例(Python [HTML解析器](https://docs.python.org/3/library/html.parser.html))中比较这三个模糊器。 我们从“空”种子开始在HTMLParser上运行所有三个模糊器$ n = 5k $次。 + +```py +from [html.parser](https://docs.python.org/3/library/html.parser.html) import HTMLParser +import [traceback](https://docs.python.org/3/library/traceback.html) + +``` + +```py +# create wrapper function +def my_parser(inp): + parser = HTMLParser() # resets the HTMLParser object for every fuzz input + parser.feed(inp) + +n = 5000 +seed_input = " " # empty seed +blackbox_fuzzer = MutationFuzzer([seed_input], Mutator(), PowerSchedule()) +greybox_fuzzer = GreyboxFuzzer([seed_input], Mutator(), PowerSchedule()) +boosted_fuzzer = CountingGreyboxFuzzer([seed_input], Mutator(), AFLFastSchedule(5)) + +``` + +```py +start = time.time() +blackbox_fuzzer.runs(FunctionCoverageRunner(my_parser), trials=n) +greybox_fuzzer.runs(FunctionCoverageRunner(my_parser), trials=n) +boosted_fuzzer.runs(FunctionCoverageRunner(my_parser), trials=n) +end = time.time() + +"It took all three fuzzers %0.2f seconds to generate and execute %d inputs." % (end - start, n) + +``` + +```py +'It took all three fuzzers 15.90 seconds to generate and execute 5000 inputs.' + +``` + +覆盖范围随时间变化的模糊测试如何比较? + +```py +_, black_coverage = population_coverage(blackbox_fuzzer.inputs, my_parser) +_, grey_coverage = population_coverage(greybox_fuzzer.inputs, my_parser) +_, boost_coverage = population_coverage(boosted_fuzzer.inputs, my_parser) +line_black, = plt.plot(black_coverage, label="Blackbox Fuzzer") +line_grey, = plt.plot(grey_coverage, label="Greybox Fuzzer") +line_boost, = plt.plot(boost_coverage, label="Boosted Greybox Fuzzer") +plt.legend(handles=[line_boost, line_grey, line_black]) +plt.title('Coverage over time') +plt.xlabel('# of inputs') +plt.ylabel('lines covered'); + +``` + +![](data:image/png;base64,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 +) + +两个灰盒模糊器明显优于灰盒模糊器。 原因是灰盒模糊器在此过程中“发现”了有趣的输入。 让我们看一下灰盒子和黑盒子模糊器生成的最后10个输入。 + +```py +blackbox_fuzzer.inputs[-10:] + +``` + +```py +['0', '', '$', '0', ' u', ' |', ' L', '!', '', 'x '] + +``` + +```py +greybox_fuzzer.inputs[-10:] + +``` + +```py +['wq e#kHy|jgQob16{4R', + '<\n92h#Z?8- &<', + 'L?', + '8\x1380S&<', + '., [, ]`)。 但是,仍然缺少许多重要的关键字,例如``。 + +要向模糊测试者告知这些重要的关键字,我们需要[语法](Grammars.html); 在[智能灰盒模糊](LangFuzzer.html)的部分中,我们将它们与上述技术结合在一起。 + +***尝试*** 。 您可以重新运行这些实验以了解模糊实验的差异。 有时,我们声称是高级的模糊器似乎并不胜过劣质的模糊器。 为此,您只需要以Jupyter笔记本打开本章。 + +## 定向灰盒模糊检测 + +有时,您只希望模糊器到达源代码中的某个危险位置。 这可能是您期望缓冲区溢出的位置。 或者您想测试代码库中的最新更改。 我们如何将模糊器引向这些位置? + +在本章中,我们将有向灰箱模糊作为优化问题进行介绍。 + +### 解决迷宫 + +为了提供一个有意义的示例,您可以在其中轻松更改代码的复杂性和目标位置,我们从作为字符串提供的迷宫中生成了迷宫源代码。 本示例基于Felipe Andres Manzano的旧版[博客文章](https://feliam.wordpress.com/2010/10/07/the-symbolic-maze/)(快速大喊大叫!)。 + +您只需将迷宫指定​​为字符串即可。 像这样 + +```py +maze_string = """ ++-+-----+ +|X| | +| | --+ | +| | | | +| +-- | | +| |#| ++-----+-+ +""" + +``` + +使用功能`generate_maze_code`生成代码。 我们将隐藏实现,而是解释其作用。 如果您对编码感兴趣,请在处转到[。](ControlFlow.html#Example:-Maze) + +```py +from [ControlFlow](ControlFlow.html) import generate_maze_code + +``` + +```py +maze_code = generate_maze_code(maze_string) +exec(maze_code) + +``` + +目的是通过提供输入`D`表示向下,`U`表示向上,`L`表示左侧,`R`表示输入,将“ X”变为“#”。 + +```py +print(maze("DDDDRRRRUULLUURRRRDDDD")) # Appending one more 'D', you have reached the target. + +``` + +```py +SOLVED + ++-+-----+ +| | | +| | --+ | +| | | | +| +-- | | +| |X| ++-----+-+ + +``` + +`maze_string`中的每个字符代表一个图块。 对于每个图块,生成图块功能。 + +* 如果当前图块为“良性”(),则调用与下一个输入字符(D,U,L,R)相对应的图块功能。 意外的输入字符将被忽略。 如果没有剩余的输入字符,它将返回“ VALID”和当前的迷宫状态。 +* 如果当前图块是“陷阱”(`+`,`|`和`-`),则返回“ INVALID”和当前迷宫状态。 +* 如果当前图块是“目标”(`#`),则它返回“已解决”和当前迷宫状态。 + +***尝试*** 。 您可以测试其他输入字符序列,甚至完全改变迷宫。 为了执行自己的代码,您只需要以Jupyter笔记本的形式打开本章。 + +为了了解生成的代码,让我们看一下静态[调用图](https://en.wikipedia.org/wiki/Call_graph)。 调用图显示了可以执行功能的顺序。 + +```py +from [ControlFlow](ControlFlow.html) import callgraph + +``` + +```py +callgraph(maze_code) + +``` + +G cluster_G cluster_callgraphX callgraph callgraphX callgraph callgraphX__maze maze (callgraph.py:84) callgraphX->callgraphX__maze callgraphX__print_maze print_maze (callgraph.py:2) callgraphX->callgraphX__print_maze callgraphX__target_tile target_tile (callgraph.py:358) callgraphX->callgraphX__target_tile callgraphX__tile_1_0 tile_1_0 (callgraph.py:26) callgraphX->callgraphX__tile_1_0 callgraphX__tile_1_1 tile_1_1 (callgraph.py:31) callgraphX->callgraphX__tile_1_1 callgraphX__tile_1_2 tile_1_2 (callgraph.py:36) callgraphX->callgraphX__tile_1_2 callgraphX__tile_1_3 tile_1_3 (callgraph.py:41) callgraphX->callgraphX__tile_1_3 callgraphX__tile_1_4 tile_1_4 (callgraph.py:46) callgraphX->callgraphX__tile_1_4 callgraphX__tile_1_5 tile_1_5 (callgraph.py:51) callgraphX->callgraphX__tile_1_5 callgraphX__tile_1_6 tile_1_6 (callgraph.py:56) callgraphX->callgraphX__tile_1_6 callgraphX__tile_1_7 tile_1_7 (callgraph.py:61) callgraphX->callgraphX__tile_1_7 callgraphX__tile_1_8 tile_1_8 (callgraph.py:66) callgraphX->callgraphX__tile_1_8 callgraphX__tile_2_0 tile_2_0 (callgraph.py:71) callgraphX->callgraphX__tile_2_0 callgraphX__tile_2_1 tile_2_1 (callgraph.py:76) callgraphX->callgraphX__tile_2_1 callgraphX__tile_2_2 tile_2_2 (callgraph.py:87) callgraphX->callgraphX__tile_2_2 callgraphX__tile_2_3 tile_2_3 (callgraph.py:92) callgraphX->callgraphX__tile_2_3 callgraphX__tile_2_4 tile_2_4 (callgraph.py:100) callgraphX->callgraphX__tile_2_4 callgraphX__tile_2_5 tile_2_5 (callgraph.py:108) callgraphX->callgraphX__tile_2_5 callgraphX__tile_2_6 tile_2_6 (callgraph.py:116) callgraphX->callgraphX__tile_2_6 callgraphX__tile_2_7 tile_2_7 (callgraph.py:124) callgraphX->callgraphX__tile_2_7 callgraphX__tile_2_8 tile_2_8 (callgraph.py:132) callgraphX->callgraphX__tile_2_8 callgraphX__tile_3_0 tile_3_0 (callgraph.py:137) callgraphX->callgraphX__tile_3_0 callgraphX__tile_3_1 tile_3_1 (callgraph.py:142) callgraphX->callgraphX__tile_3_1 callgraphX__tile_3_2 tile_3_2 (callgraph.py:150) callgraphX->callgraphX__tile_3_2 callgraphX__tile_3_3 tile_3_3 (callgraph.py:155) callgraphX->callgraphX__tile_3_3 callgraphX__tile_3_4 tile_3_4 (callgraph.py:163) callgraphX->callgraphX__tile_3_4 callgraphX__tile_3_5 tile_3_5 (callgraph.py:168) callgraphX->callgraphX__tile_3_5 callgraphX__tile_3_6 tile_3_6 (callgraph.py:173) callgraphX->callgraphX__tile_3_6 callgraphX__tile_3_7 tile_3_7 (callgraph.py:178) callgraphX->callgraphX__tile_3_7 callgraphX__tile_3_8 tile_3_8 (callgraph.py:186) callgraphX->callgraphX__tile_3_8 callgraphX__tile_4_0 tile_4_0 (callgraph.py:191) callgraphX->callgraphX__tile_4_0 callgraphX__tile_4_1 tile_4_1 (callgraph.py:196) callgraphX->callgraphX__tile_4_1 callgraphX__tile_4_2 tile_4_2 (callgraph.py:204) callgraphX->callgraphX__tile_4_2 callgraphX__tile_4_3 tile_4_3 (callgraph.py:209) callgraphX->callgraphX__tile_4_3 callgraphX__tile_4_4 tile_4_4 (callgraph.py:217) callgraphX->callgraphX__tile_4_4 callgraphX__tile_4_5 tile_4_5 (callgraph.py:225) callgraphX->callgraphX__tile_4_5 callgraphX__tile_4_6 tile_4_6 (callgraph.py:233) callgraphX->callgraphX__tile_4_6 callgraphX__tile_4_7 tile_4_7 (callgraph.py:238) callgraphX->callgraphX__tile_4_7 callgraphX__tile_4_8 tile_4_8 (callgraph.py:246) callgraphX->callgraphX__tile_4_8 callgraphX__tile_5_0 tile_5_0 (callgraph.py:251) callgraphX->callgraphX__tile_5_0 callgraphX__tile_5_1 tile_5_1 (callgraph.py:256) callgraphX->callgraphX__tile_5_1 callgraphX__tile_5_2 tile_5_2 (callgraph.py:264) callgraphX->callgraphX__tile_5_2 callgraphX__tile_5_3 tile_5_3 (callgraph.py:269) callgraphX->callgraphX__tile_5_3 callgraphX__tile_5_4 tile_5_4 (callgraph.py:274) callgraphX->callgraphX__tile_5_4 callgraphX__tile_5_5 tile_5_5 (callgraph.py:279) callgraphX->callgraphX__tile_5_5 callgraphX__tile_5_6 tile_5_6 (callgraph.py:287) callgraphX->callgraphX__tile_5_6 callgraphX__tile_5_7 tile_5_7 (callgraph.py:292) callgraphX->callgraphX__tile_5_7 callgraphX__tile_5_8 tile_5_8 (callgraph.py:300) callgraphX->callgraphX__tile_5_8 callgraphX__tile_6_0 tile_6_0 (callgraph.py:305) callgraphX->callgraphX__tile_6_0 callgraphX__tile_6_1 tile_6_1 (callgraph.py:310) callgraphX->callgraphX__tile_6_1 callgraphX__tile_6_2 tile_6_2 (callgraph.py:318) callgraphX->callgraphX__tile_6_2 callgraphX__tile_6_3 tile_6_3 (callgraph.py:326) callgraphX->callgraphX__tile_6_3 callgraphX__tile_6_4 tile_6_4 (callgraph.py:334) callgraphX->callgraphX__tile_6_4 callgraphX__tile_6_5 tile_6_5 (callgraph.py:342) callgraphX->callgraphX__tile_6_5 callgraphX__tile_6_6 tile_6_6 (callgraph.py:350) callgraphX->callgraphX__tile_6_6 callgraphX__tile_6_7 tile_6_7 (callgraph.py:355) callgraphX->callgraphX__tile_6_7 callgraphX__tile_6_8 tile_6_8 (callgraph.py:361) callgraphX->callgraphX__tile_6_8 callgraphX__tile_7_0 tile_7_0 (callgraph.py:366) callgraphX->callgraphX__tile_7_0 callgraphX__tile_7_1 tile_7_1 (callgraph.py:371) callgraphX->callgraphX__tile_7_1 callgraphX__tile_7_2 tile_7_2 (callgraph.py:376) callgraphX->callgraphX__tile_7_2 callgraphX__tile_7_3 tile_7_3 (callgraph.py:381) callgraphX->callgraphX__tile_7_3 callgraphX__tile_7_4 tile_7_4 (callgraph.py:386) callgraphX->callgraphX__tile_7_4 callgraphX__tile_7_5 tile_7_5 (callgraph.py:391) callgraphX->callgraphX__tile_7_5 callgraphX__tile_7_6 tile_7_6 (callgraph.py:396) callgraphX->callgraphX__tile_7_6 callgraphX__tile_7_7 tile_7_7 (callgraph.py:401) callgraphX->callgraphX__tile_7_7 callgraphX__tile_7_8 tile_7_8 (callgraph.py:406) callgraphX->callgraphX__tile_7_8 callgraphX__maze->callgraphX__tile_2_1 callgraphX__tile_1_0->callgraphX__print_maze callgraphX__tile_1_1->callgraphX__print_maze callgraphX__tile_1_2->callgraphX__print_maze callgraphX__tile_1_3->callgraphX__print_maze callgraphX__tile_1_4->callgraphX__print_maze callgraphX__tile_1_5->callgraphX__print_maze callgraphX__tile_1_6->callgraphX__print_maze callgraphX__tile_1_7->callgraphX__print_maze callgraphX__tile_1_8->callgraphX__print_maze callgraphX__tile_2_0->callgraphX__print_maze callgraphX__tile_2_1->callgraphX__print_maze callgraphX__tile_2_1->callgraphX__tile_1_1 callgraphX__tile_2_1->callgraphX__tile_2_0 callgraphX__tile_2_1->callgraphX__tile_2_1 callgraphX__tile_2_1->callgraphX__tile_2_2 callgraphX__tile_2_1->callgraphX__tile_3_1 callgraphX__tile_2_2->callgraphX__print_maze callgraphX__tile_2_3->callgraphX__print_maze callgraphX__tile_2_3->callgraphX__tile_1_3 callgraphX__tile_2_3->callgraphX__tile_2_2 callgraphX__tile_2_3->callgraphX__tile_2_3 callgraphX__tile_2_3->callgraphX__tile_2_4 callgraphX__tile_2_3->callgraphX__tile_3_3 callgraphX__tile_2_4->callgraphX__print_maze callgraphX__tile_2_4->callgraphX__tile_1_4 callgraphX__tile_2_4->callgraphX__tile_2_3 callgraphX__tile_2_4->callgraphX__tile_2_4 callgraphX__tile_2_4->callgraphX__tile_2_5 callgraphX__tile_2_4->callgraphX__tile_3_4 callgraphX__tile_2_5->callgraphX__print_maze callgraphX__tile_2_5->callgraphX__tile_1_5 callgraphX__tile_2_5->callgraphX__tile_2_4 callgraphX__tile_2_5->callgraphX__tile_2_5 callgraphX__tile_2_5->callgraphX__tile_2_6 callgraphX__tile_2_5->callgraphX__tile_3_5 callgraphX__tile_2_6->callgraphX__print_maze callgraphX__tile_2_6->callgraphX__tile_1_6 callgraphX__tile_2_6->callgraphX__tile_2_5 callgraphX__tile_2_6->callgraphX__tile_2_6 callgraphX__tile_2_6->callgraphX__tile_2_7 callgraphX__tile_2_6->callgraphX__tile_3_6 callgraphX__tile_2_7->callgraphX__print_maze callgraphX__tile_2_7->callgraphX__tile_1_7 callgraphX__tile_2_7->callgraphX__tile_2_6 callgraphX__tile_2_7->callgraphX__tile_2_7 callgraphX__tile_2_7->callgraphX__tile_2_8 callgraphX__tile_2_7->callgraphX__tile_3_7 callgraphX__tile_2_8->callgraphX__print_maze callgraphX__tile_3_0->callgraphX__print_maze callgraphX__tile_3_1->callgraphX__print_maze callgraphX__tile_3_1->callgraphX__tile_2_1 callgraphX__tile_3_1->callgraphX__tile_3_0 callgraphX__tile_3_1->callgraphX__tile_3_1 callgraphX__tile_3_1->callgraphX__tile_3_2 callgraphX__tile_3_1->callgraphX__tile_4_1 callgraphX__tile_3_2->callgraphX__print_maze callgraphX__tile_3_3->callgraphX__print_maze callgraphX__tile_3_3->callgraphX__tile_2_3 callgraphX__tile_3_3->callgraphX__tile_3_2 callgraphX__tile_3_3->callgraphX__tile_3_3 callgraphX__tile_3_3->callgraphX__tile_3_4 callgraphX__tile_3_3->callgraphX__tile_4_3 callgraphX__tile_3_4->callgraphX__print_maze callgraphX__tile_3_5->callgraphX__print_maze callgraphX__tile_3_6->callgraphX__print_maze callgraphX__tile_3_7->callgraphX__print_maze callgraphX__tile_3_7->callgraphX__tile_2_7 callgraphX__tile_3_7->callgraphX__tile_3_6 callgraphX__tile_3_7->callgraphX__tile_3_7 callgraphX__tile_3_7->callgraphX__tile_3_8 callgraphX__tile_3_7->callgraphX__tile_4_7 callgraphX__tile_3_8->callgraphX__print_maze callgraphX__tile_4_0->callgraphX__print_maze callgraphX__tile_4_1->callgraphX__print_maze callgraphX__tile_4_1->callgraphX__tile_3_1 callgraphX__tile_4_1->callgraphX__tile_4_0 callgraphX__tile_4_1->callgraphX__tile_4_1 callgraphX__tile_4_1->callgraphX__tile_4_2 callgraphX__tile_4_1->callgraphX__tile_5_1 callgraphX__tile_4_2->callgraphX__print_maze callgraphX__tile_4_3->callgraphX__print_maze callgraphX__tile_4_3->callgraphX__tile_3_3 callgraphX__tile_4_3->callgraphX__tile_4_2 callgraphX__tile_4_3->callgraphX__tile_4_3 callgraphX__tile_4_3->callgraphX__tile_4_4 callgraphX__tile_4_3->callgraphX__tile_5_3 callgraphX__tile_4_4->callgraphX__print_maze callgraphX__tile_4_4->callgraphX__tile_3_4 callgraphX__tile_4_4->callgraphX__tile_4_3 callgraphX__tile_4_4->callgraphX__tile_4_4 callgraphX__tile_4_4->callgraphX__tile_4_5 callgraphX__tile_4_4->callgraphX__tile_5_4 callgraphX__tile_4_5->callgraphX__print_maze callgraphX__tile_4_5->callgraphX__tile_3_5 callgraphX__tile_4_5->callgraphX__tile_4_4 callgraphX__tile_4_5->callgraphX__tile_4_5 callgraphX__tile_4_5->callgraphX__tile_4_6 callgraphX__tile_4_5->callgraphX__tile_5_5 callgraphX__tile_4_6->callgraphX__print_maze callgraphX__tile_4_7->callgraphX__print_maze callgraphX__tile_4_7->callgraphX__tile_3_7 callgraphX__tile_4_7->callgraphX__tile_4_6 callgraphX__tile_4_7->callgraphX__tile_4_7 callgraphX__tile_4_7->callgraphX__tile_4_8 callgraphX__tile_4_7->callgraphX__tile_5_7 callgraphX__tile_4_8->callgraphX__print_maze callgraphX__tile_5_0->callgraphX__print_maze callgraphX__tile_5_1->callgraphX__print_maze callgraphX__tile_5_1->callgraphX__tile_4_1 callgraphX__tile_5_1->callgraphX__tile_5_0 callgraphX__tile_5_1->callgraphX__tile_5_1 callgraphX__tile_5_1->callgraphX__tile_5_2 callgraphX__tile_5_1->callgraphX__tile_6_1 callgraphX__tile_5_2->callgraphX__print_maze callgraphX__tile_5_3->callgraphX__print_maze callgraphX__tile_5_4->callgraphX__print_maze callgraphX__tile_5_5->callgraphX__print_maze callgraphX__tile_5_5->callgraphX__tile_4_5 callgraphX__tile_5_5->callgraphX__tile_5_4 callgraphX__tile_5_5->callgraphX__tile_5_5 callgraphX__tile_5_5->callgraphX__tile_5_6 callgraphX__tile_5_5->callgraphX__tile_6_5 callgraphX__tile_5_6->callgraphX__print_maze callgraphX__tile_5_7->callgraphX__print_maze callgraphX__tile_5_7->callgraphX__tile_4_7 callgraphX__tile_5_7->callgraphX__tile_5_6 callgraphX__tile_5_7->callgraphX__tile_5_7 callgraphX__tile_5_7->callgraphX__tile_5_8 callgraphX__tile_5_7->callgraphX__tile_6_7 callgraphX__tile_5_8->callgraphX__print_maze callgraphX__tile_6_0->callgraphX__print_maze callgraphX__tile_6_1->callgraphX__print_maze callgraphX__tile_6_1->callgraphX__tile_5_1 callgraphX__tile_6_1->callgraphX__tile_6_0 callgraphX__tile_6_1->callgraphX__tile_6_1 callgraphX__tile_6_1->callgraphX__tile_6_2 callgraphX__tile_6_1->callgraphX__tile_7_1 callgraphX__tile_6_2->callgraphX__print_maze callgraphX__tile_6_2->callgraphX__tile_5_2 callgraphX__tile_6_2->callgraphX__tile_6_1 callgraphX__tile_6_2->callgraphX__tile_6_2 callgraphX__tile_6_2->callgraphX__tile_6_3 callgraphX__tile_6_2->callgraphX__tile_7_2 callgraphX__tile_6_3->callgraphX__print_maze callgraphX__tile_6_3->callgraphX__tile_5_3 callgraphX__tile_6_3->callgraphX__tile_6_2 callgraphX__tile_6_3->callgraphX__tile_6_3 callgraphX__tile_6_3->callgraphX__tile_6_4 callgraphX__tile_6_3->callgraphX__tile_7_3 callgraphX__tile_6_4->callgraphX__print_maze callgraphX__tile_6_4->callgraphX__tile_5_4 callgraphX__tile_6_4->callgraphX__tile_6_3 callgraphX__tile_6_4->callgraphX__tile_6_4 callgraphX__tile_6_4->callgraphX__tile_6_5 callgraphX__tile_6_4->callgraphX__tile_7_4 callgraphX__tile_6_5->callgraphX__print_maze callgraphX__tile_6_5->callgraphX__tile_5_5 callgraphX__tile_6_5->callgraphX__tile_6_4 callgraphX__tile_6_5->callgraphX__tile_6_5 callgraphX__tile_6_5->callgraphX__tile_6_6 callgraphX__tile_6_5->callgraphX__tile_7_5 callgraphX__tile_6_6->callgraphX__print_maze callgraphX__tile_6_7->callgraphX__print_maze callgraphX__tile_6_8->callgraphX__print_maze callgraphX__tile_7_0->callgraphX__print_maze callgraphX__tile_7_1->callgraphX__print_maze callgraphX__tile_7_2->callgraphX__print_maze callgraphX__tile_7_3->callgraphX__print_maze callgraphX__tile_7_4->callgraphX__print_maze callgraphX__tile_7_5->callgraphX__print_maze callgraphX__tile_7_6->callgraphX__print_maze callgraphX__tile_7_7->callgraphX__print_maze callgraphX__tile_7_8->callgraphX__print_maze + +### 第一次尝试 + +我们引入了`DictMutator`类,该类通过插入给定字典中的关键字来使字符串发生变异: + +```py +class DictMutator(Mutator): + def __init__(self, dictionary): + super().__init__() + self.dictionary = dictionary + self.mutators.append(self.insert_from_dictionary) + + def insert_from_dictionary(self, s): + """Returns s with a keyword from the dictionary inserted""" + pos = random.randint(0, len(s)) + random_keyword = random.choice(self.dictionary) + return s[:pos] + random_keyword + s[pos:] + +``` + +为了模糊迷宫,我们扩展了`DictMutator`类,以将字典关键字附加到种子的末尾并从种子的末尾删除字符。 + +```py +class MazeMutator(DictMutator): + def __init__(self, dictionary): + super().__init__(dictionary) + self.mutators.append(self.delete_last_character) + self.mutators.append(self.append_from_dictionary) + + def append_from_dictionary(self,s): + """Returns s with a keyword from the dictionary appended""" + random_keyword = random.choice(self.dictionary) + return s + random_keyword + + def delete_last_character(self,s): + """Returns s without the last character""" + if (len(s) > 0): + return s[:-1] + +``` + +让我们尝试一个标准的灰盒模糊器,它具有经典的功率计划和扩展的迷宫突变器(n = 10k)。 + +```py +n = 10000 +seed_input = " " # empty seed + +maze_mutator = MazeMutator(["L","R","U","D"]) +maze_schedule = PowerSchedule() +maze_fuzzer = GreyboxFuzzer([seed_input], maze_mutator, maze_schedule) + +start = time.time() +maze_fuzzer.runs(FunctionCoverageRunner(maze), trials=n) +end = time.time() + +"It took the fuzzer %0.2f seconds to generate and execute %d inputs." % (end - start, n) + +``` + +```py +'It took the fuzzer 10.89 seconds to generate and execute 10000 inputs.' + +``` + +我们将需要打印一些模糊器的统计信息。 我们为什么不为此定义一个函数? + +```py +def print_stats(fuzzer): + total = len(fuzzer.population) + solved = 0 + invalid = 0 + valid = 0 + for seed in fuzzer.population: + s = maze(str(seed.data)) + if "INVALID" in s: invalid += 1 + elif "VALID" in s: valid += 1 + elif "SOLVED" in s: + solved += 1 + if solved == 1: + print("First solution: %s" % repr(seed)) + else: print("??") + + print("""Out of %d seeds, +* %4d solved the maze, +* %4d were valid but did not solve the maze, and +* %4d were invalid""" % (total, solved, valid, invalid)) + +``` + +我们的老式灰箱模糊测试器的性能如何? + +```py +print_stats(maze_fuzzer) + +``` + +```py +Out of 844 seeds, +* 0 solved the maze, +* 204 were valid but did not solve the maze, and +* 640 were invalid + +``` + +它可能一次都无法解决迷宫问题。 我们如何使模糊测试者知道种子离目标有多远? 如果我们知道这一点,就可以为该种子分配更多的能量。 + +***试试*** 。 使用`AFLFastSchedule`和`CountingGreyboxFuzzer`打印增强型模糊器的统计信息。 它的性能可能比未增强的灰箱模糊器要好得多:最低概率路径也​​恰好是到达目标的路径。 您可以通过以Jupyter笔记本打开本章来执行自己的代码。 + +### 计算功能级距离 + +使用迷宫代码和目标函数的静态调用图,我们可以计算每个函数$ f $到目标$ t $的距离,作为$ f $和$ t $之间的最短路径的长度。 + +幸运的是,生成的迷宫代码包含一个名为`target_tile`的函数,该函数返回目标函数的名称。 + +```py +target = target_tile() +target + +``` + +```py +'tile_6_7' + +``` + +现在,我们需要在调用图中找到相应的函数。 函数`get_callgraph`将迷宫代码的调用图作为[网络x](https://networkx.github.io/) 图返回。 Networkx为图形分析提供了一些有用的功能。 + +```py +import [networkx](https://docs.python.org/3/library/networkx.html) as [nx](https://docs.python.org/3/library/nx.html) +from [ControlFlow](ControlFlow.html) import get_callgraph + +``` + +```py +cg = get_callgraph(maze_code) +for node in cg.nodes(): + if target in node: + target_node = node + break +target_node + +``` + +```py +'callgraphX__tile_6_7' + +``` + +现在,我们可以生成函数级距离。 字典`distance`包含每个功能到目标功能的距离。 如果没有到目标的路径,我们指定最大距离(`0xFFFF`)。 + +函数`nx.shortest_path_length(CG, node, target_node)`返回调用图`CG`中从函数`node`到函数`target_node`的最短路径的长度。 + +```py +distance = {} +for node in cg.nodes(): + if "__" in node: + name = node.split("__")[-1] + else: + name = node + try: + distance[name] = nx.shortest_path_length(cg, node, target_node) + except: + distance[name] = 0xFFFF + +``` + +这些是目标功能路径上所有图块功能的距离值。 + +```py +{k: distance[k] for k in list(distance) if distance[k] < 0xFFFF} + +``` + +```py +{'callgraphX': 1, + 'tile_2_1': 22, + 'maze': 23, + 'tile_6_3': 16, + 'tile_6_4': 15, + 'tile_2_3': 8, + 'tile_6_5': 14, + 'tile_5_1': 19, + 'tile_2_4': 7, + 'tile_3_7': 3, + 'tile_2_5': 6, + 'tile_6_7': 0, + 'tile_2_6': 5, + 'tile_2_7': 4, + 'tile_4_1': 20, + 'tile_5_5': 13, + 'tile_4_3': 10, + 'tile_4_4': 11, + 'tile_5_7': 1, + 'tile_3_1': 21, + 'tile_4_5': 12, + 'tile_6_2': 17, + 'tile_4_7': 2, + 'tile_6_1': 18, + 'tile_3_3': 9} + +``` + +***摘要*** 。 使用静态调用图和目标函数$ t $,我们演示了如何计算每个函数$ f $到目标$ t $的函数级距离。 + +***尝试*** 。 您可以通过以Jupyter笔记本打开本章来尝试执行自己的代码。 + +* 如果有多个目标,我们如何计算距离? (提示:[几何平均值](https://en.wikipedia.org/wiki/Geometric_mean))。 +* 给定每个函数$ f $的调用图(CG)和控制流图(CFG $ _f $),我们如何计算基本块(BB)级距离? (提示:在CFG $ _f $中,测量到目标函数路径上函数的*调用*的BB级距离。请记住,功能级距离较高的函数中BB级距离较高, 太。) + +***读取*** 。 如果您对搜索的其他方面感兴趣,可以通过阅读[基于搜索的模糊化](SearchBasedFuzzer.html)一章进行跟进。 如果您有兴趣,如何解决以上问题,可以查看我们的论文“ [定向灰盒模糊化](https://mboehme.github.io/paper/CCS17.pdf)”。 + +### 定向功率表 + +现在我们知道了如何计算功能级别的距离,让我们尝试实现一个功率调度,该功率调度将*的更多能量分配给目标函数的平均距离*较低的种子。 注意,距离值都是*预先计算的*。 这些值被注入到程序二进制文件中,就像coverage工具一样。 实际上,这使得平均距离*的计算极为有效*。 + +如果您真的想知道。 给定调用图中$ CG $中函数$ s $到函数$ t $的函数级距离$ d_f(s,t)$,我们的有向功率调度将计算以下项的种子距离$ d(i,t)$ 种子$ i $以$ t $作为$ d(i,t)= \ sum_ {s \ in CG} \ dfrac {d_f(s,t)} {| CG |} $的功能,其中$ | CG | $是 调用图中$ CG $中的节点数。 + +```py +class DirectedSchedule(PowerSchedule): + def __init__(self, distance, exponent): + self.distance = distance + self.exponent = exponent + + def __getFunctions__(self, coverage): + functions = set() + for f, _ in set(coverage): + functions.add(f) + return functions + + def assignEnergy(self, population): + """Assigns each seed energy inversely proportional + to the average function-level distance to target.""" + for seed in population: + if not hasattr(seed, 'distance'): + num_dist = 0 + sum_dist = 0 + for f in self.__getFunctions__(seed.coverage): + if f in list(distance): + sum_dist += distance[f] + num_dist += 1 + seed.distance = sum_dist / num_dist + seed.energy = (1 / seed.distance) ** self.exponent + +``` + +让我们看看有针对性的时间表如何针对旧的灰盒模糊测试器执行。 + +```py +directed_schedule = DirectedSchedule(distance, 3) +directed_fuzzer = GreyboxFuzzer([seed_input], maze_mutator, directed_schedule) + +start = time.time() +directed_fuzzer.runs(FunctionCoverageRunner(maze), trials=n) +end = time.time() + +"It took the fuzzer %0.2f seconds to generate and execute %d inputs." % (end - start, n) + +``` + +```py +'It took the fuzzer 14.61 seconds to generate and execute 10000 inputs.' + +``` + +```py +print_stats(directed_fuzzer) + +``` + +```py +Out of 1441 seeds, +* 0 solved the maze, +* 542 were valid but did not solve the maze, and +* 899 were invalid + +``` + +它可能也不能解决单个迷宫问题,但是我们有更多有效的解决方案。 因此,肯定有进步。 + +让我们看一下每个种子的距离值。 + +```py +y = [seed.distance for seed in directed_fuzzer.population] +x = range(len(y)) +plt.scatter(x, y) +plt.ylim(0,max(y)) +plt.xlabel("Seed ID") +plt.ylabel("Distance"); + +``` + +![](data:image/png;base64,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 +) + +让我们标准化y轴并提高小距离种子的重要性。 + +### 改进的有向功率计划 + +改进的有向进度计划将最小距离和最大距离之间的种子距离标准化。 同样,如果您真的想知道。 给定种子$ i $与函数$ t $的种子距离$ d(i,t)$,我们改进的功率调度将新的种子距离$ d'(i,t)$计算为$$ d'(i ,t)= \ begin {cases} 1 & \ text {if} d(i,t)= \ text {minD} = \ text {maxD} \\ \ text {maxD}-\ text {minD} & \ text {if} d(i,t)= \ text {minD} \ neq \ text {maxD} \\ \ frac {d(i,t)-\ text {minD}} {\ text {maxD} -\ text {minD}} & \ text {否则} \ end {cases} $$,其中$$ \ text {minD} = \ min_ {i \ in T} [d(i,t)] $$和 $$ \ text {maxD} = \ max_ {i \ in T} [d(i,t)] $$其中$ T $是种子集(即种群)。 + +```py +class AFLGoSchedule(DirectedSchedule): + def assignEnergy(self, population): + """Assigns each seed energy inversely proportional + to the average function-level distance to target.""" + min_dist = 0xFFFF + max_dist = 0 + for seed in population: + if not hasattr(seed, 'distance'): + num_dist = 0 + sum_dist = 0 + for f in self.__getFunctions__(seed.coverage): + if f in list(distance): + sum_dist += distance[f] + num_dist += 1 + seed.distance = sum_dist / num_dist + if seed.distance < min_dist: min_dist = seed.distance + if seed.distance > max_dist: max_dist = seed.distance + + for seed in population: + if (seed.distance == min_dist): + if min_dist == max_dist: + seed.energy = 1 + else: + seed.energy = max_dist - min_dist + else: + seed.energy = ((max_dist - min_dist) / (seed.distance - min_dist)) + +``` + +让我们看看改进的电源调度表如何执行。 + +```py +aflgo_schedule = AFLGoSchedule(distance, 3) +aflgo_fuzzer = GreyboxFuzzer([seed_input], maze_mutator, aflgo_schedule) + +start = time.time() +aflgo_fuzzer.runs(FunctionCoverageRunner(maze), trials=n) +end = time.time() + +"It took the fuzzer %0.2f seconds to generate and execute %d inputs." % (end - start, n) + +``` + +```py +'It took the fuzzer 22.91 seconds to generate and execute 10000 inputs.' + +``` + +```py +print_stats(aflgo_fuzzer) + +``` + +```py +First solution: &.@9DWDx$SD:qD?R5RX1RRsU0ULLUCU&R~RRRDDFDDDm +Out of 1987 seeds, +* 332 solved the maze, +* 192 were valid but did not solve the maze, and +* 1463 were invalid + +``` + +与以前的所有电源计划相反,该计划产生了数百种解决方案。 它产生了许多解决方案。 + +让我们从第一个解决方案中过滤掉所有被忽略的输入字符。 函数`filter(f, seed.data)`返回`seed.data`中元素`e`的列表,其中应用于`e`的函数`f`返回True。 + +```py +for seed in aflgo_fuzzer.population: + s = maze(str(seed.data)) + if "SOLVED" in s: + filtered = "".join(list(filter(lambda c: c in "UDLR", seed.data))) + print(filtered) + break + +``` + +```py +DDDDRRRRUULLUURRRRDDDDD + +``` + +绝对是开头指定的迷宫的解决方案! + +***摘要*** 。 在预先计算出到目标的功能级距离后,我们可以制定一个功率调度表,将更多的能量分配给具有到目标的平均功能级距离较小的种子。 通过标准化最小和最大种子距离之间的种子距离值,我们可以进一步提高定向功率调度。 + +***尝试*** 。 实现和评估使用最小(而不是平均)功能级距离的更简单的定向电源。 使用最小距离的缺点是什么? 为了执行您的代码,您只需要以Jupyter笔记本打开本章。 + +***读取*** 。 您可以在同名论文“ [定向灰箱模糊化](https://mboehme.github.io/paper/CCS17.pdf)” [[Böhme*等人*,2017年。](https://mboehme.github.io/paper/CCS17.pdf)中找到有关定向灰箱模糊的更多信息。 在 [http://github.com/aflgo/aflgo](http://github.com/aflgo/aflgo) 上将其实施到AFL中。 + +## 经验教训 + +* *灰盒模糊器*每秒生成数千个输入。 预处理和轻量级仪器 + * 允许在模糊测试期间维持*的效率,以及* + * 仍然提供了足够的信息来控制进度并稍微控制模糊器。 +* *功率计划表*可以控制/控制模糊器。 例如, + * 我们的[增强型灰箱模糊测试器](#Fuzzer-Boosting)在执行“不太可能”路径的种子上花费了更多能量。 希望生成的输入使用的路径更不可能。 这反过来增加了单位时间内探索路径的数量。 + * 我们的[定向灰箱模糊器](#Directed-Greybox-Fuzzing)在“更接近”目标位置的种子上花费了更多能量。 希望生成的输入甚至更接近目标。 +* *突变器*定义了模糊器的搜索空间。 [为给定程序定制变量](GreyboxFuzzer.html#A-First-Attempt)可以将搜索空间减少到仅相关输入。 在几章中,我们将学习基于[的字典和基于语法的变量](GreyboxGrammarFuzzer.html),以增加生成的有效输入的比率。 + +## 后续步骤 + +我们的目标仍然是充分涵盖功能,以便我们可以触发尽可能多的错误。 为此,我们专注于两类技术: + +1. 尝试覆盖尽可能多的*指定的*功能。 在这里,我们需要输入格式的*规范,*区分各个输入元素,例如(在我们的情况下)数字,运算符,注释和字符串-并尝试覆盖尽可能多的输入元素。 我们将在基于[语法的测试](GrammarFuzzer.html),尤其是基于[语法的突变](GreyboxGrammarFuzzer.html)中探索这一点。 + +2. 尝试覆盖尽可能多的*实现的*功能。 当讨论[基于搜索的测试](SearchBasedFuzzer.html)时,将深入探讨通过“变异”系统地“进化”的“种群”的概念。 此外,[符号测试](SymbolicFuzzer.html)介绍了如何通过解决程序路径上的条件来系统地到达程序位置。 + +这两种技术构成了本书的要旨。 当然,它们也可以相互结合。 和往常一样,我们为所有人提供可运行的代码。 请享用! + +我们完成了,所以我们清理一下: + +```py +import [shutil](https://docs.python.org/3/library/shutil.html) +import [os](https://docs.python.org/3/library/os.html) + +``` + +```py +if os.path.exists('callgraph.dot'): + os.remove('callgraph.dot') + +if os.path.exists('callgraph.py'): + os.remove('callgraph.py') + +``` + +## 背景 + +* **了解有关AFL** 的更多信息: [http://lcamt​​uf.coredump.cx/afl/](http://lcamtuf.coredump.cx/afl/) +* **了解LibFuzzer** (另一个著名的灰箱模糊器): [http://llvm.org/docs/LibFuzzer.html](http://llvm.org/docs/LibFuzzer.html) +* **白盒模糊测试仪必须多快地练习每条路径才能保持比灰盒模糊测试仪更高的效率?** MarcelBöhme和Soumya Paul。 2016\. [自动化软件测试效率的概率分析](https://mboehme.github.io/paper/TSE15.pdf),IEEE TSE,42:345-360 [[Böhme*等人*,2016。](https://doi.org/10.1109/TSE.2015.2487274)] + +## 练习 + +要添加。 \去做{} \ No newline at end of file diff --git a/new/fuzzing-book-zh/12.md b/new/fuzzing-book-zh/12.md new file mode 100644 index 0000000000000000000000000000000000000000..afba03be8bda92bd3b3f359f03fde107017fdc47 --- /dev/null +++ b/new/fuzzing-book-zh/12.md @@ -0,0 +1,1824 @@ +# 基于搜索的模糊化 + +> 原文: [https://www.fuzzingbook.org/html/SearchBasedFuzzer.html](https://www.fuzzingbook.org/html/SearchBasedFuzzer.html) + +有时,我们不仅对使尽可能多的多样化程序输入变得模糊感兴趣,而且对获得实现某些目标(例如到达程序中的特定语句)的*特定*测试输入也感兴趣。 当我们对所要查找的内容有想法时,可以*搜索*。 搜索算法是计算机科学的核心,但是应用诸如广度或深度优先搜索之类的经典搜索算法来搜索测试是不现实的,因为这些算法可能要求我们查看所有可能的输入。 但是,可以使用域知识来解决此问题。 例如,如果我们可以估计几个程序输入中的哪个更接近我们正在寻找的输入,则此信息可以指导我们更快地达到目标-此信息称为*启发式*。 *元启发式*搜索算法中捕获了系统地应用启发式方法的方式。 “元”表示这些算法是通用的,可以针对不同的问题进行不同的实例化。 元启发式方法通常从自然界中观察到的过程中获得启发。 例如,有些算法模仿进化过程,群体智能或化学反应。 通常,它们比穷举搜索方法有效得多,因此它们可以应用于广阔的搜索空间-像程序输入域一样大的搜索空间对他们而言并不是问题。 + +**前提条件** + +* 您应该知道代码覆盖率的工作原理,例如 [关于覆盖的[一章)。](Coverage.html) + +## 测试生成作为搜索问题 + +如果要应用元启发式搜索算法来生成程序的测试数据,则必须做出几种选择:首先,我们需要首先确定*搜索空间*到底是什么 。 搜索空间由我们*表示*我们要寻找的内容来定义。 我们在寻找单个整数值吗? 价值元组? 对象? XML文件? + +### 将程序输入表示为搜索问题 + +表示形式高度依赖于我们要解决的特定测试问题---我们知道要测试的程序,因此表示形式需要对目标程序的任何输入进行编码。 让我们考虑示例函数`test_me()`作为我们的测试函数: + +```py +import [fuzzingbook_utils](https://github.com/uds-se/fuzzingbook/tree/master/notebooks/fuzzingbook_utils) + +``` + +```py +import [Fuzzer](Fuzzer.html) + +``` + +```py +from [fuzzingbook_utils](https://github.com/uds-se/fuzzingbook/tree/master/notebooks/fuzzingbook_utils) import unicode_escape, terminal_escape + +``` + +```py +def test_me(x, y): + if x == 2 * (y + 1): + return True + else: + return False + +``` + +`test_me()`函数具有两个输入参数,并根据两者之间的关系返回`True`或`False`。 `test_me()`的测试输入由一对值组成,一个为`x`,一个为`y`。 例如: + +```py +test_me(0, 0) + +``` + +```py +False + +``` + +```py +test_me(4, 2) + +``` + +```py +False + +``` + +```py +test_me(22, 10) + +``` + +```py +True + +``` + +我们的搜索空间仅与输入有关,因此测试数据的简单表示形式就是输入元组`(x, y)`。 此输入空间中的每个点都有八个*邻居*: + +* `x-1, y-1` +* `x-1, y` +* `x-1, y+1` +* `x, y+1` +* `x+1, y+1` +* `x+1, y` +* `x, y-1` + +为简单起见,让我们限制搜索空间的大小(我们将在以后进行更改)。 例如,假设我们只需要-1000到1000范围内的值: + +```py +MAX = 1000 +MIN = -MAX + +``` + +为了在搜索空间中的任意点检索邻居,我们定义了函数`neighbours()`,该函数实现了基本的摩尔邻居。 也就是说,我们考虑了所有8个直接邻居,同时考虑了我们刚刚用`MAX`和`MIN`定义的边界: + +```py +def neighbours(x, y): + return [(x + dx, y + dy) for dx in [-1, 0, 1] + for dy in [-1, 0, 1] + if (dx != 0 or dy != 0) + and ((MIN <= x + dx <= MAX) + and (MIN <= y + dy <= MAX))] + +``` + +```py +print(neighbours(10, 10)) + +``` + +```py +[(9, 9), (9, 10), (9, 11), (10, 9), (10, 11), (11, 9), (11, 10), (11, 11)] + +``` + +这完全定义了我们的搜索空间:我们有一个表示形式,并且我们知道个人如何通过邻居相互联系。 现在,我们只需要找到一种算法来探索这个社区,就可以找到一种指导算法的启发式方法。 + +### 定义搜索范围:适应度函数 + +所有元启发式算法都基于启发式函数的使用,该函数可估算给定候选解决方案的质量。 该“优度”通常称为个人的*适合度*,估计适合度的试探法是*适合度函数*。 适应度函数是将搜索空间中的任何点映射为数值(适应度值)的函数。 就最佳解决方案而言,搜索空间中的候选解决方案越好,其适用性值就越好。 因此,如果在搜索空间中以适合度为高度来绘制每个点,则会得到一个景观,其最佳解表示为最高峰。 + +适应度函数取决于生成测试数据要达到的目标。 假设我们有兴趣在`test_me()`函数中覆盖if条件的真实分支,即`x == 2 * (y + 1)`。 + +该函数的给定输入元组到目标分支的距离有多近? 让我们考虑一下搜索空间中的任意点,例如 `(274, 153)`。 如果条件比较以下值: + +```py +x = 274 +y = 153 +x, 2 * (y + 1) + +``` + +```py +(274, 308) + +``` + +为了使分支正确,两个值必须相同。 因此,它们之间的差异越大,我们离实现比较结果的真实性就越远;而它们之间的差异越小,我们就可以使比较结果更加真实。 因此,我们可以通过计算`x`和`2 * (y + 1)`之间的差异来量化比较的“假性”。 因此,我们可以将该距离计算为`abs(x - 2 * (y + 1))`: + +```py +def calculate_distance(x, y): + return abs(x - 2 * (y + 1)) + +``` + +```py +calculate_distance(274, 153) + +``` + +```py +34 + +``` + +我们可以将此距离值用作适应度函数,因为我们可以很好地衡量我们与最佳解的接近程度。 但是请注意,在这种情况下,“更好”并不意味着“更大”。 距离越小越好。 这不是问题,因为还可以使用任何可以最大化值的算法来使其最小化。 + +对于整数元组的搜索空间中的每个值,此距离值定义了我们的搜索格局中的海拔。 由于我们的示例搜索空间是二维的,因此搜索范围是三维的,我们可以对其进行绘制以查看其外观: + +```py +from [mpl_toolkits.mplot3d](https://docs.python.org/3/library/mpl_toolkits.mplot3d.html) import Axes3D + +``` + +```py +import [matplotlib.pyplot](https://docs.python.org/3/library/matplotlib.pyplot.html) as [plt](https://docs.python.org/3/library/plt.html) + +``` + +```py +import [numpy](https://docs.python.org/3/library/numpy.html) as [np](https://docs.python.org/3/library/np.html) + +``` + +```py +%matplotlib inline + +x = np.outer(np.linspace(-10, 10, 30), np.ones(30)) +y = x.copy().T +z = calculate_distance(x, y) + +fig = plt.figure() +ax = plt.axes(projection='3d') + +ax.plot_surface(x, y, z, cmap=plt.cm.jet, rstride=1, cstride=1, linewidth=0); + +``` + +![](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAWQAAADuCAYAAAAOR30qAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADl0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uIDIuMi4zLCBodHRwOi8vbWF0cGxvdGxpYi5vcmcvIxREBQAAIABJREFUeJzsvXmUHHl15/uJJfesfVOppFKVVKV9K6m0dzfdbmgWQzMYj93PngceMDxjzxubgWc8D48P2OfwYHiNwdg+B3tsDsbGHhbjxjzGNjRuA9pbau1r7fueWbkvEfF7f6QyKzMjUsosldQlEZ9z+vRR5o1fLBX5jRv3d+/9SUIIbGxsbGxef+TX+wBsbGxsbDLYgmxjY2OzSrAF2cbGxmaVYAuyjY2NzSrBFmQbGxubVYItyDY2NjarBFuQbWxsbFYJtiDb2NjYrBJsQbaxsbFZJagV2ttlfTY2NjaVI5VjZHvINjY2NqsEW5BtbGxsVgm2INvY2NisEmxBtrGxsVkl2IJsY2Njs0qwBdnGxsZmlWALso2Njc0qwRZkGxsbm1WCLcg2NjY2qwRbkG1sbGxWCbYg29jY2KwSbEG2sbGxWSVU2lzIxuauCCHQdR0ARVGQpLJ6qtjY2GALss0KYRgGuq6jaRrJZDL3uSRJKIqS+0+WZWRZRpIkW6xtbIqwBdnmvjAMA03Tcl6xJEk5wRUi0601K9TF28Xjcerq6lBV1RZqGxtsQbZZBkIIDMMgFouhqplbKCukWRHOfpb//3xSqRRDQ0P4fD5SqVTBNrIsoyiKLdQ2P3XYgmxTNlkhzoYlLl68yIEDB5YllNltFEWx3Ieu6wVCnbUtDn/YQm3zOGELss09yRdiwzByXqwQYtliWOxN539uNWa+UOfvd3JyknXr1uUEujhObWPzKGELsk1JshkTmqblRDD7n6IoloJaLqUE+W72VgI7MTHB2rVrTUIN5ATaakLRxmY1YguyjQkrIZblwpR1SZIwDKOs8aLT0/haWkzb34+g549TfGyQOQchBOl0mlQqZQu1zSOBLcg2OYQQRKNRhBCoqlpS7MB6os7qu/j8PF/et4/tv/RLPPF7v4ezqmrFj7vUMZQKfQBomkY6nQZgZmYGr9dLdXW1LdQ2ryu2INsghEDTNDRNY3R0FJfLRVtb27LGmZycZGJiApfLhc/n49af/RmpcJgLX/oSfd/9Ls+++CKb3va2FfOQK8Uq8yMajeJ0OoFCoc7fpjjrwy56sXkQ2IL8U0x+MQew7NiwYRhMTEwwMjJCXV0dmzZtIp1OE5yZ4epf/mXOLjI+zksvvEDbm97Esd///Vzu8mogPz6eT3EudTaEI4S4a+jDFmub5WAL8k8hVsUcWQGRZbns2LAQguHhYcbGxmhubqa3txeHw5GL2fb/9V+TDoVM26WGh/nxm95E9Xvew2ldx+F04vP5Cv5zOByrQtRK5VJbFb3cvHmTLVu25EI9qqraQm1TEbYg/5SQP8mVFVwrgShnsk7TNEZGRohGo2iaxsGDB3E4HLn9AGjJJK9+8YuW29d5vQQjEQJ/+qeor71G74svoq5ZQyQSYW5ujqGhIdLpNIqi4Pf78Xq9OaF2Op0rLmrLSd8rFfrIxtwNwygoIc/a2kUvNnfDFuTHnGzGRCgUwuPxAKUnvCDjIRfHULOkUimGh4eZnp5m3bp1+Hw+Nm3aZGk7+g//QHJ+3vR5U1cXwQsXcv+ePXmSf37DG9j927/N9t/6LWSXK/ddOp0mFosRjUaZn59nZGSEVCqFoig5gc4WqTwIoa6E7IPobtWJdtGLzb2wBfkxJb+YQ9d1Ll68yJEjR+75A7cKWSSTSYaGhpibm6O9vZ2jR48iyzITExOm7SVJAsNg5tOfprelhenaWgYvX859X19XR6BoGyOdJvzKK7z6jW+w+Y/+iNqjRwFwOBzU1NRQU1NTYK9pGtFoNOehX79+3STUD9KjtqIcL7ucopdgMEgoFKK9vR3AMkZtZ348vtiC/Jhxtxzicn7E2Qo8gHg8zuDgIMFgkI6ODrq7u0umweUz+9JLxPv7AagbHWXN4cNcvHYNZ3U1gVdfNe/T4cC4dYvk7CwX3vIWWv/jf2TjJz+Jo7bWcnxVVXNCPT4+zt69e4FCoS7lUVsJ9f1UHGa536rF/G0Nw8hNrlpVJ4KdS/24YgvyY0I5xRzlIEkSiUSCy5cvE41G6ezsZNu2bWX/0IUQjHzucwWfJU+dYmd9PY6dO7k9Nmbapr23l/TJk7l/T375y8x/73tsfvFFGt7xjrL3nS/U+RQL9ejoKMlkEkVR8Hq9xGIx3G43Ho9n2R71Sog6ZMQ4/wFayqO2i14eT2xBfsTJ5hDne1HLEWKAcDjM4OAgsViMHTt20NjYWPEPef773ydy6ZLpc1VVqX/5ZY7s3cv16WmCk5PAnYmu0VGTfWp6mvDnP0/qb/6GphdfxLFu3bLOKbvvUkIdi8Xo7+8nHA4TCAQKhDrfo3a5XHe9FislyPcb+oDMeY2NjWEYBq2trYAt1I8KtiA/ogghiMViBfmzyxXiYDBIf38/QghaWlqIxWI0NTWVfRz5P+rB//7fLe2au7vRjx/HuHCBrR4PsWPHuHTiBOt6e0mdPWuy97W3o507hyYEsR//mMb/9t+o+eAHkYq6w90PqqpSXV2N1+tlzZo1OcHOCnU0GiUQCDA2NrZsoa6UfA+5UvInFLN/l/y8cquil6wwW6Xo2Tx8bEF+xMgv5jh37hz79u3LpZxVghCChYUFBgYGUFWVrq4uampqCAaDRCKRssbI/+EDpE6fxjE7a7JzVFdjvPba0r7jcTzHj7Nv/XpQFGYsxm5cuxZtZCRjH4kw+7GPEfr612n5oz/CtWtXxedbCVmhrq6uLvi8lFDLsozH4yGZTDI/P39fQr2SoY/8XtX5/8/fF9hFL6sJW5AfEayKORRFKbuII4sQgtnZWQYHB3G73WzdupWqvP4SlRSGFJc/pz7zGTpu36bl2DGunz+PFo8D0LJrF+L4cdP2Ho+HlrNnaT52jBuvvop2J2/X3dKCZjH5lzx/Hv2XfonUu9+N42MfQ7qTxrcSlCMypYQ6mx1hJdT53rTX68Xtdt91X/fjIVc6TjlCPTU1RTQaZcOGDTlbu+jlwWEL8irmXsUclVbVTU5OMjQ0RFVVFbt27cLr9Zrs8rMs7kW+IOuXL6N973sAGe+3tZWJLVuYvHkT6coVrEasSqdB1/EeP86+tjbGGxsZv3iR5k2b0KenTfZ1Bw7AmTNon/sc+re/jfMLXwCfr6xjvRv321Mjm8Xhdrvp6urKfa7rOrFYjEgkQjAYZHx8nEQigSzLptBHVqiz3un9spKhj6z4Zq+TXfTy4LAFeRVS3BAerCdyFEW5Zz8IwzCYnJzMeW89PT243e6S9pW01cx/ICQ++9nCLycnWTs5Sdvzb2H2xBmSRdv6d+zAe/Xq0gfj47SNj9P05DEW+/uxOitvXqGJGBwk+fzzrH3uOcSffQmpvqGsY35QWIUaFEWhqqqq4A0EloQ6Go2yuLjIxMRETqiFELlsj3yhrpQH4WnfT9FLsVjbQm2NLcirCKuVOe5VVVdKPHVdZ3x8nNHRUZqamqiqqmLz5s25uGIplhOy0Pv7SX/zm2YDh0pj33kaazRmdx5m/JVTua+qS8S9Gz3Q5ksy84ZDTPzb6dznNT09kBeHzn1+/iz+tx8h+bE/QHv+F+B1ittWMsbdhDo7uVos1KU86lKslCDrun7PewbuXfSSSCS4fv06u3fvLphwtIteCrEFeRWQn0N8+fJlurq68Hg897wxrTzkbAvN8fFxWltbc30mzp49W5bQVhKyyNom//APwWJs7xO9yFczItxy7RR1T+5icDSAUN1IeeXTWSSfF3f/ZaRwiDWLp6l7wx4GB+eIjYzjTxb72BnUzeuR+y7h+cgH0P7h70j8/h8i1neUdfwryUqIuqIoOBwO/H5/QZbL3TzqUkL9MGPRdyNfqLPiC6WX5IKf7hQ9W5BfR6yKObI3arlVdVmRTafTDA8PMzU1RVtbG4cPHy7wbMr1fCsJWUiShJidwBPuI+VUIaUtnZsEztmRAnvnzctsdroIH36G+NAgaIUPE9+RvUiXTuT+7bp+kS0uN8F3PUfqOz8w77+uhvqRm7l/qz9+Gd/bDpP8z/836V/5ECwj+2S5PMg85HJDH5OTk8TjcSRJQtM0JEkilUrh8/nKesBboeu6aSHa5VA8jl30Yo0tyK8DdyvmKCcunEWWZZLJJDdv3mRubo7169dz5MgRyx9QuRkZlYYsXP/zT3FfewXlyfXMzjhQLg8A4D6yH1ffOdM2qbpaqq79C+6n1zM3Dsr14cwXDhXP+G3zPpIJqqUZeG4joUmZ1IVbue/8B3aiXCjM3pDiMRzf/yaOE98g8bE/wtjac8/zeL1Lp5c7TimhNgyDS5cu4Xa7CYfDTE1N5YS62KO+l1CvlKetadp9hz6y41y+fJn29vbcpPTjJNS2ID9E8lfmAOv14MoV5EQiwcLCApOTk3R1dd2zz4Qsy2WNW4kgq7Ewrpe+DIBjZpS1QOrfHyP4/UtUaYuW20hbNyDdmMY5PUyrQ2LuzbvQfnQLZfcW5EFzhZ/e3YVyOxPeqJVlkj9/jND/Og9IeAauWB+YM41y6yreDzxD+hd+neSvfhw895+NcS9WunR6uWQFqqmpKdfhLzt21qMuV6gflIdcKfkTipqm4XQ6yyp6+cIXvsDv/M7vLCtX//XAFuSHgNXKHKV+vPcS5FgsxsDAAOFwGJ/Px/r168tabqmSkMW9YshCCGZmZqj67leQE9GC75w3jlP/jsOIaBqGCrczmptw9C1NzElC0DRxGeNQK6kmPwya9xWrc1N1p8e9ZBi4bx7HebCVxJrdSC//s8le37EdZfBqzt75d3+M+sp3SHz0c+hHnrvn+S+X18NDvhvZBkX5yLKM3+/H7/ebbIuFOpFIAJlQ2GoKfUCht32vXOpvfetbfPzjH1+R/T4MbEF+gGQzJm7cuEFXV1dZr1GlBDkcDjMwMEAikWDjxo3s2LGD0dHRZaWo3Y179WuYnp5mcHCQWpeDHSf/scQYEZT5K+hv2490fhR5KlOLZ+zcjHrVXCBitK/DPXwC/Wd7kc4OIs9k0tv0devwj14z7yAwg8N7lthze1FP9+NcDC99V++CuUJzeWoE57f+EHH8f5L81U8jagvLwldTyGIlsyPKPZ67CfWlS5fweDwmofZ6vXi9Xvx+f24y8W7HvZKCXG7RS3GP6kcBW5BXGKtijmAwWPYPtlg4FxcX6e/vR9d1Nm7cSH19fUFhSCXx5kqr+rIIIZiamsoIcW0tPT09+L79P1Aj5rCEsXkHynAmlKAMnkOs9aPtOoJy9ipKvzltDUBKZzokKwOvItZXo+0+gvqDk4ju9UjXzN3h9J5eHAOncUQWSHf6WHTtpOb0FWLr2vDeMu9DuNwo49eQbh5HPfd9Eu//FNqzv7ysFLlSrNSCratJ2LMORHNzM668hQMMwyAejxONRolEIkxPTxO/U5Xp8XhMoY/sfbpSggyViawtyD+F3K2YI+v1ljOpkbXN9pmQZZmNGzdSa9EbWFGUkqt7FLMcQc6v7qurq2Pfvn2ZohIthdz3HRIb2nEPF2ZSUOsu8FCleAR17CTau96EdL0PZbAwLqHv3IsysZQCJ8VCqLGTaP/uINK0ucuFkEAJT+b+7UhGqUleQX/zLlS5Bs6Pm7YJdW+mZiITn5YiQTxf+HW0f/07Er/+eURbl8l+OawmIc0ez0p52sXj5JeE55Mv1NFolJmZmZxQZwteHA5HgVA/SDRNW9GHwMPAFuT7pJxijnIn6rId3Kanp6mpqWHLli2mGfR8HpSHLIRgfHyc4eFh6uvr2b9/f4GHJJ/4W+RbP8LlVUj+zGGcJ15DSiQxNmxCtsisEC4PytRZ8EbR3ngM5UdnkFJ3HiRqiQdKrQM5PIr27DGUV04h3TlPfe8B1GFzdzgpsYDD2WeyF4qCP2Yuw1Yv/Qj9T36F6NanSGx5nlgsdl8isdpiyLByk4zlitrdhPrWrVuoqmoS6lIedSkqeRNZXFw0tVxd7diCvEyscohLTdbdS5CzsdmhoSFkWaa5uZlt27bd8xhWWpANw2BiYiL3KlosxBkjHeW7LwIgCR3XxCnEvvUYsTrweOBmv2lcfXcP6nAmv1gdO45xoB09VEU8GKVq5KrJXvirUcZfQ9JSqOPHMY50YgTcKFevI6WD1sfe2Y46OJ6xP7oRY86Bcv0mek8v6sBpk73etoGa6UvUTF+i+tJ36X/uvzJU15Xz5LKx0XK9uZX0kFfTK/ZKHE8266OhoYG6urqCsUt51KWEupI3iFAoZPlmuZqxBblChBCkUikCgQA1NTWWqWvFqKqay7DIxzAMpqamGBoaora2lj179uQa0ZRDJd3e7ibIhmEwPj7OyMgITU1NeL1etmzZYj3Oue8gT94q+ExaGIUmA9HWjpisQQotxZaFqiIHCkVanh9BBhK7D+G/Mo8UDhd8r2/fhTq0NPknz2XCHKmfew7H2RMUY9Q2oIwueeby7ABCktCeO4o0Y15oFUCsa4XBTA60PzDE7q//GulnPkDi3/0ucclJJBIxiUQ2NSx/IisrVivpIa9UyGKlWInzsooh382jTiQSOcdgdnaWWCwGgNPpJJVKMTMzk5tYLHW9gsGg7SE/ruQXcySTSfr6+jhw4EBZ2xZ7yPkC2NjYWOCJxmKxBxKGsLI1DIOxsTFGR0dpbm7mwIEDOJ1O5ubmrAcRAuUfP2v5lbGhA2X0OGJbPbp0EOXUGQD03b2oY6dM9nrbJpoipzG2N6KLrahnMmEI4XSjzFpkVgCKOoHY5UVPb0E9tyTAxpatqINFBSJCgDeJ1BZEb9qHcu78kn1jC8rwqyZ7x7/+OUrwHOrP/F94t73NdK2yqWGhUMhUvpy5PIJEInFfTetXKoa82qhkUi97Tb1eb0EJuWEYLCwsMDw8TCwWKxBqt9tt8qjtkMVjiFUxh9PptPR4S5EV5OzSOmNjY6xZsyYngFa2lYxbDvnhDV3Xc8fR0tKS63dxL6SrLyMPnjd9LmoakScygirFFlA4g/bsHuSrs8hx88rUAKKlAcb7kWNzyMyhv3Ef0sVxjA2bUEfMXrC+YSvKdCZ7Q2YG/Y37kS6MoIejqBPmvhgAkhJBDkwD0+hvOoB0fhB5fg5j0ybUQXNcWd9+AHX8DN6vvkB6xztJvuOziOo1uetnlRqWLV+emJggEolw8+bN3OoiWXHIetTFf2vL67JCnvZqCnvAyqS9ybKM0+nE7/fT0dGR+1wIURD6mJub48UXX+TcuXO43W5+7/d+j+3bt/PWt761IoFOJBI89dRTJJNJNE3j53/+5/nkJz/J4OAgL7zwAgsLC+zbt4+vfvWrZf1ty8EW5BLcrZijnOKJYiYnJ7l9+7Zln4l8KhXZSjzkZDLJ0NAQ4+PjrFmzhkOHDlkeR/b8TD0Vrn0B/cknkY8fR8rbr+jeijz6kwJbdeYi2pGDIFSkk6MZj/UORlMbykShh6pMn0d0VCPanIhRkIovb50XJvPtzyE2+Al499B88yTF6Jt2o8wtVf4pU2cRndVoO59A6Tc3vweQxFJ4w3H1JdT+V0i++ZOkD/wKlPBas+XLdXV1uFyunFDkL6w6NzfH0NAQ6XQaVVUL4tM+n6/gYbiSk3qriZVKe7PKnMhWGuZ71H/5l3/Jl7/8ZWZmZujt7eXatWvEYrGKBNnlcvHDH/4Qv99POp3miSee4K1vfSuf+9zn+PCHP8wLL7zAr/3ar/EXf/EXfOhDH7rvcwNbkE1Yrcyx3B9IKpXKCWBdXV3JPhP5PAgPWdd15ubmmJubo6Ojo6QQZ8k2GCpoBjNxGmXo+wAYR7sQkypy/w10tx95ukR+sRxACd5GP7YZxgyUob7M9u3tqGPm9DR9w3YcgVfQn9gKQ0mU0UzsWG/tRJk0e+YYaeq8V9Gf3A6DMZSxoaXv/EDRSlRSMgTdAqNpE/RHUMaHc9+lu/bgmL9YaJ9YxPnyH6COfovkMy9iNGwtdclMoYZSC6um0+lcfHp6eppoNJorBfb5fCSTSSKRCD6fr6w0yVLHstomGFdSkMu9LqFQiO7ubp5//nmef/75ivclSVLujSidTueqFn/4wx/yta99DYD3vve9fOITn7AFeSW518oclZJIJBgcHCQQCNDe3k53dzfpdLqsG3KlJuqgsBVnTU0NbW1tbNy4sexx849XObUUO5YX+xA+mdiRXogqeKfNWQzaht2owYyHqizeQlQraE8dQ74+iDJpkRongaxlwhtK8AaiXkXrPIZy8iyitQnGzXXVevc+HPMnIXgN0eDI2J86g7GmE2XK3BdDOH0owctIqRCi0YG28RjKydNImobkTkPYtAlGx1bUsR+j/M0xUgc+QurAR0B1mQ3LxOFwUFdXV5BtkJ0ozor0xMREbi7B5XIVeNRer/ee99FKCvLDrK4rh0oF+X6zLHRdZ//+/fT19fEbv/EbbNq0idra2twxrFu3jvFxs3OxXH6qBTk/h/jWrVu0tLRQXV297Js5FosxODhIKBSio6ODrVu3IkkS09PTuZLTe7ESIQtN0xgZGWFiYoJ169Zx5MgRAoEA8/PWGQdW4+aHZKTZqyh93y2wkYSBO36JdPt6jLoe5BtFXrLHgHi+vY4aOE76qZ9Bnp5DuV0omPqmXtTQUihBMjTUwHG0p/YgWVw7IcvIiaE8+zRq8DjGgQ0YznUo12+ZttG79qLOHS+0P9hBMNhA3axV/nQVSuBizt51+tOot/6e5Bu/iN52pND2PkINkiThcrly/2VTHoUQJJPJXLbBwsICsVgMwzByaWFZsc5PzVvJ8uvVVlhRboEVZAT5fif1FEXhwoULBINB3vWud3H9+nWTzUqGmH4qBTmbQ6zres6byApzuRc3vwl4JBJhYGCAWCzGxo0b2b59+7IKQ6Cy3OLicdPpNCMjI0xNTeWEOPuDqrStZr6tcvr/tbRLrNuDN34WVDCOHUK6dBspvECkfgP+BXMnNsNVjRo5jeSKoh09hDh/FUciE1eQ5JD1wdR5UQIn0Y4eRrlwDSmWsdO7DqAGzZ65UEFVX0E7egTlwtWcvVAcyFGzSMvhIdQNbtLdh1AvXkOKL7nJetdu1NnC7A0lcAvX8Y+id/SS3PtJcGY8sAcR+5UkCbfbjdvtpqFhaYmq4kms4vxdt9udWyF7uc2AYGUzPlbq2miadtclyPJZXFxcsTzk2tpann76aU6dOkUwGMx56mNjY6xdu3ZF9gE/ZYJ8t2KOUrnCpVBVlUAgwMjICJqmmfpM5FOpIFeyyCgsNaefnp5m/fr1HD582DLnc1mhkNAg8o2vm2yErODUl2Kw8uJp0p1+0ukePJKAuWHTNuHGjdSkMxkRaug0qc1VxFM9qLE0jqCFgPsaUBbP37E/hbG1AT11APXKWWTDnCUBIJpbkQLDqKGTGNsa0ZMZe727F3XBYvKvYSNVqRuQAmN7E3piK+rVswjVhRw2e0MA+BWcfV9GHfseyd7Poq1/50OdjLOaxIKlQouFhYXcclDxeNy0sojf7y8rNW81esiVhCwWFxcLQkOVMjs7i8PhoLa2lng8zg9+8AM+9rGP8cwzz/DNb36TF154ga985Su8853vXPY+ivmpEGQrIS5+8lciyIFAgHA4TH9/P93d3ff8o1cq9uWSSqVIJBKcOXOG9vZ2jhw5UtKjWW5sWh35Q0TvNsRQGHlmSWRF50HUcKHAOYwISuMioqYK3WhDWViKrQnFRbVUtIKIEQb1NRY6D+AdacQdLcx/1tu24AgupcDJqXlk5km/8VkUiwo/o6oFJbgU9pCTmZQ67Vivqfgkd1yNjRAYuGM/iyzNoh/dhxGtwjH5byZ7vakbJZh5qMiJaTw/eQ/ptrehtH0Ew3vvNqgPkmyhhRCCcDjM9u3bgdJLQCmKUtCxLZualxXqlYohr+Tk4MMU5MnJSd773vfm3qR/4Rd+gbe//e1s376dF154gd/93d+lp6eH97///cveRzGPtSCX0xA+y71EUwjB/Pw8AwMDOJ1Oampq2Lx58117TWSpxEMuh2z2xuzsLLIs31WIs2SbjZdDLq0vOYU89hUkI4locaKvewL54inQNfT0KJZ7rG9GCZ9CrHOjtR9DuXgSSRjonftRQ+b8Yq1+M/XKWcQmN2lxBPXKKSQh0BxepEXr7A3ZNYbUHkJrP4py6QTZn7qxdhNqwMJz9kvI3kG0pqMol/Psa9YWCHhu/PAFWN+B1lhonzm/GggU2jvGv0ersUCs6Y3Q+hGQXl+vsjjUUGplkWxYIxKJMD8/z8jICKlUClVV8fl8uT7I6XT6vhq8r6SnXclY0WjUVAVYCbt37+Y1i4V1N27cyJkzZ5Y97t14LAVZCEEkEiGZTOZurHs9oVVVJWmxkGa2Gfvg4CBer5ft27fj9/u5du1a2V7vSglyNo94fn6eDRs20NXVxalTp8qK81W6eKlhGCiDf4xkZK6JJFIoqZ+Q3rWexflmGhMWk2BVa5HCmRtVMhKoxnH0/ZtgyomcGrDcl6jyQjxj7+Ak+v7NMG6AvwV10dw7OVLdjT+eWUdP5QSRHetxzsiokUWUkEVqHCCpC0jxKCon0A9shZEUyvQAxpoNqAFz4Yre1ouaOAMM3LFPokwPYtSuQw5YZYgouOjHP3UKPfZPJLZ/EcO/w/riPgTKjf2qqkp1dTXV1dUFn6fT6VzGRyKR4PLly2ialluANT+HuhxvtRKvdqXGyt7rj1rV42MlyPnFHMFgkEAgULInQzHFHnK29eTw8DDV1dXs3r07VyILGZF9WIKcTCYZHBxkYWGBjo6Oey7XZEWlMWSRCqCMfMn0nUMbpW6bF10cQ7p+ATm1tGKI0dyJEi4UOCXRj9Z9CCHVId1YRNKWUi/injY88QtF9rcQzV70+jZE1IWkFT4kPQ1eyFukxC+NIloU5luPUDdzmmLfKd28B0d8Kb9Yid9ANKuk170BddpawGV51mSvtR2DmIG6YNGfua0XRyozwaiEXsV7+klSG36TVOdvg+Ix2d+NlejTS6YZAAAgAElEQVRBcb+Tcdm4aSqVKih2yabmRSIRJicniUajudS8/Ph0cWreg1ot5F7cT+rq68VjIchWxRwOh6MiEcwKcrbj2fDwMA0NDfT09FjO6qqq+kBKnLPHbxgGqVQql8/c2dnJli1bSi4AWc5KJJVkWXinvoykmeOuRsNelOQF4CZ6dxPR+U58U1cQngbkqHUFnKxMI6eHMLa1YgS3oIxmRDhdU4/HsCgQadqDmv43jG1tGIFGlLGMoOr1m1GiF032KG4a6i8hmlrQFupQxy/nvkrKYRxFpy0JDerTiLpajLkOlIkle711H0rivMlelm8j1vnR3TtRJgonIGV5ymTvGnoR2bhFuvWD6FVvsLwuD4oHlfbmdDpxOp2WOdTZYpexsTGi0SiGYeB2u/H7/bn7eSWOq9zmS6uta165PNKCnL0ZrIo5HA5H2c3bs9suLi5y8uTJgkY7pajEQ670xpAkiWvXrhEOh+ns7MzlM1thVcRRyq7sTA/iqPPfsv7SbcCdtGDFmMVXN4ux5gAi5kcJ/qvJXGvcj5rOvObL2iT4J9F2H0SaWKTKMDcREpKMLIbu2I9D1Tja7kPIt29CbbWp+g6yAn4CyQghV4+hNRxGuXUNw78Ov8U+NNmLlHwNWYpDzSgB53aqJ0ZRUmFwxSBm3ofR0I2aOA41oDUeRrl5DSkZQlu7H9UifGO4m1Fj/4Sj7x9J1/8HEm1/AGqDeeD8c19lTe7LXSopmz9dnJqX7dg2OztLJBLh3J2GUPmtNf1+/32l5pUiFAqVNb+z2nikBTm/t0TxH7TczIZsEUW22ubIkSNlvRJV4iGXSzweZ2BggEgkQltbGzt27LjnjVqJIN/LQ84u1aRG/wpn+zha8ijKYP4k2FbkhLkCTtJvILX5MeqOIg8WTtxJzggUhebV9Bm0HU8RmXNRNVtUINLUi6qdKbI/jb59G0JSTIJsoCCL20X2pxBd9RjONpQxi85xLftQU0u9N+o819A21bIQPkR9zJzbbDiqUVJLkztq+hRGdwN6ZD8SYbC4zYz6btRkJgbuWPhrlNA/kWz7NFrdvy+5dNTDFNJy0HV92U1zJEnC4/Hg8XgQQuB2u+ns7LxrD+TijI/89qaVEgwGH7leyPCICzKUFpp7ecipVIrh4WFmZmZoa2vj4MGDXLhwoez4VKlJwOWQv5L0xo0bSaVSNDQ0lHUzZsMh95oFL3vx0lof2+tfQpHi4D6BsXMbYjyJHBiAKn9B9V0Wo3EPivETJPckxs6dMBJGDg2j1+9GSZoF3HDUo3CGqsYEqYYdqOOLyOFMbFZWZsHiOSdqalD1U+g79iANzyJHMrHqkG8btZpVEUotDv/30bfvRRqZydkL2YWsm0VaJUjN5iS66EEamkSOLoUhQp6N1BqFsW5Zn8dobgWfD2O4FTmy1PlIOKpR0oWz87I2h2f4V0mLl0nW/FeE2mE+x1XmIa9U7Dd/nLv1QM5mfOSn5uXb+3w+DMMo6zqtZFHIw+SRF+RSlBLq7ATZ/Px8Qe5uNle5XCoJWZQiFovR399PNBrNrSSdLbV+WIuXzszMMDAwQE1NDT09PXjT38AxtyQusn4dsUZFb30j8uyPTWMYOJG5lmd/BdHmJN18FDkezYU3Crap34YqMt6jU7qaSZFLHYPFJGrKHIc2nC0oeuZzxbiIWJ+xVwZO4XaZ192DTEtQ9AEUcQGx3pPx9gdOojfvQ02bC0Q0bxeqnhFd0e5BSx5DGTgBsosa97DlQyIuJ6kyrqCvcxFY2E3tzGUkBHrjTtSURQvR6t04En+LmnyJZNXHSfs/BNLST3C1ecgr2X+inDe4Uu1Ns970/Pw8yWSSs2fPoiiKKeMj35t/FHshw2MsyMVP0Hg8zuDgIMFgkI6ODjZv3lxws1XqmVQassgvtY5Go7kqqo0bN9LY2LjsUutKJuuyCCGYnZ2lv7+f6upq9u7di8fjAWGgzJnLpCU0aIwgmtYgpqqR55YmwSK+7VQXeY8SKeSqeVibQp/YihK4sbRvxY/CpSL7BIrzONrmJ9BnNqMECkucjfou1LzKPIkEqvM46Z6n0af6TCERw702J+AZ+ziq6wT6jq2QTJnyiAGorgUj3/44+o7NGJH1OCIvm8x132aqXJnjVKQkdQ2XiFd3oE8quJKXwOp28gE6SCKGO/RxHPGvk6j9IoZzb+barKCHvBJpZivpIZuWAisTRVFyqXl1dXUkEgl2796da2+aXVEk297U4XDwyiuvMDY2hqZpyxbm0dFR3vOe9zA1NYUsy3zwgx/kN3/zN/nEJz7Bn//5n+cqJD/1qU/xtre97R6jlc8jL8j3uoEjkQiDg4NEo1E6OzvZtm3bitz0lVbfKYpCKBRiZGSERCLBxo0bS4YlHlTvi3wh9vv9S0KcHSv2HeT0TfN2jlYk6SwSOqwBveEo8sAVSEdwe6w7XQm/H0U6h2iT0BqPoAxeQtKi6A17ct5xPrp3J6rjJ4g2mXTDUdTB15D0OEKtQREWTfEB2T+Gp2uCdOIo6uB5JCPjjhu1G1ANc36x8FUjN55Dqz+GMvhqLsc6LjfjNswTc7LRDx0RtHihPQDV1Sav2eMYQtt1FIw1iCL7mNqOVy98ECnpi3jm3kq6+QOklI9hGPKq8mxXMvSxEg+I/JS3Uu1NU6kU4XCYvr4++vr6eP7551lcXOSll15iw4YNZe9LVVVefPFF9u3bRzgcZv/+/bzpTW8C4MMf/jAf/ehH7/t8LPf7QEZdBYRCIWKxGFevXmXTpk1lx2TL9VIqEc1wOEw0GuXGjRt0d3eX7HmxnLHLnaybn58nFosxNTVlyqkGEAg0voOK2bkzajYi53WHlx0nEFsaSYbfiDv+L6b9Gc4OFDkjcBICxXUSY0sLxtw2FG5k1LQYn3LH3kB1n8DY2oqY2QKSz1rAfXtQpEwKnOo5gbFtLWKmETk4jmJYC7jkmUVCR/Ecx9jWhphpRJ29SNLbggdz6EOv7kXhNHgmMvbTDahzlzA87ci6RYEICpKrH5npAnsAV1Mz6COmbULKJmrE5zFif8fC7O+QSOwiFArh8/mW7Z2u5hjy/VBODrLT6eSpp57i0qVLPP3007zvfe9bVm53a2srra2tAFRVVbFt27YVbbNZikdekIuFLRgM0t/fjxACr9dLT09P2TPF5WYsQHkecrbfRTqdxufz5ar87kUlYYi7ibcQgoWFBfr6+nIz3rt27bK01aUfEa//Dsmqo3hGBnGlMwIslAYk2Sw+MEd03RgL4R5aAlOoybxJreo1SAwVWMvSNPH1m0nL26gav4maXiq+0N3dOXFdsp/EaAkRdh/CP9mAmi5qHerViuwnoGWCaMvbcQeOo6YL4xi6fx8K5/Psx6FlnGT9U7gClylGAJJ7qtB+zTha/SFEVMWRNotr0tuDi1dN9tJUuKSAV9VnHgRu5xRb2n6LGs+zTE59lMWQqyCX16rNZilWo4f8oFYLKUUoFGLz5s3A/XeaGxoa4rXXXuPQoUMcP36cP/7jP+av/uqv6O3t5cUXX7yvfhnFPFp1hSXIeoBnz55lcHCQrq4uent78Xg8FXdwW4nqu1AoxGuvvcbNmzfZsGEDBw4cwOv1PpAwRCkPeWFhgbNnzzI+Ps7OnTvZvXv3XcunU+rnAdAclwh1ppmvO4KBjFG7HcliZi7pOgzKCK7a2wQ7DKJrnsZAQjjWIEkWE3MoJNyT6O4LBDeqRJqezDnKwm89Gx7zHEDzniHY6SLa9ETOXvdsQ5HMzYV0qYpE3VmCnV6ijceWxgfwWCQXA4kahcVNLiINxwocd92/HxlzxzrhHmaxc7xgfAAhQPGa+2goztNEtqwl1njU9GKgVx9AprCoZE39y+zo+m16DvfTe6CX7u5uqqqqiEajDA4Ocu7cOc6ePcu1a9cYHh5mfn6eRCJR8HddbUK6kuM8rMZCWSKRCO9+97v5/Oc/T3V1NR/60Ifo7+/nwoULtLa28pGPfOS+95HPI+8hRyIRLl68iMfjYdu2bQUeaKXFIVlBLmcCwkoIFxcXc955dmWBLA9q8dJi24WFBfr7+3E6nSaPvNRaebp0Hl1eyqCQ5ASi5TKBhsN4YjE8Rc8oAcTdqaUPpCjx2leJuXdjTLpolgpFBmA2tRNV7r9jHybR8Bph126q5iW8spWAO0i67giiHCLecIFk1UGqJiaRfNblyDF3L0hnQYJ440WS1YeomphAkmtQJHNqnC7VkHZdRZViJJoukqo5TNX4GGpyDDwWFShAzL0N5LPEG+dIVmfsHckxIuoO/LL5IaFJLaTd50h70rnjcSRGTR54PnFPA5rzw0iOb+GWP4PH02lqs5nNPAgEAoyNjeUWVvX7/bkloIrX66uU10NI70alnd7uN+0tnU7z7ne/m1/+5V/m537u5wBoaWnJff+BD3yAt7/97fe1j2IeeUF2Op3s2rXLFBOFyifeKrHPF7VsmESSJDZt2mQ5q1upyJab45x9MAQCAfr6+nA4HGzdutWySilrW+w9pZTPW46dkBqZrbpCbeodNEa/j3LHU045ezAUc7N3TZkj1FEDqbfTEP0XFDKiLQBHk2byEB3+AQZTB/CJN7CBnyBLS9c+7joMcmEur+G8xsK6rZBupjGuIOfNqhm4Sbn6iuyvsrjBBdFe6lLXC+wBYu59GQHP2V9hscONuvgOqoz/z3R+ulRL2nXFZO9cfBo5ZvamAWKeHSCdyTseN67FZ3BHIqicNdknjAY0RyZ8I9SfkPI9i5L6MErq15DIiKssy5bd27JNgcLhMIFAgOnp6ZyDkb+6iM/nK7v8+GGlvZU7TiXN6e/HQxZC8P73v59t27bxX/7Lf8l9Pjk5mYstf/vb32bnzp3L3ocVj7wgu1yukjeNw+F4YIIMmRvk1VdfRVEUuru7TV2z8nlQqWzJZJKxsTF8Pl9JIc5i5dXr0k005XsmW8NwEpVHQNIJui4RceylJWbgT50h7rF+g9CVXoR0gYDrMmF1Py2xJP70eRLOAwjF3Ow9nW7AUTdCStLoS/XSGFqkXr6OISTC8hhWvl2cNSQ8l4k4j9ISDeLTMvHfmPsgyObJPM3oIOg/z6J+lJZIAJ+eEVQDDynXbZM9UoKFqiTzPEFLZB6fvuT1xjw9BQKetY9W6QSdzaxLuvGLpfPMeOBXTPaJ2rMEvM9Sn9iVO/4si8pmVOlygb3u+n9IypdxpP4TTmO3xVXJkG0K5Ha72bhxI16vt6DXRHYZqGg0mptjKV4GqvjtaSUyknRdf+jCfr/LNx0/fpyvfvWr7Nq1i717M2mJn/rUp/jbv/1bLly4gCRJdHR08KUvmRtw3Q+PvCDf7YZRVXVZIYt7kQ0LpFIp9u7de1chrnRsKC+GvLi4SF9fH8lkksbGRrZuLb0icv64xYI8J/8vXKIRRSpsDh+K7cXw9+f+rcmTjPvBn3oBVfqJ6cYxhI+otOShasoE41XgS74Ntxy0TMsNJ7eAI5NmJ5yTzDZIJFJvpzodwuExi2si0UTcdQUJSCsjjFVJSPNvYKN8nqTbnOYGGQGHuYx9tUx18u00x14m7j4EFqESTd9KSskcU769hETKZU4JzOzDg+QdYdyzZK8QN3ngS/vYS8x5gZgja/9DFGKkqUKu7jPZCyAqz5D2/gd86f+N6uR/RqZ0n998z7ZUr4n8EuZwOMzU1FRudZGsSGuaRiqVWnb5dO74y2wIdC8qCX2Ew+GyfpeleOKJJyznW1Yy59iKR16Q78ZKC3K2QX02LHDlypWysiZg5WLIoVCIvr4+hBB0dXURi8VyvQDuRbEgJ5liTPl7ZDpoMfbil3+ABBhCIe0MWIroaDKBIW1kjeaj2vnjnE3aOIRQzJ3YIipMSB5atCfxqUv2hqhD9w4V7kMShFyXmVWOUqd78CmF6W66uhNJulRgLxqHuLjwFFXxMA2+wpisbrSTzI/rSgYh92Wijj24jSqsItFxmoHZQnvnTmqT7UjSP5vs8wU8374x6kJ3mcM6mX14i+y30xR1oAsFWblgste0XaTVzMMx6vwaCfVlahK/i0d/2nL8ckIN+SXJzc3Nuc+zlXGRSARN07h69Wqu4CIr1NkWm5W0wVwJKokhCyFW3fJT5fDIC/Ld/tgOh4NYzHp23QorQc6mjvX39+dWBM6KcNa+HA9CURRSqdQ97bK2xYKcTXY3DKNgwjCZTC47I2Na/RpC0tEJMyGF8Rtvo1m6hp5uRXKaxSQZX4NRNQrAFFFixs9SkzqLx7VIVB613GeCKjR5mnEZqvSfpUk6h0OeIqn3IKkWrTT1PUTVW0SBav3tNElnUOUZDKOBuHLDZC4Aoxrm1CiR2HOscbyKy7EAwHx4DXJNv2mbtGhnxtFPjfYOGuUTqHImpU43NpCULbrDSQsMuGrxFdlDkYBnT0GeYcJzDGGsp5FCe03fsiTgOftZpvxuEvE9NGkjuNSFgu/jUg0wkWc/zYL3/8SRfB916ffgEIVd5O4n9ptfGTc+Pk5PTw9Q2At5fHzc1GIzv3Pbg2oKX2lz+keRR16Q78ZyJvWyk2lCCObm5hgYGMDj8bBjxw5TQ5RsP4tyBXk5HnJWiHVdp6uryzRzvNzFS9MEmJX/seD7iNxPVFTjER24RB+yVDiu7NoMeX0rQnIfi44Wmo3n0OV/MnvUehcxZekVPKz0ERWtNGp7MeRS3uPStQwpt4mINpq1PTjQwULAY6EtpKsznnHCO8yo6KRZ68ErX0GutkhbA4JaAFRYVG8RNjrwBbtorT1NwtgIqrkZktB70dQ+FuUFIqKDZm0vVerLGCUEXAiViDyLlrPvoUrNvH1kQyjFGPp+ot6bxPR21mj7cva63kXKIv4uhIM5x78x43yZ1uT/QX36Z5HuZLGuVN/hfEr1Qk4kErleyLOzs5ad23RdX5Gy8HJjyNleyHY/5NeJ3BpwRSwn7S2dTjM7O8vAwABer5edO3eWXJer0ib1leQ4p1IpLly4QDqdpqurq+SM8XInC6eVryMks8euGluYcA0gR/fTzAw+3x1REy2EJIuyajnNCAEc4g00inEUeWmiLEELECywN6QY8zSSFD5qU/24nEuetdC3ElcGTfZTyjzC2EydsQFFzltoFUgU/UB1KcKkGsGb/lmcymsoRQurivQ+hHvpM0OOEK6FSPA5fJ4pXEW/CCEkovJSo/7s+CH9LVQJGVRzu86sgBfbN4oAScWcGieEQkTOiLRQ4kwySkh/Cy3STZKiDavmG5l9ZK71uPtzBNUf0Jb8CG6jfUXEr9xeyNmCo+K0vGzntkAgQCqVMjUEyv6/krS8cmPIkUjkkeyFDI+JIJeiEg9ZCEEoFGJiYgJN00qm0i13/HLFOxKJcPv2bUKhED09PdTX19/VvhIPObvQqU6UWeXvTd8LIJhOgAKGL8CUcNFivAWf9Aq62AoWebap0CaSNXMkJYgKhRbjzXikHyIZa4haecFCJSTPkpaDRGQvdbE3UOc5jiRpJKnDyntU9B3Mq7cICS8t2pvwKv+KJGmg7wCfuRhDElVMqX2Aj2btjXfs9Uz+dAmNEd5mxtUYtZFOan0ncm8Hscg2UlXmfcSlEFOyk6a88SEr4OYc5qgySEw7hEvvKLCHjLhmY8T59sN6Jw6a8YobmfPN2guZiFz4oIuql7it/CoNyd9ASM0PRZBLkd+5rb6+nmg0yt69ewsaAk1PTxONRnNvmPlCXbwEVD7lnNfi4uJ9Tei9njwWglzKQy5HMPNbUHo8HmpqasrOLVzJYo/8DnCdnZ2kUql7ijEsL2QxIP8IzdiCpBSW86bD60lX5fV0kAympT68xlFcwuzJCCDpyBMKSWdK6sdj9FKjN4ByyrSNou9ZEh9JJ+AdJ2EcpElTiDrMr+YIB4vynTJuKc2UOojHOESzESRNFWAWS1nfhqFmHgZT6hBe/QhNzKOiklDMJc962kvUMYKQ0gT8UyT1YzQxi6LcwHBbe3DxaBt61Uhu/KrEGH7fEOj7SKnmUIlkNBBU+hCSjtc4QpOxgKpcu5NBYT3PYYh1TKk3c+erZr1rvZe0al40VhI19Lu+CgdrWZRqqNG3W45bDg+iSs+qIVDxElCjo6PEYjEMwzCl5ZUbG35UW2/CYyLIpbibIOc3Za+pqWHv3r1IksTVq2YvcDnjF1NKkLM9kWOxWK4JEkBfnzn9qZJxS9lqpLiqfo+EtEiL/jN4xTkUdTHzva8RMK+jp7GGEaWfNcaz+KRXkaSMvVPfhfCaJ/PSks41R4Bm7WfwKWeQpIzHKIRERA6Z7OPyDKPGflStFa9yGklaWsVU1ffkXv+X7KeZ0NtBNOLWxlDUvNJu4WJRKWwCE1OmGBYS1VoPkhxEkgqzUhKRDRh1k3n2kwwLiabUu0k5zphj40YVibyMjpgyRdTrwLt4DLcjZvmrkowuhJoJ+cTkzPE0aW/GT5CUap54lI1aAspA7nxz9spxYhahJgBhbMrswzfPRT5Oa+rNdCb/d9S7pMiV4mGVX99tCahYLFaQlheLxXj11VcLln/y+/04HA5Todaj2JweHhNBLvUaU2pB0KmpKQYHB6mrqytYxFTTtIpbai7XQ86uEhKJRNi0aZOpJ3K5VOohT3hOk7gjqNPKMIreRru+A6c0SVAeNG0jCQ+zUmaGf0oewik2sM7wgnyKeKnjNbowlH6m1GFcxibadCeSehaHvotF1eyhKkYjM8oIQjJwGZtp02Vk9RxCyITkBYsdQFo0MacOoSS7aRcKiiOTLqbou3Ox1XwcRjsDjn48xjZaDWMpvUy4Sfst9iEJJhVIiiJ7QDa258Q1Zy4JUh4/07KHppgfj3fJ2zc0L4vF11YSzKj9zGn7qdKrkZXCqkSMLYX7kASzaj8R7Y2oUgyFsaLhqlksir9POv+ZOfUMHYn/xBq9B8kykdGa17uPhSRJBWl5hmFw/vx5enp6ChrWj4yMkEqlUFUVv9/PzZs3c/M/y6FUH+SFhQV+8Rd/kaGhITo6Ovj617++ok2FsjwWzYXKIbua9MmTJ1lcXGT//v1s27atoBSz0tWhK40ha5pGPB7n6tWrXLp0iebmZg4dOkRTU9OyY34VHbNkMOQvXIhUV5IMKtPMioPIos20idPYQTrPo0xJUQbkWaL6O4hJ5vJuWfiZlZfEIilHGFAXCOpvIGKZ+QsYHYg7MdukHGJADbKoP4OiHyRlIciKsYY5JRMW0F0xBh1hQtozYDQRls0xaIAUGe8rLi8yoGTshVGHqu8Ch/k8HHoHQWXMZC8JNyFlzGSfOXY/KTXMuDeVsweQtK0I2cKrjbUzqw4xoIQIac+gp++8ZgsfixahFYCwJDOoLLKoP4MwljxKWd+GYeE5S6KRk94/5ZT7T4hJ1g83K8rtengvVrr1ZjYtr7W1le7ubvbu3cvBgwfZtWsXTU1NzM7O8sMf/pBvfOMb7N27l3e+853MzFivKmNFtg/y9evXOXXqFH/yJ3/CtWvX+PSnP82zzz7L7du3efbZZ/n0pz993+dkuf8HMuoqQgjB2NgYIyMjNDQ0sH///pLNg5azaki5PSeSySSxWIwLFy6wadMmtm/fviJpOZUsXnoz9Qpp16Lpe5fRwm15GIVaOvVNKPJPQDJAqMyVELg5SWWBJhoCa6ipvYQkZeJ7qtiGLpvjm3HcDCoGHdpRJHVp4VS0KmYVc9hjRhlhVt9OvVaLpJ4q8O2EsQ7UQm9wRh0hrh3BQQpJLrRX9VamlGGTfdDYQC3NCG6bfMcU9eSHb2bUEQKijbVaO2nHCZO9Hmkm6B+zsN9KwmWdo51SqoFozl5Kt9KwuAG/KqH7zHFoh97J3J1wzKwyQkCsYZ22BVV5jcUSD4k4PmCBSccFZtUb7Ej+HBvTT+dS5EqxUuXOD6sXssPhoK6ujve9730kEgkaGhp4//vfz/DwcEWebKk+yC+99BKvvPIKAO9973t5+umn+cxnPnNf52TFY+EhWwmbYRiMjY3l0m96e3vZsmXLspeSsaIc7zSRSHD9+vXcAqqHDx+mufn+Z8Gz3E2QsxOWp06dIhAMkNpiLqwAkFgLkkCXNPqUaWbEYZKRNlxiN0nJHFP2GOuYkSfQpDTTdQnmxDFkoxOEi1mLTm8AMVykpRS31Xnm9SeQ9PUA6MkODMn8luHTNzOvzHBbDbCgP4lkrANAMeqZtfAehZCYl1PcVgME9CeRjCVvX7fw/CETxriuzjEe3o9krM19ruqtzCtmQUyT4rqSJKA/VWAPENfNP/q0FGeONcwaPSZ7h76esGuy4DPhSDNXlWBYXoeWbKaYSKowFqxJCYbUaYLaW9EN8/5VvZ1A3sNOkxJcdH+Nf/N8lnl50mSfz8NsKl8OlRxPOBympqYm129iuR3v8vsgT09P54S6tbW1Iq+7Eh4LQc7HMAxGR0c5efIk8Xicuro6NmzYUFE9frmzuXcLWSSTSa5fv85rr71GXV0dhw8fNk0+rMRxlBpvfn6eM2fOMDMzw969e1F2KASMToRe+EByiHompcJJsIA8z5C3ihmxAUmYH2A6jQX/XpBnuSG5SevPkRIWed+RJuaVpWyIeWWWW3IV4ehRFl0WObZANO/lbU6Z4ZZUTVp7MjNxJZkfgn59M2E5M9asMsNtqRZNexLZaM6FNwr2ISQCcubtJloV4bZUh6Y9iRBySQH36duIyxFmlekCe9VYQ6La4k1CKMzJ4TvHk7FHZEQlLcyCC+BKbWbOM8egs5F0nr2cXkPYYyGihsyQFOCWXE1aewohlkRLE01meyCJm297v8hZ5z+jYZ2nv9p6Kj/s1pvFfZAfFo9FyCK7gOjY2Bijo6O0tLRw8OBBHA4Hly5dqmiirpJVQ6w85FQqxcDAAAsLC3R2drJ169b7mqyr9GbOtuF0Op25ohaB4CfKCcYdQTzpLWxCkFIyHcUcogNDNnucangNV2umqRY7aTfipO5UpLmMZiYk87Sl1xsAACAASURBVFI2Arioh3AYO+hQ4qTzyoMVTyv5pb8AhqQzlfQQkbtZr4XAPZT7zqd3MqrMmOxHlTC6sY4mXUJTljITBBAy9XjW6FdnaNKeQJFG0ZTCMIpf38KIOmeyb9CexiGZ84gzAp4w2dcYh2jR3eAwV/j59K3Mq9NF9gdp05NMl3pI3MkYMSSdAXWGGuMArcYCQqoGzJOuzmQXSU8mNjygTuOK76ZZm8PrijHnsN7HghxDSAYXXP/KgOMSTyTeRZveVXj9VnBS7356Mmd5mIJcqg9ytvXm5ORkQf+PleSx8JAjkQgnT54knU5z8OBBurq6cjfBcpvUV2qbSqW4efMmr776KtXV1Rw5coTW1laTGJfrfVc6wRgKhTh37hxDQ0Ns3bqVPXv25CoMh6RhxuWMIMYdSa4oKZLGk7iMNiYls9clgLAjc2uEpAhXZJ208SSyqEZmHUjmc3DH1pNypYk641xVIK09gST8uPU1zCjmTmyyUAlVG8Q9SW65XYRD+zC0zFtMIG4tBF69k1llgWuyiq49gSQyE7LedCdBxeyhqsJLnzLHNdmBoT0Bd+wzAm59HVPUclVWMbRjOXvICHi4qBgDIIHGGVVjMbC7wF4AQYuJvEV5gRE6MPTC8QGUyAbiarjIPsCQVMc0a3Pnm9uHkIi4CsNVSU+M0Sov84le09sQgCvZRURemkcIyfN8z/s/+LHzH4mzNHn7qHvIy82AKNUH+fnnn+crX/kKAF/5yld45zvfuazx78Vj4SH7fD4OHTpk+QdbbpP6cmLNqqqSSqW4desWc3NzbNiwge7u7pI3clZky7mxyu2JHIlEiMVi3Lp1i+7ubsuE+OPqCdNnw/IMLfp+HMRISxcKJqlq9U0MeAvFZ1CeodbYSRVuBCOFk2xCIuotPOdBdRav0UW7UY9QLpkmwar0LmbVpaY8k9VRfMYOOlNO5v3mvFxJVxjlTpaA9P+z9+bBdeXXnd/nbm/fsL2H5WEHCHBv7mR3i612W5KjcVnWKCPLU4mtUuKk7FQyclxxOktVrDjlkTOZpCblzJTKo5lS2RMvikaRN8mWWiK7SYI7ARAASew78ICH7e3LvfeXPx6Wt1yQIJuy1XROFavA+879/e7yu9/f+Z3fOd8DE+oqbvMwwcQWOJ1YxU+7jHYiasGnPa6u4jGP0GJmsZFhbh8An1eiIMG4GsVjHqHVzJBTRvYFcJvZjKEuE6kySG63ryvDePQeZtU1C30v88oqhmTsXo+uDCOAhGrdiWY2Ma4u4zYP02bo5NXC6qbcyt8ROedk3JvCLnpp0wV5dQCJwiSxVV4qe1tmRJT77q/ziew7HNZ7X5pl+3cRPvdhLOT9eJDfffddPv/5z/P1r3+dlpYWvvnNb75Q+8+SVwKQZVneF+R+XFVD8vk8U1NTxGIxmpqauHjx4jMtiucB5GdxIqdSKcbHx8lkMthsNs6ePWuptygtMWkRX6wKGxPyFhkpT7N5ngDT5OSVAjBI1tenUMcDeZW6xHFqtBmEvZDkETA6mVQrfcFg47qapdU4j0caJb9jYQqZVbmSMjQpp5iRw0hGLU7pMXqRJec1uojaohX681QhZwP4pCSmsudqkIWNRaX0mhJykhEgnD+CKg2jlyWpuIsAfEd/GOjIf4KEUpmoowlnAcDL2m/W30CQAioB2WG2YGz3saPfor9JFVusW/iIVdO120dSTjEsQ4vxJk5pmPh+8feZegxbjJSUZkSGsPEGHukxDlHFuqPSylcMO0vaBoZs8GfOv+DGeh+9iUOEciHcbjcul+uFreWXWS3koLHFO5t6LyL78SADvPfeey/U5vPIKwHIT5OX7bLI5/PMzMwQiURoaWnB5XIRDocP1PbLSLXOZDJMTEwQj8fp6uqipqaGvr6+fQllriuV1jFAldnGslIAjDl5kyVRwxGjFbu0ypRFqJtm2pkQBf1VT5ZNEeaI4SQl32fTwoUBYDNDCHWNaWUTm2ikV28jowxsA3hlTKzbDDCtrCEksIswPXobGXUAWSisqNacz7rpY9WbZlVvpnkri+KfRALURCNZb6XV7DeC9GtrOEQzPbpMWn2IRAHAFxSrSQUWZBvrUiuHdJm0umftu4x2dLUydTsh2Xgkq/Tox3fbB1CFg3mlEqRn1VXW9G60mBvZN16ymnCbpZMEwKyySq1+ChsSgtUSfcW0s+ZOlujPK1E00UCL0YiQH+yGKO6IR3SyUnQf0eo1+s7d5vDcUTIzWTKpwrMvTmP2eDzYbLZn7o/8XdTTM03zpfT5dyEfzasuk2dVDTlorPCOvhUg67rOzMwMy8vLNDc3c+nSJWRZZm7OOsbUSj4MIGezWSYnJ9nc3KSjo6MkjnnHvVFuiSxKMfpkJ2GzhUTRxp0sFObk0o9WlwwGlS2ajSN4zHkScunGnRyrRg/s+UTzks6AEie4eQaHtxIs7aaL6SKAy0k6g6pOg3EWBR2oBGTVDCK2l/lZKc+gmqfJOEeTmSOqVcbZuo1qZvyFd5tTDSb8KmHjHFVilk2n9SScyjrBlSAj5RlQIWycw5V7gldrYlWtDGXyG0Emtu9jR79KmkaXNllUKq1NKBDQZ6SNEv2cvIrb6CBiAeB+o54JdR18Mo35M1Qrc+TlladOEgnJybyyQZNxjmpmyW1HsThyrRiOStB3mdXc0hI0GGeoY4GsUrDGZaGxqFTGphuKwVhjgvHmCf5R5k0a9KqSwqpzc3Pkcjk0TdtNYbYiBvrbLpQqhPj/+ZB/EuRlUnAWA7Ku68zOzrK0tFQCxC8iLwLIO66RaDRKW1ubZdTGjnujfOD/hTJKREoRkZycMM6iSw8x5Cw1op0VufJDrzJr6Fc2kYSHto1u7L4phKIjmyrr+6wA1zQ3UdnOSbOJtDSA2I4pdpthdAsfqombPiXPcf010spD2A5hc5ieEgDfkQV5i4jUQqNeS1oZ3M3oA1BFLeVugXllC1M/hopAEg9L9F35ALOuRIW+ojbSmK0BZbViw1LHT/HkMa9ssSxqOKofJqNWciH7jDomi+5jR79Xb2FJto5dLZAkFfpY1OJERDWH9WZcpFm1AHCfEdztY0HZYllUcVhvxlAesqJZV8su3McaS0qMiPBxWG9CV/rxGV2WE5E97WPBWQDqf+H6Mz6WP8on5VMVtJbFxEDz8/O7xPU7fMiZTIZ8Pn8ga/ppclDXxw4GfBS5kOEVAuT95EV9yLquMzc3x+LiIuFwmIsXL37omf55+JMlSWJhYYEnT57Q2tr6VB+1Za08Ulwr4g4eVOL4zEM0xddY9liT08hUAZsICaaqZPx6L91GGgcKK3IluNaaQZ64C5PdAylBrThGq7lFVlpkxsLqKmwq2TCkHP1qmpr8EerzK+iuCE6zEcMCwENGEyNqiogMIfMETWaUpDKP0/QypVikAgtYkiEqZ6g3T9BorpLczm5TpSBWfl1n3Ed/tUlV+jAhYxmxzW3hyHmZsVX2YSC4pyg4xWs0miu77QOYVFX0oUsma1QTJUCjsVqi7zVqKu7DkExGlCRes4WAoZEsy8IzCZT0YUgmQ2qMtvzHwIxAWVSLx6hmushVYkqCYTVGjXECgQpUArKR94Ezvat/1TbEQ3Waf5h+k96iJBebzUZ1dXUJM2ExMVA+n2dycpJMJrPLh7zzz+12H9i1cFCXRSqV2pe//KMgrwwgP81Cft445OXlZebn53c3657FVnXQEKGDkNQbhsHs7Czz8/PU1dUdyCK3srz/ShnFKLP2YnIOVTTgFD4cTJApYlXzmT7GpNIl+Jaa577QaDcbsYs0WamUJlLHC+wBb1RKE5VsnDEuYMoTQOm91hlBnhRtvK1pOda0ACf0ZublSktQABuSChTcEhE5zYpwc0w/jYMUprpacU69EWZ429+8LKeJCA/H9NNo0gzT+wD4utMGmGw4DTZFHUfzrejKEOg1YKv0Q9dk6nnizLIFu+3nGMAte5mxmLgkIbEgG2zImV19XRnClHJIogarSSJkhBlSE0jb92soQxhSDrdZZdkHQmJSMdnQvHQlj2J3TaBLhZhmRdRhxTOtiWr61BRH9TMIZQRjm7PEafqYt3BDrckJ/q1zmE5jmS9kj+KzSBqCUmKgmZkZTpwoVMou5kNeXl4mkUhgGMZuGagdkHa5XBUW7kEBeXNz8yNLvQmvECDvJwctdGoYBnNzc8zMzOB0OvcNo7Nq/2WUcSpObGloaKC9vR1N0w4E9OUWcowsP1IqIysEsKVpTMopXCLMcVOwKo8iAbmYAxGoDLNrEfXcU5J4RQtHTJ1VubDpVGVWMS5XWsGqkLkjmyDa6DGyrKlTu5tOWdxYhadt4WMeB916mnV1evd40KjnsVLq6xYSjCk5hAjSYrhYL0qwEMC6pFToP1STdOVP4GeTjbK065ARZsSZKdEf0lI0Gifx2MyK6xUCIkWvZKd9V6abXmFHOCt93fVGmKHtSWJHP2D2ckjPM2UxqSBgWRYl+n6zhy4zjSoUhEWoW/FENO7W8ZnddBsZdHmZKYuNRAGsbvcxrCbwmp30GHli6hMcZoN1H3ojI2qaFXmeQTXCFzJHeUNvPjCL3H58yJlMZheoV1ZWSKfTu6C+A9S6rh/oW3gZWXp/l/JKJIY8TZ7lsjBNk5mZGW7evIlpmrvZbQddSn3YyAkhBAsLC/T19ZHNZjl//jwdHR1omvbC7X5fmSJnkV7cbNSy5ix8hCnJ4JZskk504klVs+y32AgRsLhtZcclnVsyqOYJ3MKPQk2lPhAW9cSkPDFZ546qoBnHcZpeqowappVKMFYNiXEly6ac546qYjOO4zALS870PuxwQSNIRM5yR9HQY53YzUI4VMioZ6kMwAGcpo1BNcNdxYZdP76rX7DArVc/ivBxW9Vw6MexmXvhVvVGE5v2ygneQOKq3YG+1Y6S24vfFVhR6MOmnGVRakAzju1ez46EjDDRsrDALTnHY8nFPA27z6e4j6hcCooxOcc9VcbUT2MTlUv4er2RlaI+4nKeuypI+nkiFtwiAtiS9u4rKeX5urOf/83ZR0SqfOa75z1jg22nDFRtbS1tbW0cO3aMc+fOcerUKZqamlBVlWg0SiaT4c6dO/T39zM+Pr5rYZe76j7K5PTwClnI+znxZVm2HBTFFml9ff2uRbxT/vyg8rwk9TvW+g5B/uTkJDU1NZw7d67Eyi7WfZYUW8hJDP61kqfXPERUmsDcKS0ExCUb5W6EJY+EyzxGnZknIo8jitwcYaOOoWLyd2BUThE026gWDhCbFBtHkpCYK/uYnyhpHKKBE7oP5GnKjamqlJ+od++cR0oapwhzKq8yrEUqbC9NKIwre9c045NwmmGO64KEpGNlgdeZdSypBdAYUdO4RDNHdQNFSlRY4AAOofFEKYDVsJrGLZo5ohtE1XE2i1woxeJKezAcMONXcIkODmfzbNgnCSSrmXRXLv+dhsYTJUVeErhEM8e224f9J4lqs7ZgjYswR3WTNbXAUhcyGnikVFYdcQiNW1oOhSaO6YJ1ZRSkHXC1AZV7CWk8PJEV2jdc5ANLu5ucBXdT5bMaVzZ41zXIz+ab+EwujFpk432YaIfi6tdQoAQ4d+5cySbi7OwsyWThmlwuF4ODg8zMzKAoygvXFfzSl77EX/zFXxAMBhkaGgLgt37rt/j93//93bqBv/M7v8OnP/3pF763p8krbyGXSzH5UC6X48KFC3R2du5axM+7CfjcFTt0fZeBbX19ndOnT9PT01Ph8niedouTSL6tRNmQdPpkgyTd1JqFnPtGs4YZi1JBTqExKOW5rpjkRA81ZoE4yGr5vyMuEeC6AqY4jDvj2T3eIkKsWXDyuoSD72kKwuzFb+wtJ2UhsWyv/GjSksGkXINs9OIzSq2dRiNIqsz6T8sms5KXBZrwmaVEMLYyAIfC6uCOChtmF16zshhm0AiSK4rOSG7re/JniUuVn4xDaMz79o6nJIN7dhlhHEfYrQl+3Ek3eWlvtXJbhWSsg4Zcq6WV7xAao9uTROH6BcI4hsesIoG1Lze0fR9pyeCOaqKbx/AYNdTpIRaUymiMnWeVlUweV7nImUfxGoXxkMNToQ/QYITYlPP8oX2a/8b1gNGiZJuXxam8A6471UVqampoaWnhyJEjnDt3jjNnzuwSiD18+JAf/OAHnDp1irfffvu5Wdm++MUv8r3vfa/i+K//+q/T399Pf3//jw2M4RUC5GfNhjsWcV9fH5lMZpfzotw18ePK7IPCDvDc3NwuA9uRI0dKCPKL5UWqSWcx+aMiUp5FKc8t2Usgf4h40vr5NIhaMtvgMyvnuSP5qTV7qI17WSwDMgCPsPNwmyVtSs4xZA9Rle1AESor+7wCt+lHSDCp5BmUa6jTu5CEREOuhrit0oqqMVwMK1nGlTxDch11eheykJCFxIy8T0kuPIwpeYakIMFtfSgARjmAA9QbPm5oeYalEM6NINK2viYUJizuG2BRdjIshQjqnbv6AEGjjrwF7mSFjatKFXVl+jahsuCtPCHikxnI1uHYCCKVufODeh3ZsoNTSoY1s52MqC9pf+c+yieiGSXDkFxNzmxDFpX9lz+rOSXDoBygOneiIm4dChPqtLy3iptWkrzr6udf28dJYbzUGOSn+Y93iqp+7nOf4/Lly3z5y1+mv7+fb33rWyVloQ4ily9fPlAtyx+XvDKAvJ+Ypomu67t0nOfOnaO7u3vfPP0fR9WQzc1N7ty5w9raGtXV1Rw7dgync5/qGdvyrNTpcl3TNPlLeZ01C//fZE7wwNVJg9FQclwTMo/LwEpIcEvWmaWDunyldRcUNeSK3BqmBHftEnbzOKaFtenMyrsADqBLcFMVpMxDxIW1r88p9qzovCQK+uIQLXobm3KlG6dGdzKiZHf1+1RBWhyiRq8tAYxS8ezqD1V5yZk9VBm1NBpBkhYAHjK8jCq57fbZ1deEwqRVNRBAl9zb1w9Z0UP1trXZYARJW/ThT6rMeBWGq7ykRQ/+XIEgRzEkRrEuhJqTPNzcvt9qY+997XcfNaabH9ggIbqo0fcYy8rBdUdMCRbkABuiizqjvuS3sBFiq+wcIcENdYMvuie4rsT+VpNCoJRYqLq6+qX0D/B7v/d7nDhxgi996UtsbFgn67wMeWUA2YpVbXFxkZs3byKE4OTJk3R3dz8zGuJ5/U5PA/AdBrbJyUl6e3vp6ek5cFLJ81rIOUPnDy2SCAC8jjrWFbii+GArjEsUJoMWM0jcavPP9DDkdXBNqyZg9uLcDm9yCJVhqfKjFcCssPO+Uo09FsZu7k12ISWEIVc+UyMh+L6jFm2jHs3Y+9j8poMhpdJHuyDpXFPqqdG70UTpx2k3K4F9XtbZoBXNbEMrswZrDTfDZX3MKnkG5ACrNKJaWI8S3gr9+7KfWv0wGYvU8aDh4VFRH3Nynruyn2q9mznZmjTK0Pc29xYUnQdaNdX6IcJmAxkL+8Gb0nb7WJB17ig+7LFmbIa270pCEwWXzpKsc1v1EtB7sAmbJbgCVJlOhpUsK7LOTcWNT+/FYTpKIkEqrssMsCzn+Z8CEb7eIxO1GDPPI3/bXMjl8qu/+qtMTEzQ399PQ0MDv/Ebv/FS2y+WVwaQd0QIwdLSEn19fcTjcc6ePftjDYOxspATiQT9/f2Mjo7S1dXF6dOn8Xq9L4XLwkpkWeaa3WBRVFpqtaaDB/JeO0/8bsZopdFoYXwfDgqzyF94SxZM0U6T2UyjWUeqfC0NBBMKE2qhrUGfm3mpkwa9CYfQGFasATztKSwlh6r8LJjt+OMFUFW3VAyLObHLCLAgm9xQZSKii/pta82VVRnWLD54AdOyjeuqzIroJmSEdn9y7GOZtxkBfqSqRMv0q00XI7J1+v0NxcOq6MYbKwXsHeAruSQJNgkwQVtJ+1Bw08z4tQr9e4rBgNy6e7/F4rNVrmBGfDYSm60Y8coVWJXprJjs7qk6y6KNuKjBajR4zQBm0ft4oOaZk5pp1buJWDwTt2ljqOid36+BX3KP82faGgYHK8ZbLs8DyLFY7KV/76FQCEVRkGWZX/mVX+H27dsvtf1ieWUAeaduXHER053Nsuf1Cz+PFANnKpVicHCQ4eFhWlpaOHv2bEkIzov4hQ8ikizzv9v8ZFfD+NOlPmkfBf9tsSQkwUo+RDLZQbVeGhJVb7oYKLOutiTBB7iZSrfgNyoZt3SlFIzWJcEV1YMvfRibUbkiaTe8TCp797ahydz21hPI9xL1VIKlAGbze3CxKguuKn48Rg+utKcEMHakywiwtG2JrsiC95UAPr2HoOGztMARMCsXADFSpO807bs+8HLpNAKsyIIVWXDX14hX78FpOvaAryzKQBIwLau717OjD4WNUivpNAI8Ucy9+93W328lgYCIv4a7/gbsuS6cetHz31Isn1WN6eF7mgen0YvH3ANyTxm47jYjmdyTa7EZvXjLQvCCZs3uZuWOJCSTb2pJvujcZHIfy/1p8jyMcbFY7KWHvS0t7bHwffvb3+bYsWMvtf1ieWXC3nK5HBsbG5w+fbpio+ygySE7suOTPYh7QVVVMpkMw8PDxONxOjs7qa2ttXR9PM/EcBAf8k7o3B9HV5msPQTYmc+7+bjIsOiYwyup3LdwSWDCdF5jVoA9FeYtLc2cfRZTBgdesODM7cl5uWoqOJPNnDPWiPhXQZaoy2o8clYOI7sp8SPdg657uKTGmXMs7k7/SZyFiyiTeM7HQ6OaS/kYM84ldh5hu+HlrrNyzf5IN4nrXRzf3CTi39jVF0BEslX0cUeF44kempQUM45lil9Rp+Hnllqp35zvQs6DUEr1BTAvl/axuLLIT1+/y89eu0Hn7Rt88PYb/C+/825RHwFuFvVxVwWfaONcPs1Di6gHQYFpbqePewp4RBtn9SQaBqZaCciNCRuD2/PjoE3BLTo4p6eIyxFmAo7tVkv7mM0KUGFAETjMFs7nUixrc9SaNcyolauudsPL3e0J1SHCXNRzLCtT2FAYUazHdwYnw2qezypr/Oc5N/9pzo3tgAklL+pDfhH5xV/8Ra5cuUI0GiUcDvOVr3yFK1eu0N/fv1uj72tf+9oLt/8skZ4zVvAnlkZJCEEuZ725Mjk5idPp3C1S+Cy5e/cux48ffyZJfS6XY2RkhPX1dY4ePfrM4qVCCPr6+nj99defeQ2GYXDnzh0uXrxo+fva2hpjY2N4vF7+y9Zm+rXSAdsmwUktzTVH5WbQ4ayba9nSSatLgm7bGndtVFhRsglqooZitt4eSVDnWELWVYadldbL0ZSP9/U9ED2MSZVzCYcsGFAql9MOUyKeqGYnaOoYJl7XIutqklojyGOlEsAPx71cEwULsDuTwuNeJOU2qE9qDLkrrSSvLrOUqiILnJQMHM4FNtU0ggKr26RVHwk/10y1RB+gQ/czEYvxxr0+zgzc5/zVuxwbeVRx/h9+6Qv88//hnyAArxFkep8+YkJCkadIufcmww7Dz22lciLy6TKBtA+7c4kNdS/6QQDOlJ95V+U5l1Iu1rV11srIh9oML3eVynHevJnEJRus+Sq/qaARZKTsPtoNmcNGktu2ymiMBsPFQ6V0ZdVhKHwl6+OMxQqqXBYXFzFN80A0t2+//TbXr19/qcWMX5IcaPZ5ZSzkZ1FwvsyqIfl8nunpaVZWVqivr0eWZUKhkKXuQa+xXParJr21tcXo6Cg2m43jx4/T73TQn6+0lNYNiT9fbuCcJ8t83RIZdTtBRMBytjLUblxAVbSFTsVgtm6JbNEH15l0cLNM/4mQyCSaqYnpKE0bGOreXK2a8FgvBYVHyGjpJt7JC5TAOkYZhndmvLxf9P8hZGypMD9Nhrsei7hcU2JA7H3MYw4XdqOLN5MJkhaWI0Ddlsr09mUNCAV7qoWPqUmy+hIjvspn7TZl7pnqrn5dNMAXRx/Sc/fPuXivn7apMeRtg+ZxczdJlwt3qnQC/I/+zR+Tcrn47n/1G7tWZbF4jEIfaUDLd3A5mWHROYchwwZ2rFYS4ayXD4SCLRXmY0qKReccplwA13sWYOwyZa7oTrJ6Ex/LpVl2zmNsu3NS+6xW1LyPWzY/lzbirPmWMZXCfVYnZUbclfpzkslsJsyRXIaoa4580calKjwVfUwqBl+V8rQLhf8RGd9Tvg1d1w9cveSgNAY/qfLKADI8nWAonbYmOLeSg3Ait7S0cOnSJTKZDE+ePLFo5cNJOXgnEgnGx8cxDIOenp7dDKb/I2cdM9ubdnJVSFyJOwilWjlct864b5PwJgxauOOqDYlbSY0sNpqSbTT7F5mvzSAEzOY0sPgeapJOrmc1miY9dDRuMuUp2Lc9OV8JuO5IQ17m22t+2rb8tARXmfUUrDXVhBG9soMcML1RixKtoyG0ynwR8Xo5gEMhf2427ScVcxEOrTDv3gNHhyHxWHFX6P9Ad3NkvpnGFp1FT+mz7N5S6Rr5EW+NfMDHR97n1PQACc1NjBAtc2Mlur1zY4wcPUz74BTObIbFunqm21vBDZ+585eo32ri7ud/qeIeWzNedtg48rLMe4aLzsQhjtg2uOGoBD6HKdG/HZWSA94zXLQnDtHsiJBRrAG8I+3h6vbfPzSctCa6aLevIqtZBiwmCc2EMcWDIUlcU3yEk1667VEW7es4bTVYLZTD6zL3bLBiOmiId3HYscaCfY1AXmVAreyj4Daz8z6C72HwT4XMp5EsjRZd158ZJgofLjPwJ0VeKUDeTz5ssscO8dD8/HwFFefzxi0/r2QyGcbHx0kmk3R3d5cErQ+k4aEOlBkEThMGknsHI4ZMZLmWizEfOWkVKsOF6Uy7WNheVS0YMgvrYU5EVwk16VzTKi2OOkPmdqowfBawsbAY5LLHx0rdMtN5a2umKln4qKZ1henFet50JVgMLtOteywBvCUvcy9baGtqoZ7LnjTzoeUCPaUFgANoSSfjusrkQgOXvSnmgstk1k4QZgAAIABJREFUFUFn1sv7FnsC7WmJe2o98oLgrFggzHUujffx5kgfuYSDywOlVxbIxsChMlbfRffyXlmntUAVBNxc/dnX6YpM0rU5TWNqmZ3w4S/9u99mgzr+1ec+TXbb2rSbMGBUfoITSNgiLXRqeeZDy6WrFYuJaAoJOd5IdVbFVrtEruh1aSYMGaXPagaJmWyQT8ZknFXrpNXS8duRcHCjyGc7j8R8to53MgEmXEkq9hhMWNL2XERLksRStpZjawp2RwrTIs+iN+fi+vbfEeBLmPwMEv9UyDSWVxB/zqojH1UuZPh7AsjPS8G5w1tsmiYLCwvMzs7S0NDAxYsXP3Rm30Ell8uRyWS4f/8+XV1d1NXVVQy0/3VeZXXTx5lQint1WdiO9z2ecXLVYjs9llSZiYQ43ZTlXm1ud5PKbUrcT1YC3KBUx9kJgzNVOe4GMiWbWm0pZ1mpU3g/4eDNRBjTmWPGly3RD+kyd8qCaa+lPNRMNJP35sBX+Qx3ABxAIHE14aIh3cZ5/yZ/Y7EqLQZwE4krcTeNqXa6a6OMSBYALgSvzY/zzuwN3hq7yienv483X+pjff/Y61weKi2DFcissxbw86O6t3A6TZqkOZqT09QYdzmyAbebz5CPz6MZpff05f/7y0wR4OHPfIpJX4wuC3AFaMsr3M9qkNVoSLdxqDbKhD+270oCwJVw8X5Wo3qrmeMNMSYCBSa+Q/v0EdZl/mzDR+2Wl2O164z6N5EkkEyYMh2W8VfrsQCR1Vpeq9ngSWBvE7U36+a6RUr5rM3P5GozvZko0w3JvU1XAUtJpWLV9T0EJxH812Xj6qBhb5lM5kCW9E+yvFKAvJ/L4nmjLBRFYWVlhbGxMYLBIOfPn9/Xh7Wfr/dp1/i0CI5it4gsy1y6dMlyxh9LS/z7dRmBxLUlNx0roHUbLNoNHietfd/OFCRRub6gcjLmIBFOsmg3OJJy8L5FXFdvLsPdmANiTk5v2FhvShKxG/hMibspK4CDlZjM6LKLs36NSGOSqL3wPppSDqYt9jVCySw/XPVxzp9nMZxkXSvo11sAOMCyLnFvPMBxl85cU4pNbe99FwP4jiwaMq3zVTSZAr0xRXVinHemr/Lx6at8fPY6VZkCR/IDz0lkUfkeL6/c4NrRS5wa6+dJ8yEMjxe3EqUn9wSXliIXt9McK6X0PB+5R9/R85x/eBdFmMxUtTATakV16vx3w/8tv1bto7Xnkyy4sqBVjldfcs/Hv2TILEWCXIz5qArEuVL51GnUZe5kC5/yumLn6kodF+I+ksFlJnRrf2pd0skYEqumxI9WajkX85KuX6bBsHFNrvRp1RgSd9IaeSR+uFrD6ZgXo36ZqC1HJG893sLrEvckjeuJBk7O5pFCy0QdWdozNu5ZrLqcpsk/iK6RKCusetCwt83NzV1X3kdVXilA3k8OasXuhJHNzMzgcrkqGNis5EUz+8oBuZh9rqmpiUuXLnHzZvlW2p788yUFUQRwk4Yb7YngZ+p0vgsVFk6HMLkX3zs4EFdwPPbyWlWMQVmyHAnFrIr3YwquhJfzjVlwyFy1APDXMOlPF/q4u6Xhifs525hmPJDjXsriOQqIbbs97mxp+OJ+zjSluVudpTHlYMoCwM8aBndyKvM5G/64Rm9gjUdhhQbDGsCbU7NcnP0Br21c4Z0rPyRgbDEptXB8pbT80qnEAPdbXqN39gkuPU1GtjNW3Uu+1ovPsc691lNcnr1RWK1vr9gb8hHmfY3Mm42EE3tVOhZ99QifxPc+8dOcXR2iNT9LK7OwbRP8ydVf4Del7/AHjk9wuinNvaq91UrDPhPRzZSd3mWN07VZ7lWXrT6STibKntWttJ1LU40EbHnma0r1a7fBtVjuZOy4p1tosqfAv7fa2pH2pJP5oj7uZ204Z5v5ae8W7zlFRQyB24RHwrd7fCCrYZ8N82YgTtKuW8YcfD6bx5HJMBONkkqlkCQJl8tFIpEgkUigadpTN/c+6lzI8IoB8n7g+Ky6ekIIotEo4+Pj+P1+Ojs7yeVyP5bd2h1A3hlYO5mFU1NThEKhEmL8Hd3y5dp8Fv5dtNLCzpswMKXSLIMrbDKs7ekEMzBZpp8RErmISq2hEWoyeVKk35nP8ShXGo2RMiXuzNvpNQSdIZMJtfQajLJQ2oQp8cG8i0+uqTxxmcxopVbOSSPPAHvhUDFT4tqci/MbGksOuXJ0ClhP7L3jLUPiwVotx9JZgrUFX2ptfpW3Yz/indh71CVW+PmZ7yCXbUK1S7OM1R6lOzq8e0yXFGy2PO+deptwMkqPGOQ4A4Ufs2BmYKDxdU4ulrovwrlFRkPHuRLoQnaYtGiztDFLI8uQhauhjxGaLy2p5DCzfPXqP+Temb/huvEGJ9dtbDUliTgFDUkHkxZodc40uJNWYc7F8Q0bqeYki3Zz13KtEAGrMZnxjIujGzZyzUnmtzcJW5NO5iz66DZN/nrGTbemIFpSLHgLOh5T4n668ltIC4mZeS9hAWpzkinXnm/5SMrB+1LpC8wiMbvuxoxBV3OS8aIQPw34stNFY2vr7jHDMEilUmxubrKxscH8/Pxu9FN5YVVJkj7yXMjwigHyfvI0t8La2hrj4+O4XC5OnjyJy+Uiuj1D/zhkB2SLJ4FAIGBpje+Xrfd/LivkLSzU8zbB7bVtkHwi8WaDwYMqmYAMt7cq9WUhWM66WMxIyE8EH2syuROQyEgS3pT1jvVp1eRaVEHdLOjf9EnkZYnDwuRhsnKScCO4uaSRNuCNJp1bPgV92/rS92Gg05ISC7Mybzbq3PArmNsT7WvCoD9TCuoeEedo/IecnH7A18S/pyM7VALAd4Jvcmbleskxj0jQIM3SH3yDLaeC05GiR37CMYY5xjB3naeRopQEE8gSHMv0cb3+dXo3HjNafYicW6NJW+AQD1Fpx72eJKSX0j2+lf6AHzW/xdtzV9ElhSeBQyTd1Ui2LH8S/zw/5/5zHiRO4xj1ccKzxn239UpiM773rB4mVOyPfZyvzyAcconluiPFz2o4qWJ74uNCKMNEbZYHFuAKexPqWN6BOmHnUijLQCjN4ZTd0qXVYxoMprY3t0e9vF6XY6g+hSnDSMrajRFIC+5kVKQxL2/U5hhpSJFW4HNIFZt5iqLg9XpRVZWenp7CoxCCbDa7azWvrq6SSqX4kz/5E6amppAkiQ8++IATJ048NzhbcSGvr6/zC7/wC0xPT9PW1saf/umffqjEk2fJK5MYAgVf034RDzdu3ChJyNjc3GRsbAxN0+jq6sLj8ZT8trCwwNGjRw/Ub19fHxcuXDhQZt/Q0BB+v5+lpSWcTiddXV37bkQ8ePCA3t7ekt/XMvCFBxofSHLpsk9Ad8ZkrAwUQ3bBqaDB99KVc+9FzeTmUql+2CloazK5vimXuEQAVAQ1cYhk9443O3QcoRw+2VHiEtmRN2Sd6yt7fbe7TOxNAkmDRyuVfkEPAmkD4tuEFp1uE6VJMGpTOLplMJrSedO8zjvme/yU+UPOirto6CzrQZIbHjpF+ToA3jff5I3oDWbsrcx7wjjcOTqcY9iVLAtbHfTmH1acc1ec4Vh0CIfIsqn4WHT3sGJ30uxdYpNaziT6Ks4Zo5Oq9U1q9ULJJBOJaUc3q1X1RGw+LqffJ0Cs5JxVuZa3Qld5pB3hWGyNTdOJ0iQx49175ycMg8HVymflkwThrIlRD09spb8fjxk8TFWe8449z6JL5pG99Lde0+CxxfvotBlUewR3bJXj52zC4G6i9Jwmm8nxYJbv6ZVjOixMFiMSZtG4CmkmbeEU/5ff5NA+K9zbt29z/vx5y992JBaL8bWvfY2HDx/S1NTE4OAgf/RHf0RTU9NTzyuW999/H4/Hwy/90i/tAvJv/uZvUl1dzbvvvstXv/pVNjY2+N3f/d0Dt1kkf78SQ+Bg/tx4PM7YWCGGtDiet1helKT+WYAcj8dZW1sjmUxy9OjRkknASqzSp//liMIHIzJn6k3mayQi2/fcnYsxlqy8F10X/PCuzFFvjMWwm42dDRsBEQureT4t0TwpcdbM8ahKIaHsDZHzquBGtvQe5zIq4WmdOht4/YJ40bizIXiyUao/lZKRxgSfDOjMKjLJsnd2Uja4XhQKNp00OT9zm39T9wPa5Stc8PdxM3aBt/NXS86rV1dYqTIZ3ezikFkIR4uoTYw621DcJj8Mv8VP6VfpoLTWYJN/ksH145ww90A5IbnBCd/veodmc45jjmGOSHc4sv173pxkyLjAsfStkrY6meSD4JvoeZWQK0mjY5QObZQORgF433GZyxulMQ91ZpTvr3yCT4WuMpVvJykU5BnB2eoUgyE7OUUhuZrCKlbxuGRwPaYixQSvN+j0VymkZIle0xqM7QgerKis5yUuhXQe1igktlcr7n0WhCFDcGNU5WJQZ6RWIbat32pWgjHAYlZCemzngldnOGCSsO9Z480Zk/kyyInkZc6v2zkUsI6nP6j4fD4CgQCf+MQn+LVf+7UXauPy5ctMT0+XHPvOd77DlStXAPjlX/5lPv7xj78oIB9IXilAfpqYpkl/fz/5fJ7u7u6nOv9fNG55vw2HVCrF+Pg42WyWqqoqGhoangnGUBnjnMjDvxwufAT3lmU8a4I32k2uazJG2noyaEpsMSiqGI75CIwLTjWkeOBxcVYzuLtW+UGFVMGdBQld2AlEdS50m9ySZUAwbwHgADUZnRsRB8GooKfN4O72jvhpWeemRUxym2ry1481Qk6TcF2cJ9uTog3B4w2J465B3vG/x5ueD+jOjHPCVmrBvu2/yhXpMh9Pl4KbUGTm6poZy3Vzqvohja55Qizs/v5B/A1eT/WhFCVPeOUkDdWL/DD+cQJ2E09gjTbvE87K9wC4nTyLmVApLnekyQaH3PcYMs7jN1eZdIXRPHm6vWO8ZfuAkcxhwpsL+KRSa/iy9j4Dnjc5mbjGpuRj1NlDyu0i5Fnmu45PcjH+AUmjCROJu+suGlMmhxrzXLEAY800eLi2XQgViRtLKvXrJuGqGG6H9dg6IxvcyBc++b6ISnDD5FCzwZpd4p4FuMpCML1ZGFc3V1RqN03OhQ3uOFUaMmI3oaVYzqoGdzIq8xkZbzTPhTadWy6VGkzuxawjJb4csqY8AJ6rFFMsFqO+vpIV78NIJBLZpVxoaGh47gokzyuvFCBbvbhUKsXExASZTIbu7u4DpTi/rKoh2WyWiYkJtra26O7upqamhsnJyRem4Pz6Y4WN3N49JvIS10cl3qo3eJKVKa/k48FgIrk38WzmJB7MuDgcyKLUWE8e3QgiovARbuoqtx7B6ZCJt15wNV35QdXLBkNbhQy4lbTEyiOFs/UGczUSc/t8gHV5wRQQSctEZn18pvMRoaZb/CPbX3Gi7QpBba8S80qmlqn1btrV0sy4j/ve5z3pp/BlEqRcdpqqFugKTBJihfVsgK3Vyg/zY97rDEiXOJR4wKjUzYa9Gr9/k8M1j7jAHcbnOznkGi4557z7LveMcxxLDWCXckyLVmbVZhS/SWfHDLPRBt5yf1ByzhHHIwbdx2hPTuOVCo7ZLeFjVulm3atwvepTXPB9n/PynZLzftD401xeeJ+oWaDVXMzINIwLzqs6o1Uym0Wxvuc0wQ1R+g6XszLMqdiMFIF6J5tFm8GyEExtlU7aKzmZlQmZT/nyJDWJaFks8VnV4HZ2r41oTiY6KfN6VWF1UyECNos2XeNC49YUnAwYVFeZ/EhUjrmPeXTOWaRi78jfNRfy37a8UoBcLJlMhsnJSba2tujq6iKfz+P1WqSoWcjzZt+V6+9wXayurtLe3s7hw4d3J4sX5UTOGvAvhqwBLrEqEV10cL4zzV2XHXPbdfKaDa7plZOUSMHguMTHeg2uKTJi+9oCsuDeUqX+/WWZY8smbwZNrskSxTFUrabBMqXXdXdZ4fW0ATZYkCnxnjWqJrPrK3yh+4dcbrrCxxqv0uItxPGOzp+nWl0vaSvoiBKr0RlePkyLbY5HuV5idg91wVXOHLnO/cULXNauoxax2lXbN5FrTYYiRznmHEYXCuO5HmKuOkQ4zQfmBX7Kfq3kHMjR0TjBwOIJTroGd4+uiCYSbjt/7XmHE+4R2rwztBXZhgHnKnfnznDWfa/kutvd01yRLuMy09RXLdNT9YTj21a3bsjcn7vAeVepH/qw7TF/3fgp3l76ITEjQKtqcG++8GwDUVGwNjUVmT3LtVxqs3mGEgG8CYMT9XEG/V6Q4GguzsNspUsrpJi8N6biVOFSm06fTS28LwFRiz0BACkG6xGJ19t0btiV3fFwUjUYSFaO0cmYzMK0xBtNOn2OvU1agF9/inUMz0e9+eMA5FAoxNLSEg0NDSwtLREMBp990oeQVwqQJUkil8sxOTnJ+vo6HR0du2AYiUQOnBzyvLHFOxayYRjMzs6ysLBAa2srFy9erPAr72QBHkSKoyz+cExmMVV5XZ1Ok3tThT5ujzvpqDKxt5lMComRiPUHJScFaV3igyGFQ3UmolkwZsocUwTXjMpzTnlNHozLEIHDIZNMo2BKyFQrgoFl65CrxWWT6S2NY/UmtK7TUtPH68Fr/FTtVfyOCGsbQU7W9pecdih8m/szF3nNfhubnCdtOhjLHmYxX4MazrKmeHktWHrO2fBNbi1e4Lx8G22ba9cUEst6A7OeJsaMNs523qTRNU4je6nO1xYv8oZ8a/ccAK8tibs2zl+tfgqHN01D/RxN1QucomCxP1g6Q6O5iK2osoZDzXEkPMzN+fOoso7k8SJVbdBaN8Zbyg94uHKSTnMStZhsRzE51Xybm7PnOe+6w6zZxaTciOLN0d45wf/T/hk+e+MvadAdu9C/mZO4Napyss4gEDK5mq187iHF4FGiALpxXWFw3svxjMFGEOL5Sh5rgKZ0kojwEs9D35jK0RqD9aocIadKv0Xyj08WPIgopHSJG+MqvVUGuQaYlBXMfehiTioG1zIq0QmZLr+B3ASjssIJp8E73qd/C89LvfmyAfnnfu7n+MY3vsG7777LN77xDT7zmc+81PbL5ZWKsshms9y4cYPW1lYaGxtLgPXx48fU1dUduOhheVTG02RsbIx8Ps/GxgYNDQ20trbuO6svLS2RTqfp6Oh4ZrtTU1PY7XZC9Y384h+qXDFkYmUp0cdyMYbWSy0fCcF/0GNwJaaQKgtX6rTlmJgtC6+TBG8cEjxJS0RylaB/HJOHy3tArcmCC4dMhFtwfcFi9929jl1+yPmOm7TXjPN224/wOeIlOhndztxqK+eCe9UX8obK4+hhZtY6aKtdpa5uAnsRc1sq7yCy1sCpugcVfT6YeYecmUV1mdT6F/C7C6nDyZyTtY0gJ2oHKs65v3iObnOSpWwrq7KH6upVGqqXCuds1nGiZrDinAdLZ3lduk1eaIwkDrNKDU5/gtpAhI1YDSct+ulfPs0l7qHJeUwhMZrsZk4PYzjAo5qcaLxRcU5f9ON87jv/L8nyaAUBPYZBbVBwQ1N2VzcAF8lxc7kypO2MV8ehQp9dKYlw8EsmuRVIl03CKiaX/An6HF70MuPkDU3n+lwZfYAkeLs1z9WMRq4smMAmCbzrgrWizWBZElxqMfhPjub5D2ueXY9yZWWFQ4cOPVUP4LOf/Sx/8Ad/cGCa3XIp5kIOhUJ85Stf4ed//uf5/Oc/z+zsLC0tLXzzm9980SKoB7LyXilA3olRtLJwJyYm8Hg8B/Ihw8EAeSez7/Hjx3i9Xk6cOPFMmsCVlZVdn/KzZHZ2FkmSuLnSyn/8bzVqPYKOIya3cwpIUCdlWVu2YZaBriIJgsnCSjJ0SPAgv/cxvCay9EcqY0TfrDaYWZIJdAkeFukfcZmMTFZazR5V0GIKaBKMGQaH64fprRvmZzq+y7HgQ9QiovKZlVbaPFNUu0qLQ2YNjYH5U/g0g4QpUV+3iNNeAOCJxS6O1w7hLOPXzeh25ldbaHQsM7rRRVa2UVuzQsAbY3a5m6N1D7CVVdJI5x3EN+s5VN3Peqqax+u9xA0f1VUxEmaAo4E72MoqYyRzTuLrdfTWDe62Mb15mKhUh6476am7hctWGhmQyTtZXa3leGhvE9IUEtPJLkaWjlBtX6cpOLs7WRTOsbOyUs+J+kog/+OxX+S/uPL76EU1Ck+7De5PFCb77loDwjCGgl8WZCOQKa9/JeCIaTCyqtBZbaK2CJ5su5jeUHWuz1dOqG3EmV720uTOoIV1pp2FTUK7MHGsC7YsSmyfVQyW4xJV7YKH0t7vr9t0bsxa9OExuf+ZJKr1Im5XotEosVjsQAbMO++8w9WrV39S+Sz+foa97edueNlVQ3YI4r1eLy0tLSiKciDO1ucuXprL88/+pjDAowmJ6G2F3lCM1SY3hxwq1y0C9s9VCW4uFq578bbEhR6TUY+EzyYYmK60oFRJMDorsxKXmLst8foRk4dOibgp4bSgFpYkg092jKE573Lq8D2Odz9E07aLbS4fKgFjgNbgDEvrTeRzdlI5J1u5ZjZUDX8giqd5i6WFNk403y05p7NxnOnVHhpdE/jdWywn65mMdZIULvyeLaY3WznVUWopt9SP0T97ipMNA9i1wvp5K+tjdKOHdM7Pg8keTnfcJeSNEKJQELaaCCMLpzlS/wCbUhRJoRgs5Gt5NPpZ/P4tGuoWsNXHaNyOI+5fPsv5YB9qUb1Ch5YmUL3Otdk3yQkbuqISrI3g9Sc47B/i0dIRepylm4YOLUt1zSrDK0epdUUZ3+hCcbiRnDHO9d7mt5V3+e+//88QyCAgUxS4MRZVkNcEb3TrSF64ZsEcd9SjMzxROD6xLiOtC17v1nnikRmyiDsGsKcL7pWFpAOewKUunUGPzBEpwx0L10eTmuPufGFczQ/AxQ6dEa9MHInZNevv558czT0TjOH5fMi5XK6iWtBHTV4pQIaXRzC04xcuz54rJ4h3u927boiDyPMC8tVJlcGF0pH7OOKjLWsi1YjCmqUsQSQSKeM1eCJT5RIcPWwyI9SKufp8leDG4l4fN0Zkaj0mh5o3uLdRDRKEgzMc6xjkaPtDDrcPIxmC3LqNzuaJkraa6ke5MfU6F1pvosgmi5sNzG62oml2ntBFjX2D+vASxTZMY/MUfVOXuNS+t8m1nqxmPlbH4Eo3Nb4ozc1z1LhX2XE4edwx7s2c5UxrKZCH6hb57qNPEXCmcdam8PpX8NVv4WOLXF5jaOkEJ8KlrojmpnH6Z85T5d5kLhNCspvUVUXwNCdQs3kSGz5sWukk01P/iMHlSxyvu8lcvIW1dBNbeTtVNVGkesjHNbqC4yXnHG4Y4cbiG7zRcA1ZEqwngzyJdpEUblRXhqVcA+1tpXHSn+r+Lqm8i9++8j9z1GPsguuOmELi/oRCi25yrDHHkCgdr7Z06csWSNwYU3krlCemyTwo25Btdxg8WS7NcOsbV2nwmDhqrTP8gvk8C0UcsDcnVfw2nYtNW/SlairGW53D5B93HOxbPKgP+VXgQoZXEJD3E03TnisdegfAdwB5P4L4Hd2Dhsk9bzXpf3XDOv2z2Q4f3FA41m4SC8Lsto/ubLXB3ZFKi8Imwd/8tcLx1jzLAcGqWbgvCcGcRWQFzihtwfu8+dYErrp1OupG8ThLySoUVWdk6ghH2veIera2qsgqTr499lnCwXmqqjapChRcFR4gHnczu9RKS0NpFGsotMxfPf40LkcahzdFVc0m7s44buJEV2pY26yhJrC217di0tw8Rd/kJTz2JFv5ahRbHn9ojeCJFRbnG2i3r6AUlaq3aXlq6yMMzJ3kSOMwy2sdTK/XI5wQqF9ncvEYx5sfoqpFm3b2LFTFeLzcS2/9Y0xTYm61hYXNMDlVYz73ebrC9/EFIhR78hMCJlc76Kjbyxxc2axDFm6+NfMF2mqm8HnXqXZHqSYKQCzhZX69mXD1XMmz+WTX95iKtTPc/8uV7wk47TO4PqLCssKJlgSzdS42TZkOh8GD6cqxoEiCxxMKkbjMuS6dSb/MmlkYP0FTlKXOFKRFM/mgT+VMp8FsQGJ1Wz9kM3k4X2k1b2VV5oYcHHLEWKu3sabsWa7/WXcah1JuSViLruvPZfV+lLmQ4RUE5KdZyC8SW/w0gvgdeR6QPUiUxQ7PxbeuLTMQaa743WcT3B8tDLyhKRn7vOBUT5wB1UNi3XpAdrsFEUPm4aSGQzN484zBtbTM+RrBrWEZjz9Ge+8EjU3zXDp7ndq6aMn5a2tBsnkbNb69sDS7PY+9JsPVwbdQHQbe6jSO2gR2f5pG0swvhHG509hte34PrzdJSnHwaOoImiKzsuXHVp3H27hFoHeTuelmTlSX+lNrg2tsrFehr6lUeTeZXWgnLVWRcICvZYvHcz2caC+1ehvDS8wsttPGDG5XHMNQmI62EU0FMWwy03Pt9LY9IhiM7J7T0j7Lw6ljHA8PoWoFUDZNiWiijvlkmNXJLtx187hDaWpCe89ndPoUPS0PkIsiKTyuFHPJZm48vkxOkXFVJ/HXxJACMZqIsbLQgc9bGuLn88RJJf1MLHYSz3jZzAXQfHmqQmv8g4t/Tibt4MHiPy45R5EE43N7q5vBWQ9VUcH5IzoYlYRSAGcDBrcWt1n2xlV8DsHFozoziszdOQv3gIDVlUIf9yYUvHbBpaM6faZKl2ISMa1cJRmGl90QA+eG4OKRLLckGy7V5HVjgDt30miahtfrxePx4PV6cTqdFS7Cg8YhZ7PZj3Tpph155QB5P3kW41u5SJLE1NQUqVRqX4L4HXleC/lpujsuEbvdzrWBw/8fe28eHFl21/l+7p57KlNSaSuppNr37q7eu+22zWCbCcYzNGYw+C2ewREQ7w8TLO+PF/MiIPAMOGw8Y/wMQcAM2BE8xmMmWAJjDMZu3O7uquql9q5NUqmkKu2pJfe86znvj5upXKu72pgH7vYvQlGlq3vuOffcc76/5fwW9ptVZt1R3lzeAAAgAElEQVR2CeRkVvBiy8ZxPIULryd5cp/DVo9E7JmI5NzN5thtT+PVKzbPvGuB/Y9e45GPzDE40gzGWJsdIpPJo+nNcSb6C9QKSTZySTbLUQpuGjXtk9hVxjjpk58ZYHDgRlu/o2Mr3F3fy2jiDprmc2d1grydgYTEGqli5KLsOtEuj41P3mV+8QQTI1fRtQDXM1hcmWCrmkWNqJiVMiNTq1hs7cTBjE0tcW32AY7ubwJ5EKi40uLcrScJFJ/BvWuYQz4ZQhAUgcL12SMc2d9emHRi6i5nLjxFRLcxkjraYBmr3ybbv43nGKyvDzOVbB/zrskFzl97mH2j8ywVd1MQcSJ9NZKDZSoRE6Pqk+5vj9rLjM1z49ajHN73KsVakoWNKUp+CivuEliSvmiBkcxKW5sPv+9/YgQRvvydH9+59mhfwNnl9m28XVVYvKEypEpGM4JltwXkJKx1uEMWbYWz53Q+uN/jqqGx2OFp81Am4ML15norOQpnzuucmvDZKPfeE0GLU03NUzh7yeLgcMC/fZ/Pux8+AdCWJKiR0EtV1Z0sbslkEs/z7suG/HbI9AbvIEC+X9BsJIhvhEw+8MADb6oGfbfBHq1UrVaZnp7G930OHTrE/FqaP/1WyPGffjzgUqBS9hUsTXLtVu/TkOKKwsxthXc9LnjNUbDrLnLH+iRnVnz2nFpk/OQC4yfnGZxaR1EE2xcH28AYYGj/GrNz+5kYnkdVBcuLYxRqaUiD1ediuirpqdW2Nn0HNrh65ThHjl1FVSWeq7O8OIbvplgrjWFm8qT2lEjR9DAIhivMXD/AgSPNKDzf1/AUwbevvI94qkJqrIA+EZCsH6bZJYuVuyOMjLeD1cDUMmcuP4lluXimTny4jDHmo49VqCynsZ0YZqwJiqom6Z/KcXXmGNn0Fmv5YRzNIjFUQX/IZXl2hH0js5hWk4kblocchNt3p5gav83G1gBb+WG27TjmoMPfLz3D0SNXGVSbprF4skpZxlnLDTE0GErj1UqS+bvjlL0k0+ee5dCpa1jjDhbN77C9kUErCPrS+Z1rtm1xbOosH6kO8ZVXnwIUVu7haz4ZEZy+qhMxJU896HPGDfNnP5zxOXe9e9tnIoLnX9GREk7u3+b1SN+Oi5xb6LodgKgDV86rPP2Qz8u+hl+//1Aq4MbNbjPDfE7lY4ea82lZFpZltbmiBkGwA9Krq6vkcjny+Xxbus1kMolpmm378u0QpQdvM7c3uHfGN9/3OX/+/D2zRgkh2urmBUFAJBJhdHT0Tft0HIcrV67wyCOPvOm9UkrOnDmz41Lnui63bt0in89z8ODBncX5v/2qzv/8VlMyGMpKdp8QRGIKL13sURsubXP7RnMT7B71mfjgGu6eFXbtWyYxso26IRnet9rVNnduF/sfmkFRJb6nUdkYYnmjD0fV6d+1RXKw3Xbs2xrBQoyhQ01bZ+BrrC/uYmstQzJbJj5RRrOaKrydixFTKqQG2nd34KksXx5DNwO8iEZivIoaCb9f+W6WwcFFrEh7NJdTMQjWDAzDYzM/AJaOMVZDi/vkbu5i/4HwXVqpuJYkq2/T15+nmBtkIzdINWJBv0NtLcHUwW7lfuPWAFNjczv9F7ezLG0MUZMRFAnjh+52tVm9OszRIyFTalCpmOTO4gRKIEkOV4j0V2iNUl6/MsTRE1e7n7U4jOJINCVOUYP4WAlNl0ihcPaPPoyz+QDfvt6tEWUjguqygu01Aevg7oBgDAwbbqx2S5xPD/m8dLkJ1PtGBPoeiWZKrt3sBvCIJokWYLueQnVySBCbklyzNR62As71sF3/uydc/p+f7F0R/F50+fJlDhw4gBBiB6hLpRKu6+6YPNbW1lhaWuLFF1/ki1/84lt6fi+anJwkmUyiaRq6rvPaa6+9eaM3p3eeHzLcG5A7gbD1emuC+MnJSXRdZ3FxkSAI2NOSMPteFAQBr776Kk888cR9jfH06dM8/vjjzM/Ps7q6ytTUFCMjIzsc/9YinPioiegIAtFUydOHBFcdlc2O0/MTiQoLusvAo+v0Hd9kbM8SfXvzbfcEnoIzE2P8aLPkkPAVNu8MsHU3SzRbI3awhtICpLVchIRfoW+k/Vm+q5I7N0TfgMSJ6jj9Dno0bFeYTjM+cQcj0q6R2NsWcVviKbCxkcUxDcwxGz0uKF5JM3WiGxTd5T6S6VUiUYeNlQG2tvpxTQO130fLw/D+la42G9OD7N0/uwOKmyv9bG72U5MWyWSZ7GS7P7QUku3ZAfYebPcYyef6WL09zECqgpfxiA41/Y5dW0fZMBjc3d3/3fPjZKNVtu0Y0XEfbSD0wKluR8lQJZrpFjkrN/oZGL1LeWuEVSeFTEpiQ2UquRSDyXUi8XafZxGonPncT1G9c5RXHQ23Za10gmuDTgz5pCLwmqPhtLhLxnSJvg3FjkhQRZF84LjPC5s61Q7/5qeGfU5f6u7jfac8zq3rFL3uZ537vyrsH3xrEHL+/HlOnjzZ047cMHn81V/9FV/5yleYmZlhamqKkydP8slPfvK+Yw46aXJyktdee42BgYHvqv096J3nhwz3PmXtvC6lJJfLcevWLTKZTFeCeF3XcZz74+Zvpa6eEALXdTl79uxOqabOg4z/8t/1LjAGeGy/5DsvaqSTkicfE1wwaiRP5Nl/cpn4iVV2p5rqYK1gIWazZPc3D440QxI5VOXGucOkIg5bMoa6J0DbK2CvZPNmloi/hmY1JdLooE2tGEG5M4AvJdsbGWzLCts9Kdg8H2PkwN22hZQ+WGBpfozRgTWMmM3W3X62NzI4poUYhH7Xwzq+SYzmeFMnCqzemGT48DwAUsD2YpbtXBY2RonvKmOMVdDHPPR6uyClsjk9Rv/BJVrJSnjMXj6JbUqMMRdzxEMbCUhQxd+Okl8S9I01QVFRFTL7N7h54Qia6WMrEbQhD3PQwxj0WL+dZSDZXvXDjPgEg7A8N0oiXSa3touqjKIOBEROOdw4v4u9D95qk4RjmRrbm3HcDUgPFKgUYqyvDFH14uiZgJmZcSYfnidO0wCbHC6yMj/MqLqMFQ2/i121KK3vIvWeK7z+pQxDy2P0HRBcKWlENcGVmd42V81WeOmixtguQf9hweV68qcHBwJO94i4nMoI/vYbBsMDgkNHBRfq96uKZO5Ob1NJbUnBWpAcP1DhdbeZde7HTvpvGYwhFHbuZUNumDw+9rGPkUgkuHv3Lr/0S7/E66+//n1bW+9tJyEHQXBPW3Ej+m57e5vp6Wlisdg9E8RvbGywubm5U6ngzejNIvsaDGB2dhbbtnn3u9/dM5BkOQeHf9LE9ToBWTI55VKa2Cb97m1STxYwd4UbVN3QsNwSsdF2tz7pKgRXdOJDFbaXM6ipCJVRFVIS94LJ0IkVVL39k7p3LPrMPLHhKvZKgq2tDCUthjqoIFcE2eObdFL1lTi7H76LWi9vX1hJ45fSbOQjxAarRKfafbQDW4EbGkMPrrVdr62mWL+ZwexzUCYEeqap6TiLJoORPOZA+7OED8VzfSgG1HQLZbdAy4bMUZuJkZpcQjPamaW9aRKr2qiaYHOtH1uNYkwIRCbAez3KyPHFrncszSQZHV3GjLsU11IU1rJI3aKQ1rAcn/693fOSP9/HvlNNqdsvxlhdHqRQixFNOKQPtEvKUkiql5JMPNTuElgrRVi+PoppeShZiTVao5HW2q/pfPMT/5LcpWEeOl7BHISXb8S7xrK/P2C2IznV46d85kwVJQ/rhW6AfXzQ5+UWSfvRB3zmLJV9GcErPSTwoYRg67aCV09o9dBRl8WkTs5Ref4XKzw0fv/FgBt0P8npAf7wD/8QVVX5xCc+8Zb76KSpqSkymQyKovBzP/dz/OzP/uw/+Jn8QELupiAIOHfuHKqqvmmC+LeSBOjNKJ/P7zCAU6dOcf78+Xu68vzO72g8Pig5swqyLyD6SIXoo2X6nqwghjyS11XSQ9sY2SbTEQMBpUICb8YgfaBAbT1CcbmPQIlRGtVRFxSyTzQAIwRN8yGXtasjDE6toddroQUbUbZW+1gMRjHXfKIP1KCeFkAgkWnIXdjF4EPtOWHlCFx97hhG2od9oIzInXbFtRRiUSW+uxkCrUUk4pjPwgt7MGMSmY5QGVIRwxKGoXAlzVCs3RRg7XbZWE2TvqujWZL19SxSN6kO6iiPCORljcGT7eMKDlRZvzrG0IFlVDPA2UiyfjdDTY3gpTXiQY3EQyU0vJ0MycbxGoUbu0kfboJyaSVBvtzH8qUREmNlrD0ODIVzaeIT2Cqbc/1doGzs9rj4/AOYKQ85DPqIDynQCSivxTC2XWKZJoNRVIXoyTK3X51CtQS6EaWUUGHMQ3lMoTiTYmx4idai0HrU54d+62/4u//jR7lyvZ+J2z6P7vN5db09ACirdMtSL5/XeddRH9eEddlegWY0JXitA8BfvaSTTkgSx3oEJAH7U4KX/Oa6vnDNJBGV/Lsfc78rMH4rVCgU7iu8+n7opZdeYnR0lPX1dd7//vdz+PBhnnnmme/Js9+M3naA3ItaE8QfPXr0vhIMvVW/5V5UqVSYnp5GSsmRI0d20n82TBydqtidvOT37nr4P+yR+hcOETysyQJqTOwAhnZEkF/qI7FQJrontCt6OQNvMU6eKAunVSJP2VDPEqgQAmbu5UEGHs7RWndSG4a5c/vRzIBgUoUhBepmM7doElzVSRxrqs+KrqA8JFg8vRs9EqBGTPL9FowrMA7BNZWssYVOk5HpQwHlQpzgloYe9amupCkSxZ/Q4d0KylnI7t+kVfmyTjisXx9mcE/ILCp34pRWU1S1KHMZg7hTI3Y81AbUOjIoDwnWzg0z9HDz0LJyJ06pkmLxzBB9h6uIoWDn/XQE1UIUOaeQ3NvukpaPq1TOHiLI+pQzBmoLg9mazTJYzqEnmmtDjQjEiEru+gi6qpGrxghGVdQhCbsgf85kZKTd5GENuRTW+tALEs/R2F7OUJUxgl0aykMB6nWF7JENFAQN5IsdqLJ0czfj++6i6JLaZpTtpQwVP86e/3sW8duCS389xNwdeOikz3JEZa2qMpYWvHa1l9ovuTunsrCk8sDxgFxcYbleAmwyLlju4V+8b1Dw3F8bHD8SkO9TWKyn6ExZkgs9+ijXFD788D9sH90PfS+9LBoH+bt27eLZZ5/llVde+QEgfy+okSC+WCzu5ESOxXqnIeyktxpqrSjKTu4Lx3GYnZ2lXC73DCZpuL45qsZLHjzvwR3V51uJEtofgkZYab6Cin83RSpTRrZIxDKpsP76EOaspLZbw5syoOGCKST+ayaJR9pBRn1csHZmmIGEy+p2jNqohZjQ4HFgXTIgikALyKTA3m+iXU6jDtQo305iE8XfZ2E/oqFfCUgfzLcD/FHB1u0sA14etd/DuWsht2Pk/AS1rMku2yF4uN3kIJ+AyoV+4g81pUt7KUK5miR3eReRURsmJEzUxwVUilGsGdAOtJtovF0GW6/tI69IvD06yoSy025zxiIV3UZLNZmFlhbYqoV/YRDbNajpkZBR1BlM8qLEPNgevGHu96gsDhILNnALZp1RxPB3azCpkF11UA6V24qqmg+7bJwbYeDhUOqv3A0ZjKNFWE3txuivYTzo1d8vBGB5BLYu9pN9sGVeVqKoWoLzZx4kOmWj7RY0YskNPIJfncN6PYVzJ8qFyzrxqOSJxzzQYUl0m8ce2Rvw2qvhB7z0ukY0InnsYZtpW+P86z2gQYJdt7K8fl3DMiVPP+lzdkvjxGDAS/PdbR46FPDMQ9+dpvlGuWQ6qVgsfk8AuVKpIIQgmUxSqVT4xje+wa/8yq/8g597v/S2A2RFUdoSxLfmRF5eXr5vqfe7qavnOA6Li4s7/R49erTNhOJJeKWq8OeR3fyHisbLorUwkI45n8SarKAONNU7Z1yjsJomOeeyVbGoxaI4wyZMKii+JDJjY061nMCrCvZJA/NCH+p4CXs+Sq0SpZaM4u0xWVoIiE2VUYdaVMhdCtuVFNGrNSLHavg5ndpcjFotytpAhOBKgPW+diD1T2hsX87Sd3gbNRaCj7dsIDaj3Lw+gJoN4BR11T6kNS9K9DWV+CPtGdycYYONv5hA6Zc4UxZilwph0QzKCzH61gvou9qZxZYaQ/tOBKFokIxQGLPwh1QYAuO1gPRAu1eIOCDZvJ4l422jlHUqW3EKxPF367iTOsnlKpFj7e9YelBBPj/AwHs2kAJqd2NUN+J4apTy+jjpfSW0iXZVfGOXRWYa9IOhq6AUINcibPlJVr8xgPmQswP4O+vijkVC3UZLt6w3HSqJOKXnEgRpDX9Mh5H6WtoP4kqc5Giu7dBQG/Q4/EeXufpvH8Rft6jUFGYuaIxYkslswPxWiwQrodhh9q7ZCq+8FOGpowXWUwlmN9ol3mMTPlcvNiHDcRVeel7n8L6AWvtxwA790k+7fLfRzG+1Wsj3IjBkbW2NZ599dqf/j370o/zIj/zIP/i590tvu0M913V56aWXGB8fZ2xsrI3DXr9+neHh4fsq430vN7leJITg9OnTSCnZs2cPu3fvDs0SEi6VFJ53VZ6rqLxUVajUvSdOxB0WU7DdMaPKWkBMqYKr4K9aKJaO3WeBgHjVo3Kk+xOYlxwi76tCWcG/bmKUNUoiiikF9kHCJBKtfawL4lYZbV8ouajbCvYVk7g0KFUVnEe7N4F52SHynipKq/0yp+Je1MCE2rCF31IWSskLYk4F/Vg7U5OBxDwrUCxJzY/iDEbw0mF/+lWP2ONllM6YghVIa0Wkp1JbjqLqETYTEYK4RuxOFeOJ7qoT+vmA9KN5UEDesSjOxbBlBFs1iR2oou5qB1JZkyQWakRP1kE5AGfGorYUwy+oKE8JZLZdWlPmJH3DBbS+pgQoA3AuW2irEnOPzsaghUy35JJ+zSf9VLfbm7ihMWBW2diKoiYibGYsZEZF2pLEnRrR493Jq2IXAyIn8tTuxhD5OJteFGfMxC8aVP7XLHJL412HAl58XkPXJU+8V/DyuooXKJyYCLhyodvEELUCtE2o1VQef8bn3JaO0zikGwm40KNqzVOHfc48r/HkewIu5jWq9QPp/bsFr36xwn0ma+uiarXK3Nwcx48ff9N7f+InfoI/+IM/YPfu3d9dZ//49M70Qwao1Wo9VZ2ZmRnS6fR9l2G5H8+JtbU15ubmEEJw/Phx1o0Mz20p/P22ygvbKlu+wkP9gjlLodDhyjYRCdAyPnelhFkVf0EFoUJEAUeBznUoJNptn+DpOphUgWkFcipKFeRxDeLtfahrAepuD7+/5fo2cBUM2ycYMhGDLeqsECQ2Asonuw9h9Fd8jLhHBIM8FrI/BFIjL/AyXps0DEBNElurog4E6EsGlW0L2W/gJ3WY9+GJblVWm/aJnyijxCXBrI6+auBLg0pgIvtEaL5oJV8SnbUx31XXEgLQ7upUF0xkQcU7qiL62hFBXRbEB0uoI813lB4E0zrGSoDMmpSyFiLRbGdedon+ULtkD8AsxP0apmOxXolSG4sg4yrUJPGtCvoD3WYv47WA1KN5nJtRamtRbC1CbSyCkpMkD5Ug2f6OsgrJ5QqRozbSBWc6Sm01bKd7EuW93QwpuKGj/GwGbquUSs1vPzEhSB2Q6MDFHjbfk2PbXD7XFFjGRgOyhyVlqXD7evf9qioZEpKVegGD4WHByHHJhRWNL/yyzcd+9P7Nfp1ULBZZWlriyJEjb3rv+9//fr71rW8Rj3d7mPwzoXcuILuu2zPB0Pz8PKZp3lf0HbwxIG9tbTEzM0PJzLAQ289f3apxoZpmOKMwryvkO2rZ7UkIlIzCvKegINmr+IxIDaco2NySzEU6bHwBRDYk9gMtz3GAWWDNJ7Y7oBozaEsquwL6Pomf6vj2S5K0WqW8ZRCgQ0YBVQEPrKKPc6xbIk4te5THBWJWhaIKERXSCiyDelAiEh1BK6UAI+5jjwJ3FVhQoBK2iwOVQ93r0VrycR4IQsOZAOYVuKuiFCXqhEIw1DGuqiCme1QnWq75wAwMljy2pY4/aEKsOSf6UoB/3O8qAKssCVJuBXvDIpAGYthAmBp4kkTNo3ywe/1kZm2CkzWCGwZWVaXoRvH7LNSCQOz2oVPxqkmypQrBUQ/FBn/WwMuZ+NIkISSFB7vnJL7so02WUVL1itI1hWDWgHUdbEH1pIWMtgsb2dkawRPtgSOyphB/3mDl32ehQ7g+uE+QzUiubaoUWwKMNE2QdSGX6xZmPvhBl7OzOoVK+98eO+Dzygvd6+fRJzf46z+xsP4B+X62t7fZ2Ni4r2IO7373uzl//vx925z/Ceid6fb2RvRWD+p60Xyuwp+8tsG5fJpt4wlelwZbrgL1DL+rqzCREmSzMGc3v4HiKUxuCI6Y8NK85JZjEHqoqiAkrDnwUAtqaOAMAF+vP1oAScAAsjrVOQH7JG0V4kfAX1JglXATbhIugzQU/ChqzYNDLQvWAKdfh9PAU/X754Ai1OLAZVCP6oihVn8oELeV0MbbKOy8BCwrBDVIRFXK40ZoJqmbSioAZ4AnW8YqQNUUUn8HRalDnwoxBdIg0xA0PNhape6YSq2mw/MCAgV8FVIKRBRyphWOvSOw0h/T4LwChz1YVGA91EJkHxQq0XCMrQn1DIWyqsMrATwmwsHPhFpIxUzgXkzCkyotxbwRg1rITIS3c8hGCSKLClu302ivCYITBhjKjm28APAdH55p1xJqKR3l6ym0SICSNHCzBlIPbeNUJCx6cKCdWWztj2L9jUpkwEd3TPKuSZDRKe5W4dPA/0nrYQUxw+HsC1EGBgQnDlW5shQHBR47IDnz7W4peHRY8Hd/adDXB4+d8nlltlkINXe3N848dHgby+yu/P1W6H5tyA3h6/s99Sa8wyTk1dVVKpUK+/btu6/nnD17lsMnH+PF2zrPzwac3vQ4v51AtjC7sbQkNiiZKbdz5v1JwVS/pFRSuL2osFYI26iK5NRklddqPVSrOzYMakSKAtXXqQo13MQbwBTQ2WTbhz5Q+lWMJbBKUMqrIWhLoNMtU0hYdeBUaKTVypLglgJFwAYOE7ZtpXUPba9K0KK+swbM1/8/0DEuXxL3PCpTHaJRALwOxAjBISHBCuckWvSwx3VktGND5QGzfv8axCM+FUUFTUGvSPyDPaShW4TAb4M6L2HDwTI1nJKG2Kt2z2Gx3sdk+KtWFUSWA+wVlaCmwFEF9I5x3QKegLbc7gUJVyVYAqJ1bUKrt3MIzUu9NO/X/dBEkVcx4uBFw/djEThan69WKknQPIhLmFNJBFB2NOhTQobzeI8+XgL+AxDA6LDPyryGbAmdPnK8zKZmYVYVFnuUdHr6kYCXXmi+7IMPB6xJhZF+yfkXuwE8mfD50z8+x8MPh5KtlBJN03YA836l2JWVFXzfZ3y8OwVtK0kpeeaZZ7h48eIb3vdPTO9ck4XneT1DmTc3N8nlchw+fPhNn/GlV1Q+/y2b6XyCoG777U8EDE8oXN1qX1BRXXJsoooQGhHPZOGOwlJOQVEk7zopeaFHRq6Hx2u8HkRgI8BZCEJA1A2o+jCuQaxjoReBPkL/YrcuKW0KcJUw+/x4DwDMA8darpUCWPIhH8CAAYkO9N0gBPFOEFhwUBRJ0lQp1gxoAKdLaDLoAfzquoM1ZqCuK1Q21dBkYBGC+VG6gF/Z9FD3qAhLRc7XmZBH6Odm0S7F1vtIOgGlqTqAVIAFYItQO+jBXCJlH3uXFkrVDSoQAqwAJSWRCUJzDoCUkANO9thL14AExIVNxdUgVe9sS8KoEn6rVmqAclqiLTvIEmgxHc/SYZnQI6WT7gIngAgoSz6JPJTW6usio+z4RjfnhPBZjRxXQobay5ICl4D/F5540Ofs6W7QffDBLRTd4cLVIcJJD6kvFWAXVGy7fQ6iMcm731vi776dQMr29f2zP7PBZz9loSgKQRAgpdz5aZCUElVVd8C5F0jfvXsXTdPe1MToeR4f+MAHvldJgP6x6AeA3EmFQoG7d+/e16ntVkXwY79b4ZW19gQjhiZ57LjkWl7hQFJi2LC0CAtLCo8crnFuI9pVdPSJQ4JLFYWarzCVkgxIG7cA5W2DWwWlC3wV20OmAsiGkqwSSOSKApsSM1rDjUeb0hdAIEnqVUpDHeJfFfoqHvlKAFKDiE7DB8ms2rhjBlgdwJ+XaEmfYKuurisaWDo4AVbSwcl2oHUACelSHjVg2YdcADUFDIOI72BPRujye9qUobklojaZy5YAGyKjKnasg7m4del4VwuQVAQsemQtQVGa+KbWJrEqOYk8Qpd0a67bGJZKZd0McacxZQ7E4j7VgW6wSlYCSnEtBLcKoaEvBuRkyFy09j60okQZAT+uhG0aJiQV4mmfymB3H8ZGgHdc25lT7hIyr6pEnQgQ8Y42ZbD6JU6mpW8PuANp6VGwBcR1iLYcTJ4TBL8Pgd8JfpJDh9a4edPnyFEdO+jj9p1w7T18vMS5V5N00tGjFa5d2+bQIR1f7ePWfHh/xJJcu1BjqMe5eWNPCiGQUu7820qNupiKorCwsEA8Hn/TQ/iNjQ0+/vGP89xzz73hff/E9M4F5HtlfGvkHH7wwQfv2bY1+5tE4S/XH+ULZ+LETTiUkcR8ydpdhcF+eGVJwevIgvXwwYDpikrJCa+PpyXjcYnpSdYrCtc6k7LYPsQkZFtcxqRErpTDRA2qAVqUHd8hIcF2YaK7cjQbBYirUBBErCi2b4RgWHRg3ACjo+9iDUa0ENBtQXnVQjfAsyVEBPR3iJi+AL8Ck8lwHDlgE3Tfw68EMNWj2u92CQ7Em4ePLsRLPpW7bggWSbMpkUL4bvEABjueVQ5g0wUUEHUmUW+nFIrIQz2SyeRcGFRgQ0CZJnOpOhjDKl6sx/t5PuwxYM2H9QCqCnpEJaYEFId7lBJad+CIGZqWfAnLPtFKgO5DSbMgq/Tuo1YpHboAACAASURBVNWk40qSJZ/SggYJFSJAK0/KuXC4boNupU0JvhIyCZ+QSeiE36biwoEea+SbwJfbL508WeXy5WYAjGHA40/GuDrdh6gpFArdWHL48G1u3AjnT1UlDz6kcHV2hP/9fxH81m++tXOaToAWQiCE4MaNG4yMjJDNZndAupckfevWLT75yU/yZ3/2Z2+p3/+f6QeA3Emu63Lp0iUeffTRnu0alaTT6TT79u1jbm6OwcFBXrk7wL//dYN8R3WE4wcFa1Ih1+JWNJyWnBgWSA2uXVNZXm3+bSAjGZqSXF3uWFSegKoDQkF1FITUQ/AQEmQJxnqATdGGURM2feLCobKtgxIBpwJjUTA6JN+KBwMqxDXUio/I1cJDIlcPVfh0BwD6AqQPI3VkkBI2arDtQRBAJgWdBy5FGyatdoB1A1gpgqWiqwY+0ebfa34YYZjqeI7rg1/DMlWUioUMFBxRn4+aA3t6AON2EQ4l0YqCYE1F91x8TwNHwIgOVsec11yMfomXskKPlpKPveSH7oZSwlh3H5ZdxJlsfgvFE8hVH7Z8SGhgtXu96I6DHDAI4h19lwMoueApEKhhu/qcRFwfe6LHQVbOgd16yCgKEsuUONJC8yVBWmtGajbIk+B5sNeE7QB13cdyPXw3indeg7PNWx94YI1Ll7pB9If+hc/aah9Xr7bbX6amHG7fznXdPzLi8+nPLLJnIox0SyaTpFIpYrHYW/J+KJVKXLt2jYGBAXbv3r0TBdtJqqqiKAoXL17kS1/60vckF/I/Ir1zAfleGd+EELz88ss8+eSTbdeLxSLT09MYhsGBAwd2wqtnZ2dJpVLs2rWL6wsKP/kfdWY6KkAf2iMYGZVUt33uzsDKUiiVpBKSAw9IznVU9zB0yalTHpdnXGorNahoKEEKGWigOjDUA2ycIsp4AhkEsFUiLhRqWzGEDQwboHUsdqcGgxrUVX/DtRGbkiCvhJJXvKOPIACzBoMtESRChuBb89EtLQQ3tQUo3ALsTrWDL8C2DVGFlOJT3FSACKgquFUY1CHaYY5wAogI1KiCWK/WpT0LMECvwFC3ukzJhqlICJzbNmx5oZnEEzCW7J6Pige71Kb67gvYrDMXE5RUCtliNyWQ4Lmwu0PCdHzYKJHImgS2Rc3Tmu9fckPA7NRCqg7RAQV7Q8dyPeyqGjIyIUHxYajbLyxFjeJwFKMi8Ja8cE6kCraAPWaXicRwXbysAYk6M9nww/OFigzNOD0ke+PVAO+sxsGDNtPTG11/13VJNrvO+rrPU0/1c+XKHkqlUCI+dWqJ8+e7oeAnf9Lki19M4LoupVJp56dSqaCq6g5IN+rodeZyEUIwNzfH9vY2R44c6Ur+1SlJN34+/elPc+HCBb7xjW90jemfEf0AkHtRq29xrVZjZmYGx3E4ePBgV+hlp99yoQI/9190llYVLBeWZ2HuhooVkTz4pMfL19s3l6JK3vWM5MWbCvuGJSMxsDcl189DubYF2X7aYl8BXSvjZ+smCj8IzQplQUwLqMoo6O2bK2K62DEJkRbwsB3ifo1KrQZBmrZTOilQ4lVkqiN8LxBY6jqOrUFFBz9Jwysyla5SjES7bMFxo0QlZkChim5LdC+OXbbQVI8gG9qR28hz0AcC/GgMtVYjaksqqxo4KvEBn4reIaVLSSyap9qX2fmdfHgoaSJwLSM06bSSXUYZjyC1FubhCcg5pAcCCht1F8WGxOb7RAdcapEO27iQ6K6NL/2whLRrgFq3h7sFmOgRpltyUSd0RNmDLSc8xPPDKEtzRMHVO8DXF6i6hxiw6gzQhrwHVQXTFLj9PfooOqG5o/EtAgGbNmbVJZKJUqzpoGttfWD40N+xNv2AiRtVdpVrvPpqd2DJk086nDnTBOrBQZ3x8d0sLMTJ57cJgm58OXMmxcmTvd3UfN+nXC5TLBYplUqUy2FoeTweJ5VKoaoqi4uLjIyMMDExcV8ubOvr6/zyL/8yqqryyU9+8r4CSP4J6QeA3ItOnz7NI488ssOJ9+/fz8DAQM8FsLi4iBCCiYlmJIKU8B8/q/Gpz7W7DgE8+FiR15eS+IHC3jHJaJ/Ay4dCysVrKtWOKh+o29AfBy3cLCo+hl9EdaFm6+AkOgC7AhkJVjuY6rKCLzywVahaENTBWfqQLECiI7udlKAXwTSg5IcBHE4EhA5mDjID3QdxogRDcRAiNE2UAgxfw6v40B+DTrARThgoEa2PpWqHh38rTnjQFumoxiAFWBXItkjEQmA6FdxSKQRDLwmtRVxFCYbjTXBtUHULohLKgogSwy7FwvfpHNNO3z6YZYiYJH2fas4kqGggddBKMNjDZOTkYCJ8B6XkI7cDVBuE7UB/oh0UAR0bPykgXgf+INRAUopHseCClQStA8yCHIwNtl/zBPp2AV9VQ3NTC3MxlQp+v4nQ25mUKgKEqIVmjLIImYsSMpdJ+yalGYvNzdY2kj17NlhY6AbqH/5hjcuXs6yvt3/vD3zA4M//vIc28wYkhKBYLHLr1i0qlcpOgYhYLLZj7mjUz2slKSV/+qd/ym/+5m/ya7/2azz77LPfDz7I71xAFkL0DAAJgoDvfOc7mKbJ5OQko6Ojb/gh38hv+at/o/Lxn9cp1u3H42MBmUSB/v4Ii3MRZq63g8TBo4KyhOXV1grAPogNMCRUo1BL0nAV0E0b3xKgd/qgeRDLg1BIagqlNbMOVAJiZTA7wENKiGxAXz+KU0KtVRFlHWoZpF6FaLIbfLVN6E+DqqPjEJTzRKVGddMAM9YtlSplyKjh3+p9WkoVZ8sDTYITB9GyqaQPySIk2rPgIQIQxdAjpKKCGw0P8ACMHGTvwSgyJpRdKEt0T8WvxEEthVJhh0aBcCEdQCwCVYe4cKmsueH8xx2Id4wJQC3Crvq8CoHlVXBydmjrjmZB6QD4oApDVjsoex4UtsCQaEGUwEuGZojGe8dtSPfwTXfW0KIGscCklLNCpokSztNIj2/nFFFGo5hS4qwJqCioAQhHhb76e7fNh8C69SoHMwFXrowDCqdO1Th/vj3LHUAmA7XaCrqucPLkJKdPZ2ngzN/+bZJ3vas7o9wb0fb2Njdv3mRsbGzHViylpFKptJk8XNclGo3ywgsvYFkWX//61xkYGODzn//897rM0j8m/QCQGySlZHl5mfn5eTzP46mnnuriur3ozaqG3JxR+LVfDzj9vMPqSvPQYWREJZVKcfNmOygPjwr0ZJ7FBTs8FfdNQt+rgHhco1LpBAOf6ECBmhsFvwS+D75e35Qu3Y6oEM0UqJEKJU6/BH6t7h0hUJSxLp9RM17Cs2JI2fDisMN2soYikki7j9a1pBk2QVQBrQWEpASxBZZLXLOo5KIQ1M0PSgCxGpgdJhIpILaBahhQqyFKJtiZEIytIsR6SKX6BmQzGKqDly8QVw1qmwmEF0DCardxA1AJvRzMGEiBRRlRlngFQNVA7TGmxDYkmxqFIgJktRYCubDAiTaBFEDbgIFsl+kJtxh6S1TADDTchpRODbICzA7wFQHEa6DrRDwXe9OFagyCGERzkO6QlAFkEYaTICWRoIq9Hp5J4ADZVA+J24WsbGoIgQ/FEhQ9uDPN0YM1trYmiEQKzM93b/Wnn/Z56aVmWrejR9NUq3sYGkrwrW8l71tK9X2f2dlZqtUqR44c6Vmxp+01paRarfLZz36Wb37zmzsZHScnJ/mLv/iL7wfpGN7JgCyl3InW29jYYHZ2lkwmw969e7ly5QrHjx/Hsnq4BHVQPp9neXmZo0eP3vOeYlHy8Y87fPWr7V4dkQicOpWgVFJIp2vk82Vu3izieQHJpEqp1JmJRxL6Zu2p/7sJVDFNgWUJSqVRur9pgTDhbyOD8iaWVSISq1HYHqDddwo0rUIQDBIiRYMqDAxtsVmQSNdsk2RN08N1LTqjHAzTwTMKIYj4AnwjVO8JUFUfIbojFuKDBSpuHPwi+E4Y9hyYpFIuxWI3Y4n1FalSN9kE5dDdzvfrfY7QFfWhVdH7NHzRMNcEYV+iQjKtU1rLtkvpBFj9NRzRAcrCRoluoQYKQckCu4+dQIl7MQptk8guC3uriOoo6G4GtxIBrYqSMpBd4Y91UDYiKE6BuCopr5lQMyHqg9FDUm4BZQ2foGxDtT7/WipkZK2k5mEg0c6kPBdqVdAhopjY+UiTkUgfyqd58HiZSETl5ZfTbcw7EhHo+irlcjsEGIbC//gfT/AjP3J/qS83NzeZnp5mYmLiTTXUBq2urvKLv/iLpFIpfuu3fmunwMTW1lZXrvF/xvTOBuRcLsf09DSWZXHgwIEdLnzp0iX27dv3huWbGlQul5mbm+PkyZNv2t+nPuXxqU85HDqkkMnUKBS2mZ7e4JFHdnHmjEqnF56uVwiCSaRUMQwHz8sRArFNGI7XYYPUawixG7FTxSEgTNuWJwxl02mNsNI0G0UZxvc7Gc824dpw6s+oF65UPXS9D9ftnBeP8LNrhAzAQVHCd45Eoth2Z0YdQRgFMU54qrWJaVYQwkXVdFynV36DIiFjUevt8/T1lXH8GrVKCmTnO5QJk0I0JStN89CtdRwhwFPrdvTGfHj1+emwpSsBROtJgX0PfK0O2pLQtWGCLrJyEO2HoAR+tc6QdHTNwfdH6UwPY8UqOIYOqhWmlPNL4NvEI5JKIQqyA1CUABJF0FvmVdTAK2ElKjiFPnDStH7r5GCekt8RGiglidQ6ZdeHqormpgjsOtBrThiu3ZnnVHpMZP+OO3ML7N2bwjAmuXkzfJ+nnxa89FJ3he09ezT+239LkEwmdmy+yWSyq16k53lMT0/jeR6HDx8mEunhTdRBQgj+5E/+hM997nP8+q//Oh/60Ie+X6ThXvTOBWTP8zh37hz79+/fKZvUoKtXrzI2NnZf1QVs2+batWucOtUrrrWb/vN/fpHPfEalUGhH3wceyLKwECOfb0xflTDGt0wkkugKSw1BqVOSDYB1kskkQnhUKhWa31gCWbpjnl0iEQvXFWhaAc+r1Z8tgBTdzqsBiYRFudxHLFahWs3Vx+oQgl+nvU4SiwVUq2MAqGoNIbaAEiEIDtK5DuNxl0pljCagBIRA32AsKq1gE4l42PYQ3Uk2CvX7bJox1gq6HuD7abqTVvj1eTKBPIZRw/N8QgA16AJrqL/HnvrzN0kmK/W59wnNRZ3frQjspgHKqlolHi9QqpRC8AviHW0a89q6FmU4F5FymIrV12Gn2ock/B49cjtY66FXi2+HTCJoaC3F+v2dEnQZkvXn+iXwXXRF4tsS+A5QQFHgqacmuXKlj2h0i7W17irsv/d7J/joR0eoVqs7HhTFYpEgCHYO54QQrK2tMTU1xfDw8H2B6srKCr/wC79ANpvlc5/73PeTJHwveucCMoTlm3rR9PQ0mUyGwcEeNrkO8n2fc+fO8fjjvTK2NKnhG3n58mVmZqr86q/C7dtNm/LwsEE0WuX27RJNyTQkXVcIgj6k7ASQAEVRkdImlAib72OaGVy3835BIpGiXNYIpeAi4eYNSCQylMudkq/EMKJ4Xj8hWDWk7TKWZeA4vZL4azRTvFEf1zaG4aKqCRynl2fLCK0ACz6WtY2up3CcEr7f+p0EYVqzTlOLg673YZoupdIGIQgLwuWYoRf4WlYSx0mg60V8f5tQ4nXq93a+myQa1anVGgzHqc9HidBWn+14B4hEAmx7uOW6rN+/RchYGlpFSIYR4HndZqRQmxCAg6a59YCmRj7SGN1MswHKA8AWyWSFatUmCLT6WDpNYdAOygWi0W0UxadaderP6WFS4TnC7wvveU+WQsHk4sX27b97d4QrV57BNLuDPqSUFAqFHanYNE183ycajbZ5UFiW1QbQQgi+/OUv84UvfIHf+I3f4Ed/9Ee/n6XiVrqvl3jbpt9snNh20lspzdSofXcvag3zBDhx4gTHjgmeeKLAf/pPd5ieXuPWrRVWV8uNFoT5MpuSrO9LYBNFcbAsCymrOE4BKCGlIMwP2Q44rtvIoDNQ38RlNK1Cufw64eZq1QoUyuVtwk3cyAkQADVMcw3Pu0YnSDiOC4Qmj1bXPk2rEQSXCDd2A0hCBwJF2UJVhxGifXMmEsuUyzohWJUBF8cBRdEJJehWyU1FVdcQIkszO1KJIKgSBAJF2dUxVgXYIhaTVKsJmoylgOOU6etLk8933l/BMCSe1ypxVanVtslklimXqR8It+6fVTolYtuGZHKGIIhRrRZpZZjhd243zXiehmnm8Lw+LKuGbW/X57BhEsoSBHGaW1JFUcKE+FKm6/OXp8mc54EBSiVoBVRNWyMIGqDcmMMiYVKNFKBT28mPrKIoOaTcRTsUqIRZqc4BHvPzKywsFHj88T3MzKTYqjtgfOITk/cE47W1NW7fvs2+fft2clFIKbFtm2KxuJNXxnHCdf/tb3+bSCTC1772NSYnJ3n++efvq7LP243ethLyvVJw3rlzB0VR3jSlX4N6JalvRAg1IoYacfadJITk/Pl1vvKV63z967fZ3LSpVn2CIIqiJEkkBMeOxfk3/2aCj33sCLdvF/j937/MCy+ssrRUxLYDQhCwCDdTGDCwb1+E9753iD174vz5n1/n+vUCxWKrz6hBqAY3xiTYtUvjwQcHmZ1dZ2EhRxC0zo1ev791c0mSSRVVjeD7FSqVAiF4UL8vQ5cajERVB4hGBZVKAU0rEwSllvs7+b9EVQcRwiKUeqskEhXK5a36+3ZKkwJVzSJ2DuICdL1al4Ab79r6HSS6nsT3200CluXhODWa0mZTmzEMC8/r1p4Uxa1rMQWa0jaYpobnDXV5r1iWwHF21eesAYo1DEMAu/C8XnORQogUrUCqqhWSySSFQvf9yWScUqkVtBw0rUA6rbO15dX77lyX3YfDiUQNRYlSKm0TfofGunN54IFtLl2a3bm3ry/C4cP7uXnT4MaN95JItI/LcRxu3LiBpmkcOnSoy5bcSQ2Q/vSnP81zzz2HYRjUajV2797NV7/61beLdAzvdJPFvTK+raysUKvV2Lu3M2dkb+oEZCnlTkrBewFxJwkhWFpaYnFxkfHxcUZHRymXa7hujUKhQLFYxLZtLMsinU6TSqVIpVKYpsncXIELF9ZYWrL54R8e59ixdlualJL19XXOnr1GLmextqYwM5Nnfd0lm03xr//1BM8+O4nRktuiVHL44z++wfnzq8zPF1lZqVIuhxnwx8cjfOADo/z0T0+SToe2v7//+zn+5m8KzM3VyOd9qtUA31eRMo1p6gwN6TzxRJYPf3iKZ54Z5r/+1yt87WsL3Ly5RaHgEK5FhVD9Dg/Nkkk4dCjKiRMqKys2r79eYmmpQvtyTNFqR9e0gLExk6GhFHNzq2xuttamk/TSJkCSTsdQFIVSqUgQtFbXjtT76CQNwxhEiCpBUCYSKWPbpfrY290AQ1JQ1RGEUAAbVa0CeYSo1p/fnTPYMIbwvAZYBSQSDuXyBiEz9elkLLHYANVqu3tYaLMPaGg8rQCcSCQpl3t5PniEGlSDsYSMybJ0HGeo690sy8FxLrbMWUhf+MK/5Gd+ppk1sZGUa2FhgQMHDty3f/DS0hI///M/z9jYGJ/97Gd3znby+fz3pIr0PyP6ASD3AuRcLsf29jYHDx68r+c0ALnTPHG/YLyxscGtW7fo7+9ncnLynhUQpJQ4jrOjzhWLRVzXJRaL7QB0KpVqkzgaOThisRj79++/L9/q+6Varcb09DQABw8eJBqNYts2pVKJQqFAoVBgebnE2FiqjYn0cidcXCxx/vwa169vkUrF+fCH97C1tUi5XObQoUM7B6/b21VefnmN11/PMTOTZ2GhiOdFOHYsw0/91D6efHKobc6/8Y0F/vIvZ7h9u8DycoXNTRvHsYhELMbGJE89FedDHxpibKwfRYnw5S8v8M1vLjE/X6BU8nCcACktII1pSvbssXj66UE+8pG9xGIKv/3bl3j++RU2Nio0l1KrNiHo61M4fjzF00/v4vnn57l0KUet1gpeav3+5ncPGYuGomisrRWw7WrHjPXRrR1IUqk0EFAs5uvaR8Pc0YsRgapGUNUMvl8BKnV7eoUmY+m8X0XKkTpTdIlEqnXTSoFQkwjpJ37iMF/60r/a+Ra2bXP9+nUsy+LgwYP3VeVDCMEf/dEf8bu/+7t85jOf4YMf/ODbSRruRT8A5F6AfD++xa105swZHnnkkZ3f7xeIy+Uy09PTmKbJ/v3778vNp5OklNRqTSm6WCzuHIw4joOUksOHD39PJYkgCJifn2djY4P9+/fv+Hzea3wNm2Dj542YSCNA586dO0xOTt73ifv9UiNb3/DwMBMTE6iqusPkGj8NTaQxNtvW8DyNffveeA7v3t3ka187z+Kij+smeOCBIX78x6eIRttVciklp08vcelSjps3t7h9u8jmpodpxnnssQE++tF9PPBAU3pcWyvy+c+/wtmzi6yvuxQKft3GmyaZ1Dl+PMWHPjTBRz6yn3Ta4sKFNT7/+Uu8+uoyuVyNWi1AiMbZRBTTFExORnjyyUE+/OFJlpfz/P7vX+TatVYTGIRmsIYELUinFY4eTXLiRJYXX1zg+vWNDpNfALhMTcX4nd85xdBQhkQigW3b5HI5Dh06dN+eEIuLi3ziE59gcnKSz/x/7Z15eFNl2sbvk6ZL2pSutHajS9qkBaTQtEwZZxhwEJBhUJCLxRnBQT+BEajiuA2DHyqrojKKgoowoA4VuBhB7KCAVJQPmrZstnRPS/c1zU6b7Xx/lHM8p0mapE0r0PO7rl6Q5CR9k+Y873ue937u5403rDxk7lKGd0C2Z8Gp1WpRVVWF1NTUPp9PrYivX78OjUaDgIAAeiXo6+trN5B0d3ejqqoKer0eSUlJbv2ymc1m3LhxA01NTQgJCQFJktBoNLBYLPTONbV77WqzR+ZGDFXK2p+GkdQkwgyC1C67Xq9HQEAAxGJxvyYoe3R1daG8vBwkSUIikfT52swrEeqnu7sbPj4+rEmEWumbzWbI5XIolUpIJBKMGGErxdE/SJJEQ0MD6urqkJiYSCt/KLc0anx6vR6enp7037b3d/DmTSOuXWtHaKgvRKKfv28KhQLl5eWIjIxETEwMTCYLfvqpDdeutaOsTAGtlsCvf30PHn44zmpiaW/X45tvalBa2oGqKhXq67Xw8AA+/ngG4uL80dbWBrlcDqBn85vH40EoFNKfn1AotPr+WCwW7N+/Hx999BG2b9+OadOm3e2rYiZcQLYVkLu7u1FUVASpVGrzebY27MxmM31yqFQq6PV6eHl5YcSIny/X+Xw+6urqaL1lWFiY275sVJ64urqatfqjsFgsrBNYo9HQdofUGP38/OyOR6PR0KkPkUjk1tSHwWBARUUFdDodRo4cie7ubmg0GpjNZtrpKyAgAP7+/lZ2jI6wWCyora1Fc3MzbRLVH+wFaR6Ph66uLoSFhSE+Pt6tk4hWq0VpaSn8/f0hEokcXubbC9LMiZgK0kajkXYxTE5Odlia7AokSaKurg6NjY2sqzOz2UyPj+nm5u/vjzNnziAqKgp79+6FWCzGm2++aVUfMAwY3gHZnuOb2WxGfn4+MjMzrR5zZcOOme9ta2uDXq+HQCBAeHg4AgMDrfK9/YUKlgKBwKU8sclkYp3AOp0OHh4e9MkbEBAADw8PyOVy6HQ6Vi7XHTA3MhMSEqwmKIvFAp1Ox5pEeq/0bXnmUigUClRUVGDkyJGIi4tza/v3rq4ulJaWAgBCQ0PpFT9lcsNcSbs6eVksFlRXV9MeKQO5grIVpCnbgPDwcMTExPQ5EbuKTqfD9evXERgYiISEBIcTqNlshkqlwt///nfk5+eDJEn4+/tjxowZ2Lhxo1vGtGzZMpw4cQJhYWEoKioC0PPdWLhwIWpqahAXF4dDhw7ZlNDt37+fHsc//vEPLF261C1jsgMXkG0FZJIkceHCBSvlRH827JRKJSoqKuDv74/4+HhYLBY636tSqWAymejLOFdXgVTq4+bNmxCLxW4JlkajkR5bS0sL9Ho9fH19ERoaSq/0ewv1+0NnZycqKioQHByM+Ph4p9+zxWKhPXOpIA2AFaQ9PT1RVVUFk8kEiUTi1tWfxWJBXV0dmpqakJSUZJU/dyZnbssukoJKIdi6yhkolNyMIAiEh4fTlXN9raSdxWKx4MaNG2htbUVKSorTaZsbN25g1apVSE5OxrZt2yAUCtHd3Y3GxkbEx8f3962yOHfuHIRCIZYsWUIH5BdeeAHBwcF46aWXsHXrVnR2dmLbtm2s5ykUCqSnp6OgoAAEQUAqlaKwsHAwtc/DOyDbs+AEBq6coIztzWYzxGIx/PxsmMEAtJUgc1MOgFUqoXf6gboMT0hIwMiRI92aZ6NWlqGhoYiLi4PJZGIpO6h8am/5nTN0d3ejoqICRqMREomE7rwyEKhLYZVKhebmZmi1Wvj4+CAkJIQenztWgUqlEuXl5bQaxtlJxF7OnBmkBQIBqqurYTAYBiWF0NjYSOehbaVtqImYmuT0ej3raqmvIK3RaFBSUoKQkBDEx8c7NYlYLBZ88skn2LdvH9555x1MmTJlUHPFNTU1mD17Nh2QJRIJcnNzERERgaamJkyZMgVlZWWs5xw8eBC5ubn48MMPAQDLly/HlClTsHjx4sEa5vCu1HOE2Wx2ORAbjUbU1NRAoVA4VCBQrysUCiEUChEVFUX/Xuoy88aNG9BqtfDw8IC/f499YUdHByIiIjBx4kS3rqCYMrZx48bRQcHDwwMjR46kN5SYq0CFQkFbllL5XuqHmfNkTiIikcipsnRnoQJjS0sLQkJCkJ6eTm9mqtVqVFdXW6VjXFkFUvnWrq4ujBkzxu7kag+CIODr6wtfX1/cc09PdR5lF0n9jTs7O+Hl5YWAgAC0tLTQK+mBprT0ej1KSkrg5+eH9PR0u3loT09PhISEsL6vVJDWaDT0JjTzMxQKhWhpXljxVwAAIABJREFUaYFCoUBKSorTV2jV1dVYvXo1xowZg/Pnz7v8ebqDlpYWRET0OAhGRESgtbXV6piGhgZWcVh0dDQaGhqGbIz2GFYBmdqw4/P5KC4uppUTjlZYzHzoqFGjkJiY2O8Z38PDA4GBgSypGmXUTRAEfHx80NzcDIVCwVql9ndDyWw20znLpKQkh9IkgiAgEAjofDjwc4BRqVRobW1FZWUlLBYLneNVKBQIDw9HRkaGyxtzfUFtCHZ3d1sFy6CgINblpdFopFfSra2tLGUCdTXi4+ND/91IkkRzczOdZ3SnBI+a4JuamuDj44PJkyeDz+fTQbqtrQ1VVVX0xiYzneCshpdKIUgkkn7JHvsK0m1tbXS1nUAgQGNjIz1Ge+eK2WzGnj17cODAAezYsQOTJ0++rRUUtjIDt8N479qA3PvDZW7YjR8/HlqtFiqVirVKpU5c6uQFfi7sCA0NRUZGhlMnjLMYDAZUVlbi5s2bGD16NCs3193dTacR6uvr0d3dDYFAwArSfa2wmDK26OhoZGRk9HvFTRAE/Pz84OfnR/cX1Ol0KCkpgclkgr+/Pzo6OtDR0dFnOsZZmHIwWxuCtvD09ERwcDBrwjEYDPSlenNzM27evAlvb2/4+PhApVJBKBRCKpW6VVXCDJZisZg1aVCfIbV6o1JaarUara2trCDNzEkzv3NqtRqlpaUICQkZ0N/UFjweDwqFAlqtFhMnToSfnx890anVasjlctZK2mKxQK/Xw9/fH88++yxSU1Px448//iKrYibh4eFoamqiUxaUlwaT6Oho5Obm0rfr6+sxZcqUoRukHe7aHDLTpN6ZPDEzz6ZSqaDT6WA0GuHt7Y3Y2FiEhoa67cRlXuI7K5GzVSRCnbxUkKY2DTUaDcrKyuDn5+d2GRulhW5tbbXa+GKmY9RqNSsdQwVpR6kEtVqNsrIyBAYGIj4+3q0ToNlsRmVlJdrb2xEYGAiDwYCuri67GmRXUalUKC0tHZDygwpyzJy0xWKBn58fDAYDDAYDRo8e7fZiCqVSidLSUlqz3NffiArSly5dwpYtW1BeXo6oqChMnToVixcvtqlgGghlZWVYuHAhfVsul+O1117DM888A6Anhzx16lQoFArEx8ejsbERqampOHXqFLZu3QqFQoE33niD9ZoKhQJSqRSXLl0CAKSlpaGwsHAwbT6H96YeFVgDAwPpIOzMJQmlbtDpdIiLi6MbMapUKjqXSq2iXdXOUsb5crkc4eHhGDVq1IAu8SnpGDNId3V1gcfjISoqCmFhYf1epdqCutR2RSVAbRoy5Xeenp5W6RhmWx+JROJUAwFXoKr4IiMjWUUv9pQTlLyN+jv3NalRY9fpdEhOTnb7CrGjowOlpaV0SoOSCDILMfqj4+499tGjRzu94VhZWYnVq1dDKpVi48aNMBqNuHTpEkJCQhw2dBgIZrMZUVFRyMvLQ2xsLBYvXozc3Fy0tbXB09MTO3fuxMMPP4wFCxagtrYWo0aNwuHDhxEcHIyCggLs3r0be/bsAQDs3bsXmzdvBgCsW7cOf/nLXwZt3BjuAVkmk+G5556DSqVCcnIypFIpMjIykJqaavNLZzabUVtb22dhB/MSU6VSQaPR0NpKR/loSk/s4+ODxMTEfq/CbGGxWFBfX4+GhgbExsZCIBDQ+VTmhhcVBAUCgUv5Mr1ej7KyMnh6eiIpKWnAY2emEqjP0WQyITg4GJGRkQgICHDb59Pd3Y2ysjKnqvgo7CkneqcSPD096VRDbGwsIiIi3JqHpLpsGAwGpKSksMbeWyKo1WpBkqRVtVxfQZqapKKjoxEVFeXU2M1mM3bt2oXs7Gy8++67+M1vfuOW9+os3377LV599VWcP3+edX9ubi62b9+OEydODOl4XGB4B2QKo9GI4uJiXLx4Efn5+bhy5Qp4PB4mTJiAtLQ0pKWl4ccff0R4eDjS0tIQExPj0oqSeZlOBUA+n08HQIFAgPr6euj1eojFYreW3gI9J1VlZSUtY7N1AvZOx1C5VGaQthUAqQ1BhUKBpKQkt2s0qdQKpUJhpmQcGSs5gllR5g7lB1M5oVaroVQqodfrwefzERUVhaCgIIwYMcJtm5otLS2Qy+WIj49HeHi4U8HSkY6baq9kNpvpzdLegb4vysvLsWbNGkycOBGvv/66W+V7zrJs2TKkpaVh1apVrPtzc3PxyCOPIDo6GpGRkdi+fTvGjBkz5OPrAy4g24IkSWi1WhQWFiI7OxtHjhxBdHQ0QkJCkJaWBqlUiokTJzp9EtjCaDRCqVSitrYWKpUKnp6eEAqFLD+MgUqeKBkbQRCsnoHOwjQKtxUAu7u7cePGDXr15M7NI5PJhKqqKqjVaiQnJ9uUVPWVM2cGaVsBkNr4crUwxRmYgZ660ulvtaEturq6UFZWBg8PD4jF4gHn/81mMytIK5VKdHV1ISAgAPfcc49d3wkmJpMJH3zwAQ4fPoz33nvPyh98qDAYDIiMjERxcTGtAKJQq9W0n0ZOTg6ysrJQUVHxi4zTDlxA7ovu7m4sX74cL7/8MsRiMZqamiCTyeiVdGtrKxITEyGVSpGeno4JEyZAKBQ6tfnWO09MeSIwA6DJZLK5IecIV2VszkKtANva2lBXVweLxQIvLy/WhpyrwcXW76CkZq50HWY+n5kzp1JG1GW6r68vWltb6Vyuu/PQVJFEX4HemWpDWwGQUpbU19fbrBIcKAaDAeXl5TCbzUhKSmKVXWu1WhAEYVXN5+HhgdLSUqxZswb33XcfXn31Vbf6ebjKsWPH8P777+Pbb791eGxcXBwKCgr67W8yCHABeSCYzWaUlZUhLy8PeXl5uHz5MoxGI8aNG0cH6dGjR7NWuhqNBhUVFfD29naYJ2Z6OVB5VOqkoII0Mx/NDGaDtWqVy+VQqVQQi8UICAhgBReVSmV14jqj4abQarUoKyujvZvd4fMB/GysVF9fj9bWVvD5fHh7e/drjPYwm82oqqqCSqVCSkqKy4HelvqEaf7k6emJ2tpap42GXIVKfyQkJFitLO2N8X//939RVVUFpVKJ5cuXY8GCBRgzZoxbv3MUcXFx9IKEz+ejoKCA9ThJksjKysK+ffsQEBCA48ePWzUebm5upq9qZTIZ5s+fjxs3btwW2uJbcAHZ3ej1ely+fBkymQwymQzXr1+Hv78/UlJScOPGDaSmpuLZZ5/ttz8xs0yYcpXj8/nw8fGBWq2Gv78/JBKJW2Vsrq5aezvfUaoJewUYJpMJ1dXV6OzsHLCZji30ej1KS0vpzVIvLy/WGO0ZKzm7sdne3o7KykraktRdJ7jJZIJKpUJtbS2USiW8vLxoB8H+ek70htlOyZX0R0lJCVavXo1f//rXePDBB/HTTz/h0qVL2LNnj1u/exSOVrM5OTnYsWMHCgsL8cUXX2DdunXIy8vD7t27AQArVqzAzp07sWvXLvD5fAgEArz99tu/WGrFDlxAHmxIksS2bdvw0Ucf4Ve/+hU6Ozvpar6MjAxIpVJIpVJaeucqBoMBZWVl0Gq1CAwMRFdXF7q6ulwqEOkL5qaaSCTq9+v0Vk1QJvAeHh5Qq9WIjo5GXFycW1crFosFNTU1tDm6o0mQubFJme5Q5cxM/TE1xv6oM1xBpVKhrKyM3ozl8XisIgymRJAZpJ2ZSJiTLNNn2REmkwn//Oc/cfz4cXzwwQfIyMhwx1t1iKOA3NtngulVcQfBeVkMNgRBID09HVlZWfSmmsVigVwuR15eHk6fPo2tW7fSGs/09HSkp6dj3LhxDtMZlIwtISEBY8eOZaUuurq6oFKp0N7eDrlcDrPZTG8aOmNQbzQaUVVVZdVCqb94eXkhNDSUPqG0Wi1KSkpgsVgwcuRIKBQKNDc39+mH4QpM1zRnq9VslQozPZCpakgq8Gq1WiQmJrr9pKfSH2q12qoc3Nlqw95e3MyJhNlOKT093elJ9vr161i9ejXuv/9+/Pjjj26VZTqCIAhMnz4dBEFg+fLleOqpp1iP2/OduMMCslNwK+QhwGAw4Nq1a3Q++qeffoKXlxcmTJhAB+nExETweDwUFxdDq9X2KWPrDbNAhJnrZV6iU85rg9lCidn+qfeqlemHwaxAc6XTCbUxNRjWm0DPFcP169fh5eUFX19faLVaGAwGt00klO53oOkPe22pCIKAVqtFUlISbXTkCKPRiB07duDrr7/GBx98wGpXNlQ0NjYiMjISra2teOCBB/Dee+9h8uTJ9ON/+MMf8PLLL9Oa59///vd444037DaZuE3hUha3KyRJQq1WIz8/H3l5eZDJZLQvRHR0NNasWYP09PQBWW9SBvVMRQIlbRs1ahSCg4Pdugqiqvh6V8L1BbVpyBwjj8djBT9qBUl5W4hEIpveBAOBqbdOTk5macV7TyRUtxNXquSoAg+j0Yjk5GS3pz/0ej2Ki4vh4eEBoVBI/60dmekXFRVhzZo1mD59OtatWzekq2J7bNiwAUKhEH/729/o+4ZTyoILyLcBX3/9NV577TU899xz9C6xTCaDQqGAWCymV9Hjx493eaOHaWAUHx9Pd3GgvI8p7TGzFZUr3Lx5k6WbHehJ3ddEEhMTg6CgINam4UCh0h8RERFOFwX1LlnvLW1jKjtaW1tdLvBwFqYmWiKRsAp37FUbVlZWorS0FCqVClevXsXHH39spVhwF3V1dViyZAmam5vB4/Hw1FNPISsri3XMf//7XyxcuBAJCQn0d/Ojjz7CzJkz6WO+/vpr7Ny5Ezk5OcjLy8OaNWsgk8kGZcyDCBeQ7xSoDabewdBkMqGkpITWRl++fBkkSSI1NZUO0hKJxGYQJUkS9fX1dlsoUcfYKr7orT22FaSoTTXK1czdpixMGZ5IJGJ1Y2EaAlETiau7/5S1p7tM43vLxjQaDZ1KiImJQXBw8IBVE0wot72AgACn2ikBPX/v06dP4+2334bZbKZTHK+//jpmz57tlnExaWpqQlNTE9LS0qDRaCCVSvHll1+yOr7/+9//xooVKxAfHw+TyYRHH30U69atYykoSJLEqlWrcPLkSfj6+mLfvn2/SGplgHAB+W6DKowoLCykV9FlZWUICgqitdEZGRkoLi6GWq3G+PHjXa5Us5VG6C0Z0+v1qKysHJRWRFRhTVVVFWJiYmx6LDANgahxMr0mqCBt630zFQjOWnu6On7mhiyfz2cpO3qrJlxd7VNOgS0tLUhOTnZaRmgwGLB9+3acPn0au3fvxvjx4wH0THzUFchg89BDD2HVqlV44IEH6PvuAA8Kd8EF5OEA1ZE6Ly8PZ86cweHDhyEQCDB27FikpaUhIyMDEyZMwIgRIwaUj1ar1ejo6EBTUxO9ig4MDKTLwd2hT6XSH3w+3+WyYVvGT8xNw4CAAPB4PFbDWHcVp1BQq9YRI0ZAJBLZnBDsSQR7y+9sQalXqEpBZyfCq1evIisrC7Nnz8ZLL700KFpiR9TU1GDy5MkoKipi5ejvAA8Kd8EF5OHGo48+isWLF+MPf/gDKioqcPHiRchkMly6dAldXV0YO3Ys7Xo3ZswYp09Mpn8z1beNaVmpUqlYagTKstLZfDTz9d2Z/qCq+JRKJZqamqDT6eDr64ugoCA6ALojjcDURLuyagV6JhKmaoL6LJm+IkKhEA0NDWhvb3epnVJ3dzfefPNNnD17Fh9++OGg2mL2hVarxe9+9zusW7cO8+bNYz12B3hQuAsuIHP8THd3N65cuULno4uKiuDr64u0tDQ6H23LVJ1qiurIdN2WrI3ymWBak/Z+fmdnJ8rLyxEWFobY2Fi3l+YqlUqUlZXR46f8rangR+Xv7el6HUGZ0rtz/MwNuba2NrS1tcHDw4M1kThylrty5QqysrLw8MMP44UXXnD71YCzGI1GzJ49GzNmzMDatWsdHn8belC4i+EVkA8fPowNGzagpKQEMpmMlfTfsmULPvnkE3h4eODdd9/FjBkzrJ5fXV2NRYsWQaFQIC0tDZ9++ukvcmk3VJAkic7OTuTn59NBmvLJSE9Ph0gkwpdffomnn34aUqm0X5te1AqV6YVB5aP9/PzQ0dEBs9mM5ORkt+cwKUWBXq93aBrfe4VKtctiBuneAY3qPqLRaJCSkuJ2U3qz2Qy5XA6lUkm/fl8pGYFAQE9427Ztww8//IDdu3fj3nvvdeu4mJw8eRJZWVkwm8148skn8dJLL7Ee7+rqQnJyMpRKJSQSCb744gvExcWxjrkDPCjcxfAKyCUlJeDxeFi+fDm2b99OB+Tr169j8eLFkMlkaGxsxLRp01BeXm61uliwYAHmzZuHRYsWYcWKFUhNTcXKlSt/ibfyi2GxWFBVVYVNmzYhJycHY8aMobW5VKqD2bG6PxgMBtTU1NANQC0Wy4AVE0yonLpcLu+3aTyzGpIKgCaTidYeAz3FDPY2HQcK1U4pIiICo0aNsvv6zAnv6tWrWLduHdRqNeLj4/Hkk09iypQpSExMdOvYKMxmM8RiMU6dOkX3bDx48CBLQbF27Vq88847uPfee+nP8fPPP0dtbS2AO8aDwl0Mr9LplJQUm/cfO3YMixYtgre3N+Lj45GYmAiZTIZJkybRx5Akie+++w7//ve/AQBLly7Fhg0bhl1A5vF4CAkJgUgkQk1NDXx9fWE0GlFUVISLFy/iwIEDuHbtGjw8PGiD/4yMDCQlJTml5NBqtSgtLYW/vz/uu+8+8Pl8OoeqUqnQ2dmJmpqafrfKYm4KDqR5KbPzNlXxRpIklEolbezu6emJhoYGqNXqATd1paBW3VqtFuPGjXN41cDj8ejuKvn5+QgPD8fnn38Og8GAgoICXLhwYdACskwmQ2JiIhISEgAAixYtwrFjx1gBubi4GP/3f/+HSZMmwWQy4Z577sGDDz7ImmBWrVplZTY/nLlrArI9GhoaWE0XqTp4Jh0dHQgMDKQ3oWwdM1wIDg7G+vXr6duenp6YMGECJkyYgJUrV4IkSWg0GhQWFuLixYvYuHEjnWNmSu+YRRBMx7felXAEQcDHxwc+Pj60NSRTMdHU1ITy8nJWqyxqo4vp71FbW4umpqZB0UQzO3gzpXJM7XHv7uWuOrZRBSrR0dEQi8VOr7oLCgrw7LPPYuHChcjNzaW/w4PdWsmWv0ReXp7dY/h8PgICAtDR0XE35ofdxh0VkKdNm4bm5mar+zdt2oSHHnrI5nNspWRs6Vp7U1dXR2s1lUolAgMDceXKFavjHHm53m1QHhlTp07F1KlTAfR8fo2NjbTB/4cffoi2tja67VNBQQEt5ndmBUkQBIRCIYRCISIjIwGwCy9qamroVlk+Pj5QqVQICQlBenq6272EKbMeLy8vK7MeDw8PBAYGsjw7mK5yra2tNl3lmKXTJpMJFRUVuHnzpt1+j/bGtXnzZuTl5eGzzz6ze4U4WPT3vLoLc8Nu5Y4KyKdPn3b5OdHR0airq6Nv19fX0yc5RWhoKJRKJUwmE/h8Purr65GZmYlvvvkGAPDcc8/1KWU6e/bssJ71CYJAVFQU5s6di7lz5wLoqdJaunQpGhsbkZGRgRUrVsBsNlsZ/DsbQHsHP5PJhPLyciiVSoSEhODmzZvIz8+Hj4+PW1plMQs8XOngYc9VjspHU65yAoEAfD4fnZ2diIuLQ3JystPBimrgu3jxYpw9e9btk5AzOHNeUcdER0fT/s/uvnq527ijAnJ/mDNnDh599FGsXbsWjY2NqKiowMSJE1nHEASBqVOn4siRI1i0aBH2799Pr7hJksShQ4fw3Xff/RLDv2MRCAR44YUXMG3aNPo+vV6PS5cuQSaTYceOHXQRBTPV4UwnlLa2NlRWViImJgYpKSlW1qRUEUt1dXW/WmVRue4RI0YgIyNjwD35vL29ERYWRpsiGQwGlJSUQKvVIiQkBM3NzWhoaHA4zps3b2Ljxo24dOkSPv/8cyQnJw9oXAMhIyMDFRUVqK6uRlRUFLKzs+k9GIo5c+Zg//79mDRpEo4cOYL777+fWyE74K5RWfznP//B6tWr0dbWhsDAQIwfP55e4W7atAl79+4Fn8/Hjh078OCDDwIAZs2ahT179iAyMhJyuZyWvU2YMAGfffYZvL29ce7cOaxdu9ZuKiI+Ph5BQUF2vVw3bNiAjz/+mDYJ37x5M2bNmmX1Oo4kRHcjJEmivb0dMpmMdr2rr69HbGwsrY2WSqUICAgAQRDo7OykV2USicQpIyNXWmVRBR7t7e1WuW530draiqqqKiuzIXuGRUajkdbl7tq1C4899hiysrLc2rjVHs8//zy++uoreHl5QSQSYd++faz0TE5ODp555hlUV1cjODgYERERaG1txe7duzFnzhx0dXXhsccew+XLlxEcHIzs7Gx6E3AYMrxkb/3BmZz0ypUrkZiYiOeee87mazjycrVlJ9gbZyREwwVKekcF6IKCAuh0OowYMQKNjY147733MGnSpAG5ytlqlQX0pBaCg4MhEomcbvHkLAaDAaWlpSAIwuk2XFRfx9dffx1FRUXw9vZGaGgo/vSnP2H58uVuG5s9vv32W9x///3g8/l48cUXAQDbtm2zOu4uLuZwJ8NL9tYfHOWkTSYTjh49isLCQrvHUHmzsLAwzJ07FzKZjBWQncEZCdFwgcfjISkpCUlJSfjzn/+Mjo4OzJs3D9HR0Zg7dy4OHjxIe/cyDf5FIpHTkjNmPtpkMtEdPEQiEd26yV2tspgKDVe9nPPy8vD8889j6dKlOHr0KDw8PNDZ2YmOjg6Xx9Efpk+fTv8/MzMTR44cGZLfO5wZ1gHZEadPn0ZycjKio6NtPq7T6ehqKZ1Oh2+//RavvPKK1XE7d+7EgQMHkJ6ejrfeeovlWws4JyEargQFBWHXrl2syYkkSahUKtrgf/369ZDL5YiMjKS10enp6QgNDe1zldve3o6KigrExMRYSc3c0Sqrq6sLpaWl8PT0dKmdkk6nw2uvvYaioiIcOnQISUlJrM+j9/dnKNi7dy8WLlxo8zFHLZg4nGdYpywc8fjjjyMzMxMrVqyg72tsbMSTTz6JnJwcyOVyzJ07F3K5HEajEQEBAayGkps2bUJmZiYdGNavX4+mpibs3buX9XsOHz6Mb775Bnv27AEAfPrpp5DJZHjvvfcAOM7lUQw3CR4TSotMpTry8/PR2dlpZfAvEAjQ3NyMxsZG8Pl8lxqY9qdVllgsdlqhQZIkzp8/jxdffBHLli3DX//610HPFTuTttu0aRMKCgpw9OhRmxOco7QdBwAuh3z7UVNTg9mzZ6OoqIh1/4ULF7BhwwZ6E3LLli0AgJdffhkAl8vrLyaTCcXFxcjLy0N+fj4uXboEpVIJg8GA5cuXY+bMmZBIJAMKer07nFB9+Hx8fBAbG+t0qyydTocNGzagtLQUH330EUQiUb/H5E7279+P3bt348yZM075jTizZzJMcSogu9dai8OKpqYm+v//+c9/MHbsWKtjmBIig8GA7OxszJkzh358+vTptNY0MzMT9fX1gz/wuwA+n4/U1FQ89dRT+PjjjyGRSDB58mR88MEH8PLywrZt23Dfffdh1qxZWL9+PY4dO4bGxkabBQ19/Y6goCDExsYiKCgIPB4PKSkpEIlE0Ol0KC4uxoULF3D16lW6b5/JZKKfT5Ikzp07hwceeACjR4/GqVOnhiQYb9iwAVFRURg/fjzGjx+PnJwcq2NOnjyJ9evXo7W1FePGjcPWrVutjtHpdLQihErb2fqOczgHt0IeZB577DFcuXIFBEEgLi4OH374ISIiIlipD+BnCZHZbMayZcuwbt06m6/3xz/+EQsXLsSf//xnq8ccSfCGOy0tLXR5NgW16UZ1BM/Pz0dzczMSEhJoQ6UJEybA39/fbj5ar9ejpKQE/v7+No3pbbXKysvLw/fffw+j0QilUonPPvsMYrF40N57b5xZySYmJqKmpgZisRienp6or6/HDz/8gMDAQKu0HQBWCyYOK7iUxZ3EYObyHGmcu7u7sWTJEhQWFiIkJMSmTeJwwmKxoLy8nGXwbzAYrAz+CYLA999/D6FQCIlEYjOvbwuqt93WrVuRkJAAT09PFBUV4fHHHx8yox1nArKjVBqHS3CytzsJRxK8/fv348SJEzhz5ozdlZotCd59992Hp59+mqVxnjNnDku18MknnyAoKAiVlZXIzs7Giy++iC+++MJ9b+4Og8fjITk5GcnJyXj88ccB9CgmKIP/999/H4WFhVCr1ZBKpZg/fz7CwsIwYsQIh9I7jUaD9evXo6amBgcPHmRNfC4ujgYMp/65/eByyHcAJ0+exLZt23D8+HG7Gyv2cnlMjbOXlxetcWZy7NgxLF26FAAwf/58nDlzZsiDw+2Oj48PMjMz8cwzz+Dxxx9HUFAQPv/8czz99NOorq7GCy+8gMzMTDzyyCPYsmULTp06BYVCQX+OJEni7NmzmD59OtLT03Hy5EmrqxB3lxVPmzYNY8eOtfo5duwYVq5ciaqqKly5cgURERE2C584c6Chh1sh3wGsWrUK3d3ddLfezMxM7N69m5WHbmlpscrlzZw5E0eOHOFsEt3Mb37zG5w7d47WFc+cORPAz731Ll68iLNnz+LNN9+ERqOBWCxGa2srBAIBvvrqK4waNWpIxumsGdf//M//YPbs2Vb3O2MgxOFeuIB8B1BZWWnz/sjISHpTMCEhAVevXrU6xl02iXV1dViyZAmam5vB4/Hw1FNPISsri3VMbm4uHnroIcTHxwMA5s2bZ7NQ5k7HnkUmj8dDQkICEhIS8OijjwLo8aK4du0avvrqK7zyyitu7xnYX5qamhAREQHAOfWPPQMhDvfCBeS7HHfZJPL5fLz11ltIS0uDRqOBVCqlpVpMfvvb3+LEiROD94buMDw9PSGVSiGVSofk9y1cuBBlZWUA+vbxTkpKgslkAkEQ8Pb2RklJCQB24ROfz8fOnTsxY8YMWv0zZsyYIXkfwxUuIN/luMtCZo4iAAAGCElEQVQmMSIigl5R+fv7IyUlBQ0NDcPSb+N2hrkZ25ePd2hoqM0iIuZVF9DjiGjLnZBjcLg9rp84Bg3mKiclJQULFizAmDFj8Morr+D48eMAgCeeeAIdHR1ITEzE22+/bbMAgElNTQ0uX76MX/3qV1aPXbhwAampqXjwwQdRXFw8KO+JwzGUj/fixYt/6aFwuACnQ+ZwCa1Wi9/97ndYt24d5s2bx3pMrVaDx+NBKBQiJycHWVlZqKiosHoNR54bJEkiKysLOTk58PX1xb/+9S+kpaUN6vu62xiojzeH2+F0yBzuxWg04pFHHsGf/vQnq2AMgGXoPmvWLPz1r39Fe3u7TbVGX22v/vvf/6KiogIVFRXIy8vDypUrOf0rA2eKiA4ePNjn6vj8+fOsIqLk5GTOEOg2gAvIHE5BkiSeeOIJpKSkYO3atTaPaW5uprtgyGQyWCwWp53OmBw7dgxLliwBQRDIzMyEUqlkqQKGO7eLjzeH++ECModTnD9/Hp9++inuvfdeuhv35s2bUVtbCwBYsWIFjhw5gl27doHP50MgECA7O9tmIYEj/1xbFWINDQ1cQHYSd/l4c/wCkCTpyg8Hx4BpaGggSZIkW1payHHjxpHff/896/FZs2aRP/zwA337/vvvJwsKCljHlJaWkqmpqfSPv78/+c4777COOXv2LDlixAj6mFdffXWQ3tHgcOjQIXL06NEkQRBkfn4+67HNmzeTIpGIFIvF5MmTJ1mPLV26lNy1axcpl8vJiRMnkomJieQf//hHcsaMGSRJkmRVVRU5btw4cty4ceTo0aPJjRs3Dtl7GsY4FWO5FTLHkOPoctkZ7bREIqH1tWazGVFRUXSlIpM7WRc9duxYHD161Kp/3vXr15GdnY3i4mI0NjZi2rRpKC8vp13m/vWvfwEAFixYgGeffRaLFi3CihUrkJqaCsB+ERHHLw8ne+MYUpzxz50zZw4OHDgAkiRx8eJFBAQE9JmuOHPmDEQiEWJjYwd17ENNSkoKJBKJ1f3Hjh3DokWL4O3tjfj4eCQmJkImk7GOIUkS3333HebPnw8AWLp0Kb788sshGTdH/+FWyBxDij3Pjd27dwPoyUXPmjULOTk5SExMhK+vL/bt29fna2ZnZ9tVFFC66MjISGzfvv2uqDRraGhAZmYmfZvKsTPp6OhAYGAg3djA1jEctx9cQOYYUuxdLjP7FhIEgffff9+p1zMYDDh+/Djt1cskLS0NN27coHXRDz/8MK2LXrZsGU6cOIGwsDC6pZZCocDChQtRU1ODuLg4HDp0yGZD0f3792Pjxo0AgH/84x+0U15/cEbC1hvSTf4kHLcfrhaGcHDcVhAE8RCAp0mSnO7EsTUA0kmSbCcIYjIALYADJEmOvfX4GwAUJEluJQjiJQBBJEm+2Os1ggEUAEhHT6FUIQApSZKd7nxfvX5nLoC/kSRZcOv2ywBAkuSWW7e/AbCBJMkLjOcQANoA3EOSpIkgiEm3jpkxWOPkGDhcDpnjTmcxgIO2HiAI4p5bgQkEQUxEz/e9AwBIkjwHQNHrKQ8B2H/r//sBPGzjZWcAOEWSpOJWED4FYOZA34SLHAewiCAIb4Ig4gEkAWAlkcmeldZZAPNv3bUUANsIm+O2gwvIHHcsBEH4AngAwFHGfSsIgqDyH/MBFBEEcRXAuwAWkX1fEoaTJNkEALf+DbNxTBSAOsbt+lv3uR2CIOYSBFEPYBKAr2+thEGSZDGAQwCuAziJnisE863n5BAEQUlSXgSwliCISgAhAD4ZjHFyuA8uZcExbCEIIg7ACUbKQkmSZCDj8U6SJIN6Ped5AN4kSW68dXs9AD1Jkm8N2cA57lq4FTIHx8+0EAQRAQC3/m21cUw9gBjG7WgAjUMwNo5hABeQOTh+5jh6cq2A/ZzrNwCmEwQRRBBEEIDpt+7j4BgwXEDmGJYQBHEQwAUAEoIg6gmCeALAVgAPEARRgZ7c9NZbx6YTBLEHAEiSVAB4HUD+rZ/Xbt3HwTFguBwyBwcHx20Ct0Lm4ODguE3gAjIHBwfHbQIXkDk4ODhuE/4fWQOsCWokjkQAAAAASUVORK5CYII= +) + +最佳值,即那些使if条件成立的值,其适应度值为0,并且可以在图的底部清楚地看到。 离最佳值越远,搜索空间中的点越高。 + +### 仪表 + +适应度函数应为具体的测试执行计算距离值。 也就是说,我们要运行程序,然后学习该执行的距离值。 但是,分支条件隐藏在目标函数的源代码中,其值原则上可以是沿着达到它的执行路径进行的各种计算的结果。 即使在我们的示例中,条件是直接使用函数输入值的方程式,但通常情况可能并非如此。 它也可能是衍生值。 因此,我们需要直接在条件语句中观察计算距离度量所需的值。 + +通常使用*仪器*完成此操作:我们在分支条件之前或之后立即添加新代码,以跟踪观察到的值并使用这些值计算距离。 以下是我们正在测试的程序的检测版本,它在执行时打印出距离值: + +```py +def test_me_instrumented(x, y): + print("Instrumentation: Input = (%d, %d), distance = %d" % + (x, y, calculate_distance(x, y))) + if x == 2 * (y + 1): + return True + else: + return False + +``` + +让我们尝试一些示例值: + +```py +test_me_instrumented(0, 0) + +``` + +```py +Instrumentation: Input = (0, 0), distance = 2 + +``` + +```py +False + +``` + +```py +test_me_instrumented(5, 2) + +``` + +```py +Instrumentation: Input = (5, 2), distance = 1 + +``` + +```py +False + +``` + +```py +test_me_instrumented(22, 10) + +``` + +```py +Instrumentation: Input = (22, 10), distance = 0 + +``` + +```py +True + +``` + +在计算适应性值时,我们将执行检测程序版本,但是我们需要一些方法来访问在执行过程中计算出的距离值。 作为此问题的简单第一种解决方案,我们只需添加一个全局变量,然后在其中存储距离计算的值即可。 + +```py +distance = 0 + +``` + +```py +def test_me_instrumented(x, y): + global distance + distance = calculate_distance(x, y) + if x == 2 * (y + 1): + return True + else: + return False + +``` + +现在,使用此工具化的`test_me()`版本,我们终于可以定义适应性函数,该函数简单地运行工具化的`test_me_instrumented()`函数,然后检索全局`distance`变量的值: + +```py +def get_fitness(x, y): + global distance + test_me_instrumented(x, y) + fitness = distance + return fitness + +``` + +让我们在一些示例输入上尝试一下: + +```py +get_fitness(0, 0) + +``` + +```py +2 + +``` + +```py +get_fitness(1, 2) + +``` + +```py +5 + +``` + +```py +get_fitness(22, 10) + +``` + +```py +0 + +``` + +### 爬上示例 + +确定了表示形式(2个整数的整数)和适应度函数(到目标分支的距离)之后,我们现在终于可以继续执行搜索算法了。 让我们使用最简单的元启发式算法Hillclimbing探索这个搜索空间。 隐喻恰当地捕获了正在发生的事情:该算法尝试在我们的表示形式所定义的搜索空间中爬坡。 除此以外,在我们的搜索环境中,最佳值不是高值而是低值,因此从技术上讲,我们正在跌入低谷。 + +爬山算法本身非常简单: + +1. 随机出发 +2. 确定所有邻居的适应度值 +3. 以最合适的身价去邻居 +4. 如果找不到解决方案,请继续执行步骤2 + +爬山者从随机测试输入开始,即`x`和`y`的随机值。 对于任意一对随机整数,它们满足条件`x == 2 * (y + 1)`的几率很小。 假设随机值为`(274, 153)`。 该方程式的右手边`2 * (y + 1)`的计算结果为308,因此该条件显然为假。 登山者应该去哪里? 让我们看一下此测试输入及其邻居的适应性值: + +```py +x, y = 274, 153 +print("Origin %d, %d has fitness %d" % (x, y, get_fitness(x, y))) +for nx, ny in neighbours(x, y): + print("Neighbour %d, %d has fitness %d" % (nx, ny, get_fitness(nx, ny))) + +``` + +```py +Origin 274, 153 has fitness 34 +Neighbour 273, 152 has fitness 33 +Neighbour 273, 153 has fitness 35 +Neighbour 273, 154 has fitness 37 +Neighbour 274, 152 has fitness 32 +Neighbour 274, 154 has fitness 36 +Neighbour 275, 152 has fitness 31 +Neighbour 275, 153 has fitness 33 +Neighbour 275, 154 has fitness 35 + +``` + +将`y`增加1会使等式右侧的值增加到`310`。 因此,等式左边的值与增加之前的值相比,*与等式右边的值甚至更大*! 因此,增加`y`似乎不是一个好主意。 另一方面,将`x`加1可以改善性能:等式的左侧和右侧变得更相似; 他们是“不太平等”。 因此,在`(274, 153)`的八个可能邻居中,增加`x`并减少`y`(`(275, 152)`)的邻居在直观上似乎最好--条件的结果仍然是假的,但是“少” 因此”,而不是原始值。** + +现在让我们实现Hillcimbing算法。 + +```py +import [random](https://docs.python.org/3/library/random.html) + +``` + +```py +LOG_VALUES = 20 # Number of values to log + +``` + +```py +def hillclimber(): + # Create and evaluate starting point + x, y = random.randint(MIN, MAX), random.randint(MIN, MAX) + fitness = get_fitness(x, y) + print("Initial value: %d, %d at fitness %.4f" % (x, y, fitness)) + iterations = 0 + logs = 0 + + # Stop once we have found an optimal solution + while fitness > 0: + iterations += 1 + # Move to first neighbour with a better fitness + for (nextx, nexty) in neighbours(x, y): + new_fitness = get_fitness(nextx, nexty) + + # Smaller fitness values are better + if new_fitness < fitness: + x, y = nextx, nexty + fitness = new_fitness + if logs < LOG_VALUES: + print("New value: %d, %d at fitness %.4f" % (x, y, fitness)) + elif logs == LOG_VALUES: + print("...") + logs += 1 + break + + print("Found optimum after %d iterations at %d, %d" % (iterations, x, y)) + +``` + +```py +hillclimber() + +``` + +```py +Initial value: 201, -956 at fitness 2111.0000 +New value: 200, -956 at fitness 2110.0000 +New value: 199, -956 at fitness 2109.0000 +New value: 198, -956 at fitness 2108.0000 +New value: 197, -956 at fitness 2107.0000 +New value: 196, -956 at fitness 2106.0000 +New value: 195, -956 at fitness 2105.0000 +New value: 194, -956 at fitness 2104.0000 +New value: 193, -956 at fitness 2103.0000 +New value: 192, -956 at fitness 2102.0000 +New value: 191, -956 at fitness 2101.0000 +New value: 190, -956 at fitness 2100.0000 +New value: 189, -956 at fitness 2099.0000 +New value: 188, -956 at fitness 2098.0000 +New value: 187, -956 at fitness 2097.0000 +New value: 186, -956 at fitness 2096.0000 +New value: 185, -956 at fitness 2095.0000 +New value: 184, -956 at fitness 2094.0000 +New value: 183, -956 at fitness 2093.0000 +New value: 182, -956 at fitness 2092.0000 +New value: 181, -956 at fitness 2091.0000 +... +Found optimum after 1656 iterations at -1000, -501 + +``` + +通过选择`x`和`y`的随机值开始爬山。 我们使用`-1000`-`1000`(我们将`MIN`和`MAX`定义为更早)范围内的较低值来减少播放示例所花费的时间。 然后,我们通过调用`get_fitness()`确定此起点的适合度值。 回想一下,我们正在尝试找到最小的适应度值,因此,我们现在循环运行,直到找到适合度`0`(即最佳值)为止。 + +在此循环中,我们遍历所有邻居(`neighbours`),并评估每个邻居的适应度值。 一旦我们发现邻居的适应性更好(更小),爬山者便退出循环,并以此为新起点。 此简单爬坡算法的另一种替代形式是删除`break`语句:这样,将评估*所有*邻居,并选择最佳邻居。 这就是*最陡的爬坡*。 您将看到达到最佳状态所需的迭代次数会减少,尽管对于每个迭代都执行更多的测试。 + +```py +def steepest_ascent_hillclimber(): + # Create and evaluate starting point + x, y = random.randint(MIN, MAX), random.randint(MIN, MAX) + fitness = get_fitness(x, y) + print("Initial value: %d, %d at fitness %.4f" % (x, y, fitness)) + iterations = 0 + logs = 0 + + # Stop once we have found an optimal solution + while fitness > 0: + iterations += 1 + # Move to first neighbour with a better fitness + for (nextx, nexty) in neighbours(x, y): + new_fitness = get_fitness(nextx, nexty) + if new_fitness < fitness: + x, y = nextx, nexty + fitness = new_fitness + if logs < LOG_VALUES: + print("New value: %d, %d at fitness %.4f" % (x, y, fitness)) + elif logs == LOG_VALUES: + print("...") + logs += 1 + + print("Found optimum after %d iterations at %d, %d" % (iterations, x, y)) + +``` + +```py +steepest_ascent_hillclimber() + +``` + +```py +Initial value: -258, 645 at fitness 1550.0000 +New value: -259, 644 at fitness 1549.0000 +New value: -258, 644 at fitness 1548.0000 +New value: -257, 644 at fitness 1547.0000 +New value: -258, 643 at fitness 1546.0000 +New value: -257, 643 at fitness 1545.0000 +New value: -256, 643 at fitness 1544.0000 +New value: -257, 642 at fitness 1543.0000 +New value: -256, 642 at fitness 1542.0000 +New value: -255, 642 at fitness 1541.0000 +New value: -256, 641 at fitness 1540.0000 +New value: -255, 641 at fitness 1539.0000 +New value: -254, 641 at fitness 1538.0000 +New value: -255, 640 at fitness 1537.0000 +New value: -254, 640 at fitness 1536.0000 +New value: -253, 640 at fitness 1535.0000 +New value: -254, 639 at fitness 1534.0000 +New value: -253, 639 at fitness 1533.0000 +New value: -252, 639 at fitness 1532.0000 +New value: -253, 638 at fitness 1531.0000 +New value: -252, 638 at fitness 1530.0000 +... +Found optimum after 517 iterations at 258, 128 + +``` + +我们的示例程序具有非常好的健身环境-完美的坡度,爬山者将永远找到解决方案。 如果绘制随时间推移观察到的适应度值,我们可以看到这个不错的渐变: + +```py +def plotting_hillclimber(fitness_function): + data = [] + + # Create and evaluate starting point + x, y = random.randint(MIN, MAX), random.randint(MIN, MAX) + fitness = fitness_function(x, y) + data += [fitness] + iterations = 0 + + # Stop once we have found an optimal solution + while fitness > 0: + iterations += 1 + # Move to first neighbour with a better fitness + for (nextx, nexty) in neighbours(x, y): + new_fitness = fitness_function(nextx, nexty) + if new_fitness < fitness: + x, y = nextx, nexty + fitness = new_fitness + data += [fitness] + break + + print("Found optimum after %d iterations at %d, %d" % (iterations, x, y)) + return data + +``` + +```py +data = plotting_hillclimber(get_fitness) + +``` + +```py +Found optimum after 429 iterations at -1000, -501 + +``` + +```py +import [matplotlib.pyplot](https://docs.python.org/3/library/matplotlib.pyplot.html) as [plt](https://docs.python.org/3/library/plt.html) + +``` + +```py +fig = plt.figure() +ax = plt.axes() + +x = range(len(data)) +ax.plot(x, data); + +``` + +![](data:image/png;base64,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 +) + +这种梯度是理想健身景观的结果。 实际上,我们不会总是有这么好的渐变。 特别是,我们的爬山者只有在至少有一个邻居的适应性更好的情况下才能运作良好。 如果我们达到邻居的*没有*实际上具有更好的适应性值的程度,该怎么办? 考虑以下函数`test_me2`: + +```py +def test_me2(x, y): + if(x * x == y * y * (x % 20)): + return True + else: + return False + +``` + +如果我们想再次覆盖`test_me2`中if条件的真分支,那么我们可以采用与以前相同的方式来计算距离,即通过计算比较两侧之间的差异。 让我们来介绍一下`test_me2()`功能: + +```py +def test_me2_instrumented(x, y): + global distance + distance = abs(x * x - y * y * (x % 20)) + if(x * x == y * y * (x % 20)): + return True + else: + return False + +``` + +使用此工具版本,我们只需要一个适应性函数即可调用该工具版本并读取全局`distance`变量。 + +```py +def bad_fitness(x, y): + global distance + test_me2_instrumented(x, y) + fitness = distance + return fitness + +``` + +在此示例上运行hillclimber之前,让我们通过绘制一下来再次查看搜索格局: + +```py +from [mpl_toolkits.mplot3d](https://docs.python.org/3/library/mpl_toolkits.mplot3d.html) import Axes3D + +``` + +```py +from [math](https://docs.python.org/3/library/math.html) import exp, tan + +``` + +```py +x = np.outer(np.linspace(-10, 10, 30), np.ones(30)) +y = x.copy().T +z = abs(x * x - y * y * (x % 20)) + +``` + +```py +fig = plt.figure() +ax = plt.axes(projection='3d') + +ax.plot_surface(x, y, z, cmap=plt.cm.jet, rstride=1, cstride=1, linewidth=0); + +``` + +![](data:image/png;base64,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 +) + +在这一点上,使用新的适应性函数运行爬坡器可能会很好,但是存在一个问题:使用此适应性函数运行爬坡器不是一个好主意,因为它可能永远不会终止。 假设我们已经达到了所有邻居都具有相同或更差的适应度值的地步。 登山者无法在任何地方移动并永远被困在那里! 搜索景观中的这种点称为*局部最优*。 如果达到了这一点,最简单的方法就是放弃并从新的随机点重新开始。 这就是我们在*随机重启*的爬坡器中所做的事情。 + +```py +def restarting_hillclimber(fitness_function): + data = [] + + # Create and evaluate starting point + x, y = random.randint(MIN, MAX), random.randint(MIN, MAX) + fitness = fitness_function(x, y) + data += [fitness] + print("Initial value: %d, %d at fitness %.4f" % (x, y, fitness)) + iterations = 0 + + # Stop once we have found an optimal solution + while fitness > 0: + changed = False + iterations += 1 + # Move to first neighbour with a better fitness + for (nextx, nexty) in neighbours(x, y): + new_fitness = fitness_function(nextx, nexty) + if new_fitness < fitness: + x, y = nextx, nexty + fitness = new_fitness + data += [fitness] + changed = True + break + if not changed: + x, y = random.randint(MIN, MAX), random.randint(MIN, MAX) + fitness = fitness_function(x, y) + data += [fitness] + + print("Found optimum after %d iterations at %d, %d" % (iterations, x, y)) + return data + +``` + +这种变化是微不足道的:我们只是简单地跟踪是否有布尔标志发生任何移动,如果我们没有移至任何邻居,那么我们将选择一个新的随机位置重新开始。 为了方便起见,我们还使用健身功能对爬坡器进行了参数化。 让我们用`bad_fitness`进行试验,并绘制我们观察到的结果适应度值: + +```py +MAX = 1000 +MIN = -MAX + +``` + +```py +data = restarting_hillclimber(bad_fitness) + +``` + +```py +Initial value: 333, 231 at fitness 582804.0000 +Found optimum after 165 iterations at 521, 521 + +``` + +```py +import [matplotlib.pyplot](https://docs.python.org/3/library/matplotlib.pyplot.html) as [plt](https://docs.python.org/3/library/plt.html) + +``` + +```py +fig = plt.figure() +ax = plt.axes() + +x = range(len(data)) +ax.plot(x, data); + +``` + +![](data:image/png;base64,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 +) + +多次运行该示例。 有时,我们很幸运,并且存在一个可以直接找到最佳解决方案的梯度。 但是有时候,您会发现在整个搜索过程中重新启动之前会达到最佳值。 + +我们将`x`和`y`的初始值限制在`[MIN, MAX]`的较小范围内。 这是测试生成中的常见技巧,因为在*中,大多数*情况下的解决方案都倾向于由较小的值组成,并且在许多情况下,使用较小的值来开始搜索会使搜索更快。 但是,如果我们需要的解决方案在搜索空间中位于完全不同的位置,该怎么办? 我们偏向于较小的解决方案,这意味着爬山者将需要很长时间才能找到解决方案,并且如果搜索预算固定,那么实际找到解决方案的可能性就较小。 要查看其效果,我们可以简单地用`1000000`或更多替换`1000`。 我们可以尝试使用该范围,以查看我们针对简单搜索问题而获得的效果。 + +```py +MAX = 100000 +MIN = -MAX + +``` + +```py +from [Timer](Timer.html) import Timer + +``` + +```py +with Timer() as t: + restarting_hillclimber(get_fitness) + print("Search time: %.2fs" % t.elapsed_time()) + +``` + +```py +Initial value: 64543, -55357 at fitness 175255.0000 +Found optimum after 169899 iterations at -100000, -50001 +Search time: 0.74s + +``` + +在大多数情况下,直到找到解决方案为止,搜索将花费更长的时间-可能比我们准备等待这样一个简单的示例函数所需的时间更长! (尽管有时我们会很幸运,并随机获得良好的开始位置)。 如何处理“真实”示例? 不要想象是否还有更多的参数和更大的社区! + +## 测试更复杂的程序 + +让我们来看一个稍微复杂的程序:您已经从 [Coverage一章](Coverage.html)中知道了CGI解码器。 + +```py +def cgi_decode(s): + """Decode the CGI-encoded string `s`: + * replace "+" by " " + * replace "%xx" by the character with hex number xx. + Return the decoded string. Raise `ValueError` for invalid inputs.""" + + # Mapping of hex digits to their integer values + hex_values = { + '0': 0, '1': 1, '2': 2, '3': 3, '4': 4, + '5': 5, '6': 6, '7': 7, '8': 8, '9': 9, + 'a': 10, 'b': 11, 'c': 12, 'd': 13, 'e': 14, 'f': 15, + 'A': 10, 'B': 11, 'C': 12, 'D': 13, 'E': 14, 'F': 15, + } + + t = "" + i = 0 + while i < len(s): + c = s[i] + if c == '+': + t += ' ' + elif c == '%': + digit_high, digit_low = s[i + 1], s[i + 2] + i += 2 + if digit_high in hex_values and digit_low in hex_values: + v = hex_values[digit_high] * 16 + hex_values[digit_low] + t += chr(v) + else: + raise ValueError("Invalid encoding") + else: + t += c + i += 1 + return t + +``` + +### CGI解码器作为搜索问题 + +`cgi_decode()`函数具有一个类型为string的输入,并且定义字符串邻域的一种可能方式是编辑距离为1的所有可能的字符串。例如,字符串`test`将为每个字符串具有两个邻居。 它的四个字符: + +* `uest` +* `tfst` +* `tett` +* `tesu` +* `sest` +* `tdst` +* `tert` +* `tess` + +此外,在任何字符之前或之后添加任何字符也将具有1的编辑距离,并且可以将其视为邻居。 为简单起见,让我们将输入字符串的长度固定为合理的值(例如10)。 在这种情况下,每个人有20个邻居(即每个角色有两个邻居)。 + +让我们实现一个新的`neighbour_strings()`函数,该函数生成这些相邻的字符串: + +```py +def neighbour_strings(x): + n = [] + for pos in range(len(x)): + c = ord(x[pos]) + if c < 127: + n += [x[:pos] + chr(c + 1) + x[pos + 1:]] + if c > 20: + n += [x[:pos] + chr(c - 1) + x[pos + 1:]] + return n + +``` + +`neighbour_strings()`函数获取输入字符串中每个字符的数值,并创建一个新字符串,并用字母中的前一个和后继字符替换该字符。 首先,我们仅考虑可打印的ASCII字符,范围为​​20--127。 + +```py +print(neighbour_strings("Hello")) + +``` + +```py +['Iello', 'Gello', 'Hfllo', 'Hdllo', 'Hemlo', 'Heklo', 'Helmo', 'Helko', 'Hellp', 'Helln'] + +``` + +因此,我们为`cgi_decode()`函数定义了搜索空间。 在此搜索空间中搜索个人的下一个要素是适应度函数。 + +### 分支距离 + +`test_me()`函数由单个if条件组成,其中比较了两个整数。 在`cgi_decode()`函数中,我们具有三个if条件和一个while循环,它们都比较字符。 幸运的是,正如我们已经看到的,我们可以将字符视为数字,因此我们可以使用在`test_me()`示例中使用的相同距离估计。 但是,还有两个条件可以检查字符是否包含在集合中,例如: `digit_high in hex_values`。 集合中包含的值接近多少? 一个明显的解决方案是将与集合中最接近的值的距离视为估计值。 + +```py +import [sys](https://docs.python.org/3/library/sys.html) + +``` + +```py +def distance_character(target, values): + + # Initialize with very large value so that any comparison is better + minimum = sys.maxsize + + for elem in values: + distance = abs(target - elem) + if distance < minimum: + minimum = distance + return minimum + +``` + +```py +distance_character(10, [1, 5, 12, 100]) + +``` + +```py +2 + +``` + +```py +distance_character(10, [0, 50, 80, 200]) + +``` + +```py +10 + +``` + +到目前为止,我们所做的进一步简化是假设我们总是希望条件评估为真。 在实践中,我们可能还希望if条件的评估结果也为false。 因此,每个if条件实际上有两个距离估计值,一个估计它与真实条件的接近程度,另一个估计与错误条件的接近程度。 如果条件为真,则真实距离为0;否则为0。 如果条件为假,则假距为0。即,在比较`a == b`中,如果`a`小于`b`,则假距为`0`。 + +当`a`等于`b`时`a == b`为假的距离是多少? 对`a`或`b`的任何更改都会使条件评估为false,因此在这种情况下,我们可以简单地将距离定义为1。 + +更一般地,可以存在其他类型的比较,例如使用关系运算符。 考虑`cgi_decode()`:`i < len(s)`中的循环条件,即它使用小于比较运算符。 将分支距离的概念扩展到涵盖不同类型的比较,并计算正确和错误的距离是很直接的。 下表显示了如何计算不同类型的比较的距离: + +| 健康)状况 | 距离真 | 距离错误 | +| --- | --- | --- | +| a == b | abs(a-b) | 1 | +| a!= b | 1 | abs(a-b) | +| a < b | b-a + 1 | - | +| a < = b | b-a | a-b + 1 | +| a > b | a-b + 1 | b-a | + +请注意,一些计算会添加一个常量`1`。 这样做的原因很简单:假设我们要让`a < b`评估为true,然后让`a = 27`和`b = 27`成立。 条件不成立,但简单地求和将得出`0`的结果。 为了避免这种情况,我们必须添加一个常数。 该值是否为`1`并不重要-任何正常数都起作用。 + +### 处理复杂条件 + +在`cgi_decode()`函数中,我们还可以找到一个稍微复杂一些的谓词,该谓词由两个条件组成,这些条件由一个逻辑`and`连接: + +`if digit_high in hex_values and digit_low in hex_values:` + +原则上,将分支距离定义为使得使连词`A and B`为真的距离等于`A`和`B`的分支距离之和,因为两个条件都必须为真。 同样,使`A or B`为真的分支距离将为`A`和`B`的两个分支距离中的最小值,因为如果两个条件之一为真就可以使整个表达式为真。 + +但是,这并不像在实践中那样容易:谓词可以由嵌套条件和否定组成,并且在应用此计算之前,需要将表达式转换为规范形式。 此外,大多数现代编程语言都使用*短路评估*:如果存在条件`A or B`且`A`为true,则永远不会评估`B`。 如果`B`是带有副作用的表达式,那么即使短路评估会避免执行`B`的分支距离,也可以通过计算`B`的分支距离,来潜在地改变程序的行为(通过调用会导致副作用的副作用) 在正常行为中不执行),那是不可接受的。 + +此外,如果分支条件有副作用怎么办? 例如,假设分支条件为`x == 2 * foo(y)`,其中`foo()`是采用整数作为输入的函数。 幼稚的检测将导致以下代码: + +```py + distance = abs(x - 2 * foo(y)) + if x == 2 * foo(y): + ... +``` + +因此,检测将导致两次执行的`foo()`。 假设`foo()`更改了系统状态(例如,通过打印某些内容,访问文件系统,更改了一些状态变量等),然后清楚地第二次调用`foo()`是个坏主意。 解决此问题的一种方法是*转换​​*条件,而不是*添加*跟踪调用。 例如,可以创建一个临时变量来保存距离计算所需的值,然后在分支条件中使用它们: + +```py + tmp1 = x + tmp2 = 2 * foo(y) + distance = abs(tmp1 - tmp2) + if tmp1 == tmp2: + ... +``` + +除了这些问题之外,向程序中添加全局变量和方法调用的方法似乎还很笨拙---当然,我们不能开始自己考虑程序中的每个分支,也无法对要手动测试的程序进行检测, 特别是如果程序具有多个分支,例如`cgi_decode()`功能。 相反,我们应该研究如何自动仪器程序包含必需的添加语句,以便我们可以计算适合度值。 + +### 原子条件仪器 + +使用全局变量和临时变量的另一种方法是用对辅助函数的调用来代替实际的比较,该函数将原始表达式作为参数求值,而运算符是一个附加参数。 假设我们有一个函数`evaluate_condition()`,它带有四个参数: + +* `num`是标识条件的唯一ID; +* `op`是比较的运算符; +* `lhs`和`rhs`是操作数。 + +该函数计算条件的两个距离:到条件评估为真的距离和到条件评估为假的距离。 这两个结果之一将始终为真,因此其中之一将始终具有距离`0`。 由于该函数替换了原始比较,因此根据`0`的距离返回true或false。 这意味着示例表达式 + +```py + if x == 2 * foo(y) +``` + +将被替换 + +```py + if evaluate_condition(0, "Eq", x, 2 * foo(y)) +``` + +这样,参数仅计算一次,因此可以正确处理副作用。 `evaluate_condition()`函数的外观如下: + +```py +def evaluate_condition(num, op, lhs, rhs): + distance_true = 0 + distance_false = 0 + if op == "Eq": + if lhs == rhs: + distance_false = 1 + else: + distance_true = abs(lhs - rhs) + + # ... code for other types of conditions + + if distance_true == 0: + return True + else: + return False + +``` + +请注意,我们正在使用`0`初始化`distance_true`和`distance_false`。 因此,如果`lhs`等于`rhs`,则变量`distance_true`保持为0,反之亦然。 + +```py +evaluate_condition(1, "Eq", 10, 20) + +``` + +```py +False + +``` + +```py +evaluate_condition(2, "Eq", 20, 20) + +``` + +```py +True + +``` + +`evaluate_condition()`功能尚未执行的操作是存储观察到的距离。 显然,我们需要将值存储在某个位置,以便我们可以从适应性函数中访问它。 由于`cgi_decode()`程序由多个条件组成,并且对于每个条件我们可能都对真实和错误距离感兴趣,因此我们仅使用两个全局词典`distances_true`和`distances_false`,并定义一个用于存储 在字典中观察到的距离值: + +```py +def update_maps(condition_num, d_true, d_false): + global distances_true, distances_false + + if condition_num in distances_true.keys(): + distances_true[condition_num] = min( + distances_true[condition_num], d_true) + else: + distances_true[condition_num] = d_true + + if condition_num in distances_false.keys(): + distances_false[condition_num] = min( + distances_false[condition_num], d_false) + else: + distances_false[condition_num] = d_false + +``` + +变量`condition_num`是我们刚刚评估的条件的唯一ID。 如果这是我们第一次执行此特定条件,则将正确和错误的距离简单地存储在相应的字典中。 但是,同一测试有可能多次执行条件。 例如,`cgi_decode()`函数中的循环条件`i < len(s)`在每个单循环迭代之前进行评估。 但是,最后,我们希望为测试提供一个适合度值。 由于覆盖分支仅要求至少有一个执行到达分支,因此我们仅考虑最接近的一个。 因此,如果`distances_true`和`distances_false`字典已经包含与上一个执行程序的距离,则仅当新的执行程序更接近分支时才替换该值;否则,将替换该值。 这是使用`min()`功能实现的。 + +现在,我们需要从`evaluate_condition()`中调用此函数。 我们还要为`in`运算符和`<`比较添加距离计算,因为在`cgi_decode()`示例中我们都需要它们。 此外,`cgi_decode()`实际上比较字符和数字。 为了确保使用正确的类型,我们首先必须将字符转换为数字以计算距离。 这是使用Python的`ord()`函数完成的。 + +```py +def evaluate_condition(num, op, lhs, rhs): + distance_true = 0 + distance_false = 0 + + # Make sure the distance can be calculated on number and character + # comparisons + if isinstance(lhs, str): + lhs = ord(lhs) + if isinstance(rhs, str): + rhs = ord(rhs) + + if op == "Eq": + if lhs == rhs: + distance_false = 1 + else: + distance_true = abs(lhs - rhs) + + elif op == "Lt": + if lhs < rhs: + distance_false = rhs - lhs + else: + distance_true = lhs - rhs + 1 + # ... + # handle other comparison operators + # ... + + elif op == "In": + minimum = sys.maxsize + for elem in rhs.keys(): + distance = abs(lhs - ord(elem)) + if distance < minimum: + minimum = distance + + distance_true = minimum + if distance_true == 0: + distance_false = 1 + + update_maps(num, distance_true, distance_false) + + if distance_true == 0: + return True + else: + return False + +``` + +下面显示了如何使用`cgi_decode()`功能使用`cgi_decode()`进行连接。 对应于这两个条件,有两个对`evaluate_condition`的调用,并且将它们结合在一起的`and`可确保保留原始的短路行为: + +`if (evaluate_condition(4, 'In', digit_high, hex_values) and evaluate_condition(5, 'In', digit_low, hex_values))` + +当然,我们希望自动产生这种仪器化的变体。 + +### 自动检测源代码 + +实际上,使用程序的抽象语法树(AST)在Python中自动替换比较非常容易。 在AST中,比较通常是一个树节点,该节点具有一个运算符属​​性,两个运算符分别用于左手运算符和右手运算符。 要用对`evaluate_condition()`的调用替换此类比较,只需将AST中的比较节点替换为函数调用节点,这就是`BranchTransformer`类使用Python的`ast`模块中的NodeTransformer所做的事情: + +```py +import [ast](https://docs.python.org/3/library/ast.html) + +``` + +```py +class BranchTransformer(ast.NodeTransformer): + + branch_num = 0 + + def visit_FunctionDef(self, node): + node.name = node.name + "_instrumented" + return self.generic_visit(node) + + def visit_Compare(self, node): + if node.ops[0] in [ast.Is, ast.IsNot, ast.In, ast.NotIn]: + return node + + self.branch_num += 1 + return ast.Call(func=ast.Name("evaluate_condition", ast.Load()), + args=[ast.Num(self.branch_num), + ast.Str(node.ops[0].__class__.__name__), + node.left, + node.comparators[0]], + keywords=[], + starargs=None, + kwargs=None) + +``` + +`BranchTransformer`使用内置解析器`ast.parse()`解析目标Python程序,该解析器返回AST。 Python提供了用于遍历和修改此AST的API。 为了用函数调用代替比较,我们使用`ast.NodeTransformer`,它使用访问者模式,其中AST中每种类型的节点都有一个`visit_*`函数。 因为我们有兴趣替换比较,所以我们覆盖了`visit_Compare`,在这里,我们返回了一个类型为`ast.Func`的新节点,而不是原始的比较节点,这是一个函数调用节点。 该节点的第一个参数是函数`evaluate_condition()`的名称,参数是我们的`evaluate_condition()`函数所期望的四个参数: + +* 分支的数量(我们将其保留在`branch_num`中), +* 运算符(我们只使用其类名), +* 左侧,以及 +* 右边。 + +请注意,Python允许比较多个表达式(例如`1 < x < 10`); 为了使代码保持简单,我们在这里仅处理单个比较,但是通过将每个比较与对`evaluate_condition`的单独调用进行处理,可以扩展代码。 您会注意到,我们还覆盖了`visit_FunctionDef`; 这只是通过添加`_instrumented`来更改方法的名称,以便我们可以继续将原始功能与已检测的功能一起使用。 + +以下代码将`cgi_decode()`函数的源代码解析为AST,然后对其进行转换,然后再次打印出来(使用`astor`库中的`to_source()`函数): + +```py +import [inspect](https://docs.python.org/3/library/inspect.html) +import [ast](https://docs.python.org/3/library/ast.html) +import [astor](https://docs.python.org/3/library/astor.html) + +``` + +```py +from [fuzzingbook_utils](https://github.com/uds-se/fuzzingbook/tree/master/notebooks/fuzzingbook_utils) import print_content + +``` + +```py +source = inspect.getsource(cgi_decode) +node = ast.parse(source) +BranchTransformer().visit(node) + +# Make sure the line numbers are ok before printing +node = ast.fix_missing_locations(node) +print_content(astor.to_source(node), '.py') + +``` + +```py +def cgi_decode_instrumented(s): + """Decode the CGI-encoded string `s`: + * replace "+" by " " + * replace "%xx" by the character with hex number xx. + Return the decoded string. Raise `ValueError` for invalid inputs.""" + hex_values = {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, + '7': 7, '8': 8, '9': 9, 'a': 10, 'b': 11, 'c': 12, 'd': 13, 'e': 14, + 'f': 15, 'A': 10, 'B': 11, 'C': 12, 'D': 13, 'E': 14, 'F': 15} + t = '' + i = 0 + while evaluate_condition(1, 'Lt', i, len(s)): + c = s[i] + if evaluate_condition(2, 'Eq', c, '+'): + t += ' ' + elif evaluate_condition(3, 'Eq', c, '%'): + digit_high, digit_low = s[i + 1], s[i + 2] + i += 2 + if evaluate_condition(4, 'In', digit_high, hex_values + ) and evaluate_condition(5, 'In', digit_low, hex_values): + v = hex_values[digit_high] * 16 + hex_values[digit_low] + t += chr(v) + else: + raise ValueError('Invalid encoding') + else: + t += c + i += 1 + return t + +``` + +要使用工具版本计算适应性值,我们需要再次编译工具AST,这是使用Python的`compile()`函数完成的。 然后,我们需要使已编译函数可访问,为此,首先要从`sys.modules`中检索当前模块,然后使用`exec`将已检测函数的已编译代码添加到当前模块的函数列表中。 此后,可以访问`cgi_decode_instrumented()`功能。 + +```py +def create_instrumented_function(f): + source = inspect.getsource(f) + node = ast.parse(source) + node = BranchTransformer().visit(node) + + # Make sure the line numbers are ok so that it compiles + node = ast.fix_missing_locations(node) + + # Compile and add the instrumented function to the current module + current_module = sys.modules[__name__] + code = compile(node, filename="", mode="exec") + exec(code, current_module.__dict__) + +``` + +```py +# Set up the global maps +distances_true = {} +distances_false = {} + +``` + +```py +# Create instrumented function +# cgi_decode_instrumented = +create_instrumented_function(cgi_decode) + +``` + +```py +assert cgi_decode("Hello+Reader") == cgi_decode_instrumented("Hello+Reader") + +``` + +```py +cgi_decode_instrumented("Hello+Reader") + +``` + +```py +'Hello Reader' + +``` + +```py +distances_true + +``` + +```py +{1: 0, 2: 0, 3: 35} + +``` + +```py +distances_false + +``` + +```py +{1: 0, 2: 0, 3: 0} + +``` + +从`distances_true`和`distances_false`映射中可以看出,条件1和2在`cgi_decode_instrumented`上执行时已评估为true和false,而条件3仅已评估为false。 这是预期的,因为进入并离开了while循环,并且输入字符串中只有一个空格,但没有`%`-字符。 + +### 适用于创建有效十六进制输入的适应度函数 + +例如,让我们以测试`cgi_decode()`的一部分来解码有效十六进制代码为目标。 这意味着我们要使条件1为真,2为假,3为真和4为真。 为了表示这样的路径,我们可以简单地总结出这些分支的分支距离。 但是,简单地总结分支距离存在潜在的问题:如果一个条件的距离取决于很大的值的比较,而另一种条件的距离取决于小值的计算,则很可能会改进大值 导致更好的适应性改善,从而使搜索产生偏见。 为避免这种情况,我们需要先将*归一化*分支距离,然后再将其相加。 + +范围`[a, b]`的归一化函数将数字作为输入,并返回`>=a`和`<=b`的值。 该函数的重要之处在于,对于任何两个数字`x`和`y`,都需要通过规范化来保留顺序。 也就是说,如果为`x 0: + changed = False + for (nextx) in neighbour_strings(x): + new_fitness = get_fitness(nextx) + if new_fitness < fitness: + x = nextx + fitness = new_fitness + changed = True + print("New value: %s at fitness %.4f" % (x, fitness)) + break + + # Random restart if necessary + if not changed: + x = random_string(10) + fitness = get_fitness(x) + + print("Optimum at %s, fitness %.4f" % (x, fitness)) + +``` + +```py +hillclimb_cgi() + +``` + +```py +Initial input: o'@[3(rW*M at fitness 2.6667 +New value: o&@[3(rW*M at fitness 2.5000 +New value: o%@[3(rW*M at fitness 1.5000 +New value: o%A[3(rW*M at fitness 0.8571 +New value: o%A\3(rW*M at fitness 0.8333 +New value: o%A]3(rW*M at fitness 0.8000 +New value: o%A^3(rW*M at fitness 0.7500 +New value: o%A_3(rW*M at fitness 0.6667 +New value: o%A`3(rW*M at fitness 0.5000 +New value: o%Aa3(rW*M at fitness 0.0000 +Optimum at o%Aa3(rW*M, fitness 0.0000 + +``` + +运行hillclimber几次,以查看它生成带有有效十六进制字符的字符串。 有时只需要几个步骤,有时就需要更长的时间,但是最后它总是可以解决问题并生成我们想要的字符串。 + +## 进化搜索 + +如果邻里很小,爬山的效果很好。 到目前为止,在`cgi_decode()`示例中就是这种情况,因为我们将自己限制为固定数量的字符(10),并将字符范围限制为可打印的ASCII字符。 但是想象一下,如果我们不寻找ASCII字符,而是寻找UTF-16 Unicode字符,会发生什么? 确实不允许在URL中使用它们,但是让我们看看如果更改搜索空间会发生什么: + +```py +def random_unicode_string(l): + s = "" + for i in range(l): + # Limits to reflect range of UTF-16 + random_character = chr(random.randrange(0, 65536)) + s = s + random_character + return s + +``` + +```py +def unicode_string_neighbours(x): + n = [] + for pos in range(len(x)): + c = ord(x[pos]) + # Limits to reflect range of UTF-16 + if c < 65536: + n += [x[:pos] + chr(c + 1) + x[pos + 1:]] + if c > 0: + n += [x[:pos] + chr(c - 1) + x[pos + 1:]] + + return n + +``` + +UTF-8字符用16位表示,这为我们提供了65536个可能的字符范围。 以上功能适用于这些边界。 在再次运行爬坡器之前,让我们再做一个更改:我们将添加一个迭代限制,以便我们可以放弃搜索,而不是永远等待搜索完成。 + +```py +def terminal_repr(s): + return terminal_escape(repr(s)) + +``` + +```py +def hillclimb_cgi_limited(max_iterations): + x = random_unicode_string(10) + fitness = get_fitness(x) + print("Initial input: %s at fitness %.4f" % (terminal_repr(x), fitness)) + + iteration = 0 + logs = 0 + while fitness > 0 and iteration < max_iterations: + changed = False + for (nextx) in unicode_string_neighbours(x): + new_fitness = get_fitness(nextx) + if new_fitness < fitness: + x = nextx + fitness = new_fitness + changed = True + if logs < LOG_VALUES: + print("New value: %s at fitness %.4f" % + (terminal_repr(x), fitness)) + elif logs == LOG_VALUES: + print("...") + logs += 1 + break + + # Random restart if necessary + if not changed: + x = random_string(10) + fitness = get_fitness(x) + iteration += 1 + + print("Optimum at %s, fitness %.4f" % (terminal_repr(x), fitness)) + +``` + +```py +hillclimb_cgi_limited(100) + +``` + +```py +Initial input: '埂\udf19\uf67c듵騛쁥핡勸\uf172싕' at fitness 3.0000 +New value: '埂\udf19\uf67c듵騛쁥핡勷\uf172싕' at fitness 3.0000 +New value: '埂\udf19\uf67c듵騛쁥핡勶\uf172싕' at fitness 3.0000 +New value: '埂\udf19\uf67c듵騛쁥핡勵\uf172싕' at fitness 3.0000 +New value: '埂\udf19\uf67c듵騛쁥핡勴\uf172싕' at fitness 3.0000 +New value: '埂\udf19\uf67c듵騛쁥핡勳\uf172싕' at fitness 3.0000 +New value: '埂\udf19\uf67c듵騛쁥핡勲\uf172싕' at fitness 3.0000 +New value: '埂\udf19\uf67c듵騛쁥핡勱\uf172싕' at fitness 3.0000 +New value: '埂\udf19\uf67c듵騛쁥핡勰\uf172싕' at fitness 3.0000 +New value: '埂\udf19\uf67c듵騛쁥핡勯\uf172싕' at fitness 3.0000 +New value: '埂\udf19\uf67c듵騛쁥핡勮\uf172싕' at fitness 3.0000 +New value: '埂\udf19\uf67c듵騛쁥핡勭\uf172싕' at fitness 3.0000 +New value: '埂\udf19\uf67c듵騛쁥핡勬\uf172싕' at fitness 3.0000 +New value: '埂\udf19\uf67c듵騛쁥핡勫\uf172싕' at fitness 3.0000 +New value: '埂\udf19\uf67c듵騛쁥핡勪\uf172싕' at fitness 3.0000 +New value: '埂\udf19\uf67c듵騛쁥핡勩\uf172싕' at fitness 3.0000 +New value: '埂\udf19\uf67c듵騛쁥핡勨\uf172싕' at fitness 3.0000 +New value: '埂\udf19\uf67c듵騛쁥핡勧\uf172싕' at fitness 3.0000 +New value: '埂\udf19\uf67c듵騛쁥핡勦\uf172싕' at fitness 3.0000 +New value: '埂\udf19\uf67c듵騛쁥핡勥\uf172싕' at fitness 3.0000 +New value: '埂\udf19\uf67c듵騛쁥핡勤\uf172싕' at fitness 3.0000 +... +Optimum at '埂\udf19\uf67c듵騛쁥핡劔\uf172싕', fitness 3.0000 + +``` + +您可以尝试迭代限制,并查看在此搜索空间中找到解决方案需要多长时间。 除非您对随机的起点感到幸运,否则通常会花费不合理的时间。 + +### 全局搜索 + +爬山者在搜索的每个步骤中都会探索一个人的本地邻域,如果邻域太大,那么这将花费很长时间。 一种替代策略是不将搜索限制在本地附近,而是全局搜索搜索空间。 也就是说,允许搜索算法在搜索空间内进行更大的调整。 对爬山者的简单修改即可将其从本地搜索算法转换为全局搜索算法:无需查看所有直接邻居,而是以允许进行较大修改的方式对*进行了突变*。 + +*突变*是一种变化,代表围绕搜索空间的较大步幅。 实施突变时,一个重要的决定是,从理论上讲,只需连续应用突变,就可以到达搜索空间中的任何点。 但是,突变通常不应完全用随机的个体代替个体。 对于该搜索而言,重要的是,该突变构成对仍然保留其大部分特征的个体的合理改变。 对于我们的10个字符字符串的搜索问题,可能的突变是在10个字符中仅替换 *1* ,如下所示: + +```py +def flip_random_character(s): + pos = random.randint(0, len(s) - 1) + new_c = chr(random.randrange(0, 65536)) + return s[:pos] + new_c + s[pos + 1:] + +``` + +对爬山者的一个简单修改就是用突变代替对邻域的探索。 在每次迭代中,当前个体都会发生变异。 将生成的后代个体与其父代个体进行比较,两者中的更好者是搜索空间中的新点,并用于下一轮突变。 这称为“随机爬山”。 + +```py +def randomized_hillclimb(): + x = random_unicode_string(10) + fitness = get_fitness(x) + print("Initial value: %s at fitness %.4f" % + (terminal_repr(x), fitness)) + + iterations = 0 + while fitness > 0: + mutated = flip_random_character(x) + new_fitness = get_fitness(mutated) + if new_fitness <= fitness: + x = mutated + fitness = new_fitness + #print("New value: %s at fitness %.4f" %(terminal_repr(x), fitness)) + iterations += 1 + + print("Optimum at %s after %d iterations" % + (terminal_repr(x), iterations)) + +``` + +```py +randomized_hillclimb() + +``` + +```py +Initial value: '舨ᑆ\uec4f\ue27f羏\uf314䖗繣厪킨' at fitness 2.9998 +Optimum at 'ጤ〆撟%e5匸㗵暠ᴌ' after 34356 iterations + +``` + +该算法通常比标准爬坡器更快地找到解决方案,尽管它仍然相当慢! + +值得指出的是,爬山和随机版本之间有一个微妙但至关重要的变化:请注意比较`new_fitness <= fitness`,而在爬山中我们使用`new_fitness < fitness`。 这很重要,因为搜索范围可能具有适合度值(高原)相等的区域,我们需要以某种方式克服这些适合度值。 在标准的Hillcimber中,我们通过随机重启来完成此操作。 如果随机的爬山者拒绝适应度相同的后代,它将继续变异相同的个体,并可能难以摆脱高原。 如果我们也用相同的适应性替换,则允许搜索在高原上移动,最终增加逃脱的机会。 + +随机爬山者也称为 *1 + 1进化算法*(*(1 + 1)EA* )。 进化算法是一种试图启发自然进化过程的元启发式搜索算法。 自然进化的基础是环境压力导致自然选择的种群:只有适者生存,而那些不适应者将死亡,因此种群的整体适应度逐渐提高。 (1 + 1)EA是一种非常特殊的进化算法,种群大小为1,可以精确地产生1个后代。 但是,实践中最常见的搜索算法是遗传算法。 + +### 遗传算法 + +最著名的进化算法之一是*遗传算法*(GA)。 遗传算法的基础是问题解决方案可以通过基因编码:染色体由一系列基因组成,其中每个基因都编码一个人的一个特征(例如,眼睛的颜色,头发的颜色等)。 适应度函数可以获取此描述中包含的信息,即所谓的基因型,并评估所得表型的属性,即该遗传编码表示的实际解。 在表型上测量个体的适应性值。 + +对于我们的`cgi_decode()`示例,我们可以将单个字符视为基因,然后染色体是字符序列。 换句话说,到目前为止,我们已经使用了遗传编码! 但是,对于GA来说,代表所需要的操作与邻居的枚举不同。 + +遗传算法通过以下过程模拟自然进化: + +* 创建随机染色体的初始种群 +* 选择适合的个体进行繁殖 +* 通过选定个体的繁殖产生新的种群 +* 继续这样做,直到找到最佳解决方案或达到其他限制。 + +创建初始种群的第一步很简单: + +```py +def create_population(size): + return [random_unicode_string(10) for i in range(size)] + +``` + +```py +create_population(10) + +``` + +```py +['㛇莜戹豔㮝\ue008力ᒐᱯꊎ', + '\ue295쁉陵ꯃ赖蟽⍬꺲緥㲱', + '\u2e77騬⊳铁땕\uf189\ue663쌯幆艆', + '龚筓\uf3a9욀놷䓒隯迌쀐∑', + '쓛唁뗌蹽\udcd5偏躝娒鸭赌', + '蹤⪖ឺ웳ఫ䓻䤷溸ᰒﵘ', + '㲒혋Ꮘ\uea95\udf7b䢶裕㖬눹庑', + '\u2d9d鼓咝笩窻ꨔ铺扄\ueb57\uf1bf', + '\udd26Ч甄ꃢ\udcb0㼁튰㣋ช봼', + '닇軬欴鞛㈓Ẫ住畿ꔪ칀'] + +``` + +选择过程偏向于更健康的个体,因此我们需要确定人口中所有个体的适应度值。 由于我们将在算法中的多个位置需要每个人的适应度值,因此我们要通过使适应度成为*元组*的列表来存储它,每个元组都由一个人及其适应度值组成。 + +```py +def evaluate_population(population): + fitness = [get_fitness(x) for x in population] + return list(zip(population, fitness)) + +``` + +```py +population = create_population(10) + +``` + +```py +for (individual, fitness) in evaluate_population(population): + print("%s: %.4f" % (terminal_repr(individual), fitness)) + +``` + +```py +'\ue46fت팣㘇ธ漅ೇ↪䜭㎮': 2.9994 +'\ua7e9\ue9e0\ue7de턤댪囿厠‐\ue0c5ﴌ': 2.9999 +'䇯Ꜩ잣\ua9ce㙦\uf4aa歿蘸ཎ㫜': 2.9997 +'ⱳ콾哇\uef1f\uf6a8⾃䣖坤Ꮾ둰': 2.9998 +'າ伫螉\ue110㝎겱괼䆵喞\u18ac': 2.9997 +'ས婎쥤䇉韤\udd06䕮춵磒露': 2.9997 +'辺緢噑粲\uf400嚳寬\x87ꐂ쑹': 2.9899 +'뉫㻑\u1fd4㈖鬒樼\u0dcc朏\ue57eማ': 2.9997 +'얨㐁皭់멄\ud97b\ueac9㢂ቅ뮜': 2.9998 +'⒃⍜㈪㊷\udbdb乑蓍\udc48鱲뻜': 2.9999 + +``` + +在搜寻中使用适应性值通常是用“适者生存”来解释的,但达尔文对进化的主要见解之一是选择不仅取决于生存-个人通过性繁殖,而性选择描述了选择压力 在繁殖期间。 这种选择通常受以下两种情况之一的影响:竞争中的雄性为雌性而战,雄性(强力)获胜; 选择也受显示影响。 达尔文(Darwin)的例子就是孔雀:孔雀有长长而美丽的尾羽,似乎毫无用处,似乎也不支持自然选择的概念。 但是,豌豆的出现会影响其选择性伴侣的能力。 令人印象深刻的装饰品表明,男性在遗传上特别健康,并将导致健康的后代。 这在GA中得到了体现:一个人的适应度值越好,与另一个人交配的可能性就越大。 反映此概念的简单选择算法是*比赛选择*:少数随机选择的个人参加比赛,其中最好的被选中: + +```py +def selection(evaluated_population, tournament_size): + competition = random.sample(evaluated_population, tournament_size) + winner = min(competition, key=lambda individual: individual[1])[0] + + # Return a copy of the selected individual + return winner[:] + +``` + +`tournament_size`参数指定从总体中随机选择的个人中有多少人参加锦标赛。 这是一个重要的选择,因为它决定了*选择压力*:比赛规模越大,优秀个人被纳入比赛的可能性就越大。 反过来,这增加了这些非常好的个人支配下一代的可能性,从而降低了多样性并导致了过早的收敛。 相反,如果比赛的规模太小,那么这会阻碍比赛的进行。 锦标赛规模的最佳值取决于人口规模,但通常很小(例如5)。 让我们使用示例人群中的所有个体来运行一次锦标赛,以查看是否选择了最佳锦标赛: + +```py +population = create_population(10) +fitness = evaluate_population(population) +selected = selection(fitness, 10) + +``` + +```py +for (individual, fitness_value) in fitness: + print("%s: %.4f" % (terminal_repr(individual), fitness_value)) + +``` + +```py +'둚䘣蹸붢騒ꋎỺ觉\ued2a焊': 2.9999 +'닔㶐ꡡ\udca4贕굇᳆\ueccd윘赉': 2.9999 +'笁깉ရ豴\uea60ᛰ滒鶵)Ⰴ': 2.9998 +'㠒㵄\ue14cᤃᇚ\udebe\uf851쿛鏆闊': 2.9998 +'ࠝ\udd53ᶂ㮍訬廘랦铘뫘출': 2.9995 +'ꡩၔ玤姨乪药汏䗫汔悔': 2.9998 +'\uef2a潦ﳠ鹉卼㴐Ṭ\uf4b0˅얓': 2.9985 +'❱\u0ff7╇塐䲫琭煸ᮤឋ퇃': 2.9998 +'ڮ焈㱉煉ꅿᦗ툍혐خ꧓': 2.9994 +'ἂጋ\uddcc\ue3d7粖\ude2c⮌鮓讃\ueab6': 2.9998 + +``` + +```py +print("Winner: %s" % terminal_repr(selected)) + +``` + +```py +Winner: '\uef2a潦ﳠ鹉卼㴐Ṭ\uf4b0˅얓' + +``` + +就像在自然进化中一样,根据自己的健康状况选择的个体也可以繁殖,形成新一代。 在这种繁殖过程中,就像在自然繁殖过程中一样,所选亲本的遗传物质也会被合并。 这通常是通过称为 *crossover* 的过程完成的,其中后代染色体是由其父母的基因产生的。 在我们的例子中,一条染色体是一个字符序列,通过选择一个截止随机点,然后通过基于截止点组合父母染色体的两半来创建后代,可以简单地跨越两个父字符序列。 + +```py +def crossover(parent1, parent2): + pos = random.randint(1, len(parent1)) + + offspring1 = parent1[:pos] + parent2[pos:] + offspring2 = parent2[:pos] + parent1[pos:] + + return (offspring1, offspring2) + +``` + +```py +parent1 = "Hello World" +parent2 = "Goodbye Book" + +crossover(parent1, parent2) + +``` + +```py +('Hello W Book', 'Goodbyeorld') + +``` + +遗传进化的另一个重要组成部分是突变的概念:有时候,后代的基因会有细微的变化,代表着新的遗传物质和新的生理特性。 如果突变引入了代表有用特性的新遗传物质,从而导致了更好的适应性值,那么该遗传物质将生存。 如果该突变引入了无用的遗传物质,那么产生的个体将很可能具有不良适应性值并迅速死亡。 + +一个重要方面是,突变和交叉都是概率行为。 它们并不总是会发生,并且每次发生时其影响都是不同的。 对于序列遗传编码,一种常见的方法是首先使用概率来决定是否应用突变,然后选择一个基因并对其稍加更改。 或者,我们可以概率性地突变基因,使得平均一个基因发生改变。 我们通过计算每个基因被突变为1 /(基因数量)的概率`P`来做到这一点。 然后,如果基因发生突变,我们不仅会用随机字符代替它,而且更有可能只有很小的变化。 这是通过以当前值作为平均值从高斯分布中采样来实现的。 我们任意使用100的标准偏差来使小的变化比大的变化更有可能。 + +```py +def mutate(chromosome): + mutated = chromosome[:] + P = 1.0 / len(mutated) + + for pos in range(len(mutated)): + if random.random() < P: + new_c = chr(int(random.gauss(ord(mutated[pos]), 100) % 65536)) + mutated = mutated[:pos] + new_c + mutated[pos + 1:] + return mutated + +``` + +现在,终于有了构成完整算法的所有要素: + +```py +def genetic_algorithm(): + # Generate and evaluate initial population + generation = 0 + population = create_population(100) + fitness = evaluate_population(population) + best = min(fitness, key=lambda item: item[1]) + best_individual = best[0] + best_fitness = best[1] + print("Best fitness of initial population: %s - %.10f" % + (terminal_repr(best_individual), best_fitness)) + logs = 0 + + # Stop when optimum found, or we run out of patience + while best_fitness > 0 and generation < 1000: + + # The next generation will have the same size as the current one + new_population = [] + while len(new_population) < len(population): + # Selection + offspring1 = selection(fitness, 10) + offspring2 = selection(fitness, 10) + + # Crossover + if random.random() < 0.7: + (offspring1, offspring2) = crossover(offspring1, offspring2) + + # Mutation + offspring1 = mutate(offspring1) + offspring2 = mutate(offspring2) + + new_population.append(offspring1) + new_population.append(offspring2) + + # Once full, the new population replaces the old one + generation += 1 + population = new_population + fitness = evaluate_population(population) + + best = min(fitness, key=lambda item: item[1]) + best_individual = best[0] + best_fitness = best[1] + if logs < LOG_VALUES: + print( + "Best fitness at generation %d: %s - %.8f" % + (generation, terminal_repr(best_individual), best_fitness)) + elif logs == LOG_VALUES: + print("...") + logs += 1 + + print( + "Best individual: %s, fitness %.10f" % + (terminal_repr(best_individual), best_fitness)) + +``` + +让我们看看这在我们的unicode示例中如何工作。 + +```py +genetic_algorithm() + +``` + +```py +Best fitness of initial population: '俴\x8a辰燄펧䬍缯檒㤢⦡' - 2.9901960784 +Best fitness at generation 1: 'ཬə쫯ὼ酟剺鬈{Ż\ue313' - 2.98850575 +Best fitness at generation 2: '俴\x82辰燄酟剺鬢\x1bŻ\ue313' - 2.90909091 +Best fitness at generation 3: 'ྂɵ쫕ὼ酟剺鬈%Ŧ\ue384' - 1.99610895 +Best fitness at generation 4: 'ྂɵ쬆ὼ酟剺鬈%ŋ\ue384' - 1.99565217 +Best fitness at generation 5: '俴ʐ쫕ἶ酟剺髺%¼\ue384' - 1.98850575 +Best fitness at generation 6: 'ྂɵ쫪ὼ酟剒鬈%\x9e\ue37f' - 1.98245614 +Best fitness at generation 7: '俴̗쪕ἶ酟剺髱%4\ue378' - 0.99998280 +Best fitness at generation 8: '侪\x86쪕ἶ酟剺髱%4\ue378' - 0.99998280 +Best fitness at generation 9: '俆ǫ쪸ἶ醿剺髱%4\ue2b5' - 0.99998274 +Best fitness at generation 10: '俆ǫ쪸ἶ釘剺髧%4\ue2b5' - 0.99998274 +Best fitness at generation 11: '俆ɫ쫕Ồ酟剺髱%4\ue256' - 0.99998271 +Best fitness at generation 12: '侪\x86쪕ἶ醿剺髱%4\ue1f9' - 0.99998268 +Best fitness at generation 13: '侪ï쪕ἶ醿剺髱%4\ue1f9' - 0.99998268 +Best fitness at generation 14: '俆\x86쪕ἶ醿剺髱%4\ue1f9' - 0.99998268 +Best fitness at generation 15: '俆Ǩ쪸ἂ醿剺髱%4\ue150' - 0.99998263 +Best fitness at generation 16: '俴ʓ쫕ớ鄞务髱%e\ue0f0' - 0.99998260 +Best fitness at generation 17: '侪ï쪵ớ酟刖髱%F\ue0a9' - 0.99998258 +Best fitness at generation 18: '佺ģ쪵ớ鄞劐髱%e\ue05c' - 0.99998256 +Best fitness at generation 19: '侪ï쪵ớ酟刖髱%e\ue05c' - 0.99998256 +Best fitness at generation 20: '侪ï쪵ί酃劐髝%e\ue00c' - 0.99998253 +... +Best individual: '予ß쥇᳗轐咆隭%2A', fitness 0.0000000000 + +``` + +## 经验教训 + +* 元启发式搜索问题由算法,表示形式和适应度函数组成。 +* 对于测试生成,适应度函数通常会估计执行到目标位置的距离。 为了确定该距离,我们使用仪器来计算测试执行期间的距离。 +* 当邻域定义良好且不太大时,像爬坡这样的本地搜索算法可以很好地工作。 +* 全局搜索算法(例如遗传算法)非常灵活,可以很好地扩展到更大的测试问题。 + +## 后续步骤 + +在本章中,我们介绍了相当简单的程序输入。 我们还可以应用相同的搜索算法来发展复杂的测试输入,尤其是语法输入的[。](EvoGrammarFuzzer.html) + +## 背景 + +\ todo {添加更多} + +搜索的目标通常与覆盖范围有关。 有关讨论,请参见[测试简介](Intro_Testing.html)中的书。 + +## 练习 + +\ todo {稍后会添加} \ No newline at end of file diff --git a/new/fuzzing-book-zh/13.md b/new/fuzzing-book-zh/13.md new file mode 100644 index 0000000000000000000000000000000000000000..b7cb9f6f34130c22861d1cadec431752dac7d756 --- /dev/null +++ b/new/fuzzing-book-zh/13.md @@ -0,0 +1,1918 @@ +# 变异分析 + +> 原文: [https://www.fuzzingbook.org/html/MutationAnalysis.html](https://www.fuzzingbook.org/html/MutationAnalysis.html) + +在有关的[一章中,我们展示了如何识别程序的哪些部分由程序执行,从而了解一组测试用例在覆盖程序结构方面的有效性。 但是,仅覆盖可能不是衡量测试有效性的最佳方法,因为无需检查结果是否正确,覆盖范围就可以很大。 在本章中,我们介绍了另一种评估测试套件有效性的方法:将*突变* – *人为错误* –注入代码后,我们检查测试套件是否可以检测到这些人为的 故障。 这个想法是,如果它无法检测到此类突变,那么它也会错过真正的错误。](Coverage.html) + +**前提条件** + +* 您需要对程序的执行方式有所了解。 +* 您应该已经阅读[关于覆盖](Coverage.html)的章节。 + +## 内容提要 + +要使用本章中提供的代码来[,请编写](Importing.html) + +```py +>>> from [fuzzingbook.MutationAnalysis](MutationAnalysis.html) import + +``` + +然后利用以下功能。 + +本章介绍在主题程序上运行*突变分析*的两种方法。 第一类`MuFunctionAnalyzer`针对各个功能。 给定一个函数`gcd`并评估两个测试用例,一个可以对测试用例进行突变分析,如下所示: + +```py +>>> for mutant in MuFunctionAnalyzer(gcd, log=True): +>>> with mutant: +>>> assert gcd(1, 0) == 1, "Minimal" +>>> assert gcd(0, 1) == 1, "Mirror" +>>> mutant.pm.score() +-> gcd_1 +<- gcd_1 + +-> gcd_2 +<- gcd_2 + +-> gcd_3 +<- gcd_3 + +-> gcd_4 +<- gcd_4 +Detected gcd_4 Minimal + +0.25 + +``` + +第二类`MuProgramAnalyzer`针对具有测试套件的独立程序。 给定一个程序`gcd`,其源代码在`gcd_src`中提供,而测试套件由`TestGCD`提供,则可以如下评估`TestGCD`的突变评分: + +```py +>>> class TestGCD(unittest.TestCase): +>>> def test_simple(self): +>>> assert cfg.gcd(1, 0) == 1 +>>> +>>> def test_mirror(self): +>>> assert cfg.gcd(0, 1) == 1 +>>> for mutant in MuProgramAnalyzer('gcd', gcd_src): +>>> mutant[test_module].runTest('TestGCD') +>>> mutant.pm.score() +====================================================================== +FAIL: test_simple (__main__.TestGCD) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "", line 3, in test_simple + assert cfg.gcd(1, 0) == 1 +AssertionError + +---------------------------------------------------------------------- +Ran 1 test in 0.001s + +FAILED (failures=1) +====================================================================== +FAIL: test_simple (__main__.TestGCD) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "", line 3, in test_simple + assert cfg.gcd(1, 0) == 1 +AssertionError + +---------------------------------------------------------------------- +Ran 1 test in 0.001s + +FAILED (failures=1) +====================================================================== +FAIL: test_simple (__main__.TestGCD) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "", line 3, in test_simple + assert cfg.gcd(1, 0) == 1 +AssertionError + +---------------------------------------------------------------------- +Ran 1 test in 0.001s + +FAILED (failures=1) +====================================================================== +FAIL: test_simple (__main__.TestGCD) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "", line 3, in test_simple + assert cfg.gcd(1, 0) == 1 +AssertionError + +---------------------------------------------------------------------- +Ran 1 test in 0.001s + +FAILED (failures=1) +====================================================================== +FAIL: test_simple (__main__.TestGCD) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "", line 3, in test_simple + assert cfg.gcd(1, 0) == 1 +AssertionError + +---------------------------------------------------------------------- +Ran 1 test in 0.001s + +FAILED (failures=1) +====================================================================== +FAIL: test_simple (__main__.TestGCD) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "", line 3, in test_simple + assert cfg.gcd(1, 0) == 1 +AssertionError + +---------------------------------------------------------------------- +Ran 1 test in 0.001s + +FAILED (failures=1) +====================================================================== +FAIL: test_simple (__main__.TestGCD) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "", line 3, in test_simple + assert cfg.gcd(1, 0) == 1 +AssertionError + +---------------------------------------------------------------------- +Ran 1 test in 0.001s + +FAILED (failures=1) + +1.0 + +``` + +这样获得的突变评分比纯覆盖范围更好地指示了给定测试套件的质量。 + +## 为什么结构覆盖不足 + +[结构覆盖率](Coverage.html)措施的问题之一是,它无法检查测试套件生成的程序执行是否实际上*是正确的*。 也就是说,产生测试套件未注意到的错误输出的执行与产生正确覆盖率的执行的计数完全相同。 的确,如果删除典型测试用例中的断言,则新测试套件的覆盖范围不会改变,但是新测试套件的功能远不如原始测试套件有用。 例如,考虑以下“测试”: + +```py +def ineffective_test(): + execute_the_program_as_a_whole() + assert True + +``` + +无论`execute_the_program_as_a_whole()`做什么,这里的最终断言总是会通过。 好的,如果`execute_the_program_as_a_whole()`引发异常,则测试将失败,但是我们也可以解决该问题: + +```py +def ineffective_test(): + try: + execute_the_program_as_a_whole() + except: + pass + assert True + +``` + +但是,这些“测试”的问题在于`execute_the_program_as_a_whole()`可能实现100%的代码覆盖率(或任何其他结构覆盖率度量标准的100%)。 但是,这个100%的数字并不反映测试发现错误的能力,实际上是0%。 + +实际上,这不是最佳状态。 我们如何验证我们的测试确实有用? 一种选择(在有关覆盖率的章节中提供了提示)是将错误注入程序,并评估测试套件在捕获这些注入的错误方面的有效性。 但是,这带来了另一个问题。 我们如何首先产生这些错误? 开发人员对错误可能发生的位置以及产生的影响的先入之见可能会使任何手动工作都产生偏差。 此外,编写非常好的bug可能会花费大量时间,这是非常间接的好处。 因此,这样的解决方案是不够的。 + +## 通过突变分析[播种人工故障](#Seeding-Artificial-Faults-with-Mutation-Analysis) + +变异分析提供了另一种解决方案,用于评估测试套件的有效性。 突变分析的想法是将*人为错误*(称为*突变*)植入程序代码,并检查测试套件是否找到了它们。 例如,这种突变可以用`execute_the_program_as_a_whole()`中某处的`-`代替`+`。 当然,上述无效的测试不会检测到这一点,因为它们不会检查任何结果。 然而,有效的测试将; 假设测试在发现*人为*错误中越有效,则在发现*实际*错误中越有效。 + +变异分析的见解是从程序员的角度考虑错误插入的可能性。 如果假设程序中每个程序元素所收到的注意力足够相似,则可以进一步假定程序中的每个令牌都具有相似的被错误转录的可能性。 当然,程序员将纠正编译器(或其他静态分析工具)检测到的所有错误。 因此,与经过编译阶段之后的原始令牌不同的有效令牌集被认为是它的*突变*的可能集,它们表示程序中的*可能的错误*。 然后,通过测试套件检测(从而防止)此类突变的能力来对其进行判断。 在产生的所有*有效*突变体中检测到的此类突变体的比例作为突变得分。 在这一章中,我们将了解如何在Python程序中实现变异分析。 所获得的变异分数代表任何程序分析工具预防错误的能力,并且可以用于判断静态测试套件,测试生成器(例如模糊器)以及静态和符号执行框架。 + +考虑一个稍微不同的观点可能很直观。 测试套件是可以视为接受测试的程序作为其输入的程序。 评估此类程序(测试套件)的最佳方法是什么? 通过对输入程序应用小的变异,并验证所涉及的测试套件不会产生意外行为,我们实质上可以*对*进行模糊测试。 测试套件应该只允许原始版本通过。 因此,任何未被检测为有缺陷的突变体都表示测试套件中的错误。 + +## 示例的结构覆盖范围充分性 + +让我们介绍一个更详细的示例,以说明覆盖问题以及突变分析的工作原理。 下面的`triangle()`程序将边长为$ a $,$ b $和$ c $的三角形分类为适当的三角形类别。 我们要验证程序是否正常运行。 + +```py +def triangle(a, b, c): + if a == b: + if b == c: + return 'Equilateral' + else: + return 'Isosceles' + else: + if b == c: + return "Isosceles" + else: + if a == c: + return "Isosceles" + else: + return "Scalene" + +``` + +这里有一些测试用例,以确保程序正常运行。 + +```py +def strong_oracle(fn): + assert fn(1, 1, 1) == 'Equilateral' + + assert fn(1, 2, 1) == 'Isosceles' + assert fn(2, 2, 1) == 'Isosceles' + assert fn(1, 2, 2) == 'Isosceles' + + assert fn(1, 2, 3) == 'Scalene' + +``` + +运行它们实际上会使所有测试通过。 + +```py +strong_oracle(triangle) + +``` + +但是,只有当我们知道我们的测试有效时,“所有测试都通过”的声明才有价值。 我们的测试套件的功效是什么? 正如我们在关于的[一章中所看到的,可以使用结构性覆盖技术(例如语句覆盖)来获得测试用例有效性的度量。](Coverage.html) + +```py +import [fuzzingbook_utils](https://github.com/uds-se/fuzzingbook/tree/master/notebooks/fuzzingbook_utils) + +``` + +```py +from [Coverage](Coverage.html) import Coverage + +``` + +```py +import [inspect](https://docs.python.org/3/library/inspect.html) + +``` + +我们添加了一个函数`show_coverage()`以可视化获得的覆盖范围。 + +```py +class Coverage(Coverage): + def show_coverage(self, fn): + src = inspect.getsource(fn) + name = fn.__name__ + covered = set([lineno for method, + lineno in self._trace if method == name]) + for i, s in enumerate(src.split('\n')): + print('%s %2d: %s' % ('#' if i + 1 in covered else ' ', i + 1, s)) + +``` + +```py +with Coverage() as cov: + strong_oracle(triangle) + +``` + +```py +cov.show_coverage(triangle) + +``` + +```py + 1: def triangle(a, b, c): +# 2: if a == b: +# 3: if b == c: +# 4: return 'Equilateral' + 5: else: +# 6: return 'Isosceles' + 7: else: +# 8: if b == c: +# 9: return "Isosceles" + 10: else: +# 11: if a == c: +# 12: return "Isosceles" + 13: else: +# 14: return "Scalene" + 15: + +``` + +我们的`strong_oracle()`似乎已充分涵盖了所有可能的条件。 也就是说,根据结构覆盖范围,我们的测试用例集相当不错。 但是,获得的报道能说明全部情况吗? 请考虑以下测试套件: + +```py +def weak_oracle(fn): + assert fn(1, 1, 1) == 'Equilateral' + + assert fn(1, 2, 1) != 'Equilateral' + assert fn(2, 2, 1) != 'Equilateral' + assert fn(1, 2, 2) != 'Equilateral' + + assert fn(1, 2, 3) != 'Equilateral' + +``` + +我们这里要检查的只是边不相等的三角形不是等边的。 获得了什么保障? + +```py +with Coverage() as cov: + weak_oracle(triangle) + +``` + +```py +cov.show_coverage(triangle) + +``` + +```py + 1: def triangle(a, b, c): +# 2: if a == b: +# 3: if b == c: +# 4: return 'Equilateral' + 5: else: +# 6: return 'Isosceles' + 7: else: +# 8: if b == c: +# 9: return "Isosceles" + 10: else: +# 11: if a == c: +# 12: return "Isosceles" + 13: else: +# 14: return "Scalene" + 15: + +``` + +实际上,覆盖范围似乎没有*任何*差异。 `weak_oracle()`获得的覆盖范围与`strong_oracle()`完全相同。 然而,片刻的反思应该使人相信`weak_oracle()`不如`strong_oracle()`有效。 但是,*的覆盖范围*无法区分这两个测试套件。 我们在覆盖范围上缺少什么? 这里的问题是覆盖范围无法评估我们断言的*质量*。 实际上,覆盖范围根本不关心断言。 但是,如上所述,断言是测试套件有效性的极其重要的部分。 因此,我们需要一种评估断言质量的方法。 + +## 注入人为故障 + +注意,在有关的[一章中,覆盖范围是*代理*的形式,表示测试套件可能会发现错误。 如果实际上试图评估测试套件发现错误的可能性怎么办? 我们所需要做的就是一次将一个错误注入程序,并计算测试套件检测到的此类错误的数量。 检测频率将为我们提供测试套件发现漏洞的实际可能性。 此技术称为*故障注入*。 这是*故障注入*的示例。](Coverage.html) + +```py +def triangle_m1(a, b, c): + if a == b: + if b == c: + return 'Equilateral' + else: + # return 'Isosceles' + return None # <-- injected fault + else: + if b == c: + return "Isosceles" + else: + if a == c: + return "Isosceles" + else: + return "Scalene" + +``` + +让我们看看我们的测试套件是否足以应付此故障。 我们首先检查`weak_oracle()`是否可以检测到此更改。 + +```py +from [ExpectError](ExpectError.html) import ExpectError + +``` + +```py +with ExpectError(): + weak_oracle(triangle_m1) + +``` + +`weak_oracle()`无法检测到任何更改。 那我们的`strong_oracle()`呢? + +```py +with ExpectError(): + strong_oracle(triangle_m1) + +``` + +```py +Traceback (most recent call last): + File "", line 2, in + strong_oracle(triangle_m1) + File "", line 5, in strong_oracle + assert fn(2, 2, 1) == 'Isosceles' +AssertionError (expected) + +``` + +我们的`strong_oracle()`能够检测到此故障,这证明`strong_oracle()`可能是更好的测试套件。 + +*故障注入*可以很好地衡量测试套件的有效性,前提是我们提供了可能的故障列表。 问题是收集这样的*无偏*故障集是相当昂贵的。 难以创建难以检测的良好故障,这是一个手动过程。 鉴于这是一个手动过程,因此所产生的错误将受到创建它的开发人员的先入为主的偏见。 即使存在此类经过策划的错误,它们也不大可能是详尽无遗的,并且可能会错过重要的错误类别以及程序的某些部分。 因此,*故障注入*不足以替代覆盖范围。 我们可以做得更好吗? + +变异分析提供了一组精选故障的替代方法。 关键的见解是,如果假设程序员理解所讨论的程序,则大多数错误很可能是小的转录错误(少量的令牌)。 编译器很可能会捕获其中的大多数错误。 因此,程序中的大多数残余故障可能是由于正确程序的程序结构中某些点上的小(单个令牌)变化(此特定假设称为*合格程序员假说*或*有限邻里假说*)。 由程序的多个更改组成的较大故障又如何呢? 此处的关键洞察力是,对于此类故障中的绝大部分,可以检测到隔离中的单个更改的测试用例很有可能检测到包含该故障的较大复合故障。 (此假设称为*耦合效应*。)我们如何在实践中使用这些假设? 这个想法是简单地生成程序的所有可能有效的*有效*变体,这些变体与原始版本之间的差别很小(例如单个令牌更改)(这种变体称为 *突变体*)。 接下来,将给定的测试套件应用于由此生成的每个变体。 据说测试套件检测到的任何突变体均已被测试套件杀死*。 测试套件的有效性由杀死的突变体与产生的有效突变体的比例确定。* + +接下来,我们实现一个简单的突变分析框架,并使用它来评估我们的测试套件。 + +## 突变Python代码 + +要处理Python程序,我们需要使用*抽象语法树*(AST)表示形式-这是内部表示形式,编译器和解释器在读取程序文本后便会进行处理。 + +简而言之,我们将程序转换为树,然后*更改此树的一部分*-例如,通过将`+`运算符更改为`-`或反之亦然,或将实际语句更改为`pass`语句 什么也没做。 然后可以进一步处理生成的变异树。 它可以传递给Python解释器以执行,或者我们可以*将其解析为文本形式。* + +我们首先导入AST操作模块。 + +```py +import [ast](https://docs.python.org/3/library/ast.html) +import [astor](https://docs.python.org/3/library/astor.html) +import [inspect](https://docs.python.org/3/library/inspect.html) + +``` + +我们可以使用`inspect.getsource()`获得Python函数的源代码。 (请注意,这不适用于其他笔记本中定义的功能。) + +```py +triangle_source = inspect.getsource(triangle) +triangle_source + +``` + +```py +'def triangle(a, b, c):\n if a == b:\n if b == c:\n return \'Equilateral\'\n else:\n return \'Isosceles\'\n else:\n if b == c:\n return "Isosceles"\n else:\n if a == c:\n return "Isosceles"\n else:\n return "Scalene"\n' + +``` + +为了以令人愉悦的形式查看这些内容,我们的函数`print_content(s, suffix)`格式化并突出显示了字符串`s`,就像它是一个以`suffix`结尾的文件一样。 因此,我们可以像查看Python文件一样查看(并突出显示)源: + +```py +from [fuzzingbook_utils](https://github.com/uds-se/fuzzingbook/tree/master/notebooks/fuzzingbook_utils) import print_content + +``` + +```py +print_content(triangle_source, '.py') + +``` + +```py +def triangle(a, b, c): + if a == b: + if b == c: + return 'Equilateral' + else: + return 'Isosceles' + else: + if b == c: + return "Isosceles" + else: + if a == c: + return "Isosceles" + else: + return "Scalene" + +``` + +对此进行解析可以为我们提供抽象语法树(AST)-以树形式表示程序。 + +```py +triangle_ast = ast.parse(triangle_source) + +``` + +这个AST是什么样的? 辅助功能`astor.dump_tree()`(文本输出)和`showast.show_ast()`(带有 [showast](https://github.com/hchasestevens/show_ast) 的图形输出)使我们可以检查树的结构。 我们看到函数以带有名称和参数的`FunctionDef`开始,后跟一个主体,该主体是`Expr`类型(文档字符串),`Assign`类型(赋值),`While`类型的语句列表 环回自己的身体),最后`Return`。 + +```py +print(astor.dump_tree(triangle_ast)) + +``` + +```py +Module( + body=[ + FunctionDef(name='triangle', + args=arguments( + args=[arg(arg='a', annotation=None), arg(arg='b', annotation=None), arg(arg='c', annotation=None)], + vararg=None, + kwonlyargs=[], + kw_defaults=[], + kwarg=None, + defaults=[]), + body=[ + If(test=Compare(left=Name(id='a'), ops=[Eq], comparators=[Name(id='b')]), + body=[ + If(test=Compare(left=Name(id='b'), ops=[Eq], comparators=[Name(id='c')]), + body=[Return(value=Str(s='Equilateral'))], + orelse=[Return(value=Str(s='Isosceles'))])], + orelse=[ + If(test=Compare(left=Name(id='b'), ops=[Eq], comparators=[Name(id='c')]), + body=[Return(value=Str(s='Isosceles'))], + orelse=[ + If(test=Compare(left=Name(id='a'), ops=[Eq], comparators=[Name(id='c')]), + body=[Return(value=Str(s='Isosceles'))], + orelse=[Return(value=Str(s='Scalene'))])])])], + decorator_list=[], + returns=None)]) + +``` + +文字太多了吗? 这种图形表示可以使事情变得更简单。 + +```py +from [fuzzingbook_utils](https://github.com/uds-se/fuzzingbook/tree/master/notebooks/fuzzingbook_utils) import rich_output + +``` + +```py +if rich_output(): + import [showast](https://docs.python.org/3/library/showast.html) + showast.show_ast(triangle_ast) + +``` + +%3 0 FunctionDef 1 "triangle" 0--1 2 arguments 0--2 9 If 0--9 3 arg 2--3 5 arg 2--5 7 arg 2--7 4 "a" 3--4 6 "b" 5--6 8 "c" 7--8 10 Compare 9--10 18 If 9--18 33 If 9--33 11 Name 10--11 14 Eq 10--14 15 Name 10--15 12 "a" 11--12 13 Load 11--13 16 "b" 15--16 17 Load 15--17 19 Compare 18--19 27 Return 18--27 30 Return 18--30 20 Name 19--20 23 Eq 19--23 24 Name 19--24 21 "b" 20--21 22 Load 20--22 25 "c" 24--25 26 Load 24--26 28 Str 27--28 29 "Equilateral" 28--29 31 Str 30--31 32 "Isosceles" 31--32 34 Compare 33--34 42 Return 33--42 45 If 33--45 35 Name 34--35 38 Eq 34--38 39 Name 34--39 36 "b" 35--36 37 Load 35--37 40 "c" 39--40 41 Load 39--41 43 Str 42--43 44 "Isosceles" 43--44 46 Compare 45--46 54 Return 45--54 57 Return 45--57 47 Name 46--47 50 Eq 46--50 51 Name 46--51 48 "a" 47--48 49 Load 47--49 52 "c" 51--52 53 Load 51--53 55 Str 54--55 56 "Isosceles" 55--56 58 Str 57--58 59 "Scalene" 58--59 + +函数`astor.to_source()`将这种树转换回更熟悉的文本Python代码表示形式。 + +```py +print_content(astor.to_source(triangle_ast), '.py') + +``` + +```py +def triangle(a, b, c): + if a == b: + if b == c: + return 'Equilateral' + else: + return 'Isosceles' + elif b == c: + return 'Isosceles' + elif a == c: + return 'Isosceles' + else: + return 'Scalene' + +``` + +## 一个简单的函数突变器 + +现在让我们去修改`triangle()`程序。 产生此程序的有效变异版本的一种简单方法是将其某些语句替换为`pass`。 + +`MuFunctionAnalyzer`是负责测试套件突变分析的主要类。 它接受要测试的功能。 使用上面讨论的功能,它将通过解析和解析一次给出的源代码标准化。 这是必需的,以确保原始和突变体之间的`diff`之后不会因空白注释等的差异而脱轨。 + +```py +class MuFunctionAnalyzer: + def __init__(self, fn, log=False): + self.fn = fn + self.name = fn.__name__ + src = inspect.getsource(fn) + self.ast = ast.parse(src) + self.src = astor.to_source(self.ast) # normalize + self.mutator = self.mutator_object() + self.nmutations = self.get_mutation_count() + self.un_detected = set() + self.mutants = [] + self.log = log + + def mutator_object(self, locations=None): + return StmtDeletionMutator(locations) + + def register(self, m): + self.mutants.append(m) + + def finish(self): + pass + +``` + +`get_mutation_count()`获取可用的可能突变数。 稍后我们将看到如何实现。 + +```py +class MuFunctionAnalyzer(MuFunctionAnalyzer): + def get_mutation_count(self): + self.mutator.visit(self.ast) + return self.mutator.count + +``` + +`Mutator`提供了实现单个突变的基类。 它接受要突变的位置列表。 假定方法`mutable_visit()`在子类确定的所有感兴趣的节点上被调用。 当调用`Mutator`而没有要突变的位置列表时,它仅循环遍历所有可能的突变点,并在`self.count`中保留计数。 如果使用要突变的特定位置列表调用它,则`mutable_visit()`方法将调用`mutation_visit()`,该节点将在节点上执行突变。 请注意,单个位置可以产生多个突变。 (因此,哈希图)。 + +```py +class Mutator(ast.NodeTransformer): + def __init__(self, mutate_location=-1): + self.count = 0 + self.mutate_location = mutate_location + + def mutable_visit(self, node): + self.count += 1 # statements start at line no 1 + if self.count == self.mutate_location: + return self.mutation_visit(node) + return self.generic_visit(node) + +``` + +`StmtDeletionMutator`只是挂接到所有处理语句的访问者中。 它通过将给定的语句替换为`pass`来执行变异。 如您所见,它访问各种语句。 + +```py +class StmtDeletionMutator(Mutator): + def visit_Return(self, node): return self.mutable_visit(node) + def visit_Delete(self, node): return self.mutable_visit(node) + + def visit_Assign(self, node): return self.mutable_visit(node) + def visit_AnnAssign(self, node): return self.mutable_visit(node) + def visit_AugAssign(self, node): return self.mutable_visit(node) + + def visit_Raise(self, node): return self.mutable_visit(node) + def visit_Assert(self, node): return self.mutable_visit(node) + + def visit_Global(self, node): return self.mutable_visit(node) + def visit_Nonlocal(self, node): return self.mutable_visit(node) + + def visit_Expr(self, node): return self.mutable_visit(node) + + def visit_Pass(self, node): return self.mutable_visit(node) + def visit_Break(self, node): return self.mutable_visit(node) + def visit_Continue(self, node): return self.mutable_visit(node) + +``` + +实际的突变包括用`pass`语句替换节点: + +```py +class StmtDeletionMutator(StmtDeletionMutator): + def mutation_visit(self, node): return ast.Pass() + +``` + +对于`triangle()`,此访问者产生五个突变-即用`pass`替换五个`return`语句: + +```py +MuFunctionAnalyzer(triangle).nmutations + +``` + +```py +5 + +``` + +我们需要一种获得单个突变体的方法。 为此,我们将`MuFunctionAnalyzer`转换为*可迭代的*。 + +```py +class MuFunctionAnalyzer(MuFunctionAnalyzer): + def __iter__(self): + return PMIterator(self) + +``` + +`PMIterator`定义为`MuFunctionAnalyzer`的*迭代器*类。 + +```py +class PMIterator: + def __init__(self, pm): + self.pm = pm + self.idx = 0 + +``` + +`next()`方法返回相应的`Mutant`: + +```py +class PMIterator(PMIterator): + def __next__(self): + i = self.idx + if i >= self.pm.nmutations: + self.pm.finish() + raise StopIteration() + self.idx += 1 + mutant = Mutant(self.pm, self.idx, log=self.pm.log) + self.pm.register(mutant) + return mutant + +``` + +`Mutant`类包含用于在给定要突变的位置时生成突变体的逻辑。 + +```py +class Mutant: + def __init__(self, pm, location, log=False): + self.pm = pm + self.i = location + self.name = "%s_%s" % (self.pm.name, self.i) + self._src = None + self.tests = [] + self.detected = False + self.log = log + +``` + +使用方法如下: + +```py +for m in MuFunctionAnalyzer(triangle): + print(m.name) + +``` + +```py +triangle_1 +triangle_2 +triangle_3 +triangle_4 +triangle_5 + +``` + +这些名称还有些通用。 让我们看看是否可以对产生的突变有更多的了解。 + +`generate_mutant()`只需调用`mutator()`方法,并向mutator传递AST的副本。 + +```py +class Mutant(Mutant): + def generate_mutant(self, location): + mutant_ast = self.pm.mutator_object( + location).visit(ast.parse(self.pm.src)) # copy + return astor.to_source(mutant_ast) + +``` + +`src()`方法返回变异的源。 + +```py +class Mutant(Mutant): + def src(self): + if self._src is None: + self._src = self.generate_mutant(self.i) + return self._src + +``` + +这是获取突变体并显示与原始突变的区别的方式: + +```py +import [difflib](https://docs.python.org/3/library/difflib.html) + +``` + +```py +for mutant in MuFunctionAnalyzer(triangle): + shape_src = mutant.pm.src + for line in difflib.unified_diff(mutant.pm.src.split('\n'), + mutant.src().split('\n'), + fromfile=mutant.pm.name, + tofile=mutant.name, n=3): + print(line) + +``` + +```py +--- triangle + ++++ triangle_1 + +@@ -1,7 +1,7 @@ + + def triangle(a, b, c): + if a == b: + if b == c: +- return 'Equilateral' ++ pass + else: + return 'Isosceles' + elif b == c: +--- triangle + ++++ triangle_2 + +@@ -3,7 +3,7 @@ + + if b == c: + return 'Equilateral' + else: +- return 'Isosceles' ++ pass + elif b == c: + return 'Isosceles' + elif a == c: +--- triangle + ++++ triangle_3 + +@@ -5,7 +5,7 @@ + + else: + return 'Isosceles' + elif b == c: +- return 'Isosceles' ++ pass + elif a == c: + return 'Isosceles' + else: +--- triangle + ++++ triangle_4 + +@@ -7,7 +7,7 @@ + + elif b == c: + return 'Isosceles' + elif a == c: +- return 'Isosceles' ++ pass + else: + return 'Scalene' + +--- triangle + ++++ triangle_5 + +@@ -9,5 +9,5 @@ + + elif a == c: + return 'Isosceles' + else: +- return 'Scalene' ++ pass + +``` + +在此`diff`输出中,添加以`+`前缀的行,而以`-`前缀的行被删除。 我们看到五个突变体的每一个确实用`pass`语句替换了return语句。 + +我们将`diff()`方法添加到`Mutant`,以便可以直接调用它。 + +```py +class Mutant(Mutant): + def diff(self): + return '\n'.join(difflib.unified_diff(self.pm.src.split('\n'), + self.src().split('\n'), + fromfile='original', + tofile='mutant', + n=3)) + +``` + +## 评估突变 + +我们现在准备实施实际评估。 我们将变量定义为*上下文管理器*,它可以验证给定的所有断言是否成功。 这个想法是我们可以编写如下代码 + +```py +for mutant in MuFunctionAnalyzer(function): + with mutant: + assert function(x) == y + +``` + +当`mutant`处于活动状态时(即`with:`下的代码块),原始功能被替换为替换功能。 + +输入`with`块时,将调用`__enter__()`功能。 它将突变体创建为Python函数并将其放置在全局名称空间中,以使`assert`语句执行变异函数而不是原始函数。 + +```py +class Mutant(Mutant): + def __enter__(self): + if self.log: + print('->\t%s' % self.name) + c = compile(self.src(), '', 'exec') + eval(c, globals()) + +``` + +`__exit__()`函数检查是否发生了异常(即断言失败或引发了其他错误); 如果是这样,则将其标记为`detected`。 最后,它将恢复原始函数定义。 + +```py +class Mutant(Mutant): + def __exit__(self, exc_type, exc_value, traceback): + if self.log: + print('<-\t%s' % self.name) + if exc_type is not None: + self.detected = True + if self.log: + print("Detected %s" % self.name, exc_type, exc_value) + globals()[self.pm.name] = self.pm.fn + if self.log: + print() + return True + +``` + +`finish()`方法仅在突变体上调用该方法,检查是否发现了该突变体,然后返回结果。 + +```py +from [ExpectError](ExpectError.html) import ExpectTimeout + +``` + +```py +class MuFunctionAnalyzer(MuFunctionAnalyzer): + def finish(self): + self.un_detected = { + mutant for mutant in self.mutants if not mutant.detected} + +``` + +突变分数-由测试套件检测到的突变体比率-由`score()`计算。 分数为1.0表示发现了所有突变体。 得分为0.1表示仅检测到10%的突变体。 + +```py +class MuFunctionAnalyzer(MuFunctionAnalyzer): + def score(self): + return (self.nmutations - len(self.un_detected)) / self.nmutations + +``` + +这是我们如何使用我们的框架。 + +```py +import [sys](https://docs.python.org/3/library/sys.html) + +``` + +```py +for mutant in MuFunctionAnalyzer(triangle, log=True): + with mutant: + assert triangle(1, 1, 1) == 'Equilateral', "Equal Check1" + assert triangle(1, 0, 1) != 'Equilateral', "Equal Check2" + assert triangle(1, 0, 2) != 'Equilateral', "Equal Check3" +mutant.pm.score() + +``` + +```py +-> triangle_1 +<- triangle_1 +Detected triangle_1 Equal Check1 + +-> triangle_2 +<- triangle_2 + +-> triangle_3 +<- triangle_3 + +-> triangle_4 +<- triangle_4 + +-> triangle_5 +<- triangle_5 + +``` + +```py +0.2 + +``` + +五分之二的突变中只有一个导致断言失败。 因此,`weak_oracle()`测试套件的突变得分为20%。 + +```py +for mutant in MuFunctionAnalyzer(triangle): + with mutant: + weak_oracle(triangle) +mutant.pm.score() + +``` + +```py +0.2 + +``` + +由于我们正在修改全局名称空间,因此我们不必直接在mutant的for循环内引用该函数。 + +```py +def oracle(): + strong_oracle(triangle) + +``` + +```py +for mutant in MuFunctionAnalyzer(triangle, log=True): + with mutant: + oracle() +mutant.pm.score() + +``` + +```py +-> triangle_1 +<- triangle_1 +Detected triangle_1 + +-> triangle_2 +<- triangle_2 +Detected triangle_2 + +-> triangle_3 +<- triangle_3 +Detected triangle_3 + +-> triangle_4 +<- triangle_4 +Detected triangle_4 + +-> triangle_5 +<- triangle_5 +Detected triangle_5 + +``` + +```py +1.0 + +``` + +也就是说,我们可以通过`strong_oracle()`测试套件获得`100%`突变评分。 + +这是另一个例子。 `gcd()`计算两个数字的最大公约数。 + +```py +def gcd(a, b): + if a < b: + c = a + a = b + b = c + + while b != 0: + c = a + a = b + b = c % b + return a + +``` + +这是一个测试。 效果如何? + +```py +for mutant in MuFunctionAnalyzer(gcd, log=True): + with mutant: + assert gcd(1, 0) == 1, "Minimal" + assert gcd(0, 1) == 1, "Mirror" +mutant.pm.score() + +``` + +```py +-> gcd_1 +<- gcd_1 +Detected gcd_1 local variable 'c' referenced before assignment + +-> gcd_2 +<- gcd_2 +Detected gcd_2 Mirror + +-> gcd_3 +<- gcd_3 + +-> gcd_4 +<- gcd_4 + +-> gcd_5 +<- gcd_5 + +-> gcd_6 +<- gcd_6 + +-> gcd_7 +<- gcd_7 +Detected gcd_7 Minimal + +``` + +```py +0.42857142857142855 + +``` + +我们看到,我们的`TestGCD`测试套件能够获得42%的突变评分。 + +## 模块和测试套件的变量 + +考虑我们前面讨论的`triangle()`程序。 正如我们所讨论的,产生此程序的有效变异版本的一种简单方法是将其某些语句替换为`pass`。 + +出于演示目的,我们希望程序看起来像在另一个文件中一样。 我们可以通过在Python中生成`Module`对象并将函数附加到该对象来实现。 + +```py +import [imp](https://docs.python.org/3/library/imp.html) + +``` + +```py +def import_code(code, name): + module = imp.new_module(name) + exec(code, module.__dict__) + return module + +``` + +我们将`triangle()`功能附加到`shape`模块。 + +```py +shape = import_code(shape_src, 'shape') + +``` + +现在我们可以通过模块`shape`调用三角形。 + +```py +shape.triangle(1, 1, 1) + +``` + +```py +'Equilateral' + +``` + +我们要测试`triangle()`功能。 为此,我们定义了一个`StrongShapeTest`类,如下所示。 + +```py +import [unittest](https://docs.python.org/3/library/unittest.html) + +``` + +```py +class StrongShapeTest(unittest.TestCase): + + def test_equilateral(self): + assert shape.triangle(1, 1, 1) == 'Equilateral' + + def test_isosceles(self): + assert shape.triangle(1, 2, 1) == 'Isosceles' + assert shape.triangle(2, 2, 1) == 'Isosceles' + assert shape.triangle(1, 2, 2) == 'Isosceles' + + def test_scalene(self): + assert shape.triangle(1, 2, 3) == 'Scalene' + +``` + +我们定义了一个帮助程序功能`suite()`,它可以查看给定的类并标识测试功能。 + +```py +def suite(test_class): + suite = unittest.TestSuite() + for f in test_class.__dict__: + if f.startswith('test_'): + suite.addTest(test_class(f)) + return suite + +``` + +可以使用不同的测试运行程序调用`TestTriangle`类中的测试。 最简单的方法是直接调用`TestCase`的`run()`方法。 + +```py +suite(StrongShapeTest).run(unittest.TestResult()) + +``` + +```py + + +``` + +`TextTestRunner`类提供了控制执行的详细程度的能力。 它还允许在第一个*故障时返回*。** + +```py +runner = unittest.TextTestRunner(verbosity=0, failfast=True) +runner.run(suite(StrongShapeTest)) + +``` + +```py +---------------------------------------------------------------------- +Ran 3 tests in 0.000s + +OK + +``` + +```py + + +``` + +在覆盖范围内运行该程序的操作如下: + +```py +with Coverage() as cov: + suite(StrongShapeTest).run(unittest.TestResult()) + +``` + +获得的覆盖范围如下: + +```py +cov.show_coverage(triangle) + +``` + +```py + 1: def triangle(a, b, c): +# 2: if a == b: +# 3: if b == c: +# 4: return 'Equilateral' + 5: else: +# 6: return 'Isosceles' +# 7: else: +# 8: if b == c: +# 9: return "Isosceles" +# 10: else: + 11: if a == c: +# 12: return "Isosceles" + 13: else: + 14: return "Scalene" + 15: + +``` + +```py +class WeakShapeTest(unittest.TestCase): + def test_equilateral(self): + assert shape.triangle(1, 1, 1) == 'Equilateral' + + def test_isosceles(self): + assert shape.triangle(1, 2, 1) != 'Equilateral' + assert shape.triangle(2, 2, 1) != 'Equilateral' + assert shape.triangle(1, 2, 2) != 'Equilateral' + + def test_scalene(self): + assert shape.triangle(1, 2, 3) != 'Equilateral' + +``` + +它获得多少覆盖率? + +```py +with Coverage() as cov: + suite(WeakShapeTest).run(unittest.TestResult()) + +``` + +```py +cov.show_coverage(triangle) + +``` + +```py + 1: def triangle(a, b, c): +# 2: if a == b: +# 3: if b == c: +# 4: return 'Equilateral' + 5: else: +# 6: return 'Isosceles' +# 7: else: +# 8: if b == c: +# 9: return "Isosceles" +# 10: else: + 11: if a == c: +# 12: return "Isosceles" + 13: else: + 14: return "Scalene" + 15: + +``` + +`MuProgramAnalyzer`是负责测试套件突变分析的主要类。 它接受要测试的模块的名称及其源代码。 它通过解析和解析一次来标准化源代码。 这是必需的,以确保原始和突变体之间的`diff`之后不会因空白注释等的差异而脱轨。 + +```py +class MuProgramAnalyzer(MuFunctionAnalyzer): + def __init__(self, name, src): + self.name = name + self.ast = ast.parse(src) + self.src = astor.to_source(self.ast) + self.changes = [] + self.mutator = self.mutator_object() + self.nmutations = self.get_mutation_count() + self.un_detected = set() + + def mutator_object(self, locations=None): + return AdvStmtDeletionMutator(self, locations) + +``` + +The `Mutator` provides the base class for implementing individual mutations. It accepts a list of locations to mutate. It assumes that the method `mutable_visit()` is invoked on all nodes of interest as determined by the subclass. When the `Mutator` is invoked without a list of locations to mutate, it simply loops through all possible mutation points and retains a count in `self.count`. If it is invoked with a specific list of locations to mutate, the `mutable_visit()` method calls the `mutation_visit()` which performs the mutation on the node. Note that a single location can produce multiple mutations. (Hence the hashmap). + +```py +class AdvMutator(Mutator): + def __init__(self, analyzer, mutate_locations=None): + self.count = 0 + self.mutate_locations = [] if mutate_locations is None else mutate_locations + self.pm = analyzer + + def mutable_visit(self, node): + self.count += 1 # statements start at line no 1 + return self.mutation_visit(node) + +``` + +`AdvStmtDeletionMutator`只是挂接到所有处理语句的访问者中。 它通过将给定的语句替换为`pass`来执行变异。 + +```py +class AdvStmtDeletionMutator(AdvMutator, StmtDeletionMutator): + def __init__(self, analyzer, mutate_locations=None): + AdvMutator.__init__(self, analyzer, mutate_locations) + + def mutation_visit(self, node): + index = 0 # there is only one way to delete a statement -- replace it by pass + if not self.mutate_locations: # counting pass + self.pm.changes.append((self.count, index)) + return self.generic_visit(node) + else: + # get matching changes for this pass + mutating_lines = set((count, idx) + for (count, idx) in self.mutate_locations) + if (self.count, index) in mutating_lines: + return ast.Pass() + else: + return self.generic_visit(node) + +``` + +Aagin,我们可以如下获得`triangle()`产生的突变数。 + +```py +MuProgramAnalyzer('shape', shape_src).nmutations + +``` + +```py +5 + +``` + +我们需要一种获得单个突变体的方法。 为此,我们将`MuProgramAnalyzer`转换为*可迭代的*。 + +```py +class MuProgramAnalyzer(MuProgramAnalyzer): + def __iter__(self): + return AdvPMIterator(self) + +``` + +`AdvPMIterator`定义为`MuProgramAnalyzer`的*迭代器*类。 + +```py +class AdvPMIterator: + def __init__(self, pm): + self.pm = pm + self.idx = 0 + +``` + +`next()`方法返回相应的`Mutant` + +```py +class AdvPMIterator(AdvPMIterator): + def __next__(self): + i = self.idx + if i >= len(self.pm.changes): + raise StopIteration() + self.idx += 1 + # there could be multiple changes in one mutant + return AdvMutant(self.pm, [self.pm.changes[i]]) + +``` + +The `Mutant` class contains logic for generating mutants when given the locations to mutate. + +```py +class AdvMutant(Mutant): + def __init__(self, pm, locations): + self.pm = pm + self.i = locations + self.name = "%s_%s" % (self.pm.name, + '_'.join([str(i) for i in self.i])) + self._src = None + +``` + +Here is how it can be used: + +```py +shape_src = inspect.getsource(triangle) + +``` + +```py +for m in MuProgramAnalyzer('shape', shape_src): + print(m.name) + +``` + +```py +shape_(1, 0) +shape_(2, 0) +shape_(3, 0) +shape_(4, 0) +shape_(5, 0) + +``` + +The `generate_mutant()` simply calls the `mutator()` method, and passes the mutator a copy of the AST. + +```py +class AdvMutant(AdvMutant): + def generate_mutant(self, locations): + mutant_ast = self.pm.mutator_object( + locations).visit(ast.parse(self.pm.src)) # copy + return astor.to_source(mutant_ast) + +``` + +The `src()` method returns the mutated source. + +```py +class AdvMutant(AdvMutant): + def src(self): + if self._src is None: + self._src = self.generate_mutant(self.i) + return self._src + +``` + +同样,我们将突变体可视化为与原始突变体的区别: + +```py +import [difflib](https://docs.python.org/3/library/difflib.html) + +``` + +We add the `diff()` method to `Mutant` so that it can be called directly. + +```py +class AdvMutant(AdvMutant): + def diff(self): + return '\n'.join(difflib.unified_diff(self.pm.src.split('\n'), + self.src().split('\n'), + fromfile='original', + tofile='mutant', + n=3)) + +``` + +```py +for mutant in MuProgramAnalyzer('shape', shape_src): + print(mutant.name) + print(mutant.diff()) + break + +``` + +```py +shape_(1, 0) +--- original + ++++ mutant + +@@ -1,7 +1,7 @@ + + def triangle(a, b, c): + if a == b: + if b == c: +- return 'Equilateral' ++ pass + else: + return 'Isosceles' + elif b == c: + +``` + +我们现在准备实施实际评估。 为此,我们需要能够接受定义了测试套件的模块,并在其上调用测试方法。 方法`getitem`接受测试模块,将测试模块上的导入条目固定为正确指向变异模块,然后将其传递给测试运行器`MutantTestRunner`。 + +```py +class AdvMutant(AdvMutant): + def __getitem__(self, test_module): + test_module.__dict__[ + self.pm.name] = import_code( + self.src(), self.pm.name) + return MutantTestRunner(self, test_module) + +``` + +`MutantTestRunner`只需调用测试模块上的所有`test_`方法,检查是否发现了突变体,然后返回结果。 + +```py +from [ExpectError](ExpectError.html) import ExpectTimeout + +``` + +```py +class MutantTestRunner: + def __init__(self, mutant, test_module): + self.mutant = mutant + self.tm = test_module + + def runTest(self, tc): + suite = unittest.TestSuite() + test_class = self.tm.__dict__[tc] + for f in test_class.__dict__: + if f.startswith('test_'): + suite.addTest(test_class(f)) + runner = unittest.TextTestRunner(verbosity=0, failfast=True) + try: + with ExpectTimeout(1): + res = runner.run(suite) + if res.wasSuccessful(): + self.mutant.pm.un_detected.add(self) + return res + except SyntaxError: + print('Syntax Error (%s)' % self.mutant.name) + return None + raise Exception('Unhandled exception during test execution') + +``` + +突变得分通过`score()`计算。 + +```py +class MuProgramAnalyzer(MuProgramAnalyzer): + def score(self): + return (self.nmutations - len(self.un_detected)) / self.nmutations + +``` + +Here is how we use our framework. + +```py +import [sys](https://docs.python.org/3/library/sys.html) + +``` + +```py +test_module = sys.modules[__name__] +for mutant in MuProgramAnalyzer('shape', shape_src): + mutant[test_module].runTest('WeakShapeTest') +mutant.pm.score() + +``` + +```py +====================================================================== +FAIL: test_equilateral (__main__.WeakShapeTest) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "", line 3, in test_equilateral + assert shape.triangle(1, 1, 1) == 'Equilateral' +AssertionError + +---------------------------------------------------------------------- +Ran 1 test in 0.001s + +FAILED (failures=1) +---------------------------------------------------------------------- +Ran 3 tests in 0.001s + +OK +---------------------------------------------------------------------- +Ran 3 tests in 0.001s + +OK +---------------------------------------------------------------------- +Ran 3 tests in 0.001s + +OK +---------------------------------------------------------------------- +Ran 3 tests in 0.001s + +OK + +``` + +```py +0.2 + +``` + +`WeakShape`测试套件仅导致`20%`突变评分。 + +```py +for mutant in MuProgramAnalyzer('shape', shape_src): + mutant[test_module].runTest('StrongShapeTest') +mutant.pm.score() + +``` + +```py +====================================================================== +FAIL: test_equilateral (__main__.StrongShapeTest) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "", line 4, in test_equilateral + assert shape.triangle(1, 1, 1) == 'Equilateral' +AssertionError + +---------------------------------------------------------------------- +Ran 1 test in 0.001s + +FAILED (failures=1) +====================================================================== +FAIL: test_isosceles (__main__.StrongShapeTest) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "", line 8, in test_isosceles + assert shape.triangle(2, 2, 1) == 'Isosceles' +AssertionError + +---------------------------------------------------------------------- +Ran 2 tests in 0.001s + +FAILED (failures=1) +====================================================================== +FAIL: test_isosceles (__main__.StrongShapeTest) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "", line 9, in test_isosceles + assert shape.triangle(1, 2, 2) == 'Isosceles' +AssertionError + +---------------------------------------------------------------------- +Ran 2 tests in 0.001s + +FAILED (failures=1) +====================================================================== +FAIL: test_isosceles (__main__.StrongShapeTest) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "", line 7, in test_isosceles + assert shape.triangle(1, 2, 1) == 'Isosceles' +AssertionError + +---------------------------------------------------------------------- +Ran 2 tests in 0.001s + +FAILED (failures=1) +====================================================================== +FAIL: test_scalene (__main__.StrongShapeTest) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "", line 12, in test_scalene + assert shape.triangle(1, 2, 3) == 'Scalene' +AssertionError + +---------------------------------------------------------------------- +Ran 3 tests in 0.001s + +FAILED (failures=1) + +``` + +```py +1.0 + +``` + +另一方面,我们可以通过`StrongShapeTest`测试套件获得`100%`突变评分。 + +这是另一个示例`gcd()`。 + +```py +gcd_src = inspect.getsource(gcd) + +``` + +```py +class TestGCD(unittest.TestCase): + def test_simple(self): + assert cfg.gcd(1, 0) == 1 + + def test_mirror(self): + assert cfg.gcd(0, 1) == 1 + +``` + +```py +for mutant in MuProgramAnalyzer('cfg', gcd_src): + mutant[test_module].runTest('TestGCD') +mutant.pm.score() + +``` + +```py +====================================================================== +ERROR: test_mirror (__main__.TestGCD) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "", line 6, in test_mirror + assert cfg.gcd(0, 1) == 1 + File "", line 5, in gcd +UnboundLocalError: local variable 'c' referenced before assignment + +---------------------------------------------------------------------- +Ran 2 tests in 0.001s + +FAILED (errors=1) +====================================================================== +FAIL: test_mirror (__main__.TestGCD) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "", line 6, in test_mirror + assert cfg.gcd(0, 1) == 1 +AssertionError + +---------------------------------------------------------------------- +Ran 2 tests in 0.001s + +FAILED (failures=1) +---------------------------------------------------------------------- +Ran 2 tests in 0.001s + +OK +---------------------------------------------------------------------- +Ran 2 tests in 0.001s + +OK +---------------------------------------------------------------------- +Ran 2 tests in 0.001s + +OK +---------------------------------------------------------------------- +Ran 2 tests in 0.001s + +OK +====================================================================== +FAIL: test_simple (__main__.TestGCD) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "", line 3, in test_simple + assert cfg.gcd(1, 0) == 1 +AssertionError + +---------------------------------------------------------------------- +Ran 1 test in 0.001s + +FAILED (failures=1) + +``` + +```py +0.42857142857142855 + +``` + +我们看到我们的`TestGCD`测试套件能够获得`42%`突变得分。 + +## 等价突变体[的问题](#The-Problem-of--Equivalent-Mutants) + +突变分析的问题之一是,并非所有产生的突变体都必须是有缺陷的。 例如,考虑下面的`new_gcd()`程序。 + +```py +def new_gcd(a, b): + if a < b: + a, b = b, a + else: + a, b = a, b + + while b != 0: + a, b = b, a % b + return a + +``` + +可以对该程序进行突变以产生以下突变体。 + +```py +def gcd(a, b): + if a < b: + a, b = b, a + else: + pass + + while b != 0: + a, b = b, a % b + return a + +``` + +```py +for i, mutant in enumerate(MuFunctionAnalyzer(new_gcd)): + print(i,mutant.src()) + +``` + +```py +0 def new_gcd(a, b): + if a < b: + pass + else: + a, b = a, b + while b != 0: + a, b = b, a % b + return a + +1 def new_gcd(a, b): + if a < b: + a, b = b, a + else: + pass + while b != 0: + a, b = b, a % b + return a + +2 def new_gcd(a, b): + if a < b: + a, b = b, a + else: + a, b = a, b + while b != 0: + pass + return a + +3 def new_gcd(a, b): + if a < b: + a, b = b, a + else: + a, b = a, b + while b != 0: + a, b = b, a % b + pass + +``` + +尽管其他突变体与原始变异体相比有缺陷,但是`mutant 1`在语义上与原始变异没有区别,因为它删除了无关紧要的分配。 这表示`mutant 1`不代表故障。 这些不代表故障的突变体称为*等效突变体*。 等效突变体的问题在于,在存在等效突变体的情况下,很难判断突变得分。 例如,突变分数为70%时,0%至30%的突变体可能是等效的。 因此,在不知道等效突变体的实际数目的情况下,不可能判断测试可以提高多少。 我们讨论了两种处理等效突变体的方法。 + +### 等价突变体数量的统计估计 + +如果活着的突变体的数量足够少,则可以依靠简单地手动检查它们。 但是,如果突变体的数量足够大(例如> 1000),则可以从存活的突变体中随机选择较少数量的突变体,然后手动评估它们是否代表缺陷。 样本大小的确定由以下公式控制,该公式用于二项式分布(近似于正态分布): + +$$ n \ ge \ hat {p}(1- \ hat {p})\ bigg(\ frac {Z _ {\ frac {\ alpha} {2}}} {\ Delta} \ bigg)^ 2 $$ + +其中,$ n $是样本数,$ p $是概率分布的参数,$ \ alpha $是所需的精度,$ \ Delta $是精度。 对于$ 95 \%$的准确性,$ Z_ {0.95} = 1.96 $。 我们有以下值($ \ hat {p}(1- \ hat {p})的最大值= 0.25 $),而$ Z $是正态分布的临界值: + +$$ n \ ge 0.25 \ bigg(\ frac {1.96} {\ Delta} \ bigg)^ 2 $$ + +对于$ Delta = 0.01 $,(最大误差为1%),我们需要评估$ 9604 $突变体的等效性。 如果将约束放宽到$ \ Delta = 0.1 $(这是$ 10 \%$的误差),则只需要评估$ 96 $突变体的等效性即可。 + +### 统计量估计由不朽的估计 + +尽管仅采样有限数量的突变体的想法很吸引人,但它仍然受到限制,因为必须进行手动分析。 如果计算能力很便宜,另一种估算真实突变体数量(以及等效突变体数量)的方法是使用Chao的估算器。 正如我们将在[一章中看到的何时停止模糊](WhenToStopFuzzing.html)的公式一样,公式为: + +$$ \ hat S_ \ text {Chao1} = \ begin {cases} S(n)+ \ frac {f_1 ^ 2} {2f_2} & \ text {if $ f_2 > 0 $} \\ S( n)+ \ frac {f_1(f_1-1)} {2} & \ text {否则} \ end {cases} $$ + +基本思想是针对每个突变体计算每个测试的完整测试矩阵$ T \ M $的结果。 变量$ f_1 $代表被精确杀死一次的突变体的数量,变量$ f_2 $代表被精确杀死两次的变量的数量。 $ S(n)$是被杀死的突变体总数。 在这里,$ \ hat {S} _ {Chao1} $提供了对突变体真实数量的估计。 如果$ M $是生成的总突变体,则$ M-\ hat {S} _ {Chao1} $表示**不朽**突变体的数量。 请注意,这些**不朽**突变体与传统的等效突变体有些不同,因为**死亡率**取决于用于区分变体行为的预言。 也就是说,如果使用依赖于抛出的错误来检测杀戮的模糊器,它将不会检测到产生不同输出但不会抛出错误的突变体。 因此, *Chao1* 估计值将实质上是模糊器可以检测到的突变体的渐近线值,如果该突变体被赋予了无限的时间。 当使用的预言家足够强大时,**不朽的**突变体估算值将接近真实的**等效**突变体估算值。 有关更多详细信息,请参见[何时停止模糊](WhenToStopFuzzing.html)的章节。 + +## 经验教训 + +* 我们已经了解了为什么结构覆盖范围不足以评估测试套件的质量。 +* 我们已经了解了如何使用变异分析来评估测试套件的质量。 +* 我们已经了解了突变分析的局限性-等效和冗余突变体,以及如何估算它们。 + +## 后续步骤 + +* 虽然幼稚的模糊测试会产生质量较差的预言,但诸如[符号](SymbolicFuzzer.html)和[缩略语](ConcolicFuzzer.html)之类的技术可以增强模糊测试中使用的预言质量。 +* [动态不变量](DynamicInvariants.html)对提高Oracle的质量也有很大的帮助。 +* [何时停止模糊](WhenToStopFuzzing.html)的章节详细介绍了Chao估算器。 + +## 背景 + +突变分析的想法最早由Lipton等人提出。 [Lipton *等人*,1971。]。 贾等人发表了一份出色的突变分析研究综述。 [Jia *等*,2011。]。 Papadakis等人[Papadakis *等人*,2019年。有关突变分析的章节是对突变分析当前趋势的又一出色概述。 + +## 练习 + +### 练习1:算术表达变量 + +我们简单的语句删除突变只是可以对程序进行突变的一种方法。 另一类突变体是*表达突变*,其中算术运算符(例如`{+,-,*,/}`等)彼此替换。 例如,给定一个表达式,例如 + +```py +x = x + 1 +``` + +可以将其变异为 + +```py +x = x - 1 +``` + +和 + +```py +x = x * 1 +``` + +and + +```py +x = x / 1 +``` + +首先,我们需要找出要突变的节点类型。 我们通过ast函数获得这些,并发现节点类型名为BinOp + +```py +print(astor.dump_tree(ast.parse("1 + 2 - 3 * 4 / 5"))) + +``` + +```py +Module( + body=[ + Expr( + value=BinOp(left=BinOp(left=Num(n=1), op=Add, right=Num(n=2)), + op=Sub, + right=BinOp(left=BinOp(left=Num(n=3), op=Mult, right=Num(n=4)), op=Div, right=Num(n=5))))]) + +``` + +要使树变异,因此您需要更改`op`属性(其值为`Add`,`Sub`,`Mult`和`Div`之一) + +[Use the notebook](https://mybinder.org/v2/gh/uds-se/fuzzingbook/master?filepath=docs/notebooks/MutationAnalysis.ipynb#Exercises) to work on the exercises and see solutions. + +要使树变异,我们需要更改`op`属性(其值为`Add`,`Sub`,`Mult`和`Div`之一)。 编写一个进行必要突变的类`BinOpMutator`,然后创建一个类`MuBinOpAnalyzer`作为`MuFunctionAnalyzer`的子类,该类使用`BinOpMutator`。 + +[Use the notebook](https://mybinder.org/v2/gh/uds-se/fuzzingbook/master?filepath=docs/notebooks/MutationAnalysis.ipynb#Exercises) to work on the exercises and see solutions. + +### 练习2:优化突变分析 + +我们的突变分析技术效率低下,因为我们即使在代码中包含测试案例未涵盖的突变的突变体上也可以运行测试。 测试用例无法检测未涵盖的部分代码中的错误。 因此,最简单的优化方法之一是首先从给定的测试用例中恢复覆盖率信息,然后仅在突变位于测试用例所覆盖的代码中的突变体上运行测试用例。 您可以修改`MuFunctionAnalyzer`以纳入恢复覆盖范围作为第一步吗? + +[Use the notebook](https://mybinder.org/v2/gh/uds-se/fuzzingbook/master?filepath=docs/notebooks/MutationAnalysis.ipynb#Exercises) to work on the exercises and see solutions. + +### 练习3:字节码突变器 + +我们已经看到了在给定源的情况下如何突变AST。 这种方法的缺点之一是Python字节码也被其他语言作为目标。 在这种情况下,源可能不容易转换为Python AST,因此希望更改字节码。 您是否可以为Python函数实现字节码更改器,该字节器更改字节码而不是获取源代码然后对其进行更改? + +[Use the notebook](https://mybinder.org/v2/gh/uds-se/fuzzingbook/master?filepath=docs/notebooks/MutationAnalysis.ipynb#Exercises) to work on the exercises and see solutions. + +### 练习4:估计残余缺陷密度 + +程序的缺陷密度是程序中发布之前检测到的缺陷数除以程序大小。 残余缺陷密度是逃避检测的缺陷百分比。 尽管很难估计实际的残余缺陷密度,但突变分析可以提供一个上限。 仍未检测到的突变体数量是程序中剩余缺陷数量的合理上限。 但是,此上限可能太宽。 原因是一些剩余的故障可以相互影响,并且如果一起出现,则可以由可用的测试套件检测到。 因此,更严格的界限是在给定程序中可以被*并存*却没有被给定测试套件检测到的突变体的数量。 这可以通过从可能的完整突变集开始,并应用[的delta-debugging来完成,该章在减少](Reducer.html)的章节中确定了需要删除的最小数目的突变,以使突变体无法被检测到。 测试套件。 您可以通过扩展使用此技术来估计残余缺陷密度上限的`MuFunctionAnalyzer`来产生新的`RDDEstimator`吗? + +[Use the notebook](https://mybinder.org/v2/gh/uds-se/fuzzingbook/master?filepath=docs/notebooks/MutationAnalysis.ipynb#Exercises) to work on the exercises and see solutions. \ No newline at end of file diff --git a/new/fuzzing-book-zh/14.md b/new/fuzzing-book-zh/14.md new file mode 100644 index 0000000000000000000000000000000000000000..56f04a002e1476f34518589014c4bdd754a9772d --- /dev/null +++ b/new/fuzzing-book-zh/14.md @@ -0,0 +1,21 @@ +# 第三部分:语法模糊 + +> 原文: [https://www.fuzzingbook.org/html/03_Syntactical_Fuzzing.html](https://www.fuzzingbook.org/html/03_Syntactical_Fuzzing.html) + +本部分介绍了*语法*级别的测试生成,即,构成语言结构的输入。 + +* [语法](Grammars.html)为程序提供合法输入的*规范*。 通过语法指定输入可以非常系统和有效地生成测试,特别是对于复杂的输入格式。 + +* [高效的语法模糊处理](GrammarFuzzer.html)引入了基于树的语法模糊处理算法,该算法速度更快,并且可以更好地控制模糊输入的产生。 + +* [语法覆盖率](GrammarCoverageFuzzer.html)可以系统地覆盖语法元素,以便我们最大限度地提高多样性,而不会漏掉单个元素。 + +* [解析输入](Parser.html)显示了如何使用语法将给定的有效种子输入集解析和分解为相应的派生树。 + +* [概率语法模糊](ProbabilisticGrammarFuzzer.html)通过将*概率*分配给各个扩展来赋予语法更多的功能。 + +* [生成器模糊处理](GeneratorGrammarFuzzer.html)显示如何使用*函数*扩展语法-在语法扩展过程中执行的代码片段,可以生成,检查或更改生成的元素。 + +* [Greybox语法模糊](GreyboxGrammarFuzzer.html)利用结构表示法允许我们对它们的各个部分进行变异,交叉和重组,以生成新的有效的,稍有变化的输入。 + +* [减少故障导致的输入](Reducer.html)提出了以下技术:*自动将故障引起的输入减少并简化到最小*,以便于调试。 \ No newline at end of file diff --git a/new/fuzzing-book-zh/15.md b/new/fuzzing-book-zh/15.md new file mode 100644 index 0000000000000000000000000000000000000000..4203eecca4938058cb4695c7e7b9099e809ed0ca --- /dev/null +++ b/new/fuzzing-book-zh/15.md @@ -0,0 +1,2099 @@ +# 用文法模糊处理 + +> 原文: [https://www.fuzzingbook.org/html/Grammars.html](https://www.fuzzingbook.org/html/Grammars.html) + +在[“基于突变的模糊检测”](MutationFuzzer.html) 一章中,我们了解了如何使用额外的提示(例如样本输入文件)来加快测试生成速度。 在本章中,我们通过为程序提供合法输入的*规范*来使这一想法更进一步。 通过*语法*指定输入可以进行非常系统和有效的测试生成,尤其是对于复杂的输入格式。 语法还充当配置模糊测试,API模糊测试,GUI模糊测试等的基础。 + +**前提条件** + +* 您应该知道基本的模糊测试如何工作,例如 [一章中介绍了模糊](Fuzzer.html)。 +* 基于[突变的模糊](MutationFuzzer.html)和[覆盖率](Coverage.html)的知识尚不需要*,但仍推荐使用。* + +```py +import [fuzzingbook_utils](https://github.com/uds-se/fuzzingbook/tree/master/notebooks/fuzzingbook_utils) + +``` + +```py +import [Fuzzer](Fuzzer.html) + +``` + +## 内容提要 + +要使用本章中提供的代码来[,请编写](Importing.html) + +```py +>>> from [fuzzingbook.Grammars](Grammars.html) import + +``` + +然后利用以下功能。 + +本章介绍*语法*,作为指定输入语言并将其用于测试具有语法有效输入的程序的简单方法。 语法定义为非终结符到替代扩展列表的映射,如以下示例所示: + +```py +>>> US_PHONE_GRAMMAR = { +>>> "": [""], +>>> "": ["()-"], +>>> "": [""], +>>> "": [""], +>>> "": [""], +>>> "": ["2", "3", "4", "5", "6", "7", "8", "9"], +>>> "": ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"] +>>> } +>>> +>>> assert is_valid_grammar(US_PHONE_GRAMMAR) + +``` + +非终端符号括在尖括号中(例如``)。 为了从语法生成输入字符串,*生产者*以起始符号(``)开头,并为该符号随机选择一个随机扩展。 它将继续该过程,直到所有非终结符都展开为止。 函数`simple_grammar_fuzzer()`可以做到: + +```py +>>> [simple_grammar_fuzzer(US_PHONE_GRAMMAR) for i in range(5)] +['(692)449-5179', + '(519)230-7422', + '(613)761-0853', + '(979)881-3858', + '(810)914-5475'] + +``` + +但是,实际上,您应该使用[类,](GrammarFuzzer.html)类或基于[覆盖率的](GrammarCoverageFuzzer.html),基于[概率性的](ProbabilisticGrammarFuzzer.html)而不是 [基于生成器的](GeneratorGrammarFuzzer.html)衍生物; 这些功能更加有效,可防止无限增长,并提供其他一些功能。 + +本章还介绍了[语法工具箱](#A-Grammar-Toolbox),该工具箱具有多个辅助功能,可简化语法的编写,例如对字符类和重复使用快捷方式符号或扩展语法 + +## 输入语言 + +程序的所有可能的行为都可以通过其输入来触发。 这里的“输入”可以有很多种可能的来源:我们正在谈论从文件,环境或网络中读取的数据,用户输入的数据或通过与其他资源交互而获取的数据。 所有这些输入的集合决定了程序的行为方式-包括其失败。 因此,在测试时,考虑可能的输入源,如何控制它们以及*如何系统地测试它们*非常有帮助。 + +为了简单起见,我们现在假设该程序只有一个输入源; 这也是我们在前几章中一直使用的假设。 程序的有效输入集称为*语言*。 语言的范围从简单到复杂:CSV语言表示有效的逗号分隔输入的集合,而Python语言表示有效的Python程序的集合。 我们通常将数据语言和编程语言分开,尽管任何程序也可以视为输入数据(例如,对于编译器)。 文件格式的 [Wikipedia页面列出了1,000多种不同的文件格式,每种格式都是其自己的语言。](https://en.wikipedia.org/wiki/List_of_file_formats) + +为了正式描述语言,*正式语言*领域已经设计了许多描述语言的*语言规范*。 *正则表达式*表示这些语言中表示字符串集的最简单的类:例如,正则表达式`[a-z]*`表示小写字母(可能为空)的序列。 *自动机理论*将这些语言与接受这些输入的自动机联系起来; 例如,*有限状态机*可用于指定正则表达式的语言。 + +正则表达式非常适合不太复杂的输入格式,并且关联的有限状态机具有许多使它们适合推理的属性。 但是,要指定更复杂的输入,它们很快就会遇到限制。 在语言频谱的另一端,我们有*通用语法*,它们表示 *Turing机器*接受的语言。 图灵机可以计算任何可以计算的东西。 并且由于Python是图灵完备的,这意味着我们还可以使用Python程序$ p $来指定甚至枚举合法输入。 但是,然后,计算机科学理论也告诉我们,每个这样的测试程序都必须专门针对要测试的程序编写,这不是我们想要的自动化水平。 + +## 语法 + +正则表达式和图灵机之间的中间立场由*语法*覆盖。 语法是用于正式指定输入语言的最流行(和最佳理解)形式主义之一。 使用一种语法,可以表达一种输入语言的多种属性。 语法对于表示输入的*语法结构*特别有用,并且是表示嵌套或递归输入的形式化选择。 我们使用的语法是所谓的*上下文无关文法*,这是最简单和最受欢迎的语法形式主义之一。 + +### 规则和扩展 + +语法由*起始符号*和一组*扩展规则*(或简称为*规则*)组成,这些规则指示如何扩展起始符号(和其他符号) 。 例如,请考虑以下语法,表示两个数字的序列: + +```py + ::= + ::= 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 +``` + +要阅读此类语法,请从开始符号(``)开始。 扩展规则` ::= `表示左侧的符号(``)可以由右侧的字符串(``)代替。 在以上语法中,``将替换为``。 + +再次在此字符串中,``将替换为``规则右侧的字符串。 特殊运算符`|`表示*扩展替代项*(或简称为*替代项*),意味着可以为扩展选择任何数字。 因此,每个``都将扩展为给定的数字之一,最终在`00`和`99`之间产生一个字符串。 `0`到`9`没有进一步的扩展,因此我们都准备就绪。 + +关于语法的有趣之处在于它们可以是*递归*。 也就是说,扩展可以利用之前扩展的符号-然后再将其扩展。 例如,考虑描述整数的语法: + +```py + ::= + ::= | + ::= 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 +``` + +此处,``可以是一个数字,也可以是后面跟另一个整数的数字。 因此,数字`1234`将被表示为一个数字`1`,后跟一个整数`234`,后者又是一个数字`2`,然后是整数`34`。 + +如果我们想表达一个整数可以以一个符号(`+`或`-`)开头,我们可以将语法写为 + +```py + ::= + ::= | + | - + ::= | + ::= 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 +``` + +这些规则正式定义了语言:可以从起始符号派生的任何内容都是该语言的一部分; 没有的一切都不是。 + +### 算术表达式 + +让我们扩展语法以涵盖完整的*算术表达式*-语法的后代子示例。 我们看到表达式(``)是和,差,或项。 术语是产品或部门或因素; 因素是数字或带括号的表达式。 几乎所有规则都可以递归,因此可以使用任意复杂的表达式,例如`(1 + 2) * (3.4 / 5.6 - 789)`。 + +```py + ::= + ::= + | - | + ::= * | / | + ::= + | - | () | | . + ::= | + ::= 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 +``` + +在这样的语法中,如果我们以``开头,然后将一个符号扩展为另一个,随机选择替代符号,则可以快速地生成一个有效的算术表达式。 这种*语法模糊*可以产生复杂的输入,因此非常有效,这就是我们将在本章中实现的功能。 + +## 在Python 中表示语法 + +构建语法模糊器的第一步是为语法找到合适的格式。 为了使语法的编写尽可能简单,我们使用基于字符串和列表的格式。 我们在Python中的语法采用符号名称和扩展名之间的*映射*的格式,其中扩展名是*列表的*。 因此,数字的单规则语法采用以下形式: + +```py +DIGIT_GRAMMAR = { + "": + ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"] +} + +``` + +而算术表达式的完整语法如下所示: + +```py +EXPR_GRAMMAR = { + "": + [""], + + "": + [" + ", " - ", ""], + + "": + [" * ", " / ", ""], + + "": + ["+", + "-", + "()", + ".", + ""], + + "": + ["", ""], + + "": + ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"] +} + +``` + +在语法中,每个符号可以定义一次。 我们可以通过其符号访问任何规则... + +```py +EXPR_GRAMMAR[""] + +``` + +```py +['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'] + +``` + +....我们可以检查语法中是否包含符号: + +```py +"" in EXPR_GRAMMAR + +``` + +```py +False + +``` + +请注意,我们假设在规则的左侧(即映射中的键)始终是单个符号。 这是使我们的语法具有*上下文无关*的特征的属性。 + +## 一些定义 + +我们假设规范的开始符号是``: + +```py +START_SYMBOL = "" + +``` + +方便的`nonterminals()`函数从扩展中提取非终结符列表(即`<`和`>`之间的任何字符,空格除外)。 + +```py +import [re](https://docs.python.org/3/library/re.html) + +``` + +```py +RE_NONTERMINAL = re.compile(r'(<[^<> ]*>)') + +``` + +```py +def nonterminals(expansion): + # In later chapters, we allow expansions to be tuples, + # with the expansion being the first element + if isinstance(expansion, tuple): + expansion = expansion[0] + + return re.findall(RE_NONTERMINAL, expansion) + +``` + +```py +assert nonterminals(" * ") == ["", ""] +assert nonterminals("") == ["", ""] +assert nonterminals("1 < 3 > 2") == [] +assert nonterminals("1 <3> 2") == ["<3>"] +assert nonterminals("1 + 2") == [] +assert nonterminals(("<1>", {'option': 'value'})) == ["<1>"] + +``` + +同样,`is_nonterminal()`检查某个符号是否为非终结符: + +```py +def is_nonterminal(s): + return re.match(RE_NONTERMINAL, s) + +``` + +```py +assert is_nonterminal("") +assert is_nonterminal("") +assert not is_nonterminal("+") + +``` + +## 一个简单的语法模糊器 + +现在让我们使用以上语法。 我们将构建一个非常简单的语法模糊器,以起始符号(``)开头,然后继续对其进行扩展。 为了避免扩展为无限输入,我们对非终端数设置了限制(`max_nonterminals`)。 此外,为了避免陷入无法再减少符号数的情况,我们还限制了扩展步骤的总数。 + +```py +import [random](https://docs.python.org/3/library/random.html) + +``` + +```py +class ExpansionError(Exception): + pass + +``` + +```py +def simple_grammar_fuzzer(grammar, start_symbol=START_SYMBOL, + max_nonterminals=10, max_expansion_trials=100, + log=False): + term = start_symbol + expansion_trials = 0 + + while len(nonterminals(term)) > 0: + symbol_to_expand = random.choice(nonterminals(term)) + expansions = grammar[symbol_to_expand] + expansion = random.choice(expansions) + new_term = term.replace(symbol_to_expand, expansion, 1) + + if len(nonterminals(new_term)) < max_nonterminals: + term = new_term + if log: + print("%-40s" % (symbol_to_expand + " -> " + expansion), term) + expansion_trials = 0 + else: + expansion_trials += 1 + if expansion_trials >= max_expansion_trials: + raise ExpansionError("Cannot expand " + repr(term)) + + return term + +``` + +让我们看看这个简单的语法模糊器如何从起始符号中获得算术表达式: + +```py +simple_grammar_fuzzer(grammar=EXPR_GRAMMAR, max_nonterminals=3, log=True) + +``` + +```py + -> + -> + + + -> + + -> + + -> + + -> 6 6 + + -> - 6 + - + -> 6 + - + -> 6 + - + -> - 6 + - - + -> 6 + - - + -> () 6 + -() - + -> () 6 + -() - () + -> 6 + -() - () + -> 6 + -() - () + -> 6 + -() - () + -> + 6 + -(+) - () + -> + 6 + -(++) - () + -> 6 + -(++) - () + -> () 6 + -(++()) - () + -> 6 + -(++()) - () + -> 6 + -(++()) - () + -> 6 + -(++()) - () + -> 9 6 + -(++()) - (9) + -> * 6 + -(++( * )) - (9) + -> 6 + -(++( * )) - (9) + -> 6 + -(++( * )) - (9) + -> 6 + -(++( * )) - (9) + -> 2 6 + -(++(2 * )) - (9) + -> + 6 + -(++(2 * +)) - (9) + -> - 6 + -(++(2 * +-)) - (9) + -> - 6 + -(++(2 * +--)) - (9) + -> - 6 + -(++(2 * +---)) - (9) + -> - 6 + -(++(2 * +----)) - (9) + -> . 6 + -(++(2 * +----.)) - (9) + -> 6 + -(++(2 * +----.)) - (9) + -> 6 + -(++(2 * +----.)) - (9) + -> 1 6 + -(++(2 * +----1.)) - (9) + -> 7 6 + -(++(2 * +----1.7)) - (9) + +``` + +```py +'6 + -(++(2 * +----1.7)) - (9)' + +``` + +通过增加非终端的数量,我们可以快速获得更长的产量: + +```py +for i in range(10): + print(simple_grammar_fuzzer(grammar=EXPR_GRAMMAR, max_nonterminals=5)) + +``` + +```py +7 / +48.5 +-5.9 / 9 - 4 * +-(-+++((1 + (+7 - (-1 * (++-+7.7 - -+-4.0))))) * +--4 - -(6) + 64) +8.2 - 27 - -9 / +((+9 * --2 + --+-+-((-1 * +(8 - 5 - 6)) * (-((-+(((+(4))))) - ++4) / +(-+---((5.6 - --(3 * -1.8 * +(6 * +-(((-(-6) * ---+6)) / +--(+-+-7 * (-0 * (+(((((2)) + 8 - 3 - ++9.0 + ---(--+7 / (1 / +++6.37) + (1) / 482) / +++-+0)))) * -+5 + 7.513)))) - (+1 / ++((-84)))))))) * ++5 / +-(--2 - -++-9.0)))) / 5 * --++090 +1 - -3 * 7 - 28 / 9 +(+9) * +-5 * ++-926.2 - (+9.03 / -+(-(-6) / 2 * +(-+--(8) / -(+1.0) - 5 + 4)) * 3.5) +8 + -(9.6 - 3 - -+-4 * +77) +-(((((++((((+((++++-((+-37))))))))))))) / ++(-(+++(+6)) * -++-(+(++(---6 * (((7)) * (1) / (-7.6 * 535338) + +256) * 0) * 0))) - 4 + +1 +5.43 +(9 / -405 / -23 - +-((+-(2 * (13))))) + +6 - +8 - 934 +-++2 - (--+715769550) / 8 / (1) + +``` + +注意,由于大量的搜索和替换操作,此模糊器效率很低。 另一方面,实现很简单,并且在大多数情况下都能胜任。 在本章中,我们将坚持下去; 在[下一章](GrammarFuzzer.html)中,我们将展示如何构建更有效的代码。 + +## 将语法可视化为铁路图 + +使用语法,我们可以轻松指定前面讨论的几个示例的格式。 例如,上述算术表达式可以直接发送到`bc`(或任何采用算术表达式的程序)中。 在介绍一些其他语法之前,让我们给出一种方法,使*可视化*,并提供另一种视图以帮助他们理解。 + +*铁路图*,也称为*语法图*,是无上下文语法的图形表示。 按照可能的“轨道”轨迹从左到右读取它们; 轨道上遇到的符号顺序定义了语言。 + +我们使用 [RailroadDiagrams](RailroadDiagrams.html) (一个用于可视化的外部库)。 + +```py +from [RailroadDiagrams](RailroadDiagrams.html) import NonTerminal, Terminal, Choice, HorizontalChoice, Sequence, Diagram, show_diagram + +``` + +```py +from [IPython.display](https://ipython.readthedocs.io/en/stable/api/generated/IPython.display.html) import SVG, display + +``` + +我们首先定义方法`syntax_diagram_symbol()`以可视化给定的符号。 终端符号表示为椭圆形,而非终端符号(例如``)表示为矩形。 + +```py +def syntax_diagram_symbol(symbol): + if is_nonterminal(symbol): + return NonTerminal(symbol[1:-1]) + else: + return Terminal(symbol) + +``` + +```py +SVG(show_diagram(syntax_diagram_symbol(''))) + +``` + + term + +我们定义`syntax_diagram_expr()`以可视化扩展替代方案。 + +```py +def syntax_diagram_expr(expansion): + # In later chapters, we allow expansions to be tuples, + # with the expansion being the first element + if isinstance(expansion, tuple): + expansion = expansion[0] + + symbols = [sym for sym in re.split(RE_NONTERMINAL, expansion) if sym != ""] + if len(symbols) == 0: + symbols = [""] # special case: empty expansion + + return Sequence(*[syntax_diagram_symbol(sym) for sym in symbols]) + +``` + +```py +SVG(show_diagram(syntax_diagram_expr(EXPR_GRAMMAR[''][0]))) + +``` + + factor * term + +这是``的第一种选择– ``,然后是`*`和``。 + +接下来,我们定义`syntax_diagram_alt()`以显示替代表达式。 + +```py +from [itertools](https://docs.python.org/3/library/itertools.html) import zip_longest + +``` + +```py +def syntax_diagram_alt(alt): + max_len = 5 + alt_len = len(alt) + if alt_len > max_len: + iter_len = alt_len // max_len + alts = list(zip_longest(*[alt[i::iter_len] for i in range(iter_len)])) + exprs = [[syntax_diagram_expr(expr) for expr in alt + if expr is not None] for alt in alts] + choices = [Choice(len(expr) // 2, *expr) for expr in exprs] + return HorizontalChoice(*choices) + else: + return Choice(alt_len // 2, *[syntax_diagram_expr(expr) for expr in alt]) + +``` + +```py +SVG(show_diagram(syntax_diagram_alt(EXPR_GRAMMAR['']))) + +``` + + 0 1 2 3 4 5 6 7 8 9 + +我们看到``可以是`0`到`9`的任何一位数字。 + +最后,我们定义`syntax_diagram()`,它给出了一个语法,显示了其规则的语法图。 + +```py +def syntax_diagram(grammar): + from [IPython.display](https://ipython.readthedocs.io/en/stable/api/generated/IPython.display.html) import SVG, display + + for key in grammar: + print("%s" % key[1:-1]) + display(SVG(show_diagram(syntax_diagram_alt(grammar[key])))) + +``` + +```py +syntax_diagram(EXPR_GRAMMAR) + +``` + +```py +start + +``` + + expr + +```py +expr + +``` + + term + expr term - expr term + +```py +term + +``` + + factor * term factor / term factor + +```py +factor + +``` + + - factor + factor ( expr ) integer . integer integer + +```py +integer + +``` + + digit integer digit + +```py +digit + +``` + + 0 1 2 3 4 5 6 7 8 9 + +这种铁路表示形式将在可视化语法结构时派上用场-特别是对于更复杂的语法。 + +## 一些文法 + +让我们创建(并可视化)更多语法并将其用于模糊测试。 + +### CGI语法 + +这是[关于覆盖](Coverage.html)的一章中介绍的`cgi_decode()`的语法。 + +```py +CGI_GRAMMAR = { + "": + [""], + + "": + ["", ""], + + "": + ["", "", ""], + + "": + ["+"], + + "": + ["%"], + + "": + ["0", "1", "2", "3", "4", "5", "6", "7", + "8", "9", "a", "b", "c", "d", "e", "f"], + + "": # Actually, could be _all_ letters + ["0", "1", "2", "3", "4", "5", "a", "b", "c", "d", "e", "-", "_"], +} + +``` + +```py +syntax_diagram(CGI_GRAMMAR) + +``` + +```py +start + +``` + + string + +```py +string + +``` + + letter letter string + +```py +letter + +``` + + plus percent other + +```py +plus + +``` + + + + +```py +percent + +``` + + % hexdigit hexdigit + +```py +hexdigit + +``` + + 0 1 2 3 4 5 6 7 8 9 a b c d e f + +```py +other + +``` + + 0 1 2 3 4 5 a b c d e - _ + +与[基本模糊处理](Fuzzer.html)或基于[突变的模糊处理](MutationFuzzer.html)相比,语法可以快速产生各种组合: + +```py +for i in range(10): + print(simple_grammar_fuzzer(grammar=CGI_GRAMMAR, max_nonterminals=10)) + +``` + +```py ++%9a ++++%ce+ ++_ ++%c6c +++ ++%cd+5 +1%ee +%b9%d5 +%96 +%57d%42 + +``` + +### URL语法 + +我们为CGI输入看到的相同属性也适用于更复杂的输入。 让我们使用一种语法来产生大量有效的URL: + +```py +URL_GRAMMAR = { + "": + [""], + "": + ["://"], + "": + ["http", "https", "ftp", "ftps"], + "": + ["", ":", "@", "@:"], + "": # Just a few + ["cispa.saarland", "www.google.com", "fuzzingbook.com"], + "": + ["80", "8080", ""], + "": + ["", ""], + "": + ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"], + "": # Just one + ["user:password"], + "": # Just a few + ["", "/", "/"], + "": # Just a few + ["abc", "def", "x"], + "": + ["", "?"], + "": + ["", "&"], + "": # Just a few + ["=", "="], +} + +``` + +```py +syntax_diagram(URL_GRAMMAR) + +``` + +```py +start + +``` + + url + +```py +url + +``` + + scheme :// authority path query + +```py +scheme + +``` + + https http ftp ftps + +```py +authority + +``` + + host : port host userinfo @ host userinfo @ host : port + +```py +host + +``` + + cispa.saarland www.google.com fuzzingbook.com + +```py +port + +``` + + 80 8080 nat + +```py +nat + +``` + + digit digit digit + +```py +digit + +``` + + 0 1 2 3 4 5 6 7 8 9 + +```py +userinfo + +``` + + user:password + +```py +path + +``` + + / / id + +```py +id + +``` + + abc def x digit digit + +```py +query + +``` + + ? params + +```py +params + +``` + + param param & params + +```py +param + +``` + + id = id id = nat + +同样,在几毫秒内,我们可以产生大量有效的输入。 + +```py +for i in range(10): + print(simple_grammar_fuzzer(grammar=URL_GRAMMAR, max_nonterminals=10)) + +``` + +```py +https://user:password@cispa.saarland:80/ +http://fuzzingbook.com?def=56&x89=3&x46=48&def=def +ftp://cispa.saarland/?x71=5&x35=90&def=abc +https://cispa.saarland:80/def?def=7&x23=abc +https://fuzzingbook.com:80/ +https://fuzzingbook.com:80/abc?def=abc&abc=x14&def=abc&abc=2&def=38 +ftps://fuzzingbook.com/x87 +https://user:password@fuzzingbook.com:6?def=54&x44=abc +http://fuzzingbook.com:80?x33=25&def=8 +http://fuzzingbook.com:8080/def + +``` + +### 自然语言语法 + +最后,语法不仅限于诸如计算机输入之类的*形式语言*,而且还可以用于产生*自然语言*。 这是我们用来选择本书标题的语法: + +```py +TITLE_GRAMMAR = { + "": [""], + "<title>": ["<topic>: <subtopic>"], + "<topic>": ["Generating Software Tests", "<fuzzing-prefix>Fuzzing", "The Fuzzing Book"], + "<fuzzing-prefix>": ["", "The Art of ", "The Joy of "], + "<subtopic>": ["<subtopic-main>", + "<subtopic-prefix><subtopic-main>", + "<subtopic-main><subtopic-suffix>"], + "<subtopic-main>": ["Breaking Software", + "Generating Software Tests", + "Principles, Techniques and Tools"], + "<subtopic-prefix>": ["", "Tools and Techniques for "], + "<subtopic-suffix>": [" for <reader-property> and <reader-property>", + " for <software-property> and <software-property>"], + "<reader-property>": ["Fun", "Profit"], + "<software-property>": ["Robustness", "Reliability", "Security"], +} + +``` + +```py +syntax_diagram(TITLE_GRAMMAR) + +``` + +```py +start + +``` + + <svg class="railroad-diagram" height="62" viewBox="0 0 182.5 62" width="182.5" xmlns="http://www.w3.org/2000/svg"><g transform="translate(.5 .5)"><g><g><g class="non-terminal"><text x="91.25" y="35">title</text></g></g></g></g></svg> + +```py +title + +``` + + <svg class="railroad-diagram" height="62" viewBox="0 0 347.5 62" width="347.5" xmlns="http://www.w3.org/2000/svg"><g transform="translate(.5 .5)"><g><g><g class="non-terminal"><text x="91.25" y="35">topic</text></g> <g class="terminal"><text x="161.0" y="35">:</text></g> <g class="non-terminal"><text x="243.5" y="35">subtopic</text></g></g></g></g></svg> + +```py +topic + +``` + + <svg class="railroad-diagram" height="122" viewBox="0 0 358.5 122" width="358.5" xmlns="http://www.w3.org/2000/svg"><g transform="translate(.5 .5)"><g><g><g class="terminal"><text x="179.25" y="35">Generating Software Tests</text></g></g> <g><g class="non-terminal"><text x="129.5" y="65">fuzzing-prefix</text></g> <g class="terminal"><text x="258.75" y="65">Fuzzing</text></g></g> <g><g class="terminal"><text x="179.25" y="95">The Fuzzing Book</text></g></g></g></g></svg> + +```py +fuzzing-prefix + +``` + + <svg class="railroad-diagram" height="122" viewBox="0 0 233.5 122" width="233.5" xmlns="http://www.w3.org/2000/svg"><g transform="translate(.5 .5)"><g><g><g class="terminal"><text x="116.75" y="65">The Art of</text></g></g> <g><g class="terminal"><text x="116.75" y="95">The Joy of</text></g></g></g></g></svg> + +```py +subtopic + +``` + + <svg class="railroad-diagram" height="122" viewBox="0 0 418.0 122" width="418.0" xmlns="http://www.w3.org/2000/svg"><g transform="translate(.5 .5)"><g><g><g class="non-terminal"><text x="209.0" y="35">subtopic-main</text></g></g> <g><g class="non-terminal"><text x="133.75" y="65">subtopic-prefix</text></g> <g class="non-terminal"><text x="292.75" y="65">subtopic-main</text></g></g> <g><g class="non-terminal"><text x="125.25" y="95">subtopic-main</text></g> <g class="non-terminal"><text x="284.25" y="95">subtopic-suffix</text></g></g></g></g></svg> + +```py +subtopic-main + +``` + + <svg class="railroad-diagram" height="122" viewBox="0 0 412.0 122" width="412.0" xmlns="http://www.w3.org/2000/svg"><g transform="translate(.5 .5)"><g><g><g class="terminal"><text x="206.0" y="35">Breaking Software</text></g></g> <g><g class="terminal"><text x="206.0" y="65">Generating Software Tests</text></g></g> <g><g class="terminal"><text x="206.0" y="95">Principles, Techniques and Tools</text></g></g></g></g></svg> + +```py +subtopic-prefix + +``` + + <svg class="railroad-diagram" height="92" viewBox="0 0 352.5 92" width="352.5" xmlns="http://www.w3.org/2000/svg"><g transform="translate(.5 .5)"><g><g><g class="terminal"><text x="176.25" y="65">Tools and Techniques for</text></g></g></g></g></svg> + +```py +subtopic-suffix + +``` + + <svg class="railroad-diagram" height="92" viewBox="0 0 634.0 92" width="634.0" xmlns="http://www.w3.org/2000/svg"><g transform="translate(.5 .5)"><g><g><g class="terminal"><text x="108.25" y="35">for</text></g> <g class="non-terminal"><text x="233.25" y="35">reader-property</text></g> <g class="terminal"><text x="358.25" y="35">and</text></g> <g class="non-terminal"><text x="483.25" y="35">reader-property</text></g></g> <g><g class="terminal"><text x="91.25" y="65">for</text></g> <g class="non-terminal"><text x="224.75" y="65">software-property</text></g> <g class="terminal"><text x="358.25" y="65">and</text></g> <g class="non-terminal"><text x="491.75" y="65">software-property</text></g></g></g></g></svg> + +```py +reader-property + +``` + + <svg class="railroad-diagram" height="92" viewBox="0 0 191.0 92" width="191.0" xmlns="http://www.w3.org/2000/svg"><g transform="translate(.5 .5)"><g><g><g class="terminal"><text x="95.5" y="35">Fun</text></g></g> <g><g class="terminal"><text x="95.5" y="65">Profit</text></g></g></g></g></svg> + +```py +software-property + +``` + + <svg class="railroad-diagram" height="122" viewBox="0 0 233.5 122" width="233.5" xmlns="http://www.w3.org/2000/svg"><g transform="translate(.5 .5)"><g><g><g class="terminal"><text x="116.75" y="35">Robustness</text></g></g> <g><g class="terminal"><text x="116.75" y="65">Reliability</text></g></g> <g><g class="terminal"><text x="116.75" y="95">Security</text></g></g></g></g></svg> + +```py +titles = set() +while len(titles) < 10: + titles.add(simple_grammar_fuzzer( + grammar=TITLE_GRAMMAR, max_nonterminals=10)) +titles + +``` + +```py +{'Fuzzing: Generating Software Tests', + 'Fuzzing: Principles, Techniques and Tools', + 'Generating Software Tests: Breaking Software', + 'Generating Software Tests: Breaking Software for Robustness and Robustness', + 'Generating Software Tests: Principles, Techniques and Tools', + 'Generating Software Tests: Principles, Techniques and Tools for Profit and Fun', + 'Generating Software Tests: Tools and Techniques for Principles, Techniques and Tools', + 'The Fuzzing Book: Breaking Software', + 'The Fuzzing Book: Generating Software Tests for Profit and Profit', + 'The Fuzzing Book: Generating Software Tests for Robustness and Robustness'} + +``` + +(如果您发现此处存在冗余(“鲁棒性和鲁棒性”):在[我们有关基于覆盖率的模糊测试](GrammarCoverageFuzzer.html)的章节中,我们将展示如何仅覆盖每个扩展一次。并且,如果您喜欢某些替代方法 [概率语法模糊](ProbabilisticGrammarFuzzer.html)比其他要多。 + +## 语法作为突变种子 + +语法的一个非常有用的属性是它们产生大多数有效的输入。 从句法的观点来看,输入实际上*始终是*有效,因为它们满足给定语法的约束。 (当然,首先需要一个有效的语法。)但是,还有*语义*属性,这些属性无法轻松地用语法表达。 例如,如果对于URL,端口范围应该在1024到2048之间,则很难用语法来编写。 如果必须满足更复杂的约束条件,则可以很快达到语法可以表达的极限。 + +解决此问题的一种方法是对语法附加约束,因为我们将在本书的后面讨论[。 另一种可能性是将基于语法的模糊测试和基于](ConstraintFuzzer.html)[突变的模糊测试](MutationFuzzer.html)的优势结合在一起。 想法是将语法生成的输入用作*种子*,以进行进一步的基于突变的模糊测试。 这样,我们不仅可以探索*有效*输入,还可以检查有效输入和无效输入之间的*边界*。 这一点特别有趣,因为稍微无效的输入允许查找解析器错误(通常很丰富)。 就像一般的模糊测试一样,意外的情况揭示了程序中的错误。 + +要将生成的输入用作种子,我们可以将它们直接输入到前面介绍的突变模糊处理程序中: + +```py +from [MutationFuzzer](MutationFuzzer.html) import MutationFuzzer # minor dependency + +``` + +```py +number_of_seeds = 10 +seeds = [ + simple_grammar_fuzzer( + grammar=URL_GRAMMAR, + max_nonterminals=10) for i in range(number_of_seeds)] +seeds + +``` + +```py +['ftps://user:password@www.google.com:80', + 'http://cispa.saarland/', + 'ftp://www.google.com:42/', + 'ftps://user:password@fuzzingbook.com:39?abc=abc', + 'https://www.google.com?x33=1&x06=1', + 'http://www.google.com:02/', + 'https://user:password@www.google.com/', + 'ftp://cispa.saarland:8080/?abc=abc&def=def&abc=5', + 'http://www.google.com:80/def?def=abc', + 'http://user:password@cispa.saarland/'] + +``` + +```py +m = MutationFuzzer(seeds) + +``` + +```py +[m.fuzz() for i in range(20)] + +``` + +```py +['ftps://user:password@www.google.com:80', + 'http://cispa.saarland/', + 'ftp://www.google.com:42/', + 'ftps://user:password@fuzzingbook.com:39?abc=abc', + 'https://www.google.com?x33=1&x06=1', + 'http://www.google.com:02/', + 'https://user:password@www.google.com/', + 'ftp://cispa.saarland:8080/?abc=abc&def=def&abc=5', + 'http://www.google.com:80/def?def=abc', + 'http://user:password@cispa.saarland/', + 'Eh4tp:www.coogle.com:80/def?d%f=abc', + 'ftps://}ser:passwod@fuzzingbook.com:9?abc=abc', + 'uftp//cispa.sRaarland:808&0?abc=abc&def=defabc=5', + 'http://user:paswor9d@cispar.saarland/v', + 'ftp://Www.g\x7fogle.cAom:42/', + 'hht://userC:qassMword@cispy.csaarland/', + 'httx://ww.googlecom:80defde`f=ac', + 'htt://cispq.waarlnd/', + 'htFtp\t://cmspa./saarna(md/', + 'ft:/www.google.com:42\x0f'] + +``` + +前10个`fuzz()`调用返回种子输入(按设计),而后一个再次创建任意突变。 使用`MutationCoverageFuzzer`而不是`MutationFuzzer`,我们可以再次根据覆盖率进行搜索-从而将多个世界的最佳点融合在一起。 + +## 语法工具箱 + +现在让我们介绍一些有助于我们编写语法的技术。 + +### 转义 + +通过`<`和`>`在语法中定界非终结符,我们如何实际表达某些输入应包含`<`和`>`? 答案很简单:只需为它们引入一个符号即可。 + +```py +simple_nonterminal_grammar = { + "<start>": ["<nonterminal>"], + "<nonterminal>": ["<left-angle><identifier><right-angle>"], + "<left-angle>": ["<"], + "<right-angle>": [">"], + "<identifier>": ["id"] # for now +} + +``` + +在`simple_nonterminal_grammar`中,`<left-angle>`的扩展名和`<right-angle>`的扩展名都不能误认为是非终结符。 因此,我们可以生产任意数量的东西。 + +### 扩展语法 + +在本书的过程中,我们经常遇到通过*扩展*具有新功能的现有语法来创建语法的问题。 这样的扩展非常类似于面向对象编程中的子类化。 + +要从现有语法$ g $创建新语法$ g'$,我们首先将$ g $复制到$ g'$,然后使用新的替代方法扩展现有规则和/或添加新的符号。 这是一个示例,使用更好的标识符规则扩展了上述`nonterminal`语法: + +```py +import [copy](https://docs.python.org/3/library/copy.html) + +``` + +```py +nonterminal_grammar = copy.deepcopy(simple_nonterminal_grammar) +nonterminal_grammar["<identifier>"] = ["<idchar>", "<identifier><idchar>"] +nonterminal_grammar["<idchar>"] = ['a', 'b', 'c', 'd'] # for now + +``` + +```py +nonterminal_grammar + +``` + +```py +{'<start>': ['<nonterminal>'], + '<nonterminal>': ['<left-angle><identifier><right-angle>'], + '<left-angle>': ['<'], + '<right-angle>': ['>'], + '<identifier>': ['<idchar>', '<identifier><idchar>'], + '<idchar>': ['a', 'b', 'c', 'd']} + +``` + +由于语法的这种扩展是常见的操作,因此我们引入了自定义函数`extend_grammar()`,该函数首先复制给定的语法,然后使用Python字典`update()`方法从字典中对其进行更新: + +```py +def extend_grammar(grammar, extension={}): + new_grammar = copy.deepcopy(grammar) + new_grammar.update(extension) + return new_grammar + +``` + +对`extend_grammar()`的此调用将`simple_nonterminal_grammar`扩展到`nonterminal_grammar`,就像上面的“手动”示例一样: + +```py +nonterminal_grammar = extend_grammar(simple_nonterminal_grammar, + { + "<identifier>": ["<idchar>", "<identifier><idchar>"], + # for now + "<idchar>": ['a', 'b', 'c', 'd'] + } + ) + +``` + +### 字符类 + +在上面的`nonterminal_grammar`中,我们仅列举了前几个字母; 确实,如`<idchar> ::= 'a' | 'b' | 'c' ...`中那样手动枚举语法中的所有字母或数字有点痛苦。 + +但是,请记住,语法是程序的一部分,因此也可以通过程序构造。 我们引入一个函数`srange()`,该函数在字符串中构造一个字符列表: + +```py +import [string](https://docs.python.org/3/library/string.html) + +``` + +```py +def srange(characters): + """Construct a list with all characters in the string""" + return [c for c in characters] + +``` + +如果我们将包含所有ASCII字母的常量`string.ascii_letters`传递给它,则`srange()`将返回所有ASCII字母的列表: + +```py +string.ascii_letters + +``` + +```py +'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' + +``` + +```py +srange(string.ascii_letters)[:10] + +``` + +```py +['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'] + +``` + +我们可以在语法中使用这些常量来快速定义标识符: + +```py +nonterminal_grammar = extend_grammar(nonterminal_grammar, + { + "<idchar>": srange(string.ascii_letters) + srange(string.digits) + srange("-_") + } + ) + +``` + +```py +[simple_grammar_fuzzer(nonterminal_grammar, "<identifier>") for i in range(10)] + +``` + +```py +['b', 'd', 'V9', 'x4c', 'YdiEWj', 'c', 'xd', '7', 'vIU', 'QhKD'] + +``` + +快捷方式`crange(start, end)`返回`start`到(包括)`end`的ASCII范围内的所有字符的列表: + +```py +def crange(character_start, character_end): + return [chr(i) + for i in range(ord(character_start), ord(character_end) + 1)] + +``` + +我们可以使用它来表示字符范围: + +```py +crange('0', '9') + +``` + +```py +['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'] + +``` + +```py +assert crange('a', 'z') == srange(string.ascii_lowercase) + +``` + +### 语法快捷方式 + +在上面的`nonterminal_grammar`中,像在其他语法中一样,我们必须使用*递归*来表示字符的重复,即通过引用原始定义: + +```py +nonterminal_grammar["<identifier>"] + +``` + +```py +['<idchar>', '<identifier><idchar>'] + +``` + +如果我们简单地声明非终结符应该是字母的非空序列,则可能会容易一些,例如 + +```py +<identifier> = <idchar>+ +``` + +其中`+`表示其后跟符号的非空重复。 + +诸如`+`之类的运算符经常作为语法中方便的*快捷方式*引入。 正式地,我们的语法以 [Backus-Naur形式](https://en.wikipedia.org/wiki/Backus-Naur_form)或简称 *BNF* 的形式出现。 运算符*将* BNF扩展为所谓的_extended BNF *或简称为* EBNF *: + +* `<symbol>?`格式表示`<symbol>`是可选的-也就是说,它可以出现0或1次。 +* 形式`<symbol>+`表示`<symbol>`可以重复出现1次或多次。 +* 形式`<symbol>*`表示`<symbol>`可以出现0次或更多次。 (换句话说,这是一个可选的重复。) + +为了使事情变得更加有趣,我们想在上述快捷方式中使用*括号*。 因此,`(<foo><bar>)?`表示`<foo>`和`<bar>`的顺序是可选的。 + +使用此类运算符,我们可以以更简单的方式定义标识符规则。 为此,让我们创建原始语法的副本并修改`<identifier>`规则: + +```py +nonterminal_ebnf_grammar = extend_grammar(nonterminal_grammar, + { + "<identifier>": ["<idchar>+"] + } + ) + +``` + +同样,我们可以简化表达语法。 考虑一下符号是如何可选的,以及如何将整数表示为数字序列。 + +```py +EXPR_EBNF_GRAMMAR = { + "<start>": + ["<expr>"], + + "<expr>": + ["<term> + <expr>", "<term> - <expr>", "<term>"], + + "<term>": + ["<factor> * <term>", "<factor> / <term>", "<factor>"], + + "<factor>": + ["<sign>?<factor>", "(<expr>)", "<integer>(.<integer>)?"], + + "<sign>": + ["+", "-"], + + "<integer>": + ["<digit>+"], + + "<digit>": + srange(string.digits) +} + +``` + +我们的目标是将上述EBNF语法转换为常规BNF语法。 这是通过以下四个规则完成的: + +1. 具有新规则`<new-symbol> ::= content`的表达式`(content)op`变为`<new-symbol>op`,其中`op`是`?`,`+`和`*`之一。 +2. 表达式`<symbol>?`变为`<new-symbol>`,其中`<new-symbol> ::= <empty> | <symbol>`。 +3. 表达式`<symbol>+`变为`<new-symbol>`,其中`<new-symbol> ::= <symbol> | <symbol><new-symbol>`。 +4. 表达式`<symbol>*`变为`<new-symbol>`,其中`<new-symbol> ::= <empty> | <symbol><new-symbol>`。 + +这里,`<empty>`扩展为空字符串,与`<empty> ::=`相同。 (这也称为 *epsilon扩展*。) + +如果这些运算符使您想起*正则表达式*,这并非偶然:实际上,可以使用上述规则(以及使用`crange()`定义的字符类)将任何基本正则表达式转换为语法。 。 + +将这些规则应用于上面的示例将产生以下结果: + +* `<idchar>+`与`<new-symbol> ::= <idchar> | <idchar><new-symbol>`成为`<idchar><new-symbol>`。 +* `<integer>(.<integer>)?`与`<new-symbol> ::= <empty> | .<integer>`成为`<integer><new-symbol>`。 + +让我们分三步实施这些规则。 + +#### 创建新符号 + +首先,我们需要一种机制来创建新符号。 这很简单。 + +```py +def new_symbol(grammar, symbol_name="<symbol>"): + """Return a new symbol for `grammar` based on `symbol_name`""" + if symbol_name not in grammar: + return symbol_name + + count = 1 + while True: + tentative_symbol_name = symbol_name[:-1] + "-" + repr(count) + ">" + if tentative_symbol_name not in grammar: + return tentative_symbol_name + count += 1 + +``` + +```py +assert new_symbol(EXPR_EBNF_GRAMMAR, '<expr>') == '<expr-1>' + +``` + +#### 扩展括号表达式 + +接下来,我们需要一种从扩展中提取括号表达式的方法,并根据上述规则对其进行扩展。 让我们从提取表达式开始: + +```py +RE_PARENTHESIZED_EXPR = re.compile(r'\([^()]*\)[?+*]') + +``` + +```py +def parenthesized_expressions(expansion): + # In later chapters, we allow expansions to be tuples, + # with the expansion being the first element + if isinstance(expansion, tuple): + expansion = expansion[0] + + return re.findall(RE_PARENTHESIZED_EXPR, expansion) + +``` + +```py +assert parenthesized_expressions("(<foo>)* (<foo><bar>)+ (+<foo>)? <integer>(.<integer>)?") == [ + '(<foo>)*', '(<foo><bar>)+', '(+<foo>)?', '(.<integer>)?'] + +``` + +现在,我们可以使用它们来应用上面的规则编号1,为括号中的表达式引入新的符号。 + +```py +def convert_ebnf_parentheses(ebnf_grammar): + """Convert a grammar in extended BNF to BNF""" + grammar = extend_grammar(ebnf_grammar) + for nonterminal in ebnf_grammar: + expansions = ebnf_grammar[nonterminal] + + for i in range(len(expansions)): + expansion = expansions[i] + + while True: + parenthesized_exprs = parenthesized_expressions(expansion) + if len(parenthesized_exprs) == 0: + break + + for expr in parenthesized_exprs: + operator = expr[-1:] + contents = expr[1:-2] + + new_sym = new_symbol(grammar) + expansion = grammar[nonterminal][i].replace( + expr, new_sym + operator, 1) + grammar[nonterminal][i] = expansion + grammar[new_sym] = [contents] + + return grammar + +``` + +这将执行上面概述的转换: + +```py +convert_ebnf_parentheses({"<number>": ["<integer>(.<integer>)?"]}) + +``` + +```py +{'<number>': ['<integer><symbol>?'], '<symbol>': ['.<integer>']} + +``` + +它甚至适用于带括号的嵌套表达式: + +```py +convert_ebnf_parentheses({"<foo>": ["((<foo>)?)+"]}) + +``` + +```py +{'<foo>': ['<symbol-1>+'], '<symbol>': ['<foo>'], '<symbol-1>': ['<symbol>?']} + +``` + +#### 扩展运算符 + +扩展带括号的表达式后,我们现在需要注意符号后跟运算符(`?`,`*`和`+`)。 与上面的`convert_ebnf_parentheses()`一样,我们首先提取所有符号,然后提取一个运算符。 + +```py +RE_EXTENDED_NONTERMINAL = re.compile(r'(<[^<> ]*>[?+*])') + +``` + +```py +def extended_nonterminals(expansion): + # In later chapters, we allow expansions to be tuples, + # with the expansion being the first element + if isinstance(expansion, tuple): + expansion = expansion[0] + + return re.findall(RE_EXTENDED_NONTERMINAL, expansion) + +``` + +```py +assert extended_nonterminals( + "<foo>* <bar>+ <elem>? <none>") == ['<foo>*', '<bar>+', '<elem>?'] + +``` + +我们的转换器提取符号和运算符,并根据上述规则添加新符号。 + +```py +def convert_ebnf_operators(ebnf_grammar): + """Convert a grammar in extended BNF to BNF""" + grammar = extend_grammar(ebnf_grammar) + for nonterminal in ebnf_grammar: + expansions = ebnf_grammar[nonterminal] + + for i in range(len(expansions)): + expansion = expansions[i] + extended_symbols = extended_nonterminals(expansion) + + for extended_symbol in extended_symbols: + operator = extended_symbol[-1:] + original_symbol = extended_symbol[:-1] + + new_sym = new_symbol(grammar, original_symbol) + grammar[nonterminal][i] = grammar[nonterminal][i].replace( + extended_symbol, new_sym, 1) + + if operator == '?': + grammar[new_sym] = ["", original_symbol] + elif operator == '*': + grammar[new_sym] = ["", original_symbol + new_sym] + elif operator == '+': + grammar[new_sym] = [ + original_symbol, original_symbol + new_sym] + + return grammar + +``` + +```py +convert_ebnf_operators({"<integer>": ["<digit>+"]}) + +``` + +```py +{'<integer>': ['<digit>'], '<digit>': ['<digit>', '<digit><digit>']} + +``` + +#### 全部在一起 + +我们可以结合两个,首先扩展括号,然后是运算符: + +```py +def convert_ebnf_grammar(ebnf_grammar): + return convert_ebnf_operators(convert_ebnf_parentheses(ebnf_grammar)) + +``` + +```py +convert_ebnf_grammar({"<authority>": ["(<userinfo>@)?<host>(:<port>)?"]}) + +``` + +```py +{'<authority>': ['<symbol-2><host><symbol-1-1>'], + '<symbol>': ['<userinfo>@'], + '<symbol-1>': [':<port>'], + '<symbol-2>': ['', '<symbol>'], + '<symbol-1-1>': ['', '<symbol-1>']} + +``` + +```py +expr_grammar = convert_ebnf_grammar(EXPR_EBNF_GRAMMAR) +expr_grammar + +``` + +```py +{'<start>': ['<expr>'], + '<expr>': ['<term> + <expr>', '<term> - <expr>', '<term>'], + '<term>': ['<factor> * <term>', '<factor> / <term>', '<factor>'], + '<factor>': ['<sign-1><factor>', '(<expr>)', '<integer><symbol-1>'], + '<sign>': ['+', '-'], + '<integer>': ['<digit-1>'], + '<digit>': ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'], + '<symbol>': ['.<integer>'], + '<sign-1>': ['', '<sign>'], + '<symbol-1>': ['', '<symbol>'], + '<digit-1>': ['<digit>', '<digit><digit-1>']} + +``` + +成功! 我们已经很好地将EBNF语法转换为BNF。 + +通过字符类和EBNF语法转换,我们有两个功能强大的工具可以使语法编写更加容易。 在处理语法时,我们将一再使用它们。 + +### 语法扩展 + +在本书的学习过程中,我们经常希望为语法指定*附加信息*,例如 [*概率*](ProbabilisticGrammarFuzzer.html) 或 [*约束* 。 为了支持这些扩展以及可能的其他扩展,我们定义了*注释*机制。 + +我们注释语法的概念是将*注释*添加到各个扩展中。 为此,我们允许扩展不仅是字符串,而且是字符串和一组属性的*对*,如 + +```py +"<expr>": + [("<term> + <expr>", opts(min_depth=10)), + ("<term> - <expr>", opts(max_depth=2)), + "<term>"] + +``` + +在这里,`opts()`函数将使我们能够表达适用于各个扩展的注释; 在这种情况下,加法将以`min_depth`值为10注释,减法以`max_depth`值为2注释。这些注释的含义留给处理语法的各个算法; 但是,一般的想法是可以忽略它们。 + +我们的`opts()`辅助函数返回其参数到值的映射: + +```py +def opts(**kwargs): + return kwargs + +``` + +```py +opts(min_depth=10) + +``` + +```py +{'min_depth': 10} + +``` + +为了处理扩展字符串以及扩展和注释对,我们通过指定的辅助函数`exp_string()`和`exp_opts()`访问扩展字符串和相关的注释: + +```py +def exp_string(expansion): + """Return the string to be expanded""" + if isinstance(expansion, str): + return expansion + return expansion[0] + +``` + +```py +exp_string(("<term> + <expr>", opts(min_depth=10))) + +``` + +```py +'<term> + <expr>' + +``` + +```py +def exp_opts(expansion): + """Return the options of an expansion. If options are not defined, return {}""" + if isinstance(expansion, str): + return {} + return expansion[1] + +``` + +```py +def exp_opt(expansion, attribute): + """Return the given attribution of an expansion. + If attribute is not defined, return None""" + return exp_opts(expansion).get(attribute, None) + +``` + +```py +exp_opts(("<term> + <expr>", opts(min_depth=10))) + +``` + +```py +{'min_depth': 10} + +``` + +```py +exp_opt(("<term> - <expr>", opts(max_depth=2)), 'max_depth') + +``` + +```py +2 + +``` + +最后,我们定义一个设置特定选项的辅助函数: + +```py +def set_opts(grammar, symbol, expansion, opts=None): + """Set the options of the given expansion of grammar[symbol] to opts""" + expansions = grammar[symbol] + for i, exp in enumerate(expansions): + if exp_string(exp) != exp_string(expansion): + continue + + new_opts = exp_opts(exp) + if opts is None or new_opts == {}: + new_opts = opts + else: + for key in opts: + new_opts[key] = opts[key] + if new_opts == {}: + grammar[symbol][i] = exp_string(exp) + else: + grammar[symbol][i] = (exp_string(exp), new_opts) + return + + raise KeyError( + "no expansion " + + repr(symbol) + + " -> " + + repr( + exp_string(expansion))) + +``` + +## 检查语法 + +由于语法以字符串表示,因此引入错误非常容易。 因此,让我们介绍一个辅助函数,该函数检查语法的一致性。 + +辅助函数`is_valid_grammar()`遍历语法以检查是否定义了所有使用的符号,反之亦然,这对于调试非常有用; 它还检查从起始符号是否可以访问所有符号。 您不必在这里深入研究细节,但是与往常一样,在使用输入数据之前,请务必先弄清楚它。 + +```py +import [sys](https://docs.python.org/3/library/sys.html) + +``` + +```py +def def_used_nonterminals(grammar, start_symbol=START_SYMBOL): + defined_nonterminals = set() + used_nonterminals = {start_symbol} + + for defined_nonterminal in grammar: + defined_nonterminals.add(defined_nonterminal) + expansions = grammar[defined_nonterminal] + if not isinstance(expansions, list): + print(repr(defined_nonterminal) + ": expansion is not a list", + file=sys.stderr) + return None, None + + if len(expansions) == 0: + print(repr(defined_nonterminal) + ": expansion list empty", + file=sys.stderr) + return None, None + + for expansion in expansions: + if isinstance(expansion, tuple): + expansion = expansion[0] + if not isinstance(expansion, str): + print(repr(defined_nonterminal) + ": " + + repr(expansion) + ": not a string", + file=sys.stderr) + return None, None + + for used_nonterminal in nonterminals(expansion): + used_nonterminals.add(used_nonterminal) + + return defined_nonterminals, used_nonterminals + +``` + +```py +def reachable_nonterminals(grammar, start_symbol=START_SYMBOL): + reachable = set() + + def _find_reachable_nonterminals(grammar, symbol): + nonlocal reachable + reachable.add(symbol) + for expansion in grammar.get(symbol, []): + for nonterminal in nonterminals(expansion): + if nonterminal not in reachable: + _find_reachable_nonterminals(grammar, nonterminal) + + _find_reachable_nonterminals(grammar, start_symbol) + return reachable + +``` + +```py +def unreachable_nonterminals(grammar, start_symbol=START_SYMBOL): + return grammar.keys() - reachable_nonterminals(grammar, start_symbol) + +``` + +```py +def opts_used(grammar): + used_opts = set() + for symbol in grammar: + for expansion in grammar[symbol]: + used_opts |= set(exp_opts(expansion).keys()) + return used_opts + +``` + +```py +def is_valid_grammar(grammar, start_symbol=START_SYMBOL, supported_opts=None): + defined_nonterminals, used_nonterminals = \ + def_used_nonterminals(grammar, start_symbol) + if defined_nonterminals is None or used_nonterminals is None: + return False + + # Do not complain about '<start>' being not used, + # even if start_symbol is different + if START_SYMBOL in grammar: + used_nonterminals.add(START_SYMBOL) + + for unused_nonterminal in defined_nonterminals - used_nonterminals: + print(repr(unused_nonterminal) + ": defined, but not used", + file=sys.stderr) + for undefined_nonterminal in used_nonterminals - defined_nonterminals: + print(repr(undefined_nonterminal) + ": used, but not defined", + file=sys.stderr) + + # Symbols must be reachable either from <start> or given start symbol + unreachable = unreachable_nonterminals(grammar, start_symbol) + msg_start_symbol = start_symbol + if START_SYMBOL in grammar: + unreachable = unreachable - \ + reachable_nonterminals(grammar, START_SYMBOL) + if start_symbol != START_SYMBOL: + msg_start_symbol += " or " + START_SYMBOL + for unreachable_nonterminal in unreachable: + print(repr(unreachable_nonterminal) + ": unreachable from " + msg_start_symbol, + file=sys.stderr) + + used_but_not_supported_opts = set() + if supported_opts is not None: + used_but_not_supported_opts = opts_used( + grammar).difference(supported_opts) + for opt in used_but_not_supported_opts: + print( + "warning: option " + + repr(opt) + + " is not supported", + file=sys.stderr) + + return used_nonterminals == defined_nonterminals and len(unreachable) == 0 + +``` + +上面定义的语法通过了测试: + +```py +assert is_valid_grammar(EXPR_GRAMMAR) +assert is_valid_grammar(CGI_GRAMMAR) +assert is_valid_grammar(URL_GRAMMAR) + +``` + +该检查也可以应用于EBNF语法: + +```py +assert is_valid_grammar(EXPR_EBNF_GRAMMAR) + +``` + +但是,这些没有通过测试: + +```py +assert not is_valid_grammar({"<start>": ["<x>"], "<y>": ["1"]}) + +``` + +```py +'<y>': defined, but not used +'<x>': used, but not defined +'<y>': unreachable from <start> + +``` + +```py +assert not is_valid_grammar({"<start>": "123"}) + +``` + +```py +'<start>': expansion is not a list + +``` + +```py +assert not is_valid_grammar({"<start>": []}) + +``` + +```py +'<start>': expansion list empty + +``` + +```py +assert not is_valid_grammar({"<start>": [1, 2, 3]}) + +``` + +```py +'<start>': 1: not a string + +``` + +从这里开始,在定义语法时,我们将始终使用`is_valid_grammar()`。 + +## 经验教训 + +* 语法是表达和产生语法有效输入的强大工具。 +* 语法产生的输入可以直接使用,也可以用作基于突变的模糊测试的种子。 +* 可以使用字符类和运算符扩展语法,以使编写更加容易。 + +## 后续步骤 + +由于它们为生成软件测试奠定了良好的基础,因此在本书中我们一再使用语法。 作为先睹为快,我们可以对[模糊配置](ConfigurationFuzzer.html)使用语法: + +```py +<options> ::= <option>* +<option> ::= -h | --version | -v | -d | -i | --global-config <filename> +``` + +我们可以对[模糊功能和API](APIFuzzer.html) 和[模糊图形用户界面](WebFuzzer.html)使用语法: + +```py +<call-sequence> ::= <call>* +<call> ::= urlparse(<url>) | urlsplit(<url>) +``` + +我们可以将[概率](ProbabilisticGrammarFuzzer.html)和[约束](GeneratorGrammarFuzzer.html)分配给各个扩展: + +```py +<term>: 50% <factor> * <term> | 30% <factor> / <term> | 20% <factor> +<integer>: <digit>+ { <integer> >= 100 } +``` + +所有这些额外的东西变得特别有价值 + +1. *自动推断语法*,无需手动指定语法,并且 +2. *指导他们实现特定目标*,例如覆盖范围或关键功能; + +我们还将在本书中讨论所有技术。 + +但是,要到达那里,我们还有一些功课要做。 特别是,我们首先必须学习如何 + +* [创建高效的语法模糊器](GrammarFuzzer.html) + +## 背景 + +作为人类语言的基础之一,语法一直存在,只要人类语言存在就可以。 生成语法的第一个*形式化*是达克西·普特拉·帕尼(DakṣiputraPāṇini)于公元前350年。 。 作为表达用于数据和程序的形式语言的一般手段,不能夸大其在计算机科学中的作用。 Chomsky [ [Chomsky *等人*,1956\.](https://chomsky.info/wp-content/uploads/195609-.pdf) ]的开创性工作介绍了正则语言,上下文无关文法,上下文相关文法和通用文法的中心模型。 从那以后(并在计算机科学领域任教)作为一种指定输入和编程语言的方法。 + +语法在产生*的*测试输入中的使用可以追溯到Burkhardt [ [Burkhardt *等人*,1967年。](https://doi.org/10.1007/BF02235512)],稍后由Hanford重新发现并应用 [] Hanford *等*,1970。](https://doi.org/10.1147/sj.94.0242)]和Purdom [ [Purdom *等*,1972。](https://doi.org/10.1007/BF01932308)]。 此后,语法测试最重要的用途是*编译器测试*。 实际上,基于语法的测试是编译器和Web浏览器按其应有的方式工作的重要原因之一: + +* [CSmith](https://embed.cs.utah.edu/csmith/) 工具[ [Yang *等*,2011年。](https://doi.org/10.1145/1993498.1993532)]特别针对C程序,从C语法开始,然后应用其他步骤,例如引用变量 和较早定义的函数,或确保整数和类型安全。 他们的作者使用它“查找并报告了400多个以前未知的编译器错误”。 + +* [LangFuzz](http://issta2016.cispa.saarland/interview-with-christian-holler/) 作品[ [Holler *等人*,2012年。](https://www.usenix.org/system/files/conference/usenixsecurity12/sec12-final73.pdf)]与本书共有两名作者,使用通用语法产生输出,每天使用 晚上生成JavaScript程序并测试其解释器; 截止到今天,它已经在Mozilla Firefox,Google Chrome和Microsoft Edge等浏览器中发现了2,600多个错误。 + +* [EMI项目](http://web.cs.ucdavis.edu/~su/emi-project/) [ [Le *等*,2014年。](https://doi.org/10.1145/2594291.2594334)]使用语法对C编译器进行压力测试,将已知测试转换为在语义上等效的替代程序 所有输入。 同样,这已经修复了100多个C编译器中的错误。 + +* [Grammarinator](https://github.com/renatahodovan/grammarinator) [[Hodován*等人*,2018。](https://www.researchgate.net/publication/328510752_Grammarinator_a_grammar-based_open_source_fuzzer)]是一种开源语法模糊工具(用Python编写!),使用流行的ANTLR格式作为语法规范。 像LangFuzz一样,它使用语法进行解析和生成,并且在 *JerryScript* 轻量级JavaScript引擎和相关平台中发现了100多个问题。 + +* [Domato](https://github.com/googleprojectzero/domato) 是一个通用的语法生成引擎,专门用于对DOM输入进行模糊处理。 它揭示了流行的Web浏览器中的许多安全问题。 + +当然,编译器和Web浏览器不仅是测试需要语法的领域,而且还是众所周知的语法的领域。 我们在本书中的主张是,语法可用于几乎生成任何输入*,而我们的目标是使您能够准确地做到这一点。* + +## 练习 + +### 练习1:JSON语法 + +看一下 [JSON规范](http://www.json.org)并从中得出语法: + +* 使用*字符类*表示有效字符 +* 使用EBNF表示重复和可选部分 +* 假使,假设 + * 字符串是数字,ASCII字母,标点和空格字符的序列,不带引号或转义符 + * 空白只是一个空格。 +* 使用`is_valid_grammar()`确保语法有效。 + +将语法输入`simple_grammar_fuzzer()`。 您是否遇到任何错误,为什么? + +[Use the notebook](https://mybinder.org/v2/gh/uds-se/fuzzingbook/master?filepath=docs/notebooks/Grammars.ipynb#Exercises) to work on the exercises and see solutions. + +### 练习2:查找错误 + +名称`simple_grammar_fuzzer()`并非偶然出现:它扩展语法的方式受到多种限制。 如果按上述定义在`nonterminal_grammar`和`expr_grammar`上应用`simple_grammar_fuzzer()`,会发生什么,为什么? + +[Use the notebook](https://mybinder.org/v2/gh/uds-se/fuzzingbook/master?filepath=docs/notebooks/Grammars.ipynb#Exercises) to work on the exercises and see solutions. + +### 练习3:带有正则表达式的语法 + +在以正则表达式扩展的*语法中,我们可以使用特殊形式* + +```py +/regex/ +``` + +在扩展中包含正则表达式。 例如,我们可以有一条规则 + +```py +<integer> ::= /[+-]?[0-9]+/ +``` + +快速表示整数是可选符号,后跟数字序列。 + +#### 第1部分:转换正则表达式 + +编写一个使用正则表达式`r`并创建等效语法的转换器`convert_regex(r)`。 支持以下正则表达式构造: + +* `*`,`+`,`?`,`()`应该仅在上述EBNF中起作用。 +* `a|b`应该转换为替代项列表`[a, b]`。 +* `.`应该匹配除换行符之外的任何字符。 +* `[abc]`应翻译为`srange("abc")` +* `[^abc]`应该转换为ASCII字符集*,除了* `srange("abc")`之外。 +* `[a-b]`应翻译为`crange(a, b)` +* `[^a-b]`应该转换为ASCII字符集*,除了* `crange(a, b)`之外。 + +示例:`convert_regex(r"[0-9]+")`应该产生一个语法,例如 + +```py +{ + "<start>": ["<s1>"], + "<s1>": [ "<s2>", "<s1><s2>" ], + "<s2>": crange('0', '9') +} + +``` + +[Use the notebook](https://mybinder.org/v2/gh/uds-se/fuzzingbook/master?filepath=docs/notebooks/Grammars.ipynb#Exercises) to work on the exercises and see solutions. + +#### 第2部分:识别并扩展正则表达式 + +编写一个转换器`convert_regex_grammar(g)`,该转换器采用EBNF文法`g`,其中包含形式为`/.../`的正则表达式,并创建等效的BNF文法。 支持上面的正则表达式构造。 + +示例:`convert_regex_grammar({ "<integer>" : "/[+-]?[0-9]+/" })`应该产生一个语法,例如 + +```py +{ + "<integer>": ["<s1><s3>"], + "<s1>": [ "", "<s2>" ], + "<s2>": srange("+-"), + "<s3>": [ "<s4>", "<s4><s3>" ], + "<s4>": crange('0', '9') +} + +``` + +可选:支持*以正则表达式转义*:`\c`转换为文字字符`c`; `\/`转换为`/`(因此不结束正则表达式); `\\`转换为`\`。 + +[Use the notebook](https://mybinder.org/v2/gh/uds-se/fuzzingbook/master?filepath=docs/notebooks/Grammars.ipynb#Exercises) to work on the exercises and see solutions. + +### 练习4:将语法定义为函数(高级)) + +为了获得用于指定语法的更好语法,可以使用Python构造,然后再通过另一种功能将*解析为*。 例如,我们可以想象一个语法定义,它使用`|`作为分隔替代项的手段: + +[Use the notebook](https://mybinder.org/v2/gh/uds-se/fuzzingbook/master?filepath=docs/notebooks/Grammars.ipynb#Exercises) to work on the exercises and see solutions. + +```py +def expression_grammar_fn(): + start = "<expr>" + expr = "<term> + <expr>" | "<term> - <expr>" + term = "<factor> * <term>" | "<factor> / <term>" | "<factor>" + factor = "+<factor>" | "-<factor>" | "(<expr>)" | "<integer>.<integer>" | "<integer>" + integer = "<digit><integer>" | "<digit>" + digit = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' + +``` + +如果执行`expression_grammar_fn()`,将产生错误。 然而,`expression_grammar_fn()`的目的不是要执行,而是用作将要构造语法的*数据*。 + +```py +with ExpectError(): + expression_grammar_fn() + +``` + +```py +Traceback (most recent call last): + File "<ipython-input-109-612cec5468d3>", line 2, in <module> + expression_grammar_fn() + File "<ipython-input-108-f21ab929e5ee>", line 3, in expression_grammar_fn + expr = "<term> + <expr>" | "<term> - <expr>" +TypeError: unsupported operand type(s) for |: 'str' and 'str' (expected) + +``` + +为此,我们使用了`ast`(抽象语法树)和`inspect`(代码检查)模块。 + +```py +import [ast](https://docs.python.org/3/library/ast.html) +import [inspect](https://docs.python.org/3/library/inspect.html) + +``` + +首先,我们获得`expression_grammar_fn()`的源代码... + +```py +source = inspect.getsource(expression_grammar_fn) +source + +``` + +```py +'def expression_grammar_fn():\n start = "<expr>"\n expr = "<term> + <expr>" | "<term> - <expr>"\n term = "<factor> * <term>" | "<factor> / <term>" | "<factor>"\n factor = "+<factor>" | "-<factor>" | "(<expr>)" | "<integer>.<integer>" | "<integer>"\n integer = "<digit><integer>" | "<digit>"\n digit = \'0\' | \'1\' | \'2\' | \'3\' | \'4\' | \'5\' | \'6\' | \'7\' | \'8\' | \'9\'\n' + +``` + +...然后将其解析为抽象语法树: + +```py +tree = ast.parse(source) + +``` + +现在,我们可以分析树以查找运算符和替代方法。 `get_alternatives()`遍历树的所有节点`op`; 如果该节点看起来像二进制*或*(`|`)操作,我们将进行更深入的挖掘并递归。 如果没有,那么我们已经达到了单一生产,并且我们尝试从生产中获得表达。 我们根据要表示产品的方式定义`to_expr`参数。 在这种情况下,我们用单个字符串表示单个产品。 + +```py +def get_alternatives(op, to_expr=lambda o: o.s): + if isinstance(op, ast.BinOp) and isinstance(op.op, ast.BitOr): + return get_alternatives(op.left, to_expr) + [to_expr(op.right)] + return [to_expr(op)] + +``` + +`funct_parser()`接受函数的抽象语法树(例如`expression_grammar_fn()`)并遍历所有分配: + +```py +def funct_parser(tree, to_expr=lambda o: o.s): + return {assign.targets[0].id: get_alternatives(assign.value, to_expr) + for assign in tree.body[0].body} + +``` + +结果是我们常规格式的语法: + +```py +grammar = funct_parser(tree) +for symbol in grammar: + print(symbol, "::=", grammar[symbol]) + +``` + +```py +start ::= ['<expr>'] +expr ::= ['<term> + <expr>', '<term> - <expr>'] +term ::= ['<factor> * <term>', '<factor> / <term>', '<factor>'] +factor ::= ['+<factor>', '-<factor>', '(<expr>)', '<integer>.<integer>', '<integer>'] +integer ::= ['<digit><integer>', '<digit>'] +digit ::= ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'] + +``` + +#### 第1部分(a):单个功能:-One-Single-Function) + +编写单个函数`define_grammar(fn)`,该函数采用定义为函数的语法(例如`expression_grammar_fn()`)并返回常规语法。 + +[Use the notebook](https://mybinder.org/v2/gh/uds-se/fuzzingbook/master?filepath=docs/notebooks/Grammars.ipynb#Exercises) to work on the exercises and see solutions. + +#### 第1部分(b):替代表示形式:-Alternative-representations) + +我们注意到,我们先前设计的语法表示形式不允许简单生成替代项,例如`srange()`和`crange()`。 此外,可能会发现表达式的字符串表示形式受到限制。 事实证明,扩展我们的语法定义以支持如下语法很简单: + +```py +def define_name(o): + return o.id if isinstance(o, ast.Name) else o.s + +``` + +```py +def define_expr(op): + if isinstance(op, ast.BinOp) and isinstance(op.op, ast.Add): + return (*define_expr(op.left), define_name(op.right)) + return (define_name(op),) + +``` + +```py +def define_ex_grammar(fn): + return define_grammar(fn, define_expr) + +``` + +语法: + +```py +@define_ex_grammar +def expression_grammar(): + start = expr + expr = (term + '+' + expr + | term + '-' + expr) + term = (factor + '*' + term + | factor + '/' + term + | factor) + factor = ('+' + factor + | '-' + factor + | '(' + expr + ')' + | integer + '.' + integer + | integer) + integer = (digit + integer + | digit) + digit = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' + +for symbol in expression_grammar: + print(symbol, "::=", expression_grammar[symbol]) + +``` + +**注意。** 这样获得的语法数据结构比标准数据结构更详细。 它表示每个生产为元组。 + +我们注意到,我们在上述语法中未启用`srange()`或`crange()`。 您将如何添加这些? (*提示:*包装`define_expr()`以查找`ast.Call`) + +#### 第2部分:扩展文法 + +引入一个带有一对`(min, max)`的运算符`*`,其中`min`和`max`分别是最小和最大重复次数。 缺失值`min`代表零; 无限值的缺失值`max`。 + +```py +def identifier_grammar_fn(): + identifier = idchar * (1,) + +``` + +使用`*`运算符,我们可以概括EBNF运算符-`?`变为(0,1),`*`变为(0,),`+`变为(1,)。 编写一个转换器,该转换器采用使用`*`定义的扩展语法,对其进行解析,然后将其转换为BNF。 + +[Use the notebook](https://mybinder.org/v2/gh/uds-se/fuzzingbook/master?filepath=docs/notebooks/Grammars.ipynb#Exercises) to work on the exercises and see solutions. \ No newline at end of file diff --git a/new/fuzzing-book-zh/16.md b/new/fuzzing-book-zh/16.md new file mode 100644 index 0000000000000000000000000000000000000000..bb01355b58ada8b78abd99d88fc597a445ab25ea --- /dev/null +++ b/new/fuzzing-book-zh/16.md @@ -0,0 +1,1503 @@ +# 高效的语法模糊化 + +> 原文: [https://www.fuzzingbook.org/html/GrammarFuzzer.html](https://www.fuzzingbook.org/html/GrammarFuzzer.html) + +在语法的[一章中,我们已经看到了如何使用*语法*进行非常有效的测试。 在本章中,我们将以前的基于字符串的算法改进为基于树的算法,该算法速度更快,并且可以更好地控制模糊输入的产生。](Grammars.html) + +本章中的算法是其他几种技术的基础。 因此,本章是书中的“枢纽”。 + +**前提条件** + +* 您应该知道基于语法的模糊测试的工作原理,例如 摘自[语法](Grammars.html)一章。 + +## 内容提要 + +要使用本章中提供的代码来[,请编写](Importing.html) + +```py +>>> from [fuzzingbook.GrammarFuzzer](GrammarFuzzer.html) import <identifier> + +``` + +然后利用以下功能。 + +本章介绍`GrammarFuzzer`,这是一种高效的语法模糊器,它采用一种语法来产生语法上有效的输入字符串。 这是典型用法: + +```py +>>> from [Grammars](Grammars.html) import US_PHONE_GRAMMAR +>>> phone_fuzzer = GrammarFuzzer(US_PHONE_GRAMMAR) +>>> phone_fuzzer.fuzz() +'(582)871-9500' + +``` + +`GrammarFuzzer`构造函数采用许多关键字参数来控制其行为。 例如,`start_symbol`允许设置以扩展名开头的符号(而不是`<start>`): + +```py +>>> area_fuzzer = GrammarFuzzer(US_PHONE_GRAMMAR, start_symbol='<area>') +>>> area_fuzzer.fuzz() +'707' +>>> import [inspect](https://docs.python.org/3/library/inspect.html) +>>> print(inspect.getdoc(GrammarFuzzer.__init__)) +Produce strings from `grammar`, starting with `start_symbol`. +If `min_nonterminals` or `max_nonterminals` is given, use them as limits +for the number of nonterminals produced. +If `disp` is set, display the intermediate derivation trees. +If `log` is set, show intermediate steps as text on standard output. + +``` + +在内部,`GrammarFuzzer`使用[派生树](#Derivation-Trees),并逐步扩展。 生成字符串后,可以在`derivation_tree`属性中访问生成的树。 + +```py +>>> display_tree(phone_fuzzer.derivation_tree) + +``` + +! + +在派生树的内部表示中,*节点*是一对(`symbol`,`children`)。 对于非终端,`symbol`是正在扩展的符号,`children`是其他节点的列表。 对于终端,`symbol`是终端字符串,`children`为空。 + +```py +>>> phone_fuzzer.derivation_tree +('<start>', + [('<phone-number>', + [('(', []), + ('<area>', + [('<lead-digit>', [('5', [])]), + ('<digit>', [('8', [])]), + ('<digit>', [('2', [])])]), + (')', []), + ('<exchange>', + [('<lead-digit>', [('8', [])]), + ('<digit>', [('7', [])]), + ('<digit>', [('1', [])])]), + ('-', []), + ('<line>', + [('<digit>', [('9', [])]), + ('<digit>', [('5', [])]), + ('<digit>', [('0', [])]), + ('<digit>', [('0', [])])])])]) + +``` + +本章包含各种与派生树配合使用的助手,包括可视化工具。 + +## 算法 + +在[的前一章](Grammars.html)中,我们介绍了`simple_grammar_fuzzer()`函数,该函数采用语法并自动从中产生语法上有效的字符串。 但是,`simple_grammar_fuzzer()`的名称恰如其分-简单。 为了说明问题,让我们回到在语法的[一章中从`EXPR_GRAMMAR_BNF`创建的`expr_grammar`:](Grammars.html) + +```py +import [fuzzingbook_utils](https://github.com/uds-se/fuzzingbook/tree/master/notebooks/fuzzingbook_utils) + +``` + +```py +from [fuzzingbook_utils](https://github.com/uds-se/fuzzingbook/tree/master/notebooks/fuzzingbook_utils) import unicode_escape + +``` + +```py +from [Grammars](Grammars.html) import EXPR_EBNF_GRAMMAR, convert_ebnf_grammar, simple_grammar_fuzzer, is_valid_grammar, exp_string, exp_opts + +``` + +```py +expr_grammar = convert_ebnf_grammar(EXPR_EBNF_GRAMMAR) +expr_grammar + +``` + +```py +{'<start>': ['<expr>'], + '<expr>': ['<term> + <expr>', '<term> - <expr>', '<term>'], + '<term>': ['<factor> * <term>', '<factor> / <term>', '<factor>'], + '<factor>': ['<sign-1><factor>', '(<expr>)', '<integer><symbol-1>'], + '<sign>': ['+', '-'], + '<integer>': ['<digit-1>'], + '<digit>': ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'], + '<symbol>': ['.<integer>'], + '<sign-1>': ['', '<sign>'], + '<symbol-1>': ['', '<symbol>'], + '<digit-1>': ['<digit>', '<digit><digit-1>']} + +``` + +`expr_grammar`具有有趣的属性。 如果我们将其输入`simple_grammar_fuzzer()`,该函数将陷入无限扩展中: + +```py +from [ExpectError](ExpectError.html) import ExpectTimeout + +``` + +```py +with ExpectTimeout(1): + simple_grammar_fuzzer(grammar=expr_grammar, max_nonterminals=3) + +``` + +```py +Traceback (most recent call last): + File "<ipython-input-6-fbcda5f486bb>", line 2, in <module> + simple_grammar_fuzzer(grammar=expr_grammar, max_nonterminals=3) + File "<string>", line 8, in simple_grammar_fuzzer + File "<string>", line 7, in nonterminals + File "/Users/zeller/anaconda3/lib/python3.6/re.py", line 222, in findall + return _compile(pattern, flags).findall(string) + File "/Users/zeller/anaconda3/lib/python3.6/re.py", line 288, in _compile + try: + File "/Users/zeller/anaconda3/lib/python3.6/re.py", line 288, in _compile + try: + File "<string>", line 16, in check_time +TimeoutError (expected) + +``` + +为什么会这样? 问题在于此规则: + +```py +expr_grammar['<factor>'] + +``` + +```py +['<sign-1><factor>', '(<expr>)', '<integer><symbol-1>'] + +``` + +此处,除了`(expr)`以外的任何选择都会增加符号的数量,即使只是暂时的。 由于我们对要扩展的符号数设置了硬性限制,因此扩展`<factor>`剩下的唯一选择是`(<expr>)`,这将导致无限地添加括号。 + +潜在无限扩展的问题只是`simple_grammar_fuzzer()`的问题之一。 更多问题包括: + +1. *效率低下*。 每次迭代时,此模糊器将搜索到目前为止产生的字符串以扩展符号。 随着生产线的增长,这变得效率低下。 + +2. *很难控制。* 如上所述,即使在限制符号数量的情况下,仍然可以获得非常长的字符串,甚至无限长的字符串。 + +让我们通过绘制不同长度的字符串所需的时间来说明这两个问题。 + +```py +from [Grammars](Grammars.html) import simple_grammar_fuzzer + +``` + +```py +from [Grammars](Grammars.html) import START_SYMBOL, EXPR_GRAMMAR, URL_GRAMMAR, CGI_GRAMMAR + +``` + +```py +from [Grammars](Grammars.html) import RE_NONTERMINAL, nonterminals, is_nonterminal + +``` + +```py +from [Timer](Timer.html) import Timer + +``` + +```py +trials = 50 +xs = [] +ys = [] +for i in range(trials): + with Timer() as t: + s = simple_grammar_fuzzer(EXPR_GRAMMAR, max_nonterminals=15) + xs.append(len(s)) + ys.append(t.elapsed_time()) + print(i, end=" ") +print() + +``` + +```py +0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 + +``` + +```py +average_time = sum(ys) / trials +print("Average time:", average_time) + +``` + +```py +Average time: 0.2103420232999997 + +``` + +```py +%matplotlib inline + +import [matplotlib.pyplot](https://docs.python.org/3/library/matplotlib.pyplot.html) as [plt](https://docs.python.org/3/library/plt.html) +plt.scatter(xs, ys) +plt.title('Time required for generating an output') + +``` + +```py +Text(0.5,1,'Time required for generating an output') + +``` + +![](data:image/png;base64,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 +) + +我们看到(1)工作量随时间呈二次方增长,并且(2)我们可以轻松产生数万个字符长的输出。 + +为了解决这些问题,我们需要一种*更智能的算法*-一种效率更高的算法,可以更好地控制扩展,并且可以在`expr_grammar`中预见到`(expr)`替代方案会产生 与其他两个相反,无限扩展。 + +## 派生树 + +为了获得更有效的算法*和*更好地控制扩展,我们将对语法产生的字符串使用特殊表示形式。 总体思路是使用*树*结构,该结构随后将进行扩展–所谓的*派生树*。 这种表示形式使我们能够始终跟踪我们的扩展状态-回答问题,例如哪些元素已扩展为其他元素以及哪些符号仍需要扩展。 此外,向树中添加新元素比一次又一次地替换字符串要有效得多。 + +像在编程中使用的其他树一样,派生树(也称为*解析树*或*具体语法树*)由具有其他节点(称为*的*节点*)组成 ]子节点*)作为其*子节点*。 树开始于一个没有父节点的节点; 这称为*根节点*; 没有子节点的节点称为*叶*。 + +在以下步骤中,使用语法章节中的算术语法[来说明派生树的语法扩展过程。 我们从一个树的根节点开始,代表*起始符号* –在我们的例子中是`<start>`。](Grammars.html) + +```py +tree + +``` + +<svg height="23pt" viewBox="0.00 0.00 48.00 23.00" width="48pt" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><g class="graph" id="graph0" transform="scale(1 1) rotate(0) translate(4 19)"><title>root \<start\> + +为了扩展树,我们遍历它,搜索没有子元素的非终结符号$ S $。 因此,$ S $是仍然需要扩展的符号。 然后,我们从语法中选择$ S $的扩展。 然后,将扩展项添加为$ S $的新子项。 对于我们的起始符号``,唯一的扩展名是``,因此我们将其添加为子代。 + +```py +tree + +``` + +root \ \<expr\> \<start\>->\<expr\> + +为了从派生树构造生成的字符串,我们按顺序遍历树,并在树的叶子处收集符号。 在上述情况下,我们获得字符串`""`。 + +要进一步扩展树,我们选择另一个符号进行扩展,并将其扩展添加为新子代。 这将使我们获得``符号,该符号被扩展为` + `,并添加了三个子代。 + +```py +tree + +``` + +root \ \<expr\> \<start\>->\<expr\> \<expr\> \<expr\>->\<expr\> + + \<expr\>->+ \<term\> \<expr\>->\<term\> + +我们重复扩展,直到没有符号可扩展为止: + +```py +tree + +``` + +root \ \<expr\> \<start\>->\<expr\> \<expr\> \<expr\>->\<expr\> + + \<expr\>->+ \<term\> \<expr\>->\<term\> \<term\> \<expr\> ->\<term\> \<factor\> \<term\>->\<factor\> \<factor\> \<term\> ->\<factor\> \<integer\> \<factor\> ->\<integer\> \<digit\> \<integer\> ->\<digit\> 2 2 \<digit\> ->2 \<integer\> \<factor\>->\<integer\> \<digit\> \<integer\>->\<digit\> 2 2 \<digit\>->2 + +现在,我们有了字符串`2 + 2`的表示形式。 但是,与单独的字符串相反,派生树记录*所生成字符串的整个结构*(和生产历史,或*派生*历史)。 它还允许进行简单的比较和操作-例如,将一个子树(子结构)替换为另一子树。 + +## 表示派生树 + +为了表示Python中的派生树,我们使用以下格式。 一个节点是一对 + +```py +(SYMBOL_NAME, CHILDREN) + +``` + +其中`SYMBOL_NAME`是代表节点的字符串(即`""`或`"+"`),`CHILDREN`是子节点的列表。 + +`CHILDREN`可以采用一些特殊值: + +1. `None`作为将来扩展的占位符。 这意味着该节点是*非终端符号*,应进一步扩展。 +2. `[]`(即空白列表),表示没有*个*子级。 这意味着该节点是无法再扩展的*终端符号*。 + +让我们采用一个非常简单的派生树,表示上面的中间步骤` + `。 + +```py +derivation_tree = ("", + [("", + [("", None), + (" + ", []), + ("", None)] + )]) + +``` + +为了更好地理解该树的结构,让我们介绍一个可视化该树的函数。 我们通过算法使用`graphviz`包中的`dot`绘图程序,遍历上述结构。 (除非您对树的可视化非常感兴趣,可以直接跳到下面的示例。) + +```py +from [graphviz](https://docs.python.org/3/library/graphviz.html) import Digraph + +``` + +```py +from [IPython.display](https://ipython.readthedocs.io/en/stable/api/generated/IPython.display.html) import display + +``` + +```py +import [re](https://docs.python.org/3/library/re.html) + +``` + +```py +def dot_escape(s): + """Return s in a form suitable for dot""" + s = re.sub(r'([^a-zA-Z0-9" ])', r"\\\1", s) + return s + +``` + +```py +assert dot_escape("hello") == "hello" +assert dot_escape(", world") == "\\\\, world" +assert dot_escape("\\n") == "\\\\n" + +``` + +尽管我们目前对可视化`derivation_tree`感兴趣,但对可视化过程进行一般化也符合我们的利益。 特别是,如果我们的方法`display_tree()`可以显示*任何*树之类的数据结构,则将很有帮助。 为此,我们定义了一个辅助方法`extract_node()`,该方法从给定的数据结构中提取当前符号和子代。 默认实现只是从任何`derivation_tree`节点中提取符号,子级和注释。 + +```py +def extract_node(node, id): + symbol, children, *annotation = node + return symbol, children, ''.join(str(a) for a in annotation) + +``` + +在可视化树时,通常以不同的方式显示某些节点​​很有用。 例如,有时在未处理节点和已处理节点之间进行区分很有用。 我们定义了一个帮助程序`default_node_attr()`,它提供了基本显示,可以由用户自定义。 + +```py +def default_node_attr(dot, nid, symbol, ann): + dot.node(repr(nid), dot_escape(unicode_escape(symbol))) + +``` + +与节点相似,边缘也可能需要修改。 我们将`default_edge_attr()`定义为可以由用户自定义的帮助程序。 + +```py +def default_edge_attr(dot, start_node, stop_node): + dot.edge(repr(start_node), repr(stop_node)) + +``` + +在可视化一棵树时,有时可能希望更改树的外观。 例如,如果树是从左到右而不是从上到下布置的,则有时更容易查看树。 为此,我们定义了另一个帮助程序`default_graph_attr()`。 + +```py +def default_graph_attr(dot): + dot.attr('node', shape='plain') + +``` + +最后,我们定义了一种方法`display_tree()`,该方法接受这四个函数`extract_node()`,`default_edge_attr()`,`default_node_attr()`和`default_graph_attr()`并使用它们显示树。 + +```py +def display_tree(derivation_tree, + log=False, + extract_node=extract_node, + node_attr=default_node_attr, + edge_attr=default_edge_attr, + graph_attr=default_graph_attr): + + # If we import display_tree, we also have to import its functions + from [graphviz](https://docs.python.org/3/library/graphviz.html) import Digraph + + counter = 0 + + def traverse_tree(dot, tree, id=0): + (symbol, children, annotation) = extract_node(tree, id) + node_attr(dot, id, symbol, annotation) + + if children: + for child in children: + nonlocal counter + counter += 1 + child_id = counter + edge_attr(dot, id, child_id) + traverse_tree(dot, child, child_id) + + dot = Digraph(comment="Derivation Tree") + graph_attr(dot) + traverse_tree(dot, derivation_tree) + if log: + print(dot) + return dot + +``` + +这是我们的树可视化为: + +```py +display_tree(derivation_tree) + +``` + +%3 0 1 0->1 2 1->2 3 + 1->3 4 1->4 + +假设我们要自定义输出,并在其中注释某些节点和边。 这是一种方法`display_annotated_tree()`,它显示带注释的树结构,并从左到右放置图形。 + +```py +def display_annotated_tree(tree, a_nodes, a_edges, log=False): + def graph_attr(dot): + dot.attr('node', shape='plain') + dot.graph_attr['rankdir'] = 'LR' + + def annotate_node(dot, nid, symbol, ann): + if nid in a_nodes: + dot.node(repr(nid), "%s (%s)" % (dot_escape(unicode_escape(symbol)), a_nodes[nid])) + else: + dot.node(repr(nid), dot_escape(unicode_escape(symbol))) + + def annotate_edge(dot, start_node, stop_node): + if (start_node, stop_node) in a_edges: + dot.edge(repr(start_node), repr(stop_node), + a_edges[(start_node, stop_node)]) + else: + dot.edge(repr(start_node), repr(stop_node)) + + return display_tree(tree, log=log, + node_attr=annotate_node, + edge_attr=annotate_edge, + graph_attr=graph_attr) + +``` + +```py +display_annotated_tree(derivation_tree, {3: 'plus'}, {(1, 3): 'op'}, log=False) + +``` + +%3 0 1 0->1 2 1->2 3 +  (plus) 1->3 op 4 1->4 + +如果要查看树中的所有叶子节点,可以使用以下`all_terminals()`函数: + +```py +def all_terminals(tree): + (symbol, children) = tree + if children is None: + # This is a nonterminal symbol not expanded yet + return symbol + + if len(children) == 0: + # This is a terminal symbol + return symbol + + # This is an expanded symbol: + # Concatenate all terminal symbols from all children + return ''.join([all_terminals(c) for c in children]) + +``` + +```py +all_terminals(derivation_tree) + +``` + +```py +' + ' + +``` + +`all_terminals()`函数返回所有叶节点的字符串表示形式。 但是,其中一些叶节点可能是由于非终结符派生空字符串所致。 对于这些,我们要返回空字符串。 因此,我们定义了一个新函数`tree_to_string()`,可从树状结构中检索出原始字符串。 + +```py +def tree_to_string(tree): + symbol, children, *_ = tree + if children: + return ''.join(tree_to_string(c) for c in children) + else: + return '' if is_nonterminal(symbol) else symbol + +``` + +```py +tree_to_string(derivation_tree) + +``` + +```py +' + ' + +``` + +## 扩展节点 + +现在让我们开发一种算法,该算法采用带有未扩展符号的树(例如,上面的`derivation_tree`),然后依次扩展所有这些符号。 与早期的模糊测试类似,我们创建了`Fuzzer`的特殊子类-在本例中为`GrammarFuzzer`。 `GrammarFuzzer`得到一个语法和一个开始符号; 其他参数将在以后用于进一步控制创建并支持调试。 + +```py +from [Fuzzer](Fuzzer.html) import Fuzzer + +``` + +```py +class GrammarFuzzer(Fuzzer): + def __init__(self, grammar, start_symbol=START_SYMBOL, + min_nonterminals=0, max_nonterminals=10, disp=False, log=False): + """Produce strings from `grammar`, starting with `start_symbol`. + If `min_nonterminals` or `max_nonterminals` is given, use them as limits + for the number of nonterminals produced. + If `disp` is set, display the intermediate derivation trees. + If `log` is set, show intermediate steps as text on standard output.""" + + self.grammar = grammar + self.start_symbol = start_symbol + self.min_nonterminals = min_nonterminals + self.max_nonterminals = max_nonterminals + self.disp = disp + self.log = log + self.check_grammar() + +``` + +在下文中,我们将使用已经为[引入`MutationFuzzer`类](MutationFuzzer.html)的技巧,向`GrammarFuzzer`添加更多方法。 构造 + +```py +class GrammarFuzzer(GrammarFuzzer): + def new_method(self, args): + pass + +``` + +允许我们向`GrammarFuzzer`类添加新方法`new_method()`。 (实际上,我们得到了一个新的`GrammarFuzzer`类,该类扩展了旧的类,但是对于我们所有的目的,这都没有关系。) + +使用此技巧,让我们定义一个辅助方法`check_grammar()`,该方法检查给定语法的一致性: + +```py +class GrammarFuzzer(GrammarFuzzer): + def check_grammar(self): + assert self.start_symbol in self.grammar + assert is_valid_grammar( + self.grammar, + start_symbol=self.start_symbol, + supported_opts=self.supported_opts()) + + def supported_opts(self): + return set() + +``` + +现在让我们定义一个辅助方法`init_tree()`,该方法仅使用开始符号来构建树: + +```py +class GrammarFuzzer(GrammarFuzzer): + def init_tree(self): + return (self.start_symbol, None) + +``` + +```py +f = GrammarFuzzer(EXPR_GRAMMAR) +display_tree(f.init_tree()) + +``` + +%3 0 + +接下来,我们将需要一个辅助函数`expansion_to_children()`,它将展开字符串并将其分解为派生树列表-字符串中的每个符号(末端或非末端)一个。 它使用`re.split()`方法将扩展字符串拆分为子节点列表: + +```py +def expansion_to_children(expansion): + # print("Converting " + repr(expansion)) + # strings contains all substrings -- both terminals and nonterminals such + # that ''.join(strings) == expansion + + expansion = exp_string(expansion) + assert isinstance(expansion, str) + + if expansion == "": # Special case: epsilon expansion + return [("", [])] + + strings = re.split(RE_NONTERMINAL, expansion) + return [(s, None) if is_nonterminal(s) else (s, []) + for s in strings if len(s) > 0] + +``` + +```py +expansion_to_children(" + ") + +``` + +```py +[('', None), (' + ', []), ('', None)] + +``` + +*ε扩展*的情况,即像` ::=`一样扩展为空字符串需要特殊处理: + +```py +expansion_to_children("") + +``` + +```py +[('', [])] + +``` + +就像有关语法的[一章中的`nonterminals()`一样,我们提供了将来的扩展,使扩展成为具有额外数​​据的元组(将被忽略)。](Grammars.html) + +```py +expansion_to_children(("+", ["extra_data"])) + +``` + +```py +[('+', []), ('', None)] + +``` + +我们在`GrammarFuzzer`中将这种帮助器实现为一种方法,以便它可以被子类重载: + +```py +class GrammarFuzzer(GrammarFuzzer): + def expansion_to_children(self, expansion): + return expansion_to_children(expansion) + +``` + +这样,我们现在可以在树中获取一些未扩展的节点,选择随机扩展,然后返回新树。 这就是方法`expand_node_randomly()`的工作,它使用辅助函数`choose_node_expansion()`从可能的子级数组中随机选择一个索引。 (`choose_node_expansion()`可以在子类中重载。) + +```py +import [random](https://docs.python.org/3/library/random.html) + +``` + +```py +class GrammarFuzzer(GrammarFuzzer): + def choose_node_expansion(self, node, possible_children): + """Return index of expansion in `possible_children` to be selected. Defaults to random.""" + return random.randrange(0, len(possible_children)) + + def expand_node_randomly(self, node): + (symbol, children) = node + assert children is None + + if self.log: + print("Expanding", all_terminals(node), "randomly") + + # Fetch the possible expansions from grammar... + expansions = self.grammar[symbol] + possible_children = [self.expansion_to_children( + expansion) for expansion in expansions] + + # ... and select a random expansion + index = self.choose_node_expansion(node, possible_children) + chosen_children = possible_children[index] + + # Process children (for subclasses) + chosen_children = self.process_chosen_children(chosen_children, + expansions[index]) + + # Return with new children + return (symbol, chosen_children) + +``` + +通用的`expand_node()`方法可以稍后用于选择不同的扩展策略; 截至目前,它仅使用`expand_node_randomly()`。 + +```py +class GrammarFuzzer(GrammarFuzzer): + def expand_node(self, node): + return self.expand_node_randomly(node) + +``` + +辅助功能`process_chosen_children()`不执行任何操作。 子类可以重载它,以处理选定的子项。 + +```py +class GrammarFuzzer(GrammarFuzzer): + def process_chosen_children(self, chosen_children, expansion): + """Process children after selection. By default, does nothing.""" + return chosen_children + +``` + +`expand_node_randomly()`的工作方式如下: + +```py +f = GrammarFuzzer(EXPR_GRAMMAR, log=True) + +print("Before:") +tree = ("", None) +display_tree(tree) + +``` + +```py +Before: + +``` + +%3 0 + +```py +print("After:") +tree = f.expand_node_randomly(tree) +display_tree(tree) + +``` + +```py +After: +Expanding randomly + +``` + +%3 0 1 0->1 2 0->2 + +## 展开树 + +现在让我们将上述节点扩展应用于树中的某个节点。 为此,我们首先需要在树中搜索未扩展的节点。 `possible_expansions()`计算一棵树中有多少个未扩展符号: + +```py +class GrammarFuzzer(GrammarFuzzer): + def possible_expansions(self, node): + (symbol, children) = node + if children is None: + return 1 + + return sum(self.possible_expansions(c) for c in children) + +``` + +```py +f = GrammarFuzzer(EXPR_GRAMMAR) +print(f.possible_expansions(derivation_tree)) + +``` + +```py +2 + +``` + +如果树有任何未扩展的节点,则方法`any_possible_expansions()`返回True。 + +```py +class GrammarFuzzer(GrammarFuzzer): + def any_possible_expansions(self, node): + (symbol, children) = node + if children is None: + return True + + return any(self.any_possible_expansions(c) for c in children) + +``` + +```py +f = GrammarFuzzer(EXPR_GRAMMAR) +f.any_possible_expansions(derivation_tree) + +``` + +```py +True + +``` + +`expand_tree_once()`是我们的树扩展算法的核心方法。 它首先检查它当前是否正在不扩展的情况下应用于非终端符号。 如果是这样,它将如上所述调用`expand_node()`。 + +如果该节点已经展开(即具有子节点),则检查仍具有未扩展符号的子节点的子集,随机选择其中一个,然后将其自身递归应用于该子节点。 + +`expand_tree_once()`方法在位置处替换子级*,这意味着它实际上使作为参数传递的树发生了变异,而不是返回新树。 这种原位突变使此功能特别有效。 同样,我们使用辅助方法(`choose_tree_expansion()`)从可以扩展的子级列表中返回所选索引。* + +```py +class GrammarFuzzer(GrammarFuzzer): + def choose_tree_expansion(self, tree, children): + """Return index of subtree in `children` to be selected for expansion. Defaults to random.""" + return random.randrange(0, len(children)) + + def expand_tree_once(self, tree): + """Choose an unexpanded symbol in tree; expand it. Can be overloaded in subclasses.""" + (symbol, children) = tree + if children is None: + # Expand this node + return self.expand_node(tree) + + # Find all children with possible expansions + expandable_children = [ + c for c in children if self.any_possible_expansions(c)] + + # `index_map` translates an index in `expandable_children` + # back into the original index in `children` + index_map = [i for (i, c) in enumerate(children) + if c in expandable_children] + + # Select a random child + child_to_be_expanded = \ + self.choose_tree_expansion(tree, expandable_children) + + # Expand in place + children[index_map[child_to_be_expanded]] = \ + self.expand_tree_once(expandable_children[child_to_be_expanded]) + + return tree + +``` + +让我们使用它,从上方扩展两次衍生树。 + +```py +derivation_tree = ("", + [("", + [("", None), + (" + ", []), + ("", None)] + )]) +display_tree(derivation_tree) + +``` + +%3 0 1 0->1 2 1->2 3 + 1->3 4 1->4 + +```py +f = GrammarFuzzer(EXPR_GRAMMAR, log=True) +derivation_tree = f.expand_tree_once(derivation_tree) +display_tree(derivation_tree) + +``` + +```py +Expanding randomly + +``` + +%3 0 1 0->1 2 1->2 3 + 1->3 4 1->4 5 4->5 6 / 4->6 7 4->7 + +```py +derivation_tree = f.expand_tree_once(derivation_tree) +display_tree(derivation_tree) + +``` + +```py +Expanding randomly + +``` + +%3 0 1 0->1 2 1->2 6 + 1->6 7 1->7 3 2->3 4 + 2->4 5 2->5 8 7->8 9 / 7->9 10 7->10 + +我们看到,每一步都会扩展一个符号。 现在所有要做的就是一次又一次地应用它,使树越来越远。 + +## 关闭扩展 + +使用`expand_tree_once()`,我们可以继续扩展树–但是我们如何实际上停止? Luke在[ [Luke *等人*等,2000。](https://doi.org/10.1109/4235.873237)]中引入的关键思想是,在将派生树膨胀到某个最大大小之后,我们*只想应用 将树的大小增加最小的扩展*。 例如,对于``,我们希望将其扩展为``,因为这不会带来进一步的递归(和潜在的规模膨胀)。 对于``,同样,最好将其扩展为``,因为与``相比,树的大小增加较少。 + +为了确定扩展符号的*成本*,我们引入了两个相互依赖的函数: + +* `symbol_cost()`返回符号的所有扩展的最低成本,使用`expansion_cost()`计算每次扩展的成本。 +* `expansion_cost()`返回`expansions`中所有扩展的总和。 如果遍历过程中再次遇到非终结符,则扩展成本为$ \ infty $,表示(可能是无限的)递归。 + +```py +class GrammarFuzzer(GrammarFuzzer): + def symbol_cost(self, symbol, seen=set()): + expansions = self.grammar[symbol] + return min(self.expansion_cost(e, seen | {symbol}) for e in expansions) + + def expansion_cost(self, expansion, seen=set()): + symbols = nonterminals(expansion) + if len(symbols) == 0: + return 1 # no symbol + + if any(s in seen for s in symbols): + return float('inf') + + # the value of a expansion is the sum of all expandable variables + # inside + 1 + return sum(self.symbol_cost(s, seen) for s in symbols) + 1 + +``` + +这里有两个示例:扩展数字的最低成本为1,因为我们必须从其扩展之一中进行选择。 + +```py +f = GrammarFuzzer(EXPR_GRAMMAR) +assert f.symbol_cost("") == 1 + +``` + +但是,扩展``的最小成本为5,因为这是所需的最小扩展数。 (`` $ \ rightarrow $ `` $ \ rightarrow $ `` $ \ rightarrow $ `` $ \ rightarrow $ `` $ \ rightarrow $ 1) + +```py +assert f.symbol_cost("") == 5 + +``` + +现在是`expand_node()`的变体,它考虑了上述成本。 它确定所有子项的最低成本`cost`,然后使用`choose`功能从列表中选择一个子项,默认情况下这是最低成本。 如果多个孩子都具有相同的最低费用,它将在这些孩子之间随机选择。 + +```py +class GrammarFuzzer(GrammarFuzzer): + def expand_node_by_cost(self, node, choose=min): + (symbol, children) = node + assert children is None + + # Fetch the possible expansions from grammar... + expansions = self.grammar[symbol] + + possible_children_with_cost = [(self.expansion_to_children(expansion), + self.expansion_cost( + expansion, {symbol}), + expansion) + for expansion in expansions] + + costs = [cost for (child, cost, expansion) + in possible_children_with_cost] + chosen_cost = choose(costs) + children_with_chosen_cost = [child for (child, child_cost, _) in possible_children_with_cost + if child_cost == chosen_cost] + expansion_with_chosen_cost = [expansion for (_, child_cost, expansion) in possible_children_with_cost + if child_cost == chosen_cost] + + index = self.choose_node_expansion(node, children_with_chosen_cost) + + chosen_children = children_with_chosen_cost[index] + chosen_expansion = expansion_with_chosen_cost[index] + chosen_children = self.process_chosen_children( + chosen_children, chosen_expansion) + + # Return with a new list + return (symbol, chosen_children) + +``` + +快捷方式`expand_node_min_cost()`通过`min()`作为`choose`功能,从而使其以最小的成本扩展了节点。 + +```py +class GrammarFuzzer(GrammarFuzzer): + def expand_node_min_cost(self, node): + if self.log: + print("Expanding", all_terminals(node), "at minimum cost") + + return self.expand_node_by_cost(node, min) + +``` + +现在,我们可以使用此函数来关闭派生树的扩展,将`expand_tree_once()`与上面的`expand_node_min_cost()`用作扩展函数。 + +```py +class GrammarFuzzer(GrammarFuzzer): + def expand_node(self, node): + return self.expand_node_min_cost(node) + +``` + +```py +f = GrammarFuzzer(EXPR_GRAMMAR, log=True) +display_tree(derivation_tree) + +``` + +%3 0 1 0->1 2 1->2 6 + 1->6 7 1->7 3 2->3 4 + 2->4 5 2->5 8 7->8 9 / 7->9 10 7->10 + +```py +if f.any_possible_expansions(derivation_tree): + derivation_tree = f.expand_tree_once(derivation_tree) + display_tree(derivation_tree) + +``` + +```py +Expanding at minimum cost + +``` + +```py +if f.any_possible_expansions(derivation_tree): + derivation_tree = f.expand_tree_once(derivation_tree) + display_tree(derivation_tree) + +``` + +```py +Expanding at minimum cost + +``` + +```py +if f.any_possible_expansions(derivation_tree): + derivation_tree = f.expand_tree_once(derivation_tree) + display_tree(derivation_tree) + +``` + +```py +Expanding at minimum cost + +``` + +我们一直在扩展,直到所有非终端都扩展了。 + +```py +while f.any_possible_expansions(derivation_tree): + derivation_tree = f.expand_tree_once(derivation_tree) + +``` + +```py +Expanding at minimum cost +Expanding at minimum cost +Expanding at minimum cost +Expanding at minimum cost +Expanding at minimum cost +Expanding at minimum cost +Expanding at minimum cost +Expanding at minimum cost +Expanding at minimum cost +Expanding at minimum cost +Expanding at minimum cost +Expanding at minimum cost +Expanding at minimum cost + +``` + +这是最后一棵树: + +```py +display_tree(derivation_tree) + +``` + +%3 0 1 0->1 2 1->2 15 + 1->15 16 1->16 3 2->3 8 + 2->8 9 2->9 4 3->4 5 4->5 6 5->6 7 7 6->7 10 9->10 11 10->11 12 11->12 13 12->13 14 2 13->14 17 16->17 21 / 16->21 22 16->22 18 17->18 19 18->19 20 8 19->20 23 22->23 24 23->24 25 24->25 26 1 25->26 + +我们看到,在每个步骤中,`expand_node_min_cost()`选择一个不增加符号数量的扩展,最终关闭所有打开的扩展。 + +## 节点膨胀 + +尤其是在扩展开始时,我们可能有兴趣让*尽可能多的节点* –也就是说,我们希望扩展使*的更多*非终端扩展。 实际上,这与`expand_node_min_cost()`所提供的完全相反,我们可以实现一种`expand_node_max_cost()`方法,该方法将始终在*成本最高*的节点之间进行选择: + +```py +class GrammarFuzzer(GrammarFuzzer): + def expand_node_max_cost(self, node): + if self.log: + print("Expanding", all_terminals(node), "at maximum cost") + + return self.expand_node_by_cost(node, max) + +``` + +为了说明`expand_node_max_cost()`,我们可以再次重新定义`expand_node()`以使用它,然后使用`expand_tree_once()`显示一些扩展步骤: + +```py +class GrammarFuzzer(GrammarFuzzer): + def expand_node(self, node): + return self.expand_node_max_cost(node) + +``` + +```py +derivation_tree = ("", + [("", + [("", None), + (" + ", []), + ("", None)] + )]) + +``` + +```py +f = GrammarFuzzer(EXPR_GRAMMAR, log=True) +display_tree(derivation_tree) + +``` + +%3 0 1 0->1 2 1->2 3 + 1->3 4 1->4 + +```py +if f.any_possible_expansions(derivation_tree): + derivation_tree = f.expand_tree_once(derivation_tree) + display_tree(derivation_tree) + +``` + +```py +Expanding at maximum cost + +``` + +```py +if f.any_possible_expansions(derivation_tree): + derivation_tree = f.expand_tree_once(derivation_tree) + display_tree(derivation_tree) + +``` + +```py +Expanding at maximum cost + +``` + +```py +if f.any_possible_expansions(derivation_tree): + derivation_tree = f.expand_tree_once(derivation_tree) + display_tree(derivation_tree) + +``` + +```py +Expanding at maximum cost + +``` + +我们看到,每一步,非末端的数量都会增加。 显然,我们必须对此数字进行限制。 + +## 三个扩展阶段 + +现在,我们可以将所有三个阶段放到一个函数`expand_tree()`中,该函数的工作方式如下: + +1. **最大成本扩展。** 使用扩展以最大的代价扩展树,直到我们至少拥有`min_nonterminals`个非终结点。 通过将`min_nonterminals`设置为零,可以轻松跳过此阶段。 +2. **随机扩展。** 继续随机扩展树,直到我们到达`max_nonterminals`非终结点为止。 +3. **最低成本扩展。** 以最小的成本关闭扩展。 + +我们通过使`expand_node`引用要应用的扩展方法来实现这三个阶段。 通过先将`expand_node`(方法参考)设置为`expand_node_max_cost`(即,调用`expand_node()`会调用`expand_node_max_cost()`),然后是`expand_node_randomly`,最后是`expand_node_min_cost`来控制。 在前两个阶段,我们还分别设置了`min_nonterminals`和`max_nonterminals`的最大限制。 + +```py +class GrammarFuzzer(GrammarFuzzer): + def log_tree(self, tree): + """Output a tree if self.log is set; if self.display is also set, show the tree structure""" + if self.log: + print("Tree:", all_terminals(tree)) + if self.disp: + display(display_tree(tree)) + # print(self.possible_expansions(tree), "possible expansion(s) left") + + def expand_tree_with_strategy(self, tree, expand_node_method, limit=None): + """Expand tree using `expand_node_method` as node expansion function + until the number of possible expansions reaches `limit`.""" + self.expand_node = expand_node_method + while ((limit is None + or self.possible_expansions(tree) < limit) + and self.any_possible_expansions(tree)): + tree = self.expand_tree_once(tree) + self.log_tree(tree) + return tree + + def expand_tree(self, tree): + """Expand `tree` in a three-phase strategy until all expansions are complete.""" + self.log_tree(tree) + tree = self.expand_tree_with_strategy( + tree, self.expand_node_max_cost, self.min_nonterminals) + tree = self.expand_tree_with_strategy( + tree, self.expand_node_randomly, self.max_nonterminals) + tree = self.expand_tree_with_strategy( + tree, self.expand_node_min_cost) + + assert self.possible_expansions(tree) == 0 + + return tree + +``` + +让我们在我们的示例中尝试一下。 + +```py +derivation_tree = ("", + [("", + [("", None), + (" + ", []), + ("", None)] + )]) + +f = GrammarFuzzer( + EXPR_GRAMMAR, + min_nonterminals=3, + max_nonterminals=5, + log=True) +derivation_tree = f.expand_tree(derivation_tree) + +``` + +```py +Tree: + +Expanding at maximum cost +Tree: + + +Expanding randomly +Tree: + + +Expanding randomly +Tree: + + +Expanding randomly +Tree: + - + +Expanding randomly +Tree: + * - + +Expanding at minimum cost +Tree: + * - + +Expanding at minimum cost +Tree: + * - + +Expanding at minimum cost +Tree: + * - + +Expanding at minimum cost +Tree: + * - + +Expanding at minimum cost +Tree: + * - + 1 +Expanding at minimum cost +Tree: + * - + 1 +Expanding at minimum cost +Tree: + * - + 1 +Expanding at minimum cost +Tree: 8 + * - + 1 +Expanding at minimum cost +Tree: 8 + * - + 1 +Expanding at minimum cost +Tree: 8 + * - + 1 +Expanding at minimum cost +Tree: 8 + * - + 1 +Expanding at minimum cost +Tree: 8 + * - + 1 +Expanding at minimum cost +Tree: 8 + * - + 1 +Expanding at minimum cost +Tree: 8 + * - + 1 +Expanding at minimum cost +Tree: 8 + * - + 1 +Expanding at minimum cost +Tree: 8 + 8 * - + 1 +Expanding at minimum cost +Tree: 8 + 8 * - 1 + 1 +Expanding at minimum cost +Tree: 8 + 8 * 2 - 1 + 1 + +``` + +```py +display_tree(derivation_tree) + +``` + +%3 0 1 0->1 2 1->2 28 + 1->28 29 1->29 3 2->3 8 + 2->8 9 2->9 4 3->4 5 4->5 6 5->6 7 8 6->7 10 9->10 21 - 9->21 22 9->22 11 10->11 15 * 10->15 16 10->16 12 11->12 13 12->13 14 8 13->14 17 16->17 18 17->18 19 18->19 20 2 19->20 23 22->23 24 23->24 25 24->25 26 25->26 27 1 26->27 30 29->30 31 30->31 32 31->32 33 1 32->33 + +```py +all_terminals(derivation_tree) + +``` + +```py +'8 + 8 * 2 - 1 + 1' + +``` + +## 全部放在一起 + +基于此,我们现在可以定义一个函数`fuzz()`,它像`simple_grammar_fuzzer()`一样简单地采用语法并从中产生一个字符串。 因此,它不再暴露出派生树的复杂性。 + +```py +class GrammarFuzzer(GrammarFuzzer): + def fuzz_tree(self): + # Create an initial derivation tree + tree = self.init_tree() + # print(tree) + + # Expand all nonterminals + tree = self.expand_tree(tree) + if self.log: + print(repr(all_terminals(tree))) + if self.disp: + display(display_tree(tree)) + return tree + + def fuzz(self): + self.derivation_tree = self.fuzz_tree() + return all_terminals(self.derivation_tree) + +``` + +现在,我们可以将其应用到所有定义的语法中(并可视化派生树) + +```py +f = GrammarFuzzer(EXPR_GRAMMAR) +f.fuzz() + +``` + +```py +'(4 + 8) + -4 / 6 * +3 / 3 / 0 + 8 * 5 + 8' + +``` + +调用`fuzz()`之后,可以在`derivation_tree`属性中访问生成的派生树: + +```py +display_tree(f.derivation_tree) + +``` + +%3 0 1 0->1 2 1->2 19 + 1->19 20 1->20 3 2->3 4 ( 3->4 5 3->5 18 ) 3->18 6 5->6 11 + 5->11 12 5->12 7 6->7 8 7->8 9 8->9 10 4 9->10 13 12->13 14 13->14 15 14->15 16 15->16 17 8 16->17 21 20->21 54 + 20->54 55 20->55 22 21->22 28 / 21->28 29 21->29 23 - 22->23 24 22->24 25 24->25 26 25->26 27 4 26->27 30 29->30 34 * 29->34 35 29->35 31 30->31 32 31->32 33 6 32->33 36 35->36 42 / 35->42 43 35->43 37 + 36->37 38 36->38 39 38->39 40 39->40 41 3 40->41 44 43->44 48 / 43->48 49 43->49 45 44->45 46 45->46 47 3 46->47 50 49->50 51 50->51 52 51->52 53 0 52->53 56 55->56 67 + 55->67 68 55->68 57 56->57 61 * 56->61 62 56->62 58 57->58 59 58->59 60 8 59->60 63 62->63 64 63->64 65 64->65 66 5 65->66 69 68->69 70 69->70 71 70->71 72 71->72 73 8 72->73 + +让我们尝试其他语法格式的语法模糊器(及其树)。 + +```py +f = GrammarFuzzer(URL_GRAMMAR) +f.fuzz() + +``` + +```py +'https://user:password@www.google.com?def=6' + +``` + +```py +display_tree(f.derivation_tree) + +``` + +%3 0 1 0->1 2 1->2 4 :// 1->4 5 1->5 11 1->11 13 1->13 3 https 2->3 6 5->6 8 @ 5->8 9 5->9 7 user:password 6->7 10 www.google.com 9->10 12 11->12 14 ? 13->14 15 13->15 16 15->16 17 16->17 19 = 16->19 20 16->20 18 def 17->18 21 20->21 22 6 21->22 + +```py +f = GrammarFuzzer(CGI_GRAMMAR, min_nonterminals=3, max_nonterminals=5) +f.fuzz() + +``` + +```py +'c%2b%f2' + +``` + +```py +display_tree(f.derivation_tree) + +``` + +%3 0 1 0->1 2 1->2 5 1->5 3 2->3 4 c 3->4 6 5->6 13 5->13 7 6->7 8 % 7->8 9 7->9 11 7->11 10 2 9->10 12 b 11->12 14 13->14 15 14->15 16 % 15->16 17 15->17 19 15->19 18 f 17->18 20 2 19->20 + +我们如何对抗`simple_grammar_fuzzer()`? + +```py +trials = 50 +xs = [] +ys = [] +f = GrammarFuzzer(EXPR_GRAMMAR, max_nonterminals=20) +for i in range(trials): + with Timer() as t: + s = f.fuzz() + xs.append(len(s)) + ys.append(t.elapsed_time()) + print(i, end=" ") +print() + +``` + +```py +0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 + +``` + +```py +average_time = sum(ys) / trials +print("Average time:", average_time) + +``` + +```py +Average time: 0.05556378066001343 + +``` + +```py +%matplotlib inline + +import [matplotlib.pyplot](https://docs.python.org/3/library/matplotlib.pyplot.html) as [plt](https://docs.python.org/3/library/plt.html) +plt.scatter(xs, ys) +plt.title('Time required for generating an output') + +``` + +```py +Text(0.5,1,'Time required for generating an output') + +``` + +![](data:image/png;base64,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 +) + +我们的测试生成速度要快得多,但输入的内容也要小得多。 我们看到,使用派生树,我们可以更好地控制语法产生。 + +最后,在`simple_grammar_fuzzer()`失败的情况下`GrammarFuzzer`如何与`expr_grammar`一起工作? 它可以正常工作: + +```py +f = GrammarFuzzer(expr_grammar, max_nonterminals=10) +f.fuzz() + +``` + +```py +'((1 + 9) / 2 * 2 / 7 * 3 - 3.1 / 4) * -0' + +``` + +有了`GrammarFuzzer`,我们现在有了坚实的基础,可以在此基础上构建更多的模糊测试器,并从生成软件测试的世界中说明更多令人兴奋的概念。 其中许多甚至不需要编写语法-而是*从当前域中推断*语法,因此即使不编写语法也可以使用基于语法的模糊测试。 敬请关注! + +## 经验教训 + +* *派生树*对于表达输入结构很重要 +* *基于派生树的语法模糊处理* + 1. 比基于字符串的语法模糊测试效率更高, + 2. 更好地控制输入生成,并且 + 3. 有效避免遇到无限扩展。 + +## 后续步骤 + +恭喜你! 您已经达到本书的中心“枢纽”之一。 从这里开始,有大量基于语法模糊的技术。 + +### 扩展语法 + +首先,我们有许多技术可以使所有*以某种形式扩展*语法: + +* [解析和重新组合输入](Parser.html)允许使用现有输入,再次使用派生树 +* [覆盖语法扩展](GrammarCoverageFuzzer.html)允许*组合*覆盖 +* [将*概率*分配给各个扩展](ProbabilisticGrammarFuzzer.html)可对扩展进行额外控制 +* [将*约束*分配给单个扩展](GeneratorGrammarFuzzer.html)允许在单个规则上表达*语义约束*。 + +### 应用语法 + +其次,我们可以*在各种涉及自动学习某种形式的上下文中应用*语法: + +* [模糊化API](APIFuzzer.html) ,从API学习语法 +* [模糊化图形用户界面](WebFuzzer.html),从用户界面中学习语法以进行后续模糊测试 +* [挖掘语法](GrammarMiner.html),学习任意输入格式的语法 + +继续扩大! + +## 背景 + +派生树(通常称为*解析树*)是一种标准数据结构,*解析器*将其分解为输入。 *龙书*(也称为*编译器:原理,技术和工具*)[ [Aho *等人*,2006。](https://www.pearson.com/us/higher-education/program/Aho-Compilers-Principles-Techniques-and-Tools-2nd-Edition/PGM167067.html)讨论了解析。 进入派生树作为编译程序的一部分。 在解析和重组输入时,我们也使用派生树[。](Parser.html) + +本章的主要思想,即扩展到达到符号的限制,然后总是选择最短的路径,源于Luke [ [Luke *等人*,2000。](https://doi.org/10.1109/4235.873237)]。 + +## 练习 + +### 练习1:缓存方法结果 + +跟踪`GrammarFuzzer`表明,某些方法总是以相同的值反复调用。 + +用*缓存*设置类`FasterGrammarFuzzer`,该类检查该方法是否之前已被调用,如果已调用过,则返回先前计算的“记忆”值。 对`expansion_to_children()`执行此操作。 比较优化前后的调用次数。 + +**重要**:对于`expansion_to_children()`,请确保返回的每个列表都是一个单独的副本。 如果返回相同(缓存的)列表,则将干扰`GrammarFuzzer`的就地修改。 为此,请使用Python `copy.deepcopy()`函数。 + +[Use the notebook](https://mybinder.org/v2/gh/uds-se/fuzzingbook/master?filepath=docs/notebooks/GrammarFuzzer.ipynb#Exercises) to work on the exercises and see solutions. + +### 练习2:语法预编译 + +某些方法(例如`symbol_cost()`或`expansion_cost()`)返回的值仅取决于语法。 设置一个`EvenFasterGrammarFuzzer()`类,该类在初始化时会预先计算一次这些值,以便以后`symbol_cost()`或`expansion_cost()`的调用仅需要查找这些值。 + +[Use the notebook](https://mybinder.org/v2/gh/uds-se/fuzzingbook/master?filepath=docs/notebooks/GrammarFuzzer.ipynb#Exercises) to work on the exercises and see solutions. + +### 练习3:维护要扩展的树 + +在`expand_tree_once()`中,该算法一次又一次遍历树以查找仍可以扩展的非终结点。 通过在树中保留一系列仍可以扩展的非终止符号来加快处理速度。 + +[Use the notebook](https://mybinder.org/v2/gh/uds-se/fuzzingbook/master?filepath=docs/notebooks/GrammarFuzzer.ipynb#Exercises) to work on the exercises and see solutions. + +### 练习4:备用随机扩展 + +我们可以定义`expand_node_randomly()`使其仅调用`expand_node_by_cost(node, random.choice)`: + +```py +class ExerciseGrammarFuzzer(GrammarFuzzer): + def expand_node_randomly(self, node): + if self.log: + print("Expanding", all_terminals(node), "randomly by cost") + + return self.expand_node_by_cost(node, random.choice) + +``` + +原始实施和此替代之间有什么区别? + +[Use the notebook](https://mybinder.org/v2/gh/uds-se/fuzzingbook/master?filepath=docs/notebooks/GrammarFuzzer.ipynb#Exercises) to work on the exercises and see solutions. \ No newline at end of file diff --git a/new/fuzzing-book-zh/17.md b/new/fuzzing-book-zh/17.md new file mode 100644 index 0000000000000000000000000000000000000000..97f70c5886122e4d20c5193df5d4a7fd3e0d1304 --- /dev/null +++ b/new/fuzzing-book-zh/17.md @@ -0,0 +1,1566 @@ +# 语法覆盖率 + +> 原文: [https://www.fuzzingbook.org/html/GrammarCoverageFuzzer.html](https://www.fuzzingbook.org/html/GrammarCoverageFuzzer.html) + +[从语法](GrammarFuzzer.html)中产生输入时,规则的所有可能扩展都具有相同的可能性。 然而,对于产生一个全面的测试套件,使*品种*最大化是更有意义的-例如,不要一遍又一遍地重复相同的扩展。 在本章中,我们探索如何系统地*覆盖语法的*元素,以使我们最大限度地提高多样性并且不会漏掉单个元素。 + +**前提条件** + +* 您应该阅读语法的[一章。](Grammars.html) +* 您应该已经阅读[关于有效语法模糊](GrammarFuzzer.html)的章节。 + +## 内容提要 + +要使用本章中提供的代码来[,请编写](Importing.html) + +```py +>>> from [fuzzingbook.GrammarCoverageFuzzer](GrammarCoverageFuzzer.html) import + +``` + +然后利用以下功能。 + +本章介绍`GrammarCoverageFuzzer`,这是一种有效的语法模糊器,它是从[这一章中对有效语法模糊](GrammarFuzzer.html)进行扩展的。 它努力至少覆盖一次所有扩展。 例如,在下面的示例中,区号中的所有数字都不同,行号中的数字也都不同: + +```py +>>> from [Grammars](Grammars.html) import US_PHONE_GRAMMAR +>>> phone_fuzzer = GrammarCoverageFuzzer(US_PHONE_GRAMMAR) +>>> phone_fuzzer.fuzz() +'(521)383-0695' + +``` + +模糊处理后,`expansion_coverage()`方法返回所涵盖的语法扩展的映射。 + +```py +>>> phone_fuzzer.expansion_coverage() +{' -> ', + ' -> 0', + ' -> 1', + ' -> 2', + ' -> 3', + ' -> 5', + ' -> 6', + ' -> 8', + ' -> 9', + ' -> ', + ' -> 3', + ' -> 5', + ' -> ', + ' -> ()-', + ' -> '} + +``` + +随后对`fuzz()`的呼叫将进一步覆盖(例如,覆盖其他区号)。 重新调用`reset()`会清除记录的覆盖范围。 + +由于输入中的此类覆盖范围还会产生更高的代码覆盖范围,因此`GrammarCoverageFuzzer`是`GrammarFuzzer`的推荐扩展。 + +## 覆盖语法元素 + +测试生成的目的是涵盖程序的所有功能-当然希望包括失败的功能。 但是,此功能与输入的结构相关:如果我们无法生成某些输入元素,则也不会触发相关的代码和功能,从而减少了在其中查找错误的机会。 + +例如,请考虑语法[一章中的表达语法`EXPR_GRAMMAR`。](Grammars.html) 。 如果我们不产生负数,那么将不会测试负数。 如果我们不产生浮点数,那么将不会测试浮点数。 因此,我们的目标必须是*涵盖所有可能的扩展*。 + +最大化这种多样性的一种方法是*跟踪*在语法生成过程中发生的扩展:如果我们已经看到了某种扩展,则可以从可能的扩展集中选择其他可能的扩展候选。 在我们的表达语法中考虑以下规则: + +```py +import [fuzzingbook_utils](https://github.com/uds-se/fuzzingbook/tree/master/notebooks/fuzzingbook_utils) + +``` + +```py +from [Grammars](Grammars.html) import EXPR_GRAMMAR, CGI_GRAMMAR, URL_GRAMMAR, START_SYMBOL +from [Grammars](Grammars.html) import is_valid_grammar, extend_grammar + +``` + +```py +EXPR_GRAMMAR[""] + +``` + +```py +['+', '-', '()', '.', ''] + +``` + +假设我们已经在``的第一个扩展中产生了``。 在扩展下一个因子时,我们将标记``扩展已被覆盖,并选择尚未发现的替代方法之一,例如`-`(负数)或`.`(浮点数) 。 只有涵盖了所有替代方案后,我们才可以重新考虑以前涵盖的扩展。 + +### 跟踪语法覆盖率 + +*语法覆盖率*的概念非常容易实现。 我们引入了一个`GrammarCoverageFuzzer`类,该类跟踪当前已实现的语法覆盖率: + +```py +from [GrammarFuzzer](GrammarFuzzer.html) import GrammarFuzzer, all_terminals, nonterminals, display_tree + +``` + +```py +import [random](https://docs.python.org/3/library/random.html) + +``` + +```py +class TrackingGrammarCoverageFuzzer(GrammarFuzzer): + def __init__(self, *args, **kwargs): + # invoke superclass __init__(), passing all arguments + super().__init__(*args, **kwargs) + self.reset_coverage() + + def reset_coverage(self): + self.covered_expansions = set() + + def expansion_coverage(self): + return self.covered_expansions + +``` + +在这组`covered_expansions`中,我们使用函数`expansion_key()`为该对生成一个字符串表示形式,将各个扩展存储为成对的[*符号*,*扩展*)。 + +```py +def expansion_key(symbol, expansion): + """Convert (symbol, children) into a key. `children` can be an expansion string or a derivation tree.""" + if isinstance(expansion, tuple): + expansion = expansion[0] + if not isinstance(expansion, str): + children = expansion + expansion = all_terminals((symbol, children)) + return symbol + " -> " + expansion + +``` + +```py +expansion_key(START_SYMBOL, EXPR_GRAMMAR[START_SYMBOL][0]) + +``` + +```py +' -> ' + +``` + +除了*扩展*之外,我们还可以传递一个子代列表作为参数,然后将其自动转换为字符串。 + +```py +children = [("", None), (" + ", []), ("", None)] +expansion_key("", children) + +``` + +```py +' -> + ' + +``` + +我们可以通过列举所有扩展来计算语法中可能的扩展集。 方法`max_expansion_coverage()`从给定符号(默认:语法开始符号)开始递归遍历语法,并将所有扩展累积在`expansions`中。 使用`max_depth`参数(默认值:$ \ infty $),我们可以控制语法探索的深度。 在本章的后面,我们将需要它。 + +```py +class TrackingGrammarCoverageFuzzer(TrackingGrammarCoverageFuzzer): + def _max_expansion_coverage(self, symbol, max_depth): + if max_depth <= 0: + return set() + + self._symbols_seen.add(symbol) + + expansions = set() + for expansion in self.grammar[symbol]: + expansions.add(expansion_key(symbol, expansion)) + for nonterminal in nonterminals(expansion): + if nonterminal not in self._symbols_seen: + expansions |= self._max_expansion_coverage( + nonterminal, max_depth - 1) + + return expansions + + def max_expansion_coverage(self, symbol=None, max_depth=float('inf')): + """Return set of all expansions in a grammar starting with `symbol`""" + if symbol is None: + symbol = self.start_symbol + + self._symbols_seen = set() + cov = self._max_expansion_coverage(symbol, max_depth) + + if symbol == START_SYMBOL: + assert len(self._symbols_seen) == len(self.grammar) + + return cov + +``` + +我们可以使用`max_expansion_coverage()`来计算表达式语法中的所有扩展: + +```py +expr_fuzzer = TrackingGrammarCoverageFuzzer(EXPR_GRAMMAR) +expr_fuzzer.max_expansion_coverage() + +``` + +```py +{' -> 0', + ' -> 1', + ' -> 2', + ' -> 3', + ' -> 4', + ' -> 5', + ' -> 6', + ' -> 7', + ' -> 8', + ' -> 9', + ' -> ', + ' -> + ', + ' -> - ', + ' -> ()', + ' -> +', + ' -> -', + ' -> ', + ' -> .', + ' -> ', + ' -> ', + ' -> ', + ' -> ', + ' -> * ', + ' -> / '} + +``` + +在扩展过程中,我们可以跟踪看到的扩展。 为此,我们使用`choose_node_expansion()`方法,在[语法模糊器](GrammarFuzzer.html)中扩展单个节点。 + +```py +class TrackingGrammarCoverageFuzzer(TrackingGrammarCoverageFuzzer): + def add_coverage(self, symbol, new_children): + key = expansion_key(symbol, new_children) + + if self.log and key not in self.covered_expansions: + print("Now covered:", key) + self.covered_expansions.add(key) + + def choose_node_expansion(self, node, possible_children): + (symbol, children) = node + index = super().choose_node_expansion(node, possible_children) + self.add_coverage(symbol, possible_children[index]) + return index + +``` + +方法`missing_expansion_coverage()`是一个辅助方法,它返回仍然必须涵盖的扩展: + +```py +class TrackingGrammarCoverageFuzzer(TrackingGrammarCoverageFuzzer): + def missing_expansion_coverage(self): + return self.max_expansion_coverage() - self.expansion_coverage() + +``` + +让我们展示跟踪的工作原理。 为简单起见,让我们仅关注``扩展。 + +```py +digit_fuzzer = TrackingGrammarCoverageFuzzer( + EXPR_GRAMMAR, start_symbol="", log=True) +digit_fuzzer.fuzz() + +``` + +```py +Tree: +Expanding randomly +Now covered: -> 9 +Tree: 9 +'9' + +``` + +```py +'9' + +``` + +```py +digit_fuzzer.fuzz() + +``` + +```py +Tree: +Expanding randomly +Now covered: -> 0 +Tree: 0 +'0' + +``` + +```py +'0' + +``` + +```py +digit_fuzzer.fuzz() + +``` + +```py +Tree: +Expanding randomly +Now covered: -> 5 +Tree: 5 +'5' + +``` + +```py +'5' + +``` + +到目前为止,这是一组涵盖的扩展: + +```py +digit_fuzzer.expansion_coverage() + +``` + +```py +{' -> 0', ' -> 5', ' -> 9'} + +``` + +这是我们可以涵盖的所有扩展的集合: + +```py +digit_fuzzer.max_expansion_coverage() + +``` + +```py +{' -> 0', + ' -> 1', + ' -> 2', + ' -> 3', + ' -> 4', + ' -> 5', + ' -> 6', + ' -> 7', + ' -> 8', + ' -> 9'} + +``` + +这是缺少的覆盖范围: + +```py +digit_fuzzer.missing_expansion_coverage() + +``` + +```py +{' -> 1', + ' -> 2', + ' -> 3', + ' -> 4', + ' -> 6', + ' -> 7', + ' -> 8'} + +``` + +平均来说,在涵盖所有扩展之后,我们必须产生多少个字符? + +```py +def average_length_until_full_coverage(fuzzer): + trials = 50 + + sum = 0 + for trial in range(trials): + # print(trial, end=" ") + fuzzer.reset_coverage() + while len(fuzzer.missing_expansion_coverage()) > 0: + s = fuzzer.fuzz() + sum += len(s) + + return sum / trials + +``` + +```py +digit_fuzzer.log = False +average_length_until_full_coverage(digit_fuzzer) + +``` + +```py +28.4 + +``` + +对于完整表达式,这需要更长的时间: + +```py +expr_fuzzer = TrackingGrammarCoverageFuzzer(EXPR_GRAMMAR) +average_length_until_full_coverage(expr_fuzzer) + +``` + +```py +138.12 + +``` + +### 覆盖语法扩展 + +现在让我们不仅跟踪覆盖范围,而且实际上*产生*覆盖范围。 这个想法如下: + +1. 我们确定尚未发现的孩子(在`uncovered_children`中) +2. 如果所有孩子都被覆盖,我们将退回到原始方法(即,随机选择一个扩展) +3. 否则,我们从未发现的孩子中选择一个孩子,并将其标记为覆盖。 + +为此,我们引入了一个新的模糊器`SimpleGrammarCoverageFuzzer`,它在`choose_node_expansion()`方法中实现了该策略。 + +```py +class SimpleGrammarCoverageFuzzer(TrackingGrammarCoverageFuzzer): + def choose_node_expansion(self, node, possible_children): + # Prefer uncovered expansions + (symbol, children) = node + uncovered_children = [c for (i, c) in enumerate(possible_children) + if expansion_key(symbol, c) not in self.covered_expansions] + index_map = [i for (i, c) in enumerate(possible_children) + if c in uncovered_children] + + if len(uncovered_children) == 0: + # All expansions covered - use superclass method + return self.choose_covered_node_expansion(node, possible_children) + + # Select from uncovered nodes + index = self.choose_uncovered_node_expansion(node, uncovered_children) + + return index_map[index] + +``` + +为子类提供了两种方法`choose_covered_node_expansion()`和`choose_uncovered_node_expansion()`: + +```py +class SimpleGrammarCoverageFuzzer(SimpleGrammarCoverageFuzzer): + def choose_uncovered_node_expansion(self, node, possible_children): + return TrackingGrammarCoverageFuzzer.choose_node_expansion( + self, node, possible_children) + + def choose_covered_node_expansion(self, node, possible_children): + return TrackingGrammarCoverageFuzzer.choose_node_expansion( + self, node, possible_children) + +``` + +通过返回到目前为止涵盖的扩展集,我们可以多次调用模糊器,每次都增加语法覆盖率。 例如,使用`EXPR_GRAMMAR`语法产生数字,模糊器产生的数字比另一数字大: + +```py +f = SimpleGrammarCoverageFuzzer(EXPR_GRAMMAR, start_symbol="") +f.fuzz() + +``` + +```py +'5' + +``` + +```py +f.fuzz() + +``` + +```py +'2' + +``` + +```py +f.fuzz() + +``` + +```py +'1' + +``` + +Here's the set of covered expansions so far: + +```py +f.expansion_coverage() + +``` + +```py +{' -> 1', ' -> 2', ' -> 5'} + +``` + +让我们再模糊一些。 我们看到,每次迭代都覆盖了另一个扩展: + +```py +for i in range(7): + print(f.fuzz(), end=" ") + +``` + +```py +0 9 7 4 8 3 6 + +``` + +最后,涵盖所有扩展: + +```py +f.missing_expansion_coverage() + +``` + +```py +set() + +``` + +让我们将其应用于更复杂的语法-例如,完整表达语法。 我们看到,经过几次迭代,我们涵盖了每个数字,运算符和扩展: + +```py +f = SimpleGrammarCoverageFuzzer(EXPR_GRAMMAR) +for i in range(10): + print(f.fuzz()) + +``` + +```py ++(0.31 / (5) / 9 + 4 * 6 / 3 - 8 - 7) * -2 ++++2 / 87360 +((4) * 0 - 1) / -9.6 + 7 / 6 + 1 * 8 + 7 * 8 +++++26 / -64.45 +(8 / 1 / 6 + 9 + 7 + 8) * 1.1 / 0 * 1 +7.7 +++(3.5 / 3) - (-4 + 3) / (8 / 0) / -4 * 2 / 1 ++(90 / --(28 * 8 / 5 + 5 / (5 / 8))) - +9.36 / 2.5 * (5 * (7 * 6 * 5) / 8) +9.11 / 7.28 +1 / (9 - 5 * 6) / 6 / 7 / 7 + 1 + 1 - 7 * -3 + +``` + +同样,所有扩展都包括在内: + +```py +f.missing_expansion_coverage() + +``` + +```py +set() + +``` + +我们看到,与随机方法相比,我们的策略在实现覆盖率方面更为有效: + +```py +average_length_until_full_coverage(SimpleGrammarCoverageFuzzer(EXPR_GRAMMAR)) + +``` + +```py +52.28 + +``` + +## 远见卓识 + +为单个规则选择扩展是一个好的开始; 但是,如下面的示例所示,这还不够。 我们将覆盖范围的[一章应用于CGI语法的覆盖范围模糊器:](Coverage.html) + +```py +CGI_GRAMMAR + +``` + +```py +{'': [''], + '': ['', ''], + '': ['', '', ''], + '': ['+'], + '': ['%'], + '': ['0', + '1', + '2', + '3', + '4', + '5', + '6', + '7', + '8', + '9', + 'a', + 'b', + 'c', + 'd', + 'e', + 'f'], + '': ['0', '1', '2', '3', '4', '5', 'a', 'b', 'c', 'd', 'e', '-', '_']} + +``` + +```py +f = SimpleGrammarCoverageFuzzer(CGI_GRAMMAR) +for i in range(10): + print(f.fuzz()) + +``` + +```py +c ++%a6++ ++- ++ +++ +%18%b7 ++e +_ +d2+%e3 +%d0 + +``` + +经过10次迭代,我们仍然发现了许多扩展: + +```py +f.missing_expansion_coverage() + +``` + +```py +{' -> 2', + ' -> 4', + ' -> 5', + ' -> 9', + ' -> c', + ' -> f', + ' -> 0', + ' -> 1', + ' -> 3', + ' -> 4', + ' -> 5', + ' -> a', + ' -> b'} + +``` + +为什么会这样? 问题在于,在CGI语法中,`hexdigit`规则中要涵盖的变体数量最多。 但是,我们首先需要*达到*这种扩展。 扩展``符号时,我们可以在三种可能的扩展之间进行选择: + +```py +CGI_GRAMMAR[""] + +``` + +```py +['', '', ''] + +``` + +如果已经涵盖了所有三个扩展,则上面的`choose_node_expansion()`将随机选择一个-即使选择``时可能需要覆盖更多扩展。 + +我们需要的是一个更好的策略,如果后续有更多未发现的扩展,即使``被覆盖,也将选择``。 W. Burkhardt [ [Burkhardt *等人*,1967。](https://doi.org/10.1007/BF02235512)]首先以“最短路径选择”的名称讨论了这种策略: + +> 此版本从几种开发选择中选择了该语法单元,该语法单元从最短路径开始仍然有一个未使用的单元。 + +这是我们将在后续步骤中实现的。 + +### 确定每个符号的最大覆盖范围 + +为了解决此问题,我们引入了一个基于`SimpleGrammarCoverageFuzzer`的新类`GrammarCoverageFuzzer`,但具有更好的策略。 首先,我们需要计算从特定符号可以达到的*最大扩展集*,正如我们已经在`max_expansion_coverage()`中实现的那样。 我们的想法是稍后计算该集合的*交集*和已经涵盖的展开,以便我们可以偏爱带有非空交集的那些展开。 + +第一步-计算符号可以达到的最大扩展集-已经实现。 通过将`symbol`参数传递给`max_expansion_coverage()`,我们可以计算每个符号的可能扩展: + +```py +f = SimpleGrammarCoverageFuzzer(EXPR_GRAMMAR) +f.max_expansion_coverage('') + +``` + +```py +{' -> 0', + ' -> 1', + ' -> 2', + ' -> 3', + ' -> 4', + ' -> 5', + ' -> 6', + ' -> 7', + ' -> 8', + ' -> 9', + ' -> ', + ' -> '} + +``` + +```py +f.max_expansion_coverage('') + +``` + +```py +{' -> 0', + ' -> 1', + ' -> 2', + ' -> 3', + ' -> 4', + ' -> 5', + ' -> 6', + ' -> 7', + ' -> 8', + ' -> 9'} + +``` + +### 确定尚未发现的孩子 + +现在我们可以开始实现`GrammarCoverageFuzzer`。 计算`max_expansion_coverage()`可以让我们确定每个孩子的*失踪覆盖率*。 为此,我们*从可以获得的覆盖范围中减去*已经看到的覆盖范围(`expansion_coverage()`)。 + +```py +class GrammarCoverageFuzzer(SimpleGrammarCoverageFuzzer): + def _new_child_coverage(self, children, max_depth): + new_cov = set() + for (c_symbol, _) in children: + if c_symbol in self.grammar: + new_cov |= self.max_expansion_coverage( + c_symbol, max_depth) + return new_cov + + def new_child_coverage(self, symbol, children, max_depth=float('inf')): + """Return new coverage that would be obtained by expanding (symbol, children)""" + new_cov = self._new_child_coverage(children, max_depth) + new_cov.add(expansion_key(symbol, children)) + new_cov -= self.expansion_coverage() # -= is set subtraction + return new_cov + +``` + +让我们说明`new_child_coverage()`。 我们再次开始模糊测试,随机选择扩展。 + +```py +f = GrammarCoverageFuzzer(EXPR_GRAMMAR, start_symbol="", log=True) +f.fuzz() + +``` + +```py +Tree: +Expanding randomly +Now covered: -> 2 +Tree: 2 +'2' + +``` + +```py +'2' + +``` + +这是我们目前的报道: + +```py +f.expansion_coverage() + +``` + +```py +{' -> 2'} + +``` + +当我们查看``的单个扩展可能性时,我们看到所有扩展都提供了额外的覆盖范围,*除了*才适用。 + +```py +for expansion in EXPR_GRAMMAR[""]: + children = f.expansion_to_children(expansion) + print(expansion, f.new_child_coverage("", children)) + +``` + +```py +0 {' -> 0'} +1 {' -> 1'} +2 set() +3 {' -> 3'} +4 {' -> 4'} +5 {' -> 5'} +6 {' -> 6'} +7 {' -> 7'} +8 {' -> 8'} +9 {' -> 9'} + +``` + +这意味着无论何时选择扩展,我们都可以使用`new_child_coverage()`并在提供最大新(看不见)覆盖范围的扩展中进行选择。 + +### 自适应前瞻 + +当选择一个孩子时,我们不会期望获得最大的总体覆盖范围,因为这会导致扩展,而许多未发现的可能性将完全主导其他扩展。 相反,我们的目标是采用*广度优先*策略,首先涵盖到达给定深度的所有扩展,然后才寻找更大的深度。 方法`new_coverages()`是此策略的核心:从最大深度(`max_depth`)为零开始,它会增加深度,直到找到至少一个未发现的扩展为止。 + +```py +class GrammarCoverageFuzzer(GrammarCoverageFuzzer): + def new_coverages(self, node, possible_children): + """Return coverage to be obtained for each child at minimum depth""" + (symbol, children) = node + for max_depth in range(len(self.grammar)): + new_coverages = [ + self.new_child_coverage( + symbol, c, max_depth) for c in possible_children] + max_new_coverage = max(len(new_coverage) + for new_coverage in new_coverages) + if max_new_coverage > 0: + # Uncovered node found + return new_coverages + + # All covered + return None + +``` + +### 全部在一起 + +现在,我们可以定义`choose_node_expansion()`来使用此策略:首先,我们确定要获得的可能覆盖范围(使用`new_coverages()`); 然后,我们(随机地)选择运动范围最大的孩子。 + +```py +class GrammarCoverageFuzzer(GrammarCoverageFuzzer): + def choose_node_expansion(self, node, possible_children): + (symbol, children) = node + new_coverages = self.new_coverages(node, possible_children) + + if new_coverages is None: + # All expansions covered - use superclass method + return self.choose_covered_node_expansion(node, possible_children) + + max_new_coverage = max(len(cov) for cov in new_coverages) + + children_with_max_new_coverage = [c for (i, c) in enumerate(possible_children) + if len(new_coverages[i]) == max_new_coverage] + index_map = [i for (i, c) in enumerate(possible_children) + if len(new_coverages[i]) == max_new_coverage] + + # Select a random expansion + new_children_index = self.choose_uncovered_node_expansion( + node, children_with_max_new_coverage) + new_children = children_with_max_new_coverage[new_children_index] + + # Save the expansion as covered + key = expansion_key(symbol, new_children) + + if self.log: + print("Now covered:", key) + self.covered_expansions.add(key) + + return index_map[new_children_index] + +``` + +现在我们的模糊器已经完成。 让我们将其应用于一系列示例。 在表达式上,它可以快速覆盖所有数字和运算符: + +```py +f = GrammarCoverageFuzzer(EXPR_GRAMMAR, min_nonterminals=3) +f.fuzz() + +``` + +```py +'-4.02 / (1) * +3 + 5.9 / 7 * 8 - 6' + +``` + +```py +f.max_expansion_coverage() - f.expansion_coverage() + +``` + +```py +set() + +``` + +平均而言,它比简单策略还快: + +```py +average_length_until_full_coverage(GrammarCoverageFuzzer(EXPR_GRAMMAR)) + +``` + +```py +50.74 + +``` + +在CGI语法上,只需几次迭代即可覆盖所有字母和数字: + +```py +f = GrammarCoverageFuzzer(CGI_GRAMMAR, min_nonterminals=5) +while len(f.max_expansion_coverage() - f.expansion_coverage()) > 0: + print(f.fuzz()) + +``` + +```py +%18%d03 +%c3%94%7f+cd +%a6%b5%e2%5e%4c-54e01a2 +%5eb%7cb_ec%a0+ + +``` + +通过比较CGI语法的随机,仅扩展和深度预见策略,也可以看到这种改进: + +```py +average_length_until_full_coverage(TrackingGrammarCoverageFuzzer(CGI_GRAMMAR)) + +``` + +```py +211.34 + +``` + +```py +average_length_until_full_coverage(SimpleGrammarCoverageFuzzer(CGI_GRAMMAR)) + +``` + +```py +68.64 + +``` + +```py +average_length_until_full_coverage(GrammarCoverageFuzzer(CGI_GRAMMAR)) + +``` + +```py +40.38 + +``` + +## 上下文[的覆盖范围](#Coverage-in-Context) + +有时,语法元素会在多个地方使用。 例如,在我们的表达式语法中,``符号用于整数以及浮点数: + +```py +EXPR_GRAMMAR[""] + +``` + +```py +['+', '-', '()', '.', ''] + +``` + +如上所定义,我们的覆盖产品将确保覆盖所有``扩展(即所有``扩展)。 但是,在语法中所有出现的``时,单个数字都是*分布*。 如果我们基于覆盖的模糊器产生`1234.56`和`7890`,那么我们将完全覆盖所有数字扩展。 但是,上述``扩展中的`.`和``仅单独覆盖了一部分数字。 如果浮点数和整数具有不同的读取功能,则我们希望对所有这些功能进行全数字测试; 也许我们还希望对浮点数的整个和小数部分进行测试,每个数字都包含数字。 + +如果我们可以假定该符号的所有出现都得到相同的对待,则忽略使用符号的上下文(在我们的示例中,``上下文中``和``的各种用法)可能很有用。 但是,如果不是,则一种确保符号的出现独立于其他出现而被系统地覆盖的方法是将该出现分配给新符号,该符号是旧符号的*副本*。 我们将首先展示如何手动创建此类重复项,然后介绍一种自动执行此操作的专用功能。 + +### 手动扩展文法以覆盖上下文 + +如上所述,一种实现上下文覆盖的简单方法是通过*复制*符号及其引用的规则。 例如,我们可以将`.`替换为`.`,并赋予``和``与原始``相同的定义。 这意味着不仅将覆盖``的所有扩展,而且还将覆盖``和``的所有扩展。 + +让我们用实际代码说明这一点: + +```py +dup_expr_grammar = extend_grammar(EXPR_GRAMMAR, + { + "": ["+", "-", "()", ".", ""], + "": ["", ""], + "": ["", ""], + "": + ["0", "1", "2", "3", "4", + "5", "6", "7", "8", "9"], + "": + ["0", "1", "2", "3", "4", + "5", "6", "7", "8", "9"] + } + ) + +``` + +```py +assert is_valid_grammar(dup_expr_grammar) + +``` + +如果现在在扩展语法上运行基于覆盖率的模糊器,那么我们将覆盖正整数的所有数字,以及浮点数的整数和小数部分的所有数字: + +```py +f = GrammarCoverageFuzzer(dup_expr_grammar, start_symbol="") +for i in range(10): + print(f.fuzz()) + +``` + +```py +-(43.76 / 8.0 * 5.5 / 6.9 * 6 / 4 + +03) +(90.1 - 1 * 7.3 * 9 + 5 / 8 / 7) +2.8 +1.2 +10.4 +2 +4386 +7 +0 +08929.4302 + +``` + +我们将看到我们的“有远见”的覆盖率模糊器是如何专门生成浮点数的,该浮点数覆盖整个和小数部分的所有数字。 + +### 以编程方式扩展文法覆盖上下文 + +如果我们想增强上下文的覆盖范围,则手动调整语法可能不是理想的选择,因为对语法的任何更改都必须复制在所有重复项中。 取而代之的是,我们引入了一个将为我们做重复的功能。 + +函数`duplicate_context()`接受语法,语法中的符号以及该符号的扩展名(`None`或未提供:符号的所有扩展名),并且它将扩展名更改为引用所有原始引用规则的副本 。 这个想法是我们以 + +```py +dup_expr_grammar = extend_grammar(EXPR_GRAMMAR) +duplicate_context(dup_expr_grammar, "", ".") + +``` + +并获得与上述手动更改类似的结果。 + +这是代码: + +```py +from [Grammars](Grammars.html) import new_symbol, unreachable_nonterminals +from [GrammarFuzzer](GrammarFuzzer.html) import expansion_to_children + +``` + +```py +def duplicate_context(grammar, symbol, expansion=None, depth=float('inf')): + """Duplicate an expansion within a grammar. + + In the given grammar, take the given expansion of the given symbol + (if expansion is omitted: all symbols), and replace it with a + new expansion referring to a duplicate of all originally referenced rules. + + If depth is given, limit duplication to `depth` references (default: unlimited) + """ + orig_grammar = extend_grammar(grammar) + _duplicate_context(grammar, orig_grammar, symbol, + expansion, depth, seen={}) + + # After duplication, we may have unreachable rules; delete them + for nonterminal in unreachable_nonterminals(grammar): + del grammar[nonterminal] + +``` + +大部分工作都在此辅助功能中进行。 附加参数`seen`跟踪已扩展的符号,并避免无限递归。 + +```py +import [copy](https://docs.python.org/3/library/copy.html) + +``` + +```py +def _duplicate_context(grammar, orig_grammar, symbol, expansion, depth, seen): + for i in range(len(grammar[symbol])): + if expansion is None or grammar[symbol][i] == expansion: + new_expansion = "" + for (s, c) in expansion_to_children(grammar[symbol][i]): + if s in seen: # Duplicated already + new_expansion += seen[s] + elif c == [] or depth == 0: # Terminal symbol or end of recursion + new_expansion += s + else: # Nonterminal symbol - duplicate + # Add new symbol with copy of rule + new_s = new_symbol(grammar, s) + grammar[new_s] = copy.deepcopy(orig_grammar[s]) + + # Duplicate its expansions recursively + # {**seen, **{s: new_s}} is seen + {s: new_s} + _duplicate_context(grammar, orig_grammar, new_s, expansion=None, + depth=depth - 1, seen={**seen, **{s: new_s}}) + new_expansion += new_s + + grammar[symbol][i] = new_expansion + +``` + +这是我们上面的`duplicate_context()`工作原理示例,现在有了结果。 我们让它在表达式语法中复制`.`扩展名,并获得一个带有`.`扩展名的新语法,其中``和``都引用原始规则的副本: + +```py +dup_expr_grammar = extend_grammar(EXPR_GRAMMAR) +duplicate_context(dup_expr_grammar, "", ".") +dup_expr_grammar + +``` + +```py +{'': [''], + '': [' + ', ' - ', ''], + '': [' * ', ' / ', ''], + '': ['+', + '-', + '()', + '.', + ''], + '': ['', ''], + '': ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'], + '': ['', ''], + '': ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'], + '': ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'], + '': ['', ''], + '': ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'], + '': ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']} + +``` + +就像上面一样,使用这样的语法进行覆盖模糊处理现在将覆盖许多上下文中的数字。 准确地说,有五个上下文:正则整数,以及浮点数的一位和多位整数和小数部分。 + +```py +f = GrammarCoverageFuzzer(dup_expr_grammar, start_symbol="") +for i in range(10): + print(f.fuzz()) + +``` + +```py +(57.5) +2 ++-(1 / 3 + 6 / 0 - 7 * 59 * 3 + 8 * 4) +374.88 +5.709 +0.93 +01.1 +892.27 +219.50 +6.636 + +``` + +`depth`参数控制复制应进行的深度。 将`depth`设置为1只会复制下一条规则: + +```py +dup_expr_grammar = extend_grammar(EXPR_GRAMMAR) +duplicate_context(dup_expr_grammar, "", ".", depth=1) +dup_expr_grammar + +``` + +```py +{'': [''], + '': [' + ', ' - ', ''], + '': [' * ', ' / ', ''], + '': ['+', + '-', + '()', + '.', + ''], + '': ['', ''], + '': ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'], + '': ['', ''], + '': ['', '']} + +``` + +```py +assert is_valid_grammar(dup_expr_grammar) + +``` + +默认情况下,`depth`设置为$ \ infty $,表示无限复制。 真正的无界重复可能会导致诸如`EXPR_GRAMMAR`之类的递归语法出现问题,因此一旦将`duplicate_context()`设置为不再重复的符号。 尽管如此,如果我们将其应用于所有 ``扩展的*重复项,我们将获得不少于296条规则的语法:* + +```py +dup_expr_grammar = extend_grammar(EXPR_GRAMMAR) +duplicate_context(dup_expr_grammar, "") + +``` + +```py +assert is_valid_grammar(dup_expr_grammar) +len(dup_expr_grammar) + +``` + +```py +292 + +``` + +这使我们可以扩展将近2000个范围: + +```py +f = GrammarCoverageFuzzer(dup_expr_grammar) +len(f.max_expansion_coverage()) + +``` + +```py +1981 + +``` + +再重复一遍,既使语法又提高了覆盖率要求: + +```py +dup_expr_grammar = extend_grammar(EXPR_GRAMMAR) +duplicate_context(dup_expr_grammar, "") +duplicate_context(dup_expr_grammar, "") +len(dup_expr_grammar) + +``` + +```py +594 + +``` + +```py +f = GrammarCoverageFuzzer(dup_expr_grammar) +len(f.max_expansion_coverage()) + +``` + +```py +3994 + +``` + +在这一点上,可以单独涵盖很多上下文,例如,加法中元素的乘法: + +```py +dup_expr_grammar[""] + +``` + +```py +[' + ', ' - ', ''] + +``` + +```py +dup_expr_grammar[""] + +``` + +```py +[' * ', ' / ', ''] + +``` + +```py +dup_expr_grammar[""] + +``` + +```py +['+', + '-', + '()', + '.', + ''] + +``` + +产生的语法可能不再对人类维护有用; 但是运行覆盖率驱动的模糊器(例如`GrammarCoverageFuzzer()`)将可以覆盖所有情况下的所有这些扩展。 如果您想覆盖大量上下文中的元素,那么`duplicate_context()`和覆盖率驱动的模糊器是您的朋友。 + +## 通过覆盖语法来覆盖代码 + +有无上下文:通过系统地覆盖所有输入元素,我们可以在输入中获得更大的种类-但这是否会转化为更广泛的程序行为? 毕竟,这些行为是我们要涵盖的,包括意外行为。 + +在语法中,有些元素直接对应于程序功能。 处理算术表达式的程序将具有直接由各个元素触发的功能-例如,由`+`存在触发的加法功能,由`-`存在触发的减法和由`-`存在触发的浮点算术 输入中的浮点数。 + +输入结构和功能之间的这种联系导致语法覆盖范围和代码覆盖范围之间具有很强的*相关性。 换句话说:如果我们可以实现较高的语法覆盖率,那么这也将导致较高的代码覆盖率。* + +### CGI语法 + +让我们在我们的一种语法中探索这种关系,例如,关于覆盖范围的[章中的CGI解码器。 我们计算映射`coverages`,其中在`coverages[x]` = `{y_1, y_2, ...}`中,`x`是获得的语法覆盖率,`y_n`是从第`n`次运行获得的代码覆盖率。](Coverage.html) + +我们首先计算最大覆盖率,如关于覆盖率的[一章:](Coverage.html) + +```py +from [Coverage](Coverage.html) import Coverage, cgi_decode + +``` + +```py +with Coverage() as cov_max: + cgi_decode('+') + cgi_decode('%20') + cgi_decode('abc') + try: + cgi_decode('%?a') + except: + pass + +``` + +现在,我们进行实验: + +```py +f = GrammarCoverageFuzzer(CGI_GRAMMAR, max_nonterminals=2) +coverages = {} + +trials = 100 +for trial in range(trials): + f.reset_coverage() + overall_cov = set() + max_cov = 30 + + for i in range(10): + s = f.fuzz() + with Coverage() as cov: + cgi_decode(s) + overall_cov |= cov.coverage() + + x = len(f.expansion_coverage()) * 100 / len(f.max_expansion_coverage()) + y = len(overall_cov) * 100 / len(cov_max.coverage()) + if x not in coverages: + coverages[x] = [] + coverages[x].append(y) + +``` + +我们计算`y`值的平均值: + +```py +xs = list(coverages.keys()) +ys = [sum(coverages[x]) / len(coverages[x]) for x in coverages] + +``` + +并创建散点图: + +```py +%matplotlib inline + +``` + +```py +import [matplotlib.pyplot](https://docs.python.org/3/library/matplotlib.pyplot.html) as [plt](https://docs.python.org/3/library/plt.html) + +``` + +```py +import [matplotlib.ticker](https://docs.python.org/3/library/matplotlib.ticker.html) as [mtick](https://docs.python.org/3/library/mtick.html) + +``` + +```py +ax = plt.axes(label="coverage") +ax.yaxis.set_major_formatter(mtick.PercentFormatter()) +ax.xaxis.set_major_formatter(mtick.PercentFormatter()) + +plt.xlim(0, max(xs)) +plt.ylim(0, max(ys)) + +plt.title('Coverage of cgi_decode() vs. grammar coverage') +plt.xlabel('grammar coverage (expansions)') +plt.ylabel('code coverage (lines)') +plt.scatter(xs, ys); + +``` + +![](data:image/png;base64,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 +) + +我们看到语法覆盖率越高,代码覆盖率越高。 + +这也将转化为约0.9的相关系数,表明相关性很强: + +```py +import [numpy](https://docs.python.org/3/library/numpy.html) as [np](https://docs.python.org/3/library/np.html) + +``` + +```py +np.corrcoef(xs, ys) + +``` + +```py +array([[1\. , 0.81663071], + [0.81663071, 1\. ]]) + +``` + +Spearman等级相关性也证实了这一点: + +```py +from [scipy.stats](https://docs.python.org/3/library/scipy.stats.html) import spearmanr + +``` + +```py +spearmanr(xs, ys) + +``` + +```py +SpearmanrResult(correlation=0.937547293248041, pvalue=1.0928720949027369e-09) + +``` + +### URL语法 + +让我们在URL语法上重复此实验。 除了交换语法和功能外,我们使用与上述相同的代码: + +```py +try: + from [urlparse](https://docs.python.org/3/library/urlparse.html) import urlparse # Python 2 +except ImportError: + from [urllib.parse](https://docs.python.org/3/library/urllib.parse.html) import urlparse # Python 3 + +``` + +再次,我们首先计算最大覆盖率,如关于覆盖率的[一章所述,进行有根据的猜测:](Coverage.html) + +```py +with Coverage() as cov_max: + urlparse("http://foo.bar/path") + urlparse("https://foo.bar#fragment") + urlparse("ftp://user:password@foo.bar?query=value") + urlparse("ftps://127.0.0.1/?x=1&y=2") + +``` + +这是实际的实验: + +```py +f = GrammarCoverageFuzzer(URL_GRAMMAR, max_nonterminals=2) +coverages = {} + +trials = 100 +for trial in range(trials): + f.reset_coverage() + overall_cov = set() + + for i in range(20): + s = f.fuzz() + with Coverage() as cov: + urlparse(s) + overall_cov |= cov.coverage() + + x = len(f.expansion_coverage()) * 100 / len(f.max_expansion_coverage()) + y = len(overall_cov) * 100 / len(cov_max.coverage()) + if x not in coverages: + coverages[x] = [] + coverages[x].append(y) + +``` + +```py +xs = list(coverages.keys()) +ys = [sum(coverages[x]) / len(coverages[x]) for x in coverages] + +``` + +```py +ax = plt.axes(label="coverage") +ax.yaxis.set_major_formatter(mtick.PercentFormatter()) +ax.xaxis.set_major_formatter(mtick.PercentFormatter()) + +plt.xlim(0, max(xs)) +plt.ylim(0, max(ys)) + +plt.title('Coverage of urlparse() vs. grammar coverage') +plt.xlabel('grammar coverage (expansions)') +plt.ylabel('code coverage (lines)') +plt.scatter(xs, ys); + +``` + +![](data:image/png;base64,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 +) + +在这里,我们的相关系数甚至超过了0.95: + +```py +np.corrcoef(xs, ys) + +``` + +```py +array([[1\. , 0.94110679], + [0.94110679, 1\. ]]) + +``` + +This is also confirmed by the Spearman rank correlation: + +```py +spearmanr(xs, ys) + +``` + +```py +SpearmanrResult(correlation=1.0, pvalue=0.0) + +``` + +我们得出的结论是:如果要获得较高的代码覆盖率,则首先争取较高的语法覆盖率是一个好主意。 + +### 这会一直有效吗? + +对于CGI和URL示例观察到的相关性并不适用于每个程序和每个结构。 + +#### 等效元素 + +首先,即使某个语法元素将它们视为不同的符号,它们也会被程序统一处理。 例如,在URL的主机名中,我们可以有许多不同的字符,尽管URL处理程序将它们完全相同。 同样,单个数字一旦组合成一个数字,其影响就小于数字本身的值。 因此,实现数字或字符的多样性不一定会在功能上产生很大的差异。 + +如上所述,可以通过*取决于元素的上下文*区分元素并覆盖每种上下文的替代方案来解决此问题。 关键是确定需要多样性的环境,而不是不需要多样性的环境。 + +#### 深度数据处理 + +其次,数据的处理方式可能会有很大的不同。 考虑到*媒体播放器*的输入,该输入由压缩的媒体数据组成。 在处理媒体数据时,媒体播放器将显示行为上的差异(特别是在其输出中),但是无法通过媒体数据的各个元素直接触发这些差异。 同样,在大量输入上训练的*机器学习器*通常不会通过输入的单个语法元素来控制其行为。 (嗯,可以,但是然后,我们将不需要机器学习者。)在“深度”数据处理的这些情况下,在语法上实现结构覆盖不一定会导致代码覆盖。 + +解决此问题的一种方法不仅是实现*语法*,而且实际上是*语义*变体。 在[关于带约束的模糊化](GeneratorGrammarFuzzer.html)的章节中,我们将看到如何专门生成和过滤输入值,尤其是数值。 这样的生成器也可以在上下文中应用,从而可以分别控制输入的每个方面。 同样,在以上示例中,*输入的某些*部分仍可以在结构上覆盖:*元数据*(例如媒体播放器的作者姓名或作曲者)或*配置数据[* (例如机器学习者的设置)可以并且应该被系统地涵盖; 我们将在“配置模糊” 一章中看到如何完成此操作。 + +## 经验教训 + +* 快速实现*语法覆盖率*会产生各种各样的输入。 +* 复制语法规则允许覆盖特定*上下文*中的元素。 +* 实现语法覆盖率可以帮助获得*代码覆盖率*。 + +## 后续步骤 + +从这里,您可以学习如何 + +* [使用语法覆盖系统地测试配置](ConfigurationFuzzer.html)。 + +## 背景 + +确保语法中的每个扩展至少使用一次的想法可以追溯到Burkhardt [ [Burkhardt *等人*,1967。](https://doi.org/10.1007/BF02235512)],稍后由Paul Purdom [[重新发现。 Purdom *等*,1972。](https://doi.org/10.1007/BF01932308)。 语法覆盖率和代码覆盖率之间的关系是由Nikolas Havrikov发现的,他在其博士学位论文中对此进行了探讨。 + +## 练习 + +### 练习1:测试ls + +考虑用于列出目录内容的Unix `ls`程序。 创建用于调用`ls`的语法: + +```py +LS_EBNF_GRAMMAR = { + '': ['-'], + '': ['