# 概率语法模糊化

> 原文： [https://www.fuzzingbook.org/html/ProbabilisticGrammarFuzzer.html](https://www.fuzzingbook.org/html/ProbabilisticGrammarFuzzer.html)

通过将*概率*分配给各个扩展，让我们赋予语法更多的功能。 这使我们能够控制应生产每个元素的数量，从而使我们能够将*定位为针对特定功能的*目标。 我们还将展示如何从给定的样本输入中学习此类概率，以及具体地将我们的测试引向这些样本中不常见的输入特征。

**前提条件**

*   您应该阅读语法的[一章。](Grammars.html)
*   我们的实现与[“有效语法模糊测试”](GrammarFuzzer.html) 中引入的基于语法的模糊测试挂钩。
*   为了从样本中学习概率，我们使用[解析器](Parser.html)。

## 内容提要

要使用本章中提供的代码来[，请编写](Importing.html)

```py
>>> from [fuzzingbook.ProbabilisticGrammarFuzzer](ProbabilisticGrammarFuzzer.html) import <identifier>

```

然后利用以下功能。

*概率*语法允许将各个*概率*附加到生产规则。 要将单个扩展`S`的概率设置为`X`值（0到1之间），请用一对替换

```py
(opts(S, prob=X))

```

如果我们要确保生成的90％的电话号码具有以`9`开头的区号，我们可以编写：

```py
>>> from [Grammars](Grammars.html) import US_PHONE_GRAMMAR, extend_grammar, opts
>>> PROBABILISTIC_US_PHONE_GRAMMAR = extend_grammar(US_PHONE_GRAMMAR,
>>> {
>>>       "<lead-digit>": ["2", "3", "4", "5", "6", "7", "8", 
>>>                       ("9", opts(prob=0.9))],                                              
>>> })

```

`ProbabilisticGrammarFuzzer`将提取并解释这些选项。 这是一个例子：

```py
>>> probabilistic_us_phone_fuzzer = ProbabilisticGrammarFuzzer(PROBABILISTIC_US_PHONE_GRAMMAR)
>>> [probabilistic_us_phone_fuzzer.fuzz() for i in range(5)]
['(965)906-9430',
 '(977)953-0547',
 '(973)971-2092',
 '(944)961-7546',
 '(929)918-3600']

```

如您所见，现在大多数区号都以`9`开头。

## 前导数字定律

到目前为止，在我们所有的示例中，您可能已经注意到，程序生成的输入与“自然”输入在现实生活中存在很大差异。 即使对于诸如数字之类的无害元素，也是如此-是的，到目前为止，我们实际生成的数字*与现实世界中的数字有所不同。 这是因为在现实的数字数据集中，*前导有效数字*可能很小：实际上，平均而言，前导`1`的出现次数是*的六倍* 与前导数字`8`或`9`一样多。 结果表明，该结果适用于各种数据集，包括电费，街道地址，股票价格，房价，人口数量，死亡率，河流长度，物理和数学常数（维基百科）。*

这种领先的数字定律首先由Newcomb [ [Simon Newcomb，1881\.](http://www.jstor.org/stable/2369148) ]观察，后来由Benford在[ [Frank Benford，1938\.](http://links.jstor.org/sici?sici=0003-049X%2819380331%2978%3A4%3C551%3ATLOAN%3E2.0.CO%3B2-G) ]中正式化。 让我们看一下确定数字首位的条件。 我们可以通过将数字转换为字符串并采用第一个字符来轻松计算第一个数字：

```py
def first_digit_via_string(x):
    return ord(repr(x)[0]) - ord('0')

```

```py
first_digit_via_string(2001)

```

```py
2

```

但是，要在数学上做到这一点，我们必须取其对数的小数部分，或者形式为

$$ d = 10 ^ {\ {\ log_ {10}（x）\}} $$

其中$ \ {x \} $是$ x $的小数部分（即$ \ {1.234 \} = 0.234 $）。

```py
import [math](https://docs.python.org/3/library/math.html)

```

```py
def first_digit_via_log(x):
    frac, whole = math.modf(math.log10(x))
    return int(10 ** frac)

```

```py
first_digit_via_log(2001)

```

```py
2

```

大多数“自然”出现的数字集的对数的小数部分不应有任何偏差，因此，小数部分$ \ {\ log_ {10}（x）\} $通常是均匀分布的。 但是，各个数字的小数部分不是*而是均匀分布的*。

对于以数字$ d $开头的数字，必须满足条件$ d < 10 ^ {\ {\ log_ {10}（x）\}} < d + 1 $。 因此，要从数字1开始，小数部分$ \ {\ log_ {10}（x）\} $必须在范围内

```py
(math.log10(1), math.log10(2))

```

```py
(0.0, 0.3010299956639812)

```

但是要以数字2开头，它必须在范围内

```py
(math.log10(2), math.log10(3))

```

```py
(0.3010299956639812, 0.47712125471966244)

```

这要小得多。 形式上，前导数字$ d $的概率$ P（d）$（再次假设均匀分布的小数部分）被称为本福德定律：$$ P（d）= \ log_ {10}（d +1）- \ log_ {10}（d）$$，可为我们提供：

```py
def prob_leading_digit(d):
    return math.log10(d + 1) - math.log10(d)

```

让我们计算所有数字的这些概率：

```py
digit_probs = [prob_leading_digit(d) for d in range(1, 10)]
[(d, "%.2f" % digit_probs[d - 1]) for d in range(1, 10)]

```

```py
[(1, '0.30'),
 (2, '0.18'),
 (3, '0.12'),
 (4, '0.10'),
 (5, '0.08'),
 (6, '0.07'),
 (7, '0.06'),
 (8, '0.05'),
 (9, '0.05')]

```

```py
import [matplotlib.pyplot](https://docs.python.org/3/library/matplotlib.pyplot.html) as [plt](https://docs.python.org/3/library/plt.html)

```

```py
labels = range(1, 10)
fig1, ax1 = plt.subplots()
ax1.pie(digit_probs, labels=labels, shadow=True, autopct='%1.1f%%',
        counterclock=False, startangle=90)
ax1.axis('equal');

```

![](data:image/png;base64, 
)

我们看到，领先1实际上是领先9的六倍。

本福德定律有许多应用。 最值得注意的是，它可用于检测“非自然”数字，即显然是随机创建的数字，而不是来自“自然”来源的数字。 如果您通过放入随机数来撰写科学论文和伪造数据（例如，使用我们的语法模糊器对整数使用[），则可能会违反本福德定律，这确实可以被发现。 另一方面，如果我们*希望*创建符合本福德定律的数字，我们将如何进行？ 为此，我们需要能够在语法中对*进行上述编码，例如*，以便我们可以确保在所有情况的30％中，前导数字确实是`1`。](GrammarFuzzer.html)

## 指定概率

本章的目的是将*概率*分配给语法中的各个扩展，以便我们可以表示应优先选择某些扩展替代方案。 这不仅对于生成看起来“自然”的数字很有用，而且对于*将*测试生成引向特定目标更有用。 如果您最近更改了程序中的某些代码，则可能要生成恰好执行此代码的输入。 通过提高与更改后的代码关联的输入元素的概率，您将获得更多使用更改后的代码的测试。

我们表达概率的概念是，使用语法中介绍的[中引入的注释机制，以*为*单个扩展加上诸如概率之类的属性。 为此，我们允许扩展不仅是字符串，而且是字符串和一组属性的*对*，如](Grammars.html)

```py
"<expr>":
        [("<term> + <expr>", opts(prob=0.1)),
         ("<term> - <expr>", opts(prob=0.2)),
         "<term>"]

```

在这里，`opts()`函数将使我们能够表达选择单个扩展的概率。 加法的概率为10％，减法为20％。 剩余的概率（在这种情况下为70％）平均分配给未归因的扩展（在此情况下为最后一个）。

现在，我们可以将对与`opts()`一起使用，以将概率分配给表达式语法：

```py
import [fuzzingbook_utils](https://github.com/uds-se/fuzzingbook/tree/master/notebooks/fuzzingbook_utils)

```

```py
from [GrammarFuzzer](GrammarFuzzer.html) import GrammarFuzzer, all_terminals, display_tree

```

```py
from [Grammars](Grammars.html) import is_valid_grammar, EXPR_GRAMMAR, START_SYMBOL, crange, syntax_diagram
from [Grammars](Grammars.html) import opts, exp_string, exp_opt, set_opts

```

```py
PROBABILISTIC_EXPR_GRAMMAR = {
    "<start>":
        ["<expr>"],

    "<expr>":
        [("<term> + <expr>", opts(prob=0.1)),
         ("<term> - <expr>", opts(prob=0.2)),
         "<term>"],

    "<term>":
        [("<factor> * <term>", opts(prob=0.1)),
         ("<factor> / <term>", opts(prob=0.1)),
         "<factor>"
         ],

    "<factor>":
        ["+<factor>", "-<factor>", "(<expr>)",
            "<leadinteger>", "<leadinteger>.<integer>"],

    "<leadinteger>":
        ["<leaddigit><integer>", "<leaddigit>"],

    # Benford's law: frequency distribution of leading digits
    "<leaddigit>":
        [("1", opts(prob=0.301)),
         ("2", opts(prob=0.176)),
         ("3", opts(prob=0.125)),
         ("4", opts(prob=0.097)),
         ("5", opts(prob=0.079)),
         ("6", opts(prob=0.067)),
         ("7", opts(prob=0.058)),
         ("8", opts(prob=0.051)),
         ("9", opts(prob=0.046)),
         ],

    # Remaining digits are equally distributed
    "<integer>":
        ["<digit><integer>", "<digit>"],

    "<digit>":
        ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"],
}

```

```py
assert is_valid_grammar(PROBABILISTIC_EXPR_GRAMMAR, supported_opts={'prob'})

```

这是内部语法扩展的方式：

```py
PROBABILISTIC_EXPR_GRAMMAR["<leaddigit>"]

```

```py
[('1', {'prob': 0.301}),
 ('2', {'prob': 0.176}),
 ('3', {'prob': 0.125}),
 ('4', {'prob': 0.097}),
 ('5', {'prob': 0.079}),
 ('6', {'prob': 0.067}),
 ('7', {'prob': 0.058}),
 ('8', {'prob': 0.051}),
 ('9', {'prob': 0.046})]

```

但是，我们通常通过指定的辅助函数`exp_string()`（从[Gammar 的[章中）和`exp_prob()`）访问扩展字符串和相关的概率：](Grammars.html)

```py
leaddigit_expansion = PROBABILISTIC_EXPR_GRAMMAR["<leaddigit>"][0]
leaddigit_expansion

```

```py
('1', {'prob': 0.301})

```

```py
exp_string(leaddigit_expansion)

```

```py
'1'

```

```py
def exp_prob(expansion):
    """Return the options of an expansion"""
    return exp_opt(expansion, 'prob')

```

```py
exp_prob(leaddigit_expansion)

```

```py
0.301

```

我们现有的模糊器均已设置为可使用这种方式注释的语法。 他们只是忽略所有注释。

```py
f = GrammarFuzzer(PROBABILISTIC_EXPR_GRAMMAR)
f.fuzz()

```

```py
warning: option 'prob' is not supported

```

```py
'4 + ++--7.0 - -7 - +++7.3 * (1 * 3 + 5 / 3 / 5 + 2) * 38 * (2 + 8)'

```

```py
from [GrammarCoverageFuzzer](GrammarCoverageFuzzer.html) import GrammarCoverageFuzzer  # minor dependency

```

```py
f = GrammarCoverageFuzzer(PROBABILISTIC_EXPR_GRAMMAR)
f.fuzz()

```

```py
warning: option 'prob' is not supported

```

```py
'1.30'

```

## 计算概率

让我们定义访问给定扩展概率的函数。 在这样做的同时，他们还会检查不一致之处。

### 分布概率

这是我们在没有指定概率的情况下分配扩展概率的方法。 给定扩展规则

$$ S :: = a_1 \：| \：a_2 \：| \：\ dots \：| \：a_n \：| \：u_1 \：| \：u_2 \：| \：\ dots u_m $$

其中$ n \ ge 0 $个备选方案$ a_i $，其概率$ p（a_i）$被指定为，$ m \ ge 0 $个备选方案$ u_j $，其概率$ p（u_j）$ 如果*未指定*，则“剩余”概率平均分布在所有$ u_j $上； 换一种说法，

$$ p（u_j）= \ frac {1-\ sum_ {i = 0} ^ {n} p（a_i）} {m} $$

如果未指定任何概率（$ n = 0 $），则所有扩展都具有相同的概率。

概率的总和必须为1：

$$ \ sum_ {i = 0} ^ {n} p（a_i）+ \ sum_ {j = 0} ^ {m} p（u_i）= 1 $$

我们在分配概率的同时检查这些属性。

函数`exp_probabilities()`返回规则中所有扩展到它们各自概率的映射。

```py
def exp_probabilities(expansions, nonterminal="<symbol>"):
    probabilities = [exp_prob(expansion) for expansion in expansions]
    prob_dist = prob_distribution(probabilities, nonterminal)

    prob_mapping = {}
    for i in range(len(expansions)):
        expansion = exp_string(expansions[i])
        prob_mapping[expansion] = prob_dist[i]

    return prob_mapping

```

`exp_probabilities()`的要点在`prob_distribution()`中进行处理，它进行实际的检查和计算。

```py
def prob_distribution(probabilities, nonterminal="<symbol>"):
    epsilon = 0.00001

    number_of_unspecified_probabilities = probabilities.count(None)
    if number_of_unspecified_probabilities == 0:
        assert abs(sum(probabilities) - 1.0) < epsilon, \
            nonterminal + ": sum of probabilities must be 1.0"
        return probabilities

    sum_of_specified_probabilities = 0.0
    for p in probabilities:
        if p is not None:
            sum_of_specified_probabilities += p
    assert 0 <= sum_of_specified_probabilities <= 1.0, \
        nonterminal + ": sum of specified probabilities must be between 0.0 and 1.0"

    default_probability = ((1.0 - sum_of_specified_probabilities)
                           / number_of_unspecified_probabilities)
    all_probabilities = []
    for p in probabilities:
        if p is None:
            p = default_probability
        all_probabilities.append(p)

    assert abs(sum(all_probabilities) - 1.0) < epsilon
    return all_probabilities

```

这是带注释的`<leaddigit>`元素的映射`exp_probabilities()`返回：

```py
print(exp_probabilities(PROBABILISTIC_EXPR_GRAMMAR["<leaddigit>"]))

```

```py
{'1': 0.301, '2': 0.176, '3': 0.125, '4': 0.097, '5': 0.079, '6': 0.067, '7': 0.058, '8': 0.051, '9': 0.046}

```

如果没有注释扩展，则所有扩展都具有与我们之前的语法模糊检查器相同的选择可能性。

```py
print(exp_probabilities(PROBABILISTIC_EXPR_GRAMMAR["<digit>"]))

```

```py
{'0': 0.1, '1': 0.1, '2': 0.1, '3': 0.1, '4': 0.1, '5': 0.1, '6': 0.1, '7': 0.1, '8': 0.1, '9': 0.1}

```

`exp_probabilities()`会在未注释的扩展中分配剩余的概率：

```py
exp_probabilities(PROBABILISTIC_EXPR_GRAMMAR["<expr>"])

```

```py
{'<term> + <expr>': 0.1, '<term> - <expr>': 0.2, '<term>': 0.7}

```

### 检查概率

我们可以使用`exp_probabilities()`的检查功能来检查概率语法的一致性：

```py
def is_valid_probabilistic_grammar(grammar, start_symbol=START_SYMBOL):
    if not is_valid_grammar(grammar, start_symbol):
        return False

    for nonterminal in grammar:
        expansions = grammar[nonterminal]
        prob_dist = exp_probabilities(expansions, nonterminal)

    return True

```

```py
assert is_valid_probabilistic_grammar(PROBABILISTIC_EXPR_GRAMMAR)

```

```py
assert is_valid_probabilistic_grammar(EXPR_GRAMMAR)

```

```py
from [ExpectError](ExpectError.html) import ExpectError

```

```py
with ExpectError():
    assert is_valid_probabilistic_grammar({"<start>": [("1", opts(prob=0.5))]})

```

```py
Traceback (most recent call last):
  File "<ipython-input-34-79503424e212>", line 2, in <module>
    assert is_valid_probabilistic_grammar({"<start>": [("1", opts(prob=0.5))]})
  File "<ipython-input-30-c1c25dbf0315>", line 7, in is_valid_probabilistic_grammar
    prob_dist = exp_probabilities(expansions, nonterminal)
  File "<ipython-input-25-9042a394542d>", line 3, in exp_probabilities
    prob_dist = prob_distribution(probabilities, nonterminal)
  File "<ipython-input-26-57a3f95cf4ed>", line 6, in prob_distribution
    assert abs(sum(probabilities) - 1.0) < epsilon,             nonterminal + ": sum of probabilities must be 1.0"
AssertionError: <start>: sum of probabilities must be 1.0 (expected)

```

```py
with ExpectError():
    assert is_valid_probabilistic_grammar(
        {"<start>": [("1", opts(prob=1.5)), "2"]})

```

```py
Traceback (most recent call last):
  File "<ipython-input-35-79fe851796f9>", line 3, in <module>
    {"<start>": [("1", opts(prob=1.5)), "2"]})
  File "<ipython-input-30-c1c25dbf0315>", line 7, in is_valid_probabilistic_grammar
    prob_dist = exp_probabilities(expansions, nonterminal)
  File "<ipython-input-25-9042a394542d>", line 3, in exp_probabilities
    prob_dist = prob_distribution(probabilities, nonterminal)
  File "<ipython-input-26-57a3f95cf4ed>", line 13, in prob_distribution
    assert 0 <= sum_of_specified_probabilities <= 1.0,         nonterminal + ": sum of specified probabilities must be between 0.0 and 1.0"
AssertionError: <start>: sum of specified probabilities must be between 0.0 and 1.0 (expected)

```

## 按概率[扩展](#Expanding-by-Probability)

既然我们已经了解了如何为语法指定概率，那么我们实际上可以实现概率扩展。 在我们的`ProbabilisticGrammarFuzzer`中，重载一种方法`choose_node_expansion()`就足够了。 对于每个孩子，我们都可以选择（通常是符号的所有扩展），我们确定它们的概率（使用上面定义的`exp_probabilities()`），然后使用`random.choices()`和`weight`参数进行加权选择。

```py
import [random](https://docs.python.org/3/library/random.html)

```

```py
class ProbabilisticGrammarFuzzer(GrammarFuzzer):
    def check_grammar(self):
        super().check_grammar()
        assert is_valid_probabilistic_grammar(self.grammar)

    def supported_opts(self):
        return super().supported_opts() | {'prob'}

```

```py
class ProbabilisticGrammarFuzzer(ProbabilisticGrammarFuzzer):
    def choose_node_expansion(self, node, possible_children):
        (symbol, tree) = node
        expansions = self.grammar[symbol]
        probabilities = exp_probabilities(expansions)

        weights = []
        for child in possible_children:
            expansion = all_terminals((node, child))
            child_weight = probabilities[expansion]
            if self.log:
                print(repr(expansion), "p =", child_weight)
            weights.append(child_weight)

        if sum(weights) == 0:
            # No alternative (probably expanding at minimum cost)
            weights = None

        return random.choices(
            range(len(possible_children)), weights=weights)[0]

```

我们的概率语法模糊器的工作原理与非概率语法模糊器相同，只是它实际上尊重概率注释。 让我们生成几个遵循本福德定律的“自然”数：

```py
natural_fuzzer = ProbabilisticGrammarFuzzer(
    PROBABILISTIC_EXPR_GRAMMAR, start_symbol="<leadinteger>")
print([natural_fuzzer.fuzz() for i in range(20)])

```

```py
['2588', '106', '10', '2', '7', '1', '95', '4', '192', '8', '2', '1', '1', '2', '2', '208', '1036', '5592', '157', '1442']

```

相反，这些数字是纯随机的：

```py
integer_fuzzer = GrammarFuzzer(
    PROBABILISTIC_EXPR_GRAMMAR, start_symbol="<leadinteger>")
print([integer_fuzzer.fuzz() for i in range(20)])

```

```py
['3', '1', '56', '5', '408251024', '7', '27', '2', '9', '6456', '7', '32', '1', '4', '7', '19', '2', '6', '2', '5']

```

```py
warning: option 'prob' is not supported

```

“自然”数字真的比随机数字更“自然”吗？ 为了表明`ProbabilisticGrammarFuzzer`确实遵守概率注释，让我们为前导数字创建一个特定的模糊器：

```py
leaddigit_fuzzer = ProbabilisticGrammarFuzzer(
    PROBABILISTIC_EXPR_GRAMMAR, start_symbol="<leaddigit>")
leaddigit_fuzzer.fuzz()

```

```py
'8'

```

如果我们生成数千个导数位，则它们的分布应再次遵循本福德定律：

```py
trials = 10000

count = {}
for c in crange('0', '9'):
    count[c] = 0

for i in range(trials):
    count[leaddigit_fuzzer.fuzz()] += 1

print([(digit, count[digit] / trials) for digit in count])

```

```py
[('0', 0.0), ('1', 0.3003), ('2', 0.1756), ('3', 0.1222), ('4', 0.0938), ('5', 0.0816), ('6', 0.0651), ('7', 0.06), ('8', 0.0537), ('9', 0.0477)]

```

Quod erat示范！ 该分布几乎完全与最初指定的一样。 现在，我们有了一个模糊器，可以在其中通过指定概率来进行控制。

## 定向模糊

将概率分配给各个扩展可让我们很好地控制应生成哪些输入。 通过明智地选择概率，我们可以将模糊化指向特定的功能和特性-例如，针对特别关键，容易出现故障或最近已更改的功能。

例如，请考虑语法的[一章中的URL语法。 让我们假设我们刚刚更改了安全FTP协议的实现。 通过为`ftps`方案分配更高的可能性，我们可以生成更多将专门测试此功能的URL。](Grammars.html)

首先，让我们定义一个设置特定选项的辅助函数：

这是专门针对概率的专业化：

```py
def set_prob(grammar, symbol, expansion, prob):
    """Set the probability of the given expansion of grammar[symbol]"""
    set_opts(grammar, symbol, expansion, opts(prob=prob))

```

让我们使用`set_prob()`赋予`ftps`扩展80％的概率：

```py
from [Grammars](Grammars.html) import URL_GRAMMAR, extend_grammar

```

```py
probabilistic_url_grammar = extend_grammar(URL_GRAMMAR)
set_prob(probabilistic_url_grammar, "<scheme>", "ftps", 0.8)
assert is_valid_probabilistic_grammar(probabilistic_url_grammar)

```

```py
probabilistic_url_grammar["<scheme>"]

```

```py
['http', 'https', 'ftp', ('ftps', {'prob': 0.8})]

```

如果我们将此语法用于模糊测试，则会得到大量的`ftps:`前缀：

```py
prob_url_fuzzer = ProbabilisticGrammarFuzzer(probabilistic_url_grammar)
for i in range(10):
    print(prob_url_fuzzer.fuzz())

```

```py
ftps://cispa.saarland:80
ftps://user:password@cispa.saarland/
ftps://fuzzingbook.com/abc
ftps://fuzzingbook.com/x48
ftps://user:password@fuzzingbook.com/
ftps://www.google.com:2?x18=8
ftps://user:password@www.google.com:6
ftps://user:password@cispa.saarland/def
ftps://user:password@cispa.saarland/def?def=52
ftps://user:password@cispa.saarland/

```

同样，我们可以将URL生成定向到特定的主机或端口。 我们可以使用带有查询，片段或登录名的URL，或者不包含这些的URL。 要做的只是设置适当的概率。

通过将扩展的可能性设置为零，我们可以有效地禁用特定的扩展：

```py
set_prob(probabilistic_url_grammar, "<scheme>", "ftps", 0.0)
assert is_valid_probabilistic_grammar(probabilistic_url_grammar)

```

```py
prob_url_fuzzer = ProbabilisticGrammarFuzzer(probabilistic_url_grammar)
for i in range(10):
    print(prob_url_fuzzer.fuzz())

```

```py
ftp://user:password@cispa.saarland/x00
https://user:password@www.google.com/?def=6&def=x18&def=def
https://user:password@fuzzingbook.com:7/?def=abc
https://user:password@www.google.com:8080/
ftp://www.google.com/?abc=36&x34=5
http://user:password@cispa.saarland/
https://www.google.com/
https://user:password@www.google.com:85/?def=18
http://user:password@www.google.com:80/
http://fuzzingbook.com:80/?abc=def

```

请注意，即使我们将展开的可能性设置为零，也可能会看到展开的情况。 当扩展以最低成本关闭时，这可能会在语法模糊器的[的“关闭”阶段发生。 在此阶段，如果需要关闭扩展，则将采取“零”概率的扩展。](GrammarFuzzer.html)

让我们使用表达式语法中的`<expr>`规则来说明此功能：

```py
from [Grammars](Grammars.html) import EXPR_GRAMMAR

```

```py
probabilistic_expr_grammar = extend_grammar(EXPR_GRAMMAR)
probabilistic_expr_grammar["<expr>"]

```

```py
['<term> + <expr>', '<term> - <expr>', '<term>']

```

如果将`<term>`扩展的概率设置为零，则字符串应一次又一次扩展。

```py
set_prob(probabilistic_expr_grammar, "<expr>", "<term>", 0.0)
assert is_valid_probabilistic_grammar(probabilistic_expr_grammar)

```

不过，在“关闭”阶段，子表达式最终将扩展为`<term>`，因为这是关闭扩展的唯一方法。 跟踪`choose_node_expansion()`显示仅使用一个可能的扩展`<term>`调用了该扩展，即使其指定的概率为零，也必须执行该扩展。

```py
prob_expr_fuzzer = ProbabilisticGrammarFuzzer(probabilistic_expr_grammar)
prob_expr_fuzzer.fuzz()

```

```py
'44 / 7 / 1 * 3 / 6 - +1.63 + 3 * 7 + 1 - 2'

```

## 上下文中的概率

虽然指定的概率为我们提供了控制多长时间进行一次扩展的方法，但仅凭这种控制可能还不够。 例如，请考虑以下IPv4地址语法：

```py
def decrange(start, end):
    """Return a list with string representations of numbers in the range [start, end)"""
    return [repr(n) for n in range(start, end)]

```

```py
IP_ADDRESS_GRAMMAR = {
    "<start>": ["<address>"],
    "<address>": ["<octet>.<octet>.<octet>.<octet>"],
    # ["0", "1", "2", ..., "255"]
    "<octet>": decrange(0, 256)
}

```

```py
print(IP_ADDRESS_GRAMMAR["<octet>"][:20])

```

```py
['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15', '16', '17', '18', '19']

```

```py
assert is_valid_grammar(IP_ADDRESS_GRAMMAR)

```

我们可以轻松地使用以下语法创建IP地址：

```py
ip_fuzzer = ProbabilisticGrammarFuzzer(IP_ADDRESS_GRAMMAR)
ip_fuzzer.fuzz()

```

```py
'34.155.77.136'

```

但是，如果我们要为四个八位位组之一分配特定的概率，那我们就不走运了。 我们所能做的就是为所有四个八位位组分配相同的概率分布：

```py
probabilistic_ip_address_grammar = extend_grammar(IP_ADDRESS_GRAMMAR)
set_prob(probabilistic_ip_address_grammar, "<octet>", "127", 0.8)

```

```py
probabilistic_ip_fuzzer = ProbabilisticGrammarFuzzer(
    probabilistic_ip_address_grammar)
probabilistic_ip_fuzzer.fuzz()

```

```py
'127.127.127.127'

```

如果我们要为四个八位位组分别分配*个不同的*个概率，我们该怎么做？

答案在于*上下文*的概念，我们在讨论覆盖范围驱动的模糊器时已经看到了[。 与覆盖驱动的模糊测试一样，其思想是*复制*元素，我们希望根据其上下文设置其概率。 在我们的情况下，这意味着将`<octet>`元素复制为四个单独的元素，然后每个元素都可以获得单独的概率分布。 我们可以使用`duplicate_context()`方法以编程方式执行此操作：](GrammarCoverageFuzzer.html)

```py
from [GrammarCoverageFuzzer](GrammarCoverageFuzzer.html) import duplicate_context  # minor dependency

```

```py
probabilistic_ip_address_grammar = extend_grammar(IP_ADDRESS_GRAMMAR)
duplicate_context(probabilistic_ip_address_grammar, "<address>")

```

```py
probabilistic_ip_address_grammar["<address>"]

```

```py
['<octet-1>.<octet-2>.<octet-3>.<octet-4>']

```

现在，我们可以为每个`<octet>`符号分配不同的概率。 例如，我们可以通过将特定扩展的概率设置为100％来强制执行特定扩展：

```py
set_prob(probabilistic_ip_address_grammar, "<octet-1>", "127", 1.0)
set_prob(probabilistic_ip_address_grammar, "<octet-2>", "0", 1.0)

```

```py
assert is_valid_probabilistic_grammar(probabilistic_ip_address_grammar)

```

其余两个八位位组`<octet-3>`和`<octet-4>`没有设置特定的概率。 在模糊测试期间，它们的所有扩展（所有八位位组）仍然可用：

```py
probabilistic_ip_fuzzer = ProbabilisticGrammarFuzzer(
    probabilistic_ip_address_grammar)
[probabilistic_ip_fuzzer.fuzz() for i in range(5)]

```

```py
['127.0.201.77',
 '127.0.98.36',
 '127.0.12.232',
 '127.0.146.161',
 '127.0.245.151']

```

就像覆盖率一样，我们可以经常任意地复制语法规则，从而获得对概率的越来越细粒度的控制。 但是，这种更细粒度的控制也以必须维护这些概率为代价。 因此，在下一部分中，我们将讨论自动分配和调整此类概率的方法。

## 从样本中学习概率

不必始终手动设置概率。 还可以从其他来源*来学习*，特别是通过计算*在给定的一组输入*中单个扩展出现的频率。 这在许多情况下很有用，包括：**

1.  测试*的常见*功能。 这个想法是，在测试过程中，可能需要首先关注经常发生（或经常使用）的功能，以确保最常见用法的正确功能。
2.  测试*不常见的*功能。 这里的想法是让测试生成集中于输入中很少看到（或根本看不到）的功能。 这与[语法覆盖率](GrammarCoverageFuzzer.html)相同，但从概率的角度来看。
3.  专注于特定的*切片*。 可能会有一组特别令人感兴趣的输入（例如，因为它们具有关键功能，或者最近发现了错误）。 使用这种学习到的分布进行模糊测试，可以使*专注于*精确地关注这些感兴趣的功能。

让我们首先介绍计数扩展和学习概率，然后详细介绍这些方案。

### 计算扩展

我们首先实现一种获取一组输入并确定该组扩展数量的方法。 为此，我们需要在上一章中介绍的*解析器*来将字符串输入转换为派生树。 对于我们的IP地址语法，这是这样工作的：

```py
from [Parser](Parser.html) import Parser, EarleyParser, PEGParser

```

```py
IP_ADDRESS_TOKENS = {"<octet>"}  # EarleyParser needs explicit tokens

```

```py
parser = EarleyParser(IP_ADDRESS_GRAMMAR)

```

```py
tree, *_ = parser.parse("127.0.0.1")
display_tree(tree)

```

<svg height="176pt" viewBox="0.00 0.00 303.00 176.00" width="303pt" 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 172)"><title>%3</title> <g class="node" id="node1"><title>0</title> <text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="147.5" y="-156.8"><start></text></g> <g class="node" id="node2"><title>1</title> <text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="147.5" y="-105.8"><address></text></g> <g class="edge" id="edge1"><title>0->1</title></g> <g class="node" id="node3"><title>2</title> <text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="21.5" y="-54.8"><octet></text></g> <g class="edge" id="edge2"><title>1->2</title></g> <g class="node" id="node5"><title>4</title> <text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="63.5" y="-54.8">.</text></g> <g class="edge" id="edge4"><title>1->4</title></g> <g class="node" id="node6"><title>5</title> <text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="105.5" y="-54.8"><octet></text></g> <g class="edge" id="edge5"><title>1->5</title></g> <g class="node" id="node8"><title>7</title> <text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="147.5" y="-54.8">.</text></g> <g class="edge" id="edge7"><title>1->7</title></g> <g class="node" id="node9"><title>8</title> <text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="189.5" y="-54.8"><octet></text></g> <g class="edge" id="edge8"><title>1->8</title></g> <g class="node" id="node11"><title>10</title> <text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="231.5" y="-54.8">.</text></g> <g class="edge" id="edge10"><title>1->10</title></g> <g class="node" id="node12"><title>11</title> <text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="273.5" y="-54.8"><octet></text></g> <g class="edge" id="edge11"><title>1->11</title></g> <g class="node" id="node4"><title>3</title> <text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="21.5" y="-3.8">127</text></g> <g class="edge" id="edge3"><title>2->3</title></g> <g class="node" id="node7"><title>6</title> <text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="105.5" y="-3.8">0</text></g> <g class="edge" id="edge6"><title>5->6</title></g> <g class="node" id="node10"><title>9</title> <text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="189.5" y="-3.8">0</text></g> <g class="edge" id="edge9"><title>8->9</title></g> <g class="node" id="node13"><title>12</title> <text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="273.5" y="-3.8">1</text></g> <g class="edge" id="edge12"><title>11->12</title></g></g></svg>

在像这样的树中，我们现在可以*数*个单个扩展。 例如，在上面的树中，我们将`<octet>`扩展为`0`，一次扩展为`1`，另一次扩展为`127`。 换句话说，将`<octet>`扩展为`0`占全部可见扩展的50％； `127`和`1`的扩展分别占25％，其他扩展为0％。 这些是我们希望分配给“学习的”语法的概率。

我们引入了`ExpansionCountMiner`类，该类允许我们计算单个扩展的发生频率。 它的初始化方法采用解析器（例如`EarleyParser`），该解析器将使用适当的语法进行初始化。

```py
class ExpansionCountMiner(object):
    def __init__(self, parser, log=False):
        assert isinstance(parser, Parser)
        self.grammar = extend_grammar(parser.grammar())
        self.parser = parser
        self.log = log
        self.reset()

```

属性`expansion_counts`包含看到的扩展； 用`add_tree()`添加一棵树会遍历给定的树，并添加所有可见的扩展。

```py
from [GrammarCoverageFuzzer](GrammarCoverageFuzzer.html) import expansion_key  # minor dependency

```

```py
from [Grammars](Grammars.html) import is_nonterminal

```

```py
class ExpansionCountMiner(ExpansionCountMiner):
    def reset(self):
        self.expansion_counts = {}

    def add_coverage(self, symbol, children):
        key = expansion_key(symbol, children)

        if self.log:
            print("Found", key)

        if key not in self.expansion_counts:
            self.expansion_counts[key] = 0
        self.expansion_counts[key] += 1

    def add_tree(self, tree):
        (symbol, children) = tree
        if not is_nonterminal(symbol):
            return

        direct_children = [
            (symbol, None) if is_nonterminal(symbol) else (
                symbol, []) for symbol, c in children]
        self.add_coverage(symbol, direct_children)

        for c in children:
            self.add_tree(c)

```

`count_expansions()`方法是面向公众的方法； 它获取输入列表，解析它们，并处理生成的树。 方法`counts()`返回找到的计数。

```py
class ExpansionCountMiner(ExpansionCountMiner):
    def count_expansions(self, inputs):
        for inp in inputs:
            tree, *_ = self.parser.parse(inp)
            self.add_tree(tree)

    def counts(self):
        return self.expansion_counts

```

让我们在IP地址语法中尝试一下。 我们为我们的IP地址语法创建一个`ExpansionCountMiner`：

```py
expansion_count_miner = ExpansionCountMiner(EarleyParser(IP_ADDRESS_GRAMMAR))

```

我们解析一组（小）IP地址并计算发生的扩展：

```py
expansion_count_miner.count_expansions(["127.0.0.1", "1.2.3.4"])
expansion_count_miner.counts()

```

```py
{'<start> -> <address>': 2,
 '<address> -> <octet>.<octet>.<octet>.<octet>': 2,
 '<octet> -> 127': 1,
 '<octet> -> 0': 2,
 '<octet> -> 1': 2,
 '<octet> -> 2': 1,
 '<octet> -> 3': 1,
 '<octet> -> 4': 1}

```

您会看到我们有一个扩展到`127`，另外两个扩展到`0`。 这些是我们可以用来分配概率的计数。

### 分配概率

由`ExpansionCountMiner`确定的计数分布是我们可以用来将概率分配给语法的方法。 为此，我们引入了一个子类`ProbabilisticGrammarMiner`，其方法`set_expansion_probabilities()`处理给定符号的所有扩展，检查它是否出现在给定的计数分布中，并使用以下公式分配概率。

给定一组导出树（从样本中提取），我们确定符号$ S \ rightarrow a_1 |的每个替代$ a_i $的概率$ p_i $。 \ dots | a_n $为

$$ p_i = \ frac {\ text {$ T的扩展\ rightarrow a_i $ in $ T $}} {\ text {$ S $的扩展$ T $}} $$

如果$ S $在$ T $中根本不出现，则$ p_i $为*未指定*。

这是`set_expansion_probabilities()`的实现，实现上述公式：

```py
class ProbabilisticGrammarMiner(ExpansionCountMiner):
    def set_probabilities(self, counts):
        for symbol in self.grammar:
            self.set_expansion_probabilities(symbol, counts)

    def set_expansion_probabilities(self, symbol, counts):
        expansions = self.grammar[symbol]
        if len(expansions) == 1:
            set_prob(self.grammar, symbol, expansions[0], None)
            return

        expansion_counts = [
            counts.get(
                expansion_key(
                    symbol,
                    expansion),
                0) for expansion in expansions]
        total = sum(expansion_counts)
        for i, expansion in enumerate(expansions):
            p = expansion_counts[i] / total if total > 0 else None
            # if self.log:
            #     print("Setting", expansion_key(symbol, expansion), p)
            set_prob(self.grammar, symbol, expansion, p)

```

`ProbabilisticGrammarMiner`的典型用法是通过`mine_probabilistic_grammar()`使用，它首先根据一组输入确定分布，然后相应地设置概率。

```py
class ProbabilisticGrammarMiner(ProbabilisticGrammarMiner):
    def mine_probabilistic_grammar(self, inputs):
        self.count_expansions(inputs)
        self.set_probabilities(self.counts())
        return self.grammar

```

让我们使用它。 我们为IP地址创建一个语法挖掘器：

```py
probabilistic_grammar_miner = ProbabilisticGrammarMiner(
    EarleyParser(IP_ADDRESS_GRAMMAR))

```

现在，我们使用`mine_probabilistic_grammar()`挖掘语法：

```py
probabilistic_ip_address_grammar = probabilistic_grammar_miner.mine_probabilistic_grammar([
                                                                                          "127.0.0.1", "1.2.3.4"])

```

```py
assert is_valid_probabilistic_grammar(probabilistic_ip_address_grammar)

```

这是语法中八位位组的结果分布：

```py
[expansion for expansion in probabilistic_ip_address_grammar['<octet>']
    if exp_prob(expansion) > 0]

```

```py
[('0', {'prob': 0.25}),
 ('1', {'prob': 0.25}),
 ('2', {'prob': 0.125}),
 ('3', {'prob': 0.125}),
 ('4', {'prob': 0.125}),
 ('127', {'prob': 0.125})]

```

如果使用这些概率进行模糊测试，则将获得与示例中相同的八位位组分布：

```py
probabilistic_ip_fuzzer = ProbabilisticGrammarFuzzer(
    probabilistic_ip_address_grammar)
[probabilistic_ip_fuzzer.fuzz() for i in range(10)]

```

```py
['4.2.2.0',
 '2.1.4.0',
 '0.1.3.127',
 '0.3.0.127',
 '4.0.2.1',
 '3.127.0.0',
 '2.2.1.1',
 '4.0.1.0',
 '2.4.0.1',
 '0.0.3.127']

```

通过从样本中学习，我们可以针对该样本的（语法）特性调整模糊度。

### 测试通用功能

现在让我们进入三种使用场景。 第一种情况是直接从样本中创建概率分布，并在测试生成过程中使用这些分布。 这有助于将测试生成的重点放在最常用的 **上，从而最大程度地降低了客户遇到故障的风险。**

为了说明常见功能的测试，我们选择URL域。 让我们假设我们正在运行一些与Web相关的服务，这是客户最常访问的URL的示例：

```py
URL_SAMPLE = [
    "https://user:password@cispa.saarland:80/",
    "https://fuzzingbook.com?def=56&x89=3&x46=48&def=def",
    "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",
]

```

使用关于解析器的[一章的Earley解析器，我们可以将这些输入中的任何一个解析为一个解析树。 不过，我们必须指定一个令牌集。](Parser.html)

```py
URL_TOKENS = {"<scheme>", "<userinfo>", "<host>", "<port>", "<id>"}

```

```py
url_parser = EarleyParser(URL_GRAMMAR, tokens=URL_TOKENS)
url_input = URL_SAMPLE[2]
print(url_input)
tree, *_ = url_parser.parse(url_input)
display_tree(tree)

```

```py
https://cispa.saarland:80/def?def=7&x23=abc

```

<svg height="380pt" viewBox="0.00 0.00 469.50 380.00" width="470pt" 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 376)"><title>%3</title> <g class="node" id="node1"><title>0</title> <text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="140" y="-360.8"><start></text></g> <g class="node" id="node2"><title>1</title> <text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="140" y="-309.8"><url></text></g> <g class="edge" id="edge1"><title>0->1</title></g> <g class="node" id="node3"><title>2</title> <text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="29" y="-258.8"><scheme></text></g> <g class="edge" id="edge2"><title>1->2</title></g> <g class="node" id="node5"><title>4</title> <text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="82" y="-258.8">://</text></g> <g class="edge" id="edge4"><title>1->4</title></g> <g class="node" id="node6"><title>5</title> <text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="140" y="-258.8"><authority></text></g> <g class="edge" id="edge5"><title>1->5</title></g> <g class="node" id="node12"><title>11</title> <text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="212" y="-258.8"><path></text></g> <g class="edge" id="edge11"><title>1->11</title></g> <g class="node" id="node16"><title>15</title> <text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="277" y="-258.8"><query></text></g> <g class="edge" id="edge15"><title>1->15</title></g> <g class="node" id="node4"><title>3</title> <text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="29" y="-207.8">https</text></g> <g class="edge" id="edge3"><title>2->3</title></g> <g class="node" id="node7"><title>6</title> <text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="85" y="-207.8"><host></text></g> <g class="edge" id="edge6"><title>5->6</title></g> <g class="node" id="node9"><title>8</title> <text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="125" y="-207.8">:</text></g> <g class="edge" id="edge8"><title>5->8</title></g> <g class="node" id="node10"><title>9</title> <text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="165" y="-207.8"><port></text></g> <g class="edge" id="edge9"><title>5->9</title></g> <g class="node" id="node8"><title>7</title> <text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="85" y="-156.8">cispa.saarland</text></g> <g class="edge" id="edge7"><title>6->7</title></g> <g class="node" id="node11"><title>10</title> <text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="165" y="-156.8">80</text></g> <g class="edge" id="edge10"><title>9->10</title></g> <g class="node" id="node13"><title>12</title> <text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="205" y="-207.8">/</text></g> <g class="edge" id="edge12"><title>11->12</title></g> <g class="node" id="node14"><title>13</title> <text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="239" y="-207.8"><id></text></g> <g class="edge" id="edge13"><title>11->13</title></g> <g class="node" id="node15"><title>14</title> <text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="235" y="-156.8">def</text></g> <g class="edge" id="edge14"><title>13->14</title></g> <g class="node" id="node17"><title>16</title> <text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="277" y="-207.8">?</text></g> <g class="edge" id="edge16"><title>15->16</title></g> <g class="node" id="node18"><title>17</title> <text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="334" y="-207.8"><params></text></g> <g class="edge" id="edge17"><title>15->17</title></g> <g class="node" id="node19"><title>18</title> <text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="288" y="-156.8"><param></text></g> <g class="edge" id="edge18"><title>17->18</title></g> <g class="node" id="node26"><title>25</title> <text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="337" y="-156.8">&</text></g> <g class="edge" id="edge25"><title>17->25</title></g> <g class="node" id="node27"><title>26</title> <text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="396" y="-156.8"><params></text></g> <g class="edge" id="edge26"><title>17->26</title></g> <g class="node" id="node20"><title>19</title> <text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="250" y="-105.8"><id></text></g> <g class="edge" id="edge19"><title>18->19</title></g> <g class="node" id="node22"><title>21</title> <text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="286" y="-105.8">=</text></g> <g class="edge" id="edge21"><title>18->21</title></g> <g class="node" id="node23"><title>22</title> <text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="325" y="-105.8"><nat></text></g> <g class="edge" id="edge22"><title>18->22</title></g> <g class="node" id="node21"><title>20</title> <text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="250" y="-54.8">def</text></g> <g class="edge" id="edge20"><title>19->20</title></g> <g class="node" id="node24"><title>23</title> <text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="323" y="-54.8"><digit></text></g> <g class="edge" id="edge23"><title>22->23</title></g> <g class="node" id="node25"><title>24</title> <text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="323" y="-3.8">7</text></g> <g class="edge" id="edge24"><title>23->24</title></g> <g class="node" id="node28"><title>27</title> <text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="399" y="-105.8"><param></text></g> <g class="edge" id="edge27"><title>26->27</title></g> <g class="node" id="node29"><title>28</title> <text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="376" y="-54.8"><id></text></g> <g class="edge" id="edge28"><title>27->28</title></g> <g class="node" id="node31"><title>30</title> <text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="412" y="-54.8">=</text></g> <g class="edge" id="edge30"><title>27->30</title></g> <g class="node" id="node32"><title>31</title> <text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="448" y="-54.8"><id></text></g> <g class="edge" id="edge31"><title>27->31</title></g> <g class="node" id="node30"><title>29</title> <text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="376" y="-3.8">x23</text></g> <g class="edge" id="edge29"><title>28->29</title></g> <g class="node" id="node33"><title>32</title> <text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="448" y="-3.8">abc</text></g> <g class="edge" id="edge32"><title>31->32</title></g></g></svg>

让我们使用上面的`url_parser`解析器将`ProbabilisticGrammarMiner`类应用于这些输入，并获得概率URL语法：

```py
probabilistic_grammar_miner = ProbabilisticGrammarMiner(url_parser)
probabilistic_url_grammar = probabilistic_grammar_miner.mine_probabilistic_grammar(
    URL_SAMPLE)

```

这些是我们在解析过程中获得的计数：

```py
print(probabilistic_grammar_miner.counts())

```

```py
{'<start> -> <url>': 9, '<url> -> <scheme>://<authority><path><query>': 9, '<scheme> -> https': 6, '<authority> -> <userinfo>@<host>:<port>': 2, '<userinfo> -> user:password': 2, '<host> -> cispa.saarland': 2, '<port> -> 80': 5, '<path> -> /': 2, '<query> -> <query>': 4, '<authority> -> <host>': 2, '<host> -> fuzzingbook.com': 7, '<path> -> <path>': 3, '<query> -> ?<params>': 5, '<params> -> <param>&<params>': 10, '<param> -> <id>=<nat>': 9, '<id> -> def': 11, '<nat> -> <digit><digit>': 5, '<digit> -> 5': 3, '<digit> -> 6': 1, '<id> -> x89': 1, '<nat> -> <digit>': 4, '<digit> -> 3': 2, '<id> -> x46': 1, '<digit> -> 4': 2, '<digit> -> 8': 3, '<params> -> <param>': 5, '<param> -> <id>=<id>': 6, '<authority> -> <host>:<port>': 5, '<path> -> /<id>': 4, '<digit> -> 7': 1, '<id> -> x23': 1, '<id> -> abc': 7, '<id> -> x14': 1, '<digit> -> 2': 2, '<scheme> -> ftps': 1, '<id> -> x87': 1, '<port> -> 6': 1, '<id> -> x44': 1, '<scheme> -> http': 2, '<id> -> x33': 1, '<port> -> 8080': 1}

```

这些计数转化为个体概率。 我们看到在示例中，大多数URL使用`https:`方案，而没有输入使用`ftp:`方案。

```py
probabilistic_url_grammar['<scheme>']

```

```py
[('http', {'prob': 0.2222222222222222}),
 ('https', {'prob': 0.6666666666666666}),
 ('ftp', {'prob': 0.0}),
 ('ftps', {'prob': 0.1111111111111111})]

```

同样，我们看到大多数给定的URL具有多个参数：

```py
probabilistic_url_grammar['<params>']

```

```py
[('<param>', {'prob': 0.3333333333333333}),
 ('<param>&<params>', {'prob': 0.6666666666666666})]

```

当我们使用这种概率语法进行模糊测试时，这些分布会反映在我们生成的输入中-也没有`ftp:`方案，而且大多数输入都有多个参数。

```py
g = ProbabilisticGrammarFuzzer(probabilistic_url_grammar)
[g.fuzz() for i in range(10)]

```

```py
['https://fuzzingbook.com/def?def=abc&def=abc&def=def&def=abc&abc=def&def=def&abc=def',
 'http://fuzzingbook.com:80/def?def=7&def=abc&abc=88',
 'https://cispa.saarland/def?def=2',
 'http://user:password@fuzzingbook.com:80/def?abc=abc&def=78',
 'http://cispa.saarland:80/?def=54&def=abc',
 'https://fuzzingbook.com:80/def?def=def',
 'http://fuzzingbook.com:80/abc?abc=abc&abc=def&def=85&abc=7&def=6&abc=2&def=abc',
 'https://fuzzingbook.com/abc?def=32&def=3&abc=4',
 'http://fuzzingbook.com/def?abc=24&def=def&def=48',
 'https://cispa.saarland:80/?abc=abc']

```

能够复制从样本中学到的概率分布不仅对于关注常用功能非常重要。 它也可以帮助实现*有效输入*，特别是如果一个人在上下文中学习了概率*时，如上所述：如果在给定的上下文中，某些元素比其他元素更有可能（因为它们 相互依赖），学习到的概率分布将反映出这一点； 因此，从该获悉的概率分布中生成的输入也将具有较高的有效机会。 我们将在下面的[练习](#Exercises)中进一步探讨这一点。*

### 测试罕见功能

到目前为止，我们已经将重点放在*常见*功能上； 但是从测试的角度来看，一个人可能同样会测试*和*的不常见功能-也就是说，这些功能在我们的使用示例中很少出现，因此在实践中将很少使用。 这是安全测试中的一种常见情况，其中的重点是罕见的（可能鲜为人知）功能，因为更少的用户意味着更少的错误报告，因此，更多的错误需要发现和利用。

为了使我们的概率语法模糊器关注*和*不常见的特征，我们*更改了学习的概率*，以使常见的特征（即，具有较高的学习概率的特征）的概率较低，反之 反之亦然：倒数第一，倒数第二。 实现这种*倒置*概率的一种特别简单的方法是*交换*：概率最高和最低的替代方案交换其概率，因此，第二高和第二的替代方案将交换其概率。 最低的概率，具有最高和最低第三的替代方案，依此类推。

函数`invert_expansion()`从语法中提取扩展（替代列表），并返回新的反向扩展，其中已根据上述规则交换了概率。 它创建一个索引列表，并按增加的概率对其进行排序，然后为每个$ n $元素分配索引中最后$ n $个元素的概率。

```py
import [copy](https://docs.python.org/3/library/copy.html)

```

```py
def invert_expansion(expansion):
    def sort_by_prob(x):
        index, prob = x
        return prob if prob is not None else 0

    inverted_expansion = copy.deepcopy(expansion)
    indexes = [(index, exp_prob(alternative))
               for index, alternative in enumerate(expansion)]
    indexes.sort(key=sort_by_prob)
    indexes = [i for (i, _) in indexes]

    for j in range(len(indexes)):
        k = len(indexes) - 1 - j
        # print(indexes[j], "gets", indexes[k])
        inverted_expansion[indexes[j]
                           ][1]['prob'] = expansion[indexes[k]][1]['prob']

    return inverted_expansion

```

这是`invert_expansion()`的实际操作。 这是URL方案的原始概率分布：

```py
probabilistic_url_grammar['<scheme>']

```

```py
[('http', {'prob': 0.2222222222222222}),
 ('https', {'prob': 0.6666666666666666}),
 ('ftp', {'prob': 0.0}),
 ('ftps', {'prob': 0.1111111111111111})]

```

这就是“反向”分布。 我们看到，以前具有零概率的`ftp:`方案现在具有最高的概率，而最常见的方案`https:`现在具有`ftp:`方案的先前零概率。

```py
invert_expansion(probabilistic_url_grammar['<scheme>'])

```

```py
[('http', {'prob': 0.1111111111111111}),
 ('https', {'prob': 0.0}),
 ('ftp', {'prob': 0.6666666666666666}),
 ('ftps', {'prob': 0.2222222222222222})]

```

这种概率交换的一个很好的特点是，概率之和保持不变。 无需标准化。 另一个不错的功能是，反转的反转将返回原始分布：

```py
invert_expansion(invert_expansion(probabilistic_url_grammar['<scheme>']))

```

```py
[('http', {'prob': 0.2222222222222222}),
 ('https', {'prob': 0.6666666666666666}),
 ('ftp', {'prob': 0.0}),
 ('ftps', {'prob': 0.1111111111111111})]

```

请注意，我们的实现并不能普遍满足此属性：如果扩展中的两个替代方案$ a_1 $和$ a_2 $共享相同的概率，则第二个反演可能会为$ a_1 $和$ a_2 $分配不同的概率。

我们可以在整个语法中应用扩展的倒置：

```py
def invert_probs(grammar):
    inverted_grammar = extend_grammar(grammar)
    for symbol in grammar:
        inverted_grammar[symbol] = invert_expansion(grammar[symbol])
    return inverted_grammar

```

这意味着将为每个扩展交换概率：

```py
probabilistic_url_grammar["<digit>"]

```

```py
[('0', {'prob': 0.0}),
 ('1', {'prob': 0.0}),
 ('2', {'prob': 0.14285714285714285}),
 ('3', {'prob': 0.14285714285714285}),
 ('4', {'prob': 0.14285714285714285}),
 ('5', {'prob': 0.21428571428571427}),
 ('6', {'prob': 0.07142857142857142}),
 ('7', {'prob': 0.07142857142857142}),
 ('8', {'prob': 0.21428571428571427}),
 ('9', {'prob': 0.0})]

```

```py
inverted_probabilistic_url_grammar = invert_probs(probabilistic_url_grammar)
inverted_probabilistic_url_grammar["<digit>"]

```

```py
[('0', {'prob': 0.21428571428571427}),
 ('1', {'prob': 0.21428571428571427}),
 ('2', {'prob': 0.07142857142857142}),
 ('3', {'prob': 0.07142857142857142}),
 ('4', {'prob': 0.0}),
 ('5', {'prob': 0.0}),
 ('6', {'prob': 0.14285714285714285}),
 ('7', {'prob': 0.14285714285714285}),
 ('8', {'prob': 0.0}),
 ('9', {'prob': 0.14285714285714285})]

```

如果现在使用这种“反”语法进行模糊处理，则生成的输入将集中在输入样本的*补码上。 我们将进行大量的用户/密码功能测试，以及`ftp:`方案-本质上，所有功能都以我们的语言提供，但很少在输入样本中使用（如果有的话）。*

```py
g = ProbabilisticGrammarFuzzer(inverted_probabilistic_url_grammar)
[g.fuzz() for i in range(10)]

```

```py
['ftp://www.google.com',
 'ftp://www.google.com/',
 'ftp://www.google.com/',
 'ftp://user:password@cispa.saarland',
 'ftps://www.google.com/',
 'ftp://user:password@www.google.com/',
 'ftp://user:password@www.google.com',
 'ftp://www.google.com/',
 'ftp://user:password@www.google.com',
 'ftp://user:password@www.google.com/']

```

除了仅具有*常见*或仅罕见的功能外，还可以创建混合形式-例如，在常见上下文中测试罕见的功能。 这对于安全测试很有帮助，在安全测试中，可能需要一个无害（通用）的“信封”和一个（不常见）“有效载荷”。 这完全取决于我们在哪里以及如何调整概率。

### 从输入切片学习概率

在前面的示例中，我们从*中了解了所有*输入，以生成公共或非公共输入。 但是，我们还可以从输入的*子集*中学习，以专注于该子集中存在的特征（或者相反，*避免*其特征）。 例如，如果我们知道有一些输入子集涵盖了感兴趣的功能（例如，因为它特别关键或因为它最近已被更改），那么我们可以从这一子集中学习并将测试生成重点放在 其功能。

为了说明这种方法，让我们使用关于覆盖范围的[一章中介绍的CGI语法。 我们对CGI解码器中的第25行有特别的兴趣-即处理`%`字符后跟两个有效十六进制数字的行：](Coverage.html)

```py
...
        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] ### Line 25
                t += chr(v)
        ...

```

让我们假设我们不确切知道第25行在什么条件下执行-但是，我们仍然想对其进行彻底的测试。 但是，借助概率学习工具，我们可以学习这些条件。 我们从一组随机输入开始，并考虑覆盖第25行的子集。

```py
from [Coverage](Coverage.html) import Coverage, cgi_decode
from [Grammars](Grammars.html) import CGI_GRAMMAR

```

```py
cgi_fuzzer = GrammarFuzzer(CGI_GRAMMAR)

```

```py
trials = 100
coverage = {}

for i in range(trials):
    cgi_input = cgi_fuzzer.fuzz()
    with Coverage() as cov:
        cgi_decode(cgi_input)
    coverage[cgi_input] = cov.coverage()

```

这些都是涵盖第25行的随机输入：

```py
coverage_slice = [cgi_input for cgi_input in coverage
                  if ('cgi_decode', 25) in coverage[cgi_input]]

```

```py
print(coverage_slice)

```

```py
['%36%c1%d2c2%4f++e', '%2c%90+', 'c++%8b', 'a+%95', '%76%00', '%71', '%a2', '%39%51%db%7c%66', '%2a', '%b9225', '%13+b', '%32', '-1%b11%d8', '%08', '%a3', '%fe', '+%82%ed%42', '%d5', '%5bc', '%b0', '%47', 'b+%20', '%d7', '0+%17', '%a5', '%84', 'e+4%fc', 'd%6f+++1a', 'd+%95+', '%1e', '%244', '%3c', '5%75+%99%3c', '%80%74+a', '%a7', '%c1%da', '%c5+', 'b%44', '%70%c4_3', '4%63', '%364+', '%79%ab', '%8a%f6', '%53%43', '+++%55+b5', '%51+++', '+%28', '1%1c+', '+%41%9b', '%0d%20', '+%3d+%c2']

```

实际上，大约一半的输入覆盖第25行：

```py
len(coverage_slice) / trials

```

```py
0.51

```

现在让我们从这部分输入中学习概率语法：

```py
probabilistic_grammar_miner = ProbabilisticGrammarMiner(
    EarleyParser(CGI_GRAMMAR))
probabilistic_cgi_grammar = probabilistic_grammar_miner.mine_probabilistic_grammar(
    coverage_slice)

```

```py
assert is_valid_probabilistic_grammar(probabilistic_cgi_grammar)

```

我们看到百分号很可能会发生：

```py
probabilistic_cgi_grammar['<letter>']

```

```py
[('<plus>', {'prob': 0.2318840579710145}),
 ('<percent>', {'prob': 0.5362318840579711}),
 ('<other>', {'prob': 0.2318840579710145})]

```

使用此语法，我们现在可以生成专门针对第25行的测试：

```py
probabilistic_cgi_fuzzer = ProbabilisticGrammarFuzzer(
    probabilistic_cgi_grammar)
print([probabilistic_cgi_fuzzer.fuzz() for i in range(20)])

```

```py
['%44', '%17', '+', '_%3b', '%91%74', 'b', 'b', '%40+b%e1%35%f7+c4%7a%37', '%21%f6%59a', '2b', '%ba', '%d9%29eab+++%c1+', '3', '%60e2', '%71%f2+', '++%3b', '0%03%70+%34%6e', 'e', '%65a', '+%5aac+']

```

```py
trials = 100
coverage = {}

for i in range(trials):
    cgi_input = probabilistic_cgi_fuzzer.fuzz()
    with Coverage() as cov:
        cgi_decode(cgi_input)
    coverage[cgi_input] = cov.coverage()

```

我们看到，覆盖第25行的输入比例已经高得多，这表明我们的工作重点是：

```py
coverage_slice = [cgi_input for cgi_input in coverage
                  if ('cgi_decode', 25) in coverage[cgi_input]]

```

```py
len(coverage_slice) / trials

```

```py
0.76

```

再重复一次，可以得到更高的聚焦度：

```py
for run in range(3):
    probabilistic_cgi_grammar = probabilistic_grammar_miner.mine_probabilistic_grammar(
        coverage_slice)
    probabilistic_cgi_fuzzer = ProbabilisticGrammarFuzzer(
        probabilistic_cgi_grammar)

    trials = 100
    coverage = {}

    for i in range(trials):
        cgi_input = probabilistic_cgi_fuzzer.fuzz()
        with Coverage() as cov:
            cgi_decode(cgi_input)
        coverage[cgi_input] = cov.coverage()

    coverage_slice = [cgi_input for cgi_input in coverage
                      if ('cgi_decode', 25) in coverage[cgi_input]]

```

```py
len(coverage_slice) / trials

```

```py
0.85

```

通过从样本输入的子集中学习（并重新学习）概率，我们可以使*使*模糊测试器针对该子集的属性进行专门化–在我们的示例中，输入中包含百分比符号和有效的十六进制字母。 我们可以使事物专门化的程度取决于我们可以控制的变量数量–在我们的情况下，是各个规则的概率。 如上所述，向语法添加更多上下文将增加变量的数量，从而增加专业化的数量。

但是，高度的专业化限制了我们探索落在所选范围之外的*组合的可能性，并且限制了我们发现由这些组合引起的错误的可能性。 这种权衡在机器学习中被称为*探索与开发* –是应该尝试探索尽可能多的（可能是浅表的）组合，还是专注于（开发）特定区域？ 最后，这完全取决于错误的位置以及我们最有可能找到它们的位置。 分配和学习概率使我们能够控制搜索策略-从常见到罕见，再到特定子集。*

## 检测非自然数

让我们回到介绍性的例子来结束本章。 我们说过，本福德定律不仅允许我们产生，而且还可以检测“不自然的”铅位数分布，例如由简单的随机选择产生的铅位数分布。

如果我们使用常规的`GrammarFuzzer`类（忽略概率）来生成（随机）前导数字，则这是我们为每个前导数字获得的分布：

```py
sample_size = 1000
random_integer_fuzzer = GrammarFuzzer(
    PROBABILISTIC_EXPR_GRAMMAR,
    start_symbol="<leaddigit>")
random_integers = [random_integer_fuzzer.fuzz() for i in range(sample_size)]

```

```py
warning: option 'prob' is not supported

```

```py
random_counts = [random_integers.count(c) for c in crange('1', '9')]
random_counts

```

```py
[122, 123, 116, 98, 117, 105, 99, 114, 106]

```

（为简单起见，我们在这里使用简单列表`count()`方法，而不是部署完整的`ProbabilisticGrammarMiner`。）

如果我们有自然的铅位数分布，这就是我们所期望的：

```py
expected_prob_counts = [
    exp_prob(
        PROBABILISTIC_EXPR_GRAMMAR["<leaddigit>"][i]) *
    sample_size for i in range(9)]
print(expected_prob_counts)

```

```py
[301.0, 176.0, 125.0, 97.0, 79.0, 67.0, 58.0, 51.0, 46.0]

```

如果我们有一个随机分布，我们将期望一个相等的分布：

```py
expected_random_counts = [sample_size / 9 for i in range(9)]
print(expected_random_counts)

```

```py
[111.11111111111111, 111.11111111111111, 111.11111111111111, 111.11111111111111, 111.11111111111111, 111.11111111111111, 111.11111111111111, 111.11111111111111, 111.11111111111111]

```

哪个分布更匹配我们的`random_counts`前导数字？ 为此，我们运行了一个\\ chi ^ 2 $ -test来比较发现的分布（`random_counts`）与“自然”铅位数分布`expected_prob_counts`和随机分布`expected_random_counts`。

```py
from [scipy.stats](https://docs.python.org/3/library/scipy.stats.html) import chisquare

```

事实证明，观察到的分布遵循“自然”分布的可能性为零（`pvalue` = 0.0）：

```py
chisquare(random_counts, expected_prob_counts)

```

```py
Power_divergenceResult(statistic=347.96491895076656, pvalue=2.4618008235897177e-70)

```

但是，有97％的机会观察到的行为遵循随机分布：

```py
chisquare(random_counts, expected_random_counts)

```

```py
Power_divergenceResult(statistic=6.38, pvalue=0.6047465691667375)

```

因此，如果您发现一些数字并怀疑其有效性，则可以运行上述测试以检查它们是否可能是自然的。 更好的是，坚持要求作者使用Jupyter笔记本生成结果，以便您可以检查计算的每个步骤:-)

## 经验教训

*   通过指定概率，可以将模糊引导到感兴趣的输入特征。
*   从样本中学习概率可以使人们专注于输入样本中常见或不常见的特征。
*   从一部分样本中学习概率可以使人们产生更多相似的输入。

## 后续步骤

现在，我们将概率和语法（以及重新分析器和语法）结合在一起，我们为许多应用程序奠定了基础。 我们的下一章将重点介绍

*   如何 [*将*失败的输入减少到最小](Reducer.html)
*   如何在功能级别上雕刻[和](Carver.html)[产生](APIFuzzer.html)测试
*   如何[自动测试（Web）用户界面](WebFuzzer.html)

请享用！

## 背景

通过解析数据集挖掘概率的想法首先在“学习模糊：输入数据的概率生成模型与应用程序无关的模糊测试” [ [Patra *等人*，2016年中提出。](http://mp.binaervarianz.de/TreeFuzz_TR_Nov2016.pdf) ，它还为派生树学习并应用概率规则。 首先在“地狱的投入：从普通样本中生成不常见的投入” [ [Esteban Pavese *等人，*等，2018年的工作中，首先将这种思想应用于概率语法以及反转概率或从切片中学习。](http://arxiv.org/abs/1812.07525) 。

我们对本福德定律的解释遵循[本文](https://brilliant.org/wiki/benfords-law/)。

## 练习

### 练习1：覆盖范围内的概率模糊

创建一个扩展了`GrammarCoverageFuzzer`并具有概率功能的类`ProbabilisticGrammarCoverageFuzzer`。 这个想法是首先覆盖所有未发现的扩展（如`GrammarCoverageFuzzer`），一旦覆盖了所有扩展，就按概率进行处理（如`ProbabilisticGrammarFuzzer`）。

为此，定义`choose_covered_node_expansion()`和`choose_uncovered_node_expansion()`方法的新实例，这些实例根据给定的权重选择扩展。

[Use the notebook](https://mybinder.org/v2/gh/uds-se/fuzzingbook/master?filepath=docs/notebooks/ProbabilisticGrammarFuzzer.ipynb#Exercises) to work on the exercises and see solutions.

如果您是高级程序员，请通过从`GrammarCoverageFuzzer`和`ProbabilisticGrammarFuzzer`继承*多重继承*来实现该类。

多重继承是一件棘手的事情。 如果您有两个都从$ A $继承的类$ A'$和$ A''$，则$ A $的相同方法$ m（）$可能在$ A'$和$ A''$中都被重载。 。 如果现在从*继承* $ A'$和$ A''$，并调用$ m（）$，应调用哪个$ m（）$实现？ Python通过简单地调用一个从第一个继承的类中的$ m（）$方法来“解决”该冲突。

为了避免这种冲突，可以检查一个人继承的顺序是否有所不同。 方法`inheritance_conflicts()`相互比较属性； 如果他们引用不同的代码，则必须解决冲突。

[Use the notebook](https://mybinder.org/v2/gh/uds-se/fuzzingbook/master?filepath=docs/notebooks/ProbabilisticGrammarFuzzer.ipynb#Exercises) to work on the exercises and see solutions.

```py
from [fuzzingbook_utils](https://github.com/uds-se/fuzzingbook/tree/master/notebooks/fuzzingbook_utils) import inheritance_conflicts

```

```py
inheritance_conflicts(GrammarCoverageFuzzer, ProbabilisticGrammarFuzzer)

```

```py
['choose_node_expansion']

```

除了`choose_covered_node_expansion()`和`choose_uncovered_node_expansion()`之外，这是*还具有*实现多重继承的方法。

[Use the notebook](https://mybinder.org/v2/gh/uds-se/fuzzingbook/master?filepath=docs/notebooks/ProbabilisticGrammarFuzzer.ipynb#Exercises) to work on the exercises and see solutions.

### 练习2：从过去的错误中学习

如果从*输入中学习以前已知会导致故障的内容，那么从一组输入中学习可能会非常有价值。* 在本练习中，您将去学习过去漏洞的分布。

1.  下载 [`js-vuln-db`](https://github.com/tunz/js-vuln-db) ，这是一组JavaScript引擎漏洞。 每个漏洞均附带执行该漏洞的代码。
2.  使用带有适当正则表达式的`re.findall()`，从代码中提取所有*数字文字*。
3.  将这些文字转换为（十进制）*数值*并计算它们各自的出现次数。
4.  创建一个语法`RISKY_NUMBERS`来生成这些数字，并带有反映上述计数的概率。

当然，漏洞不仅是特定数量的漏洞，而且某些漏洞比其他漏洞更容易引发错误。 下次使系统模糊时，请不要随机生成数字。 而是从`RISKY_NUMBERS`中选择一个：-)

[Use the notebook](https://mybinder.org/v2/gh/uds-se/fuzzingbook/master?filepath=docs/notebooks/ProbabilisticGrammarFuzzer.ipynb#Exercises) to work on the exercises and see solutions.