模糊测试之书-二-
模糊测试之书(二)
原文:
exploringjs.com/ts/book/index.html译者:飞龙
代码覆盖率
在上一章中,我们介绍了基本模糊测试——即生成随机输入来测试程序。我们如何衡量这些测试的有效性呢?一种方法就是检查找到的(数量和严重性)错误;但如果错误很少,我们需要一个测试发现错误的概率的代理。在本章中,我们引入了代码覆盖率的概念,测量在测试运行期间程序的实际执行部分。测量这种覆盖率对于试图覆盖尽可能多代码的测试生成器来说也非常关键。
from [bookutils](https://github.com/uds-se/fuzzingbook//tree/master/notebooks/shared/bookutils) import YouTubeVideo
YouTubeVideo('8HxW8j9287A')
先决条件
-
你需要了解程序是如何执行的。
-
你应该在上一章中学习过基本模糊测试。
概述
要使用本章提供的代码,请编写
>>> from fuzzingbook.Coverage import <identifier>
然后利用以下功能。
本章介绍了一个Coverage类,允许你测量 Python 程序的覆盖率。在本书的上下文中,我们使用覆盖率信息来引导模糊测试向未覆盖的位置发展。
Coverage类的典型用法是与with子句结合使用:
>>> with Coverage() as cov:
>>> cgi_decode("a+b")
打印出覆盖率对象显示了覆盖的函数,未覆盖的行以#为前缀:
>>> print(cov)
# 1 def cgi_decode(s: str) -> str:
# 2 """Decode the CGI-encoded string `s`:
# 3 * replace '+' by ' '
# 4 * replace "%xx" by the character with hex number xx.
# 5 Return the decoded string. Raise `ValueError` for invalid inputs."""
# 6
# 7 # Mapping of hex digits to their integer values
8 hex_values = {
9 '0': 0, '1': 1, '2': 2, '3': 3, '4': 4,
10 '5': 5, '6': 6, '7': 7, '8': 8, '9': 9,
11 'a': 10, 'b': 11, 'c': 12, 'd': 13, 'e': 14, 'f': 15,
12 'A': 10, 'B': 11, 'C': 12, 'D': 13, 'E': 14, 'F': 15,
# 13 }
# 14
15 t = ""
16 i = 0
17 while i < len(s):
18 c = s[i]
19 if c == '+':
20 t += ' '
21 elif c == '%':
# 22 digit_high, digit_low = s[i + 1], s[i + 2]
# 23 i += 2
# 24 if digit_high in hex_values and digit_low in hex_values:
# 25 v = hex_values[digit_high] * 16 + hex_values[digit_low]
# 26 t += chr(v)
# 27 else:
# 28 raise ValueError("Invalid encoding")
# 29 else:
30 t += c
31 i += 1
32 return t
trace()方法返回跟踪——即按顺序执行的代码位置列表。每个位置都作为一对(函数名,行)出现。
>>> cov.trace()
[('cgi_decode', 8),
('cgi_decode', 9),
('cgi_decode', 8),
('cgi_decode', 9),
('cgi_decode', 8),
('cgi_decode', 9),
('cgi_decode', 8),
('cgi_decode', 9),
('cgi_decode', 8),
('cgi_decode', 9),
('cgi_decode', 8),
('cgi_decode', 10),
('cgi_decode', 8),
('cgi_decode', 10),
('cgi_decode', 8),
('cgi_decode', 10),
('cgi_decode', 8),
('cgi_decode', 10),
('cgi_decode', 8),
('cgi_decode', 10),
('cgi_decode', 8),
('cgi_decode', 11),
('cgi_decode', 8),
('cgi_decode', 11),
('cgi_decode', 8),
('cgi_decode', 11),
('cgi_decode', 8),
('cgi_decode', 11),
('cgi_decode', 8),
('cgi_decode', 11),
('cgi_decode', 8),
('cgi_decode', 11),
('cgi_decode', 8),
('cgi_decode', 12),
('cgi_decode', 8),
('cgi_decode', 12),
('cgi_decode', 8),
('cgi_decode', 15),
('cgi_decode', 16),
('cgi_decode', 17),
('cgi_decode', 18),
('cgi_decode', 19),
('cgi_decode', 21),
('cgi_decode', 30),
('cgi_decode', 31),
('cgi_decode', 17),
('cgi_decode', 18),
('cgi_decode', 19),
('cgi_decode', 20),
('cgi_decode', 31),
('cgi_decode', 17),
('cgi_decode', 18),
('cgi_decode', 19),
('cgi_decode', 21),
('cgi_decode', 30),
('cgi_decode', 31),
('cgi_decode', 17),
('cgi_decode', 32)]
coverage()方法返回覆盖率,即至少执行一次的跟踪中的位置集合:
>>> cov.coverage()
{('cgi_decode', 8),
('cgi_decode', 9),
('cgi_decode', 10),
('cgi_decode', 11),
('cgi_decode', 12),
('cgi_decode', 15),
('cgi_decode', 16),
('cgi_decode', 17),
('cgi_decode', 18),
('cgi_decode', 19),
('cgi_decode', 20),
('cgi_decode', 21),
('cgi_decode', 30),
('cgi_decode', 31),
('cgi_decode', 32)}
覆盖集可以受到集合操作的影响,例如交集(哪些位置在多次执行中被覆盖)和差集(哪些位置在运行a中被覆盖,但在b中没有)。
本章还讨论了如何从 C 程序中获取此类覆盖率。
在with块内跟踪覆盖率。使用如下
with Coverage() as cov:
function_to_be_traced()
c = cov.coverage()
```"><text text-anchor="start" x="27.12" y="-120.2" font-family="Patua One, Helvetica, sans-serif" font-weight="bold" font-size="14.00" fill="#b03a2e">覆盖率</text> <g id="a_node1_0"><a xlink:href="#" xlink:title="Coverage"><g id="a_node1_1"><a xlink:href="#" xlink:title="__enter__(self) -> Any:
`with`块的开始。开启跟踪。"><text text-anchor="start" x="8" y="-98" font-family="'Fira Mono', 'Source Code Pro', 'Courier', monospace" font-weight="bold" font-size="10.00">__enter__()</text></a></g> <g id="a_node1_2"><a xlink:href="#" xlink:title="__exit__(self, exc_type: Type, exc_value: BaseException, tb: traceback) -> Optional[bool]:
`with`块结束。关闭跟踪。"><text text-anchor="start" x="8" y="-85.25" font-family="'Fira Mono', 'Source Code Pro', 'Courier', monospace" font-weight="bold" font-size="10.00">__exit__()</text></a></g> <g id="a_node1_3"><a xlink:href="#" xlink:title="__init__(self) -> None:
构造函数"><text text-anchor="start" x="8" y="-72.5" font-family="'Fira Mono', 'Source Code Pro', 'Courier', monospace" font-weight="bold" font-size="10.00">__init__()</text></a></g> <g id="a_node1_4"><a xlink:href="#" xlink:title="__exit__(self) -> None:
返回此对象的字符串表示。
显示覆盖(和未覆盖)的程序代码"><text text-anchor="start" x="8" y="-59.75" font-family="'Fira Mono', 'Source Code Pro', 'Courier', monospace" font-weight="bold" font-size="10.00">__repr__()</text></a></g> <g id="a_node1_5"><a xlink:href="#" xlink:title="coverage(self) -> Set[Location]:
执行的行列表,作为(函数名称,行号)对"><text text-anchor="start" x="8" y="-47" font-family="'Fira Mono', 'Source Code Pro', 'Courier', monospace" font-weight="bold" font-size="10.00">coverage()</text></a></g> <g id="a_node1_6"><a xlink:href="#" xlink:title="function_names(self) -> Set[str]:
在函数名称集合中看到的`function_names()`"><text text-anchor="start" x="8" y="-34.25" font-family="'Fira Mono', 'Source Code Pro', 'Courier', monospace" font-weight="bold" font-size="10.00">function_names()</text></a></g> <g id="a_node1_7"><a xlink:href="#" xlink:title="trace(self) -> List[Location]:
执行的行列表,作为(函数名称,行号)对"><text text-anchor="start" x="8" y="-21.5" font-family="'Fira Mono', 'Source Code Pro', 'Courier', monospace" font-weight="bold" font-size="10.00">trace()</text></a></g> <g id="a_node1_8"><a xlink:href="#" xlink:title="traceit(self, frame: frame, event: str, arg: Any) -> Optional[Callable]:
跟踪函数。在子类中重载。"><text text-anchor="start" x="8" y="-8.75" font-family="'Fira Mono', 'Source Code Pro', 'Courier', monospace" font-style="italic" font-size="10.00">traceit()</text></a></g></a></g></a></g></g> <g id="node2" class="node"><title>图例</title> <text text-anchor="start" x="130.38" y="-84.5" font-family="Patua One, Helvetica, sans-serif" font-weight="bold" font-size="10.00" fill="#b03a2e">图例</text> <text text-anchor="start" x="130.38" y="-74.5" font-family="Patua One, Helvetica, sans-serif" font-size="10.00">• </text> <text text-anchor="start" x="136.38" y="-74.5" font-family="'Fira Mono', 'Source Code Pro', 'Courier', monospace" font-weight="bold" font-size="8.00">public_method()</text> <text text-anchor="start" x="130.38" y="-64.5" font-family="Patua One, Helvetica, sans-serif" font-size="10.00">• </text> <text text-anchor="start" x="136.38" y="-64.5" font-family="'Fira Mono', 'Source Code Pro', 'Courier', monospace" font-size="8.00">private_method()</text> <text text-anchor="start" x="130.38" y="-54.5" font-family="Patua One, Helvetica, sans-serif" font-size="10.00">• </text> <text text-anchor="start" x="136.38" y="-54.5" font-family="'Fira Mono', 'Source Code Pro', 'Courier', monospace" font-style="italic" font-size="8.00">overloaded_method()</text> <text text-anchor="start" x="130.38" y="-45.45" font-family="Helvetica,sans-Serif" font-size="9.00">将鼠标悬停在名称上以查看文档</text></g></g></svg>
```py
import [bookutils.setup](https://github.com/uds-se/fuzzingbook//tree/master/notebooks/shared/bookutils)
CGI 解码器
我们首先介绍一个简单的 Python 函数,该函数解码 CGI 编码的字符串。CGI 编码用于 URL(即 Web 地址)中,以编码在 URL 中无效的字符,例如空白和某些标点符号:
-
空白将被替换为
'+' -
其他无效字符将被替换为 '
%xx',其中xx是两位十六进制等效值。
在 CGI 编码中,字符串 "Hello, world!" 因此会变成 "Hello%2c+world%21",其中 2c 和 21 分别是 ',' 和 '!' 的十六进制等效值。
函数 cgi_decode() 接收这样的编码字符串并将其解码回其原始形式。我们的实现复制了来自 [Pezzè et al, 2008] 的代码。(它甚至包括其错误——但我们现在不会透露它们。)
def cgi_decode(s: str) -> str:
"""Decode the CGI-encoded string `s`:
* replace '+' by ' '
* replace "%xx" by the character with hex number xx.
Return the decoded string. Raise `ValueError` for invalid inputs."""
# Mapping of hex digits to their integer values
hex_values = {
'0': 0, '1': 1, '2': 2, '3': 3, '4': 4,
'5': 5, '6': 6, '7': 7, '8': 8, '9': 9,
'a': 10, 'b': 11, 'c': 12, 'd': 13, 'e': 14, 'f': 15,
'A': 10, 'B': 11, 'C': 12, 'D': 13, 'E': 14, 'F': 15,
}
t = ""
i = 0
while i < len(s):
c = s[i]
if c == '+':
t += ' '
elif c == '%':
digit_high, digit_low = s[i + 1], s[i + 2]
i += 2
if digit_high in hex_values and digit_low in hex_values:
v = hex_values[digit_high] * 16 + hex_values[digit_low]
t += chr(v)
else:
raise ValueError("Invalid encoding")
else:
t += c
i += 1
return t
下面是 cgi_decode() 的工作示例:
cgi_decode("Hello+world")
'Hello world'
如果我们要系统地测试 cgi_decode(),我们将如何进行?
测试文献区分了两种推导测试的方法:黑盒测试 和 白盒测试。
黑盒测试
黑盒测试的思想是从 规范 中推导测试。在上面的情况下,因此我们必须通过指定的和记录的特性来测试 cgi_decode(),包括
-
测试
'+'的正确替换; -
测试正确替换
"%xx"; -
测试其他字符不被替换;
-
测试对非法输入的识别。
这里包含了涵盖这四个特征的四个断言(测试)。我们可以看到它们都通过了:
assert cgi_decode('+') == ' '
assert cgi_decode('%20') == ' '
assert cgi_decode('abc') == 'abc'
try:
cgi_decode('%?a')
assert False
except ValueError:
pass
黑盒测试的优势在于它能够找到指定行为中的错误。它与特定的实现无关,因此可以在实现之前创建测试。缺点是通常实现的行为比指定行为覆盖的范围更广,因此仅基于规格说明的测试通常不会覆盖所有实现细节。
白盒测试
与黑盒测试相反,白盒测试 从 实现 中推导测试,特别是内部结构。白盒测试与代码结构特征的 覆盖 概念紧密相关。例如,如果在测试期间代码中的某个语句没有被执行,这意味着这个语句中的错误也无法被触发。因此,白盒测试引入了一系列必须满足的 覆盖率标准,在测试被认为足够之前。最常用的覆盖率标准是
-
语句覆盖率 – 代码中的每个语句都必须至少被一个测试输入执行。
-
分支覆盖率 – 代码中的每个分支都必须至少被一个测试输入执行。(这相当于每个
if和while决策至少一次为真,至少一次为假。)
除了这些,还有更多的覆盖率标准,包括分支序列的执行、循环迭代(零次、一次、多次)、变量定义和用法之间的数据流等等;[Pezzè 等人,2008] 有一个很好的概述。
让我们考虑上面的 cgi_decode(),并推理出我们需要做什么才能确保代码中的每个语句至少执行一次。我们需要覆盖
-
if c == '+'后面的代码块 -
if c == '%'后面的两个代码块(一个用于有效输入,一个用于无效输入) -
所有其他字符的最终
else情况。
这会导致与上面黑盒测试相同的条件;再次强调,上面的断言确实会覆盖代码中的每个语句。这种对应关系实际上是相当常见的,因为程序员倾向于在不同的代码位置实现不同的行为;因此,覆盖这些位置将导致覆盖不同(指定)行为的测试用例。
白盒测试的优势在于它能够找到实现行为中的错误。即使在规格说明没有提供足够细节的情况下也可以进行;实际上,它有助于识别(并因此指定)规格说明中的边缘情况。缺点是它可能会错过未实现的行为:如果某些指定的功能缺失,白盒测试将不会找到它。
跟踪执行
白盒测试的一个优点是,可以实际自动评估某个程序特性是否被覆盖。为此,需要对程序的执行进行 仪器化,以便在执行过程中,一个特殊的功能能够跟踪执行了哪些代码。测试完成后,这些信息可以传递给程序员,然后程序员可以专注于编写覆盖尚未覆盖代码的测试。
在大多数编程语言中,设置程序以便能够跟踪其执行是非常困难的。但在 Python 中并非如此。sys.settrace(f) 函数允许定义一个 跟踪函数 f(),该函数会在每执行一行代码时被调用。更好的是,它还可以访问当前函数及其名称、当前变量内容等。因此,它是一个理想的 动态分析 工具——也就是说,分析执行过程中实际发生的事情。
为了说明它是如何工作的,让我们再次查看 cgi_decode() 的一个特定执行。
cgi_decode("a+b")
'a b'
为了跟踪执行通过 cgi_decode() 的过程,我们使用 sys.settrace()。首先,我们定义一个 跟踪函数,该函数将在每行被调用。它有三个参数:
-
frame参数让你获取当前的 帧,允许访问当前位置和变量:-
frame.f_code是当前正在执行的代码,其中frame.f_code.co_name是函数名; -
frame.f_lineno保存当前的行号;并且 -
frame.f_locals保存当前的局部变量和参数。
-
-
event参数是一个字符串,包含如"line"(到达了新的一行)或"call"(正在调用一个函数)等值。 -
arg参数是某些事件的附加 参数;例如,对于"return"事件,arg包含返回的值。
我们使用跟踪函数仅用于报告当前通过 frame 参数访问的执行行。
from [types](https://docs.python.org/3/library/types.html) import FrameType, TracebackType
coverage = []
def traceit(frame: FrameType, event: str, arg: Any) -> Optional[Callable]:
"""Trace program execution. To be passed to sys.settrace()."""
if event == 'line':
global coverage
function_name = frame.f_code.co_name
lineno = frame.f_lineno
coverage.append(lineno)
return traceit
我们可以使用 sys.settrace() 来开启和关闭跟踪:
import [sys](https://docs.python.org/3/library/sys.html)
def cgi_decode_traced(s: str) -> None:
global coverage
coverage = []
sys.settrace(traceit) # Turn on
cgi_decode(s)
sys.settrace(None) # Turn off
当我们计算 cgi_decode("a+b") 时,现在我们可以看到执行是如何通过 cgi_decode() 的。在初始化 hex_values、t 和 i 之后,我们看到 while 循环被执行了三次——对应输入中的每个字符。
cgi_decode_traced("a+b")
print(coverage)
[8, 9, 8, 9, 8, 9, 8, 9, 8, 9, 8, 10, 8, 10, 8, 10, 8, 10, 8, 10, 8, 11, 8, 11, 8, 11, 8, 11, 8, 11, 8, 11, 8, 12, 8, 12, 8, 15, 16, 17, 18, 19, 21, 30, 31, 17, 18, 19, 20, 31, 17, 18, 19, 21, 30, 31, 17, 32]
这些实际上是哪些行?为此,我们获取 cgi_decode_code 的源代码并将其编码到一个数组 cgi_decode_lines 中,然后我们将添加覆盖率信息。首先,让我们获取 cgi_encode 的源代码:
import [inspect](https://docs.python.org/3/library/inspect.html)
cgi_decode_code = inspect.getsource(cgi_decode)
cgi_decode_code 是一个包含源代码的字符串。我们可以使用 Python 语法高亮打印它:
from [bookutils](https://github.com/uds-se/fuzzingbook//tree/master/notebooks/shared/bookutils) import print_content, print_file
print_content(cgi_decode_code[:300] + "...", ".py")
def cgi_decode(s: str) -> str:
"""Decode the CGI-encoded string `s`:
* replace '+' by ' '
* replace "%xx" by the character with hex number xx.
Return the decoded string. Raise `ValueError` for invalid inputs."""
# Mapping of hex digits to their integer values
hex_v...
使用 splitlines(),我们将代码分割成一个按行号索引的行数组。
cgi_decode_lines = [""] + cgi_decode_code.splitlines()
cgi_decode_lines[L] 是源代码的第 L 行。
cgi_decode_lines[1]
'def cgi_decode(s: str) -> str:'
我们可以看到,实际执行的第一行(9)实际上是 hex_values 的初始化...
cgi_decode_lines[9:13]
[" '0': 0, '1': 1, '2': 2, '3': 3, '4': 4,",
" '5': 5, '6': 6, '7': 7, '8': 8, '9': 9,",
" 'a': 10, 'b': 11, 'c': 12, 'd': 13, 'e': 14, 'f': 15,",
" 'A': 10, 'B': 11, 'C': 12, 'D': 13, 'E': 14, 'F': 15,"]
...然后是 t 的初始化:
cgi_decode_lines[15]
' t = ""'
要查看哪些行至少执行过一次,我们可以将 coverage 转换为一个集合:
covered_lines = set(coverage)
print(covered_lines)
{32, 8, 9, 10, 11, 12, 15, 16, 17, 18, 19, 20, 21, 30, 31}
让我们打印出完整的代码,并标注未覆盖的行。这种注释的目的是将开发者的注意力引向未覆盖的行。
for lineno in range(1, len(cgi_decode_lines)):
if lineno not in covered_lines:
print("# ", end="")
else:
print(" ", end="")
print("%2d " % lineno, end="")
print_content(cgi_decode_lines[lineno], '.py')
print()
# 1 def cgi_decode(s: str) -> str:
# 2 """Decode the CGI-encoded string `s`:
# 3 * replace '+' by ' '
# 4 * replace "%xx" by the character with hex number xx.
# 5 Return the decoded string. Raise `ValueError` for invalid inputs."""
# 6
# 7 # Mapping of hex digits to their integer values
8 hex_values = {
9 '0': 0, '1': 1, '2': 2, '3': 3, '4': 4,
10 '5': 5, '6': 6, '7': 7, '8': 8, '9': 9,
11 'a': 10, 'b': 11, 'c': 12, 'd': 13, 'e': 14, 'f': 15,
12 'A': 10, 'B': 11, 'C': 12, 'D': 13, 'E': 14, 'F': 15,
# 13 }
# 14
15 t = ""
16 i = 0
17 while i < len(s):
18 c = s[i]
19 if c == '+':
20 t += ' '
21 elif c == '%':
# 22 digit_high, digit_low = s[i + 1], s[i + 2]
# 23 i += 2
# 24 if digit_high in hex_values and digit_low in hex_values:
# 25 v = hex_values[digit_high] * 16 + hex_values[digit_low]
# 26 t += chr(v)
# 27 else:
# 28 raise ValueError("Invalid encoding")
# 29 else:
30 t += c
31 i += 1
32 return t
我们可以看到,许多行(特别是注释)尚未执行(用#标记),仅仅是因为它们不可执行。然而,我们也可以看到,在elif c == '%'下的行尚未执行。如果"a+b"是我们迄今为止的唯一测试用例,那么现在这个缺失的覆盖率将鼓励我们创建另一个测试用例,实际上覆盖这些#标记的行。
覆盖率类
在这本书中,我们将反复使用覆盖率——不仅是为了测量不同测试生成技术的有效性,而且是为了引导测试生成向代码覆盖率方向发展。我们之前的全局coverage变量实现有点繁琐。因此,我们实现了一些有助于我们轻松测量覆盖率的功能。
获取覆盖率的关键思想是利用 Python 的with语句。一般形式
with OBJECT [as VARIABLE]:
BODY
使用BODY执行时,OBJECT被定义(并存储在VARIABLE中)。有趣的是,在BODY的开始和结束时,特殊方法OBJECT.__enter__()和OBJECT.__exit__()会自动调用;即使BODY抛出异常。这允许我们定义一个Coverage对象,其中Coverage.__enter__()会自动开启跟踪,而Coverage.__exit__()会自动关闭跟踪。跟踪后,我们可以使用特殊方法来访问覆盖率。这是使用时的样子:
with Coverage() as cov:
function_to_be_traced()
c = cov.coverage()
在这里,跟踪在function_to_be_traced()期间自动开启,在with块之后再次关闭;之后,我们可以访问已执行的行集。
这是包含所有功能的完整实现。您不必了解所有内容;您只需要知道如何使用它:
Location = Tuple[str, int]
class Coverage:
"""Track coverage within a `with` block. Use as
with Coverage() as cov:
function_to_be_traced()
c = cov.coverage()
"""
def __init__(self) -> None:
"""Constructor"""
self._trace: List[Location] = []
# Trace function
def traceit(self, frame: FrameType, event: str, arg: Any) -> Optional[Callable]:
"""Tracing function. To be overloaded in subclasses."""
if self.original_trace_function is not None:
self.original_trace_function(frame, event, arg)
if event == "line":
function_name = frame.f_code.co_name
lineno = frame.f_lineno
if function_name != '__exit__': # avoid tracing ourselves:
self._trace.append((function_name, lineno))
return self.traceit
def __enter__(self) -> Any:
"""Start of `with` block. Turn on tracing."""
self.original_trace_function = sys.gettrace()
sys.settrace(self.traceit)
return self
def __exit__(self, exc_type: Type, exc_value: BaseException,
tb: TracebackType) -> Optional[bool]:
"""End of `with` block. Turn off tracing."""
sys.settrace(self.original_trace_function)
return None # default: pass all exceptions
def trace(self) -> List[Location]:
"""The list of executed lines, as (function_name, line_number) pairs"""
return self._trace
def coverage(self) -> Set[Location]:
"""The set of executed lines, as (function_name, line_number) pairs"""
return set(self.trace())
def function_names(self) -> Set[str]:
"""The set of function names seen"""
return set(function_name for (function_name, line_number) in self.coverage())
def __repr__(self) -> str:
"""Return a string representation of this object.
Show covered (and uncovered) program code"""
t = ""
for function_name in self.function_names():
# Similar code as in the example above
try:
fun = eval(function_name)
except Exception as exc:
t += f"Skipping {function_name}: {exc}"
continue
source_lines, start_line_number = inspect.getsourcelines(fun)
for lineno in range(start_line_number, start_line_number + len(source_lines)):
if (function_name, lineno) not in self.trace():
t += "# "
else:
t += " "
t += "%2d " % lineno
t += source_lines[lineno - start_line_number]
return t
让我们将其应用于实际:
with Coverage() as cov:
cgi_decode("a+b")
print(cov.coverage())
{('cgi_decode', 8), ('cgi_decode', 11), ('cgi_decode', 17), ('cgi_decode', 30), ('cgi_decode', 20), ('cgi_decode', 10), ('cgi_decode', 16), ('cgi_decode', 19), ('cgi_decode', 9), ('cgi_decode', 32), ('cgi_decode', 12), ('cgi_decode', 31), ('cgi_decode', 15), ('cgi_decode', 21), ('cgi_decode', 18)}
如您所见,Coverage()类不仅跟踪已执行的行,还跟踪函数名。如果您有一个跨越多个文件的程序,这很有用。
对于交互式使用,我们只需简单地打印覆盖率对象,并获得代码列表,其中未覆盖的行用#标记。
print(cov)
# 1 def cgi_decode(s: str) -> str:
# 2 """Decode the CGI-encoded string `s`:
# 3 * replace '+' by ' '
# 4 * replace "%xx" by the character with hex number xx.
# 5 Return the decoded string. Raise `ValueError` for invalid inputs."""
# 6
# 7 # Mapping of hex digits to their integer values
8 hex_values = {
9 '0': 0, '1': 1, '2': 2, '3': 3, '4': 4,
10 '5': 5, '6': 6, '7': 7, '8': 8, '9': 9,
11 'a': 10, 'b': 11, 'c': 12, 'd': 13, 'e': 14, 'f': 15,
12 'A': 10, 'B': 11, 'C': 12, 'D': 13, 'E': 14, 'F': 15,
# 13 }
# 14
15 t = ""
16 i = 0
17 while i < len(s):
18 c = s[i]
19 if c == '+':
20 t += ' '
21 elif c == '%':
# 22 digit_high, digit_low = s[i + 1], s[i + 2]
# 23 i += 2
# 24 if digit_high in hex_values and digit_low in hex_values:
# 25 v = hex_values[digit_high] * 16 + hex_values[digit_low]
# 26 t += chr(v)
# 27 else:
# 28 raise ValueError("Invalid encoding")
# 29 else:
30 t += c
31 i += 1
32 return t
比较覆盖率
由于我们将覆盖率表示为已执行行的集合,因此我们也可以对这些集合应用集合操作。例如,我们可以找出哪些行被单个测试用例覆盖,但未被其他测试用例覆盖:
with Coverage() as cov_plus:
cgi_decode("a+b")
with Coverage() as cov_standard:
cgi_decode("abc")
cov_plus.coverage() - cov_standard.coverage()
{('cgi_decode', 20)}
这是代码中仅在'a+b'输入下执行的单独一行。
我们还可以比较集合,以找出哪些行仍然需要被覆盖。让我们定义cov_max为我们能实现的最大覆盖率。(在这里,我们通过执行我们已有的“良好”测试用例来完成此操作。在实践中,人们会静态分析代码结构,我们将在符号测试章节中介绍。)
with Coverage() as cov_max:
cgi_decode('+')
cgi_decode('%20')
cgi_decode('abc')
try:
cgi_decode('%?a')
except Exception:
pass
然后,我们可以轻松地看到哪些行还没有被测试用例覆盖:
cov_max.coverage() - cov_plus.coverage()
{('cgi_decode', 22),
('cgi_decode', 23),
('cgi_decode', 24),
('cgi_decode', 25),
('cgi_decode', 26),
('cgi_decode', 28)}
再次,这些是处理"%xx"的行,我们还没有在输入中遇到。
基本模糊测试覆盖率
我们现在可以使用我们的覆盖率跟踪来评估测试方法的有效性——特别是测试生成方法的有效性。我们的挑战是仅通过随机输入在cgi_decode()中实现最大覆盖率。原则上,我们最终应该达到那里,因为最终我们将产生宇宙中每一个可能字符串——但具体需要多长时间呢?为此,让我们对cgi_decode()运行一次模糊测试迭代:
from Fuzzer import fuzzer
sample = fuzzer()
sample
'!7#%"*#0=)$;%6*;>638:*>80"=</>(/*:-(2<4 !:5*6856&?""11<7+%<%7,4.8,*+&,,$,."'
这是调用和获得的覆盖率。我们将cgi_decode()包裹在try...except块中,这样我们就可以忽略由非法%xx格式引发的ValueError异常。
with Coverage() as cov_fuzz:
try:
cgi_decode(sample)
except:
pass
cov_fuzz.coverage()
{('cgi_decode', 8),
('cgi_decode', 9),
('cgi_decode', 10),
('cgi_decode', 11),
('cgi_decode', 12),
('cgi_decode', 15),
('cgi_decode', 16),
('cgi_decode', 17),
('cgi_decode', 18),
('cgi_decode', 19),
('cgi_decode', 21),
('cgi_decode', 22),
('cgi_decode', 23),
('cgi_decode', 24),
('cgi_decode', 28),
('cgi_decode', 30),
('cgi_decode', 31)}
这已经是最大覆盖率了吗?显然,还有一些行没有覆盖到:
cov_max.coverage() - cov_fuzz.coverage()
{('cgi_decode', 20),
('cgi_decode', 25),
('cgi_decode', 26),
('cgi_decode', 32)}
让我们再次尝试,通过 100 个随机输入增加覆盖率。我们使用一个数组cumulative_coverage来存储随时间获得的覆盖率;cumulative_coverage[0]是在输入 1 后覆盖的行总数,cumulative_coverage[1]是在输入 1-2 后覆盖的行数,依此类推。
trials = 100
def population_coverage(population: List[str], function: Callable) \
-> Tuple[Set[Location], List[int]]:
cumulative_coverage: List[int] = []
all_coverage: Set[Location] = set()
for s in population:
with Coverage() as cov:
try:
function(s)
except:
pass
all_coverage |= cov.coverage()
cumulative_coverage.append(len(all_coverage))
return all_coverage, cumulative_coverage
让我们创建一百个输入来确定覆盖率是如何增加的:
def hundred_inputs() -> List[str]:
population = []
for i in range(trials):
population.append(fuzzer())
return population
每次输入增加覆盖率的方式如下:
all_coverage, cumulative_coverage = \
population_coverage(hundred_inputs(), cgi_decode)
%matplotlib inline
import [matplotlib.pyplot](https://matplotlib.org/) as plt
plt.plot(cumulative_coverage)
plt.title('Coverage of cgi_decode() with random inputs')
plt.xlabel('# of inputs')
plt.ylabel('lines covered')
Text(0, 0.5, 'lines covered')

当然,这只是一次运行;所以让我们重复多次并绘制平均值。
runs = 100
# Create an array with TRIALS elements, all zero
sum_coverage = [0] * trials
for run in range(runs):
all_coverage, coverage = population_coverage(hundred_inputs(), cgi_decode)
assert len(coverage) == trials
for i in range(trials):
sum_coverage[i] += coverage[i]
average_coverage = []
for i in range(trials):
average_coverage.append(sum_coverage[i] / runs)
plt.plot(average_coverage)
plt.title('Average coverage of cgi_decode() with random inputs')
plt.xlabel('# of inputs')
plt.ylabel('lines covered')
Text(0, 0.5, 'lines covered')

我们看到,平均来说,在 40-60 次模糊测试输入后,我们就能获得完整的覆盖率。
从外部程序获取覆盖率
当然,并非全世界都在用 Python 编程。好消息是,获取覆盖率的问题无处不在,几乎每种编程语言都有一些工具可以测量覆盖率。因此,让我们以一个例子来展示如何为一个 C 程序获取覆盖率。
我们的 C 程序(再次)实现了cgi_decode;这次是一个可以从命令行执行的程序:
$ ./cgi_decode 'Hello+World'
Hello World
下面是 C 代码,首先作为一个 Python 字符串。我们以常用的 C 头文件开始:
cgi_c_code = """
/* CGI decoding as C program */
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
"""
接下来是hex_values的初始化:
cgi_c_code += r"""
int hex_values[256];
void init_hex_values() {
for (int i = 0; i < sizeof(hex_values) / sizeof(int); i++) {
hex_values[i] = -1;
}
hex_values['0'] = 0; hex_values['1'] = 1; hex_values['2'] = 2; hex_values['3'] = 3;
hex_values['4'] = 4; hex_values['5'] = 5; hex_values['6'] = 6; hex_values['7'] = 7;
hex_values['8'] = 8; hex_values['9'] = 9;
hex_values['a'] = 10; hex_values['b'] = 11; hex_values['c'] = 12; hex_values['d'] = 13;
hex_values['e'] = 14; hex_values['f'] = 15;
hex_values['A'] = 10; hex_values['B'] = 11; hex_values['C'] = 12; hex_values['D'] = 13;
hex_values['E'] = 14; hex_values['F'] = 15;
}
"""
下面是cgi_decode()的实际实现,使用指针作为输入源(s)和输出目标(t):
cgi_c_code += r"""
int cgi_decode(char *s, char *t) {
while (*s != '\0') {
if (*s == '+')
*t++ = ' ';
else if (*s == '%') {
int digit_high = *++s;
int digit_low = *++s;
if (hex_values[digit_high] >= 0 && hex_values[digit_low] >= 0) {
*t++ = hex_values[digit_high] * 16 + hex_values[digit_low];
}
else
return -1;
}
else
*t++ = *s;
s++;
}
*t = '\0';
return 0;
}
"""
最后,这是一个驱动程序,它接受第一个参数并使用它调用cgi_decode:
cgi_c_code += r"""
int main(int argc, char *argv[]) {
init_hex_values();
if (argc >= 2) {
char *s = argv[1];
char *t = malloc(strlen(s) + 1); /* output is at most as long as input */
int ret = cgi_decode(s, t);
printf("%s\n", t);
return ret;
}
else
{
printf("cgi_decode: usage: cgi_decode STRING\n");
return 1;
}
}
"""
让我们创建 C 源代码:(注意,以下命令将覆盖当前工作目录中已存在的cgi_decode.c文件。如果您下载了笔记本并在本地工作,请注意这一点。)
with open("cgi_decode.c", "w") as f:
f.write(cgi_c_code)
这里是我们带有语法高亮的 C 代码:
from [bookutils](https://github.com/uds-se/fuzzingbook//tree/master/notebooks/shared/bookutils) import print_file
print_file("cgi_decode.c")
/* CGI decoding as C program */
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
int hex_values[256];
void init_hex_values() {
for (int i = 0; i < sizeof(hex_values) / sizeof(int); i++) {
hex_values[i] = -1;
}
hex_values['0'] = 0; hex_values['1'] = 1; hex_values['2'] = 2; hex_values['3'] = 3;
hex_values['4'] = 4; hex_values['5'] = 5; hex_values['6'] = 6; hex_values['7'] = 7;
hex_values['8'] = 8; hex_values['9'] = 9;
hex_values['a'] = 10; hex_values['b'] = 11; hex_values['c'] = 12; hex_values['d'] = 13;
hex_values['e'] = 14; hex_values['f'] = 15;
hex_values['A'] = 10; hex_values['B'] = 11; hex_values['C'] = 12; hex_values['D'] = 13;
hex_values['E'] = 14; hex_values['F'] = 15;
}
int cgi_decode(char *s, char *t) {
while (*s != '\0') {
if (*s == '+')
*t++ = ' ';
else if (*s == '%') {
int digit_high = *++s;
int digit_low = *++s;
if (hex_values[digit_high] >= 0 && hex_values[digit_low] >= 0) {
*t++ = hex_values[digit_high] * 16 + hex_values[digit_low];
}
else
return -1;
}
else
*t++ = *s;
s++;
}
*t = '\0';
return 0;
}
int main(int argc, char *argv[]) {
init_hex_values();
if (argc >= 2) {
char *s = argv[1];
char *t = malloc(strlen(s) + 1); /* output is at most as long as input */
int ret = cgi_decode(s, t);
printf("%s\n", t);
return ret;
}
else
{
printf("cgi_decode: usage: cgi_decode STRING\n");
return 1;
}
}
我们现在可以将 C 代码编译成可执行文件。--coverage选项指示 C 编译器对代码进行仪器化,以便在运行时收集覆盖率信息。(具体的选项因编译器而异。)
!cc --coverage -o cgi_decode cgi_decode.c
当我们现在执行程序时,覆盖率信息将自动收集并存储在辅助文件中:
!./cgi_decode 'Send+mail+to+me%40fuzzingbook.org'
Send mail to me@fuzzingbook.org
覆盖率信息是由 gcov 程序收集的。对于每个提供的源文件,它都会生成一个新的 .gcov 文件,其中包含覆盖率信息。这些信息存储在 cgi_decode... 或 cgi_decode-cgi_decode... 文件中。
!gcov cgi_decode cgi_decode-cgi_decode
cgi_decode.gcno: No such file or directory
File 'cgi_decode.c'
Lines executed:92.50% of 40
Creating 'cgi_decode.c.gcov'
在 .gcov 文件中,每一行都带有被调用的次数(- 表示不可执行的行,##### 表示零)以及行号。我们可以查看 cgi_decode(),例如,并看到唯一尚未执行的代码是非法输入的 return -1。
lines = open('cgi_decode.c.gcov').readlines()
for i in range(30, 50):
print(lines[i], end='')
1: 26:int cgi_decode(char *s, char *t) {
32: 27: while (*s != '\0') {
31: 28: if (*s == '+')
3: 29: *t++ = ' ';
28: 30: else if (*s == '%') {
1: 31: int digit_high = *++s;
1: 32: int digit_low = *++s;
1: 33: if (hex_values[digit_high] >= 0 && hex_values[digit_low] >= 0) {
1: 34: *t++ = hex_values[digit_high] * 16 + hex_values[digit_low];
1: 35: }
-: 36: else
#####: 37: return -1;
1: 38: }
-: 39: else
27: 40: *t++ = *s;
31: 41: s++;
-: 42: }
1: 43: *t = '\0';
1: 44: return 0;
1: 45:}
让我们在该文件中读取以获取覆盖集:
def read_gcov_coverage(c_file):
gcov_file = c_file + ".gcov"
coverage = set()
with open(gcov_file) as file:
for line in file.readlines():
elems = line.split(':')
covered = elems[0].strip()
line_number = int(elems[1].strip())
if covered.startswith('-') or covered.startswith('#'):
continue
coverage.add((c_file, line_number))
return coverage
coverage = read_gcov_coverage('cgi_decode.c')
list(coverage)[:5]
[('cgi_decode.c', 53),
('cgi_decode.c', 62),
('cgi_decode.c', 16),
('cgi_decode.c', 13),
('cgi_decode.c', 19)]
使用这个集合,我们现在可以执行与我们的 Python 程序相同的覆盖计算。
使用基本模糊测试查找错误
给予足够的时间,我们确实可以覆盖 cgi_decode() 中的每一行,无论编程语言是什么。但这并不意味着它们会没有错误。由于我们没有检查 cgi_decode() 的结果,该函数可以返回任何值,而无需我们检查或注意。为了捕获这样的错误,我们需要设置一个 结果检查器(通常称为 预言机),它会验证测试结果。在我们的情况下,我们可以比较 cgi_decode() 的 C 和 Python 实现,看看它们是否产生相同的结果。
尽管模糊测试在发现 内部错误 方面很出色,即使不检查结果也可以检测到这些错误。实际上,如果有人在我们的 fuzzer() 上运行 cgi_decode(),很快就会找到这样的错误,如下面的代码所示:
from ExpectError import ExpectError
with ExpectError():
for i in range(trials):
try:
s = fuzzer()
cgi_decode(s)
except ValueError:
pass
Traceback (most recent call last):
File "/var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/ipykernel_2687/2238772797.py", line 5, in <module>
cgi_decode(s)
File "/var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/ipykernel_2687/1071239422.py", line 22, in cgi_decode
digit_high, digit_low = s[i + 1], s[i + 2]
~^^^^^^^
IndexError: string index out of range (expected)
因此,可以导致 cgi_decode() 崩溃。为什么是这样呢?让我们看看它的输入:
s
'82 202*&<1&($34\'"/\'.<5/!8"\'5:!4))%;'
这里的问题是字符串的末尾。在 '%' 字符之后,我们的实现将始终尝试访问两个额外的(十六进制)字符,但如果这些字符不存在,我们将得到一个 IndexError 异常。
这个问题也存在于我们的 C 变体中,它是从原始实现继承的 [Pezzè et al, 2008]:
int digit_high = *++s;
int digit_low = *++s;
在这里,s 是指向要读取的字符的指针;++ 通过一个字符增加它。在 C 实现中,问题实际上更严重。如果字符串末尾有 '%' 字符,上述代码将首先读取一个终止字符(C 字符串中的 '\0'),然后是字符串之后的下一个字符,这可能是字符串之后的任何内存内容,因此可能会使程序无法控制地失败。有些好消息是 '\0' 不是一个有效的十六进制字符,因此 C 版本“仅”会读取字符串末尾的一个字符。
令人惊讶的是,我们之前设计的所有手动测试都不会触发这个错误。实际上,无论是语句覆盖率、分支覆盖率,还是文献中常讨论的任何覆盖率标准,都无法找到它。然而,通过简单的模糊测试运行,可以在几次运行中识别出这个错误——如果有适当的运行时检查来发现这样的溢出。这确实需要更多的模糊测试!
经验教训
-
覆盖率指标是一种简单且完全自动化的方法,可以近似地估算在测试运行期间程序实际执行的功能量。
-
存在许多覆盖率指标,其中最重要的包括语句覆盖率和分支覆盖率。
-
在 Python 中,在执行期间访问程序状态非常容易,包括当前正在执行的代码。
最后,让我们清理一下:(注意以下命令将删除当前工作目录中所有符合模式cgi_decode.*的文件。如果你已经下载了笔记本并在本地工作,请注意这一点。)
import [os](https://docs.python.org/3/library/os.html)
import [glob](https://docs.python.org/3/library/glob.html)
for file in (glob.glob("cgi_decode") +
glob.glob("cgi_decode.*") +
glob.glob("cgi_decode-*")):
os.remove(file)
下一步
覆盖率不仅是一种测量测试有效性的工具,而且是一种很好的指导测试生成以实现特定目标的工具——特别是未覆盖的代码。我们使用覆盖率来
- 在突变模糊测试章节中指导现有输入的突变以获得更好的覆盖率
背景
覆盖率是系统化软件测试中的一个核心概念。有关讨论,请参阅测试简介中的书籍。
练习
练习 1:修复cgi_decode()
创建一个适当的测试用例来重现上面讨论的IndexError。修复cgi_decode()以防止错误。展示你的测试(以及额外的fuzzer()运行)不再暴露错误。对于 C 变体也做同样的事情。
使用笔记本来完成练习并查看解决方案。
练习 2:分支覆盖率
除了语句覆盖率外,分支覆盖率是确定测试质量最常用的标准之一。简而言之,分支覆盖率衡量代码中做出的控制决策数量。在以下语句中
if CONDITION:
do_a()
else:
do_b()
例如,无论是CONDITION为真(分支到do_a())还是CONDITION为假(分支到do_b()),这两种情况都需要被覆盖。这适用于所有带有条件的控制语句(如if、while等)。
分支覆盖率与语句覆盖率有何不同?在上面的例子中,实际上没有区别。然而,在这个例子中,确实有:
if CONDITION:
do_a()
something_else()
使用语句覆盖率,只要有一个测试用例中CONDITION为真,就足以覆盖对do_a()的调用。然而,使用分支覆盖率,我们还需要创建一个测试用例,其中do_a()没有被调用。
使用我们的Coverage基础设施,我们可以通过考虑后续执行的代码行对来模拟分支覆盖率。trace()方法为我们提供了依次执行的代码行列表:
with Coverage() as cov:
cgi_decode("a+b")
trace = cov.trace()
trace[:5]
[('cgi_decode', 8),
('cgi_decode', 9),
('cgi_decode', 8),
('cgi_decode', 9),
('cgi_decode', 8)]
第一部分:计算分支覆盖率
定义一个函数 branch_coverage(),它接受一个跟踪并返回跟踪中后续行的对集 - 在上述示例中,这将是对
set(
(('cgi_decode', 9), ('cgi_decode', 10)),
(('cgi_decode', 10), ('cgi_decode', 11)),
# more_pairs
)
高级 Python 程序员奖励:将 BranchCoverage 定义为 Coverage 的子类,并使 branch_coverage() 成为 BranchCoverage 的 coverage() 方法。
使用笔记本 来完成练习并查看解决方案。
第二部分:比较语句覆盖率和分支覆盖率
使用 branch_coverage() 重复本章中的实验,使用分支覆盖率而不是语句覆盖率。手动编写的测试用例是否覆盖了所有分支?
使用笔记本 来完成练习并查看解决方案。
第三部分:平均覆盖率
再次,使用分支覆盖率重复上述实验。fuzzer() 是否覆盖了所有分支,如果是的话,平均需要多少次测试?
使用笔记本 来完成练习并查看解决方案。
本项目的内容受 Creative Commons Attribution-NonCommercial-ShareAlike 4.0 国际许可协议 的许可。作为内容一部分的源代码,以及用于格式化和显示该内容的源代码,受 MIT 许可协议 的许可。 最后修改:2024-11-09 17:28:17+01:00 • 引用 • 版权信息
如何引用本作品
Andreas Zeller, Rahul Gopinath, Marcel Böhme, Gordon Fraser, and Christian Holler: "代码覆盖率". In Andreas Zeller, Rahul Gopinath, Marcel Böhme, Gordon Fraser, and Christian Holler, "模糊测试书", www.fuzzingbook.org/html/Coverage.html. Retrieved 2024-11-09 17:28:17+01:00.
@incollection{fuzzingbook2024:Coverage,
author = {Andreas Zeller and Rahul Gopinath and Marcel B{\"o}hme and Gordon Fraser and Christian Holler},
booktitle = {The Fuzzing Book},
title = {Code Coverage},
year = {2024},
publisher = {CISPA Helmholtz Center for Information Security},
howpublished = {\url{https://www.fuzzingbook.org/html/Coverage.html}},
note = {Retrieved 2024-11-09 17:28:17+01:00},
url = {https://www.fuzzingbook.org/html/Coverage.html},
urldate = {2024-11-09 17:28:17+01:00}
}
基于变异的模糊测试
大多数 随机生成的输入 在语法上是 无效的,因此很快就会被处理程序拒绝。为了在输入处理之外锻炼功能,我们必须增加获得有效输入的机会。一种方法就是所谓的 变异模糊测试 —— 也就是说,对现有输入进行小的修改,这些修改可能仍然保持输入有效,但可以锻炼新的行为。我们展示了如何创建这样的变异,以及如何引导它们指向尚未覆盖的代码,应用了流行的 AFL 模糊测试器的核心概念。
from [bookutils](https://github.com/uds-se/fuzzingbook//tree/master/notebooks/shared/bookutils) import YouTubeVideo
YouTubeVideo('5ROhc_42jQU')
先决条件
-
您应该了解基本模糊测试的工作原理;例如,从 "模糊测试" 章节中。
-
您应该了解 获取覆盖率 的基础知识。
概述
要 使用本章提供的代码,请编写
>>> from fuzzingbook.MutationFuzzer import <identifier>
然后利用以下功能。
本章介绍了一个 MutationFuzzer 类,它接受一个 种子输入 列表,然后对其进行变异:
>>> seed_input = "http://www.google.com/search?q=fuzzing"
>>> mutation_fuzzer = MutationFuzzer(seed=[seed_input])
>>> [mutation_fuzzer.fuzz() for i in range(10)]
['http://www.google.com/search?q=fuzzing',
']hTtp://ww,google\x1ecom/searc?q=fuzzig',
'hppY://www.google.cm/seacH?q=fduzzing',
'http:&//www.goole.com/sear#h?q=fuzz(ingw',
'http://vww.googlje.om/{earch?q5fuzzing',
'http://www.google.com/seach?q=uzzing',
'hv,tp*//www.ggogle.com/seagrch/q=fuzzing',
"h(tpy:/wGw.goo'l%.com/searc?q=fuz~ing",
'hp://www.gooelecom/)search?q=fuz?zing?',
"httpv//www.goo6gl.#om'search?q=Puzzin"]
MutationCoverageFuzzer 维护一个输入的 种群,然后通过进化来最大化覆盖率。
>>> mutation_fuzzer = MutationCoverageFuzzer(seed=[seed_input])
>>> mutation_fuzzer.runs(http_runner, trials=10000)
>>> mutation_fuzzer.population[:5]
['http://www.google.com/search?q=fuzzing',
'http://www.google.com</searc?q=fzzinw',
'http://twww.google.com/searc`pnu<zzing',
'http://Btwnww.gIoog|e.cnsearc`pn<zzing',
'http://www\x0egoogle.com</sea#r#?q=_fdz0zinw']
基于覆盖率进行变异输入的模糊测试">
在跟踪覆盖率的同时运行函数(inp)。
如果我们达到新的覆盖率,
将 inp 添加到种群中,并将其覆盖率添加到种群覆盖率中">
将种群设置为初始种子。
在子类中重载。">
变异模糊的基类">
构造函数。
seed - 一个包含(输入)字符串的列表,用于变异。
min_mutations - 应用变异的最小次数。
max_mutations - 应用变异的最大次数。">
返回模糊输入">
通过变异种群成员创建一个新的候选者。">
将种群设置为初始种子。
在子类中重载。">
模糊器的基类。">
构造函数">
返回模糊输入
使用模糊输入运行 runner
使用模糊输入运行 runner,trials 次数
使用变异进行模糊测试
2013 年 11 月,美国模糊跳鼠(AFL)的第一个版本发布。从那时起,AFL 已成为最成功的模糊测试工具之一,并有许多变体,例如AFLFast、AFLGo和AFLSmart(本书中将有讨论)。AFL 使模糊测试成为自动化漏洞检测的热门选择。它是第一个证明可以在许多安全关键的实际应用中大规模自动检测漏洞的工具。

图 1. 美国模糊跳鼠命令行用户界面
在本章中,我们将介绍突变模糊测试的基础知识;下一章将进一步展示如何将模糊测试引导到特定的代码目标。
模糊测试 URL 解析器
许多程序在实际上处理输入之前,期望它们的输入以非常特定的格式到来。作为一个例子,想想一个接受 URL(一个网页地址)的程序。URL 必须是有效的格式(即 URL 格式),这样程序才能处理它。当使用随机输入进行模糊测试时,我们实际上产生一个有效 URL 的可能性有多大?
为了更深入地了解问题,让我们探索一下 URL 由什么组成。一个 URL 由多个元素组成:
scheme://netloc/path?query#fragment
其中
-
scheme是要使用的协议,包括http、https、ftp、file... -
netloc是要连接的主机名,例如www.google.com -
path是在特定主机上的路径,例如search -
query是一组键/值对,例如q=fuzzing -
fragment是指向检索到的文档中位置的标记,例如#result
在 Python 中,我们可以使用 urlparse() 函数来解析和分解一个 URL 到其各个部分。
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 Tuple, List, Callable, Set, Any
from [urllib.parse](https://docs.python.org/3/library/urllib.parse.html) import urlparse
urlparse("http://www.google.com/search?q=fuzzing")
ParseResult(scheme='http', netloc='www.google.com', path='/search', params='', query='q=fuzzing', fragment='')
我们可以看到结果如何将 URL 的各个部分编码在不同的属性中。
现在我们假设我们有一个接受 URL 作为输入的程序。为了简化问题,我们不会让它做很多事情;我们只是让它检查传入的 URL 是否有效。如果 URL 是有效的,它返回 True;否则,它引发异常。
def http_program(url: str) -> bool:
supported_schemes = ["http", "https"]
result = urlparse(url)
if result.scheme not in supported_schemes:
raise ValueError("Scheme must be one of " +
repr(supported_schemes))
if result.netloc == '':
raise ValueError("Host must be non-empty")
# Do something with the URL
return True
现在我们来模糊测试 http_program()。为了进行模糊测试,我们使用可打印的 ASCII 字符的全范围,包括 :, / 和小写字母。
from Fuzzer import fuzzer
fuzzer(char_start=32, char_range=96)
'"N&+slk%h\x7fyp5o\'@[3(rW*M5W]tMFPU4\\P@tz%X?uo\\1?b4T;1bDeYtHx #UJ5w}pMmPodJM,_'
让我们尝试使用 1000 个随机输入进行模糊测试,看看我们是否有一些成功。
for i in range(1000):
try:
url = fuzzer()
result = http_program(url)
print("Success!")
except ValueError:
pass
实际上得到一个有效 URL 的可能性有多大?我们需要我们的字符串以 "http://" 或 "https://" 开头。我们先来看一下 "http://" 的情况。我们需要以这七个非常具体的字符开始。随机产生这七个字符的概率(字符范围为 96 个不同的字符)是 \(1 : 96⁷\),或者说
96 ** 7
75144747810816
产生 "https://" 前缀的概率甚至更糟,为 \(1 : 96⁸\):
96 ** 8
7213895789838336
这给我们一个总概率为
likelihood = 1 / (96 ** 7) + 1 / (96 ** 8)
likelihood
1.344627131107667e-14
这是我们需要产生一个有效的 URL 方案的平均运行次数:
1 / likelihood
74370059689055.02
让我们来测量一下 http_program() 的一次运行需要多长时间:
from [Timer import Timer
trials = 1000
with Timer() as t:
for i in range(trials):
try:
url = fuzzer()
result = http_program(url)
print("Success!")
except ValueError:
pass
duration_per_run_in_seconds = t.elapsed_time() / trials
duration_per_run_in_seconds
1.798770803725347e-05
这相当快,不是吗?不幸的是,我们有很多运行要覆盖。
seconds_until_success = duration_per_run_in_seconds * (1 / likelihood)
seconds_until_success
1337746920.3998353
这相当于
hours_until_success = seconds_until_success / 3600
days_until_success = hours_until_success / 24
years_until_success = days_until_success / 365.25
years_until_success
42.39064188657678
即使我们将事情并行化很多,我们仍然需要等待数月到数年。这是为了得到 一个 成功的运行,这将更深入地了解 http_program()。
基本模糊测试做得好的是测试 urlparse(),如果这个解析函数中存在错误,它有很大的机会揭示出来。但只要我们不能产生一个有效的输入,我们就无法触及任何更深层的功能。
突变输入
从头开始生成随机字符串的替代方法是,从一个给定的有效输入开始,然后随后对其进行突变。在这个上下文中,突变是一种简单的字符串操作——比如说,插入一个(随机)字符,删除一个字符,或者在字符表示中翻转一个位。这被称为突变模糊测试——与之前讨论的代际模糊测试技术相对。
这里有一些突变供您开始:
import [random](https://docs.python.org/3/library/random.html)
def delete_random_character(s: str) -> str:
"""Returns s with a random character deleted"""
if s == "":
return s
pos = random.randint(0, len(s) - 1)
# print("Deleting", repr(s[pos]), "at", pos)
return s[:pos] + s[pos + 1:]
seed_input = "A quick brown fox"
for i in range(10):
x = delete_random_character(seed_input)
print(repr(x))
'A uick brown fox'
'A quic brown fox'
'A quick brown fo'
'A quic brown fox'
'A quick bown fox'
'A quick bown fox'
'A quick brown fx'
'A quick brown ox'
'A quick brow fox'
'A quic brown fox'
def insert_random_character(s: str) -> str:
"""Returns s with a random character inserted"""
pos = random.randint(0, len(s))
random_character = chr(random.randrange(32, 127))
# print("Inserting", repr(random_character), "at", pos)
return s[:pos] + random_character + s[pos:]
for i in range(10):
print(repr(insert_random_character(seed_input)))
'A quick brvown fox'
'A quwick brown fox'
'A qBuick brown fox'
'A quick broSwn fox'
'A quick brown fvox'
'A quick brown 3fox'
'A quick brNown fox'
'A quick brow4n fox'
'A quick brown fox8'
'A equick brown fox'
def flip_random_character(s):
"""Returns s with a random bit flipped in a random position"""
if s == "":
return s
pos = random.randint(0, len(s) - 1)
c = s[pos]
bit = 1 << random.randint(0, 6)
new_c = chr(ord(c) ^ bit)
# print("Flipping", bit, "in", repr(c) + ", giving", repr(new_c))
return s[:pos] + new_c + s[pos + 1:]
for i in range(10):
print(repr(flip_random_character(seed_input)))
'A quick bRown fox'
'A quici brown fox'
'A"quick brown fox'
'A quick brown$fox'
'A quick bpown fox'
'A quick brown!fox'
'A 1uick brown fox'
'@ quick brown fox'
'A quic+ brown fox'
'A quick bsown fox'
让我们现在创建一个随机突变器,它随机选择要应用的突变:
def mutate(s: str) -> str:
"""Return s with a random mutation applied"""
mutators = [
delete_random_character,
insert_random_character,
flip_random_character
]
mutator = random.choice(mutators)
# print(mutator)
return mutator(s)
for i in range(10):
print(repr(mutate("A quick brown fox")))
'A qzuick brown fox'
' quick brown fox'
'A quick Brown fox'
'A qMuick brown fox'
'A qu_ick brown fox'
'A quick bXrown fox'
'A quick brown fx'
'A quick!brown fox'
'A! quick brown fox'
'A quick brownfox'
现在的想法是,如果我们一开始有一些有效输入,我们可以通过应用上述突变之一来创建更多的输入候选。为了了解这是如何工作的,让我们回到 URL。
突变 URL
让我们现在回到我们的 URL 解析问题。让我们创建一个函数is_valid_url(),该函数检查http_program()是否接受输入。
def is_valid_url(url: str) -> bool:
try:
result = http_program(url)
return True
except ValueError:
return False
assert is_valid_url("http://www.google.com/search?q=fuzzing")
assert not is_valid_url("xyzzy")
让我们现在在一个给定的 URL 上应用mutate()函数,看看我们获得多少有效输入。
seed_input = "http://www.google.com/search?q=fuzzing"
valid_inputs = set()
trials = 20
for i in range(trials):
inp = mutate(seed_input)
if is_valid_url(inp):
valid_inputs.add(inp)
我们现在可以观察到,通过突变原始输入,我们得到了高比例的有效输入:
len(valid_inputs) / trials
0.8
通过突变一个http:样本种子输入,产生一个https:前缀的概率是多少?我们必须插入(\(1 : 3\))正确的字符's'(\(1 : 96\))到正确的位置(\(1 : l\)),其中\(l\)是我们种子输入的长度。这意味着平均来说,我们需要这么多运行:
trials = 3 * 96 * len(seed_input)
trials
10944
我们实际上可以承担得起。让我们试试:
from Timer import Timer
trials = 0
with Timer() as t:
while True:
trials += 1
inp = mutate(seed_input)
if inp.startswith("https://"):
print(
"Success after",
trials,
"trials in",
t.elapsed_time(),
"seconds")
break
Success after 3656 trials in 0.004294582991860807 seconds
当然,如果我们想得到,比如说,一个"ftp://"前缀,我们需要更多的突变和更多的运行——最重要的是,我们需要应用多重突变。
多重突变
到目前为止,我们只在样本字符串上应用了一个单一的突变。然而,我们也可以应用多重突变,进一步改变它。例如,如果我们对样本字符串应用 20 次突变会发生什么?
seed_input = "http://www.google.com/search?q=fuzzing"
mutations = 50
inp = seed_input
for i in range(mutations):
if i % 5 == 0:
print(i, "mutations:", repr(inp))
inp = mutate(inp)
0 mutations: 'http://www.google.com/search?q=fuzzing'
5 mutations: 'http:/L/www.googlej.com/seaRchq=fuz:ing'
10 mutations: 'http:/L/www.ggoWglej.com/seaRchqfu:in'
15 mutations: 'http:/L/wwggoWglej.com/seaR3hqf,u:in'
20 mutations: 'htt://wwggoVgle"j.som/seaR3hqf,u:in'
25 mutations: 'htt://fwggoVgle"j.som/eaRd3hqf,u^:in'
30 mutations: 'htv://>fwggoVgle"j.qom/ea0Rd3hqf,u^:i'
35 mutations: 'htv://>fwggozVle"Bj.qom/eapRd[3hqf,u^:i'
40 mutations: 'htv://>fwgeo6zTle"Bj.\'qom/eapRd[3hqf,tu^:i'
45 mutations: 'htv://>fwgeo]6zTle"BjM.\'qom/eaR3hqf,tu^:i'
如您所见,原始种子输入几乎无法辨认。通过反复突变输入,我们得到更多样化的输入。
要在一个单独的包中实现这种多重突变,让我们引入一个MutationFuzzer类。它接受一个种子(字符串列表)以及突变的最小和最大数量。
from [Fuzzer import Fuzzer
class MutationFuzzer(Fuzzer):
"""Base class for mutational fuzzing"""
def __init__(self, seed: List[str],
min_mutations: int = 2,
max_mutations: int = 10) -> None:
"""Constructor.
`seed` - a list of (input) strings to mutate.
`min_mutations` - the minimum number of mutations to apply.
`max_mutations` - the maximum number of mutations to apply.
"""
self.seed = seed
self.min_mutations = min_mutations
self.max_mutations = max_mutations
self.reset()
def reset(self) -> None:
"""Set population to initial seed.
To be overloaded in subclasses."""
self.population = self.seed
self.seed_index = 0
在以下内容中,让我们通过向其中添加更多方法来进一步开发MutationFuzzer。Python 语言要求我们定义一个包含所有方法的整个类作为一个单一、连续的单位;然而,我们希望一个接一个地引入方法。为了避免这个问题,我们使用一个特殊的技巧:每当我们要向某个类C引入一个新方法时,我们使用以下构造:
class C(C):
def new_method(self, args):
pass
这似乎将C定义为它自己的子类,这毫无意义——但实际上,它引入了一个新的C类作为旧C类的子类,并覆盖了旧的C定义。这给我们的是一个具有new_method()方法的C类,这正是我们想要的。(尽管如此,之前定义的C对象将保留早期的C定义,因此必须重建。)
使用这个技巧,我们现在可以添加一个mutate()方法,它实际上会调用上面的mutate()函数。当我们要在以后扩展MutationFuzzer时,将mutate()作为一个方法是有用的。
class MutationFuzzer(MutationFuzzer):
def mutate(self, inp: str) -> str:
return mutate(inp)
让我们回到我们的策略,最大化种群中的覆盖率多样性。首先,让我们创建一个方法create_candidate(),它从当前种群(self.population)中随机选择一些输入,然后应用min_mutations和max_mutations之间的突变步骤,返回最终结果:
class MutationFuzzer(MutationFuzzer):
def create_candidate(self) -> str:
"""Create a new candidate by mutating a population member"""
candidate = random.choice(self.population)
trials = random.randint(self.min_mutations, self.max_mutations)
for i in range(trials):
candidate = self.mutate(candidate)
return candidate
fuzz()方法首先选择种子;当这些种子用完时,我们进行突变:
class MutationFuzzer(MutationFuzzer):
def fuzz(self) -> str:
if self.seed_index < len(self.seed):
# Still seeding
self.inp = self.seed[self.seed_index]
self.seed_index += 1
else:
# Mutating
self.inp = self.create_candidate()
return self.inp
这里是fuzz()方法的实际应用。每次调用fuzz()方法时,我们都会得到一个新的变体,其中应用了多个突变。
seed_input = "http://www.google.com/search?q=fuzzing"
mutation_fuzzer = MutationFuzzer(seed=[seed_input])
mutation_fuzzer.fuzz()
'http://www.google.com/search?q=fuzzing'
mutation_fuzzer.fuzz()
'http://www.gogl9ecom/earch?qfuzzing'
mutation_fuzzer.fuzz()
'htotq:/www.googleom/yseach?q=fzzijg'
输入的多样性越高,无效输入的风险就越大。成功的关键在于引导这些突变的思想——也就是说,保留那些特别有价值的突变。
通过覆盖率引导
为了覆盖尽可能多的功能,可以依靠指定或实现的功能,如["覆盖率"(Coverage.html)]章节中所述。现在,我们不会假设存在程序行为的规范(尽管肯定会有好处!)。然而,我们将假设要测试的程序存在——并且我们可以利用其结构来指导测试生成。
由于测试总是执行当前程序,因此可以始终收集有关其执行的信息——至少是决定测试是否通过所需的最基本信息。由于覆盖率通常也用于确定测试质量,我们假设我们还可以检索测试运行的覆盖率。那么问题是:我们如何利用覆盖率来指导测试生成?
一个特别成功的想法被应用于流行的模糊测试工具美国模糊跳鼠中,简称AFL。就像我们上面的例子一样,AFL 会进化那些已经成功的测试用例——但对于 AFL 来说,“成功”意味着在程序执行中找到一条新的路径。这样,AFL 可以继续突变那些迄今为止已经找到新路径的输入;如果某个输入找到了另一条路径,它也会被保留。
让我们构建这样的策略。我们首先引入一个Runner类,它捕获给定函数的覆盖率。首先,一个FunctionRunner类:
from Fuzzer import Runner
class FunctionRunner(Runner):
def __init__(self, function: Callable) -> None:
"""Initialize. `function` is a function to be executed"""
self.function = function
def run_function(self, inp: str) -> Any:
return self.function(inp)
def run(self, inp: str) -> Tuple[Any, str]:
try:
result = self.run_function(inp)
outcome = self.PASS
except Exception:
result = None
outcome = self.FAIL
return result, outcome
http_runner = FunctionRunner(http_program)
http_runner.run("https://foo.bar/")
(True, 'PASS')
我们现在可以扩展FunctionRunner类,使其也能测量覆盖率。在调用run()之后,coverage()方法返回上次运行中实现的覆盖率。
from Coverage import Coverage, population_coverage, Location
class FunctionCoverageRunner(FunctionRunner):
def run_function(self, inp: str) -> Any:
with Coverage() as cov:
try:
result = super().run_function(inp)
except Exception as exc:
self._coverage = cov.coverage()
raise exc
self._coverage = cov.coverage()
return result
def coverage(self) -> Set[Location]:
return self._coverage
http_runner = FunctionCoverageRunner(http_program)
http_runner.run("https://foo.bar/")
(True, 'PASS')
这里列出了前五个覆盖的位置:
print(list(http_runner.coverage())[:5])
[('http_program', 11), ('run_function', 7), ('urlparse', 395), ('_coerce_args', 129), ('urlparse', 401)]
现在是主要类。我们维护种群和已经实现的覆盖率集合(coverages_seen)。fuzz()辅助函数接受一个输入并在其上运行给定的function()。如果其覆盖率是新的(即不在coverages_seen中),则输入被添加到population中,覆盖率被添加到coverages_seen。
class MutationCoverageFuzzer(MutationFuzzer):
"""Fuzz with mutated inputs based on coverage"""
def reset(self) -> None:
super().reset()
self.coverages_seen: Set[frozenset] = set()
# Now empty; we fill this with seed in the first fuzz runs
self.population = []
def run(self, runner: FunctionCoverageRunner) -> Any:
"""Run function(inp) while tracking coverage.
If we reach new coverage,
add inp to population and its coverage to population_coverage
"""
result, outcome = super().run(runner)
new_coverage = frozenset(runner.coverage())
if outcome == Runner.PASS and new_coverage not in self.coverages_seen:
# We have new coverage
self.population.append(self.inp)
self.coverages_seen.add(new_coverage)
return result
让我们现在将其应用于实际操作:
seed_input = "http://www.google.com/search?q=fuzzing"
mutation_fuzzer = MutationCoverageFuzzer(seed=[seed_input])
mutation_fuzzer.runs(http_runner, trials=10000)
mutation_fuzzer.population
['http://www.google.com/search?q=fuzzing',
'http://ww.google.co/searc(?q=fuzzin_g',
'http://ww.google.#o/sarc(?q=fuzzhn_w',
'Http://www.g/ogle.com/earchq=fuzzing',
'http://$\x7fw,go0ogle.co/searc(;q=fXzin_g',
'http://$\x7fw(g\x7f0ogle&@cosWearc(;q3=fXzin_g',
'Http://www.g/ogle*com;/ea+chq=fuzzing',
'http://ww.Google.co/saarch?#q=fuzzi|n_o',
'http://{$H\x7fw,o0ogle.co/sarc(;q=fXzi#n_"g',
'http://ww.gogle.bm/s;eqrh?q=fuzzi)ng',
'Http://www.g/ogQle*com;/ea#ch=fu~ring',
'Http://\x7fww.g/ogle*com;/Yeachq?8fuzzing',
'Http://wwwd.g/ogQle*com;?ea#ch=fu~ring',
'Http://www.g/ogQle*com;/1ea?#ch=fu~ring']
成功!在我们的种群中,每个输入现在都是有效的,并且具有不同的覆盖率,来自各种方案、路径、查询和片段的组合。
all_coverage, cumulative_coverage = population_coverage(
mutation_fuzzer.population, http_program)
import [matplotlib.pyplot](https://matplotlib.org/) as plt
plt.plot(cumulative_coverage)
plt.title('Coverage of urlparse() with random inputs')
plt.xlabel('# of inputs')
plt.ylabel('lines covered');

这种策略的好处是,应用于更大的程序时,它会愉快地探索一条路径接着一条路径——覆盖功能后再覆盖功能。所需的一切只是一个捕获覆盖率的方法。
经验教训
-
随机生成的输入通常无效——因此练习主要测试输入处理功能。
-
从现有有效输入中产生的变异有更高的可能性是有效的,因此可以超出输入处理的功能进行测试。
下一步
在下一章关于 greybox 模糊测试中,我们进一步扩展了基于变异测试的概念,使用功率调度,允许在测试“不太可能”的路径和“更接近”目标位置的种子上花费更多精力。
练习
练习 1:使用变异进行 CGI 解码模糊测试
在"覆盖率"章节中,将上述基于引导变异的模糊测试技术应用于cgi_decode()。你需要多少次试验才能覆盖+、%(有效和无效)以及常规字符的所有变体?
from Coverage import cgi_decode
seed = ["Hello World"]
cgi_runner = FunctionCoverageRunner(cgi_decode)
m = MutationCoverageFuzzer(seed)
results = m.runs(cgi_runner, 10000)
m.population
['Hello World', 'he_<+llo(or<D', 'L}eml &Wol%dD', 'L)q<}aml &cWol%d3D+']
cgi_runner.coverage()
{('cgi_decode', 16),
('cgi_decode', 17),
('cgi_decode', 18),
('cgi_decode', 19),
('cgi_decode', 20),
('cgi_decode', 23),
('cgi_decode', 24),
('cgi_decode', 25),
('cgi_decode', 26),
('cgi_decode', 27),
('cgi_decode', 29),
('cgi_decode', 38),
('cgi_decode', 39),
('cgi_decode', 40),
('run_function', 7)}
all_coverage, cumulative_coverage = population_coverage(
m.population, cgi_decode)
import [matplotlib.pyplot](https://matplotlib.org/) as plt
plt.plot(cumulative_coverage)
plt.title('Coverage of cgi_decode() with random inputs')
plt.xlabel('# of inputs')
plt.ylabel('lines covered');

经过 10,000 次运行,我们已经成功合成了一个+字符和一个有效的%xx形式。我们仍然可以做得更好。
练习 2:使用变异模糊测试 bc
在"模糊测试介绍"章节中,将上述基于变异的模糊测试技术应用于bc。
第一部分:非引导变异
从非引导变异开始。有多少输入是有效的?
使用笔记本来练习练习并查看解决方案。
第二部分:引导变异
继续进行引导变异。为此,你需要找到一种从 C 程序(如bc)中提取覆盖率的方法。按照以下步骤进行:
首先,获取GNU bc;下载,例如,bc-1.07.1.tar.gz并解压它:
!curl -O mirrors.kernel.org/gnu/bc/bc-1.07.1.tar.gz
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 410k 100 410k 0 0 350k 0 0:00:01 0:00:01 --:--:-- 350k
!tar xfz bc-1.07.1.tar.gz
第二,配置软件包:
!cd bc-1.07.1; ./configure
checking for a BSD-compatible install... /opt/homebrew/bin/ginstall -c
checking whether build environment is sane... yes
checking for a thread-safe mkdir -p... /opt/homebrew/bin/gmkdir -p
checking for gawk... no
checking for mawk... no
checking for nawk... no
checking for awk... awk
checking whether make sets $(MAKE)... yes
checking whether make supports nested variables... yes
checking for gcc... gcc
checking whether the C compiler works... yes
checking for C compiler default output file name... a.out
checking for suffix of executables...
checking whether we are cross compiling... no
checking for suffix of object files... o
checking whether we are using the GNU C compiler... yes
checking whether gcc accepts -g... yes
checking for gcc option to accept ISO C89... none needed
checking whether gcc understands -c and -o together... yes
checking for style of include used by make... GNU
checking dependency style of gcc... gcc3
checking how to run the C preprocessor... gcc -E
checking for grep that handles long lines and -e... /usr/bin/grep
checking for egrep... /usr/bin/grep -E
checking for ANSI C header files... yes
checking for sys/types.h... yes
checking for sys/stat.h... yes
checking for stdlib.h... yes
checking for string.h... yes
checking for memory.h... yes
checking for strings.h... yes
checking for inttypes.h... yes
checking for stdint.h... yes
checking for unistd.h... yes
checking minix/config.h usability... no
checking minix/config.h presence... no
checking for minix/config.h... no
checking whether it is safe to define __EXTENSIONS__... yes
checking for flex... flex
checking lex output file root... lex.yy
checking lex library... -ll
checking whether yytext is a pointer... yes
checking for ar... ar
checking the archiver (ar) interface... ar
checking for bison... bison -y
checking for ranlib... ranlib
checking whether make sets $(MAKE)... (cached) yes
checking for stdarg.h... yes
checking for stddef.h... yes
checking for stdlib.h... (cached) yes
checking for string.h... (cached) yes
checking for errno.h... yes
checking for limits.h... yes
checking for unistd.h... (cached) yes
checking for lib.h... no
checking for an ANSI C-conforming const... yes
checking for size_t... yes
checking for ptrdiff_t... yes
checking for vprintf... yes
checking for _doprnt... no
checking for isgraph... yes
checking for setvbuf... yes
checking for fstat... yes
checking for strtol... yes
Adding GCC specific compile flags.
checking that generated files are newer than configure... done
configure: creating ./config.status
config.status: creating Makefile
config.status: creating bc/Makefile
config.status: creating dc/Makefile
config.status: creating lib/Makefile
config.status: creating doc/Makefile
config.status: creating doc/texi-ver.incl
config.status: creating config.h
config.status: executing depfiles commands
第三,使用特殊标志编译软件包:
!cd bc-1.07.1 && make -k CFLAGS="--coverage"
/Applications/Xcode.app/Contents/Developer/usr/bin/make all-recursive
Making all in lib
gcc -DHAVE_CONFIG_H -I. -I.. -I. -I.. -I./../h -g -O2 -Wall -funsigned-char --coverage -MT getopt.o -MD -MP -MF .deps/getopt.Tpo -c -o getopt.o getopt.c
getopt.c:348:28: warning: passing arguments to 'getenv'
without a prototype is deprecated in all versions of C and is not
supported in C2x [-Wdeprecated-non-prototype]
348 | posixly_correct = getenv ("POSIXLY_CORRECT");
| ^ In file included from getopt.c:106:
./../h/getopt.h:144:12: warning: a function declaration
without a prototype is deprecated in all versions of C and is treated as a
zero-parameter prototype in C2x, conflicting with a subsequent definition
[-Wdeprecated-non-prototype]
144 | extern int getopt ();
| ^ getopt.c:1135:1: note: conflicting prototype is here
1135 | getopt (int argc, char *const *argv, const char *optstring)
| ^ 2 warnings generated.
mv -f .deps/getopt.Tpo .deps/getopt.Po
gcc -DHAVE_CONFIG_H -I. -I.. -I. -I.. -I./../h -g -O2 -Wall -funsigned-char --coverage -MT getopt1.o -MD -MP -MF .deps/getopt1.Tpo -c -o getopt1.o getopt1.c
mv -f .deps/getopt1.Tpo .deps/getopt1.Po
gcc -DHAVE_CONFIG_H -I. -I.. -I. -I.. -I./../h -g -O2 -Wall -funsigned-char --coverage -MT vfprintf.o -MD -MP -MF .deps/vfprintf.Tpo -c -o vfprintf.o vfprintf.c
mv -f .deps/vfprintf.Tpo .deps/vfprintf.Po
gcc -DHAVE_CONFIG_H -I. -I.. -I. -I.. -I./../h -g -O2 -Wall -funsigned-char --coverage -MT number.o -MD -MP -MF .deps/number.Tpo -c -o number.o number.c
mv -f .deps/number.Tpo .deps/number.Po
rm -f libbc.a
ar cru libbc.a getopt.o getopt1.o vfprintf.o number.o
ranlib libbc.a
Making all in bc
gcc -DHAVE_CONFIG_H -I. -I.. -I. -I./../h -g -O2 -Wall -funsigned-char --coverage -MT main.o -MD -MP -MF .deps/main.Tpo -c -o main.o main.c
In file included from main.c:34:
./../h/getopt.h:144:12: warning: a function declaration
without a prototype is deprecated in all versions of C and is treated as a
zero-parameter prototype in C2x, conflicting with a previous declaration
[-Wdeprecated-non-prototype]
144 | extern int getopt ();
| ^ /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/unistd.h:509:6: note:
conflicting prototype is here
509 | int getopt(int, char * const [], const char *) __DARWIN_ALIAS(getopt);
| ^ 1 warning generated.
mv -f .deps/main.Tpo .deps/main.Po
gcc -DHAVE_CONFIG_H -I. -I.. -I. -I./../h -g -O2 -Wall -funsigned-char --coverage -MT bc.o -MD -MP -MF .deps/bc.Tpo -c -o bc.o bc.c
mv -f .deps/bc.Tpo .deps/bc.Po
gcc -DHAVE_CONFIG_H -I. -I.. -I. -I./../h -g -O2 -Wall -funsigned-char --coverage -MT scan.o -MD -MP -MF .deps/scan.Tpo -c -o scan.o scan.c
mv -f .deps/scan.Tpo .deps/scan.Po
gcc -DHAVE_CONFIG_H -I. -I.. -I. -I./../h -g -O2 -Wall -funsigned-char --coverage -MT execute.o -MD -MP -MF .deps/execute.Tpo -c -o execute.o execute.c
mv -f .deps/execute.Tpo .deps/execute.Po
gcc -DHAVE_CONFIG_H -I. -I.. -I. -I./../h -g -O2 -Wall -funsigned-char --coverage -MT load.o -MD -MP -MF .deps/load.Tpo -c -o load.o load.c
mv -f .deps/load.Tpo .deps/load.Po
gcc -DHAVE_CONFIG_H -I. -I.. -I. -I./../h -g -O2 -Wall -funsigned-char --coverage -MT storage.o -MD -MP -MF .deps/storage.Tpo -c -o storage.o storage.c
mv -f .deps/storage.Tpo .deps/storage.Po
gcc -DHAVE_CONFIG_H -I. -I.. -I. -I./../h -g -O2 -Wall -funsigned-char --coverage -MT util.o -MD -MP -MF .deps/util.Tpo -c -o util.o util.c
mv -f .deps/util.Tpo .deps/util.Po
gcc -DHAVE_CONFIG_H -I. -I.. -I. -I./../h -g -O2 -Wall -funsigned-char --coverage -MT warranty.o -MD -MP -MF .deps/warranty.Tpo -c -o warranty.o warranty.c
warranty.c:56:1: warning: a function definition without a
prototype is deprecated in all versions of C and is not supported in C2x
[-Wdeprecated-non-prototype]
56 | warranty(prefix)
| ^ 1 warning generated.
mv -f .deps/warranty.Tpo .deps/warranty.Po
echo '{0}' > libmath.h
/Applications/Xcode.app/Contents/Developer/usr/bin/make global.o
gcc -DHAVE_CONFIG_H -I. -I.. -I. -I./../h -g -O2 -Wall -funsigned-char --coverage -MT global.o -MD -MP -MF .deps/global.Tpo -c -o global.o global.c
mv -f .deps/global.Tpo .deps/global.Po
gcc -g -O2 -Wall -funsigned-char --coverage -o libmath.h -o fbc main.o bc.o scan.o execute.o load.o storage.o util.o warranty.o global.o ../lib/libbc.a -ll
./fbc -c ./libmath.b </dev/null >libmath.h
./fix-libmath_h
2655
2793
rm -f ./fbc ./global.o
gcc -DHAVE_CONFIG_H -I. -I.. -I. -I./../h -g -O2 -Wall -funsigned-char --coverage -MT global.o -MD -MP -MF .deps/global.Tpo -c -o global.o global.c
mv -f .deps/global.Tpo .deps/global.Po
gcc -g -O2 -Wall -funsigned-char --coverage -o bc main.o bc.o scan.o execute.o load.o storage.o util.o global.o warranty.o ../lib/libbc.a -ll
Making all in dc
gcc -DHAVE_CONFIG_H -I. -I.. -I./.. -I./../h -g -O2 -Wall -funsigned-char --coverage -MT dc.o -MD -MP -MF .deps/dc.Tpo -c -o dc.o dc.c
mv -f .deps/dc.Tpo .deps/dc.Po
gcc -DHAVE_CONFIG_H -I. -I.. -I./.. -I./../h -g -O2 -Wall -funsigned-char --coverage -MT misc.o -MD -MP -MF .deps/misc.Tpo -c -o misc.o misc.c
mv -f .deps/misc.Tpo .deps/misc.Po
gcc -DHAVE_CONFIG_H -I. -I.. -I./.. -I./../h -g -O2 -Wall -funsigned-char --coverage -MT eval.o -MD -MP -MF .deps/eval.Tpo -c -o eval.o eval.c
mv -f .deps/eval.Tpo .deps/eval.Po
gcc -DHAVE_CONFIG_H -I. -I.. -I./.. -I./../h -g -O2 -Wall -funsigned-char --coverage -MT stack.o -MD -MP -MF .deps/stack.Tpo -c -o stack.o stack.c
mv -f .deps/stack.Tpo .deps/stack.Po
gcc -DHAVE_CONFIG_H -I. -I.. -I./.. -I./../h -g -O2 -Wall -funsigned-char --coverage -MT array.o -MD -MP -MF .deps/array.Tpo -c -o array.o array.c
mv -f .deps/array.Tpo .deps/array.Po
gcc -DHAVE_CONFIG_H -I. -I.. -I./.. -I./../h -g -O2 -Wall -funsigned-char --coverage -MT numeric.o -MD -MP -MF .deps/numeric.Tpo -c -o numeric.o numeric.c
numeric.c:576:1: warning: a function definition without a
prototype is deprecated in all versions of C and is not supported in C2x
[-Wdeprecated-non-prototype]
576 | out_char (ch)
| ^ 1 warning generated.
mv -f .deps/numeric.Tpo .deps/numeric.Po
gcc -DHAVE_CONFIG_H -I. -I.. -I./.. -I./../h -g -O2 -Wall -funsigned-char --coverage -MT string.o -MD -MP -MF .deps/string.Tpo -c -o string.o string.c
mv -f .deps/string.Tpo .deps/string.Po
gcc -g -O2 -Wall -funsigned-char --coverage -o dc dc.o misc.o eval.o stack.o array.o numeric.o string.o ../lib/libbc.a
Making all in doc
restore=: && backupdir=".am$$" && \
am__cwd=`pwd` && CDPATH="${ZSH_VERSION+.}:" && cd . && \
rm -rf $backupdir && mkdir $backupdir && \
if (makeinfo --no-split --version) >/dev/null 2>&1; then \
for f in bc.info bc.info-[0-9] bc.info-[0-9][0-9] bc.i[0-9] bc.i[0-9][0-9]; do \
if test -f $f; then mv $f $backupdir; restore=mv; else :; fi; \
done; \
else :; fi && \
cd "$am__cwd"; \
if makeinfo --no-split -I . \
-o bc.info bc.texi; \
then \
rc=0; \
CDPATH="${ZSH_VERSION+.}:" && cd .; \
else \
rc=$?; \
CDPATH="${ZSH_VERSION+.}:" && cd . && \
$restore $backupdir/* `echo "./bc.info" | sed 's|[^/]*$||'`; \
fi; \
rm -rf $backupdir; exit $rc
restore=: && backupdir=".am$$" && \
am__cwd=`pwd` && CDPATH="${ZSH_VERSION+.}:" && cd . && \
rm -rf $backupdir && mkdir $backupdir && \
if (makeinfo --no-split --version) >/dev/null 2>&1; then \
for f in dc.info dc.info-[0-9] dc.info-[0-9][0-9] dc.i[0-9] dc.i[0-9][0-9]; do \
if test -f $f; then mv $f $backupdir; restore=mv; else :; fi; \
done; \
else :; fi && \
cd "$am__cwd"; \
if makeinfo --no-split -I . \
-o dc.info dc.texi; \
then \
rc=0; \
CDPATH="${ZSH_VERSION+.}:" && cd .; \
else \
rc=$?; \
CDPATH="${ZSH_VERSION+.}:" && cd . && \
$restore $backupdir/* `echo "./dc.info" | sed 's|[^/]*$||'`; \
fi; \
rm -rf $backupdir; exit $rc
make[3]: Nothing to be done for `all-am'.
bc/bc文件现在应该是可执行的...
!cd bc-1.07.1/bc; echo 2 + 2 | ./bc
4
...你应该能够运行gcov程序来检索覆盖率信息。
!cd bc-1.07.1/bc; gcov main.c
File 'main.c'
Lines executed:52.55% of 137
Creating 'main.c.gcov'
如"覆盖率"章节中所述,文件 bc-1.07.1/bc/main.c.gcov 现在包含了bc.c的覆盖率信息。每一行都带有执行次数的前缀。#####表示零次;-表示不可执行行。
解析bc的 GCOV 文件并创建一个coverage集合,如FunctionCoverageRunner中所示。将其制作成一个ProgramCoverageRunner类,该类将使用源文件列表(bc.c、main.c、load.c)构建,以运行gcov。
完成后,别忘了清理:
!rm -fr bc-1.07.1 bc-1.07.1.tar.gz
练习 3
在这篇博客文章中,American Fuzzy Lop (AFL)的作者,一个非常流行的基于变异的模糊测试工具,讨论了各种变异算子的效率。如上例所示,实现其中的四个并评估它们的效率。
练习 4
当向候选元素列表中添加新元素时,AFL 实际上并不比较覆盖率,而是如果它执行了一个新的分支,就会添加一个元素。使用"覆盖率"章节中的练习中的分支覆盖率,实现这种"分支"策略,并将其与上面的"覆盖率"策略进行比较。
练习 5
设计并实现一个系统,该系统将从网络中收集一组 URL。你能用这些样本实现更高的覆盖率吗?如果你将它们用作进一步变异的初始种群,会怎样?
本项目的内容受Creative Commons Attribution-NonCommercial-ShareAlike 4.0 国际许可协议的许可。作为内容一部分的源代码,以及用于格式化和显示该内容的源代码,受MIT 许可协议的许可。最后修改时间:2024-11-09 17:25:56+01:00。引用 版权信息
如何引用本作品
Andreas Zeller, Rahul Gopinath, Marcel Böhme, Gordon Fraser, and Christian Holler: "基于变异的模糊测试"。在 Andreas Zeller, Rahul Gopinath, Marcel Böhme, Gordon Fraser, and Christian Holler 的"模糊测试书"中。www.fuzzingbook.org/html/MutationFuzzer.html。检索时间:2024-11-09 17:25:56+01:00。
@incollection{fuzzingbook2024:MutationFuzzer,
author = {Andreas Zeller and Rahul Gopinath and Marcel B{\"o}hme and Gordon Fraser and Christian Holler},
booktitle = {The Fuzzing Book},
title = {Mutation-Based Fuzzing},
year = {2024},
publisher = {CISPA Helmholtz Center for Information Security},
howpublished = {\url{https://www.fuzzingbook.org/html/MutationFuzzer.html}},
note = {Retrieved 2024-11-09 17:25:56+01:00},
url = {https://www.fuzzingbook.org/html/MutationFuzzer.html},
urldate = {2024-11-09 17:25:56+01:00}
}


浙公网安备 33010602011771号