模糊测试之书-十八-

模糊测试之书(十八)

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

译者:飞龙

协议:CC BY-NC-SA 4.0

何时停止模糊测试

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

在前面的章节中,我们讨论了几种模糊测试技术。知道做什么很重要,但知道何时停止做事情也同样重要。在本章中,我们将学习何时停止模糊测试——并使用一个突出的例子来说明这一点:在第二次世界大战中,纳粹德国海军使用的用于加密通信的Enigma机器,以及艾伦·图灵和 I.J. Good 如何使用模糊测试技术来破解海军 Enigma 机器的密码。

图灵不仅发展了计算机科学的基础——图灵机,他还与他的助手 I.J. Good 一起发明了用于从未发生过的事件的概率估计器。我们展示了 Good-Turing 估计器如何用于量化没有发现漏洞的模糊测试活动的剩余风险。这意味着我们展示了如何估计在整个模糊测试活动中没有观察到漏洞时发现漏洞的概率。

我们讨论了加快基于覆盖率的模糊器的方法,并介绍了一系列估计和外推方法来评估和预测模糊测试的进度和剩余风险。

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

先决条件

  • 覆盖这一章节讨论了如何使用执行测试输入的覆盖信息来指导基于覆盖率的突变灰盒模糊器

  • 一定的统计学知识是有帮助的。

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 Dict, List 

Enigma Machine

1938 年秋天。图灵刚刚在普林斯顿大学完成他的博士学位,展示了计算的极限并为计算机科学理论奠定了基础。纳粹德国正在重新武装。它重新占领了莱茵兰,违反了凡尔赛条约,并吞并了奥地利。它刚刚吞并了捷克斯洛伐克的苏台德地区,并开始准备接管捷克斯洛伐克的其余部分,尽管在慕尼黑刚刚签署了一项协议。

同时,英国情报机构正在增强他们破解德国用于军事和海军信息通信的加密信息的能力。德国人使用Enigma 机器进行加密。Enigma 机器使用一系列机电式转子加密机来保护军事通信。以下是 Enigma 机器的图片:

Enigma Machine

当图灵加入英国布莱切利公园时,波兰情报机构已经逆向工程了 Enigma 机器的逻辑结构,并建造了一台名为Bomba的解密机(可能是因为它们发出的滴答声)。Bomba 可以同时模拟六个 Enigma 机器,并尝试不同的解密密钥,直到密码被破解。波兰的 Bomba 可能是非常早期的模糊器

图灵承担了破解海军恩尼格玛机密码的任务,这些密码因其难以破解而闻名。海军恩尼格玛机作为其加密密钥的一部分,使用了一个称为三元组的三个字母序列。这些三元组是从一本书中选出的,这本书称为《肯尼格鲁本》,其中包含了随机顺序的所有三元组。

《肯尼格鲁本》

让我们从《肯尼格鲁本》(K-Book)开始。

