freeBuf
主站

分类

漏洞 工具 极客 Web安全 系统安全 网络安全 无线安全 设备/客户端安全 数据安全 安全管理 企业安全 工控安全

特色

头条 人物志 活动 视频 观点 招聘 报告 资讯 区块链安全 标准与合规 容器安全 公开课

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

Fuzzbook系列(4):代码覆盖率计算
2021-03-19 15:09:32

上一章中,我们介绍了基本的模糊测试—生成随机输入以测试程序。那么我们该如何衡量这些测试的有效性?一种方法是检查发现的错误的数量(和严重程度),但是如果错误很少,我们需要一个别的方法来从数值上衡量有效性。 在本章中,我们将介绍代码覆盖率的概念,它可以衡量在测试运行期间实际执行程序哪些部分,对于尝试覆盖尽可能多的代码的测试样例生成器,覆盖率这一指标也至关重要。

先决条件

  • 读者需要对程序的执行方式有所了解

  • 读者需要先行学习过基本的模糊测试

CGI解码器

我们首先了解一个简单的Python函数,该函数对CGI编码的字符串进行解码。URL(即Web地址)中使用CGI编码来编码URL中无效的字符,例如空格和某些标点符号:

  • 将空格全都替换为 “+”

  • 将其它的无效字符统一替换为“%xx”,其中XX是对应的无效字符十六进制等效项

例如在CGI的编码中,字符串Hello, world!将被替换为Hello%2c+world%21,其中2c21是十六进制的,!

目标函数cgi_decode()的作用就是将其解码回其原始形式。该部分代码复制了[Pezzè et al, 2008.]中的成果。(它的里面其实包含了几个错误,但我们之后再说)

def cgi_decode(s):
    """Decode the CGI-encoded string `s`:
       * replace "+" by " "
       * replace "%xx" by the character with hex number xx.
       Return the decoded string.  Raise `ValueError` for invalid inputs."""

    # Mapping of hex digits to their integer values
    hex_values = {
        '0': 0, '1': 1, '2': 2, '3': 3, '4': 4,
        '5': 5, '6': 6, '7': 7, '8': 8, '9': 9,
        'a': 10, 'b': 11, 'c': 12, 'd': 13, 'e': 14, 'f': 15,
        'A': 10, 'B': 11, 'C': 12, 'D': 13, 'E': 14, 'F': 15,
    }

    t = ""
    i = 0
    while i < len(s):
        c = s[i]
        if c == '+':
            t += ' '
        elif c == '%':
            digit_high, digit_low = s[i + 1], s[i + 2]
            i += 2
            if digit_high in hex_values and digit_low in hex_values:
                v = hex_values[digit_high] * 16 + hex_values[digit_low]
                t += chr(v)
            else:
                raise ValueError("Invalid encoding")
        else:
            t += c
        i += 1
    return t

这里有一个例子,让我们看看它是如何运行的:

cgi_decode("Hello+world")
'Hello world'

但如果我们要系统地测试cgi_decode(),我们该如何进行?

相关的文献区分了两种测试的方式:黑盒测试白盒测试。

黑盒测试

黑盒测试的思想是在标准规范中进行测试,而不知道内部细节。因此在上述情况下,我们必须通过指定的功能对 cgi_decode() 进行测试并记录结果。

  • 测试是否正确更换'+'

  • 测试是否正确更换“%xx”

  • 测试是否正确的替换其他字符

  • 测试能否识别非法输入

以下是涵盖这四个功能的四个测试(assert断言),我们可以看到它全部通过了测试。

assert cgi_decode('+') == ' '
assert cgi_decode('%20') == ' '
assert cgi_decode('abc') == 'abc'

try:
    cgi_decode('%?a')
    assert False
except ValueError:
    pass

黑盒测试的优点是可以直接发现 指定行为中的错误。它独立于被测试程序本身,因此即使在程序完全实现之前也可以创建测试,看进展如何。不利的一面是,待测试程序包含的行为通常比指定的行为覆盖更多的分支,因此仅基于规范的测试通常不会覆盖所有实现细节。

