模糊测试之书-九-

模糊测试之书(九)

原文:exploringjs.com/ts/book/index.html

译者:飞龙

协议:CC BY-NC-SA 4.0

第四部分:语义模糊测试

原文:www.fuzzingbook.org/html/04_Semantical_Fuzzing.html

本部分介绍了考虑输入的语义的测试生成技术,特别是处理输入的程序的行为。

  • 带有约束的模糊测试向语法中添加语义约束。通过自动解决这些约束,我们可以生成既在语法上又在语义上有效的输入。

  • 语法挖掘展示了如何通过分析输入的各个部分是如何被处理的来从程序中提取输入语法。这些生成的语法可以直接用于模糊测试。

  • 跟踪信息流展示了如何在整个程序中跟踪输入,以便发现信息泄露并进一步改进分析技术。

  • 并发模糊测试分析程序代码以解决程序中的路径约束,以覆盖难以到达的分支和行为。

  • 符号模糊测试的工作原理类似于并发模糊测试,但根本不需要任何执行。

  • 挖掘函数规范从程序执行中提取类型信息以及前置条件和后置条件——这些信息对于程序分析、测试和验证非常有用。

Creative Commons License 本项目的内容受Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License许可。作为内容一部分的源代码,以及用于格式化和显示该内容的源代码,受MIT License许可。最后修改时间:2023-01-07 15:48:35+01:00。引用 版权信息

如何引用本作品

Andreas Zeller, Rahul Gopinath, Marcel Böhme, Gordon Fraser, 和 Christian Holler: "第四部分:语义模糊测试"。在 Andreas Zeller, Rahul Gopinath, Marcel Böhme, Gordon Fraser, 和 Christian Holler 的"模糊测试书"中。www.fuzzingbook.org/html/04_Semantical_Fuzzing.html。检索时间:2023-01-07 15:48:35+01:00。