我们将使用以下 Python 函数。

  • random.shuffle(elements) - 打乱elements并随机排列项目。

  • random.choices(elements, weights) - 从elements中随机选择一个项目。如果一个元素具有两倍的weight,那么它被选中的概率是两倍。

  • log(a) - 返回a的自然对数。

  • a ** b - 表示ab次幂(也称为幂运算符

import [string](https://docs.python.org/3/library/string.html) 
import [numpy](https://numpy.org/)
from [numpy](https://numpy.org/) import log 
import [random](https://docs.python.org/3/library/random.html) 

我们首先创建三元组的集合:

letters = list(string.ascii_letters[26:])  # upper-case characters
trigrams = [str(a + b + c) for a in letters for b in letters for c in letters] 
random.shuffle(trigrams) 
trigrams[:10] 
['TJK', 'NWV', 'LBM', 'AZC', 'GZP', 'ADE', 'DNO', 'OQL', 'FGK', 'IPT']

这些现在将进入《肯尼格鲁本》。然而,观察到某些三元组比其他三元组更有可能被选中。例如,任何页面左上角的三元组,或者第一页或最后几页上的三元组,比书中或页面中间的某个地方的三元组更有可能。我们通过为每个三元组分配一个概率来反映这种分布差异,使用在概率模糊测试中介绍的贝福特定律。

回想一下,本福特定律将第i位数字分配给概率\(\log_{10}\left(1 + \frac{1}{i}\right)\),其中以 10 为底是因为有 10 个数字\(i\in [0,9]\)。然而,本福特定律适用于任意数量的“数字”。因此,我们将第i个三元组分配给概率\(\log_b\left(1 + \frac{1}{i}\right)\),其中底数\(b\)是所有可能的三元组的数量\(b=26³\)

k_book = {}  # Kenngruppenbuch

for i in range(1, len(trigrams) + 1):
    trigram = trigrams[i - 1]
    # choose weights according to Benford's law
    k_book[trigram] = log(1 + 1 / i) / log(26**3 + 1) 

这里是一个来自《肯尼格鲁本》的随机三元组:

random_trigram = random.choices(list(k_book.keys()), weights=list(k_book.values()))[0]
random_trigram 
'PSK'

这是它的概率:

k_book[random_trigram] 
np.float64(0.0008284144853894445)

模糊恩尼格玛

在以下内容中,我们将介绍基于 K-Book 中的三元组的海军恩尼格玛的极其简化的实现。当然,实际恩尼格玛机的加密机制要复杂得多,值得进行更详细的研究。我们鼓励感兴趣的读者继续阅读背景部分中列出的进一步阅读材料。

布莱切利公园的工作人员只能检查一个编码消息是否使用了一个(猜测的)三元组。我们的实现naval_enigma()接受一个message和一个key(即猜测的三元组)。如果给定的密钥与消息的(先前计算出的)密钥匹配,naval_enigma()返回True

from Fuzzer import RandomFuzzer
from Fuzzer import Runner 
class EnigmaMachine(Runner):
    def __init__(self, k_book):
        self.k_book = k_book
        self.reset()

    def reset(self):
  """Resets the key register"""
        self.msg2key = {}
        self.cur_msg = ""

    def internal_msg2key(self, message):
  """Internal helper method. 
 Returns the trigram for an encoded message."""
        if message not in self.msg2key:
            # Simulating how an officer chooses a key from the Kenngruppenbuch
            # to encode the message.
            self.msg2key[message] = \
                random.choices(list(self.k_book.keys()),
                               weights=list(self.k_book.values()))[0]
        trigram = self.msg2key[message]
        return trigram

    def naval_enigma(self, message, key):
  """Returns true if 'message' is encoded with 'key'"""
        if key == self.internal_msg2key(message):
            return True
        else:
            return False 

为了“模糊”naval_enigma(),我们的任务将是找到一个与给定(加密)消息匹配的密钥。由于密钥只有三个字符,我们有很大的机会在不到一秒的时间内完成这项任务。(当然,更长的密钥通过随机模糊测试将更难找到。)

class EnigmaMachine(EnigmaMachine):
    def run(self, tri):
  """PASS if cur_msg is encoded with trigram tri"""
        if self.naval_enigma(self.cur_msg, tri):
            outcome = self.PASS
        else:
            outcome = self.FAIL

        return (tri, outcome) 

现在我们可以使用EnigmaMachine来检查某个消息是否使用特定的三元组进行编码。

enigma = EnigmaMachine(k_book)
enigma.cur_msg = "BrEaK mE. L0Lzz"
enigma.run("AAA") 
('AAA', 'FAIL')

破解编码消息的最简单方法是暴力破解。假设在 Bletchley park,他们会尝试随机的三元组,直到一条消息被破解。

class BletchleyPark:
    def __init__(self, enigma):
        self.enigma = enigma
        self.enigma.reset()
        self.enigma_fuzzer = RandomFuzzer(
            min_length=3,
            max_length=3,
            char_start=65,
            char_range=26)

    def break_message(self, message):
  """Returning the trigram for an encoded message"""
        self.enigma.cur_msg = message
        while True:
            (trigram, outcome) = self.enigma_fuzzer.run(self.enigma)
            if outcome == self.enigma.PASS:
                break
        return trigram 

使用这种暴力破解方法,Bletchley park 需要多长时间才能找到密钥?

from Timer import Timer 
enigma = EnigmaMachine(k_book)
bletchley = BletchleyPark(enigma)

with Timer() as t:
    trigram = bletchley.break_message("BrEaK mE. L0Lzz") 

这是当前消息的密钥:

trigram 
'XQC'

并且,这并没有花费很长时间:

'%f seconds' % t.elapsed_time() 
'0.079044 seconds'

'Bletchley cracks about %d messages per second' % (1/t.elapsed_time()) 
'Bletchley cracks about 12 messages per second'

图灵的观察

好的,让我们破解几条消息并计算每个三元组被观察到的次数。

from [collections](https://docs.python.org/3/library/collections.html) import defaultdict 
n = 100  # messages to crack 
observed: Dict[str, int] = defaultdict(int)
for msg in range(0, n):
    trigram = bletchley.break_message(msg)
    observed[trigram] += 1

# list of trigrams that have been observed
counts = [k for k, v in observed.items() if int(v) > 0]

t_trigrams = len(k_book)
o_trigrams = len(counts) 
"After cracking %d messages, we observed %d out of %d trigrams." % (
    n, o_trigrams, t_trigrams) 
'After cracking 100 messages, we observed 72 out of 17576 trigrams.'

singletons = len([k for k, v in observed.items() if int(v) == 1]) 
"From the %d observed trigrams, %d were observed only once." % (
    o_trigrams, singletons) 
'From the 72 observed trigrams, 63 were observed only once.'

给定一组之前使用的条目样本,图灵想要*估计当前未知条目之前已被使用的可能性,并且进一步估计之前使用条目的概率分布。这导致了缺失质量估计器和样本中发生项集的真实概率质量的估计的发展。古德在战争期间与图灵合作,并在图灵的许可下,于 1953 年发表了这些估计器偏差的分析。

假设我们在找到 n=100 条消息的密钥之后,已经观察到三元组"ABC"恰好\(X_\text{ABC}=10\)次。那么"ABC"是下一条消息密钥的概率\(p_\text{ABC}\)是多少?从经验上讲,我们会估计\(\hat p_\text{ABC}=\frac{X_\text{ABC}}{n}=0.1\)。我们可以推导出所有其他观察到的三元组的经验估计。然而,很快就会很明显,整个概率质量都分布在观察到的三元组上。这留给未观察到的三元组没有质量,即发现新三元组的概率。这被称为缺失的概率质量或发现概率。

图灵和古德推导出了发现概率 \(p_0\) 的估计,即发现一个未观察到的三元组的概率,作为观察到的三元组数量\(f_1\)与破解的总消息数量\(n\)的比值:$$ p_0 = \frac{f_1}{n} $$ 其中\(f_1\)是单例的数量,\(n\)是破解的消息数量。

让我们稍微探讨一下这个想法。我们将扩展BletchleyPark以破解n条消息,并记录随着破解消息数量的增加观察到的三元组数量。

class BletchleyPark(BletchleyPark):
    def break_message(self, message):
  """Returning the trigram for an encoded message"""
        # For the following experiment, we want to make it practical
        #   to break a large number of messages. So, we remove the
        #   loop and just return the trigram for a message.
        #
        # enigma.cur_msg = message
        # while True:
        #     (trigram, outcome) = self.enigma_fuzzer.run(self.enigma)
        #     if outcome == self.enigma.PASS:
        #         break
        trigram = enigma.internal_msg2key(message)
        return trigram

    def break_n_messages(self, n):
  """Returns how often each trigram has been observed, 
 and #trigrams discovered for each message."""
        observed = defaultdict(int)
        timeseries = [0] * n

        # Crack n messages and record #trigrams observed as #messages increases
        cur_observed = 0
        for cur_msg in range(0, n):
            trigram = self.break_message(cur_msg)

            observed[trigram] += 1
            if (observed[trigram] == 1):
                cur_observed += 1
            timeseries[cur_msg] = cur_observed

        return (observed, timeseries) 

让我们破解 2000 条消息并计算 GT 估计。

n = 2000        # messages to crack 
bletchley = BletchleyPark(enigma)
(observed, timeseries) = bletchley.break_n_messages(n) 

让我们确定下一个三元组之前未被观察到的 Good-Turing 概率估计:

singletons = len([k for k, v in observed.items() if int(v) == 1])
gt = singletons / n
gt 
0.401

我们可以通过经验验证 Good-Turing 估计,并计算下一个三元组之前未被观察到的经验概率。为此,我们重复以下实验repeats=1000次,报告平均值:如果下一条消息是一个新的三元组,则返回 1,否则返回 0。注意,在这里,我们不记录新发现的元组为已观察到的。

repeats = 1000  # experiment repetitions 
newly_discovered = 0
for cur_msg in range(n, n + repeats):
    trigram = bletchley.break_message(cur_msg)
    if(observed[trigram] == 0):
        newly_discovered += 1

newly_discovered / repeats 
0.412

看起来相当准确,对吧?估计值之间的差异相对较小,可能低于 0.03。然而,Good-Turing 估计并不需要像经验估计那样多的计算资源。与经验估计不同,Good-Turing 估计可以在战役期间进行计算。与经验估计不同,Good-Turing 估计不需要额外的、冗余的重复。

事实上,Good-Turing (GT) 估计器通常在任意分布的估计中表现接近最佳估计器(在这里尝试!)。当然,发现 的概念并不仅限于三元组。GT 估计器也用于自然语言的研究,以估计我们遇到下一个单词时,我们没有听过或阅读过该单词的可能性。GT 估计器在生态学中用于估计在我们对地球上的所有物种进行编目时发现新、未见物种的可能性。稍后,我们将看到它如何被用来估计在尚未观察到的情况下发现漏洞的概率(即残余风险)。

阿兰·图灵对补集 \((1-GT)\) 感兴趣,它给出了所有消息中,英国人已经观察到用于解密的三元组的比例。因此,补集也被称为样本覆盖率。样本覆盖率 量化了在只有少量已解密消息的情况下,我们对所有消息解密了解的程度。

下一个消息可以使用先前发现的三元组进行解密的概率是:

1 - gt 
0.599

GT-估计的倒数(1/GT)是使用先前观察到的三元组解密预期消息数量的最大似然估计。在我们的设置中,在需要发现新的三元组来解密消息之前,我们可以期望重用先前三元组的消息数量是:

1 / gt 
2.4937655860349124

但为什么 GT 是如此准确的?直观上,尽管进行了大量的采样工作(即破解 \(n\) 条消息),仍有 \(f_1\) 个三元组只被观察到一次。我们可以说这样的“单例”是非常罕见的三元组。因此,下一个消息使用这种罕见但已观察到的三元组进行编码的概率,为下一个消息使用明显更罕见、未观察到的三元组进行编码的概率提供了一个良好的上限。自从图林 80 年前的观察以来,围绕稀有的、已观察到的“物种”是未观察到的物种的良好预测者的假设,已经发展出整个统计理论。

让我们来看看罕见三元组的分布。

%matplotlib inline 
import [matplotlib.pyplot](https://matplotlib.org/) as plt 
frequencies = [v for k, v in observed.items() if int(v) > 0]
frequencies.sort(reverse=True)
# Uncomment to see how often each discovered trigram has been observed
# print(frequencies)

# frequency of rare trigrams
plt.figure(num=None, figsize=(12, 4), dpi=80, facecolor='w', edgecolor='k')
plt.subplot(1, 2, 1)
plt.hist(frequencies, range=[1, 21], bins=numpy.arange(1, 21) - 0.5)
plt.xticks(range(1, 21))
plt.xlabel('# of occurrences (e.g., 1 represents singleton trigrams)')
plt.ylabel('Frequency of occurances')
plt.title('Figure 1\. Frequency of Rare Trigrams')

# trigram discovery over time
plt.subplot(1, 2, 2)
plt.plot(timeseries)
plt.xlabel('# of messages cracked')
plt.ylabel('# of trigrams discovered')
plt.title('Figure 2\. Trigram Discovery Over Time'); 

图片

# Statistics for most and least often observed trigrams
singletons = len([v for k, v in observed.items() if int(v) == 1])
total = len(frequencies)

print("%3d of %3d trigrams (%.3f%%) have been observed   1 time (i.e., are singleton trigrams)."
      % (singletons, total, singletons * 100 / total))

print("%3d of %3d trigrams ( %.3f%%) have been observed %d times."
      % (1, total, 1 / total, frequencies[0])) 
802 of 1009 trigrams (79.485%) have been observed   1 time (i.e., are singleton trigrams).
  1 of 1009 trigrams ( 0.001%) have been observed 152 times.

大多数三角词只被观察到一次,如图 1(左)所示。换句话说,大多数观察到的三角词是“罕见”的单个词。在图 2(右)中,我们可以看到发现正处于全速进行。轨迹似乎几乎是线性的。然而,由于三角词的数量是有限的(26³ = 17,576),三角词的发现速度将放缓,并最终接近一个渐近线(三角词的总数)。

提升 BletchleyPark 的性能

一些三角词被观察到的频率非常高。我们称这些为“丰富”的三角词。

print("Trigram : Frequency")
for trigram in sorted(observed, key=observed.get, reverse=True):
    if observed[trigram] > 10:
        print(" %s : %d" % (trigram, observed[trigram])) 
Trigram : Frequency
    TJK : 152
    LBM : 69
    NWV : 64
    AZC : 43
    GZP : 41
    ADE : 37
    DNO : 27
    OQL : 26
    TCO : 20
    BDA : 19
    ARO : 18
    IPT : 16
    FGK : 16
    MSV : 15
    ONO : 15
    EOR : 13
    JTV : 11
    IBT : 11
    PWN : 11

我们将通过首先尝试丰富的三角词来加快密码破解的速度。

首先,我们将找出在 Bledgley park 使用最大尝试次数的情况下,现有暴力破解策略可以破解多少信息。我们还将跟踪随时间破解的信息数量(timeseries)。

class BletchleyPark(BletchleyPark):
    def __init__(self, enigma):
        super().__init__(enigma)
        self.cur_attempts = 0
        self.cur_observed = 0
        self.observed = defaultdict(int)
        self.timeseries = [None] * max_attempts * 2

    def break_message(self, message):
  """Returns the trigram for an encoded message, and
 track #trigrams observed as #attempts increases."""
        self.enigma.cur_msg = message
        while True:
            self.cur_attempts += 1                                 # NEW
            (trigram, outcome) = self.enigma_fuzzer.run(self.enigma)
            self.timeseries[self.cur_attempts] = self.cur_observed # NEW
            if outcome == self.enigma.PASS: 
                break
        return trigram

    def break_max_attempts(self, max_attempts):
  """Returns #messages successfully cracked after a given #attempts."""
        cur_msg = 0
        n_messages = 0

        while True:
            trigram = self.break_message(cur_msg)

            # stop when reaching max_attempts
            if self.cur_attempts >= max_attempts:
                break

            # update observed trigrams
            n_messages += 1
            self.observed[trigram] += 1
            if (self.observed[trigram] == 1):
                self.cur_observed += 1
                self.timeseries[self.cur_attempts] = self.cur_observed
            cur_msg += 1

        return n_messages 

original是在 100k 次尝试下,通过暴力破解策略破解的信息数量。我们能打败这个记录吗?

max_attempts = 100000 
bletchley = BletchleyPark(enigma)
original = bletchley.break_max_attempts(max_attempts)
original 
3

现在,我们将通过首先尝试之前观察到的最频繁的三角词来创建一个增强策略。

class BoostedBletchleyPark(BletchleyPark):
    def __init__(self, enigma, prior):
        super().__init__(enigma)
        self.prior = prior

    def break_message(self, message):
  """Returns the trigram for an encoded message, and
 track #trigrams observed as #attempts increases."""
        self.enigma.cur_msg = message

        # boost cracking by trying observed trigrams first
        for trigram in sorted(self.prior, key=self.prior.get, reverse=True):
            self.cur_attempts += 1
            (_, outcome) = self.enigma.run(trigram)
            self.timeseries[self.cur_attempts] = self.cur_observed
            if outcome == self.enigma.PASS:
                return trigram

        # else fall back to normal cracking
        return super().break_message(message) 

boosted是通过增强策略破解的信息数量。

boostedBletchley = BoostedBletchleyPark(enigma, prior=observed)
boosted = boostedBletchley.break_max_attempts(max_attempts)
boosted 
23

我们看到,增强技术破解了大量的信息。记录每个三角词作为密钥被使用的频率并按其出现的顺序尝试它们是值得的。

试试看出于实际原因,我们使用大量之前的观察作为先验(boostedBletchley.prior = observed)。你可以尝试修改代码,使策略使用在活动期间观察到的三角词频率(self.observed)来增强活动。你需要增加max_attempts并等待一段时间。

让我们比较随时间发现的三角词数量。

# print plots
line_old, = plt.plot(bletchley.timeseries, label="Bruteforce Strategy")
line_new, = plt.plot(boostedBletchley.timeseries, label="Boosted Strategy")
plt.legend(handles=[line_old, line_new])
plt.xlabel('# of cracking attempts')
plt.ylabel('# of trigrams discovered')
plt.title('Trigram Discovery Over Time'); 

我们看到,增强模糊器始终优于随机模糊器。

估计路径发现概率

那么,图灵对海军恩尼格玛机的观察与模糊化任意程序有什么关系?图灵的助手 I.J. Good 在《生物统计学》杂志上扩展并发表了图灵关于估计程序的工作,这是一本至今仍存在的理论生物统计学期刊。Good 没有谈论三角词。相反,他称它们为“物种”。因此,GT 估计器被提出,以估计在给定现有个体样本(每个个体恰好属于一个物种)的情况下发现新物种的可能性。

现在,我们也可以将程序输入与物种关联起来。例如,我们可以定义输入执行的路径为该输入的物种。这将使我们能够估计模糊化发现新路径的概率。稍后,我们将看到这种发现概率估计也估计了在尚未看到的情况下发现漏洞的可能性(残余风险)。

让我们这样做。我们通过计算输入执行的语句集合的哈希-id 来识别输入的种类。在覆盖率章节中,我们学习了关于覆盖率类的内容,该类收集执行 Python 函数的覆盖率信息。例如,介绍了函数[cgi_decode()](Coverage.html#A-CGI-Decoder)。函数cgi_decode()接受为网站 URL 编码的字符串,并将其解码回原始形式。

这里是cgi_decode()的功能以及覆盖率是如何计算的。

from Coverage import Coverage, cgi_decode 
encoded = "Hello%2c+world%21"
with Coverage() as cov:
    decoded = cgi_decode(encoded) 
decoded 
'Hello, world!'

print(cov.coverage()); 
{('cgi_decode', 18), ('cgi_decode', 24), ('cgi_decode', 27), ('cgi_decode', 33), ('cgi_decode', 30), ('cgi_decode', 39), ('cgi_decode', 17), ('cgi_decode', 20), ('cgi_decode', 26), ('cgi_decode', 23), ('cgi_decode', 29), ('cgi_decode', 32), ('cgi_decode', 38), ('cgi_decode', 19), ('cgi_decode', 16), ('cgi_decode', 25), ('cgi_decode', 31), ('cgi_decode', 28), ('cgi_decode', 34), ('cgi_decode', 40)}

跟踪覆盖率

首先,我们将介绍执行跟踪的概念,它是输入执行路径的粗略抽象。与路径的定义相比,跟踪忽略了语句执行的顺序或每个语句被执行的频率。

  • pickle.dumps() - 通过从对象的所有信息生成字节数组来序列化对象

  • hashlib.md5() - 从字节数组生成 128 位哈希值

import [pickle](https://docs.python.org/3/library/pickle.html)
import [hashlib](https://docs.python.org/3/library/hashlib.html) 
def getTraceHash(cov):
    pickledCov = pickle.dumps(cov.coverage())
    hashedCov = hashlib.md5(pickledCov).hexdigest()
    return hashedCov 

记得我们海军恩尼格玛机的模型吗?每条信息必须使用恰好一个三元组进行解密,而多条信息可能由同一个三元组解密。同样,我们需要每个输入产生恰好一个跟踪哈希,而多个输入可以产生相同的跟踪哈希。

让我们看看这在我们getTraceHash()函数中是否成立。

inp1 = "a+b"
inp2 = "a+b+c"
inp3 = "abc"

with Coverage() as cov1:
    cgi_decode(inp1)
with Coverage() as cov2:
    cgi_decode(inp2)
with Coverage() as cov3:
    cgi_decode(inp3) 

输入inp1inp2执行了相同的语句:

inp1, inp2 
('a+b', 'a+b+c')

cov1.coverage() - cov2.coverage() 
set()

两个覆盖率集之间的差异为空。因此,跟踪哈希应该相同:

getTraceHash(cov1) 
'2b4ac7d0fe0c21a377a594f1a3ec1be2'

getTraceHash(cov2) 
'2b4ac7d0fe0c21a377a594f1a3ec1be2'

assert getTraceHash(cov1) == getTraceHash(cov2) 

相比之下,输入inp1inp3执行了不同的语句:

inp1, inp3 
('a+b', 'abc')

cov1.coverage() - cov3.coverage() 
{('cgi_decode', 28)}

因此,跟踪哈希也应该不同:

getTraceHash(cov1) 
'2b4ac7d0fe0c21a377a594f1a3ec1be2'

getTraceHash(cov3) 
'17f0b5cb3f5ca871198dc25635d631f9'

assert getTraceHash(cov1) != getTraceHash(cov3) 

随时间测量跟踪覆盖率

为了测量执行一个population模糊输入的function的跟踪覆盖率,我们稍微修改了来自覆盖率章节的population_coverage()函数。

def population_trace_coverage(population, function):
    cumulative_coverage = []
    all_coverage = set()
    cumulative_singletons = []
    cumulative_doubletons = []
    singletons = set()
    doubletons = set()

    for s in population:
        with Coverage() as cov:
            try:
                function(s)
            except BaseException:
                pass
        cur_coverage = set([getTraceHash(cov)])

        # singletons and doubletons -- we will need them later
        doubletons -= cur_coverage
        doubletons |= singletons & cur_coverage
        singletons -= cur_coverage
        singletons |= cur_coverage - (cur_coverage & all_coverage)
        cumulative_singletons.append(len(singletons))
        cumulative_doubletons.append(len(doubletons))

        # all and cumulative coverage
        all_coverage |= cur_coverage
        cumulative_coverage.append(len(all_coverage))

    return all_coverage, cumulative_coverage, cumulative_singletons, cumulative_doubletons 

让我们看看我们的新函数是否真的只包含针对cgi_decode的三个输入中的两个跟踪的覆盖率信息。

all_coverage = population_trace_coverage([inp1, inp2, inp3], cgi_decode)[0]
assert len(all_coverage) == 2 

不幸的是,cgi_decode()函数太简单了。因此,我们将使用原始 Python HTMLParser作为我们的测试对象。

from Coverage import population_coverage
from [html.parser](https://docs.python.org/3/library/html.parser.html) import HTMLParser 
trials = 50000  # number of random inputs generated 

让我们运行一个随机模糊器,重复次数为\(n=50000\),并绘制跟踪覆盖率随时间的变化。

# create wrapper function
def my_parser(inp):
    parser = HTMLParser()  # resets the HTMLParser object for every fuzz input
    parser.feed(inp) 
# create random fuzzer
fuzzer = RandomFuzzer(min_length=1, max_length=100,
                      char_start=32, char_range=94)

# create population of fuzz inputs
population = []
for i in range(trials):
    population.append(fuzzer.fuzz())

# execute and measure trace coverage
trace_timeseries = population_trace_coverage(population, my_parser)[1]

# execute and measure code coverage
code_timeseries = population_coverage(population, my_parser)[1]

# plot trace coverage over time
plt.figure(num=None, figsize=(12, 4), dpi=80, facecolor='w', edgecolor='k')
plt.subplot(1, 2, 1)
plt.plot(trace_timeseries)
plt.xlabel('# of fuzz inputs')
plt.ylabel('# of traces exercised')
plt.title('Trace Coverage Over Time')

# plot code coverage over time
plt.subplot(1, 2, 2)
plt.plot(code_timeseries)
plt.xlabel('# of fuzz inputs')
plt.ylabel('# of statements covered')
plt.title('Code Coverage Over Time'); 

在上面,我们可以看到随时间变化的跟踪覆盖率(左侧)和代码覆盖率(右侧)。以下是我们的观察结果。

  1. 跟踪覆盖率更稳健。 与代码覆盖率相比,图中跳跃更少。

  2. 跟踪覆盖率更细粒度。 最终覆盖的跟踪比语句多(y 轴)。

  3. 跟踪覆盖率增长更稳定。 代码覆盖率在 50k 个输入后,第一次输入执行了超过一半的语句。相反,覆盖的跟踪数量缓慢而稳定地增长,因为每个输入只能产生一个执行跟踪。

正是因为这个原因,今天最突出和最成功的模糊测试工具之一,美国模糊跳蚤(AFL),使用了一个类似的进度衡量标准(对输入执行的分支计算出的哈希值)。

评估发现概率估计

让我们来看看当我们在模糊测试中寻找执行轨迹而不是三元组时,Good-Turing 估计器作为发现概率估计的表现如何。

为了测量经验概率,我们执行相同的输入种群(n=50000)并在常规间隔(measurements=100间隔)内进行测量。在每次测量中,我们重复以下实验repeats=500次,报告平均值:如果下一个输入产生新的轨迹,则返回 1,否则返回 0。注意,在这些重复中,我们不记录新发现的轨迹。

repeats = 500      # experiment repetitions
measurements = 100  # experiment measurements 
emp_timeseries = []
all_coverage = set()
step = int(trials / measurements)

for i in range(0, trials, step):
    if i - step >= 0:
        for j in range(step):
            inp = population[i - j]
            with Coverage() as cov:
                try:
                    my_parser(inp)
                except BaseException:
                    pass
            all_coverage |= set([getTraceHash(cov)])

    discoveries = 0
    for _ in range(repeats):
        inp = fuzzer.fuzz()
        with Coverage() as cov:
            try:
                my_parser(inp)
            except BaseException:
                pass
        if getTraceHash(cov) not in all_coverage:
            discoveries += 1
    emp_timeseries.append(discoveries / repeats) 

现在,我们计算 Good-Turing 估计随时间的变化。

gt_timeseries = []
singleton_timeseries = population_trace_coverage(population, my_parser)[2]
for i in range(1, trials + 1, step):
    gt_timeseries.append(singleton_timeseries[i - 1] / i) 

让我们继续绘制这两个时间序列。

line_emp, = plt.semilogy(emp_timeseries, label="Empirical")
line_gt, = plt.semilogy(gt_timeseries, label="Good-Turing")
plt.legend(handles=[line_emp, line_gt])
plt.xticks(range(0, measurements + 1, int(measurements / 5)),
           range(0, trials + 1, int(trials / 5)))
plt.xlabel('# of fuzz inputs')
plt.ylabel('discovery probability')
plt.title('Discovery Probability Over Time'); 

图片

再次,Good-Turing 估计似乎非常准确。事实上,经验估计器的精度要低得多,如大幅波动所示。你可以尝试增加重复次数(repeats)以获得更精确的经验估计,然而,这会以等待更长的时间为代价。

发现概率量化残余风险

好吧。你已经掌握了几台强大的机器,并使用它们对软件系统进行了几个月的模糊测试,但没有发现任何漏洞。系统有漏洞吗?

好吧,谁知道呢?我们无法肯定;总是存在一些残余风险。测试不是验证。也许下一个生成的测试输入会揭示一个漏洞。

假设残余风险是下一次测试输入揭示尚未发现的漏洞的概率。Böhme [B"{o}hme et al, 2018] 已经表明,Good-Turing 发现概率的估计也是最大残余风险的估计。

证明草图(残余风险)。这里有一个证明草图,它表明对于任意物种定义的发现概率估计给出了在尚未发现任何漏洞的情况下发现漏洞概率的上界:假设对于每个“旧”物种 A(在这里,执行轨迹),我们推导出两个“新”物种:一些属于 A 的输入暴露了漏洞,而其他属于 A 的输入没有。我们知道只有未暴露漏洞的物种已经被发现。因此,所有暴露漏洞的物种和一些未暴露漏洞的物种仍然未被发现。因此,发现新物种的概率给出了发现(暴露漏洞的)物种概率的上界。QED

发现概率的估计在许多其他方面都很有用。

  1. 发现概率。我们可以在模糊测试活动的任何时刻估计下一个输入属于之前未见物种的概率(在这里,它产生新的执行轨迹,即执行了一组新的语句)。

  2. 发现概率的补数。我们可以估计模糊器可以生成的所有输入中,我们已经看到物种(在这里,是执行轨迹)的比例。在某种意义上,这使我们能够量化模糊测试活动向完成的进度:如果发现新物种的概率太低,我们可能最好终止活动。

  3. 发现概率的倒数。我们可以预测需要多少测试输入,以便我们可以期望发现新的物种(在这里,执行轨迹)。

我们如何知道何时停止模糊测试?

在模糊测试中,我们有进度指标,如代码覆盖率或语法覆盖率。假设我们感兴趣的是覆盖程序中的所有语句。已经覆盖的语句的百分比量化了我们离完成模糊测试活动的“距离”。然而,有时我们只知道在生成\(n\)个模糊输入后发现的物种数量\(S(n)\)(在这里,是语句)。只有当我们知道物种的总数\(S\)时,才能计算百分比\(S(n)/S\)。即使如此,并非所有物种都是可行的。

成功率估计器

如果我们不知道物种的总数,那么至少让我们估计它:正如我们之前所看到的,物种发现会随着时间的推移而减慢。一开始,会发现许多新的物种。后来,在发现下一个物种之前需要生成许多输入。事实上,给定足够的时间,模糊测试活动会接近一个渐近线。正是这个渐近线我们可以进行估计。

在 1984 年,著名的理论生物统计学家 Anne Chao 开发了一个估计器\(\hat S\),它估计渐近总物种数\(S\):\begin{align} \hat S_\text{Chao1} = \begin{cases} S(n) + \frac{f_1²}{2f_2} & \text{if \(f_2>0\)}\ S(n) + \frac{f_1(f_1-1)}{2} & \text{otherwise} \end{cases} \end{align}

  • 其中\(f_1\)\(f_2\)分别是单例和双例物种的数量(分别被观察过一次或两次),并且

  • 其中\(S(n)\)是在生成\(n\)个模糊输入后发现的物种数量。

那么,Chao 的估计表现如何?为了调查这一点,我们使用一个模糊器设置生成trials=400000个模糊输入,这个设置允许我们在几秒钟内看到渐近线:我们测量轨迹覆盖率。在我们的模糊测试活动进行到一半时(trials/2=100000),我们生成 Chao 的渐近总物种数估计\(\hat S\)。然后,我们继续剩余的活动以看到“经验”渐近线。

trials = 400000
fuzzer = RandomFuzzer(min_length=2, max_length=4,
                      char_start=32, char_range=32)
population = []
for i in range(trials):
    population.append(fuzzer.fuzz())

_, trace_ts, f1_ts, f2_ts = population_trace_coverage(population, my_parser) 
time = int(trials / 2)
time 
200000

f1 = f1_ts[time]
f2 = f2_ts[time]
Sn = trace_ts[time]
if f2 > 0:
    hat_S = Sn + f1 * f1 / (2 * f2)
else:
    hat_S = Sn + f1 * (f1 - 1) / 2 

执行time个模糊输入(所有输入的一半)后,我们已经覆盖了这么多轨迹:

time 
200000

Sn 
61

我们可以估计总共有这么多轨迹:

hat_S 
65.5

因此,我们达到了这个估计的百分比:

100 * Sn / hat_S 
93.12977099236642

执行trials个模糊输入后,我们覆盖了这么多轨迹:

trials 
400000

trace_ts[trials - 1] 
67

潮的估计器的准确性相当合理。它并不总是准确的——尤其是在模糊测试活动的开始阶段,当发现概率仍然非常高时。尽管如此,它展示了报告一个百分比来评估模糊测试活动完成进度的主要好处。

尝试一下尝试将trials设置为 100 万,将time设置为int(trials / 4).

外推模糊测试成功

假设你已经运行了模糊测试器一周,生成了\(n\)个模糊输入并发现了\(S(n)\)个物种(在这里,覆盖了\(S(n)\)个执行轨迹)。而不是再运行一周的模糊测试器,你希望预测你将发现多少更多物种。在 2003 年,安妮·潮和她的团队开发了一种外推方法来做到这一点。我们感兴趣的是,如果生成了\(m^*\)更多模糊输入,发现的物种数量\(S(n+m^*)\)是多少:

\begin{align} \hat S(n + m^) = S(n) + \hat f_0 \left[1-\left(1-\frac{f_1}{n\hat f_0 + f_1}\right){m}\right] \end{align}

  • 其中\(\hat f_0=\hat S - S(n)\)是未发现物种数量\(f_0\)的估计,并且

  • 其中\(f_1\)是单种物种的数量,即我们恰好观察过一次的那些。

单个物种的数量\(f_1\),我们可以在模糊测试活动本身中跟踪。未发现物种的数量\(\hat f_0\)的估计,我们可以简单地使用潮的估计\(\hat S\)和观察到的物种数量\(S(n)\)来推导。

让我们通过比较预测的物种数量与经验物种数量来查看潮的外推器表现如何。

prediction_ts: List[float] = [None] * time
f0 = hat_S - Sn

for m in range(trials - time):
    assert (time * f0 + f1) != 0 , 'time:%s f0:%s f1:%s' % (time, f0,f1)
    prediction_ts.append(Sn + f0 * (1 - (1 - f1 / (time * f0 + f1)) ** m)) 
plt.figure(num=None, figsize=(12, 3), dpi=80, facecolor='w', edgecolor='k')
plt.subplot(1, 3, 1)
plt.plot(trace_ts, color='white')
plt.plot(trace_ts[:time])
plt.xticks(range(0, trials + 1, int(time)))
plt.xlabel('# of fuzz inputs')
plt.ylabel('# of traces exercised')

plt.subplot(1, 3, 2)
line_cur, = plt.plot(trace_ts[:time], label="Ongoing fuzzing campaign")
line_pred, = plt.plot(prediction_ts, linestyle='--',
                      color='black', label="Predicted progress")
plt.legend(handles=[line_cur, line_pred])
plt.xticks(range(0, trials + 1, int(time)))
plt.xlabel('# of fuzz inputs')
plt.ylabel('# of traces exercised')

plt.subplot(1, 3, 3)
line_emp, = plt.plot(trace_ts, color='grey', label="Actual progress")
line_cur, = plt.plot(trace_ts[:time], label="Ongoing fuzzing campaign")
line_pred, = plt.plot(prediction_ts, linestyle='--',
                      color='black', label="Predicted progress")
plt.legend(handles=[line_emp, line_cur, line_pred])
plt.xticks(range(0, trials + 1, int(time)))
plt.xlabel('# of fuzz inputs')
plt.ylabel('# of traces exercised'); 

潮的预测器从预测看起来相当准确。我们在time=trials/4时做出预测。尽管进行了 3 倍的预测(即在试验时),我们可以看到预测值(黑色,虚线)与经验值(灰色,实线)非常接近。

尝试一下。再次,尝试将trials设置为 100 万,将time设置为int(trials / 4)

经验教训

  • 可以测量模糊测试活动的进度(即物种随时间的变化,即\(S(n)\))。

  • 可以测量模糊测试活动的有效性(即渐近总物种数\(S\))。

  • 可以使用 Chao1-估计器\(\hat S\)来估计模糊测试活动的有效性

  • 可以外推模糊测试活动的进度\(\hat S(n+m^*)\)

  • 可以使用物种发现概率的 Good-Turing 估计器\(GT\)来估计残余风险(即存在尚未发现的漏洞的概率)。

下一步

这本书的最后一章!如果你想继续阅读,请查看附录。否则,利用你所学的知识,去创造伟大的模糊测试器和测试生成器!

背景

练习

I.J. Good 和 Alan Turing 为每个输入恰好属于一个物种的情况开发了一个估计器。例如,每个输入产生一个精确的执行跟踪(见函数getTraceHash)。然而,这并不普遍。例如,每个输入在源代码中执行多个语句和分支。一般来说,每个输入可以属于一个或多个物种。

在这个扩展模型中,基础统计相当不同。然而,我们在本章讨论的所有估计器最终都几乎与简单单物种模型的估计器相同。例如,Good-Turing 估计器\(C\)定义为$$C=\frac{Q_1}{n}$$其中\(Q_1\)是单例物种的数量,\(n\)是生成的测试用例数量。在整个模糊测试活动中,我们记录每个物种的发生频率,即属于该物种的输入数量。再次,如果我们恰好看到一个属于物种\(i\)的输入,我们定义物种\(i\)单例物种

练习 1:估计和评估语句覆盖率的发现概率

在这个练习中,我们为简单的模糊器创建一个 Good-Turing 估计器。

使用笔记本进行练习并查看解决方案。

第一部分:种群覆盖率

实现一个名为population_stmt_coverage()的函数,如在估计发现概率的章节中所述,该函数监控随时间变化的单例和双例数量,即随着测试输入数量\(i\)的增加。

使用笔记本进行练习并查看解决方案。

from Coverage import population_coverage
... 

使用笔记本进行练习并查看解决方案。

第二部分:种群

使用来自模糊器章节的随机fuzzer(min_length=1, max_length=1000, char_start=0, char_range=255)生成一个包含\(n=10000\)模糊输入的种群。

使用笔记本进行练习并查看解决方案。

from Fuzzer import RandomFuzzer
from [html.parser](https://docs.python.org/3/library/html.parser.html) import HTMLParser
...; 

使用笔记本进行练习并查看解决方案。

第三部分:估计概率

在 Python HTML 解析器(from html.parser import HTMLParser)上执行生成的输入,并使用 Good-Turing 估计器估计下一个输入覆盖先前未覆盖语句的概率(即发现概率)。

使用笔记本进行练习并查看解决方案。

第四部分:经验评估

通过实验方法评估 Good-Turing 估计器(使用\(10000\)次重复)覆盖新语句的概率的准确性,实验方法见估计发现概率部分的结尾。

使用笔记本进行练习并查看解决方案。

练习 2:外推和评估语句覆盖率

在这个练习中,我们使用 Chao 的外推方法来估计模糊测试的成功率。

使用笔记本进行练习并查看解决方案。

第一部分:创建种群

使用随机fuzzer(min_length=1, max_length=1000, char_start=0, char_range=255)生成一个包含\(n=400000\)模糊输入的种群。

使用笔记本进行练习并查看解决方案。

第二部分:计算估计值

在生成\(n/4=100000\)模糊输入后,计算语句总数\(\hat S\)的估计值。在扩展模型中,\(\hat S\)的计算如下:

  • 其中 \(Q_1\)\(Q_2\) 分别表示单例和双例语句的数量(即被恰好一个或两个模糊输入测试过的语句),以及

  • 其中 \(S(n)\) 是在生成 \(n\) 个模糊输入后发现的(或未发现的)语句数量。

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

第三部分:计算和评估外推器

通过比较预测的语句数量与经验性的语句数量来计算和评估赵的外推器。

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

Creative Commons License 本项目的内容受Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License许可。内容的一部分源代码,以及用于格式化和显示该内容的源代码受MIT License许可。最后更改日期:2024-11-09 17:07:29+01:00。github.com/uds-se/fuzzingbook/commits/master/notebooks/WhenToStopFuzzing.ipynb • 引用 • 印记

如何引用本作品

安德烈亚斯·泽勒(Andreas Zeller)、拉胡尔·戈皮纳特(Rahul Gopinath)、马塞尔·博姆(Marcel Böhme)、戈登·弗朗西斯(Gordon Fraser)和克里斯蒂安·霍勒(Christian Holler):"何时停止模糊测试"。在安德烈亚斯·泽勒(Andreas Zeller)、拉胡尔·戈皮纳特(Rahul Gopinath)、马塞尔·博姆(Marcel Böhme)、戈登·弗朗西斯(Gordon Fraser)和克里斯蒂安·霍勒(Christian Holler)的《模糊测试书》(The Fuzzing Book)中。www.fuzzingbook.org/html/WhenToStopFuzzing.html。检索日期:2024-11-09 17:07:29+01:00。

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

附录

原文:www.fuzzingbook.org/html/99_Appendices.html

这一部分包含支持其他笔记本的笔记本和模块。

  • 处理错误 提供在笔记本中捕获和报告预期错误的方法。

  • 计时器 是一个简单的实用工具,用于测量执行过程中经过的时间。

  • 控制流 帮助确定 Python 程序中的控制流。

  • 铁路图 将语法可视化表示为铁路图。

  • 索引 是本书的索引。

Creative Commons License 本项目的内容受 Creative Commons Attribution-NonCommercial-ShareAlike 4.0 国际许可协议 的许可。作为内容一部分的源代码,以及用于格式化和显示该内容的源代码,受 MIT 许可协议 的许可。 最后更改:2020-10-13 15:12:26+02: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/99_Appendices.html. Retrieved 2020-10-13 15:12:26+02:00.

@incollection{fuzzingbook2020:99_Appendices,
    author = {Andreas Zeller and Rahul Gopinath and Marcel B{\"o}hme and Gordon Fraser and Christian Holler},
    booktitle = {The Fuzzing Book},
    title = {Appendices},
    year = {2020},
    publisher = {CISPA Helmholtz Center for Information Security},
    howpublished = {\url{https://www.fuzzingbook.org/html/99_Appendices.html}},
    note = {Retrieved 2020-10-13 15:12:26+02:00},
    url = {https://www.fuzzingbook.org/html/99_Appendices.html},
    urldate = {2020-10-13 15:12:26+02:00}
}

学术原型设计

www.fuzzingbook.org/html/AcademicPrototyping.html

这是安德烈亚斯·策勒尔(Andreas Zeller)在 ESEC/FSE 2022 会议上的教程 "Academic Prototyping" 的手稿。

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

关于本教程

在我们的 Fuzzing Book 中,我们使用 Python 来实现自动化测试技术,并将其作为大多数测试主题的语言。为什么是 Python?简短的答案是

Python 让我们惊人地 高效。本书中的大多数技术只需 2-3 天 就可以实施。这比 "经典" 语言如 C 或 Java 快 10-20 倍

生产力的提升达到 10-20 倍是巨大的,几乎是荒谬的。为什么会这样,这对研究和教学有什么影响?

我将讨论什么

在这个教程中,我将通过从头开始原型设计一个 符号测试生成器 来演示这一点。这通常被认为是一个非常困难的任务,需要数月时间来构建。然而,本章中开发代码的时间不到两小时——解释它的时间不到 20 分钟。

我们将探讨这种生产力的原因,将 PythonJupyter notebooks 作为主要驱动力。我们还将讨论这对学术软件开发的影响。

你将学到什么

  • 使用 Python 和其库作为原型设计的语言

  • 使用 Jupyter Notebooks 作为编码和运行评估的工具

  • 理解生产力和可重复性的提升来自哪里

分析编译语言的困难

在我的职业生涯中,我构建(并允许构建)了几个在编译语言(如 C 或 Java)上操作的工具——特别是

  • 自动化调试工具,这些工具将提取和分析程序执行中的动态信息,

  • 自动化测试工具,这些工具将使用(生成的)输入对单个应用程序或函数进行测试。

从这次经历中学到的东西可以总结如下:

  1. 配置编译代码是 困难的。你需要连接到现有的基础设施,如 Java 或 C 编译器,找出要添加哪些代码,找出如何适应相应的编译器。

  2. 从编译代码中捕获信息也很 困难。同样,你需要对代码进行 配置(见上文),但还需要确保信息被存储(高效地?),稍后可以检索和解码。

所有这些都充满了错误。成功的测试生成器之所以假设基础设施绝对最小化(例如,使用现有工具获取覆盖率,而无需更多),是有充分理由的。

如果你想要进行静态分析甚至符号分析,情况会更糟。自己解析 C 和 Java 代码是困难的;对于大规模代码,几乎是不可能的。由于现有基础设施(如 Soot)的力量,在某种中间表示上工作是可能的。但如果你想要扩展这些,你将面临数周甚至数月的工作。

符号分析特别困难,因为你通常希望在一个至少与源代码一样高的抽象级别上操作,这要求你在符号级别、源代码级别和实际机器代码之间建立联系。所有这些都是可能的,但很困难。

现在,如果你的重点是真正分析大规模代码,例如,因为

  • 领域非常重要,问题具有挑战性

  • 你在工业界工作,并且从事安全关键系统

  • 你想在编译语言的大量样本上评估某种方法

那么最终将你的方法应用于大规模代码可能是有意义的。但在那之前,你可能想检查你的方法是否真的有效。

这就是原型设计发挥作用的地方。原型设计意味着开发一个快速解决方案来探索某种方法的可行性

  • 从实践中收集反馈

  • 快速进化一种方法

  • 以证明某事可能有效

换句话说,这是我们通常需要在学术领域展示的概念验证。

Python 很简单

Python 是一种高级语言,它允许人们专注于实际的算法,而不是单个位和字节如何在内存中传递。对于这本书来说,这一点很重要:我们希望关注个别技术是如何工作的,而不是它们的优化。关注算法允许你玩弄和调整它们,并快速开发自己的。一旦你找到了做事的方法,你仍然可以将你的方法移植到其他语言或专门的设置中。

例如,考虑一下(不)著名的三角形程序,它将长度为\(a\)\(b\)\(c\)的三角形分类为三个类别之一。它看起来像伪代码;然而,我们可以轻松地执行它。

def triangle(a, b, c):
    if a == b:
        if b == c:
            return 'equilateral'
        else:
            return 'isosceles #1'
    else:
        if b == c:
            return 'isosceles #2'
        else:
            if a == c:
                return 'isosceles #3'
            else:
                return 'scalene' 

这里是执行triangle()函数的一个示例:

triangle(2, 3, 4) 
'scalene'

在本章剩余部分,我们将使用triangle()函数作为测试程序的持续示例。当然,triangle()函数的复杂性远远低于大型系统,本章所展示的内容也不适用于,比如说,由数千个相互交织的微服务组成的生态系统。然而,其目的是展示某些技术如果拥有合适的语言和环境,可以多么容易实现。

构建一个最小测试器

如果你想用随机值测试triangle(),这相当简单。只需携带一个 Python 随机数生成器,并将它们投入triangle()中。

from [random](https://docs.python.org/3/library/random.html) import randrange 
for i in range(10):
    a = randrange(1, 10)
    b = randrange(1, 10)
    c = randrange(1, 10)

    t = triangle(a, b, c)
    print(f"triangle({a}, {b}, {c}) = {repr(t)}") 
triangle(4, 1, 7) = 'scalene'
triangle(4, 5, 3) = 'scalene'
triangle(7, 2, 1) = 'scalene'
triangle(7, 6, 9) = 'scalene'
triangle(5, 5, 8) = 'isosceles #1'
triangle(1, 7, 1) = 'isosceles #3'
triangle(6, 4, 8) = 'scalene'
triangle(8, 9, 5) = 'scalene'
triangle(4, 8, 3) = 'scalene'
triangle(6, 1, 1) = 'isosceles #2'

到目前为止,一切顺利——但这在几乎任何编程语言中都可以做到。是什么让 Python 变得特别?

Python 中的动态分析:如此简单以至于令人痛苦

动态分析是跟踪程序执行期间发生情况的能力。Python 的settrace()机制允许你在程序执行时跟踪所有代码行、所有变量、所有值——所有这些都在几行代码中完成。我们来自覆盖率章节的Coverage类展示了如何用五行代码捕获所有执行行的跟踪;这样的跟踪可以轻松地转换为执行行或分支的集合。再加上两行,你可以轻松地跟踪所有函数、参数、变量值——例如,查看我们的动态不变性章节。你甚至可以访问单个函数的源代码(并且可以打印出来!)所有这些只需要 10 分钟,也许 20 分钟就可以实现。

这里有一段 Python 代码可以完成所有这些。我们跟踪执行的行,并为每一行打印其源代码和所有局部变量的当前值:

import [sys](https://docs.python.org/3/library/sys.html)
import [inspect](https://docs.python.org/3/library/inspect.html) 
def traceit(frame, event, arg):
    function_code = frame.f_code
    function_name = function_code.co_name
    lineno = frame.f_lineno
    vars = frame.f_locals

    source_lines, starting_line_no = inspect.getsourcelines(frame.f_code)
    loc = f"{function_name}:{lineno}  {source_lines[lineno  -  starting_line_no].rstrip()}"
    vars = ", ".join(f"{name} = {vars[name]}" for name in vars)

    print(f"{loc:50} ({vars})")

    return traceit 

函数sys.settrace()traceit()注册为跟踪函数;然后它会跟踪triangle()的给定调用:

def triangle_traced():
    sys.settrace(traceit)
    triangle(2, 2, 1)
    sys.settrace(None) 
triangle_traced() 
triangle:1 def triangle(a, b, c):                  (a = 2, b = 2, c = 1)
triangle:2     if a == b:                          (a = 2, b = 2, c = 1)
triangle:3         if b == c:                      (a = 2, b = 2, c = 1)
triangle:6             return 'isosceles #1'       (a = 2, b = 2, c = 1)
triangle:6             return 'isosceles #1'       (a = 2, b = 2, c = 1)

相比之下,尝试为 C 语言构建这样的动态分析。你可以选择仪器化代码以跟踪所有执行的行并记录变量值,将结果信息存储在某个数据库中。这可能需要几周,甚至几个月才能实现。你也可以通过调试器(逐步打印-打印-逐步打印-打印)运行你的代码;但同样,编程交互可能需要几天。一旦你得到初步结果,你可能会意识到你需要其他或更好的东西,所以你回到起点。这不是一件有趣的事情。

与上述动态分析一起,你可以使模糊测试更加智能。例如,基于搜索的测试会进化一个输入种群,以实现特定目标,如覆盖率。有了良好的动态分析,你可以快速实现针对任意目标的基于搜索的策略。

Python 中的静态分析:仍然简单

静态分析指的是在不实际执行程序代码的情况下分析程序代码的能力。对 Python 代码进行静态分析以推断任何属性可能是一个噩梦,因为这种语言非常动态。(更多内容见下文。)

如果你的静态分析不需要是可靠的,例如,因为你只使用它来支持指导其他技术,如测试,那么 Python 中的静态分析可以非常简单。ast模块允许你将任何 Python 函数转换为抽象语法树(AST),然后你可以随意遍历它。这是我们的triangle()函数的 AST:

from [bookutils](https://github.com/uds-se/fuzzingbook//tree/master/notebooks/shared/bookutils) import rich_output 
import [ast](https://docs.python.org/3/library/ast.html) 
if rich_output():
    # Normally, this will do
    from [showast](https://pypi.org/project/showast/) import show_ast
else:
    def show_ast(tree):
        ast.dump(tree, indent=4) 
triangle_source = inspect.getsource(triangle)
triangle_ast = ast.parse(triangle_source)
show_ast(triangle_ast) 

0 FunctionDef 1 "triangle" 0--1 2 arguments 0--2 9 If 0--9 3 arg 2--3 5 arg 2--5 7 arg 2--7 4 "a" 3--4 6 "b" 5--6 8 "c" 7--8 10 Compare 9--10 18 If 9--18 33 If 9--33 11 Name 10--11 14 Eq 10--14 15 Name 10--15 12 "a" 11--12 13 Load 11--13 16 "b" 15--16 17 Load 15--17 19 Compare 18--19 27 Return 18--27 30 Return 18--30 20 Name 19--20 23 Eq 19--23 24 Name 19--24 21 "b" 20--21 22 Load 20--22 25 "c" 24--25 26 Load 24--26 28 Constant 27--28 29 "equilateral" 28--29 31 Constant 30--31 32 "isosceles #1" 31--32 34 Compare 33--34 42 Return 33--42 45 If 33--45 35 Name 34--35 38 Eq 34--38 39 Name 34--39 36 "b" 35--36 37 Load 35--37 40 "c" 39--40 41 Load 39--41 43 Constant 42--43 44 "isosceles #2" 43--44 46 Compare 45--46 54 Return 45--54 57 Return 45--57 47 Name 46--47 50 Eq 46--50 51 Name 46--51 48 "a" 47--48 49 Load 47--49 52 "c" 51--52 53 Load 51--53 55 Constant 54--55 56 "isosceles #3" 55--56 58 Constant 57--58 59 "scalene" 58--59

现在假设有人想使用静态分析来识别所有triangle分支及其条件。你会遍历 AST,寻找If节点,并取它们的第一个子节点(条件)。这也很简单:

def collect_conditions(tree):
    conditions = []

    def traverse(node):
        if isinstance(node, ast.If):
            cond = ast.unparse(node.test).strip()
            conditions.append(cond)

        for child in ast.iter_child_nodes(node):
            traverse(child)

    traverse(tree)
    return conditions 

这里是triangle()代码中出现的四个if条件:

collect_conditions(triangle_ast) 
['a == b', 'b == c', 'b == c', 'a == c']

我们不仅可以从程序中提取单个元素,还可以随意更改它们,并将树转换回源代码。程序转换(例如,用于仪器或突变分析)变得轻而易举。上述代码仅用五分钟就写好了。再次尝试用 Java 或 C 来完成。

Python 中的符号推理:有相应的包

让我们回到测试。我们已经展示了如何从代码中提取条件。要到达triangle()函数的特定位置,需要找到导致该分支的路径条件的解决方案。要到达triangle()的最后一行('scalene'分支),我们必须找到满足$$a \ne b \land b \ne c \land a \ne c$$的解决方案

我们可以使用一个约束求解器来完成这项工作,例如微软的Z3求解器

import [z3](https://github.com/Z3Prover/z3#readme) 

让我们使用 Z3 来找到'scalene'分支条件的解决方案:

a = z3.Int('a')
b = z3.Int('b')
c = z3.Int('c') 
s = z3.Solver()
s.add(z3.And(a > 0, b > 0, c > 0))  # Triangle edges are positive
s.add(z3.And(a != b, b != c, a != c))  # Our condition
s.check() 

sat

Z3 已经向我们表明存在一个解决方案(“sat”=“可满足”)。让我们找到一个:

m = s.model()
m 

[a = 1, c = 3, b = 2]

我们可以直接使用这个解决方案来测试triangle()函数,并发现它确实覆盖了'scalene'分支。as_long()方法将 Z3 结果转换为数值。

triangle(m[a].as_long(), m[b].as_long(), m[c].as_long()) 
'scalene'

符号测试生成器

通过我们所看到的,我们现在可以构建一个符号测试生成器——一个试图系统地创建覆盖所有路径的测试输入的工具。让我们通过探索树中的所有路径来找到我们需要解决的所有条件。我们立即将这些路径转换为 Z3 格式:

def collect_path_conditions(tree):
    paths = []

    def traverse_if_children(children, context, cond):
        old_paths = len(paths)
        for child in children:
            traverse(child, context + [cond])
        if len(paths) == old_paths:
            paths.append(context + [cond])

    def traverse(node, context):
        if isinstance(node, ast.If):
            cond = ast.unparse(node.test).strip()
            not_cond = "z3.Not(" + cond + ")"

            traverse_if_children(node.body, context, cond)
            traverse_if_children(node.orelse, context, not_cond)

        else:
            for child in ast.iter_child_nodes(node):
                traverse(child, context)

    traverse(tree, [])

    return ["z3.And(" + ", ".join(path) + ")" for path in paths] 
path_conditions = collect_path_conditions(triangle_ast)
path_conditions 
['z3.And(a == b, b == c)',
 'z3.And(a == b, z3.Not(b == c))',
 'z3.And(z3.Not(a == b), b == c)',
 'z3.And(z3.Not(a == b), z3.Not(b == c), a == c)',
 'z3.And(z3.Not(a == b), z3.Not(b == c), z3.Not(a == c))']

现在我们需要做的就是将这些约束输入到 Z3 中。我们看到我们很容易覆盖所有分支:

for path_condition in path_conditions:
    s = z3.Solver()
    s.add(a > 0, b > 0, c > 0)
    eval(f"s.check({path_condition})")
    m = s.model()
    print(m, triangle(m[a].as_long(), m[b].as_long(), m[c].as_long())) 
[a = 1, c = 1, b = 1] equilateral
[c = 2, a = 1, b = 1] isosceles #1
[c = 2, a = 1, b = 2] isosceles #2
[c = 1, a = 1, b = 2] isosceles #3
[c = 3, a = 1, b = 2] scalene

成功!我们已经覆盖了三角形程序的 所有分支!

现在,上述内容仍然非常有限——并且针对triangle()代码的能力进行了定制。完整的实现实际上

  • 将整个 Python 条件翻译成 Z3 语法(如果可能),

  • 处理更多的控制流结构,如返回、断言、异常

  • 以及更多(循环、调用,等等)

其中一些可能不受 Z3 理论的支持。

为了让约束求解器更容易找到解决方案,你也可以提供从早期执行中观察到的具体值,这些值已知能够到达程序中的特定路径。这些具体值将从上述跟踪机制中收集,然后:你将拥有一个相当强大且可扩展的符号化(具体-符号)测试生成器。

现在,上述可能需要你一两天的时间,并且随着你将测试生成器扩展到triangle()之外,你将添加越来越多的功能。好的部分是,你将发明出的每一个功能实际上可能是一项研究贡献——一些以前没有人想过的事情。无论你有什么想法:你都可以快速实现它,并在原型中尝试。而且,这会比传统语言快得多。

可复现的实验

仅使用像 Python 这样的语言可能会让你成为一个(且富有创意的)代码研究员。然而,作为一个好的科学家,你还需要在运行实验时保持纪律你到底做了什么来达到论文中所述的结果?这就是我的第二个不那么秘密的武器发挥作用的地方:笔记本。

Jupyter 笔记本可以存储所有你的代码(尽管许多人更喜欢使用 IDE 来做这件事);但至少,它可以记录你在实验中进行的所有步骤,包括解释设置和你的理由的富文本,以及(当然!)丰富的可视化图表,这些图表可以直观地展示你的结果。这正是 Jupyter 笔记本擅长的,如果你做得正确,你可以从原始数据记录到图表和数字,就像你在论文中找到的那样,记录和复制你所有的实验。

这里有一些我们在我(基于 Jupyter)的书中使用图表的例子。首先是一些“标准”的数据可视化:

然而,我们也创建了我们的自己的可视化——例如

所有这些都会随着程序代码及其结果自动更新——因此始终是最新的。吃掉它,LaTeX——还有 TikZ!

但不仅如此——你还可以检查结果是否与你已经在论文中写下的内容相符。如果你在论文中写道你发现\(p \le 0.05\),那么一个如下的计算

p = ...   # insert lengthy computation here
assert p <= 0.05 

将确保你的论文中的陈述(a)得到验证,并且(b)可以被其他人验证,包括整个计算路径。(并且连续测试将自动检测是否出现任何差异。)

原型设计流程

关于原型设计(使用 Python 或其他语言),有一件很棒的事情是它允许你完全专注于你的方法,而不是基础设施。很明显,这对教学很有用——你可以在讲座中使用上述例子来快速传达程序分析、测试生成、调试等基本技术。

但原型设计有更多的优势。一个 Jupyter Notebook(就像这个一样)记录了你如何开发你的方法,包括示例、实验和理由——同时仍然关注本质。如果你以“经典”的方式编写工具,你最终会交付数千行代码,这些代码可以做任何事情,但只有在你实现了所有这些之后,你才会知道这些事情实际上是否可行。这是一个巨大的风险,而且如果你还需要更改某些东西,你将不得不一次又一次地重构代码。此外,对于任何将来会处理这段代码的人来说,如果它被埋藏在大量的基础设施和重构之下,可能需要几天甚至几周的时间才能重新提取出方法的基本思想。

在这个阶段,我们的结果是现在我们两次实施新想法:

  • 首先,我们将事物作为笔记本(就像这个一样)来实现,尝试各种方法和参数,直到我们找到正确的方法。

  • 只有当我们确保方法正确,并且我们有信心它能够工作后,我们才在可以处理大规模程序的工具中重新实现它。这仍然可能需要几周到几个月的时间,但至少我们知道我们走在正确的道路上。

顺便说一下,原始笔记本可能寿命更长,因为它们更简单、文档更好,并捕捉到了我们新颖想法的精髓。这就是这本书中几个笔记本的由来。

保持事物更新

Python 是一个动态环境。你使用的包越多,将来某天有人可能引入一个改变,使得你的代码无法运行的可能性就越高。因此,我建议你设置一个持续测试方案,其中所有笔记本都会在固定的时间间隔自动运行,运行并重新运行你的实验。

对于模糊测试书籍和调试书籍,我每周需要花费大约 30 分钟的时间来更新内容并修复错误。有了持续测试,其他人实际使用你的代码的可能性要高得多(因为它们将在他们的机器上工作)。即使作为博士导师,我也可以轻松地花费这些时间,这让我有一种良好的感觉,即其他人实际上可以利用我们的工作。

不会工作的事情

Python 以其静态分析困难而闻名,这是事实;其动态特性使得传统的静态分析难以排除特定的行为。

我们认为 Python 是原型设计自动化测试和动态分析技术的优秀语言,也是展示轻量级静态和符号分析技术的良好语言,这些技术将被用来指导支持其他技术(例如,生成软件测试)。

但如果你只想通过代码的静态分析来证明特定的属性(或其不存在),那么 Python 至少是一个挑战;对于某些领域,我们肯定会警告不要使用它。

(无)类型检查

使用 Python 来演示静态类型检查将不会是最优的,因为 Python 程序通常不包含类型注解。在编码时,我通常在没有类型注解的情况下开始,但一旦代码稳定并进行了文档化,就会回溯添加它们;这是因为我认为有类型签名使得其他人更容易使用代码:

def typed_triangle(a: int, b: int, c: int) -> str:
    return triangle(a, b, c) 

对于 Python,有可用的静态类型检查器,特别是 mypy;这些工具在揭示代码中的类型错误方面做得足够好。尽管如此,如果你的代码经过良好的测试(而且应该是这样),静态类型检查器发现新错误的机会很小。不过,如果你想展示类型检查的优点(或者自己构建新的静态类型检查器或类型系统),Python 可能是一个不错的游乐场。

(无)程序证明

Python 是一种高度动态的语言,你可以在运行时更改任何东西。将不同类型分配给变量没有问题,就像

x = 42
x = "a string" 

或者根据某些运行时条件改变变量的存在(和作用域):

p1, p2 = True, False

if p1:
    x = 42
if p2:
    del x

# Does x exist at this point? 

这些特性使得对代码进行符号推理(包括静态分析和类型检查)变得非常困难,如果不是完全不可能的话。如果你需要轻量级的静态和符号分析技术来指导其他技术(比如测试生成),那么不精确可能不会造成太大的伤害。但如果你想从你的代码中推导出保证,请不要使用 Python 作为测试对象;再次强调,像 Java/ML/Haskell(或一些非常受限的玩具语言)这样的强静态类型语言是实验的更好基础。

这并不意味着像 Python 这样的语言不应该进行静态检查。相反,Python 的广泛应用强烈呼吁更好的静态检查工具。但如果你想教授或研究静态和符号技术,我们绝对不会选择 Python 作为我们的首选语言。

尝试一下!

上面所有的代码示例你都可以运行——并且按你的意愿进行修改!从网页上,最简单的方法是转到“资源 \(\rightarrow\) 作为笔记本编辑”,你可以在浏览器中直接实验原始的 Jupyter Notebook。(使用 Shift + Return 来执行代码。)

从“资源”菜单中,你还可以下载 Python 代码(.py)以在 Python 环境中运行,或者下载笔记本(.ipynb)以在 Jupyter 中运行——同样,按你的意愿进行修改。如果你想在你的机器上运行此代码,你需要以下包:

pip install showast
pip install z3-solver

享受吧!

经验教训

Python 是一个用于原型设计、测试和调试工具的伟大语言:

  • 在 Python 中,动态分析和静态分析的实施非常容易。

  • Python 提供了大量的基础设施,用于解析、将程序作为树处理以及约束求解。

  • 这些可以在数小时内而不是数周内帮助你开发新技术。

尽管如此,Python 并不推荐作为纯符号代码分析领域。

  • 静态类型很少,甚至没有

  • 这种语言高度动态,几乎没有静态保证。

然而,即使是潜在的不稳定的符号分析也能指导测试生成——而且这又非常容易实现。

Jupyter Notebooks(使用 Python 或其他语言)非常适合原型设计

  • 笔记本记录了你的方法要点,包括示例和实验。

  • 这对于教学、沟通甚至文档都是非常好的。

  • 在原型上早期进行实验可以降低后续大规模实施的风险。

下一步

如果你想看到更多我们使用 Python 进行原型设计的例子——看看这本书这里!特别是,

  • 看看我们如何逐步开发 fuzzers;

  • 看看我们如何使用动态分析来检查覆盖率;或者

  • 看看我们如何分析 Python 代码进行 concolic 和 symbolic 以及模糊测试。

有很多东西要学——享受阅读吧!

背景

三角形问题是从 Myers 和 Sandler 的《软件测试的艺术》中改编的[Myers et al,2004]。这是一个据说很简单的问题,但当你考虑所有可能出错的事情时,它揭示了令人惊讶的深度。

本章中使用的Z3 求解器是在微软研究院 Leonardo de Moura 和 Nikolaj Bjørner 的领导下开发的[De Moura et al,2008]。它是功能最强大、最受欢迎的求解器之一。

练习

练习 1:特性!特性!

我们的路径收集器仍然非常有限。不工作的事情包括

  • 复杂条件,例如布尔运算符。Python 运算符a and b需要翻译成 Z3 语法z3.And(a, b)

  • 早期返回。在if A: return之后,后续语句的条件not A必须成立。

  • 作业。

  • 循环。

  • 函数调用。

实现这些功能越多,你将越接近一个完整的 Python 符号测试生成器。但到了某个阶段,你的原型可能就不再是原型了,那时,Python 可能就不再是最佳的语言选择。找到一个合适的时机,从原型工具切换到生产工具。

Creative Commons License 本项目的内容根据Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License授权。内容中的源代码,以及用于格式化和显示该内容的源代码,根据MIT License授权。最后更改:2023-01-07 13:43:14+01:00 • 引用 • 印记

如何引用这篇作品

安德烈亚斯·策勒,拉胡尔·戈皮纳特,马塞尔·博 hme,戈登·弗莱泽,以及克里斯蒂安·霍勒:"学术原型设计"。收录于安德烈亚斯·策勒,拉胡尔·戈皮纳特,马塞尔·博 hme,戈登·弗莱泽,以及克里斯蒂安·霍勒所著的"模糊测试书籍"中。www.fuzzingbook.org/html/AcademicPrototyping.html。检索日期:2023-01-07 13:43:14+01:00.

@incollection{fuzzingbook2023:AcademicPrototyping,
    author = {Andreas Zeller and Rahul Gopinath and Marcel B{\"o}hme and Gordon Fraser and Christian Holler},
    booktitle = {The Fuzzing Book},
    title = {Academic Prototyping},
    year = {2023},
    publisher = {CISPA Helmholtz Center for Information Security},
    howpublished = {\url{https://www.fuzzingbook.org/html/AcademicPrototyping.html}},
    note = {Retrieved 2023-01-07 13:43:14+01:00},
    url = {https://www.fuzzingbook.org/html/AcademicPrototyping.html},
    urldate = {2023-01-07 13:43:14+01:00}
}

使用 Python 进行原型设计

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

这是安德烈亚斯·策勒尔在 TAIC PART 2020 会议上发表的“几分钟内编写有效的测试工具”主题演讲的手稿。

在我们的 Fuzzing Book 中,我们使用 Python 来实现自动化测试技术,并将其作为大多数测试主题的语言。为什么是 Python?简短的答案是

Python 使我们非常高效。本书中的大多数技术实现只需要2-3 天。这比“经典”语言如 C 或 Java 快10-20 倍

生产率提高 10-20 倍是巨大的,几乎是荒谬的。为什么会这样,这对研究和教学有什么影响?

在本文中,我们将探讨一些原因,从头开始原型设计一个符号测试生成器。这通常被认为是一项非常困难的任务,需要花费数月时间来构建。然而,本章中开发代码仅用了不到两个小时——而解释它则不到 20 分钟。

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

Python 很简单

Python 是一种高级语言,允许人们专注于实际的算法,而不是单个位和字节如何在内存中传递。对于这本书来说,这一点很重要:我们希望专注于单个技术是如何工作的,而不是它们的优化。关注算法允许你玩弄和调整它们,并快速开发自己的。一旦你找到了做事的方法,你仍然可以将你的方法移植到其他语言或专门的设置中。

以(不)著名的三角形程序为例,它将长度为\(a\)\(b\)\(c\)的三角形分类为三个类别之一。它看起来像伪代码;然而,我们可以轻松地执行它。

def triangle(a, b, c):
    if a == b:
        if b == c:
            return 'equilateral'
        else:
            return 'isosceles #1'
    else:
        if b == c:
            return 'isosceles #2'
        else:
            if a == c:
                return 'isosceles #3'
            else:
                return 'scalene' 

下面是执行triangle()函数的一个示例:

triangle(2, 3, 4) 
'scalene'

在本章剩余部分,我们将使用triangle()函数作为测试程序的持续示例。当然,triangle()函数的复杂度与大型系统相比相去甚远,本章所展示的内容也不适用于,比如说,由数千个相互交织的微服务组成的生态系统。然而,其目的是展示某些技术如果拥有合适的语言和环境,可以多么容易实现。

模糊测试就像往常一样简单

如果你想用随机值测试triangle(),这相当简单。只需携带一个 Python 随机数生成器并将它们投入triangle()中。

from [random](https://docs.python.org/3/library/random.html) import randrange 
for i in range(10):
    a = randrange(1, 10)
    b = randrange(1, 10)
    c = randrange(1, 10)

    t = triangle(a, b, c)
    print(f"triangle({a}, {b}, {c}) = {repr(t)}") 
triangle(4, 4, 8) = 'isosceles #1'
triangle(8, 8, 6) = 'isosceles #1'
triangle(5, 9, 6) = 'scalene'
triangle(7, 5, 2) = 'scalene'
triangle(8, 6, 1) = 'scalene'
triangle(1, 2, 1) = 'isosceles #3'
triangle(8, 9, 2) = 'scalene'
triangle(7, 6, 6) = 'isosceles #2'
triangle(1, 6, 5) = 'scalene'
triangle(9, 1, 2) = 'scalene'

到目前为止,一切顺利——但这几乎可以在任何编程语言中做到。是什么让 Python 变得特别?

Python 中的动态分析:如此简单以至于令人痛苦

动态分析是跟踪程序执行过程中发生情况的能力。Python 的 settrace() 机制允许你在程序执行时跟踪所有代码行、所有变量、所有值——所有这些都在几行代码中完成。我们来自 覆盖率章节 的 Coverage 类展示了如何用五行代码捕获所有执行行的跟踪;这样的跟踪可以轻松地转换为执行行或分支的集合。再加上两行,你可以轻松地跟踪所有函数、参数、变量值——例如,参见我们的 动态不变性章节。你甚至可以访问单个函数的源代码(并且可以将其打印出来!)所有这些只需要 10 分钟,也许 20 分钟就能实现。

这里有一段 Python 代码实现了所有这些功能。我们跟踪执行过的行,并为每一行打印其源代码和所有局部变量的当前值:

import [sys](https://docs.python.org/3/library/sys.html)
import [inspect](https://docs.python.org/3/library/inspect.html) 
def traceit(frame, event, arg):
    function_code = frame.f_code
    function_name = function_code.co_name
    lineno = frame.f_lineno
    vars = frame.f_locals

    source_lines, starting_line_no = inspect.getsourcelines(frame.f_code)
    loc = f"{function_name}:{lineno}  {source_lines[lineno  -  starting_line_no].rstrip()}"
    vars = ", ".join(f"{name} = {vars[name]}" for name in vars)

    print(f"{loc:50} ({vars})")

    return traceit 

函数 sys.settrace()traceit() 注册为跟踪函数;它将跟踪 triangle() 的给定调用:

def triangle_traced():
    sys.settrace(traceit)
    triangle(2, 2, 1)
    sys.settrace(None) 
triangle_traced() 
triangle:1 def triangle(a, b, c):                  (a = 2, b = 2, c = 1)
triangle:2     if a == b:                          (a = 2, b = 2, c = 1)
triangle:3         if b == c:                      (a = 2, b = 2, c = 1)
triangle:6             return 'isosceles #1'       (a = 2, b = 2, c = 1)
triangle:6             return 'isosceles #1'       (a = 2, b = 2, c = 1)

相比之下,尝试为,比如说,C 语言构建这样的动态分析。你可以选择 instrument 代码以跟踪所有执行的行并记录变量值,将结果信息存储在某个数据库中。这可能会花费你 数周,甚至 数月 的时间来实现。你也可以通过调试器(一步一步地打印-打印-打印-打印)运行你的代码;但同样,编程交互可能需要几天时间。一旦你得到初步结果,你可能会意识到你需要其他或更好的东西,然后你回到画板前。这不是一件有趣的事情。

结合上述动态分析,你可以使模糊测试变得更加智能。例如,基于搜索的测试会进化一个输入种群,以实现特定目标,如覆盖率。有了良好的动态分析,你可以快速实现针对任意目标的基于搜索的策略。

Python 中的静态分析:仍然简单

静态分析指的是在不实际执行程序代码的情况下分析其能力。对 Python 代码进行静态分析以推断任何属性可能是一场噩梦,因为这种语言非常动态。(更多内容见下文。)

如果你的静态分析不需要是 sound 的——例如,因为你只使用它来 supportguide 另一种技术,如测试——那么 Python 中的静态分析可以非常简单。ast 模块允许你将任何 Python 函数转换为抽象语法树(AST),然后你可以按需遍历它。这是我们的 triangle() 函数的 AST:

from [bookutils](https://github.com/uds-se/fuzzingbook//tree/master/notebooks/shared/bookutils) import rich_output 
import [ast](https://docs.python.org/3/library/ast.html) 
if rich_output():
    # Normally, this will do
    from [showast](https://pypi.org/project/showast/) import show_ast
else:
    def show_ast(tree):
        ast.dump(tree, indent=4) 
triangle_source = inspect.getsource(triangle)
triangle_ast = ast.parse(triangle_source)
show_ast(triangle_ast) 

0 FunctionDef 1 "triangle" 0--1 2 arguments 0--2 9 If 0--9 3 arg 2--3 5 arg 2--5 7 arg 2--7 4 "a" 3--4 6 "b" 5--6 8 "c" 7--8 10 Compare 9--10 18 If 9--18 33 If 9--33 11 Name 10--11 14 Eq 10--14 15 Name 10--15 12 "a" 11--12 13 Load 11--13 16 "b" 15--16 17 Load 15--17 19 Compare 18--19 27 Return 18--27 30 Return 18--30 20 Name 19--20 23 Eq 19--23 24 Name 19--24 21 "b" 20--21 22 Load 20--22 25 "c" 24--25 26 Load 24--26 28 Constant 27--28 29 "equilateral" 28--29 31 Constant 30--31 32 "isosceles #1" 31--32 34 Compare 33--34 42 Return 33--42 45 If 33--45 35 Name 34--35 38 Eq 34--38 39 Name 34--39 36 "b" 35--36 37 Load 35--37 40 "c" 39--40 41 Load 39--41 43 Constant 42--43 44 "isosceles #2" 43--44 46 Compare 45--46 54 Return 45--54 57 Return 45--57 47 Name 46--47 50 Eq 46--50 51 Name 46--51 48 "a" 47--48 49 Load 47--49 52 "c" 51--52 53 Load 51--53 55 Constant 54--55 56 "isosceles #3" 55--56 58 Constant 57--58 59 "scalene" 58--59

现在假设有人想通过静态分析来识别所有 三角形 分支及其条件。你会遍历抽象语法树(AST),寻找 If 节点,并取它们的第一个子节点(条件)。这也很简单:

def collect_conditions(tree):
    conditions = []

    def traverse(node):
        if isinstance(node, ast.If):
            cond = ast.unparse(node.test).strip()
            conditions.append(cond)

        for child in ast.iter_child_nodes(node):
            traverse(child)

    traverse(tree)
    return conditions 

这里是 triangle() 代码中出现的四个 if 条件:

collect_conditions(triangle_ast) 
['a == b', 'b == c', 'b == c', 'a == c']

不仅可以从程序中提取单个程序元素,还可以随意更改它们,并将树转换回源代码。程序转换(例如,用于仪器或突变分析)变得轻而易举。上面的代码只花了五分钟就写出来了。再次尝试用 Java 或 C 来做。

Python 中的符号推理:有一个包可以做到这一点

让我们回到测试环节。我们已经展示了如何从代码中提取条件。要到达 triangle() 函数的特定位置,需要找到通向该分支的 路径条件 的解决方案。要到达 triangle() 函数中的最后一行(即 'scalene' 分支),我们必须找到满足以下条件的解决方案:$$a \ne b \land b \ne c \land a \ne c$$

我们可以使用一个 约束求解器 来实现这一点,例如微软的 Z3 求解器

import [z3](https://github.com/Z3Prover/z3#readme) 

让我们使用 Z3 来找到 'scalene' 分支条件的解决方案:

a = z3.Int('a')
b = z3.Int('b')
c = z3.Int('c') 
s = z3.Solver()
s.add(z3.And(a > 0, b > 0, c > 0))  # Triangle edges are positive
s.add(z3.And(a != b, b != c, a != c))  # Our condition
s.check() 

sat

Z3 已经向我们展示了存在一个解决方案("sat" = "satisfiable")。让我们找到一个:

m = s.model()
m 

[a = 1, c = 3, b = 2]

我们可以直接使用这个解决方案来测试 triangle() 函数,并发现它确实覆盖了 'scalene' 分支。as_long() 方法将 Z3 结果转换为数值。

triangle(m[a].as_long(), m[b].as_long(), m[c].as_long()) 
'scalene'

符号测试生成器

通过我们所看到的,我们现在可以构建一个 符号测试生成器 —— 一个试图系统地创建覆盖所有路径的测试输入的工具。让我们通过探索树中的所有路径来找到我们需要解决的所有条件。我们立即将这些路径转换为 Z3 格式:

def collect_path_conditions(tree):
    paths = []

    def traverse_if_children(children, context, cond):
        old_paths = len(paths)
        for child in children:
            traverse(child, context + [cond])
        if len(paths) == old_paths:
            paths.append(context + [cond])

    def traverse(node, context):
        if isinstance(node, ast.If):
            cond = ast.unparse(node.test).strip()
            not_cond = "z3.Not(" + cond + ")"

            traverse_if_children(node.body, context, cond)
            traverse_if_children(node.orelse, context, not_cond)

        else:
            for child in ast.iter_child_nodes(node):
                traverse(child, context)

    traverse(tree, [])

    return ["z3.And(" + ", ".join(path) + ")" for path in paths] 
path_conditions = collect_path_conditions(triangle_ast)
path_conditions 
['z3.And(a == b, b == c)',
 'z3.And(a == b, z3.Not(b == c))',
 'z3.And(z3.Not(a == b), b == c)',
 'z3.And(z3.Not(a == b), z3.Not(b == c), a == c)',
 'z3.And(z3.Not(a == b), z3.Not(b == c), z3.Not(a == c))']

现在我们只需要将这些约束输入到 Z3 中。我们看到我们很容易覆盖所有分支:

for path_condition in path_conditions:
    s = z3.Solver()
    s.add(a > 0, b > 0, c > 0)
    eval(f"s.check({path_condition})")
    m = s.model()
    print(m, triangle(m[a].as_long(), m[b].as_long(), m[c].as_long())) 
[a = 1, c = 1, b = 1] equilateral
[c = 2, a = 1, b = 1] isosceles #1
[c = 2, a = 1, b = 2] isosceles #2
[c = 1, a = 1, b = 2] isosceles #3
[c = 3, a = 1, b = 2] scalene

成功!我们已经覆盖了三角形程序的 所有分支!

现在,上面的内容仍然非常有限——并且针对 triangle() 代码的能力进行了定制。完整的实现实际上

  • 将整个 Python 条件翻译成 Z3 语法(如果可能),

  • 处理更多控制流结构,如返回、断言、异常

  • 以及更多(循环、调用,等等)

其中一些可能不受 Z3 理论的支持。

为了让约束求解器更容易找到解决方案,你也可以提供从早期执行中观察到的 具体值,这些值已经知道可以到达程序中的特定路径。这些具体值将从上面的跟踪机制中收集,然后:你将拥有一个非常强大且可扩展的 concolic(具体-符号)测试生成器。

现在,这可能会花你一两天的时间,并且随着你的测试生成器超出 triangle() 的范围,你将添加越来越多的功能。好的部分是,你将发明出的每一个功能实际上可能是一项研究贡献——是以前没有人想到的。无论你有什么想法:你都可以快速实现它,并在原型中尝试。而且,这会比传统语言快得多。

不会工作的事情

Python 以难以进行静态分析而闻名,这是事实;其动态特性使得传统的静态分析难以排除特定的行为。

我们认为 Python 是原型设计自动化测试和动态分析技术的优秀语言,也是展示轻量级静态和符号分析技术的良好语言,这些技术将被用来指导和支持其他技术(比如生成软件测试)。

但如果你只想通过代码的静态分析来证明特定的属性(或其不存在),那么 Python 至少是一个挑战;对于某些领域,我们肯定会警告不要使用它。

(无)类型检查

使用 Python 来演示静态类型检查将是不理想的(至少可以说),因为,嗯,Python 程序通常不包含类型注解。当然,你可以像我们在关于符号模糊测试的章节中假设的那样,用类型注解变量:

def typed_triangle(a: int, b: int, c: int) -> str:
    return triangle(a, b, c) 

大多数现实世界的 Python 代码都不会带有类型注解。虽然你也可以像我们在关于动态不变性的章节中讨论的那样后置它们,但 Python 简单来说并不是一个展示类型检查的好领域。如果你想展示类型检查的美丽和实用性,请使用像 Java、ML 或 Haskell 这样的强类型语言。

(无)程序证明

Python 是一种高度动态的语言,你可以在运行时改变任何东西。将不同类型分配给变量没有任何问题,就像

x = 42
x = "a string" 

或者根据某些运行时条件改变变量存在(和范围):

p1, p2 = True, False

if p1:
    x = 42
if p2:
    del x

# Does x exist at this point? 

这些属性使得对代码进行符号推理(包括静态分析和类型检查)变得非常困难,如果不是完全不可能的话。如果你需要轻量级的静态和符号分析技术来指导其他技术(比如测试生成),那么不精确可能不会造成太大的伤害。但如果你想要从你的代码中推导出保证,请不要使用 Python 作为测试对象;再次强调,像 Java/ML/Haskell(或一些非常受限的玩具语言)这样的强类型语言是实验的更好基础。

这并不意味着像 Python 这样的语言不应该进行静态检查。相反,Python 的广泛应用强烈呼吁更好的静态检查工具。但如果你想要教授或研究静态和符号技术,我们绝对不会选择 Python 作为我们的首选语言。

原型设计的优点

原型设计(使用 Python 或其他任何语言)的一个优点是,它允许你完全专注于你的方法,而不是基础设施。显然,这对于教学是有用的——你可以在讲座中使用上述例子,快速传达程序分析和测试生成的关键技术。

但原型设计有更多的优势。Jupyter 笔记本(就像这个一样)记录了您如何开发您的方案,包括示例、实验和理由——同时仍然关注重点。如果您以“经典”的方式编写工具,您最终会交付数千行代码,这些代码可以做任何事情,但只有当您实现了所有内容后,您才会知道这些事情实际上是否可行。这是一个巨大的风险,而且如果您还需要更改某些内容,您将不得不一次又一次地重构代码。此外,对于任何将来会处理该代码的人来说,如果它被埋藏在大量的基础设施和重构之下,可能需要几天甚至几周的时间才能重新提取出方案的基本思想。

我们现在的做法是,我们现在将新想法重复实现两次

  • 首先,我们将事物作为笔记本(就像这个一样)实现,尝试各种方法和参数,直到我们找到正确的方法。

  • 只有当我们找到了正确的方法,并且我们有信心它可行时,我们才会在一个可以处理大规模程序的工具中重新实现它。这仍然可能需要几周到几个月的时间,但至少我们知道我们走在正确的道路上。

顺便说一句,原始笔记本可能寿命更长,因为它们更简单、文档更完善,并捕捉到了我们新颖想法的精髓。这就是本书中几个笔记本的由来。

尝试一下吧!

上述所有代码示例都可以由您运行——并且可以随意更改!从网页上,最简单的方法是转到“资源 \(\rightarrow\) 作为笔记本编辑”,您可以在浏览器中直接实验原始 Jupyter 笔记本。(使用 Shift + Return 执行代码。)

从“资源”菜单中,您还可以下载 Python 代码(.py)以在 Python 环境中运行,或下载笔记本(.ipynb)以在 Jupyter 中运行——而且,您可以随意更改它们。如果您想在您的机器上运行此代码,您将需要以下包:

pip install showast
pip install z3-solver

享受吧!

经验教训

Python 是一种非常适合原型设计、测试和调试工具的语言:

  • 在 Python 中,动态分析和静态分析非常容易实现。

  • Python 提供了强大的基础设施,用于解析、将程序作为树处理以及约束求解。

  • 这些可以在几小时内而不是几周内帮助您开发新技术。

虽然 Python 不建议用作纯符号代码分析领域,尽管如此。

  • 几乎没有静态类型

  • 该语言高度动态,几乎没有静态保证

然而,即使是潜在的不健全的符号分析仍然可以指导测试生成——而且这同样非常容易构建。

Jupyter 笔记本(使用 Python 或其他语言)非常适合原型设计

  • 笔记本记录了您的方法要点,包括示例和实验。

  • 这对于教学、沟通甚至文档编写都非常好。

  • 在原型上早期进行实验可以降低后续大规模实施的风险。

下一步

如果你想看到更多我们使用 Python 进行原型设计的例子——看看这本书这里!特别是,

  • 看看我们是如何逐步开发 fuzzers 的;

  • 看看我们是如何使用动态分析来检查覆盖率;或者

  • 看看我们是如何分析 Python 代码进行 concolic 和 symbolic 以及模糊测试的。

有很多东西要学——享受阅读吧!

背景

三角形问题是从 Myers 和 Sandler 的《软件测试的艺术》中改编的 [Myers et al, 2004]。这是一个据说很简单的问题,但当你考虑所有可能出错的事情时,它揭示了令人惊讶的深度。

本章中我们使用的 Z3 solver 是在微软研究院 Leonardo de Moura 和 Nikolaj Bjørner 的领导下开发的 [De Moura et al, 2008]。它是最强大和最受欢迎的求解器之一。

练习

练习 1:特性!特性!

我们的路径收集器仍然非常有限。以下是一些不工作的事情

  • 复杂条件,例如布尔运算符。Python 运算符 a and b 需要翻译为 Z3 语法 z3.And(a, b)

  • 早期反馈。在 if A: return 之后,后续语句必须满足条件 not A

  • 作业。

  • 循环。

  • 函数调用。

实现的这些越多,你将越接近一个完整的 Python 符号测试生成器。但到了某个时候,你的原型可能不再是原型了,那时,Python 可能不再是最好的语言。找到一个合适的时机,从原型工具切换到生产工具。

Creative Commons License 本项目的内容受Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License许可。内容中的源代码,以及用于格式化和显示该内容的源代码,受MIT License许可。 最后更改:2023-01-07 15:02:34+01:00 • 引用 • 印记

如何引用本作品

Andreas Zeller, Rahul Gopinath, Marcel Böhme, Gordon Fraser, and Christian Holler: "使用 Python 进行原型设计". In Andreas Zeller, Rahul Gopinath, Marcel Böhme, Gordon Fraser, and Christian Holler, "模糊测试书籍", www.fuzzingbook.org/html/PrototypingWithPython.html. Retrieved 2023-01-07 15:02:34+01:00.

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

错误处理

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

这个笔记本中的代码有助于处理错误。通常,笔记本中的错误会导致代码执行停止;而笔记本中的无限循环会导致笔记本无限运行。这个笔记本提供了两个类来帮助解决这些问题。

先决条件

  • 这个笔记本需要对 Python 的高级概念有所了解,特别是

    • Python 的 with 语句

    • 跟踪

    • 测量时间

    • 异常

概述

要使用本章提供的代码(Importing.html),请编写

>>> from fuzzingbook.ExpectError import <identifier> 

然后利用以下功能。

ExpectError 类允许你捕获并报告异常,同时继续执行。这在笔记本中很有用,因为它们通常会一遇到异常就中断执行。它的典型用法是与 with 语句结合:

>>> with ExpectError():
>>>     x = 1 / 0
Traceback (most recent call last):
  File "/var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/ipykernel_71827/2664980466.py", line 2, in <module>
    x = 1 / 0
        ~~^~~
ZeroDivisionError: division by zero (expected) 

ExpectTimeout 类允许你在指定的时间后中断执行。这对于中断可能无限运行的代码很有用。

>>> with ExpectTimeout(5):
>>>     long_running_test()
Start
0 seconds have passed
1 seconds have passed
2 seconds have passed
3 seconds have passed

Traceback (most recent call last):
  File "/var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/ipykernel_71827/1223755941.py", line 2, in <module>
    long_running_test()
  File "/var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/ipykernel_71827/3930412460.py", line 4, in long_running_test
    time.sleep(1)
  File "Timeout.ipynb", line 43, in timeout_handler
    raise TimeoutError()
TimeoutError (expected) 

异常及其相关的堆栈跟踪会作为错误消息打印。如果你不希望这样,可以使用以下关键字选项:

  • print_traceback(默认为 True)可以设置为 False 以避免打印堆栈跟踪

  • mute(默认为 False)可以设置为 True 以完全避免任何输出。

捕获错误

ExpectError 类允许表达某些代码会产生异常。典型的用法如下:

from ExpectError import ExpectError

with ExpectError():
    function_that_is_supposed_to_fail() 

如果发生异常,它会在标准错误上打印;然而,执行继续。

import [bookutils.setup](https://github.com/uds-se/fuzzingbook//tree/master/notebooks/shared/bookutils) 
import [traceback](https://docs.python.org/3/library/traceback.html)
import [sys](https://docs.python.org/3/library/sys.html) 
from [types](https://docs.python.org/3/library/types.html) import FrameType, TracebackType 
class ExpectError:
  """Execute a code block expecting (and catching) an error."""

    def __init__(self, exc_type: Optional[type] = None, 
                 print_traceback: bool = True, mute: bool = False):
  """
 Constructor. Expect an exception of type `exc_type` (`None`: any exception).
 If `print_traceback` is set (default), print a traceback to stderr.
 If `mute` is set (default: False), do not print anything.
 """
        self.print_traceback = print_traceback
        self.mute = mute
        self.expected_exc_type = exc_type

    def __enter__(self) -> Any:
  """Begin of `with` block"""
        return self

    def __exit__(self, exc_type: type, 
                 exc_value: BaseException, tb: TracebackType) -> Optional[bool]:
  """End of `with` block"""
        if exc_type is None:
            # No exception
            return

        if (self.expected_exc_type is not None
            and exc_type != self.expected_exc_type):
            raise  # Unexpected exception

        # An exception occurred
        if self.print_traceback:
            lines = ''.join(
                traceback.format_exception(
                    exc_type,
                    exc_value,
                    tb)).strip()
        else:
            lines = traceback.format_exception_only(
                exc_type, exc_value)[-1].strip()

        if not self.mute:
            print(lines, "(expected)", file=sys.stderr)
        return True  # Ignore it 

这里有一个例子:

def fail_test() -> None:
    # Trigger an exception
    x = 1 / 0 
with ExpectError():
    fail_test() 
Traceback (most recent call last):
  File "/var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/ipykernel_71827/1235320646.py", line 2, in <module>
    fail_test()
  File "/var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/ipykernel_71827/278441162.py", line 3, in fail_test
    x = 1 / 0
        ~~^~~
ZeroDivisionError: division by zero (expected)

with ExpectError(print_traceback=False):
    fail_test() 
ZeroDivisionError: division by zero (expected)

我们可以指定期望的异常类型。这样,如果发生其他情况,我们会收到通知。

with ExpectError(ZeroDivisionError):
    fail_test() 
Traceback (most recent call last):
  File "/var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/ipykernel_71827/1259188418.py", line 2, in <module>
    fail_test()
  File "/var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/ipykernel_71827/278441162.py", line 3, in fail_test
    x = 1 / 0
        ~~^~~
ZeroDivisionError: division by zero (expected)

with ExpectError():
    with ExpectError(ZeroDivisionError):
        some_nonexisting_function() 
Traceback (most recent call last):
  File "/var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/ipykernel_71827/2242794116.py", line 2, in <module>
    with ExpectError(ZeroDivisionError):
         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/ipykernel_71827/2242794116.py", line 3, in <module>
    some_nonexisting_function()  # type: ignore
    ^^^^^^^^^^^^^^^^^^^^^^^^^
NameError: name 'some_nonexisting_function' is not defined (expected)

捕获超时

ExpectTimeout(seconds) 类允许表达某些代码可能运行很长时间或无限时间;因此,在 seconds 秒后中断执行。典型的用法如下:

from ExpectError import ExpectTimeout

with ExpectTimeout(2) as t:
    function_that_is_supposed_to_hang() 

如果发生异常,它会在标准错误上打印(与 ExpectError 类似);然而,执行继续。

如果需要在 with 块内取消超时,t.cancel() 将会起作用。

实现使用 sys.settrace(),因为这似乎是实现超时最可移植的方法。然而,它并不高效。此外,它只适用于单个 Python 代码行,并且不会中断长时间运行的系统函数。

import [sys](https://docs.python.org/3/library/sys.html)
import [time](https://docs.python.org/3/library/time.html) 
from Timeout import Timeout 
class ExpectTimeout(Timeout):
  """Execute a code block expecting (and catching) a timeout."""

    def __init__(self, timeout: Union[int, float],
                 print_traceback: bool = True, mute: bool = False):
  """
 Constructor. Interrupt execution after `seconds` seconds.
 If `print_traceback` is set (default), print a traceback to stderr.
 If `mute` is set (default: False), do not print anything.
 """
        super().__init__(timeout)

        self.print_traceback = print_traceback
        self.mute = mute

    def __exit__(self, exc_type: type,
                 exc_value: BaseException, tb: TracebackType) -> Optional[bool]:
  """End of `with` block"""

        super().__exit__(exc_type, exc_value, tb)

        if exc_type is None:
            return

        # An exception occurred
        if self.print_traceback:
            lines = ''.join(
                traceback.format_exception(
                    exc_type,
                    exc_value,
                    tb)).strip()
        else:
            lines = traceback.format_exception_only(
                exc_type, exc_value)[-1].strip()

        if not self.mute:
            print(lines, "(expected)", file=sys.stderr)

        return True  # Ignore exception 

这里有一个例子:

def long_running_test() -> None:
    print("Start")
    for i in range(10):
        time.sleep(1)
        print(i, "seconds have passed")
    print("End") 
with ExpectTimeout(5, print_traceback=False):
    long_running_test() 
Start
0 seconds have passed
1 seconds have passed
2 seconds have passed
3 seconds have passed

TimeoutError (expected)

注意,可以嵌套多个超时。

with ExpectTimeout(5, print_traceback=False):
    with ExpectTimeout(3, print_traceback=False):
        long_running_test()
    long_running_test() 
Start
0 seconds have passed
1 seconds have passed

TimeoutError (expected)

Start
0 seconds have passed
1 seconds have passed
2 seconds have passed
3 seconds have passed

TimeoutError (expected)

就这样,朋友们——享受吧!

经验教训

  • 使用 ExpectError 类,可以非常容易地处理错误,而不会中断笔记本执行。

Creative Commons License 本项目的内 容受Creative Commons Attribution-NonCommercial-ShareAlike 4.0 国际许可协议的许可。作为内容一部分的源代码,以及用于格式化和显示该内容的源代码,受MIT 许可协议的许可。 最后修改:2023-11-11 18:25:46+01:00 • 引用 • 版权信息

如何引用这篇作品

Andreas Zeller, Rahul Gopinath, Marcel Böhme, Gordon Fraser, 和 Christian Holler: "错误处理". In Andreas Zeller, Rahul Gopinath, Marcel Böhme, Gordon Fraser, 和 Christian Holler, "模糊测试书", www.fuzzingbook.org/html/ExpectError.html. Retrieved 2023-11-11 18:25:46+01:00.

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

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