白盒测试

与黑盒测试相反,白盒测试知道程序的内部细节,其测试样例是根据程序本身生成的。白盒测试与代码结构特征的概念紧密相关,例如,如果在测试过程中未执行代码中的某一个语句,则意味着该语句中的错误也无法触发。因此,白盒测试引入了许多覆盖的标准,在可以判定该测试覆盖完全之前,必须满足这些基本标准。最常用的覆盖标准是:

  • 语句覆盖范围——代码中的每个语句必须至少由一个测试输入执行

  • 分支覆盖范围——代码中的每个分支都必须至少有一个测试输入(这意味着每个ifwhile语句都要一次结果为真,一次结果为假,才能覆盖全面)

除此之外还有更多的覆盖标准,包括采用的分支顺序,采用的循环迭代(零次,一次,多次),变量定义和用法之间的数据流等等,[ Pezzèet al,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参数访问该行信息。

coverage = []
def traceit(frame, event, arg):
    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
def cgi_decode_traced(s):
    global coverage
    coverage = []
    sys.settrace(traceit)  # Turn on
    cgi_decode(s)
    sys.settrace(None)    # Turn off

当我们计算cgi_decode("a+b")时,可以看到执行过程。

cgi_decode_traced("a+b")
print(coverage)
[9, 10, 11, 12, 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
cgi_decode_code = inspect.getsource(cgi_decode)

cgi_decode_code是包含源代码的字符串。我们可以使用Python语法高亮显示它:

from bookutils import print_content, print_file
print_content(cgi_decode_code[:300] + "...", ".py")
def cgi_decode(s):
    """Decode the CGI-encoded string `s`:
       * replace "+" by " "
       * replace "%xx" by the character with hex number xx.
       Return the decoded string.  Raise `ValueError` for invalid inputs."""

    # Mapping of hex digits to their integer values
    hex_values = {
  ...

使用splitlines(),我们将代码分成几行,并按行号索引。

cgi_decode_lines = [""] + cgi_decode_code.splitlines()

cgi_decode_lines[L]是源代码的L行。

cgi_decode_lines [ 1 ]
'def cgi_decode(s):'

我们看到执行的第一行line(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, 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')
#  1  def cgi_decode(s):
#  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

我们看到许多行(尤其是注释)尚未执行,仅仅是因为它们不是可执行的。然而,我们也看到,在该行if c == '%'没有执行到。如果这"a+b"是到目前为止我们唯一的测试用例,那么现在缺少的覆盖范围将指导我们创建另一个实际覆盖这些行的测试用例。

覆盖率类

之后我们会多次引用测量代码覆盖率的功能,使用全局coverage变量的实现总是有点麻烦。因此,我们创建一个类,可以帮助我们轻松地测量覆盖率。

这里获得覆盖率的关键思想是利用Python的with语句。一般形式为

with OBJECT [as VARIABLE]:
    BODY

BODYOBJECT定义的方式执行(并存储在VARIABLE中)。有趣的是,在BODY的开始和结尾处,特殊方法OBJECT.__enter__()OBJECT.__exit__()被自动调用,即使BODY引发异常。这使我们能够定义一个Coverage对象,在该对象中Coverage.__enter__()可以自动打开跟踪并且Coverage.__exit__()可以自动关闭跟踪。之后,我们就可以用这种方法来访问coverage。这是在使用过程中的样子:

with Coverage() as cov:
    function_to_be_traced()
c = cov.coverage()

在此,跟踪function_to_be_traced()with块期间自动打开,并在块之后再次关闭。之后,我们可以访问执行的覆盖路径。

以下是实现的全部细节,无需追求每一个细节都懂,只要知道该如何使用他们就够了。

class Coverage(object):
    # Trace function
    def traceit(self, frame, event, arg):
        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
            self._trace.append((function_name, lineno))

        return self.traceit

    def __init__(self):
        self._trace = []

    # Start of `with` block
    def __enter__(self):
        self.original_trace_function = sys.gettrace()
        sys.settrace(self.traceit)
        return self

    # End of `with` block
    def __exit__(self, exc_type, exc_value, tb):
        sys.settrace(self.original_trace_function)

    def trace(self):
        """The list of executed lines, as (function_name, line_number) pairs"""
        return self._trace

    def coverage(self):
        """The set of executed lines, as (function_name, line_number) pairs"""
        return set(self.trace())

来试着用一次看看:

with Coverage() as cov:
    cgi_decode("a+b")

print(cov.coverage())
{('cgi_decode', 17), ('cgi_decode', 18), ('cgi_decode', 19), ('cgi_decode', 30), ('cgi_decode', 9), ('cgi_decode', 20), ('cgi_decode', 31), ('cgi_decode', 10), ('cgi_decode', 21), ('cgi_decode', 32), ('cgi_decode', 11), ('__exit__', 25), ('cgi_decode', 12), ('cgi_decode', 15), ('cgi_decode', 16)}

如您所见,Coverage()该类不仅跟踪执行的行,而且跟踪函数名。如果被测试对象是一个调用多个不同文件的程序,这将很有用。

比较覆盖率

我们用coverage表示由已执行数组成的组,因此也可以在此之上执行一些操作。例如,我们可以找出某个测试用例涵盖哪些行,而某些测试用例则无法覆盖:

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为我们可以实现的最大覆盖范围。(在这里,我们通过执行已经存在的“好”测试用例来执行此操作。在实践中,我们将静态分析代码结构,这将在符号执行章节中介绍)

import bookutils
with Coverage() as cov_max:
    cgi_decode('+')
    cgi_decode('%20')
    cgi_decode('abc')
    try:
        cgi_decode('%?a')
    except:
        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格式引发的异常。

with Coverage() as cov_fuzz:
    try:
        cgi_decode(sample)
    except:
        pass
cov_fuzz.coverage()
{('__exit__', 25),
 ('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]是第一个随机输入执行之后覆盖的总行数,cumulative_coverage[1]是1-2个随机输入之后覆盖的总行数 ,依此类推。

trials = 100
def population_coverage(population, function):
    cumulative_coverage = []
    all_coverage = 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():
    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 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')

image-20210310110452495

当然,这只是一次运行的结果。让我们多重复几次,并绘制它的平均值图。

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')

image-20210310110658992

我们可以看到,平均后大概输入在40—60次左右我们就能够得到完整的覆盖率范围。

从其他程序获取覆盖率

可惜世界上不只有python一种程序。对于其他程序,获取覆盖率的问题也无处不在,几乎每种语言都有测量覆盖率的功能,我们来看一下如何获取C语言程序的覆盖率。

我们使用C语言再次实现cgi_decode的功能,只是这次作为要从命令行执行的程序:

$ ./cgi_decode 'Hello+World'
Hello World

以下是C语言代码,和python类似,先从include开始。

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源代码:

with open("cgi_decode.c", "w") as f:
    f.write(cgi_c_code)

在这里,我们的C代码突出显示了其语法:

from 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具有覆盖率信息的新文件。

!gcov cgi_decode.c
File 'cgi_decode.c'
Lines executed:92.50% of 40
cgi_decode.c: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', 17),
 ('cgi_decode.c', 38),
 ('cgi_decode.c', 48),
 ('cgi_decode.c', 41),
 ('cgi_decode.c', 29)]

有了这个设置,我们现在可以执行与Python程序相同的coverage计算。

总结

  • 覆盖率指标是一种简单且完全自动化的方法,用于估算在测试运行期间实际执行了多少程序功能。

  • 存在许多覆盖率指标,最重要的是语句覆盖率和分支覆盖率。

  • 在Python中,执行期间访问程序状态非常容易,包括当前执行的代码。

# 模糊测试 # fuzz # 二进制
本文为 独立观点,未经允许不得转载,授权请联系FreeBuf客服小蜜蜂,微信:freebee2022
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者