@incollection{fuzzingbook2023:04_Semantical_Fuzzing,
    author = {Andreas Zeller and Rahul Gopinath and Marcel B{\"o}hme and Gordon Fraser and Christian Holler},
    booktitle = {The Fuzzing Book},
    title = {Part IV: Semantic Fuzzing},
    year = {2023},
    publisher = {CISPA Helmholtz Center for Information Security},
    howpublished = {\url{https://www.fuzzingbook.org/html/04_Semantical_Fuzzing.html}},
    note = {Retrieved 2023-01-07 15:48:35+01:00},
    url = {https://www.fuzzingbook.org/html/04_Semantical_Fuzzing.html},
    urldate = {2023-01-07 15:48:35+01:00}
}

带约束的模糊测试

原文:www.fuzzingbook.org/html/FuzzingWithConstraints.html

在前面的章节中,我们看到了基于语法的模糊测试如何使我们能够高效地生成大量的语法有效输入。然而,有一些语义输入特征无法在上下文无关语法中表示,例如

  • "\(X\)\(Y\) 的长度";

  • "\(X\) 是之前声明的标识符";或者

  • "\(X\) 应该大于 4,096 字节"。

在本章中,我们展示了 ISLa 框架如何使我们能够将此类特征作为添加到语法中的约束来表示。通过让 ISLa 自动解决这些约束,我们产生的输入不仅语法有效,而且实际上语义有效。此外,此类约束允许我们非常精确地塑造我们想要用于测试的输入。

from [bookutils](https://github.com/uds-se/fuzzingbook//tree/master/notebooks/shared/bookutils) import YouTubeVideo
YouTubeVideo("dgaGuwn-1OU") 

先决条件

  • 你应该已经阅读了关于语法的章节。

  • 关于生成器和过滤器的章节解决了一个类似的问题,但使用程序代码而不是约束。

import [bookutils.setup](https://github.com/uds-se/fuzzingbook//tree/master/notebooks/shared/bookutils) 

概述

要使用本章节提供的代码,请编写

>>> from fuzzingbook.FuzzingWithConstraints import <identifier> 

然后利用以下功能。

本章介绍了 ISLa 框架,该框架包括

  • ISLa 规范语言,允许向语法添加约束

  • ISLa 求解器,解决这些约束以生成语义(和语法)上有效的输入

  • ISLa 检查器,检查给定输入是否满足这些约束。

ISLa 求解器的一个典型用法如下。首先,使用以下命令安装 ISLa:

$  pip  install  isla-solver 

然后,你可以导入求解器作为

>>> from [isla.solver](https://rindphi.github.io/isla/) import ISLaSolver 

ISLa 求解器需要两个东西。首先,一个语法——比如说,美国电话号码。

>>> from Grammars import US_PHONE_GRAMMAR 

其次,你需要约束——一个表示一个或多个语法元素条件的字符串。常见的函数包括

  • str.len(),返回字符串的长度

  • str.to.int(),将字符串转换为整数

在这里,我们使用一个约束实例化 ISLa 求解器,该约束指出区号应大于 900:

>>> solver = ISLaSolver(US_PHONE_GRAMMAR, 
>>>             """
>>>             str.to.int(<area>) > 900
>>>             """) 

使用它,调用 solver.solve() 返回约束的解决方案

>>> str(solver.solve())
'(904)657-8423' 

solve() 返回一个推导树,通常使用 str() 转换为字符串,如上所述。print() 函数隐式地执行此操作。

后续调用 solve() 返回更多解决方案:

>>> for _ in range(10):
>>>     print(solver.solve())
(904)596-1030
(904)436-4047
(904)723-4207
(904)323-0627
(904)249-7959
(904)879-4209
(904)910-7560
(904)900-4775
(904)565-2710
(910)223-7794 

我们看到求解器产生了一系列满足约束的输入——区号总是大于 900。

ISLaSolver() 构造函数提供了几个额外的参数来配置求解器,如下文所述。额外的 ISLaSolver 方法允许检查输入是否满足约束,并提供额外的功能。

ISLaSolver <a xlink:href="isla.solver.ipynb" xlink:title="类 ISLaSolver:

ISLa 公式的约束求解器类。其顶级方法包括

:meth:~isla.solver.ISLaSolver.solve

用于生成 ISLa 约束的解决方案。

:meth:~isla.solver.ISLaSolver.check

用于检查给定输入是否满足 ISLa 约束。

:meth:~isla.solver.ISLaSolver.parse

用于解析和验证输入。

:meth:~isla.solver.ISLaSolver.repair

用于修复输入,使其满足约束。

:meth:~isla.solver.ISLaSolver.mutate

用于变异输入,使得结果满足约束。">ISLaSolver <a xlink:href="isla.solver.ipynb" xlink:title="init(self, grammar: Union[Mapping[str, Sequence[str]], str], formula: Union[isla.language.Formula, str, NoneType] = None, structural_predicates: Set[isla.language.StructuralPredicate] = frozenset({StructuralPredicate(name='inside', arity=2, eval_fun=), StructuralPredicate(name='level', arity=4, eval_fun=), StructuralPredicate(name='consecutive', arity=2, eval_fun=), StructuralPredicate(name='before', arity=2, eval_fun=), StructuralPredicate(name='nth', arity=3, eval_fun=), StructuralPredicate(name='same_position', arity=2, eval_fun=), StructuralPredicate(name='after', arity=2, eval_fun=), StructuralPredicate(name='different_position', arity=2, eval_fun=), StructuralPredicate(name='direct_child', arity=2, eval_fun=)}), semantic_predicates: Set[isla.language.SemanticPredicate] = frozenset({SemanticPredicate(count, 3)}), max_number_free_instantiations: int = 10, max_number_smt_instantiations: int = 10, max_number_tree_insertion_results: int = 5, enforce_unique_trees_in_queue: bool = False, debug: bool = False, cost_computer: Optional[ForwardRef('CostComputer')] = None, timeout_seconds: Optional[int] = None, global_fuzzer: bool = False, predicates_unique_in_int_arg: Tuple[isla.language.SemanticPredicate, ...] = (SemanticPredicate(count, 3),), fuzzer_factory: Callable[[Mapping[str, Sequence[str]]], isla.fuzzer.GrammarFuzzer] = <function SolverDefaults.>, tree_insertion_methods: Optional[int] = None, activate_unsat_support: bool = False, grammar_unwinding_threshold: int = 4, initial_tree: returns.maybe.Maybe[isla.derivation_tree.DerivationTree] = , enable_optimized_z3_queries: bool = True, start_symbol: Optional[str] = None):

:class:~isla.solver.ISLaSolver 构造函数接受大量参数。然而,除了第一个参数 :code:grammar 之外,其余的都是可选的。

参数。但是,除了第一个参数 :code:grammar 之外,其余的都是可选的。

构建 ISLa 求解器的最简单方法就是只向它提供一个

仅语法;它就像一个语法模糊器。

import random

random.seed(1)

import string

LANG_GRAMMAR = {

...     "":

...         [""],

...     "":

...         [" ; ", ""],

...     "":

...         [" := "],

...     "":

...         ["", ""],

...     "": list(string.ascii_lowercase),

...     "": list(string.digits)

... }

from isla.solver import ISLaSolver

solver = ISLaSolver(LANG_GRAMMAR)

str(solver.solve())

'd := 9'

str(solver.solve())

'v := n ; s := r'

:param grammar: 基础语法;可以是“Fuzzing Book”字典

或在 BNF 语法中。

:param formula: 要解决的公式;可以是字符串或易于解析的

公式。如果没有给出公式,则假定默认的true约束,并且

如果解算器回退到语法 fuzzer。产生的解决方案数量

然后将绑定到max_number_free_instantiations

:param structural_predicates: 解析公式时使用的结构谓词

公式。

:param semantic_predicates: 解析公式时使用的语义谓词

:param max_number_free_instantiations: 非终结符实例化的次数

都不受任何公式约束,应由基于覆盖率的 fuzzer 扩展。

:param max_number_smt_instantiations: 实例化通用整数量词的解决方案数量

应该被产生。

:param max_number_tree_insertion_results: 当

通过树插入解决存在量词。

:param enforce_unique_trees_in_queue: 如果为 true,则具有与队列中

已经存在的树在队列中被丢弃,不考虑

约束。

:param debug: 如果为 true,则关于状态演化的调试信息

收集,特别是在字段 state_tree 中。树的根在

field state_tree_root. 字段 costs 存储了计算的成本值

所有新节点。

:param cost_computer: 用于计算相关成本的CostComputer

将状态放入 ISLa 的队列中。

:param timeout_seconds: 解算器将在多少秒后终止。

:param global_fuzzer: 如果设置为 true,则仅使用一个基于覆盖率的

对象用于完成无约束的开放派生树

整个生成时间。这可能对某些目标有利;例如,我们

经验表明 CSV 运行速度明显更快。然而,达到的 k 路径

覆盖率可能会降低。

:param predicates_unique_in_int_arg: 在某些情况下需要

instantiating universal integer quantifiers. 提供的谓词应该

恰好有一个整数参数,并且对于恰好一个整数值成立

一旦所有其他参数都固定。

:param fuzzer_factory: 要使用的 fuzzer 的构造函数,用于实例化

"free"非终结符。

:param tree_insertion_methods: 要使用的存在量词插入方法的组合

通过树插入进行量词消除。全选择:DIRECT_EMBEDDING & CONTEXT_ADDITION

SELF_EMBEDDING & CONTEXT_ADDITION`.

:param activate_unsat_support: 如果假设公式可能

可能不可满足。这会触发对不可满足性的额外测试

降低输入生成性能,但可能确保终止(带有

求解器结果(对于不可满足的问题,求解器可以

否则将发散。

:param grammar_unwinding_threshold: 当查询 SMT 求解器时,ISLa 传递一个

涉及的非终端的语法正则表达式。如果该

如果涉及的语法不是正则的,我们在参考语法中展开相应的部分

深度达到grammar_unwinding_threshold。如果这个深度太浅,它可能会

发生方程等无法解决的情况;如果太深,它可能会

会严重影响性能(并且非常严重)。

:param initial_tree: 如果求解器应该从队列开始,则对应的初始输入树

不从树(<start>, None)开始。

:param enable_optimized_z3_queries: 启用 Z3 查询的预处理(主要是

与长度等事物相关的数值问题)。这可以提高性能

显著;然而,可能会发生某些问题无法解决的情况

不再需要。在这种情况下,此选项可以/应该被禁用。

:param start_symbol: 这是initial_tree的替代方案,用于从

一个不同的起始符号<start>。如果提供了start_symbol,则树

由单个根节点组成,其值为start_symbol,作为

初始树。">init() <a xlink:href="isla.solver.ipynb" xlink:title="check(self, inp: isla.derivation_tree.DerivationTree | str) -> bool:

评估给定的推导树是否满足传递给

的求解器。如果无法评估,则引发UnknownResultError

(例如,由于求解器超时或无法解决的语义谓词),则引发

评估)。

:param inp: 要评估的输入,可以是已解析的或字符串形式。

:return: 一个布尔值。">check() <a xlink:href="isla.solver.ipynb" xlink:title="parse(self, inp: str, nonterminal: str = '', skip_check: bool = False, silent: bool = False) -> isla.derivation_tree.DerivationTree:

解析给定的输入inp。如果输入不满足语法,则引发SyntaxError

如果它满足语法,则返回SemanticError,如果不满足约束

(仅在nonterminal<start>时进行检查),并返回解析

DerivationTree否则。

:param inp: 要解析的输入。

:param nonterminal: 如果提供了字符串,则从该非终端开始解析

对应于子语法的树。我们不检查语义

在那种情况下可能不正确。

:param skip_check: 如果为 True,则省略语义检查。

:param silent: 如果为 True,则在发生错误时不会将错误发送到日志流。

解析失败。

:return: 解析后的 DerivationTree。《parse() <a xlink:href="isla.solver.ipynb" xlink:title="solve(self) -> isla.derivation_tree.DerivationTree:

尝试计算给定 ISLa 公式的解决方案。返回该解决方案,

如果有任何。此函数可以重复调用以获取更多解决方案,直到

会引发两种异常类型之一:一个 :class:StopIteration 表示

没有更多解决方案可找到;如果超时,将引发一个 :class:TimeoutError

发生。之后,每次都会引发异常。

超时可以通过 :code:timeout_seconds

:meth:构造函数 <isla.solver.ISLaSolver.__init__> 参数。

:return: 传递给 ISLa 公式的解决方案

:class:isla.solver.ISLaSolver。《solve() 图例 图例 •  public_method() •  private_method() •  overloaded_method() 将鼠标悬停在名称上以查看文档

ISLa 功能性也适用于命令行:

>>> !isla --help
usage: isla [-h] [-v]
            {solve,fuzz,check,find,parse,repair,mutate,create,config} ...

The ISLa command line interface.

options:
  -h, --help            show this help message and exit
  -v, --version         Print the ISLa version number

Commands:
  {solve,fuzz,check,find,parse,repair,mutate,create,config}
    solve               create solutions to ISLa constraints or check their
                        unsatisfiability
    fuzz                pass solutions to an ISLa constraint to a test subject
    check               check whether an input satisfies an ISLa constraint
    find                filter files satisfying syntactic & semantic
                        constraints
    parse               parse an input into a derivation tree if it satisfies
                        an ISLa constraint
    repair              try to repair an existing input such that it satisfies
                        an ISLa constraint
    mutate              mutate an input such that the result satisfies an ISLa
                        constraint
    create              create grammar and constraint stubs
    config              dumps the default configuration file 

语义输入属性

在这本书中,我们经常使用语法来系统地生成输入,以覆盖输入结构等。但是,虽然使用语法表达输入的语法相对容易,但有些输入属性是无法使用语法表达的。这些输入属性被称为语义属性。

让我们用一个简单的例子来说明语义属性。我们想要测试一个由两个设置配置的系统,一个是页面大小,另一个是缓冲区大小。这两个设置都作为整数数字作为人类可读配置文件的一部分。该文件的语法由以下语法给出:

from Grammars import Grammar, is_valid_grammar, syntax_diagram, crange 
import [string](https://docs.python.org/3/library/string.html) 
CONFIG_GRAMMAR: Grammar = {
    "<start>": ["<config>"],
    "<config>": [
        "pagesize=<pagesize>\n"
        "bufsize=<bufsize>"
    ],
    "<pagesize>": ["<int>"],
    "<bufsize>": ["<int>"],
    "<int>": ["<leaddigit><digits>"],
    "<digits>": ["", "<digit><digits>"],
    "<digit>": list("0123456789"),
    "<leaddigit>": list("123456789")
} 
assert is_valid_grammar(CONFIG_GRAMMAR) 

这是一个将此语法作为铁路图可视化的例子,显示了其结构:

start

config

config

pagesize= pagesize bufsize= bufsize

pagesize

int

bufsize

int

int

leaddigit digits

digits

digit digits

digit

0 1 2 3 4 5 6 7 8 9

leaddigit

1 2 3 4 5 6 7 8 9

使用这个语法,我们现在可以使用我们的任何基于语法的模糊测试器来生成有效输入。例如:

from GrammarFuzzer import GrammarFuzzer, DerivationTree 
fuzzer = GrammarFuzzer(CONFIG_GRAMMAR) 
for i in range(10):
    print(i)
    print(fuzzer.fuzz()) 
0
pagesize=4057
bufsize=817
1
pagesize=9
bufsize=8
2
pagesize=5
bufsize=25
3
pagesize=1
bufsize=2
4
pagesize=62
bufsize=57
5
pagesize=2
bufsize=893
6
pagesize=1
bufsize=33
7
pagesize=7537
bufsize=3
8
pagesize=97
bufsize=983
9
pagesize=2
bufsize=2

到目前为止,一切顺利——确实,这些随机值将帮助我们测试我们的(假设的)系统。但如果我们想进一步控制这些值,对系统进行测试呢?

语法给我们带来了一定的控制。例如,如果我们想确保页面大小至少为 100,000,那么可以设置如下规则:

"<bufsize>": ["<leaddigit><digit><digit><digit><digit><digit>"] 

就可以完成这项工作。我们也可以通过使其以奇数位结束来表达页面大小应该是奇数。但如果我们想声明页面大小应该是 8 的倍数,或者更大或小于缓冲区大小,我们就无能为力了。

在关于使用生成器的模糊测试章节中,我们看到了如何将程序代码附加到单个规则上——程序代码可以立即生成单个元素,或者只过滤满足特定条件的元素。

附加代码使事情变得非常灵活,但也存在几个缺点:

  • 首先,同时满足多个约束条件是非常困难的。本质上,你必须编写自己的策略来生成输入,这在某种程度上抵消了拥有像语法这样的抽象表示的优势。

  • 第二,你的代码是不可移植的。虽然语法可以很容易地适应任何基于语法的模糊测试器,但添加,比如说,Python 代码,将你永远绑定在 Python 环境中。

  • 第三,程序代码只能用于生成输入或检查输入,但不能两者兼得。这又与纯语法表示相比是一个缺点。

因此,我们正在寻找一种更通用的方式来表达语义属性——以及一种更声明式的方式来表达语义属性。

无限制语法

解决这个问题的一个非常通用的方法就是使用无限制语法,而不是我们迄今为止使用的上下文无关语法。在一个无限制语法中,一个可以在扩展规则的左侧有多个符号,这使得它们非常灵活。事实上,无限制语法是图灵通用的,这意味着它们可以表达任何在程序代码中也可以表达的特征;因此,它们可以检查和生成具有任意特征的任意字符串。(如果它们完成了,那就是说——无限制语法也受到停机问题的困扰。)缺点是实际上没有对无限制语法的编程支持——我们不得不从头开始在一个语法中实现所有算术、字符串和其他功能,这——嗯——不是很有趣。

指定约束

在最近的研究中,多米尼克·施泰因霍费尔安德烈亚斯·策勒(本书的作者之一)提出了一种基础设施,允许生成具有任意属性的输入,但无需麻烦地实现生成器或检查器。相反,他们建议一种专门用于指定输入的语言,称为ISLa(输入指定语言)。ISLa结合了一个标准的上下文无关语法和表达输入及其元素语义属性约束。ISLa 可以用作模糊器(生成满足约束的输入)以及检查器(检查输入是否满足给定的约束)。

让我们通过例子来说明 ISLa。ISLa 是一个名为isla-solver的 Python 包,可以使用pip轻松安装:

$  pip  install  isla-solver 

这也将安装所有依赖包。

ISLa 的核心是ISLa 求解器——实际解决约束以生成满足输入的组件。

import [isla](https://rindphi.github.io/isla/) 
from [isla.solver](https://rindphi.github.io/isla/) import ISLaSolver 

ISLaSolver的构造函数接受两个必选参数。

  • 语法是求解器应该从中生成输入的语法。

  • 约束是指产生的输入应该满足的约束。

要表达一个约束,我们有各种函数谓词可供选择。这些可以应用于语法的单个元素,特别是它们的非终结符。例如,函数str.len()返回字符串的长度。如果我们想要有页面大小至少有 6 位数的输入,我们可以这样写:

solver = ISLaSolver(CONFIG_GRAMMAR, 'str.len(<pagesize>) >= 6') 

solve()方法返回 ISLa 求解器产生的下一个字符串,作为一个推导树(见关于使用语法进行模糊测试的章节)。要将这些转换为字符串,我们可以使用str()转换器:

str(solver.solve()) 
'pagesize=111534185\nbufsize=3'

print()方法会隐式地将其参数转换为字符串。要获取,比如说,下一个 10 个解决方案,我们可以这样写:

for _ in range(10):
    print(solver.solve()) 
pagesize=111534185
bufsize=402493567181
pagesize=111534185
bufsize=1
pagesize=111534185
bufsize=96
pagesize=111534185
bufsize=81
pagesize=111534185
bufsize=514
pagesize=111534185
bufsize=2
pagesize=111534185
bufsize=635
pagesize=111534185
bufsize=7
pagesize=111534185
bufsize=3
pagesize=25395746815885
bufsize=44

...我们看到,确实,每个页面大小正好有六位数字。

推导树

如果你直接检查solve()返回的推导树,你会得到一个非常复杂的结构:

solution = solver.solve()
solution 
DerivationTree('<start>', (DerivationTree('<config>', (DerivationTree('pagesize=', (), id=2), DerivationTree('<pagesize>', (DerivationTree('<int>', (DerivationTree('<leaddigit>', (DerivationTree('2', (), id=824),), id=825), DerivationTree('<digits>', (DerivationTree('<digit>', (DerivationTree('5', (), id=834),), id=835), DerivationTree('<digits>', (DerivationTree('<digit>', (DerivationTree('3', (), id=844),), id=845), DerivationTree('<digits>', (DerivationTree('<digit>', (DerivationTree('9', (), id=854),), id=855), DerivationTree('<digits>', (DerivationTree('<digit>', (DerivationTree('5', (), id=864),), id=865), DerivationTree('<digits>', (DerivationTree('<digit>', (DerivationTree('7', (), id=874),), id=875), DerivationTree('<digits>', (DerivationTree('<digit>', (DerivationTree('4', (), id=884),), id=885), DerivationTree('<digits>', (DerivationTree('<digit>', (DerivationTree('6', (), id=894),), id=895), DerivationTree('<digits>', (DerivationTree('<digit>', (DerivationTree('8', (), id=904),), id=905), DerivationTree('<digits>', (DerivationTree('<digit>', (DerivationTree('1', (), id=914),), id=915), DerivationTree('<digits>', (DerivationTree('<digit>', (DerivationTree('5', (), id=924),), id=925), DerivationTree('<digits>', (DerivationTree('<digit>', (DerivationTree('8', (), id=934),), id=935), DerivationTree('<digits>', (DerivationTree('<digit>', (DerivationTree('8', (), id=944),), id=945), DerivationTree('<digits>', (DerivationTree('<digit>', (DerivationTree('5', (), id=954),), id=955), DerivationTree('<digits>', (), id=959)), id=948)), id=938)), id=928)), id=918)), id=908)), id=898)), id=888)), id=878)), id=868)), id=858)), id=848)), id=838)), id=828)), id=819),), id=816), DerivationTree('\nbufsize=', (), id=4), DerivationTree('<bufsize>', (DerivationTree('<int>', (DerivationTree('<leaddigit>', (DerivationTree('3', (), id=1614),), id=1621), DerivationTree('<digits>', (DerivationTree('<digit>', (DerivationTree('0', (), id=1630),), id=1640), DerivationTree('<digits>', (DerivationTree('<digit>', (DerivationTree('1', (), id=1646),), id=1655), DerivationTree('<digits>', (DerivationTree('', (), id=1641),), id=1644)), id=1629)), id=1625)), id=1611),), id=1608)), id=1),), id=0)

我们可以轻松地可视化树,揭示其结构:

display_tree(solution) 

0 1 0->1 2 pagesize= 1->2 3 1->3 47 \nbufsize= 1->47 48 1->48 4 3->4 5 4->5 7 4->7 6 2 (50) 5->6 8 7->8 10 7->10 9 5 (53) 8->9 11 10->11 13 10->13 12 3 (51) 11->12 14 13->14 16 13->16 15 9 (57) 14->15 17 16->17 19 16->19 18 5 (53) 17->18 20 19->20 22 19->22 21 7 (55) 20->21 23 22->23 25 22->25 24 4 (52) 23->24 26 25->26 28 25->28 27 6 (54) 26->27 29 28->29 31 28->31 30 8 (56) 29->30 32 31->32 34 31->34 33 1 (49) 32->33 35 34->35 37 34->37 36 5 (53) 35->36 38 37->38 40 37->40 39 8 (56) 38->39 41 40->41 43 40->43 42 8 (56) 41->42 44 43->44 46 43->46 45 5 (53) 44->45 49 48->49 50 49->50 52 49->52 51 3 (51) 50->51 53 52->53 55 52->55 54 0 (48) 53->54 56 55->56 58 55->58 57 1 (49) 56->57 59 58->59

通过将推导树转换为字符串,我们得到表示的字符串:

str(solution) 
'pagesize=25395746815885\nbufsize=301'

print() 隐式地执行此操作,因此打印解决方案会给我们字符串:

print(solution) 
pagesize=25395746815885
bufsize=301

除非你想检查推导树或访问其元素,将其转换为字符串会使它更容易管理。

要表达最小数值,我们可以使用更优雅的方式。例如,函数 str.to.int() 将字符串转换为整数。为了获得至少 100000 的页面大小,我们也可以这样写

solver = ISLaSolver(CONFIG_GRAMMAR,
                    'str.to.int(<pagesize>) >= 100000') 
print(solver.solve()) 
pagesize=100005
bufsize=25

如果我们想使页面大小在 100 到 200 之间,我们可以将其表述为一个逻辑合取(使用 and

solver = ISLaSolver(CONFIG_GRAMMAR, 
  '''
 str.to.int(<pagesize>) >= 100 and 
 str.to.int(<pagesize>) <= 200
 ''')
print(solver.solve()) 
pagesize=168
bufsize=9

如果我们想使页面大小是七的倍数,我们可以这样写

solver = ISLaSolver(CONFIG_GRAMMAR, 
  '''
 str.to.int(<pagesize>) mod 7 = 0
 ''')
print(solver.solve()) 
pagesize=7
bufsize=7

from [bookutils](https://github.com/uds-se/fuzzingbook//tree/master/notebooks/shared/bookutils) import quiz 

习题

以下哪个约束表示页面大小和缓冲区大小必须相等?试一试!

事实上,ISLa 约束也可以涉及多个元素。表达两个元素之间的等式很简单,使用单个等号。在 ISLa 中没有赋值操作(= 可能会引起混淆)。

solver = ISLaSolver(CONFIG_GRAMMAR, 
  '''
 <pagesize> = <bufsize>
 ''')
print(solver.solve()) 
pagesize=8
bufsize=8

我们还可以使用数值约束,声明缓冲区大小应该总是比页面大小多一个:

solver = ISLaSolver(CONFIG_GRAMMAR, 
  '''
 str.to.int(<pagesize>) > 1024 and
 str.to.int(<bufsize>) = str.to.int(<pagesize>) + 1
 ''')
print(solver.solve()) 
pagesize=1025
bufsize=1026

所有上述功能(如 str.to.int())实际上都源于 SMT-LIB 库,用于 可满足性模理论(SMT),这是表达约束求解器(如 ISLa)约束的标准。SMT-LIB 中定义的所有理论列表 列出了数十个可以在 ISLa 约束中使用的函数和谓词。

习题

以下哪些约束是确保所有数字都在 1 到 3 之间的必要条件?

使用 SMT-LIB 语法

除了上述对程序员熟悉的“中置”语法之外,ISLa 还支持完整的 SMT-LIB 语法。SMT-LIB 使用类似于 LISP 的“前缀”语法来表示函数和运算符,其中所有函数和运算符都写作 (f x_1 x_2 x_3 ...)。因此,上述谓词将被写成

solver = ISLaSolver(CONFIG_GRAMMAR, 
  '''
 (and
 (> (str.to.int <pagesize>) 1024)
 (= (str.to.int <bufsize>)
 (+ (str.to.int <pagesize>) 1)))
 ''')
print(solver.solve()) 
pagesize=1025
bufsize=1026

注意,对于布尔运算符,如 and,我们仍然使用 ISLa 中置语法;让 ISLa 处理这些运算符比将它们传递给约束求解器更有效率。

ISLa 命令行

当你安装 isla-solver 时,你也会得到一个 isla 命令行工具。这允许你从命令行或 shell 脚本创建输入。

让我们首先创建一个适合 isla语法文件isla 接受 Fuzzingbook 格式的语法;它们需要定义一个名为 grammar 的变量。

with open('grammar.py', 'w') as f:
    f.write('grammar = ')
    f.write(str(CONFIG_GRAMMAR)) 
from [bookutils](https://github.com/uds-se/fuzzingbook//tree/master/notebooks/shared/bookutils) import print_file 
print_file('grammar.py') 
grammar = {'<start>': ['<config>'], '<config>': ['pagesize=<pagesize>\nbufsize=<bufsize>'], '<pagesize>': ['<int>'], '<bufsize>': ['<int>'], '<int>': ['<leaddigit><digits>'], '<digits>': ['', '<digit><digits>'], '<digit>': ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'], '<leaddigit>': ['1', '2', '3', '4', '5', '6', '7', '8', '9']}

使用这种方式,我们可以简单地将 isla 作为语法模糊器使用。默认情况下,isla solve 产生单个匹配输出:

!isla  solve  grammar.py 
pagesize=5
bufsize=81452639708

然而,isla 的真正威力在于我们(再次)添加要解决的 约束 - 要么在单独的 约束文件 中,要么(更简单)直接在命令行上:

!isla  solve  grammar.py  --constraint  '<pagesize> = <bufsize>' 
pagesize=8
bufsize=8

!isla  solve  grammar.py  \
    --constraint '<pagesize> = <bufsize> and str.to.int(<pagesize>) > 10000' 
pagesize=99290248
bufsize=99290248

isla 可执行文件提供了几个选项和命令,并且在命令行上是一个很好的替代品。

!isla  --help 
usage: isla [-h] [-v]
            {solve,fuzz,check,find,parse,repair,mutate,create,config} ...

The ISLa command line interface.

options:
  -h, --help            show this help message and exit
  -v, --version         Print the ISLa version number

Commands:
  {solve,fuzz,check,find,parse,repair,mutate,create,config}
    solve               create solutions to ISLa constraints or check their
                        unsatisfiability
    fuzz                pass solutions to an ISLa constraint to a test subject
    check               check whether an input satisfies an ISLa constraint
    find                filter files satisfying syntactic & semantic
                        constraints
    parse               parse an input into a derivation tree if it satisfies
                        an ISLa constraint
    repair              try to repair an existing input such that it satisfies
                        an ISLa constraint
    mutate              mutate an input such that the result satisfies an ISLa
                        constraint
    create              create grammar and constraint stubs
    config              dumps the default configuration file

访问元素

到目前为止,我们只是通过引用它们的名称来访问非终结符,例如 <bufsize><pagesize>。然而,在某些情况下,这种方法并不足够。例如,在我们的配置语法中,我们可能想要访问(或约束)<int> 元素。但是,我们不想一次约束所有的整数,而只想约束特定上下文中的那些——比如说,那些作为 <pagesize> 元素一部分出现的,或者只那些作为 <config> 元素一部分出现的。

为了达到这个目的,ISLa 允许使用两个特殊操作符来引用给定元素的部分。

表达式 <a>.<b> 指的是某个元素 <a>直接子部分 <b>。也就是说,<b> 必须出现在 <a> 的某个展开规则中。例如,<pagesize>.<int> 指的是页面大小的 <int> 元素。以下是一个使用点操作符的示例:

solver = ISLaSolver(CONFIG_GRAMMAR, 
  '''
 <pagesize>.<int> = <bufsize>.<int>
 ''')
print(solver.solve()) 
pagesize=8
bufsize=8

然而,表达式 <a>..<b> 指的是某个元素 <a>任何子部分 <b>。也就是说,<b> 可以出现在 <a> 的展开中,也可以出现在任何子元素(及其子元素)的展开中。以下是一个使用双点操作符的示例,强制 <config> 元素中的每个数字都是 7

from ExpectError import ExpectError 
solver = ISLaSolver(CONFIG_GRAMMAR, 
  '''
 <config>..<digit> = "7" and <config>..<leaddigit> = "7"
 ''')
print(solver.solve()) 
pagesize=77
bufsize=7

要理解点和双点,将问题字符串可视化为推导树有助于理解,该推导树在基于语法的模糊测试章节中讨论过。输入的推导树如下所示:

pagesize=12
bufsize=34

例如,看起来是这样的:

0 1 0->1 2 pagesize= 1->2 3 1->3 11 \nbufsize= 1->11 12 1->12 4 3->4 5 4->5 7 4->7 6 1 (49) 5->6 8 7->8 10 7->10 9 2 (50) 8->9 13 12->13 14 13->14 16 13->16 15 3 (51) 14->15 17 16->17 19 16->19 18 4 (52) 17->18

在这个树中,. 语法指的是直接子元素。<bufsize>.<int> 是一个 <int> 节点,它是 <bufsize> 的直接后裔(但不是任何其他 <int> 节点)。相比之下,<config>..<digit> 指的是 <config> 节点的所有 <digit> 后裔。

如果一个元素有多个相同类型的直接子元素,可以使用特殊 <a>[$n$] 语法来访问类型为 <a> 的第 \(n\) 个子元素。要访问第一个子元素,\(n\) 等于一,而不是零,就像在 XPath 简写语法 中一样。在我们的配置语法中,没有包含相同非终结符多次展开的情况,因此我们不需要这个功能。

为了演示索引点,考虑以下语法,它生成由三个 "a" 或 "b" 字符组成的行:

LINES_OF_THREE_AS_OR_BS_GRAMMAR: Grammar = {
    '<start>': ['<A>'],
    '<A>': ['<B><B><B>', '<B><B><B>\n<A>'],
    '<B>': ['a', 'b']
} 
fuzzer = GrammarFuzzer(LINES_OF_THREE_AS_OR_BS_GRAMMAR)
for i in range(5):
    print(i)
    print(fuzzer.fuzz()) 
0
aab
bab
1
aaa
aba
2
aba
baa
bba
3
bbb
abb
4
baa

我们可以强制,比如说,一行中的第二个字符始终是 "b:"

solver = ISLaSolver(LINES_OF_THREE_AS_OR_BS_GRAMMAR, 
  '''
 <A>.<B>[2] = "b"
 ''')

for i in range(5):
    print(i)
    print(solver.solve()) 
0
abb
1
bbb
2
abb
3
bbb
4
abb

量词

默认情况下,ISLa 约束中的所有非终结符都是普遍量化的——也就是说,任何应用于,比如说,某些 <int> 元素的约束都应用于结果字符串中的所有 <int> 元素。但是,如果你只想约束一个元素,你必须在 ISLa 中指定这一点(并且可以这样做),使用存在量词

要在 ISLa 中使用存在量词,使用以下构造

exists TYPE VARIABLE in CONTEXT:
    (CONSTRAINT)

其中VARIABLE是某个标识符,TYPE是其类型(作为一个非终端),CONTEXT是约束应该成立的上下文(再次是一个非终端)。CONSTRAINT再次是一个约束表达式,你现在可以使用VARIABLE作为你假设存在的元素。

让我们再次用一个简单的例子来说明存在量词。我们想要确保在我们生成的字符串中至少有一个整数的值大于 1000。因此我们写

solver = ISLaSolver(CONFIG_GRAMMAR, 
  '''
 exists <int> i in <start>:
 str.to.int(i) > 1000
 ''')

for _ in range(10):
    print(solver.solve()) 
pagesize=1007
bufsize=5
pagesize=1007
bufsize=2280617349521
pagesize=1007
bufsize=8
pagesize=1007
bufsize=7
pagesize=1007
bufsize=3
pagesize=1007
bufsize=93
pagesize=1007
bufsize=630
pagesize=1007
bufsize=4
pagesize=1007
bufsize=14
pagesize=1007
bufsize=5

我们注意到所有生成的输入都满足至少有一个整数满足约束的要求。

指定变量名是可选的;如果你省略它,你可以使用量词非终端。因此,上述约束也可以用更紧凑的方式表达:

solver = ISLaSolver(CONFIG_GRAMMAR, 
  '''
 exists <int> in <start>:
 str.to.int(<int>) > 1000
 ''')

print(solver.solve()) 
pagesize=1003
bufsize=64

除了存在量词之外,还有全称量词,使用forall关键字代替exists。如果我们想要某个上下文中的所有元素都满足一个约束,这将很有用。

solver = ISLaSolver(CONFIG_GRAMMAR, 
  '''
 forall <int> in <start>:
 str.to.int(<int>) > 1000
 ''')

for _ in range(10):
    print(solver.solve()) 
pagesize=1001
bufsize=1001
pagesize=1001
bufsize=1002
pagesize=1001
bufsize=1003
pagesize=1001
bufsize=1004
pagesize=1002
bufsize=1001
pagesize=1002
bufsize=1002
pagesize=1002
bufsize=1003
pagesize=1002
bufsize=1004
pagesize=1003
bufsize=1001
pagesize=1003
bufsize=1002

我们可以看到所有<int>元素都满足约束。

默认情况下,所有直接在约束中重用的非终端在<start>符号内都是全称量词,所以上述实际上可以简化为

solver = ISLaSolver(CONFIG_GRAMMAR, 
  '''
 str.to.int(<int>) > 1000
 ''')

for _ in range(10):
    print(solver.solve()) 
pagesize=1001
bufsize=1001
pagesize=1001
bufsize=1002
pagesize=1001
bufsize=1003
pagesize=1001
bufsize=1004
pagesize=1002
bufsize=1001
pagesize=1002
bufsize=1002
pagesize=1002
bufsize=1003
pagesize=1002
bufsize=1004
pagesize=1003
bufsize=1001
pagesize=1003
bufsize=1002

...然后你意识到在我们所有的初始约束中,我们总是有一个隐含的全称量词。

选择扩展

有时,我们希望量词只适用于非终端的一个特定扩展。形式

forall TYPE VARIABLE=PATTERN in CONTEXT:
    (CONSTRAINT)

表示约束只适用于与模式中给出的扩展匹配的实际变量。 (再次,我们可以用exists替换forall,使其成为一个存在量词而不是全称量词。)

这里是使用forall的一个例子:

solver = ISLaSolver(CONFIG_GRAMMAR, 
  '''
 forall <int>="<leaddigit><digits>" in <start>:
 (<leaddigit> = "7")
 ''') 

这确保了当<int>扩展为一个前导数字后跟更多数字时,前导数字变为7。结果是所有<int>值现在都以一个7位数字开头:

str(solver.solve()) 
'pagesize=71\nbufsize=770835426929713'

同样,我们可以约束整个<int>,从而确保所有数字都大于 100:

solver = ISLaSolver(CONFIG_GRAMMAR, 
  '''
 forall <int> in <start>:
 (str.to.int(<int>) > 100)
 ''')

str(solver.solve()) 
'pagesize=101\nbufsize=101'

默认情况下,所有变量在<start>中都是全称量词,所以上述也可以表示为

solver = ISLaSolver(CONFIG_GRAMMAR, 
  '''
 str.to.int(<int>) > 100
 ''')

str(solver.solve()) 
'pagesize=101\nbufsize=101'

匹配扩展元素

在一个量词模式中,我们也可以为单个非终端元素命名并在约束中使用它们。这是通过将非终端<ID>替换为特殊形式{<ID> VARIABLE}(用大括号括起来)来完成的,这使得变量VARIABLE成为由ID匹配的值的占位符;VARIABLE然后可以在约束中使用。

这里有一个例子。在扩展<leaddigit><int>中,我们想要确保<leaddigit>始终是9。使用特殊的括号形式,我们将lead设为一个变量,其值为<leaddigit>,然后可以在约束中使用它:

solver = ISLaSolver(CONFIG_GRAMMAR, 
  '''
 forall <int> i="{<leaddigit> lead}<digits>" in <start>:
 (lead = "9")
 ''') 

这(再次)确保了所有前导数字应该是9

for _ in range(10):
    print(solver.solve()) 
pagesize=92
bufsize=9168435097796
pagesize=9
bufsize=9188
pagesize=981
bufsize=999
pagesize=9
bufsize=9
pagesize=9
bufsize=9
pagesize=9
bufsize=9
pagesize=91
bufsize=9
pagesize=9
bufsize=9
pagesize=90
bufsize=9
pagesize=9
bufsize=9242

我们能否用更简单的方式表达上述内容?是的!首先,我们可以直接引用<leaddigit>而不是引入像ilead这样的变量:

solver = ISLaSolver(CONFIG_GRAMMAR, 
  '''
 forall <int>="<leaddigit><digits>" in <start>:
 (<leaddigit> = "9")
 ''')
print(solver.solve()) 
pagesize=9
bufsize=941890257631

此外,使用隐含的全称量词和之前引入的点符号,我们可以写出,例如

solver = ISLaSolver(CONFIG_GRAMMAR, 
  '''
 <int>.<leaddigit> = "9"
 ''')
print(solver.solve()) 
pagesize=99
bufsize=9501387624

或者,也可以

solver = ISLaSolver(CONFIG_GRAMMAR, 
  '''
 <leaddigit> = "9"
 ''')
print(solver.solve()) 
pagesize=98
bufsize=93762401955

并获得相同的结果(尽管由于随机性,结果可能不是完全相同的值):

但是,尽管全称量词和点符号对于许多情况来说是足够的,模式匹配符号更加通用和灵活——即使它可能更难阅读。

检查字符串

使用ISLaSolver,我们还可以检查一个字符串是否满足约束。这可以应用于输入,也可以应用于输出;因此,ISLa 约束可以作为预言机——即检查测试结果的谓词

让我们检查在给定的字符串中,<pagesize><bufsize>是否相同。

constraint = '<pagesize> = <bufsize>'
solver = ISLaSolver(CONFIG_GRAMMAR, constraint) 

要检查树,我们可以将其传递给solver对象的evaluate()方法——并发现给定的输入满足我们的约束。

solver.check('pagesize=12\nbufsize=34') 
False

然而,如果我们用满足约束的输入重复上述操作,我们将获得一个True结果。

solver.check('pagesize=27\nbufsize=27') 
True

检查约束比解决约束更有效,因为 ISLa 不需要搜索可能的解决方案。

案例研究

让我们进一步通过几个案例研究来阐述 ISLa。

在 XML 中匹配标识符

可扩展标记语言(XML)是输入语言无法完全用上下文无关文法表达的典型例子。问题并不在于表达 XML 的语法——基础相当简单:

XML_GRAMMAR: Grammar = {
    "<start>": ["<xml-tree>"],
    "<xml-tree>": ["<open-tag><xml-content><close-tag>"],
    "<open-tag>": ["<<id>>"],
    "<close-tag>": ["</<id>>"],
    "<xml-content>": ["Text", "<xml-tree>"],
    "<id>": ["<letter>", "<id><letter>"],
    "<letter>": crange('a', 'z')
} 
assert is_valid_grammar(XML_GRAMMAR) 
start

xml-tree

xml-tree

open-tag xml-content close-tag

open-tag

< id >

close-tag

</ id >

xml-content

Text xml-tree

id

letter id letter

letter

b a c d e g f h i j l k m n o q p r s t v u w x y z

当我们从文法生成输入时,问题变得明显:<open-tag><close-tag>中的<id>元素不匹配。

fuzzer = GrammarFuzzer(XML_GRAMMAR)
fuzzer.fuzz() 
'<xdps><s><x><f>Text</g></ka></k></hk>'

如果我们想要标签 ID 匹配,我们需要提出一个有限的标签集(比如,HTML 中的标签);然后我们可以为每个标签扩展一个规则——<body>...</body><p>...</p><strong>...</strong>等等。但对于一个无限的标签集,就像我们的语法一样,在上下文无关文法中表达两个标签 ID 必须匹配是不可能的。

然而,使用 ISLa,约束文法是很容易的。我们需要的只是约束<xml-tree>的规则:

solver = ISLaSolver(XML_GRAMMAR, 
  '''
 <xml-tree>.<open-tag>.<id> = <xml-tree>.<close-tag>.<id>
 ''', max_number_smt_instantiations=1)

for _ in range(3):
    print(solver.solve()) 
<p>Text</p>
<p><p>Text</p></p>
<p><p><p>Text</p></p></p>

我们看到,<id>标签现在确实相互匹配。

<details id="Excursion:-Solver-Configuration-Parameters"><summary>Solver Configuration Parameters</summary>

我们传递给ISLaSolver对象的配置参数max_number_smt_instantiations限制了 ISLa 底层 SMT 求解器的调用次数。一般来说,更高的数字会导致每次生成更多的输入。尽管许多输入看起来结构上相似。如果我们旨在生成结构多样化的输入,并且不关心,例如,标签的名称,那么为这个参数选择一个较低的值是有意义的。这就是max_number_smt_instantiations=10发生的情况,这是当前的默认值:

solver = ISLaSolver(XML_GRAMMAR, 
  '''
 <xml-tree>.<open-tag>.<id> = <xml-tree>.<close-tag>.<id>
 ''', max_number_smt_instantiations=10)

for _ in range(3):
    print(solver.solve()) 
<p>Text</p>
<h>Text</h>
<k>Text</k>

参数 max_number_free_instantiations 具有类似的作用:ISLa 随机实例化非终结符号,其值不受约束的限制。它选择——惊喜!——最多 max_number_free_instantiations 这样随机的实例化。

其他有趣的配置参数是 structural_predicatessemantic_predicates,它们允许你通过向求解器传递自定义的结构和语义谓词来扩展 ISLa 语言。你可以在 ISLa 约束中使用这些集合中的所有谓词来解决。默认情况下,语义谓词 count(in_tree, NEEDLE, NUM) 和以下结构谓词是可用的:

  • after(node_1, node_2)

  • before(node_1, node_2)

  • consecutive(node_1, node_2)

  • count(in_tree, NEEDLE, NUM)

  • different_position(node_1, node_2)

  • direct_child(node_1, node_2)

  • inside(node_1, node_2)

  • level(PRED, NONTERMINAL, node_1, node_2)

  • nth(N, node_1, node_2)

  • same_position(node_1, node_2)

与 生成器章节 中的“输入生成器”解决方案相比,我们的基于约束的解决方案是纯声明性的——也可以用于解析和检查输入。当然,我们还可以轻松地添加更多约束:

solver = ISLaSolver(XML_GRAMMAR, 
  '''
 <xml-tree>.<open-tag>.<id> = <xml-tree>.<close-tag>.<id>
 and
 str.len(<id>) > 10
 ''', max_number_smt_instantiations=1)

for _ in range(3):
    print(solver.solve()) 
<ppkphklftkp>Text</ppkphklftkp>
<ppkphklftkp><ppnpxcqktdh>Text</ppnpxcqktdh></ppkphklftkp>
<ppkphklftkp><ppnpxcqktdh><ppkxsixmahk>Text</ppkxsixmahk></ppnpxcqktdh></ppkphklftkp>

编程语言中的定义和用法

在使用生成的程序代码测试编译器时,人们经常遇到一个问题,即在 使用 一个标识符之前,必须先 声明 它——指定其类型、一些初始值等等。

在以下语法中,这个问题很容易说明,该语法产生 赋值序列。变量名由单个小写字母组成;值只能是数字;赋值由分号分隔。

LANG_GRAMMAR: Grammar = {
    "<start>":
        ["<stmt>"],
    "<stmt>":
        ["<assgn>", "<assgn>; <stmt>"],
    "<assgn>":
        ["<lhs> := <rhs>"],
    "<lhs>":
        ["<var>"],
    "<rhs>":
        ["<var>", "<digit>"],
    "<var>": list(string.ascii_lowercase),
    "<digit>": list(string.digits)
} 
assert is_valid_grammar(LANG_GRAMMAR) 
syntax_diagram(LANG_GRAMMAR) 
start

stmt

stmt

assgn assgn ; stmt

assgn

lhs := rhs

lhs

var

rhs

var digit

var

b a c d e g f h i j l k m n o q p r s t v u w x y z

digit

0 1 2 3 4 5 6 7 8 9

这里是语法产生的某些赋值序列:

fuzzer = GrammarFuzzer(LANG_GRAMMAR) 
for _ in range(10):
    print(fuzzer.fuzz()) 
w := 7; g := 2
f := m; x := 3
p := 3
h := f; h := u
n := k; k := z; z := 6
m := x; g := h
d := 3
h := 6
s := m
k := q

我们可以看到,赋值 语法 与我们在常见编程语言中看到的是相似的。然而,语义 呢,嗯,值得怀疑,因为我们通常访问尚未定义的值的变量。再次强调,这是一个 语义 属性,仅凭上下文无关文法是无法表达的。

我们需要在这里指定一个约束,即赋值的右侧只能有出现在左侧的变量名。在 ISLa 中,我们通过以下约束来实现这一点:

solver = ISLaSolver(LANG_GRAMMAR, 
  '''
 forall <rhs> in <assgn>:
 exists <assgn> declaration:
 <rhs>.<var> = declaration.<lhs>.<var>
 ''',
            max_number_smt_instantiations=1,
            max_number_free_instantiations=1) 
for _ in range(10):
    print(solver.solve()) 
y := 1
a := 0; t := 5
h := h
u := 2; l := 4; p := 7
p := 3; r := 8; v := p
x := 9; b := 6; a := a
s := 4; h := 4; f := h
c := 5; p := p; k := 5
d := 5; q := d; n := 2
e := 6; m := p; p := 9

这已经好多了,但还不是完美的——我们可能仍然有像 a := aa := b; b := 5 这样的赋值。这是因为我们的约束还没有考虑到 顺序 —— 在 <rhs> 元素中,我们只能使用之前定义的变量。

为了这个目的,ISLa 提供了一个 before() 谓词:before(A, B) 表示元素 A 必须出现在元素 B 之前。使用 before(),我们可以将我们的约束重写为

solver = ISLaSolver(LANG_GRAMMAR, 
  '''
 forall <rhs> in <assgn>:
 exists <assgn> declaration:
 (before(declaration, <assgn>) and
 <rhs>.<var> = declaration.<lhs>.<var>)
 ''',
            max_number_free_instantiations=1,
            max_number_smt_instantiations=1) 

... 因此确保在赋值的右侧,我们只使用之前定义的标识符。

for _ in range(10):
    print(solver.solve()) 
p := 1
w := 4; o := 6
b := 3; p := b; b := p; f := b
p := 0; p := p; z := p; g := 2
a := 9; a := a; r := 8; i := a
a := 5; a := a; a := 7; t := a
h := 0; j := h; h := 0; s := h
h := 2; h := h; h := 0; u := h
b := 9; b := 5; p := b; b := p; e := b
b := 9; b := b; p := b; b := p; c := b

如果您发现分配序列太短,可以使用 ISLa 的 count() 谓词。count(VARIABLE, NONTERMINAL, N) 确保 VARIABLE 中的 NONTERMINAL 数量正好是 N。要编写具有恰好 5 个赋值的语句,请写

solver = ISLaSolver(LANG_GRAMMAR, 
  '''
 forall <rhs> in <assgn>:
 exists <assgn> declaration:
 (before(declaration, <assgn>) and
 <rhs>.<var> = declaration.<lhs>.<var>)
 and
 count(start, "<assgn>", "5")
 ''', 
            max_number_smt_instantiations=1,
            max_number_free_instantiations=1) 
for _ in range(10):
    print(solver.solve()) 
h := 8; y := h; c := 3; f := 9; a := 0
p := 2; k := p; p := 4; e := 7; o := p
h := 1; r := h; m := 6; g := 5; x := h
p := 5; h := p; w := 0; d := 2; s := h
p := 4; q := p; n := 0; p := 4; u := p
a := 5; i := a; b := a; z := 9; t := 6
p := 3; b := p; l := b; j := 0; v := 0
b := 3; d := b; q := d; p := 1; b := p
d := 4; l := d; l := d; p := 5; i := p
h := 5; d := h; o := d; b := 5; n := h

经验教训

  • 使用 ISLa,我们可以向语法添加并解决 约束,从而表达我们的测试输入的 语义属性

  • 声明约束(并让求解器解决它们)比添加生成器代码更灵活,而且也是语言无关的

  • 使用 ISLa 很有趣 😃

下一步

在接下来的章节中,我们将继续关注语义。其中之一,我们将学习如何

  • 从现有输入中提取语法

  • 使用 symbolic fuzzing - 即,使用约束求解器达到特定位置

  • 使用 concolic fuzzing - 即,将符号模糊测试与具体运行相结合以提高效率

背景

练习

练习 1:字符串编码

表示字符串的一种常见方式是 长度前缀字符串,这种表示方法由 PASCAL 编程语言普及。长度前缀字符串以几个字节开始,这些字节编码了字符串的长度 \(L\),然后是 \(L\) 个实际字符。例如,假设使用两个字节来编码长度,字符串 "Hello" 可以表示为以下序列

0x00 0x05 'H' 'e' 'l' 'l' 'o'

第一部分:语法

编写一个定义长度前缀字符串语法的语法。

使用笔记本 来完成练习并查看解决方案。

第二部分:语义

使用 ISLa 生成有效的长度前缀字符串。利用 SMT-LIB 字符串库 找到适当的转换函数。

使用笔记本 来完成练习并查看解决方案。

Creative Commons License 本项目的内容受 Creative Commons Attribution-NonCommercial-ShareAlike 4.0 国际许可协议 的许可。内容的一部分源代码,以及用于格式化和显示该内容的源代码,受 MIT 许可协议 的许可。 最后更改:2024-11-09 17:07:29+01:00 • 引用 • 印记

如何引用本作品

安德烈亚斯·策勒,拉胡尔·戈皮纳特,马塞尔·博 hme,戈登·弗莱泽,以及克里斯蒂安·霍勒:"带有约束的模糊测试"。收录于安德烈亚斯·策勒,拉胡尔·戈皮纳特,马塞尔·博 hme,戈登·弗莱泽,以及克里斯蒂安·霍勒所著的"模糊测试书籍"中。www.fuzzingbook.org/html/FuzzingWithConstraints.html。检索日期:2024-11-09 17:07:29+01:00.

@incollection{fuzzingbook2024:FuzzingWithConstraints,
    author = {Andreas Zeller and Rahul Gopinath and Marcel B{\"o}hme and Gordon Fraser and Christian Holler},
    booktitle = {The Fuzzing Book},
    title = {Fuzzing with Constraints},
    year = {2024},
    publisher = {CISPA Helmholtz Center for Information Security},
    howpublished = {\url{https://www.fuzzingbook.org/html/FuzzingWithConstraints.html}},
    note = {Retrieved 2024-11-09 17:07:29+01:00},
    url = {https://www.fuzzingbook.org/html/FuzzingWithConstraints.html},
    urldate = {2024-11-09 17:07:29+01:00}
}

矿输入语法

原文:www.fuzzingbook.org/html/GrammarMiner.html

到目前为止,我们看到的语法大多是手动指定的——也就是说,您(或了解输入格式的人)最初必须设计和编写一个语法。虽然我们迄今为止看到的语法相对简单,但为复杂输入创建语法可能需要相当多的努力。因此,在本章中,我们介绍了从程序中自动挖掘语法的技术——通过执行程序并观察它们如何处理输入的哪些部分。结合语法模糊器,这使我们能够

  1. 取一个程序,

  2. 提取其输入语法,并且

  3. 使用本书中的概念以高效率和效果进行模糊测试,

from [bookutils](https://github.com/uds-se/fuzzingbook//tree/master/notebooks/shared/bookutils) import YouTubeVideo
YouTubeVideo("ddM1oL2LYDI") 

先决条件

  • 您应该已经阅读了语法章节。

  • 配置模糊化章节介绍了配置选项的语法挖掘,以及执行过程中的变量和值观察。

  • 我们使用覆盖率章节中的跟踪器。

  • 从解析器章节中关于解析的概念也是很有用的。

概述

要使用本章节提供的代码(导入),请编写

>>> from fuzzingbook.GrammarMiner import <identifier> 

然后利用以下功能。

本章提供了一些类,可以从现有程序中挖掘输入语法。函数recover_grammar()可能是最容易使用的。它接受一个函数和一组输入,并返回一个描述其输入语言的语法。

我们在url_parse()函数上应用recover_grammar(),该函数接收并分解 URL:

>>> url_parse('https://www.fuzzingbook.org/')
>>> URLS
['http://user:pass@www.google.com:80/?q=path#ref',
 'https://www.cispa.saarland:80/',
 'http://www.fuzzingbook.org/#News'] 

我们使用recover_grammar()url_parse()中提取输入语法:

>>> grammar = recover_grammar(url_parse, URLS, files=['urllib/parse.py'])
>>> grammar
{'<start>': ['<urlsplit@452:url>'],
 '<urlsplit@452:url>': ['<urlparse@396:scheme>:<_splitnetloc@413:url>'],
 '<urlparse@396:scheme>': ['http', 'https'],
 '<_splitnetloc@413:url>': ['//<urlparse@396:netloc>/',
  '//<urlparse@396:netloc><urlsplit@494:url>'],
 '<urlparse@396:netloc>': ['www.cispa.saarland:80',
  'www.fuzzingbook.org',
  'user:pass@www.google.com:80'],
 '<urlsplit@494:url>': ['<urlsplit@502:url>#<urlparse@396:fragment>',
  '/#<urlparse@396:fragment>'],
 '<urlsplit@502:url>': ['/?<urlparse@396:query>'],
 '<urlparse@396:query>': ['q=path'],
 '<urlparse@396:fragment>': ['News', 'ref']} 

非终结符的名称有点技术性;但语法很好地代表了输入的结构;例如,不同的方案("http""https")都被识别出来:

>>> syntax_diagram(grammar)
start 

urlsplit@452:url

urlsplit@452:url

urlparse@396:scheme : _splitnetloc@413:url

urlparse@396:scheme

http https

_splitnetloc@413:url

// urlparse@396:netloc / // urlparse@396:netloc urlsplit@494:url

urlparse@396:netloc

www.cispa.saarland:80 www.fuzzingbook.org user:pass@www.google.com:80

urlsplit@494:url

urlsplit@502:url # urlparse@396:fragment /# urlparse@396:fragment

urlsplit@502:url

/? urlparse@396:query

urlparse@396:query

q=path

urlparse@396:fragment

News ref

该语法可以立即用于模糊测试,生成任意组合的输入元素,这些元素都是语法上有效的。

>>> from GrammarCoverageFuzzer import GrammarCoverageFuzzer
>>> fuzzer = GrammarCoverageFuzzer(grammar)
>>> [fuzzer.fuzz() for i in range(5)]
['https://www.cispa.saarland:80/#ref',
 'http://www.fuzzingbook.org/',
 'http://user:pass@www.google.com:80/?q=path#News',
 'https://www.fuzzingbook.org/?q=path#ref',
 'http://www.cispa.saarland:80/#News'] 

能够自动提取语法并使用该语法进行模糊测试,可以以最少的手动工作生成非常有效的测试。

语法挑战

考虑解析器章节中的process_inventory()方法:

import [bookutils.setup](https://github.com/uds-se/fuzzingbook//tree/master/notebooks/shared/bookutils) 
from [typing](https://docs.python.org/3/library/typing.html) import List, Tuple, Callable, Any
from [collections.abc](https://docs.python.org/3/library/collections.abc.html) import Iterable 
from Parser import process_inventory, process_vehicle, process_car, process_van, lr_graph  # minor dependency 

它接受以下形式的输入。

INVENTORY = """\
1997,van,Ford,E350
2000,car,Mercury,Cougar
1999,car,Chevy,Venture\
""" 
print(process_inventory(INVENTORY)) 
We have a Ford E350 van from 1997 vintage.
It is an old but reliable model!
We have a Mercury Cougar car from 2000 vintage.
It is an old but reliable model!
We have a Chevy Venture car from 1999 vintage.
It is an old but reliable model!

从解析器章节中我们发现,当输入格式包括仅在代码中表达的具体细节时,粗略的语法在模糊测试中效果不佳。也就是说,尽管我们有 CSV 文件的正式规范(RFC 4180),库存系统还包括针对 CSV 文件每个索引处期望内容的进一步规则。简单地重新组合现有输入虽然实用,但并不完整。特别是,它依赖于最初就存在正式的输入规范。然而,我们无法保证程序遵守所提供的输入规范。

解决这一困境的一种方法是对正在测试的程序进行询问,以了解其输入规范是什么。也就是说,如果正在测试的程序是以特定方法负责处理输入特定部分的方式编写的,那么可以通过观察解析过程来恢复解析树。此外,可以通过从多个输入树中抽象出合理的语法近似。

我们首先假设(1)程序是以这样的方式编写的,即特定方法负责解析程序的特定片段--这几乎包括所有临时解析器

理念如下:

  • 将钩子连接到 Python 执行中,并观察输入字符串片段在不同方法中被产生和命名。

  • 将输入片段以树状结构拼接起来以检索解析树

  • 从多个解析树中提取公共元素以生成输入的上下文无关语法

简单语法挖掘器

假设我们想要获取process_vehicle()函数的输入语法。我们首先收集这个函数的样本输入。

VEHICLES = INVENTORY.split('\n') 

负责处理库存的方法集合如下。

INVENTORY_METHODS = {
    'process_inventory',
    'process_vehicle',
    'process_van',
    'process_car'} 

我们从配置模糊测试章节中看到,可以连接到 Python 运行时来观察函数的参数和创建的任何局部变量。我们还看到,可以通过检查frame参数来获得执行上下文。这里有一个简单的跟踪器,可以返回跟踪函数中的局部变量和其他上下文信息。我们重用了Coverage跟踪类。

跟踪器

from Coverage import Coverage 
import [inspect](https://docs.python.org/3/library/inspect.html) 
class Tracer(Coverage):
    def traceit(self, frame, event, arg):
        method_name = inspect.getframeinfo(frame).function
        if method_name not in INVENTORY_METHODS:
            return
        file_name = inspect.getframeinfo(frame).filename

        param_names = inspect.getargvalues(frame).args
        lineno = inspect.getframeinfo(frame).lineno
        local_vars = inspect.getargvalues(frame).locals
        print(event, file_name, lineno, method_name, param_names, local_vars)
        return self.traceit 

我们在跟踪上下文中运行代码。

with Tracer() as tracer:
    process_vehicle(VEHICLES[0]) 
call Parser.ipynb 29 process_vehicle ['vehicle'] {'vehicle': '1997,van,Ford,E350'}
line Parser.ipynb 30 process_vehicle ['vehicle'] {'vehicle': '1997,van,Ford,E350'}
line Parser.ipynb 31 process_vehicle ['vehicle'] {'vehicle': '1997,van,Ford,E350', 'year': '1997', 'kind': 'van', 'company': 'Ford', 'model': 'E350', '_': []}
line Parser.ipynb 32 process_vehicle ['vehicle'] {'vehicle': '1997,van,Ford,E350', 'year': '1997', 'kind': 'van', 'company': 'Ford', 'model': 'E350', '_': []}
call Parser.ipynb 40 process_van ['year', 'company', 'model'] {'year': '1997', 'company': 'Ford', 'model': 'E350'}
line Parser.ipynb 40 process_van ['year', 'company', 'model'] {'year': '1997', 'company': 'Ford', 'model': 'E350'}
line Parser.ipynb 41 process_van ['year', 'company', 'model'] {'year': '1997', 'company': 'Ford', 'model': 'E350'}
line Parser.ipynb 42 process_van ['year', 'company', 'model'] {'year': '1997', 'company': 'Ford', 'model': 'E350', 'res': ['We have a Ford E350 van from 1997 vintage.']}
line Parser.ipynb 43 process_van ['year', 'company', 'model'] {'year': '1997', 'company': 'Ford', 'model': 'E350', 'res': ['We have a Ford E350 van from 1997 vintage.'], 'iyear': 1997}
line Parser.ipynb 46 process_van ['year', 'company', 'model'] {'year': '1997', 'company': 'Ford', 'model': 'E350', 'res': ['We have a Ford E350 van from 1997 vintage.'], 'iyear': 1997}
line Parser.ipynb 47 process_van ['year', 'company', 'model'] {'year': '1997', 'company': 'Ford', 'model': 'E350', 'res': ['We have a Ford E350 van from 1997 vintage.', 'It is an old but reliable model!'], 'iyear': 1997}
return Parser.ipynb 47 process_van ['year', 'company', 'model'] {'year': '1997', 'company': 'Ford', 'model': 'E350', 'res': ['We have a Ford E350 van from 1997 vintage.', 'It is an old but reliable model!'], 'iyear': 1997}
return Parser.ipynb 32 process_vehicle ['vehicle'] {'vehicle': '1997,van,Ford,E350', 'year': '1997', 'kind': 'van', 'company': 'Ford', 'model': 'E350', '_': []}

我们从跟踪中想要得到的主要是输入片段分配给不同变量的列表。我们可以使用跟踪设施settrace()来获取,就像上面所展示的那样。

然而,settrace()函数连接到 Python 调试设施。当它在运行时,没有任何调试器可以连接到程序。也就是说,如果我们的语法挖掘器存在问题,我们将无法将其附加到调试器上来理解发生了什么。这并不理想。因此,我们将跟踪器限制在可能的最简单实现中,并在后续阶段实现语法挖掘的核心。

traceit()函数依赖于frame变量提供的信息,该变量暴露了 Python 的内部信息。我们定义了一个context类,用于封装我们从frame中需要的信息。

上下文

Context类提供了对当前模块和参数名称等信息轻松访问的接口。

class Context:
    def __init__(self, frame, track_caller=True):
        self.method = inspect.getframeinfo(frame).function
        self.parameter_names = inspect.getargvalues(frame).args
        self.file_name = inspect.getframeinfo(frame).filename
        self.line_no = inspect.getframeinfo(frame).lineno

    def _t(self):
        return (self.file_name, self.line_no, self.method,
                ','.join(self.parameter_names))

    def __repr__(self):
        return "%s:%d:%s(%s)" % self._t() 

在这里,我们添加了一些方便的方法,这些方法在frame上操作以转换为Context

class Context(Context):
    def extract_vars(self, frame):
        return inspect.getargvalues(frame).locals

    def parameters(self, all_vars):
        return {k: v for k, v in all_vars.items() if k in self.parameter_names}

    def qualified(self, all_vars):
        return {"%s:%s" % (self.method, k): v for k, v in all_vars.items()} 

我们将打印上下文挂钩到traceit(),以观察其作用。首先,我们定义一个log_event()来显示事件。

def log_event(event, var):
    print({'call': '->', 'return': '<-'}.get(event, '  '), var) 

并在traceit()函数中使用log_event()

class Tracer(Tracer):
    def traceit(self, frame, event, arg):
        log_event(event, Context(frame))
        return self.traceit 

在跟踪模式下运行process_vehicle()会打印遇到的上下文。

with Tracer() as tracer:
    process_vehicle(VEHICLES[0]) 
-> Parser.ipynb:29:process_vehicle(vehicle)
   Parser.ipynb:30:process_vehicle(vehicle)
   Parser.ipynb:31:process_vehicle(vehicle)
   Parser.ipynb:32:process_vehicle(vehicle)
-> Parser.ipynb:40:process_van(year,company,model)
   Parser.ipynb:40:process_van(year,company,model)
   Parser.ipynb:41:process_van(year,company,model)
   Parser.ipynb:42:process_van(year,company,model)
   Parser.ipynb:43:process_van(year,company,model)
   Parser.ipynb:46:process_van(year,company,model)
   Parser.ipynb:47:process_van(year,company,model)
<- Parser.ipynb:47:process_van(year,company,model)
<- Parser.ipynb:32:process_vehicle(vehicle)
-> Coverage.ipynb:102:__exit__(self,exc_type,exc_value,tb)
   Coverage.ipynb:105:__exit__(self,exc_type,exc_value,tb)

执行任何函数产生的跟踪信息可能会非常大。因此,我们需要将注意力限制在特定的模块上。此外,我们还专门将注意力限制在str变量上,因为这些变量更有可能包含输入片段。(我们将在练习中展示如何处理复杂对象。)

我们之前开发的Context类用于决定要监控哪些模块以及要跟踪哪些变量。

我们存储当前的输入字符串,以便可以用来确定是否有任何特定的字符串片段来自当前输入字符串。任何可选参数都单独处理。

class Tracer(Tracer):
    def __init__(self, my_input, **kwargs):
        self.options(kwargs)
        self.my_input, self.trace = my_input, [] 

我们使用可选参数files来指示我们感兴趣的特定源文件,使用methods来指示感兴趣的特定方法。此外,我们还使用log来指定在跟踪期间是否启用详细日志记录。我们使用之前定义的log_event()方法进行日志记录。

选项处理如下。

class Tracer(Tracer):
    def options(self, kwargs):
        self.files = kwargs.get('files', [])
        self.methods = kwargs.get('methods', [])
        self.log = log_event if kwargs.get('log') else lambda _evt, _var: None 

检查filesmethods以确定特定事件是否应该被跟踪。

class Tracer(Tracer):
    def tracing_context(self, cxt, event, arg):
        fres = not self.files or any(
            cxt.file_name.endswith(f) for f in self.files)
        mres = not self.methods or any(cxt.method == m for m in self.methods)
        return fres and mres 

与事件上下文类似,我们还想将注意力限制在特定的变量上。目前,我们只想关注字符串。(请参阅本章末尾的练习,了解如何将其扩展到其他类型的对象。)

class Tracer(Tracer):
    def tracing_var(self, k, v):
        return isinstance(v, str) 

我们修改了traceit(),使其仅在特定事件上调用带有上下文信息的on_event()函数。

class Tracer(Tracer):
    def on_event(self, event, arg, cxt, my_vars):
        self.trace.append((event, arg, cxt, my_vars))

    def create_context(self, frame):
        return Context(frame)

    def traceit(self, frame, event, arg):
        cxt = self.create_context(frame)
        if not self.tracing_context(cxt, event, arg):
            return self.traceit
        self.log(event, cxt)

        my_vars = {
            k: v
            for k, v in cxt.extract_vars(frame).items()
            if self.tracing_var(k, v)
        }
        self.on_event(event, arg, cxt, my_vars)
        return self.traceit 

Tracer类现在可以专注于特定文件上的特定类型的事件。此外,它为我们感兴趣的变量提供了一个一级过滤器。例如,我们只想关注包含输入片段的process_*方法中的变量。以下是我们的更新后的Tracer如何使用。

with Tracer(VEHICLES[0], methods=INVENTORY_METHODS, log=True) as tracer:
    process_vehicle(VEHICLES[0]) 
-> Parser.ipynb:29:process_vehicle(vehicle)
   Parser.ipynb:30:process_vehicle(vehicle)
   Parser.ipynb:31:process_vehicle(vehicle)
   Parser.ipynb:32:process_vehicle(vehicle)
-> Parser.ipynb:40:process_van(year,company,model)
   Parser.ipynb:40:process_van(year,company,model)
   Parser.ipynb:41:process_van(year,company,model)
   Parser.ipynb:42:process_van(year,company,model)
   Parser.ipynb:43:process_van(year,company,model)
   Parser.ipynb:46:process_van(year,company,model)
   Parser.ipynb:47:process_van(year,company,model)
<- Parser.ipynb:47:process_van(year,company,model)
<- Parser.ipynb:32:process_vehicle(vehicle)

执行产生了以下跟踪信息。

for t in tracer.trace:
    print(t[0], t[2].method, dict(t[3])) 
call process_vehicle {'vehicle': '1997,van,Ford,E350'}
line process_vehicle {'vehicle': '1997,van,Ford,E350'}
line process_vehicle {'vehicle': '1997,van,Ford,E350', 'year': '1997', 'kind': 'van', 'company': 'Ford', 'model': 'E350'}
line process_vehicle {'vehicle': '1997,van,Ford,E350', 'year': '1997', 'kind': 'van', 'company': 'Ford', 'model': 'E350'}
call process_van {'year': '1997', 'company': 'Ford', 'model': 'E350'}
line process_van {'year': '1997', 'company': 'Ford', 'model': 'E350'}
line process_van {'year': '1997', 'company': 'Ford', 'model': 'E350'}
line process_van {'year': '1997', 'company': 'Ford', 'model': 'E350'}
line process_van {'year': '1997', 'company': 'Ford', 'model': 'E350'}
line process_van {'year': '1997', 'company': 'Ford', 'model': 'E350'}
line process_van {'year': '1997', 'company': 'Ford', 'model': 'E350'}
return process_van {'year': '1997', 'company': 'Ford', 'model': 'E350'}
return process_vehicle {'vehicle': '1997,van,Ford,E350', 'year': '1997', 'kind': 'van', 'company': 'Ford', 'model': 'E350'}

由于我们已经在Tracer中保存了输入,因此再次将输入作为参数指定是多余的。

with Tracer(VEHICLES[0], methods=INVENTORY_METHODS, log=True) as tracer:
    process_vehicle(tracer.my_input) 
-> Parser.ipynb:29:process_vehicle(vehicle)
   Parser.ipynb:30:process_vehicle(vehicle)
   Parser.ipynb:31:process_vehicle(vehicle)
   Parser.ipynb:32:process_vehicle(vehicle)
-> Parser.ipynb:40:process_van(year,company,model)
   Parser.ipynb:40:process_van(year,company,model)
   Parser.ipynb:41:process_van(year,company,model)
   Parser.ipynb:42:process_van(year,company,model)
   Parser.ipynb:43:process_van(year,company,model)
   Parser.ipynb:46:process_van(year,company,model)
   Parser.ipynb:47:process_van(year,company,model)
<- Parser.ipynb:47:process_van(year,company,model)
<- Parser.ipynb:32:process_vehicle(vehicle)

DefineTracker

我们定义了一个DefineTracker类,用于处理来自Tracer的跟踪信息。其思路是存储不同的变量定义,这些定义是输入片段。

跟踪器识别出输入字符串中的字符串片段,并将它们存储在字典 my_assignments 中。它保存了跟踪记录,以及相应的输入以进行处理。最后,它调用 process() 来处理它所接收的 trace。我们将从一个依赖于某些假设的简单跟踪器开始,稍后我们将看到这些假设如何被放宽。

class DefineTracker:
    def __init__(self, my_input, trace, **kwargs):
        self.options(kwargs)
        self.my_input = my_input
        self.trace = trace
        self.my_assignments = {}
        self.process() 

使用子串搜索的一个问题是,短字符串序列往往包含在其他字符串序列中,即使它们可能不是来自原始字符串。也就是说,如果输入片段是 v,它同样可能来自 vanchevy。我们依赖于能够预测给定片段在输入中确切位置的能力。因此,我们定义了一个常量 FRAGMENT_LEN,这样我们忽略长度不超过该长度的字符串。我们同样也加入了日志记录功能。

FRAGMENT_LEN = 3 
class DefineTracker(DefineTracker):
    def options(self, kwargs):
        self.log = log_event if kwargs.get('log') else lambda _evt, _var: None
        self.fragment_len = kwargs.get('fragment_len', FRAGMENT_LEN) 

我们的跟踪器简单地记录变量值,正如它们出现时。接下来,我们需要检查变量是否包含来自 输入字符串 的值。常见的做法是依赖于符号执行或至少动态污染,这些方法强大但复杂。然而,通过简单地依赖子串搜索可以获得一个合理的近似。也就是说,我们考虑任何产生的子串,如果它是原始输入字符串的子串,我们就认为它来自原始输入。

我们定义了一个 is_input_fragment() 方法,该方法依赖于字符串包含来检测字符串是否来自输入。

class DefineTracker(DefineTracker):
    def is_input_fragment(self, var, value):
        return len(value) >= self.fragment_len and value in self.my_input 

我们可以使用 is_input_fragment() 来选择仅定义的变量子集,如下所示在 fragments() 中实现。

class DefineTracker(DefineTracker):
    def fragments(self, variables):
        return {k: v for k, v in variables.items(
        ) if self.is_input_fragment(k, v)} 

跟踪器处理每个事件,并在每个事件中,它将包含输入字符串部分的当前局部变量更新到字典 my_assignments 中。请注意,这里有一个关于重新赋值期间发生什么的选择。我们可以丢弃所有重新赋值,或者只保留最后的赋值。在这里,我们选择后者。如果您想要前者行为,在存储片段之前检查值是否存在于 my_assignments 中。

class DefineTracker(DefineTracker):
    def track_event(self, event, arg, cxt, my_vars):
        self.log(event, (cxt.method, my_vars))
        self.my_assignments.update(self.fragments(my_vars))

    def process(self):
        for event, arg, cxt, my_vars in self.trace:
            self.track_event(event, arg, cxt, my_vars) 

使用跟踪器,我们可以获取输入片段。例如,假设我们只对至少 5 个字符长的字符串感兴趣。

tracker = DefineTracker(tracer.my_input, tracer.trace, fragment_len=5)
for k, v in tracker.my_assignments.items():
    print(k, '=', repr(v)) 
vehicle = '1997,van,Ford,E350'

或者长度为 2 个字符的字符串(默认)。

tracker = DefineTracker(tracer.my_input, tracer.trace)
for k, v in tracker.my_assignments.items():
    print(k, '=', repr(v)) 
vehicle = '1997,van,Ford,E350'
year = '1997'
kind = 'van'
company = 'Ford'
model = 'E350'

class DefineTracker(DefineTracker):
    def assignments(self):
        return self.my_assignments.items() 

组装推导树

from Grammars import START_SYMBOL, syntax_diagram, \
    is_nonterminal, Grammar 
from GrammarFuzzer import GrammarFuzzer, display_tree, \
    DerivationTree 

来自 DefineTracker 的输入片段只讲述了故事的一半。片段可能在解析的不同阶段被创建。因此,我们需要将片段组装成输入的推导树。基本思路如下:

我们上一步的输入是:

"1997,van,Ford,E350" 

我们开始一个推导树,并将其与文法中的起始符号关联。

derivation_tree: DerivationTree = (START_SYMBOL, [("1997,van,Ford,E350", [])]) 
display_tree(derivation_tree) 

0 1 1997,van,Ford,E350 0->1

下一个输入是:

vehicle = "1997,van,Ford,E350" 

由于车辆节点完全覆盖了 <start> 节点的值,我们用车辆节点替换了该值。

derivation_tree: DerivationTree = (START_SYMBOL, 
                                   [('<vehicle>', [("1997,van,Ford,E350", [])],
                                                   [])]) 
display_tree(derivation_tree) 

0 1 0->1 2 1997,van,Ford,E350 1->2

下一个输入是:

year = '1997' 

<start> 遍历推导树,我们看到它替换了 <vehicle> 节点值的一部分。因此,我们将 <vehicle> 节点的值分割成两个子节点,其中一个对应于值 "1997",另一个对应于 ",van,Ford,E350",并将第一个替换为 <year> 节点。

derivation_tree: DerivationTree = (START_SYMBOL, 
                                   [('<vehicle>', [('<year>', [('1997', [])]),
                                                   (",van,Ford,E350", [])], [])]) 
display_tree(derivation_tree) 

0 1 0->1 2 1->2 4 ,van,Ford,E350 1->4 3 1997 2->3

我们对以下进行类似操作:

company = 'Ford' 
derivation_tree: DerivationTree = (START_SYMBOL, 
                                   [('<vehicle>', [('<year>', [('1997', [])]),
                                                   (",van,", []),
                                                   ('<company>', [('Ford', [])]),
                                                   (",E350", [])], [])]) 
display_tree(derivation_tree) 

0 1 0->1 2 1->2 4 ,van, 1->4 5 1->5 7 ,E350 1->7 3 1997 2->3 6 Ford 5->6

类似地,对于

kind = 'van' 

model = 'E350' 
derivation_tree: DerivationTree = (START_SYMBOL, 
                                   [('<vehicle>', [('<year>', [('1997', [])]),
                                                   (",", []),
                                                   ("<kind>", [('van', [])]),
                                                   (",", []),
                                                   ('<company>', [('Ford', [])]),
                                                   (",", []),
                                                   ("<model>", [('E350', [])])
                                                   ], [])]) 
display_tree(derivation_tree) 

0 1 0->1 2 1->2 4 , (44) 1->4 5 1->5 7 , (44) 1->7 8 1->8 10 , (44) 1->10 11 1->11 3 1997 2->3 6 van 5->6 9 Ford 8->9 12 E350 11->12

我们现在根据上述描述的步骤开发完整的算法。TreeMiner 初始化为输入字符串、变量分配,并将其转换为相应的推导树。

class TreeMiner:
    def __init__(self, my_input, my_assignments, **kwargs):
        self.options(kwargs)
        self.my_input = my_input
        self.my_assignments = my_assignments
        self.tree = self.get_derivation_tree()

    def options(self, kwargs):
        self.log = log_call if kwargs.get('log') else lambda _i, _v: None

    def get_derivation_tree(self):
        return (START_SYMBOL, []) 

log_call() 如下。

def log_call(indent, var):
    print('\t' * indent, var) 

基本思想如下:

  • 目前,我们假设分配给变量的值是稳定的。也就是说,它不会被重新分配。特别是,没有递归调用,或者从不同部分对同一函数的多次调用。(我们将在稍后展示如何克服这个限制)。

  • 对于在 my_assignments 中找到的每个 varvalue 对:

    1. 我们在推导树中递归地搜索 value val 的出现。

    2. 如果在节点 P1 的值 V1 中找到一个出现,我们将节点 P1 的值分成三部分,中间部分匹配 value val,第一部分和最后一部分是 V1 中的对应的前缀和后缀。

    3. 重新构建具有三个子节点的节点 P1,其中前面提到的前缀和后缀是字符串值,匹配的值 val 被替换为一个具有单个值 val 的节点 var

首先,我们定义一个包装器来从变量名生成一个非终结符。

def to_nonterminal(var):
    return "<" + var.lower() + ">" 

string_part_of_value() 方法检查给定的 part 值是否是整体的一部分。

class TreeMiner(TreeMiner):
    def string_part_of_value(self, part, value):
        return (part in value) 

partition_by_part() 方法如果匹配,则按给定的部分分割 value,并返回一个包含第一个部分、被替换的部分和最后一个部分的列表。这是一个可以作为子列表一部分的格式。

class TreeMiner(TreeMiner):
    def partition(self, part, value):
        return value.partition(part) 
class TreeMiner(TreeMiner):
    def partition_by_part(self, pair, value):
        k, part = pair
        prefix_k_suffix = [
                    (k, [[part, []]]) if i == 1 else (e, [])
                    for i, e in enumerate(self.partition(part, value))
                    if e]
        return prefix_k_suffix 

insert_into_tree() 方法接受一个给定的树 tree 和一个 (k,v) 对。它递归地检查给定的对是否可以应用。如果对可以应用,它应用该对并返回 True

class TreeMiner(TreeMiner):
    def insert_into_tree(self, my_tree, pair):
        var, values = my_tree
        k, v = pair
        self.log(1, "- Node: %s\t\t? (%s:%s)" % (var, k, repr(v)))
        applied = False
        for i, value_ in enumerate(values):
            value, arr = value_
            self.log(2, "-> [%d] %s" % (i, repr(value)))
            if is_nonterminal(value):
                applied = self.insert_into_tree(value_, pair)
                if applied:
                    break
            elif self.string_part_of_value(v, value):
                prefix_k_suffix = self.partition_by_part(pair, value)
                del values[i]
                for j, rep in enumerate(prefix_k_suffix):
                    values.insert(j + i, rep)
                applied = True

                self.log(2, " > %s" % (repr([i[0] for i in prefix_k_suffix])))
                break
            else:
                continue
        return applied 

这是 insert_into_tree() 的用法。

tree: DerivationTree = (START_SYMBOL, [("1997,van,Ford,E350", [])])
m = TreeMiner('', {}, log=True) 

首先,我们的输入字符串作为唯一的节点。

display_tree(tree) 

0 1 1997,van,Ford,E350 0->1

插入 <vehicle> 节点。

v = m.insert_into_tree(tree, ('<vehicle>', "1997,van,Ford,E350")) 
	 - Node: <start>		? (<vehicle>:'1997,van,Ford,E350')
		 -> [0] '1997,van,Ford,E350'
		  > ['<vehicle>']

display_tree(tree) 

0 1 0->1 2 1997,van,Ford,E350 1->2

插入 <model> 节点。

v = m.insert_into_tree(tree, ('<model>', 'E350')) 
	 - Node: <start>		? (<model>:'E350')
		 -> [0] '<vehicle>'
	 - Node: <vehicle>		? (<model>:'E350')
		 -> [0] '1997,van,Ford,E350'
		  > ['1997,van,Ford,', '<model>']

display_tree((tree)) 

0 1 0->1 2 1997,van,Ford, 1->2 3 1->3 4 E350 3->4

插入 <company>

v = m.insert_into_tree(tree, ('<company>', 'Ford')) 
	 - Node: <start>		? (<company>:'Ford')
		 -> [0] '<vehicle>'
	 - Node: <vehicle>		? (<company>:'Ford')
		 -> [0] '1997,van,Ford,'
		  > ['1997,van,', '<company>', ',']

display_tree(tree) 

0 1 0->1 2 1997,van, 1->2 3 1->3 5 , (44) 1->5 6 1->6 4 Ford 3->4 7 E350 6->7

插入 <kind>

v = m.insert_into_tree(tree, ('<kind>', 'van')) 
	 - Node: <start>		? (<kind>:'van')
		 -> [0] '<vehicle>'
	 - Node: <vehicle>		? (<kind>:'van')
		 -> [0] '1997,van,'
		  > ['1997,', '<kind>', ',']

display_tree(tree) 

0 1 0->1 2 1997, 1->2 3 1->3 5 , (44) 1->5 6 1->6 8 , (44) 1->8 9 1->9 4 van 3->4 7 Ford 6->7 10 E350 9->10

插入 <year>

v = m.insert_into_tree(tree, ('<year>', '1997')) 
	 - Node: <start>		? (<year>:'1997')
		 -> [0] '<vehicle>'
	 - Node: <vehicle>		? (<year>:'1997')
		 -> [0] '1997,'
		  > ['<year>', ',']

display_tree(tree) 

0 1 0->1 2 1->2 4 , (44) 1->4 5 1->5 7 , (44) 1->7 8 1->8 10 , (44) 1->10 11 1->11 3 1997 2->3 6 van 5->6 9 Ford 8->9 12 E350 11->12

为了简化生活,我们定义了一个包装函数 nt_var(),它将把一个标记转换为相应的非终结符符号。

class TreeMiner(TreeMiner):
    def nt_var(self, var):
        return var if is_nonterminal(var) else to_nonterminal(var) 

现在,我们需要将一个新的定义应用到整个语法中。

class TreeMiner(TreeMiner):
    def apply_new_definition(self, tree, var, value):
        nt_var = self.nt_var(var)
        return self.insert_into_tree(tree, (nt_var, value)) 

此算法实现为 get_derivation_tree()

class TreeMiner(TreeMiner):
    def get_derivation_tree(self):
        tree = (START_SYMBOL, [(self.my_input, [])])

        for var, value in self.my_assignments:
            self.log(0, "%s=%s" % (var, repr(value)))
            self.apply_new_definition(tree, var, value)
        return tree 

TreeMiner 的用法如下:

with Tracer(VEHICLES[0]) as tracer:
    process_vehicle(tracer.my_input)
assignments = DefineTracker(tracer.my_input, tracer.trace).assignments()
dt = TreeMiner(tracer.my_input, assignments, log=True)
dt.tree 
 vehicle='1997,van,Ford,E350'
	 - Node: <start>		? (<vehicle>:'1997,van,Ford,E350')
		 -> [0] '1997,van,Ford,E350'
		  > ['<vehicle>']
 year='1997'
	 - Node: <start>		? (<year>:'1997')
		 -> [0] '<vehicle>'
	 - Node: <vehicle>		? (<year>:'1997')
		 -> [0] '1997,van,Ford,E350'
		  > ['<year>', ',van,Ford,E350']
 kind='van'
	 - Node: <start>		? (<kind>:'van')
		 -> [0] '<vehicle>'
	 - Node: <vehicle>		? (<kind>:'van')
		 -> [0] '<year>'
	 - Node: <year>		? (<kind>:'van')
		 -> [0] '1997'
		 -> [1] ',van,Ford,E350'
		  > [',', '<kind>', ',Ford,E350']
 company='Ford'
	 - Node: <start>		? (<company>:'Ford')
		 -> [0] '<vehicle>'
	 - Node: <vehicle>		? (<company>:'Ford')
		 -> [0] '<year>'
	 - Node: <year>		? (<company>:'Ford')
		 -> [0] '1997'
		 -> [1] ','
		 -> [2] '<kind>'
	 - Node: <kind>		? (<company>:'Ford')
		 -> [0] 'van'
		 -> [3] ',Ford,E350'
		  > [',', '<company>', ',E350']
 model='E350'
	 - Node: <start>		? (<model>:'E350')
		 -> [0] '<vehicle>'
	 - Node: <vehicle>		? (<model>:'E350')
		 -> [0] '<year>'
	 - Node: <year>		? (<model>:'E350')
		 -> [0] '1997'
		 -> [1] ','
		 -> [2] '<kind>'
	 - Node: <kind>		? (<model>:'E350')
		 -> [0] 'van'
		 -> [3] ','
		 -> [4] '<company>'
	 - Node: <company>		? (<model>:'E350')
		 -> [0] 'Ford'
		 -> [5] ',E350'
		  > [',', '<model>']

('<start>',
 [('<vehicle>',
   [('<year>', [['1997', []]]),
    (',', []),
    ('<kind>', [['van', []]]),
    (',', []),
    ('<company>', [['Ford', []]]),
    (',', []),
    ('<model>', [['E350', []]])])])

获得的推导树如下。

display_tree(TreeMiner(tracer.my_input, assignments).tree) 

0 1 0->1 2 1->2 4 , (44) 1->4 5 1->5 7 , (44) 1->7 8 1->8 10 , (44) 1->10 11 1->11 3 1997 2->3 6 van 5->6 9 Ford 8->9 12 E350 11->12

结合所有片段:

trees = []
for vehicle in VEHICLES:
    print(vehicle)
    with Tracer(vehicle) as tracer:
        process_vehicle(tracer.my_input)
    assignments = DefineTracker(tracer.my_input, tracer.trace).assignments()
    trees.append((tracer.my_input, assignments))
    for var, val in assignments:
        print(var + " = " + repr(val))
    print() 
1997,van,Ford,E350
vehicle = '1997,van,Ford,E350'
year = '1997'
kind = 'van'
company = 'Ford'
model = 'E350'

2000,car,Mercury,Cougar
vehicle = '2000,car,Mercury,Cougar'
year = '2000'
kind = 'car'
company = 'Mercury'
model = 'Cougar'

1999,car,Chevy,Venture
vehicle = '1999,car,Chevy,Venture'
year = '1999'
kind = 'car'
company = 'Chevy'
model = 'Venture'

对应的推导树如下。

csv_dt = []
for inputstr, assignments in trees:
    print(inputstr)
    dt = TreeMiner(inputstr, assignments)
    csv_dt.append(dt)
    display_tree(dt.tree) 
1997,van,Ford,E350
2000,car,Mercury,Cougar
1999,car,Chevy,Venture

从推导树恢复语法

我们定义了一个名为 Miner 的类,它可以组合多个推导树以生成语法。初始语法为空。

class GrammarMiner:
    def __init__(self):
        self.grammar = {} 

tree_to_grammar() 方法通过一次选择一个节点,并将其添加到语法中,将我们的推导树转换为语法。节点名称成为键,它拥有的任何子列表都成为该键的另一个备选方案。

class GrammarMiner(GrammarMiner):
    def tree_to_grammar(self, tree):
        node, children = tree
        one_alt = [ck for ck, gc in children]
        hsh = {node: [one_alt] if one_alt else []}
        for child in children:
            if not is_nonterminal(child[0]):
                continue
            chsh = self.tree_to_grammar(child)
            for k in chsh:
                if k not in hsh:
                    hsh[k] = chsh[k]
                else:
                    hsh[k].extend(chsh[k])
        return hsh 
gm = GrammarMiner()
gm.tree_to_grammar(csv_dt[0].tree) 
{'<start>': [['<vehicle>']],
 '<vehicle>': [['<year>', ',', '<kind>', ',', '<company>', ',', '<model>']],
 '<year>': [['1997']],
 '<kind>': [['van']],
 '<company>': [['Ford']],
 '<model>': [['E350']]}

这里生成的语法是 规范化的。我们定义了一个函数 readable(),它接受一个规范化的语法,并以可读的形式返回它。

def readable(grammar):
    def readable_rule(rule):
        return ''.join(rule)

    return {k: list(set(readable_rule(a) for a in grammar[k]))
            for k in grammar} 
syntax_diagram(readable(gm.tree_to_grammar(csv_dt[0].tree))) 
start

vehicle

vehicle

year , kind , company , model

year

1997

kind

van

company

Ford

model

E350

add_tree() 方法从当前语法中获取非终结符的组合列表,以及要添加到语法中的树,并更新每个非终结符的定义。

import [itertools](https://docs.python.org/3/library/itertools.html) 
class GrammarMiner(GrammarMiner):
    def add_tree(self, t):
        t_grammar = self.tree_to_grammar(t.tree)
        self.grammar = {
            key: self.grammar.get(key, []) + t_grammar.get(key, [])
            for key in itertools.chain(self.grammar.keys(), t_grammar.keys())
        } 

add_tree() 的使用如下:

inventory_grammar_miner = GrammarMiner()
for dt in csv_dt:
    inventory_grammar_miner.add_tree(dt) 
syntax_diagram(readable(inventory_grammar_miner.grammar)) 
start

vehicle

vehicle

year , kind , company , model

year

1999 2000 1997

kind

car van

company

Mercury Chevy Ford

model

E350 Cougar Venture

给定来自各种输入的执行跟踪,可以定义 update_grammar() 以从跟踪中获取完整的语法。

class GrammarMiner(GrammarMiner):
    def update_grammar(self, inputstr, trace):
        at = self.create_tracker(inputstr, trace)
        dt = self.create_tree_miner(inputstr, at.assignments())
        self.add_tree(dt)
        return self.grammar

    def create_tracker(self, *args):
        return DefineTracker(*args)

    def create_tree_miner(self, *args):
        return TreeMiner(*args) 

完整的语法恢复在 recover_grammar() 中实现。

def recover_grammar(fn: Callable, inputs: Iterable[str], 
                    **kwargs: Any) -> Grammar:
    miner = GrammarMiner()

    for inputstr in inputs:
        with Tracer(inputstr, **kwargs) as tracer:
            fn(tracer.my_input)
        miner.update_grammar(tracer.my_input, tracer.trace)

    return readable(miner.grammar) 

注意,语法可以直接从跟踪器中检索,而不需要中间的推导树阶段。然而,通过推导树可以检查正在分段的输入,并验证它是否正确发生。

示例 1. 恢复库存语法

inventory_grammar = recover_grammar(process_vehicle, VEHICLES) 
inventory_grammar 
{'<start>': ['<vehicle>'],
 '<vehicle>': ['<year>,<kind>,<company>,<model>'],
 '<year>': ['1999', '2000', '1997'],
 '<kind>': ['car', 'van'],
 '<company>': ['Mercury', 'Chevy', 'Ford'],
 '<model>': ['E350', 'Cougar', 'Venture']}

示例 2. 恢复 URL 语法

我们的算法足够鲁棒,可以从现实世界的程序中恢复语法。例如,Python urlib 模块中的 urlparse 函数接受以下示例 URL。

URLS = [
    'http://user:pass@www.google.com:80/?q=path#ref',
    'https://www.cispa.saarland:80/',
    'http://www.fuzzingbook.org/#News',
] 

urllib 缓存其中间结果以实现快速访问。因此,我们需要在每次调用后使用 clear_cache() 禁用它。

from [urllib.parse](https://docs.python.org/3/library/urllib.parse.html) import urlparse, clear_cache 

我们使用样本 URL 如下恢复语法。urlparse 函数倾向于缓存其之前的解析结果。因此,我们定义了一个新的方法 url_parse(),在每次调用之前清除缓存。

def url_parse(url):
    clear_cache()
    urlparse(url) 
trees = []
for url in URLS:
    print(url)
    with Tracer(url) as tracer:
        url_parse(tracer.my_input)
    assignments = DefineTracker(tracer.my_input, tracer.trace).assignments()
    trees.append((tracer.my_input, assignments))
    for var, val in assignments:
        print(var + " = " + repr(val))
    print()

url_dt = []
for inputstr, assignments in trees:
    print(inputstr)
    dt = TreeMiner(inputstr, assignments)
    url_dt.append(dt)
    display_tree(dt.tree) 
http://user:pass@www.google.com:80/?q=path#ref
url = 'http://user:pass@www.google.com:80/?q=path#ref'
scheme = 'http'
netloc = 'user:pass@www.google.com:80'
fragment = 'ref'
query = 'q=path'

https://www.cispa.saarland:80/
url = 'https://www.cispa.saarland:80/'
scheme = 'https'
netloc = 'www.cispa.saarland:80'

http://www.fuzzingbook.org/#News
url = 'http://www.fuzzingbook.org/#News'
scheme = 'http'
netloc = 'www.fuzzingbook.org'
fragment = 'News'

http://user:pass@www.google.com:80/?q=path#ref
https://www.cispa.saarland:80/
http://www.fuzzingbook.org/#News

让我们使用 url_parse() 来恢复语法:

url_grammar = recover_grammar(url_parse, URLS, files=['urllib/parse.py']) 
syntax_diagram(url_grammar) 
start

url

url

scheme 😕/ netloc /? query # fragment scheme 😕/ netloc / scheme 😕/ netloc /# fragment

scheme

http https

netloc

www.cispa.saarland:80 www.fuzzingbook.org user:pass@www.google.com:80

query

q=path

fragment

News ref

恢复的语法对 URL 格式的描述相当合理。

模糊测试

我们现在可以使用恢复的语法进行如下模糊测试。

首先,库存语法。

f = GrammarFuzzer(inventory_grammar)
for _ in range(10):
    print(f.fuzz()) 
1997,car,Mercury,E350
2000,car,Chevy,Cougar
1997,van,Mercury,Venture
1999,car,Ford,Venture
2000,car,Mercury,E350
2000,car,Mercury,Cougar
1997,car,Chevy,E350
1997,car,Chevy,E350
1997,car,Mercury,Cougar
1999,car,Chevy,E350

接下来,是 URL 语法。

f = GrammarFuzzer(url_grammar)
for _ in range(10):
    print(f.fuzz()) 
https://user:pass@www.google.com:80/
http://www.cispa.saarland:80/?q=path#News
https://user:pass@www.google.com:80/
https://user:pass@www.google.com:80/#ref
http://user:pass@www.google.com:80/
http://user:pass@www.google.com:80/#ref
http://user:pass@www.google.com:80/?q=path#News
http://www.fuzzingbook.org/?q=path#News
http://www.fuzzingbook.org/?q=path#ref
http://www.cispa.saarland:80/

这意味着我们现在可以取一个程序和一些样本,提取其语法,然后使用这个语法进行模糊测试。这真是个好机会!

简单挖掘器的问题

我们简单语法挖掘器的问题之一是假设变量分配的值是稳定的。不幸的是,这可能在所有情况下都不成立。例如,这里有一个格式略有不同的 URL。

URLS_X = URLS + ['ftp://freebsd.org/releases/5.8'] 

从这组样本生成的语法不如我们之前得到的那么好

url_grammar = recover_grammar(url_parse, URLS_X, files=['urllib/parse.py']) 
syntax_diagram(url_grammar) 
start

url scheme 😕/ netloc url

url

scheme 😕/ netloc /? query # fragment /releases/5.8 scheme 😕/ netloc / scheme 😕/ netloc /# fragment

scheme

http ftp https

netloc

www.fuzzingbook.org www.cispa.saarland:80 freebsd.org user:pass@www.google.com:80

query

q=path

fragment

News ref

显然,出了些问题。

为了调查 url 定义出错的原因,让我们检查 URL 的跟踪。

clear_cache()
with Tracer(URLS_X[0]) as tracer:
    urlparse(tracer.my_input)
for i, t in enumerate(tracer.trace):
    if t[0] in {'call', 'line'} and 'parse.py' in str(t[2]) and t[3]:
        print(i, t[2]._t()[1], t[3:]) 
0 374 ({'url': 'http://user:pass@www.google.com:80/?q=path#ref', 'scheme': ''},)
1 394 ({'url': 'http://user:pass@www.google.com:80/?q=path#ref', 'scheme': ''},)
5 129 ({'arg': ''},)
6 126 ({'arg': ''},)
7 131 ({'arg': ''},)
8 132 ({'arg': ''},)
10 395 ({'url': 'http://user:pass@www.google.com:80/?q=path#ref', 'scheme': ''},)
11 452 ({'url': 'http://user:pass@www.google.com:80/?q=path#ref', 'scheme': ''},)
12 474 ({'url': 'http://user:pass@www.google.com:80/?q=path#ref', 'scheme': ''},)
16 129 ({'arg': ''},)
17 126 ({'arg': ''},)
18 131 ({'arg': ''},)
19 132 ({'arg': ''},)
21 477 ({'url': 'http://user:pass@www.google.com:80/?q=path#ref', 'scheme': ''},)
22 478 ({'url': 'http://user:pass@www.google.com:80/?q=path#ref', 'scheme': ''},)
23 480 ({'url': 'http://user:pass@www.google.com:80/?q=path#ref', 'scheme': ''},)
24 481 ({'url': 'http://user:pass@www.google.com:80/?q=path#ref', 'scheme': '', 'b': '\t'},)
25 482 ({'url': 'http://user:pass@www.google.com:80/?q=path#ref', 'scheme': '', 'b': '\t'},)
26 480 ({'url': 'http://user:pass@www.google.com:80/?q=path#ref', 'scheme': '', 'b': '\t'},)
27 481 ({'url': 'http://user:pass@www.google.com:80/?q=path#ref', 'scheme': '', 'b': '\r'},)
28 482 ({'url': 'http://user:pass@www.google.com:80/?q=path#ref', 'scheme': '', 'b': '\r'},)
29 480 ({'url': 'http://user:pass@www.google.com:80/?q=path#ref', 'scheme': '', 'b': '\r'},)
30 481 ({'url': 'http://user:pass@www.google.com:80/?q=path#ref', 'scheme': '', 'b': '\n'},)
31 482 ({'url': 'http://user:pass@www.google.com:80/?q=path#ref', 'scheme': '', 'b': '\n'},)
32 480 ({'url': 'http://user:pass@www.google.com:80/?q=path#ref', 'scheme': '', 'b': '\n'},)
33 484 ({'url': 'http://user:pass@www.google.com:80/?q=path#ref', 'scheme': '', 'b': '\n'},)
34 485 ({'url': 'http://user:pass@www.google.com:80/?q=path#ref', 'scheme': '', 'b': '\n'},)
35 486 ({'url': 'http://user:pass@www.google.com:80/?q=path#ref', 'scheme': '', 'b': '\n', 'netloc': '', 'query': '', 'fragment': ''},)
36 487 ({'url': 'http://user:pass@www.google.com:80/?q=path#ref', 'scheme': '', 'b': '\n', 'netloc': '', 'query': '', 'fragment': ''},)
37 488 ({'url': 'http://user:pass@www.google.com:80/?q=path#ref', 'scheme': '', 'b': '\n', 'netloc': '', 'query': '', 'fragment': ''},)
38 489 ({'url': 'http://user:pass@www.google.com:80/?q=path#ref', 'scheme': '', 'b': '\n', 'netloc': '', 'query': '', 'fragment': '', 'c': 'h'},)
39 488 ({'url': 'http://user:pass@www.google.com:80/?q=path#ref', 'scheme': '', 'b': '\n', 'netloc': '', 'query': '', 'fragment': '', 'c': 'h'},)
40 489 ({'url': 'http://user:pass@www.google.com:80/?q=path#ref', 'scheme': '', 'b': '\n', 'netloc': '', 'query': '', 'fragment': '', 'c': 't'},)
41 488 ({'url': 'http://user:pass@www.google.com:80/?q=path#ref', 'scheme': '', 'b': '\n', 'netloc': '', 'query': '', 'fragment': '', 'c': 't'},)
42 489 ({'url': 'http://user:pass@www.google.com:80/?q=path#ref', 'scheme': '', 'b': '\n', 'netloc': '', 'query': '', 'fragment': '', 'c': 't'},)
43 488 ({'url': 'http://user:pass@www.google.com:80/?q=path#ref', 'scheme': '', 'b': '\n', 'netloc': '', 'query': '', 'fragment': '', 'c': 't'},)
44 489 ({'url': 'http://user:pass@www.google.com:80/?q=path#ref', 'scheme': '', 'b': '\n', 'netloc': '', 'query': '', 'fragment': '', 'c': 'p'},)
45 488 ({'url': 'http://user:pass@www.google.com:80/?q=path#ref', 'scheme': '', 'b': '\n', 'netloc': '', 'query': '', 'fragment': '', 'c': 'p'},)
46 492 ({'url': 'http://user:pass@www.google.com:80/?q=path#ref', 'scheme': '', 'b': '\n', 'netloc': '', 'query': '', 'fragment': '', 'c': 'p'},)
47 493 ({'url': '//user:pass@www.google.com:80/?q=path#ref', 'scheme': 'http', 'b': '\n', 'netloc': '', 'query': '', 'fragment': '', 'c': 'p'},)
48 494 ({'url': '//user:pass@www.google.com:80/?q=path#ref', 'scheme': 'http', 'b': '\n', 'netloc': '', 'query': '', 'fragment': '', 'c': 'p'},)
49 413 ({'url': '//user:pass@www.google.com:80/?q=path#ref'},)
50 414 ({'url': '//user:pass@www.google.com:80/?q=path#ref'},)
51 415 ({'url': '//user:pass@www.google.com:80/?q=path#ref'},)
52 416 ({'url': '//user:pass@www.google.com:80/?q=path#ref', 'c': '/'},)
53 417 ({'url': '//user:pass@www.google.com:80/?q=path#ref', 'c': '/'},)
54 418 ({'url': '//user:pass@www.google.com:80/?q=path#ref', 'c': '/'},)
55 415 ({'url': '//user:pass@www.google.com:80/?q=path#ref', 'c': '/'},)
56 416 ({'url': '//user:pass@www.google.com:80/?q=path#ref', 'c': '?'},)
57 417 ({'url': '//user:pass@www.google.com:80/?q=path#ref', 'c': '?'},)
58 418 ({'url': '//user:pass@www.google.com:80/?q=path#ref', 'c': '?'},)
59 415 ({'url': '//user:pass@www.google.com:80/?q=path#ref', 'c': '?'},)
60 416 ({'url': '//user:pass@www.google.com:80/?q=path#ref', 'c': '#'},)
61 417 ({'url': '//user:pass@www.google.com:80/?q=path#ref', 'c': '#'},)
62 418 ({'url': '//user:pass@www.google.com:80/?q=path#ref', 'c': '#'},)
63 415 ({'url': '//user:pass@www.google.com:80/?q=path#ref', 'c': '#'},)
64 419 ({'url': '//user:pass@www.google.com:80/?q=path#ref', 'c': '#'},)
66 495 ({'url': '/?q=path#ref', 'scheme': 'http', 'b': '\n', 'netloc': 'user:pass@www.google.com:80', 'query': '', 'fragment': '', 'c': 'p'},)
67 496 ({'url': '/?q=path#ref', 'scheme': 'http', 'b': '\n', 'netloc': 'user:pass@www.google.com:80', 'query': '', 'fragment': '', 'c': 'p'},)
68 498 ({'url': '/?q=path#ref', 'scheme': 'http', 'b': '\n', 'netloc': 'user:pass@www.google.com:80', 'query': '', 'fragment': '', 'c': 'p'},)
69 501 ({'url': '/?q=path#ref', 'scheme': 'http', 'b': '\n', 'netloc': 'user:pass@www.google.com:80', 'query': '', 'fragment': '', 'c': 'p'},)
70 502 ({'url': '/?q=path#ref', 'scheme': 'http', 'b': '\n', 'netloc': 'user:pass@www.google.com:80', 'query': '', 'fragment': '', 'c': 'p'},)
71 503 ({'url': '/?q=path', 'scheme': 'http', 'b': '\n', 'netloc': 'user:pass@www.google.com:80', 'query': '', 'fragment': 'ref', 'c': 'p'},)
72 504 ({'url': '/?q=path', 'scheme': 'http', 'b': '\n', 'netloc': 'user:pass@www.google.com:80', 'query': '', 'fragment': 'ref', 'c': 'p'},)
73 505 ({'url': '/', 'scheme': 'http', 'b': '\n', 'netloc': 'user:pass@www.google.com:80', 'query': 'q=path', 'fragment': 'ref', 'c': 'p'},)
74 421 ({'netloc': 'user:pass@www.google.com:80'},)
75 422 ({'netloc': 'user:pass@www.google.com:80'},)
76 423 ({'netloc': 'user:pass@www.google.com:80'},)
78 506 ({'url': '/', 'scheme': 'http', 'b': '\n', 'netloc': 'user:pass@www.google.com:80', 'query': 'q=path', 'fragment': 'ref', 'c': 'p'},)
82 507 ({'url': '/', 'scheme': 'http', 'b': '\n', 'netloc': 'user:pass@www.google.com:80', 'query': 'q=path', 'fragment': 'ref', 'c': 'p'},)
87 396 ({'url': 'http://user:pass@www.google.com:80/?q=path#ref', 'scheme': ''},)
88 397 ({'url': '/', 'scheme': 'http', 'netloc': 'user:pass@www.google.com:80', 'query': 'q=path', 'fragment': 'ref'},)
89 400 ({'url': '/', 'scheme': 'http', 'netloc': 'user:pass@www.google.com:80', 'query': 'q=path', 'fragment': 'ref'},)
90 401 ({'url': '/', 'scheme': 'http', 'netloc': 'user:pass@www.google.com:80', 'query': 'q=path', 'fragment': 'ref', 'params': ''},)
94 402 ({'url': '/', 'scheme': 'http', 'netloc': 'user:pass@www.google.com:80', 'query': 'q=path', 'fragment': 'ref', 'params': ''},)

注意,随着解析的进行,url 的值是如何变化的?这违反了我们对变量赋值值稳定的假设。接下来,我们将探讨如何消除这种限制。

带有重新分配的语法挖掘器

唯一识别不同变量的方法之一是在它们定义和值变化时都使用 行号 进行注释。考虑下面的代码片段

跟踪变量赋值位置

def C(cp_1):
    c_2 = cp_1 + '@2'
    c_3 = c_2 + '@3'
    return c_3 
def B(bp_7):
    b_8 = bp_7 + '@8'
    return C(b_8) 
def A(ap_12):
    a_13 = ap_12 + '@13'
    a_14 = B(a_13) + '@14'
    a_14 = a_14 + '@15'
    a_13 = a_14 + '@16'
    a_14 = B(a_13) + '@17'
    a_14 = B(a_13) + '@18' 

注意,所有变量要么命名与它们定义的位置相对应,要么值被注释以表明它已被更改。

让我们运行这个带有跟踪的代码。

with Tracer('____') as tracer:
    A(tracer.my_input)

for t in tracer.trace:
    print(t[0], "%d:%s" % (t[2].line_no, t[2].method), t[3]) 
call 1:A {'ap_12': '____'}
line 2:A {'ap_12': '____'}
line 3:A {'ap_12': '____', 'a_13': '____@13'}
call 1:B {'bp_7': '____@13'}
line 2:B {'bp_7': '____@13'}
line 3:B {'bp_7': '____@13', 'b_8': '____@13@8'}
call 1:C {'cp_1': '____@13@8'}
line 2:C {'cp_1': '____@13@8'}
line 3:C {'cp_1': '____@13@8', 'c_2': '____@13@8@2'}
line 4:C {'cp_1': '____@13@8', 'c_2': '____@13@8@2', 'c_3': '____@13@8@2@3'}
return 4:C {'cp_1': '____@13@8', 'c_2': '____@13@8@2', 'c_3': '____@13@8@2@3'}
return 3:B {'bp_7': '____@13', 'b_8': '____@13@8'}
line 4:A {'ap_12': '____', 'a_13': '____@13', 'a_14': '____@13@8@2@3@14'}
line 5:A {'ap_12': '____', 'a_13': '____@13', 'a_14': '____@13@8@2@3@14@15'}
line 6:A {'ap_12': '____', 'a_13': '____@13@8@2@3@14@15@16', 'a_14': '____@13@8@2@3@14@15'}
call 1:B {'bp_7': '____@13@8@2@3@14@15@16'}
line 2:B {'bp_7': '____@13@8@2@3@14@15@16'}
line 3:B {'bp_7': '____@13@8@2@3@14@15@16', 'b_8': '____@13@8@2@3@14@15@16@8'}
call 1:C {'cp_1': '____@13@8@2@3@14@15@16@8'}
line 2:C {'cp_1': '____@13@8@2@3@14@15@16@8'}
line 3:C {'cp_1': '____@13@8@2@3@14@15@16@8', 'c_2': '____@13@8@2@3@14@15@16@8@2'}
line 4:C {'cp_1': '____@13@8@2@3@14@15@16@8', 'c_2': '____@13@8@2@3@14@15@16@8@2', 'c_3': '____@13@8@2@3@14@15@16@8@2@3'}
return 4:C {'cp_1': '____@13@8@2@3@14@15@16@8', 'c_2': '____@13@8@2@3@14@15@16@8@2', 'c_3': '____@13@8@2@3@14@15@16@8@2@3'}
return 3:B {'bp_7': '____@13@8@2@3@14@15@16', 'b_8': '____@13@8@2@3@14@15@16@8'}
line 7:A {'ap_12': '____', 'a_13': '____@13@8@2@3@14@15@16', 'a_14': '____@13@8@2@3@14@15@16@8@2@3@17'}
call 1:B {'bp_7': '____@13@8@2@3@14@15@16'}
line 2:B {'bp_7': '____@13@8@2@3@14@15@16'}
line 3:B {'bp_7': '____@13@8@2@3@14@15@16', 'b_8': '____@13@8@2@3@14@15@16@8'}
call 1:C {'cp_1': '____@13@8@2@3@14@15@16@8'}
line 2:C {'cp_1': '____@13@8@2@3@14@15@16@8'}
line 3:C {'cp_1': '____@13@8@2@3@14@15@16@8', 'c_2': '____@13@8@2@3@14@15@16@8@2'}
line 4:C {'cp_1': '____@13@8@2@3@14@15@16@8', 'c_2': '____@13@8@2@3@14@15@16@8@2', 'c_3': '____@13@8@2@3@14@15@16@8@2@3'}
return 4:C {'cp_1': '____@13@8@2@3@14@15@16@8', 'c_2': '____@13@8@2@3@14@15@16@8@2', 'c_3': '____@13@8@2@3@14@15@16@8@2@3'}
return 3:B {'bp_7': '____@13@8@2@3@14@15@16', 'b_8': '____@13@8@2@3@14@15@16@8'}
return 7:A {'ap_12': '____', 'a_13': '____@13@8@2@3@14@15@16', 'a_14': '____@13@8@2@3@14@15@16@8@2@3@18'}
call 102:__exit__ {}
line 105:__exit__ {}

每个变量首先被引用如下:

  • cp_1 -- 调用 1:C

  • c_2 -- 3:C(但上一个事件是 2:C

  • c_3 -- 4:C(但上一个事件是 3:C

  • bp_7 -- 调用 7:B

  • b_8 -- 9:B(但上一个事件是 8:B

  • ap_12 -- 调用 12:A

  • a_13 -- 14:A(但上一个事件是 13:A

  • a_14 -- 15:A(上一个事件是返回 9:B。然而,A()中的上一个事件是 14:A

  • 在第 15 行重新分配a_14 -- 16:A(上一个事件是 15:A

  • 在第 16 行重新分配a_13 -- 17:A(上一个事件是 16:A

  • 在第 17 行重新分配a_14 -- 返回 17:AA()中的上一个事件是 17:A

  • 在第 18 行重新分配a_14 -- 返回 18:AA()中的上一个事件是 18:A

因此,我们的观察结果是,如果是一个调用,当前位置是定义任何新变量的正确位置。另一方面,如果首次引用变量(或重新分配了新值),那么考虑的正确位置是同一方法调用中的上一个位置。接下来,让我们看看我们如何将此信息纳入变量命名。

接下来,我们需要一种方法来跟踪正在进行的单个方法调用。为此,我们定义了类CallStack。每次方法调用都会获得一个单独的标识符,当方法调用结束时,标识符会被重置。

CallStack

class CallStack:
    def __init__(self, **kwargs):
        self.options(kwargs)
        self.method_id = (START_SYMBOL, 0)
        self.method_register = 0
        self.mstack = [self.method_id]

    def enter(self, method):
        self.method_register += 1
        self.method_id = (method, self.method_register)
        self.log('call', "%s%s" % (self.indent(), str(self)))
        self.mstack.append(self.method_id)

    def leave(self):
        self.mstack.pop()
        self.log('return', "%s%s" % (self.indent(), str(self)))
        self.method_id = self.mstack[-1] 

几个额外的函数以简化生活。

class CallStack(CallStack):
    def options(self, kwargs):
        self.log = log_event if kwargs.get('log') else lambda _evt, _var: None

    def indent(self):
        return len(self.mstack) * "\t"

    def at(self, n):
        return self.mstack[n]

    def __len__(self):
        return len(mstack) - 1

    def __str__(self):
        return "%s:%d" % self.method_id

    def __repr__(self):
        return repr(self.method_id) 

我们还定义了一个方便的方法来显示给定的堆栈。

def display_stack(istack):
    def stack_to_tree(stack):
        current, *rest = stack
        if not rest:
            return (repr(current), [])
        return (repr(current), [stack_to_tree(rest)])
    display_tree(stack_to_tree(istack.mstack), graph_attr=lr_graph) 

这是我们可以使用CallStack的方法。

cs = CallStack()
display_stack(cs)
cs 
('<start>', 0)

cs.enter('hello')
display_stack(cs)
cs 
('hello', 1)

cs.enter('world')
display_stack(cs)
cs 
('world', 2)

cs.leave()
display_stack(cs)
cs 
('hello', 1)

cs.enter('world')
display_stack(cs)
cs 
('world', 3)

cs.leave()
display_stack(cs)
cs 
('hello', 1)

为了处理变量重新分配,我们需要一个比字典更智能的数据结构来存储变量。我们首先定义了一个简单的接口Vars。它作为变量的容器,并在my_assignments处实例化。

Vars

Vars在其内部字典defs中存储变量引用,该字典在解析过程中出现。我们用原始字符串初始化字典。

class Vars:
    def __init__(self, original):
        self.defs = {}
        self.my_input = original 

字典需要两个方法:update(),它接受一组键值对以更新自身,以及_set_kv(),它更新特定的键值对。

class Vars(Vars):
    def _set_kv(self, k, v):
        self.defs[k] = v

    def __setitem__(self, k, v):
        self._set_kv(k, v)

    def update(self, v):
        for k, v in v.items():
            self._set_kv(k, v) 

Vars是内部字典的代理。例如,以下是使用它的方法。

v = Vars('')
v.defs 
{}

v['x'] = 'X'
v.defs 
{'x': 'X'}

v.update({'x': 'x', 'y': 'y'})
v.defs 
{'x': 'x', 'y': 'y'}

AssignmentVars

我们现在扩展简单的Vars以处理变量重新分配。为此,我们定义了AssignmentVars

检测重新分配和重命名变量的想法如下:我们使用accessed_seq_var跟踪特定变量的上一个重新分配。它包含任何特定变量的最后重命名作为其对应值。new_vars包含在本迭代中添加的所有新变量的列表。

class AssignmentVars(Vars):
    def __init__(self, original):
        super().__init__(original)
        self.accessed_seq_var = {}
        self.var_def_lines = {}
        self.current_event = None
        self.new_vars = set()
        self.method_init() 

method_init()方法负责使用call_stack中保存的记录来跟踪方法调用。event_locations用于跟踪此方法内访问的位置。这是用于变量定义行号跟踪的。

class AssignmentVars(AssignmentVars):
    def method_init(self):
        self.call_stack = CallStack()
        self.event_locations = {self.call_stack.method_id: []} 

update()现在被修改为使用var_location_register()跟踪任何更改的行号。我们使用后重新初始化new_vars以供下一个事件使用。

class AssignmentVars(AssignmentVars):
    def update(self, v):
        for k, v in v.items():
            self._set_kv(k, v)
        self.var_location_register(self.new_vars)
        self.new_vars = set() 

变量名现在包含一个索引,表示它经历了多少次重新赋值,从而使得每次重新赋值都是一个唯一的变量。

class AssignmentVars(AssignmentVars):
    def var_name(self, var):
        return (var, self.accessed_seq_var[var]) 

在存储变量时,我们首先需要检查它之前是否已知。如果它不是,我们需要初始化重命名计数。这是通过var_access完成的。

class AssignmentVars(AssignmentVars):
    def var_access(self, var):
        if var not in self.accessed_seq_var:
            self.accessed_seq_var[var] = 0
        return self.var_name(var) 

在变量重新赋值期间,我们更新accessed_seq_var以反映新的计数。

class AssignmentVars(AssignmentVars):
    def var_assign(self, var):
        self.accessed_seq_var[var] += 1
        self.new_vars.add(self.var_name(var))
        return self.var_name(var) 

这些方法可以这样使用

sav = AssignmentVars('')
sav.defs 
{}

sav.var_access('v1') 
('v1', 0)

sav.var_assign('v1') 
('v1', 1)

再次赋值给它会增加计数器。

sav.var_assign('v1') 
('v1', 2)

逻辑的核心在于_set_kv()。当一个变量被赋值时,我们获取序列化变量名s_var。如果序列化变量名在defs中之前是未知的,那么我们就没有进一步的担忧。我们将序列化变量添加到defs中。

如果变量之前已知,那么这是一个可能重新赋值的指示。在这种情况下,我们查看变量所持有的值。我们检查值是否已更改。如果没有,那么它就不是。

如果值已经改变,它就是一个重新赋值。我们首先使用var_assign增加变量使用序列,检索新名称,并在defs中更新新名称。

class AssignmentVars(AssignmentVars):
    def _set_kv(self, var, val):
        s_var = self.var_access(var)
        if s_var in self.defs and self.defs[s_var] == val:
            return
        self.defs[self.var_assign(var)] = val 

这里是如何使用它的。第一次将变量赋值初始化其计数器。

sav = AssignmentVars('')
sav['x'] = 'X'
sav.defs 
{('x', 1): 'X'}

如果变量再次以相同的值赋值,那么它可能不是重新赋值。

sav['x'] = 'X'
sav.defs 
{('x', 1): 'X'}

然而,如果值发生了变化,它就是一个重新赋值。

sav['x'] = 'Y'
sav.defs 
{('x', 1): 'X', ('x', 2): 'Y'}

这里有一个微妙之处。子方法可以从父方法的中部被调用,并且两者可以使用不同的值具有相同的变量名。在这种情况下,当子方法返回时,父方法将具有上下文中的旧变量和旧值。在我们的实现中,我们将其视为重新赋值。然而,这是可以的,因为添加新的重新赋值是无害的,但遗漏一个则不行。此外,我们稍后还会讨论如何避免这种情况。

我们还定义了register_event()method_enter()method_exit()的记账代码,这些是负责跟踪方法栈的方法。基本思想是,每个method_enter()代表一个新的方法调用。因此,它值得一个新的方法 ID,该 ID 由method_register生成,并保存在method_id中。由于这是一个新的方法,方法栈通过一个具有此 ID 的元素进行扩展。在method_exit()的情况下,我们弹出方法栈,并将当前method_id重置为当前一个以下的值。

class AssignmentVars(AssignmentVars):
    def method_enter(self, cxt, my_vars):
        self.current_event = 'call'
        self.call_stack.enter(cxt.method)
        self.event_locations[self.call_stack.method_id] = []
        self.register_event(cxt)
        self.update(my_vars)

    def method_exit(self, cxt, my_vars):
        self.current_event = 'return'
        self.register_event(cxt)
        self.update(my_vars)
        self.call_stack.leave()

    def method_statement(self, cxt, my_vars):
        self.current_event = 'line'
        self.register_event(cxt)
        self.update(my_vars) 

对于每个方法事件,我们还会使用 register_event() 注册事件,该事件跟踪 方法中引用的行号。

class AssignmentVars(AssignmentVars):
    def register_event(self, cxt):
        self.event_locations[self.call_stack.method_id].append(cxt.line_no) 

var_location_register() 保存新添加变量的位置。在 call 中的变量定义位置是 当前 位置。然而,对于 line,它将是当前方法中的前一个事件。

class AssignmentVars(AssignmentVars):
    def var_location_register(self, my_vars):
        def loc(mid):
            if self.current_event == 'call':
                return self.event_locations[mid][-1]
            elif self.current_event == 'line':
                return self.event_locations[mid][-2]
            elif self.current_event == 'return':
                return self.event_locations[mid][-2]
            else:
                assert False

        my_loc = loc(self.call_stack.method_id)
        for var in my_vars:
            self.var_def_lines[var] = my_loc 

我们定义 defined_vars(),它返回以下标注行号的变量的名称。

class AssignmentVars(AssignmentVars):
    def defined_vars(self, formatted=True):
        def fmt(k):
            v = (k[0], self.var_def_lines[k])
            return "%s@%s" % v if formatted else v

        return [(fmt(k), v) for k, v in self.defs.items()] 

defined_vars() 类似,我们定义了 seq_vars(),它用使用次数标注不同的变量。

class AssignmentVars(AssignmentVars):
    def seq_vars(self, formatted=True):
        def fmt(k):
            v = (k[0], self.var_def_lines[k], k[1])
            return "%s@%s:%s" % v if formatted else v

        return {fmt(k): v for k, v in self.defs.items()} 

AssignmentTracker

AssignmentTracker 使用我们之前定义的 AssignmentVars 保存赋值定义。

class AssignmentTracker(DefineTracker):
    def __init__(self, my_input, trace, **kwargs):
        self.options(kwargs)
        self.my_input = my_input

        self.my_assignments = self.create_assignments(my_input)

        self.trace = trace
        self.process()

    def create_assignments(self, *args):
        return AssignmentVars(*args) 

为了微调过程,我们定义了一个可选参数,称为 track_return。在跟踪方法返回时,Python 会产生一个包含返回值结果的虚拟变量。如果设置 track_return,我们将捕获此值作为变量。

  • track_return -- 如果为真,则向 Vars 添加一个 虚拟变量 来表示返回值
class AssignmentTracker(AssignmentTracker):
    def options(self, kwargs):
        self.track_return = kwargs.get('track_return', False)
        super().options(kwargs) 

在跟踪过程中,可能会有不同类型的事件,包括 call(当函数进入时),return(当函数返回时),exception(当抛出异常时)和 line(当执行语句时)。

之前的 Tracker 过于简单,因为它没有区分不同的事件。我们纠正了这一点,并分别定义了 on_call()on_return()on_line(),它们将在对应的事件上被调用。

注意,on_line() 也会被调用于 on_return()。原因是,Python 在执行相应的行之前会调用跟踪函数。因此,实际上,on_return() 是在环境执行前一个语句产生的绑定下被调用的。我们的处理实际上是在前一个语句绑定的值上进行的。因此,在这里调用 on_line() 是合适的,因为它给事件处理程序提供了一个机会来处理前一个绑定。

class AssignmentTracker(AssignmentTracker):
    def on_call(self, arg, cxt, my_vars):
        my_vars = cxt.parameters(my_vars)
        self.my_assignments.method_enter(cxt, self.fragments(my_vars))

    def on_line(self, arg, cxt, my_vars):
        self.my_assignments.method_statement(cxt, self.fragments(my_vars))

    def on_return(self, arg, cxt, my_vars):
        self.on_line(arg, cxt, my_vars)
        my_vars = {'<-%s' % cxt.method: arg} if self.track_return else {}
        self.my_assignments.method_exit(cxt, my_vars)

    def on_exception(self, arg, cxt, my_vara):
        return

    def track_event(self, event, arg, cxt, my_vars):
        self.current_event = event
        dispatch = {
            'call': self.on_call,
            'return': self.on_return,
            'line': self.on_line,
            'exception': self.on_exception
        }
        dispatchevent 

我们现在可以使用 AssignmentTracker 跟踪不同的变量。为了验证我们的变量行号推断是否有效,我们从函数 A()B()C()(移除了数据注释,以便正确识别输入片段)中恢复定义。

def C(cp_1):
    c_2 = cp_1
    c_3 = c_2
    return c_3 
def B(bp_7):
    b_8 = bp_7
    return C(b_8) 
def A(ap_12):
    a_13 = ap_12
    a_14 = B(a_13)
    a_14 = a_14
    a_13 = a_14
    a_14 = B(a_13)
    a_14 = B(a_14)[3:] 

用足够的输入运行 A()

with Tracer('---xxx') as tracer:
    A(tracer.my_input)
tracker = AssignmentTracker(tracer.my_input, tracer.trace, log=True)
for k, v in tracker.my_assignments.seq_vars().items():
    print(k, '=', repr(v))
print()
for k, v in tracker.my_assignments.defined_vars(formatted=True):
    print(k, '=', repr(v)) 
ap_12@1:1 = '---xxx'
a_13@2:1 = '---xxx'
bp_7@1:1 = '---xxx'
b_8@2:1 = '---xxx'
cp_1@1:1 = '---xxx'
c_2@2:1 = '---xxx'
c_3@3:1 = '---xxx'
a_14@3:1 = '---xxx'
a_14@7:2 = 'xxx'

ap_12@1 = '---xxx'
a_13@2 = '---xxx'
bp_7@1 = '---xxx'
b_8@2 = '---xxx'
cp_1@1 = '---xxx'
c_2@2 = '---xxx'
c_3@3 = '---xxx'
a_14@3 = '---xxx'
a_14@7 = 'xxx'

如所示,现在每个变量的行号都已正确识别。

让我们尝试检索一个真实世界示例的赋值。

traces = []
for inputstr in URLS_X:
    clear_cache()
    with Tracer(inputstr, files=['urllib/parse.py']) as tracer:
        urlparse(tracer.my_input)
    traces.append((tracer.my_input, tracer.trace))

    tracker = AssignmentTracker(tracer.my_input, tracer.trace, log=True)
    for k, v in tracker.my_assignments.defined_vars():
        print(k, '=', repr(v))
    print() 
url@374 = 'http://user:pass@www.google.com:80/?q=path#ref'
url@492 = '//user:pass@www.google.com:80/?q=path#ref'
scheme@492 = 'http'
url@494 = '/?q=path#ref'
netloc@494 = 'user:pass@www.google.com:80'
url@502 = '/?q=path'
fragment@502 = 'ref'
query@504 = 'q=path'
url@395 = 'http://user:pass@www.google.com:80/?q=path#ref'

url@374 = 'https://www.cispa.saarland:80/'
url@492 = '//www.cispa.saarland:80/'
scheme@492 = 'https'
netloc@494 = 'www.cispa.saarland:80'
url@395 = 'https://www.cispa.saarland:80/'

url@374 = 'http://www.fuzzingbook.org/#News'
url@492 = '//www.fuzzingbook.org/#News'
scheme@492 = 'http'
url@494 = '/#News'
netloc@494 = 'www.fuzzingbook.org'
fragment@502 = 'News'
url@395 = 'http://www.fuzzingbook.org/#News'

url@374 = 'ftp://freebsd.org/releases/5.8'
url@492 = '//freebsd.org/releases/5.8'
scheme@492 = 'ftp'
url@494 = '/releases/5.8'
netloc@494 = 'freebsd.org'
url@395 = 'ftp://freebsd.org/releases/5.8'
url@396 = '/releases/5.8'

变量的行号可以从 urllib/parse.py 的源代码中进行验证。

恢复推导树

处理变量重新赋值是否有助于我们的 URL 示例?我们接下来看看这些。

class TreeMiner(TreeMiner):
    def get_derivation_tree(self):
        tree = (START_SYMBOL, [(self.my_input, [])])
        for var, value in self.my_assignments:
            self.log(0, "%s=%s" % (var, repr(value)))
            self.apply_new_definition(tree, var, value)
        return tree 

示例 1:恢复 URL 推导树

首先,我们获取 URL 1 的推导树

URL 1 推导树
clear_cache()
with Tracer(URLS_X[0], files=['urllib/parse.py']) as tracer:
    urlparse(tracer.my_input)
sm = AssignmentTracker(tracer.my_input, tracer.trace)
dt = TreeMiner(tracer.my_input, sm.my_assignments.defined_vars())
display_tree(dt.tree) 

0 1 url@374 0->1 2 scheme@492 1->2 4 : (58) 1->4 5 url@492 1->5 3 http 2->3 6 // 5->6 7 netloc@494 5->7 9 url@494 5->9 8 user:pass@www.google.com:80 7->8 10 url@502 9->10 14 # (35) 9->14 15 fragment@502 9->15 11 /? 10->11 12 query@504 10->12 13 q=path 12->13 16 ref 15->16

接下来,我们获取 URL 4 的推导树

URL 4 推导树
clear_cache()
with Tracer(URLS_X[-1], files=['urllib/parse.py']) as tracer:
    urlparse(tracer.my_input)
sm = AssignmentTracker(tracer.my_input, tracer.trace)
dt = TreeMiner(tracer.my_input, sm.my_assignments.defined_vars())
display_tree(dt.tree) 

0 1 url@374 0->1 2 scheme@492 1->2 4 : (58) 1->4 5 url@492 1->5 3 ftp 2->3 6 // 5->6 7 netloc@494 5->7 9 url@494 5->9 8 freebsd.org 7->8 10 url@396 9->10 11 /releases/5.8 10->11

分析树似乎属于同一个语法。因此,我们获得了整个集合的语法。首先,我们更新recover_grammar()以使用AssignTracker

恢复语法

class GrammarMiner(GrammarMiner):
    def update_grammar(self, inputstr, trace):
        at = self.create_tracker(inputstr, trace)
        dt = self.create_tree_miner(inputstr, at.my_assignments.defined_vars())
        self.add_tree(dt)
        return self.grammar

    def create_tracker(self, *args):
        return AssignmentTracker(*args)

    def create_tree_miner(self, *args):
        return TreeMiner(*args) 

接下来,我们使用修改后的recover_grammar()对从 URL 获得的推导树进行处理。

url_grammar = recover_grammar(url_parse, URLS_X, files=['urllib/parse.py']) 

恢复的语法如下。

syntax_diagram(url_grammar) 
start

url@374

url@374

scheme@492 : url@492

scheme@492

http ftp https

url@492

// netloc@494 url@494 // netloc@494 /

netloc@494

www.fuzzingbook.org www.cispa.saarland:80 freebsd.org user:pass@www.google.com:80

url@494

url@396 /# fragment@502 url@502 # fragment@502

url@502

/? query@504

query@504

q=path

fragment@502

News ref

url@396

/releases/5.8

让我们稍微模糊一下,看看产生的值是否合理。

f = GrammarFuzzer(url_grammar)
for _ in range(10):
    print(f.fuzz()) 
http://freebsd.org/
ftp://freebsd.org/releases/5.8
http://www.cispa.saarland:80/
ftp://freebsd.org/releases/5.8
https://user:pass@www.google.com:80/releases/5.8
https://freebsd.org/
ftp://www.cispa.saarland:80/?q=path#News
http://www.fuzzingbook.org/
https://www.cispa.saarland:80/
ftp://user:pass@www.google.com:80/

我们的修改似乎有所帮助。接下来,我们检查是否还能检索到库存的语法。

示例 2:恢复库存语法

inventory_grammar = recover_grammar(process_vehicle, VEHICLES) 
syntax_diagram(inventory_grammar) 
start

vehicle@29

vehicle@29

year@30 , kind@30 , company@30 , model@30

year@30

1999 2000 1997

kind@30

car van

company@30

Mercury Chevy Ford

model@30

E350 Cougar Venture

使用模糊化从语法中产生值。

f = GrammarFuzzer(inventory_grammar)
for _ in range(10):
    print(f.fuzz()) 
1997,van,Chevy,E350
1999,van,Mercury,E350
2000,van,Chevy,Venture
2000,van,Ford,E350
1997,van,Mercury,Cougar
1997,car,Ford,E350
1997,car,Mercury,Venture
1997,car,Mercury,E350
2000,van,Mercury,Cougar
1997,car,Chevy,Venture

语法挖掘器重新赋值的问题

我们语法挖掘器的一个问题是它还没有考虑到当前上下文。也就是说,在替换时,一个变量可以替换它无法访问的标记(因此,它不是一个片段)。考虑以下例子。

with Tracer(INVENTORY) as tracer:
    process_inventory(tracer.my_input)
sm = AssignmentTracker(tracer.my_input, tracer.trace)
dt = TreeMiner(tracer.my_input, sm.my_assignments.defined_vars())
display_tree(dt.tree, graph_attr=lr_graph) 

0 1 inventory@22 0->1 2 vehicle@24 1->2 14 \n (10) 1->14 15 vehicle@24 1->15 27 \n (10) 1->27 28 vehicle@24 1->28 3 year@30 2->3 5 , (44) 2->5 6 kind@30 2->6 8 , (44) 2->8 9 company@30 2->9 11 , (44) 2->11 12 model@30 2->12 4 1997 3->4 7 van 6->7 10 Ford 9->10 13 E350 12->13 16 year@30 15->16 18 , (44) 15->18 19 kind@30 15->19 21 , (44) 15->21 22 company@30 15->22 24 , (44) 15->24 25 model@30 15->25 17 2000 16->17 20 car 19->20 23 Mercury 22->23 26 Cougar 25->26 29 year@30 28->29 31 ,car, 28->31 32 company@30 28->32 34 , (44) 28->34 35 model@30 28->35 30 1999 29->30 33 Chevy 32->33 36 Venture 35->36

如所示,我们获得的分析树并不完全符合我们的预期。如果我们启用TreeMiner的日志记录,问题就很容易被发现。

dt = TreeMiner(tracer.my_input, sm.my_assignments.defined_vars(), log=True) 
 inventory@22='1997,van,Ford,E350\n2000,car,Mercury,Cougar\n1999,car,Chevy,Venture'
	 - Node: <start>		? (<inventory@22>:'1997,van,Ford,E350\n2000,car,Mercury,Cougar\n1999,car,Chevy,Venture')
		 -> [0] '1997,van,Ford,E350\n2000,car,Mercury,Cougar\n1999,car,Chevy,Venture'
		  > ['<inventory@22>']
 vehicle@24='1997,van,Ford,E350'
	 - Node: <start>		? (<vehicle@24>:'1997,van,Ford,E350')
		 -> [0] '<inventory@22>'
	 - Node: <inventory@22>		? (<vehicle@24>:'1997,van,Ford,E350')
		 -> [0] '1997,van,Ford,E350\n2000,car,Mercury,Cougar\n1999,car,Chevy,Venture'
		  > ['<vehicle@24>', '\n2000,car,Mercury,Cougar\n1999,car,Chevy,Venture']
 year@30='1997'
	 - Node: <start>		? (<year@30>:'1997')
		 -> [0] '<inventory@22>'
	 - Node: <inventory@22>		? (<year@30>:'1997')
		 -> [0] '<vehicle@24>'
	 - Node: <vehicle@24>		? (<year@30>:'1997')
		 -> [0] '1997,van,Ford,E350'
		  > ['<year@30>', ',van,Ford,E350']
 kind@30='van'
	 - Node: <start>		? (<kind@30>:'van')
		 -> [0] '<inventory@22>'
	 - Node: <inventory@22>		? (<kind@30>:'van')
		 -> [0] '<vehicle@24>'
	 - Node: <vehicle@24>		? (<kind@30>:'van')
		 -> [0] '<year@30>'
	 - Node: <year@30>		? (<kind@30>:'van')
		 -> [0] '1997'
		 -> [1] ',van,Ford,E350'
		  > [',', '<kind@30>', ',Ford,E350']
 company@30='Ford'
	 - Node: <start>		? (<company@30>:'Ford')
		 -> [0] '<inventory@22>'
	 - Node: <inventory@22>		? (<company@30>:'Ford')
		 -> [0] '<vehicle@24>'
	 - Node: <vehicle@24>		? (<company@30>:'Ford')
		 -> [0] '<year@30>'
	 - Node: <year@30>		? (<company@30>:'Ford')
		 -> [0] '1997'
		 -> [1] ','
		 -> [2] '<kind@30>'
	 - Node: <kind@30>		? (<company@30>:'Ford')
		 -> [0] 'van'
		 -> [3] ',Ford,E350'
		  > [',', '<company@30>', ',E350']
 model@30='E350'
	 - Node: <start>		? (<model@30>:'E350')
		 -> [0] '<inventory@22>'
	 - Node: <inventory@22>		? (<model@30>:'E350')
		 -> [0] '<vehicle@24>'
	 - Node: <vehicle@24>		? (<model@30>:'E350')
		 -> [0] '<year@30>'
	 - Node: <year@30>		? (<model@30>:'E350')
		 -> [0] '1997'
		 -> [1] ','
		 -> [2] '<kind@30>'
	 - Node: <kind@30>		? (<model@30>:'E350')
		 -> [0] 'van'
		 -> [3] ','
		 -> [4] '<company@30>'
	 - Node: <company@30>		? (<model@30>:'E350')
		 -> [0] 'Ford'
		 -> [5] ',E350'
		  > [',', '<model@30>']
 vehicle@24='2000,car,Mercury,Cougar'
	 - Node: <start>		? (<vehicle@24>:'2000,car,Mercury,Cougar')
		 -> [0] '<inventory@22>'
	 - Node: <inventory@22>		? (<vehicle@24>:'2000,car,Mercury,Cougar')
		 -> [0] '<vehicle@24>'
	 - Node: <vehicle@24>		? (<vehicle@24>:'2000,car,Mercury,Cougar')
		 -> [0] '<year@30>'
	 - Node: <year@30>		? (<vehicle@24>:'2000,car,Mercury,Cougar')
		 -> [0] '1997'
		 -> [1] ','
		 -> [2] '<kind@30>'
	 - Node: <kind@30>		? (<vehicle@24>:'2000,car,Mercury,Cougar')
		 -> [0] 'van'
		 -> [3] ','
		 -> [4] '<company@30>'
	 - Node: <company@30>		? (<vehicle@24>:'2000,car,Mercury,Cougar')
		 -> [0] 'Ford'
		 -> [5] ','
		 -> [6] '<model@30>'
	 - Node: <model@30>		? (<vehicle@24>:'2000,car,Mercury,Cougar')
		 -> [0] 'E350'
		 -> [1] '\n2000,car,Mercury,Cougar\n1999,car,Chevy,Venture'
		  > ['\n', '<vehicle@24>', '\n1999,car,Chevy,Venture']
 year@30='2000'
	 - Node: <start>		? (<year@30>:'2000')
		 -> [0] '<inventory@22>'
	 - Node: <inventory@22>		? (<year@30>:'2000')
		 -> [0] '<vehicle@24>'
	 - Node: <vehicle@24>		? (<year@30>:'2000')
		 -> [0] '<year@30>'
	 - Node: <year@30>		? (<year@30>:'2000')
		 -> [0] '1997'
		 -> [1] ','
		 -> [2] '<kind@30>'
	 - Node: <kind@30>		? (<year@30>:'2000')
		 -> [0] 'van'
		 -> [3] ','
		 -> [4] '<company@30>'
	 - Node: <company@30>		? (<year@30>:'2000')
		 -> [0] 'Ford'
		 -> [5] ','
		 -> [6] '<model@30>'
	 - Node: <model@30>		? (<year@30>:'2000')
		 -> [0] 'E350'
		 -> [1] '\n'
		 -> [2] '<vehicle@24>'
	 - Node: <vehicle@24>		? (<year@30>:'2000')
		 -> [0] '2000,car,Mercury,Cougar'
		  > ['<year@30>', ',car,Mercury,Cougar']
 kind@30='car'
	 - Node: <start>		? (<kind@30>:'car')
		 -> [0] '<inventory@22>'
	 - Node: <inventory@22>		? (<kind@30>:'car')
		 -> [0] '<vehicle@24>'
	 - Node: <vehicle@24>		? (<kind@30>:'car')
		 -> [0] '<year@30>'
	 - Node: <year@30>		? (<kind@30>:'car')
		 -> [0] '1997'
		 -> [1] ','
		 -> [2] '<kind@30>'
	 - Node: <kind@30>		? (<kind@30>:'car')
		 -> [0] 'van'
		 -> [3] ','
		 -> [4] '<company@30>'
	 - Node: <company@30>		? (<kind@30>:'car')
		 -> [0] 'Ford'
		 -> [5] ','
		 -> [6] '<model@30>'
	 - Node: <model@30>		? (<kind@30>:'car')
		 -> [0] 'E350'
		 -> [1] '\n'
		 -> [2] '<vehicle@24>'
	 - Node: <vehicle@24>		? (<kind@30>:'car')
		 -> [0] '<year@30>'
	 - Node: <year@30>		? (<kind@30>:'car')
		 -> [0] '2000'
		 -> [1] ',car,Mercury,Cougar'
		  > [',', '<kind@30>', ',Mercury,Cougar']
 company@30='Mercury'
	 - Node: <start>		? (<company@30>:'Mercury')
		 -> [0] '<inventory@22>'
	 - Node: <inventory@22>		? (<company@30>:'Mercury')
		 -> [0] '<vehicle@24>'
	 - Node: <vehicle@24>		? (<company@30>:'Mercury')
		 -> [0] '<year@30>'
	 - Node: <year@30>		? (<company@30>:'Mercury')
		 -> [0] '1997'
		 -> [1] ','
		 -> [2] '<kind@30>'
	 - Node: <kind@30>		? (<company@30>:'Mercury')
		 -> [0] 'van'
		 -> [3] ','
		 -> [4] '<company@30>'
	 - Node: <company@30>		? (<company@30>:'Mercury')
		 -> [0] 'Ford'
		 -> [5] ','
		 -> [6] '<model@30>'
	 - Node: <model@30>		? (<company@30>:'Mercury')
		 -> [0] 'E350'
		 -> [1] '\n'
		 -> [2] '<vehicle@24>'
	 - Node: <vehicle@24>		? (<company@30>:'Mercury')
		 -> [0] '<year@30>'
	 - Node: <year@30>		? (<company@30>:'Mercury')
		 -> [0] '2000'
		 -> [1] ','
		 -> [2] '<kind@30>'
	 - Node: <kind@30>		? (<company@30>:'Mercury')
		 -> [0] 'car'
		 -> [3] ',Mercury,Cougar'
		  > [',', '<company@30>', ',Cougar']
 model@30='Cougar'
	 - Node: <start>		? (<model@30>:'Cougar')
		 -> [0] '<inventory@22>'
	 - Node: <inventory@22>		? (<model@30>:'Cougar')
		 -> [0] '<vehicle@24>'
	 - Node: <vehicle@24>		? (<model@30>:'Cougar')
		 -> [0] '<year@30>'
	 - Node: <year@30>		? (<model@30>:'Cougar')
		 -> [0] '1997'
		 -> [1] ','
		 -> [2] '<kind@30>'
	 - Node: <kind@30>		? (<model@30>:'Cougar')
		 -> [0] 'van'
		 -> [3] ','
		 -> [4] '<company@30>'
	 - Node: <company@30>		? (<model@30>:'Cougar')
		 -> [0] 'Ford'
		 -> [5] ','
		 -> [6] '<model@30>'
	 - Node: <model@30>		? (<model@30>:'Cougar')
		 -> [0] 'E350'
		 -> [1] '\n'
		 -> [2] '<vehicle@24>'
	 - Node: <vehicle@24>		? (<model@30>:'Cougar')
		 -> [0] '<year@30>'
	 - Node: <year@30>		? (<model@30>:'Cougar')
		 -> [0] '2000'
		 -> [1] ','
		 -> [2] '<kind@30>'
	 - Node: <kind@30>		? (<model@30>:'Cougar')
		 -> [0] 'car'
		 -> [3] ','
		 -> [4] '<company@30>'
	 - Node: <company@30>		? (<model@30>:'Cougar')
		 -> [0] 'Mercury'
		 -> [5] ',Cougar'
		  > [',', '<model@30>']
 vehicle@24='1999,car,Chevy,Venture'
	 - Node: <start>		? (<vehicle@24>:'1999,car,Chevy,Venture')
		 -> [0] '<inventory@22>'
	 - Node: <inventory@22>		? (<vehicle@24>:'1999,car,Chevy,Venture')
		 -> [0] '<vehicle@24>'
	 - Node: <vehicle@24>		? (<vehicle@24>:'1999,car,Chevy,Venture')
		 -> [0] '<year@30>'
	 - Node: <year@30>		? (<vehicle@24>:'1999,car,Chevy,Venture')
		 -> [0] '1997'
		 -> [1] ','
		 -> [2] '<kind@30>'
	 - Node: <kind@30>		? (<vehicle@24>:'1999,car,Chevy,Venture')
		 -> [0] 'van'
		 -> [3] ','
		 -> [4] '<company@30>'
	 - Node: <company@30>		? (<vehicle@24>:'1999,car,Chevy,Venture')
		 -> [0] 'Ford'
		 -> [5] ','
		 -> [6] '<model@30>'
	 - Node: <model@30>		? (<vehicle@24>:'1999,car,Chevy,Venture')
		 -> [0] 'E350'
		 -> [1] '\n'
		 -> [2] '<vehicle@24>'
	 - Node: <vehicle@24>		? (<vehicle@24>:'1999,car,Chevy,Venture')
		 -> [0] '<year@30>'
	 - Node: <year@30>		? (<vehicle@24>:'1999,car,Chevy,Venture')
		 -> [0] '2000'
		 -> [1] ','
		 -> [2] '<kind@30>'
	 - Node: <kind@30>		? (<vehicle@24>:'1999,car,Chevy,Venture')
		 -> [0] 'car'
		 -> [3] ','
		 -> [4] '<company@30>'
	 - Node: <company@30>		? (<vehicle@24>:'1999,car,Chevy,Venture')
		 -> [0] 'Mercury'
		 -> [5] ','
		 -> [6] '<model@30>'
	 - Node: <model@30>		? (<vehicle@24>:'1999,car,Chevy,Venture')
		 -> [0] 'Cougar'
		 -> [3] '\n1999,car,Chevy,Venture'
		  > ['\n', '<vehicle@24>']
 year@30='1999'
	 - Node: <start>		? (<year@30>:'1999')
		 -> [0] '<inventory@22>'
	 - Node: <inventory@22>		? (<year@30>:'1999')
		 -> [0] '<vehicle@24>'
	 - Node: <vehicle@24>		? (<year@30>:'1999')
		 -> [0] '<year@30>'
	 - Node: <year@30>		? (<year@30>:'1999')
		 -> [0] '1997'
		 -> [1] ','
		 -> [2] '<kind@30>'
	 - Node: <kind@30>		? (<year@30>:'1999')
		 -> [0] 'van'
		 -> [3] ','
		 -> [4] '<company@30>'
	 - Node: <company@30>		? (<year@30>:'1999')
		 -> [0] 'Ford'
		 -> [5] ','
		 -> [6] '<model@30>'
	 - Node: <model@30>		? (<year@30>:'1999')
		 -> [0] 'E350'
		 -> [1] '\n'
		 -> [2] '<vehicle@24>'
	 - Node: <vehicle@24>		? (<year@30>:'1999')
		 -> [0] '<year@30>'
	 - Node: <year@30>		? (<year@30>:'1999')
		 -> [0] '2000'
		 -> [1] ','
		 -> [2] '<kind@30>'
	 - Node: <kind@30>		? (<year@30>:'1999')
		 -> [0] 'car'
		 -> [3] ','
		 -> [4] '<company@30>'
	 - Node: <company@30>		? (<year@30>:'1999')
		 -> [0] 'Mercury'
		 -> [5] ','
		 -> [6] '<model@30>'
	 - Node: <model@30>		? (<year@30>:'1999')
		 -> [0] 'Cougar'
		 -> [3] '\n'
		 -> [4] '<vehicle@24>'
	 - Node: <vehicle@24>		? (<year@30>:'1999')
		 -> [0] '1999,car,Chevy,Venture'
		  > ['<year@30>', ',car,Chevy,Venture']
 company@30='Chevy'
	 - Node: <start>		? (<company@30>:'Chevy')
		 -> [0] '<inventory@22>'
	 - Node: <inventory@22>		? (<company@30>:'Chevy')
		 -> [0] '<vehicle@24>'
	 - Node: <vehicle@24>		? (<company@30>:'Chevy')
		 -> [0] '<year@30>'
	 - Node: <year@30>		? (<company@30>:'Chevy')
		 -> [0] '1997'
		 -> [1] ','
		 -> [2] '<kind@30>'
	 - Node: <kind@30>		? (<company@30>:'Chevy')
		 -> [0] 'van'
		 -> [3] ','
		 -> [4] '<company@30>'
	 - Node: <company@30>		? (<company@30>:'Chevy')
		 -> [0] 'Ford'
		 -> [5] ','
		 -> [6] '<model@30>'
	 - Node: <model@30>		? (<company@30>:'Chevy')
		 -> [0] 'E350'
		 -> [1] '\n'
		 -> [2] '<vehicle@24>'
	 - Node: <vehicle@24>		? (<company@30>:'Chevy')
		 -> [0] '<year@30>'
	 - Node: <year@30>		? (<company@30>:'Chevy')
		 -> [0] '2000'
		 -> [1] ','
		 -> [2] '<kind@30>'
	 - Node: <kind@30>		? (<company@30>:'Chevy')
		 -> [0] 'car'
		 -> [3] ','
		 -> [4] '<company@30>'
	 - Node: <company@30>		? (<company@30>:'Chevy')
		 -> [0] 'Mercury'
		 -> [5] ','
		 -> [6] '<model@30>'
	 - Node: <model@30>		? (<company@30>:'Chevy')
		 -> [0] 'Cougar'
		 -> [3] '\n'
		 -> [4] '<vehicle@24>'
	 - Node: <vehicle@24>		? (<company@30>:'Chevy')
		 -> [0] '<year@30>'
	 - Node: <year@30>		? (<company@30>:'Chevy')
		 -> [0] '1999'
		 -> [1] ',car,Chevy,Venture'
		  > [',car,', '<company@30>', ',Venture']
 model@30='Venture'
	 - Node: <start>		? (<model@30>:'Venture')
		 -> [0] '<inventory@22>'
	 - Node: <inventory@22>		? (<model@30>:'Venture')
		 -> [0] '<vehicle@24>'
	 - Node: <vehicle@24>		? (<model@30>:'Venture')
		 -> [0] '<year@30>'
	 - Node: <year@30>		? (<model@30>:'Venture')
		 -> [0] '1997'
		 -> [1] ','
		 -> [2] '<kind@30>'
	 - Node: <kind@30>		? (<model@30>:'Venture')
		 -> [0] 'van'
		 -> [3] ','
		 -> [4] '<company@30>'
	 - Node: <company@30>		? (<model@30>:'Venture')
		 -> [0] 'Ford'
		 -> [5] ','
		 -> [6] '<model@30>'
	 - Node: <model@30>		? (<model@30>:'Venture')
		 -> [0] 'E350'
		 -> [1] '\n'
		 -> [2] '<vehicle@24>'
	 - Node: <vehicle@24>		? (<model@30>:'Venture')
		 -> [0] '<year@30>'
	 - Node: <year@30>		? (<model@30>:'Venture')
		 -> [0] '2000'
		 -> [1] ','
		 -> [2] '<kind@30>'
	 - Node: <kind@30>		? (<model@30>:'Venture')
		 -> [0] 'car'
		 -> [3] ','
		 -> [4] '<company@30>'
	 - Node: <company@30>		? (<model@30>:'Venture')
		 -> [0] 'Mercury'
		 -> [5] ','
		 -> [6] '<model@30>'
	 - Node: <model@30>		? (<model@30>:'Venture')
		 -> [0] 'Cougar'
		 -> [3] '\n'
		 -> [4] '<vehicle@24>'
	 - Node: <vehicle@24>		? (<model@30>:'Venture')
		 -> [0] '<year@30>'
	 - Node: <year@30>		? (<model@30>:'Venture')
		 -> [0] '1999'
		 -> [1] ',car,'
		 -> [2] '<company@30>'
	 - Node: <company@30>		? (<model@30>:'Venture')
		 -> [0] 'Chevy'
		 -> [3] ',Venture'
		  > [',', '<model@30>']

看看最后一条语句。我们有一个值1999,car,,其中只有year被替换了。我们不再有'car'变量来继续这里的替换。这是因为'1999,car,Chevy,Venture'中的'car'值没有被当作新值处理,因为'car'值在另一个方法调用('2000,car,Mercury,Cougar')的相同位置已经出现过。

带作用域的语法挖掘器

我们需要将当前上下文中的变量检查纳入其中。我们已经有了一个方法调用栈,这样我们可以在任何时刻获取当前方法。我们需要对变量做同样的处理。

为了做到这一点,我们将CallStack扩展到新的类InputStack,该类保存了调用的方法以及观察到的参数。它本质上是对方法激活的记录。我们从栈底的原始输入开始,对于每个新的方法调用,我们将该调用的参数作为新记录推入栈中。

输入栈

class InputStack(CallStack):
    def __init__(self, i, fragment_len=FRAGMENT_LEN):
        self.inputs = [{START_SYMBOL: i}]
        self.fragment_len = fragment_len
        super().__init__() 

为了检查特定变量是否需要保存,我们定义了in_current_record(),它只检查当前作用域中的变量是否包含(而不是原始输入字符串)。

class InputStack(InputStack):
    def in_current_record(self, val):
        return any(val in var for var in self.inputs[-1].values()) 
my_istack = InputStack('hello my world') 
my_istack.in_current_record('hello') 
True

my_istack.in_current_record('bye') 
False

my_istack.inputs.append({'greeting': 'hello', 'location': 'world'}) 
my_istack.in_current_record('hello') 
True

my_istack.in_current_record('my') 
False

我们定义了一个ignored()方法,如果变量不是字符串,或者变量长度小于定义的fragment_len,则返回 true。

class InputStack(InputStack):
    def ignored(self, val):
        return not (isinstance(val, str) and len(val) >= self.fragment_len) 
my_istack = InputStack('hello world')
my_istack.ignored(1) 
True

my_istack.ignored('a') 
True

my_istack.ignored('help') 
False

我们现在可以定义一个in_scope()方法,该方法检查变量是否需要被忽略,如果不忽略,则检查变量值是否存在于当前作用域中。

class InputStack(InputStack):
    def in_scope(self, k, val):
        if self.ignored(val):
            return False
        return self.in_current_record(val) 

最后,我们更新enter(),将当前上下文中的相关变量推送到栈中。

class InputStack(InputStack):
    def enter(self, method, inputs):
        my_inputs = {k: v for k, v in inputs.items() if self.in_scope(k, v)}
        self.inputs.append(my_inputs)
        super().enter(method) 

当一个方法返回时,我们还需要一个相应的leave()来弹出输入并展开栈。

class InputStack(InputStack):
    def leave(self):
        self.inputs.pop()
        super().leave() 

ScopedVars

我们需要更新我们的AssignmentVars,包括变量定义的作用域信息。我们首先从更新method_init()开始。

class ScopedVars(AssignmentVars):
    def method_init(self):
        self.call_stack = self.create_call_stack(self.my_input)
        self.event_locations = {self.call_stack.method_id: []}

    def create_call_stack(self, i):
        return InputStack(i) 

同样,method_enter()现在初始化当前方法调用的accessed_seq_var

class ScopedVars(ScopedVars):
    def method_enter(self, cxt, my_vars):
        self.current_event = 'call'
        self.call_stack.enter(cxt.method, my_vars)
        self.accessed_seq_var[self.call_stack.method_id] = {}
        self.event_locations[self.call_stack.method_id] = []
        self.register_event(cxt)
        self.update(my_vars) 

update()方法现在保存定义值的上下文。在函数参数的情况下,上下文应该是函数被调用的上下文。另一方面,在语句执行期间定义的值将具有当前上下文。

此外,我们在值上而不是在键上做注释,因为我们不希望在下一行参数上下文中重复变量。它们将具有相同的值,但不同的上下文,因为它们存在于语句执行中。

class ScopedVars(ScopedVars):
    def update(self, v):
        if self.current_event == 'call':
            context = -2
        elif self.current_event == 'line':
            context = -1
        else:
            context = -1
        for k, v in v.items():
            self._set_kv(k, (v, self.call_stack.at(context)))
        self.var_location_register(self.new_vars)
        self.new_vars = set() 

我们还需要保存当前方法调用以确定哪些变量在作用域中。现在,这个信息被纳入变量名作为accessed_seq_var[method_id][var]

class ScopedVars(ScopedVars):
    def var_name(self, var):
        return (var, self.call_stack.method_id,
                self.accessed_seq_var[self.call_stack.method_id][var]) 

如前所述,var_access简单地初始化相应的计数器,这次是在method_id的上下文中。

class ScopedVars(ScopedVars):
    def var_access(self, var):
        if var not in self.accessed_seq_var[self.call_stack.method_id]:
            self.accessed_seq_var[self.call_stack.method_id][var] = 0
        return self.var_name(var) 

在变量重新赋值期间,我们更新accessed_seq_var以反映新的计数。

class ScopedVars(ScopedVars):
    def var_assign(self, var):
        self.accessed_seq_var[self.call_stack.method_id][var] += 1
        self.new_vars.add(self.var_name(var))
        return self.var_name(var) 

我们现在更新defined_vars()以考虑新的信息。

class ScopedVars(ScopedVars):
    def defined_vars(self, formatted=True):
        def fmt(k):
            method, i = k[1]
            v = (method, i, k[0], self.var_def_lines[k])
            return "%s[%d]:%s@%s" % v if formatted else v

        return [(fmt(k), v) for k, v in self.defs.items()] 

更新seq_vars()以考虑新的信息。

class ScopedVars(ScopedVars):
    def seq_vars(self, formatted=True):
        def fmt(k):
            method, i = k[1]
            v = (method, i, k[0], self.var_def_lines[k], k[2])
            return "%s[%d]:%s@%s:%s" % v if formatted else v

        return {fmt(k): v for k, v in self.defs.items()} 

作用域跟踪器

在定义了InputStackVars之后,我们现在可以定义ScopeTrackerScopeTracker只保存当前作用域中存在的变量。

class ScopeTracker(AssignmentTracker):
    def __init__(self, my_input, trace, **kwargs):
        self.current_event = None
        super().__init__(my_input, trace, **kwargs)

    def create_assignments(self, *args):
        return ScopedVars(*args) 

我们定义了一个检查变量是否存在于作用域中的包装器。

class ScopeTracker(ScopeTracker):
    def is_input_fragment(self, var, value):
        return self.my_assignments.call_stack.in_scope(var, value) 

我们可以这样使用ScopeTracker

vehicle_traces = []
with Tracer(INVENTORY) as tracer:
    process_inventory(tracer.my_input)
sm = ScopeTracker(tracer.my_input, tracer.trace)
vehicle_traces.append((tracer.my_input, sm))
for k, v in sm.my_assignments.seq_vars().items():
    print(k, '=', repr(v)) 
process_inventory[1]:inventory@22:1 = ('1997,van,Ford,E350\n2000,car,Mercury,Cougar\n1999,car,Chevy,Venture', ('<start>', 0))
process_inventory[1]:inventory@22:2 = ('1997,van,Ford,E350\n2000,car,Mercury,Cougar\n1999,car,Chevy,Venture', ('process_inventory', 1))
process_inventory[1]:vehicle@24:1 = ('1997,van,Ford,E350', ('process_inventory', 1))
process_vehicle[2]:vehicle@29:1 = ('1997,van,Ford,E350', ('process_inventory', 1))
process_vehicle[2]:vehicle@29:2 = ('1997,van,Ford,E350', ('process_vehicle', 2))
process_vehicle[2]:year@30:1 = ('1997', ('process_vehicle', 2))
process_vehicle[2]:kind@30:1 = ('van', ('process_vehicle', 2))
process_vehicle[2]:company@30:1 = ('Ford', ('process_vehicle', 2))
process_vehicle[2]:model@30:1 = ('E350', ('process_vehicle', 2))
process_van[3]:year@40:1 = ('1997', ('process_vehicle', 2))
process_van[3]:company@40:1 = ('Ford', ('process_vehicle', 2))
process_van[3]:model@40:1 = ('E350', ('process_vehicle', 2))
process_van[3]:year@40:2 = ('1997', ('process_van', 3))
process_van[3]:company@40:2 = ('Ford', ('process_van', 3))
process_van[3]:model@40:2 = ('E350', ('process_van', 3))
process_inventory[1]:vehicle@24:2 = ('2000,car,Mercury,Cougar', ('process_inventory', 1))
process_vehicle[4]:vehicle@29:1 = ('2000,car,Mercury,Cougar', ('process_inventory', 1))
process_vehicle[4]:vehicle@29:2 = ('2000,car,Mercury,Cougar', ('process_vehicle', 4))
process_vehicle[4]:year@30:1 = ('2000', ('process_vehicle', 4))
process_vehicle[4]:kind@30:1 = ('car', ('process_vehicle', 4))
process_vehicle[4]:company@30:1 = ('Mercury', ('process_vehicle', 4))
process_vehicle[4]:model@30:1 = ('Cougar', ('process_vehicle', 4))
process_car[5]:year@49:1 = ('2000', ('process_vehicle', 4))
process_car[5]:company@49:1 = ('Mercury', ('process_vehicle', 4))
process_car[5]:model@49:1 = ('Cougar', ('process_vehicle', 4))
process_car[5]:year@49:2 = ('2000', ('process_car', 5))
process_car[5]:company@49:2 = ('Mercury', ('process_car', 5))
process_car[5]:model@49:2 = ('Cougar', ('process_car', 5))
process_inventory[1]:vehicle@24:3 = ('1999,car,Chevy,Venture', ('process_inventory', 1))
process_vehicle[6]:vehicle@29:1 = ('1999,car,Chevy,Venture', ('process_inventory', 1))
process_vehicle[6]:vehicle@29:2 = ('1999,car,Chevy,Venture', ('process_vehicle', 6))
process_vehicle[6]:year@30:1 = ('1999', ('process_vehicle', 6))
process_vehicle[6]:kind@30:1 = ('car', ('process_vehicle', 6))
process_vehicle[6]:company@30:1 = ('Chevy', ('process_vehicle', 6))
process_vehicle[6]:model@30:1 = ('Venture', ('process_vehicle', 6))
process_car[7]:year@49:1 = ('1999', ('process_vehicle', 6))
process_car[7]:company@49:1 = ('Chevy', ('process_vehicle', 6))
process_car[7]:model@49:1 = ('Venture', ('process_vehicle', 6))
process_car[7]:year@49:2 = ('1999', ('process_car', 7))
process_car[7]:company@49:2 = ('Chevy', ('process_car', 7))
process_car[7]:model@49:2 = ('Venture', ('process_car', 7))

恢复推导树

apply_new_definition()中的主要区别是,我们添加了一个第二个条件来检查作用域。特别是,变量只能替换作用域中存在的字符串片段的部分。变量作用域由scope指示。然而,仅仅考虑作用域是不够的。例如,考虑下面的片段。

def my_fn(stringval):
    partA, partB = stringval.split('/')
    return partA, partB

svalue = ...
v1, v2 = my_fn(svalue) 

在这里,v1v2从先前的函数调用中获取它们的值,而不是从它们当前上下文中获取。也就是说,我们必须为内部子方法调用可能生成的大片段的情况提供异常。为了考虑这一点,我们定义了mseq(),它检索方法调用序列。在上面的情况下,内部子方法调用的mseq()将大于当前的mseq()。如果是这样,我们允许替换进行。

class ScopeTreeMiner(TreeMiner):
    def mseq(self, key):
        method, seq, var, lno = key
        return seq 

nt_var()方法需要接受元组并从中生成一个非终结符号。我们跳过方法序列,因为它与语法无关。

class ScopeTreeMiner(ScopeTreeMiner):
    def nt_var(self, key):
        method, seq, var, lno = key
        return to_nonterminal("%s@%d:%s" % (method, lno, var)) 

我们现在重新定义apply_new_definition()以考虑上下文和作用域。特别是,只有当变量在作用域内时,变量才能替换值的一部分 -- 即,它的作用域(方法序列号)与值的序列号相同,无论是作为参数的调用上下文还是当前上下文。当值的序列号大于变量的序列号时,会做出例外。在这种情况下,值可能来自内部调用。在这种情况下,我们允许替换继续进行。

class ScopeTreeMiner(ScopeTreeMiner):
    def partition(self, part, value):
        return value.partition(part)
    def partition_by_part(self, pair, value):
        (nt_var, nt_seq), (v, v_scope) = pair
        prefix_k_suffix = [
                    (nt_var, [(v, [], nt_seq)]) if i == 1 else (e, [])
                    for i, e in enumerate(self.partition(v, value))
                    if e]
        return prefix_k_suffix

    def insert_into_tree(self, my_tree, pair):
        var, values, my_scope = my_tree
        (nt_var, nt_seq), (v, v_scope) = pair
        applied = False
        for i, value_ in enumerate(values):
            key, arr, scope = value_
            self.log(2, "-> [%d] %s" % (i, repr(value_)))
            if is_nonterminal(key):
                applied = self.insert_into_tree(value_, pair)
                if applied:
                    break
            else:
                if v_scope != scope:
                    if nt_seq > scope:
                        continue
                if not v or not self.string_part_of_value(v, key):
                    continue
                prefix_k_suffix = [(k, children, scope) for k, children
                                   in self.partition_by_part(pair, key)]
                del values[i]
                for j, rep in enumerate(prefix_k_suffix):
                    values.insert(j + i, rep)

                applied = True
                self.log(2, " > %s" % (repr([i[0] for i in prefix_k_suffix])))
                break
        return applied 

apply_new_definition()现在被修改为携带额外的上下文信息mseq

class ScopeTreeMiner(ScopeTreeMiner):
    def apply_new_definition(self, tree, var, value_):
        nt_var = self.nt_var(var)
        seq = self.mseq(var)
        val, (smethod, mseq) = value_
        return self.insert_into_tree(tree, ((nt_var, seq), (val, mseq))) 

我们也修改了get_derivation_tree(),使得初始节点携带上下文。

class ScopeTreeMiner(ScopeTreeMiner):
    def get_derivation_tree(self):
        tree = (START_SYMBOL, [(self.my_input, [], 0)], 0)
        for var, value in self.my_assignments:
            self.log(0, "%s=%s" % (var, repr(value)))
            self.apply_new_definition(tree, var, value)
        return tree 

示例 1:恢复 URL 解析树

我们验证我们的 URL 解析树恢复仍然按预期工作。

url_dts = []
for inputstr in URLS_X:
    clear_cache()
    with Tracer(inputstr, files=['urllib/parse.py']) as tracer:
        urlparse(tracer.my_input)
    sm = ScopeTracker(tracer.my_input, tracer.trace)
    for k, v in sm.my_assignments.defined_vars(formatted=False):
        print(k, '=', repr(v))
    dt = ScopeTreeMiner(
        tracer.my_input,
        sm.my_assignments.defined_vars(
            formatted=False))
    display_tree(dt.tree, graph_attr=lr_graph)
    url_dts.append(dt) 
('urlparse', 1, 'url', 374) = ('http://user:pass@www.google.com:80/?q=path#ref', ('<start>', 0))
('urlparse', 1, 'url', 374) = ('http://user:pass@www.google.com:80/?q=path#ref', ('urlparse', 1))
('urlsplit', 3, 'url', 452) = ('http://user:pass@www.google.com:80/?q=path#ref', ('urlparse', 1))
('urlsplit', 3, 'url', 452) = ('http://user:pass@www.google.com:80/?q=path#ref', ('urlsplit', 3))
('urlsplit', 3, 'url', 492) = ('//user:pass@www.google.com:80/?q=path#ref', ('urlsplit', 3))
('urlsplit', 3, 'scheme', 492) = ('http', ('urlsplit', 3))
('_splitnetloc', 5, 'url', 413) = ('//user:pass@www.google.com:80/?q=path#ref', ('urlsplit', 3))
('_splitnetloc', 5, 'url', 413) = ('//user:pass@www.google.com:80/?q=path#ref', ('_splitnetloc', 5))
('urlsplit', 3, 'url', 494) = ('/?q=path#ref', ('urlsplit', 3))
('urlsplit', 3, 'netloc', 494) = ('user:pass@www.google.com:80', ('urlsplit', 3))
('urlsplit', 3, 'url', 502) = ('/?q=path', ('urlsplit', 3))
('urlsplit', 3, 'fragment', 502) = ('ref', ('urlsplit', 3))
('urlsplit', 3, 'query', 504) = ('q=path', ('urlsplit', 3))
('_checknetloc', 6, 'netloc', 421) = ('user:pass@www.google.com:80', ('urlsplit', 3))
('_checknetloc', 6, 'netloc', 421) = ('user:pass@www.google.com:80', ('_checknetloc', 6))
('urlparse', 1, 'scheme', 396) = ('http', ('urlparse', 1))
('urlparse', 1, 'netloc', 396) = ('user:pass@www.google.com:80', ('urlparse', 1))
('urlparse', 1, 'query', 396) = ('q=path', ('urlparse', 1))
('urlparse', 1, 'fragment', 396) = ('ref', ('urlparse', 1))
('urlparse', 1, 'url', 374) = ('https://www.cispa.saarland:80/', ('<start>', 0))
('urlparse', 1, 'url', 374) = ('https://www.cispa.saarland:80/', ('urlparse', 1))
('urlsplit', 3, 'url', 452) = ('https://www.cispa.saarland:80/', ('urlparse', 1))
('urlsplit', 3, 'url', 452) = ('https://www.cispa.saarland:80/', ('urlsplit', 3))
('urlsplit', 3, 'url', 492) = ('//www.cispa.saarland:80/', ('urlsplit', 3))
('urlsplit', 3, 'scheme', 492) = ('https', ('urlsplit', 3))
('_splitnetloc', 5, 'url', 413) = ('//www.cispa.saarland:80/', ('urlsplit', 3))
('_splitnetloc', 5, 'url', 413) = ('//www.cispa.saarland:80/', ('_splitnetloc', 5))
('urlsplit', 3, 'netloc', 494) = ('www.cispa.saarland:80', ('urlsplit', 3))
('_checknetloc', 6, 'netloc', 421) = ('www.cispa.saarland:80', ('urlsplit', 3))
('_checknetloc', 6, 'netloc', 421) = ('www.cispa.saarland:80', ('_checknetloc', 6))
('urlparse', 1, 'scheme', 396) = ('https', ('urlparse', 1))
('urlparse', 1, 'netloc', 396) = ('www.cispa.saarland:80', ('urlparse', 1))
('urlparse', 1, 'url', 374) = ('http://www.fuzzingbook.org/#News', ('<start>', 0))
('urlparse', 1, 'url', 374) = ('http://www.fuzzingbook.org/#News', ('urlparse', 1))
('urlsplit', 3, 'url', 452) = ('http://www.fuzzingbook.org/#News', ('urlparse', 1))
('urlsplit', 3, 'url', 452) = ('http://www.fuzzingbook.org/#News', ('urlsplit', 3))
('urlsplit', 3, 'url', 492) = ('//www.fuzzingbook.org/#News', ('urlsplit', 3))
('urlsplit', 3, 'scheme', 492) = ('http', ('urlsplit', 3))
('_splitnetloc', 5, 'url', 413) = ('//www.fuzzingbook.org/#News', ('urlsplit', 3))
('_splitnetloc', 5, 'url', 413) = ('//www.fuzzingbook.org/#News', ('_splitnetloc', 5))
('urlsplit', 3, 'url', 494) = ('/#News', ('urlsplit', 3))
('urlsplit', 3, 'netloc', 494) = ('www.fuzzingbook.org', ('urlsplit', 3))
('urlsplit', 3, 'fragment', 502) = ('News', ('urlsplit', 3))
('_checknetloc', 6, 'netloc', 421) = ('www.fuzzingbook.org', ('urlsplit', 3))
('_checknetloc', 6, 'netloc', 421) = ('www.fuzzingbook.org', ('_checknetloc', 6))
('urlparse', 1, 'scheme', 396) = ('http', ('urlparse', 1))
('urlparse', 1, 'netloc', 396) = ('www.fuzzingbook.org', ('urlparse', 1))
('urlparse', 1, 'fragment', 396) = ('News', ('urlparse', 1))
('urlparse', 1, 'url', 374) = ('ftp://freebsd.org/releases/5.8', ('<start>', 0))
('urlparse', 1, 'url', 374) = ('ftp://freebsd.org/releases/5.8', ('urlparse', 1))
('urlsplit', 3, 'url', 452) = ('ftp://freebsd.org/releases/5.8', ('urlparse', 1))
('urlsplit', 3, 'url', 452) = ('ftp://freebsd.org/releases/5.8', ('urlsplit', 3))
('urlsplit', 3, 'url', 492) = ('//freebsd.org/releases/5.8', ('urlsplit', 3))
('urlsplit', 3, 'scheme', 492) = ('ftp', ('urlsplit', 3))
('_splitnetloc', 5, 'url', 413) = ('//freebsd.org/releases/5.8', ('urlsplit', 3))
('_splitnetloc', 5, 'url', 413) = ('//freebsd.org/releases/5.8', ('_splitnetloc', 5))
('urlsplit', 3, 'url', 494) = ('/releases/5.8', ('urlsplit', 3))
('urlsplit', 3, 'netloc', 494) = ('freebsd.org', ('urlsplit', 3))
('_checknetloc', 6, 'netloc', 421) = ('freebsd.org', ('urlsplit', 3))
('_checknetloc', 6, 'netloc', 421) = ('freebsd.org', ('_checknetloc', 6))
('urlparse', 1, 'url', 396) = ('/releases/5.8', ('urlparse', 1))
('urlparse', 1, 'scheme', 396) = ('ftp', ('urlparse', 1))
('urlparse', 1, 'netloc', 396) = ('freebsd.org', ('urlparse', 1))

示例 2:恢复库存解析树

接下来,我们看看如何从上次失败的process_inventory()中恢复解析树。

with Tracer(INVENTORY) as tracer:
    process_inventory(tracer.my_input)

sm = ScopeTracker(tracer.my_input, tracer.trace)
for k, v in sm.my_assignments.defined_vars():
    print(k, '=', repr(v))
inventory_dt = ScopeTreeMiner(
    tracer.my_input,
    sm.my_assignments.defined_vars(
        formatted=False))
display_tree(inventory_dt.tree, graph_attr=lr_graph) 
process_inventory[1]:inventory@22 = ('1997,van,Ford,E350\n2000,car,Mercury,Cougar\n1999,car,Chevy,Venture', ('<start>', 0))
process_inventory[1]:inventory@22 = ('1997,van,Ford,E350\n2000,car,Mercury,Cougar\n1999,car,Chevy,Venture', ('process_inventory', 1))
process_inventory[1]:vehicle@24 = ('1997,van,Ford,E350', ('process_inventory', 1))
process_vehicle[2]:vehicle@29 = ('1997,van,Ford,E350', ('process_inventory', 1))
process_vehicle[2]:vehicle@29 = ('1997,van,Ford,E350', ('process_vehicle', 2))
process_vehicle[2]:year@30 = ('1997', ('process_vehicle', 2))
process_vehicle[2]:kind@30 = ('van', ('process_vehicle', 2))
process_vehicle[2]:company@30 = ('Ford', ('process_vehicle', 2))
process_vehicle[2]:model@30 = ('E350', ('process_vehicle', 2))
process_van[3]:year@40 = ('1997', ('process_vehicle', 2))
process_van[3]:company@40 = ('Ford', ('process_vehicle', 2))
process_van[3]:model@40 = ('E350', ('process_vehicle', 2))
process_van[3]:year@40 = ('1997', ('process_van', 3))
process_van[3]:company@40 = ('Ford', ('process_van', 3))
process_van[3]:model@40 = ('E350', ('process_van', 3))
process_inventory[1]:vehicle@24 = ('2000,car,Mercury,Cougar', ('process_inventory', 1))
process_vehicle[4]:vehicle@29 = ('2000,car,Mercury,Cougar', ('process_inventory', 1))
process_vehicle[4]:vehicle@29 = ('2000,car,Mercury,Cougar', ('process_vehicle', 4))
process_vehicle[4]:year@30 = ('2000', ('process_vehicle', 4))
process_vehicle[4]:kind@30 = ('car', ('process_vehicle', 4))
process_vehicle[4]:company@30 = ('Mercury', ('process_vehicle', 4))
process_vehicle[4]:model@30 = ('Cougar', ('process_vehicle', 4))
process_car[5]:year@49 = ('2000', ('process_vehicle', 4))
process_car[5]:company@49 = ('Mercury', ('process_vehicle', 4))
process_car[5]:model@49 = ('Cougar', ('process_vehicle', 4))
process_car[5]:year@49 = ('2000', ('process_car', 5))
process_car[5]:company@49 = ('Mercury', ('process_car', 5))
process_car[5]:model@49 = ('Cougar', ('process_car', 5))
process_inventory[1]:vehicle@24 = ('1999,car,Chevy,Venture', ('process_inventory', 1))
process_vehicle[6]:vehicle@29 = ('1999,car,Chevy,Venture', ('process_inventory', 1))
process_vehicle[6]:vehicle@29 = ('1999,car,Chevy,Venture', ('process_vehicle', 6))
process_vehicle[6]:year@30 = ('1999', ('process_vehicle', 6))
process_vehicle[6]:kind@30 = ('car', ('process_vehicle', 6))
process_vehicle[6]:company@30 = ('Chevy', ('process_vehicle', 6))
process_vehicle[6]:model@30 = ('Venture', ('process_vehicle', 6))
process_car[7]:year@49 = ('1999', ('process_vehicle', 6))
process_car[7]:company@49 = ('Chevy', ('process_vehicle', 6))
process_car[7]:model@49 = ('Venture', ('process_vehicle', 6))
process_car[7]:year@49 = ('1999', ('process_car', 7))
process_car[7]:company@49 = ('Chevy', ('process_car', 7))
process_car[7]:model@49 = ('Venture', ('process_car', 7))

0 1 <process_inventory@22:inventory> 0->1 2 <process_inventory@22:inventory> 1->2 3 <process_inventory@24:vehicle> 2->3 23 \n (10) 2->23 24 <process_inventory@24:vehicle> 2->24 44 \n (10) 2->44 45 <process_inventory@24:vehicle> 2->45 4 <process_vehicle@29:vehicle> 3->4 5 <process_vehicle@29:vehicle> 4->5 6 <process_vehicle@30:year> 5->6 10 , (44) 5->10 11 <process_vehicle@30:kind> 5->11 13 , (44) 5->13 14 <process_vehicle@30:company> 5->14 18 , (44) 5->18 19 <process_vehicle@30:model> 5->19 7 <process_van@40:year> 6->7 8 <process_van@40:year> 7->8 9 1997 8->9 12 van 11->12 15 <process_van@40:company> 14->15 16 <process_van@40:company> 15->16 17 Ford 16->17 20 <process_van@40:model> 19->20 21 <process_van@40:model> 20->21 22 E350 21->22 25 <process_vehicle@29:vehicle> 24->25 26 <process_vehicle@29:vehicle> 25->26 27 <process_vehicle@30:year> 26->27 31 , (44) 26->31 32 <process_vehicle@30:kind> 26->32 34 , (44) 26->34 35 <process_vehicle@30:company> 26->35 39 , (44) 26->39 40 <process_vehicle@30:model> 26->40 28 <process_car@49:year> 27->28 29 <process_car@49:year> 28->29 30 2000 29->30 33 car 32->33 36 <process_car@49:company> 35->36 37 <process_car@49:company> 36->37 38 Mercury 37->38 41 <process_car@49:model> 40->41 42 <process_car@49:model> 41->42 43 Cougar 42->43 46 <process_vehicle@29:vehicle> 45->46 47 <process_vehicle@29:vehicle> 46->47 48 <process_vehicle@30:year> 47->48 52 , (44) 47->52 53 <process_vehicle@30:kind> 47->53 55 , (44) 47->55 56 <process_vehicle@30:company> 47->56 60 , (44) 47->60 61 <process_vehicle@30:model> 47->61 49 <process_car@49:year> 48->49 50 <process_car@49:year> 49->50 51 1999 50->51 54 car 53->54 57 <process_car@49:company> 56->57 58 <process_car@49:company> 57->58 59 Chevy 58->59 62 <process_car@49:model> 61->62 63 <process_car@49:model> 62->63 64 Venture 63->64

恢复的解析树看起来是合理的。

从我们的示例(2)中,人们可能会注意到三个子树 -- vehicle[2:1]vehicle[4:1]vehicle[6:1]非常相似。我们将探讨如何利用这一点直接生成语法。

语法挖掘

tree_to_grammar()现在重新定义为如下,以考虑节点中的额外作用域。

class ScopedGrammarMiner(GrammarMiner):
    def tree_to_grammar(self, tree):
        key, children, scope = tree
        one_alt = [ckey for ckey, gchildren, cscope in children if ckey != key]
        hsh = {key: [one_alt] if one_alt else []}
        for child in children:
            (ckey, _gc, _cscope) = child
            if not is_nonterminal(ckey):
                continue
            chsh = self.tree_to_grammar(child)
            for k in chsh:
                if k not in hsh:
                    hsh[k] = chsh[k]
                else:
                    hsh[k].extend(chsh[k])
        return hsh 

语法是规范形式,需要调整以显示。首先,恢复的库存语法。

si = ScopedGrammarMiner()
si.add_tree(inventory_dt)
syntax_diagram(readable(si.grammar)) 
start

process_inventory@22:inventory

process_inventory@22:inventory

process_inventory@24:vehicleprocess_inventory@24:vehicleprocess_inventory@24:vehicle

process_inventory@24:vehicle

process_vehicle@29:vehicle

process_vehicle@29:vehicle

process_vehicle@30:year , process_vehicle@30:kind , process_vehicle@30:company , process_vehicle@30:model

process_vehicle@30:year

process_car@49:year process_van@40:year

process_van@40:year

1997

process_vehicle@30:kind

car van

process_vehicle@30:company

process_van@40:company process_car@49:company

process_van@40:company

Ford

process_vehicle@30:model

process_van@40:model process_car@49:model

process_van@40:model

E350

process_car@49:year

1999 2000

process_car@49:company

Mercury Chevy

process_car@49:model

Cougar Venture

恢复的 URL 语法。

su = ScopedGrammarMiner()
for t in url_dts:
    su.add_tree(t)
syntax_diagram(readable(su.grammar)) 
start

urlparse@374:url

urlparse@374:url

urlsplit@452:url

urlsplit@452:url

urlsplit@492:scheme : urlsplit@492:url

urlsplit@492:scheme

urlparse@396:scheme

urlparse@396:scheme

http ftp https

urlsplit@492:url

_splitnetloc@413:url

_splitnetloc@413:url

// urlsplit@494:netloc urlsplit@494:url // urlsplit@494:netloc /

urlsplit@494:netloc

_checknetloc@421:netloc

_checknetloc@421:netloc

urlparse@396:netloc

urlparse@396:netloc

www.fuzzingbook.org www.cispa.saarland:80 freebsd.org user:pass@www.google.com:80

urlsplit@494:url

urlparse@396:url urlsplit@502:url # urlsplit@502:fragment /# urlsplit@502:fragment

urlsplit@502:url

/? urlsplit@504:query

urlsplit@504:query

urlparse@396:query

urlparse@396:query

q=path

urlsplit@502:fragment

urlparse@396:fragment

urlparse@396:fragment

News ref

urlparse@396:url

/releases/5.8

人们可能会注意到语法并非完全可读,有许多单标记定义。

因此,最后一部分拼图是清理方法clean_grammar(),它清理了这样的定义。想法是寻找单标记定义,其中键正好由另一个键(单选方案、单标记、非终结符)定义。

class ScopedGrammarMiner(ScopedGrammarMiner):
    def get_replacements(self, grammar):
        replacements = {}
        for k in grammar:
            if k == START_SYMBOL:
                continue
            alts = grammar[k]
            if len(set([str(i) for i in alts])) != 1:
                continue
            rule = alts[0]
            if len(rule) != 1:
                continue
            tok = rule[0]
            if not is_nonterminal(tok):
                continue
            replacements[k] = tok
        return replacements 

一旦我们有了这样一个列表,就迭代地用我们之前找到的标记替换原始键的任何位置。重复此操作,直到没有剩余的键。

class ScopedGrammarMiner(ScopedGrammarMiner):
    def clean_grammar(self):
        replacements = self.get_replacements(self.grammar)

        while True:
            changed = set()
            for k in self.grammar:
                if k in replacements:
                    continue
                new_alts = []
                for alt in self.grammar[k]:
                    new_alt = []
                    for t in alt:
                        if t in replacements:
                            new_alt.append(replacements[t])
                            changed.add(t)
                        else:
                            new_alt.append(t)
                    new_alts.append(new_alt)
                self.grammar[k] = new_alts
            if not changed:
                break
            for k in changed:
                self.grammar.pop(k, None)
        return readable(self.grammar) 

clean_grammar()的使用如下:

si = ScopedGrammarMiner()
si.add_tree(inventory_dt)
syntax_diagram(readable(si.clean_grammar())) 
start

process_inventory@22:inventory

process_inventory@22:inventory

process_vehicle@29:vehicleprocess_vehicle@29:vehicleprocess_vehicle@29:vehicle

process_vehicle@29:vehicle

process_vehicle@30:year , process_vehicle@30:kind , process_vehicle@30:company , process_vehicle@30:model

process_vehicle@30:year

process_car@49:year process_van@40:year

process_van@40:year

1997

process_vehicle@30:kind

car van

process_vehicle@30:company

process_van@40:company process_car@49:company

process_van@40:company

Ford

process_vehicle@30:model

process_van@40:model process_car@49:model

process_van@40:model

E350

process_car@49:year

1999 2000

process_car@49:company

Mercury Chevy

process_car@49:model

Cougar Venture

我们更新了update_grammar()以使用正确的跟踪器和挖掘器。

class ScopedGrammarMiner(ScopedGrammarMiner):
    def update_grammar(self, inputstr, trace):
        at = self.create_tracker(inputstr, trace)
        dt = self.create_tree_miner(
            inputstr, at.my_assignments.defined_vars(
                formatted=False))
        self.add_tree(dt)
        return self.grammar

    def create_tracker(self, *args):
        return ScopeTracker(*args)

    def create_tree_miner(self, *args):
        return ScopeTreeMiner(*args) 

recover_grammar()使用正确的挖掘器,并返回一个清理过的语法。

def recover_grammar(fn, inputs, **kwargs):
    miner = ScopedGrammarMiner()
    for inputstr in inputs:
        with Tracer(inputstr, **kwargs) as tracer:
            fn(tracer.my_input)
        miner.update_grammar(tracer.my_input, tracer.trace)
    return readable(miner.clean_grammar()) 
url_grammar = recover_grammar(url_parse, URLS_X, files=['urllib/parse.py']) 
syntax_diagram(url_grammar) 
start

urlsplit@452:url

urlsplit@452:url

urlparse@396:scheme : _splitnetloc@413:url

urlparse@396:scheme

http ftp https

_splitnetloc@413:url

// urlparse@396:netloc / // urlparse@396:netloc urlsplit@494:url

urlparse@396:netloc

www.fuzzingbook.org www.cispa.saarland:80 freebsd.org user:pass@www.google.com:80

urlsplit@494:url

urlparse@396:url urlsplit@502:url # urlparse@396:fragment /# urlparse@396:fragment

urlsplit@502:url

/? urlparse@396:query

urlparse@396:query

q=path

urlparse@396:fragment

News ref

urlparse@396:url

/releases/5.8

f = GrammarFuzzer(url_grammar)
for _ in range(10):
    print(f.fuzz()) 
ftp://user:pass@www.google.com:80/
ftp://freebsd.org/?q=path#News
ftp://freebsd.org/
ftp://www.fuzzingbook.org/?q=path#News
https://www.fuzzingbook.org/
ftp://www.fuzzingbook.org/#News
ftp://www.fuzzingbook.org/releases/5.8
http://www.fuzzingbook.org/?q=path#ref
http://freebsd.org/
ftp://user:pass@www.google.com:80/

inventory_grammar = recover_grammar(process_inventory, [INVENTORY]) 
syntax_diagram(inventory_grammar) 
start

process_inventory@22:inventory

process_inventory@22:inventory

process_vehicle@29:vehicleprocess_vehicle@29:vehicleprocess_vehicle@29:vehicle

process_vehicle@29:vehicle

process_vehicle@30:year , process_vehicle@30:kind , process_vehicle@30:company , process_vehicle@30:model

process_vehicle@30:year

process_car@49:year process_van@40:year

process_van@40:year

1997

process_vehicle@30:kind

car van

process_vehicle@30:company

process_van@40:company process_car@49:company

process_van@40:company

Ford

process_vehicle@30:model

process_van@40:model process_car@49:model

process_van@40:model

E350

process_car@49:year

1999 2000

process_car@49:company

Mercury Chevy

process_car@49:model

Cougar Venture

f = GrammarFuzzer(inventory_grammar)
for _ in range(10):
    print(f.fuzz()) 
1997,van,Mercury,E350
1999,car,Ford,Venture
2000,car,Ford,Cougar
2000,van,Chevy,Venture
1999,car,Mercury,E350
1997,van,Ford,Venture
1997,car,Chevy,Cougar
1999,car,Ford,E350
1999,car,Chevy,Cougar
1997,car,Chevy,Venture
1997,car,Ford,E350
2000,car,Mercury,E350
1999,van,Chevy,E350
1997,van,Ford,Cougar
1999,van,Chevy,Venture
1999,car,Ford,E350
1999,van,Mercury,Venture
1997,car,Ford,Cougar
1999,car,Mercury,Venture
1997,van,Mercury,E350
1999,car,Chevy,Cougar
2000,van,Chevy,Venture
2000,car,Ford,Venture
1997,car,Mercury,E350
1997,van,Chevy,E350
1997,van,Ford,E350
2000,car,Ford,E350
1997,car,Chevy,Venture
1997,van,Ford,E350
2000,van,Chevy,Cougar

我们看到跟踪作用域如何帮助我们提取更精确的语法。

注意,我们使用 字符串 包含测试作为确定特定字符串片段是否来自原始输入字符串的一种方式。虽然与动态污染相比,这可能看起来相当容易出错,但我们注意到,许多跟踪工具,如 dtrace()ptrace(),允许在不同的平台上直接从二进制执行中获取我们所需的信息。然而,获取动态污染的方法几乎总是涉及在它们可以使用之前对二进制文件进行仪器化。因此,这种字符串包含的方法可以比动态污染方法更普遍地应用。此外,动态污染通常由于隐式传输或在 PythonC 代码之间的边界而丢失。字符串包含没有这样的问题。因此,我们的方法通常可以获得比依赖于动态污染更好的结果。

经验教训

  • 给定一组程序的样本输入,如果程序依赖于手写解析器,我们可以通过检查执行过程中的变量值来学习输入语法。

  • 简单的字符串包含检查足以从现实世界程序中获得相当准确的语法。

  • 结果语法可以直接用于模糊测试,并且可以对您拥有的任何样本产生乘数效应。

下一步

  • 学习如何使用 信息流 来进一步提高映射输入到状态。

背景

从一组样本中恢复语言(即不考虑可能处理它们的程序)是一个经过充分研究的话题。Higuera 的优秀参考文献 [De la Higuera 等人,2010] 涵盖了所有经典方法。黑盒语法挖掘的最新状态由 Clark 等人 [Clark 等人,2013] 描述。

从一个程序中学习输入语言,无论是否有样本,尽管它具有模糊测试的潜力,但仍然是一个新兴话题。在这个领域,Lin 等人进行了开创性的工作 [Lin 等人,2008],他们发明了一种从自顶向下和自底向上解析器中检索解析树的方法。本章中描述的方法直接基于 Hoschele 等人的 AUTOGRAM 工作 [Höschele 等人,2017]。

练习

练习 1:简化复杂对象

我们的语法挖掘器只检查字符串片段。然而,程序可能经常传递包含输入片段的容器或自定义对象。例如,考虑我们对库存处理器的合理修改,我们使用自定义对象 Vehicle 来携带片段。

class Vehicle:
    def __init__(self, vehicle: str):
        year, kind, company, model, *_ = vehicle.split(',')
        self.year, self.kind, self.company, self.model = year, kind, company, model 
def process_inventory_with_obj(inventory: str) -> str:
    res = []
    for vehicle in inventory.split('\n'):
        ret = process_vehicle(vehicle)
        res.extend(ret)

    return '\n'.join(res) 
def process_vehicle_with_obj(vehicle: str) -> List[str]:
    v = Vehicle(vehicle)
    if v.kind == 'van':
        return process_van_with_obj(v)

    elif v.kind == 'car':
        return process_car_with_obj(v)

    else:
        raise Exception('Invalid entry') 
def process_van_with_obj(vehicle: Vehicle) -> List[str]:
    res = [
        "We have a %s  %s van from %s vintage." % (vehicle.company,
                                                  vehicle.model, vehicle.year)
    ]
    iyear = int(vehicle.year)
    if iyear > 2010:
        res.append("It is a recent model!")
    else:
        res.append("It is an old but reliable model!")
    return res 
def process_car_with_obj(vehicle: Vehicle) -> List[str]:
    res = [
        "We have a %s  %s car from %s vintage." % (vehicle.company,
                                                  vehicle.model, vehicle.year)
    ]
    iyear = int(vehicle.year)
    if iyear > 2016:
        res.append("It is a recent model!")
    else:
        res.append("It is an old but reliable model!")
    return res 

我们像以前一样恢复语法。

vehicle_grammar = recover_grammar(
    process_inventory_with_obj,
    [INVENTORY],
    methods=INVENTORY_METHODS) 

新的车辆语法在细节上缺失,特别是关于货车和汽车的不同的模型和公司。

syntax_diagram(vehicle_grammar) 
start

process_vehicle@29:vehicleprocess_vehicle@29:vehicleprocess_vehicle@29:vehicle

process_vehicle@29:vehicle

process_vehicle@30:year , process_vehicle@30:kind , process_vehicle@30:company , process_vehicle@30:model

process_vehicle@30:year

process_car@49:year process_van@40:year

process_van@40:year

1997

process_vehicle@30:kind

car van

process_vehicle@30:company

process_van@40:company process_car@49:company

process_van@40:company

Ford

process_vehicle@30:model

process_van@40:model process_car@49:model

process_van@40:model

E350

process_car@49:year

1999 2000

process_car@49:company

Mercury Chevy

process_car@49:model

Cougar Venture

问题在于,我们在追踪过程中特别寻找包含输入字符串片段的字符串对象。你能修改我们的语法挖掘器,以便正确地考虑复杂的对象吗?

使用笔记本来处理练习并查看解决方案。

练习 2:从 InformationFlow 中引入污点

我们一直使用字符串包含来检查特定的片段是否来自输入字符串。这并不令人满意,因为它要求我们在跟踪字符串的大小上做出妥协,这限制在大于FRAGMENT_LEN的字符串。此外,可能存在一种方法可以处理一个字符串,其中片段重复,但属于不同的标记。例如,CSV 文件中的嵌套逗号会导致我们的解析器失败。避免这种情况的一种方法是通过动态污点,并检查污点包含而不是字符串包含。

关于信息流的章节详细说明了如何引入动态污点。你能根据作用域更新我们的语法挖掘器,使用动态污点吗?

使用笔记本来处理练习并查看解决方案。

Creative Commons License本项目的内容受Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License许可。作为内容一部分的源代码,以及用于格式化和显示该内容的源代码受MIT License许可。 最后更改:2023-11-11 18:18:06+01:00 • 引用 • 印记

如何引用这篇作品

Andreas Zeller, Rahul Gopinath, Marcel Böhme, Gordon Fraser, and Christian Holler: "Mining Input Grammars". In Andreas Zeller, Rahul Gopinath, Marcel Böhme, Gordon Fraser, and Christian Holler, "The Fuzzing Book", www.fuzzingbook.org/html/GrammarMiner.html. Retrieved 2023-11-11 18:18:06+01:00.

@incollection{fuzzingbook2023:GrammarMiner,
    author = {Andreas Zeller and Rahul Gopinath and Marcel B{\"o}hme and Gordon Fraser and Christian Holler},
    booktitle = {The Fuzzing Book},
    title = {Mining Input Grammars},
    year = {2023},
    publisher = {CISPA Helmholtz Center for Information Security},
    howpublished = {\url{https://www.fuzzingbook.org/html/GrammarMiner.html}},
    note = {Retrieved 2023-11-11 18:18:06+01:00},
    url = {https://www.fuzzingbook.org/html/GrammarMiner.html},
    urldate = {2023-11-11 18:18:06+01:00}
}

posted @ 2025-12-13 18:14  绝不原创的飞龙  阅读(1)  评论(0)    收藏  举报