Peach Fuzzer 实现原理介绍
3/27/2021
Author's Profile Avatar
Peach fuzzer 是一款著名的通用黑盒模糊测试工具,被广泛地应用于真实世界的软件测试中。本文将简要介绍 peach 模糊测试工具的实现原理。

简介

Peach 是一款基于生成的模糊测试工具。Peach 运行时需要用户提供一个以 XML 格式编码的配置文件,这个配置文件中给出了测试目标程序、测试方式、测试例数据结构等一切测试所需的信息。因此,只要弄明白这个配置文件中提供的信息的具体含义,就能够理解 Peach 的运作方式。但是,在我们探索 Peach 的配置文件的各个部分的含义之前,我们需要首先对 Peach 的设计目标有基本的了解。

设计目标

测试目标的多样性

与经典的 AFL 等模糊测试工具不同,Peach 不仅支持对传统的、通过 CLI 运行的程序的测试,也支持对其他类型的程序(例如 Windows 服务等)进行测试。测试目标的多样性会导致:
  • 运行目标程序的方式是多样的。可能直接使用命令行参数启动程序、可能通过 Windows 服务相关设施启动服务、可能复用已有的程序实例等;
  • 为目标程序提供输入数据的方式是多样的。可能通过文件输入数据、通过 stdin 输入数据、通过网络连接输入数据等;
  • 监控目标程序中产生的异常行为的方式也是多样的(可能通过调试器监控异常、可能通过 sanitizer 监控异常等)。
因此,在实现 Peach 时,我们需要支持让用户能够在配置文件中说明目标程序的启动方式、为目标程序提供输入数据的方式以及监控目标程序中产生的异常行为的方式。

测试例结构的多样性

绝大部分程序的输入数据都是有特定的结构的。例如,一个 HTTP 服务器的输入数据应该满足 HTTP RFC 中规定的 HTTP 请求的语法结构。因此,Peach 也必须支持让用户能够在配置文件中说明目标程序期望的测试例的数据结构。

自定义交互

许多命令行程序都是“一次交互”的。即,当用户通过命令行命令启动程序并提供所有的输入数据后,程序将一次性处理完毕输入数据并退出,不会存在程序处理完最初的输入数据后又再次向用户请求新的输入数据的情况。然而,也有许多程序是“多次交互”的。经典的例子是基于 TCP 协议的网络服务器,这些网络服务器至少期望“两次交互”:用户首先向其发送一个 SYN 包(第一次交互),网络服务器返回一个 SYN+ACK 包,并期望用户再次向其发送一个 ACK 包(第二次交互)。当上述的两次交互完毕后,TCP 连接建立,服务器可能会进一步期望更多次的交互以向用户提供特定的服务。为了支持对网络服务器这一类需要“多次交互”的程序的测试,Peach 也必须支持让用户能够在配置文件中说明应该如何交互式地为目标程序提供输入数据。

实现原理

针对设计目标一节中提到的三个问题,Peach 均设计了相应的机制以满足这些设计目标。

Agent

Peach 设计了 agent 机制以解决第一个问题中的程序执行和异常行为监控这两个子问题。Agent 是实现模糊测试逻辑与执行逻辑解耦合的重要设计决策。
所有的模糊测试器都可以分为两个逻辑独立的部分:模糊测试器(Fuzzer)和执行器(Executor 或 Agent)。前者负责运行模糊测试循环(Fuzzing Loop),即负责维护种子队列、生成种子、调度种子、变异种子等;后者负责实际运行目标程序、向目标程序注入前者生成的测试例并监控目标程序的异常行为。在真实世界的模糊测试场景中,将模糊测试器和执行器解耦合是通用模糊测试器(General Fuzzer)的最佳工程实践。通过模糊测试器和执行器的解耦合,这两个部分可以独立地开发、适配、改进和组合,可以极大地提高模糊测试器的灵活性。相反,如果模糊测试器与执行器是紧耦合的(例如 AFL),则会造成目标适配困难等工程问题。
在 Peach 中,Agent 负责目标程序的运行和异常行为监控。Agent 中包含一系列的 Monitor,每个 Monitor 负责启动目标程序并监控目标程序的异常行为。Peach 为每个测试平台提供了很多种类的 Monitor,例如在 Windows 平台上提供了通过 WinDbg 工具启动并监控程序行为的 Monitor:
<Agent name="LocalAgent">
    <Monitor class="WindowsDebugger">
        <Param name="CommandLine" value="PuT.exe" />
        <Param name="WinDbgPath" value="C:\WinDbg" />
    </Monitor>
</Agent>
Agent 可以分为两大类:Local Agent 和 Remote Agent。Local Agent 直接运行在模糊测试器内部,与模糊测试器处在同一进程内;Remote Agent 运行在模糊测试器外部(可以运行在同一主机上,也可以运行在不同主机上),模糊测试器通过网络连接、消息队列(ZeroMQ)或其他通信方式与 Remote Agent 进行通信。
Peach 提供了三种 Remote Agent:TCP Agent、ZeroMQ Agent 与 JSON REST Agent。其中前两种 Agent 的实现由 Peach 提供,JSON REST Agent 的实现需要由用户自己定义。对于 TCP Agent 与 ZeroMQ Agent ,模糊测试器将分别使用基于 TCP 的 RPC 和 ZeroMQ 与 agent 进行通信。对于 JSON REST Agent,模糊测试器将使用 REST API 与其进行通信。用户自定义的 Agent 只要实现 REST API 服务器即可接入 Peach 进行测试。
Remote Agent 是 Peach 设计的亮点之一。Remote Agent 使得用户可以将模糊测试器与执行器(即 Agent)部署在不同的主机上,其中部署模糊测试器的主机只需要提供模糊测试器的运行环境即可,所有与测试目标相关的运行环境全部由部署 agent 的主机提供。这种做法可以有效降低模糊测试全系统的维护复杂性。

Publisher

Peach 设计了 publisher 机制以解决第一个问题中的向目标程序提供输入的子问题。Publisher 可以看作是一个双工管道的一端,管道的另一端则连接着程序的输入和输出端。向 Publisher 中写入数据即是为目标程序注入输入数据,从 Publisher 中读出数据即是读出目标程序的输出数据。
为适配不同程序的不同输入形式,Peach 提供了多种类型的 Publisher。其中比较常用的是 File Publisher、TCP Client Publisher 和 TCP Listener Publisher。
  • File Publisher 连接到一个数据文件。向 File Publisher 中写入数据,即是向连接到的文件中写入数据;从 File Publisher 中读取数据,即是从连接到的文件中读出数据;
  • TCP Client Publisher 连接到一个远程 TCP 服务器。向 TCP Client Publisher 中写入数据,即是向远程 TCP 服务器发送数据;从 TCP Client Publisher 中读取数据,即是从 TCP 连接中读取远程 TCP 服务器发送的数据;
  • TCP Listener Publisher 可以监听客户端程序向某一个特定的 TCP endpoint 发起的连接请求,并自动 accept 第一个连接请求。向 TCP Listener Publisher 中写入数据,即是向发起连接的 TCP 客户端发送数据;从 TCP Listener Publisher 中读出数据,即是从发起连接的 TCP 客户端处接收数据。
Publisher 往往需要与 Agent 配合使用,才能在测试时正确地向目标程序提供输入数据,并与目标程序进行交互。

Data Model

Peach 设计了 data model 机制以解决第二个问题。在许多用户对 Peach 的印象中,data model 机制是 Peach 的标志。Data Model 以数据模板的方式定义了目标程序的输入数据的结构信息。Peach 使用 data model 有两种方式:
  • 通过 data model 生成和变异符合数据模板条件的输入数据;
  • 通过 data model 验证和解包目标程序的输出数据。
第二种使用方式在交互式的测试中极为有用。例如,一个有状态的网络服务器可能会在接收到客户端发送的某种具有特定格式的数据包后切换自身的状态。在使用 Peach 对客户端程序进行测试时,需要由 Peach 自身对服务器行为进行模拟。此时便可以为触发状态切换的数据包格式定义相应的 data model,并通过配置使得 Peach 在接收到符合这个 data model 的条件的数据包后切换自身状态。(Peach 的基于状态机模型的模糊测试器将在之后介绍)
限于篇幅,在此不详细介绍 data model 的配置方式。请参阅 Peach 的文档以了解配置细节。

State Model

Data model 定义了 Peach 为测试目标程序所需的数据模板,state model 则定义了 Peach 为测试目标程序所需的交互行为模板。
Peach 将模糊测试器与目标程序进行交互的这一过程抽象为一个自定义的确定性有限状态自动机(以下简称这个状态机为 Peach 状态机)的计算过程。在模糊测试器的每一轮模糊测试循环中,Peach 会运行 Peach 状态机以启动与目标程序的交互测试;Peach 状态机停机则标志着这一轮交互测试结束。Peach 状态机在 Peach 配置文件中使用 StateModel 这一个配置节进行描述。一个简单的例子如下:
<StateModel name="TheState" initialState="initial">
    <State name="initial">
        <Action type="accept" />
        <Action type="changeState" ref="receiving" />
    </State>
    <State name="receiving">
        <Action type="input">
            <DataModel ref="HelloModel" />
        </Action>
        <Action type="changeState" ref="sending" />
    </State>
    <State name="sending">
        <Action type="output">
            <DataModel ref="ResponseModel" />
        </Action>
    </State>
</StateModel>
在这个例子中,Peach 状态机中一共有三个状态:initialreceiving 以及 sending,其中 initial 状态是状态机的初始状态(由 StateModel 标签的 initialState 属性指定)。Peach 状态机的每个状态中均包含一个或多个 Action,每个 Action 代表了 Peach 模糊测试器需要执行的一个动作。每个 Action 具体代表的动作由其 type 属性指定。当 Peach 状态机进入到某个状态中时,它会依次执行这个状态中的所有 Action。当没有 Action 可以执行时,Peach 状态机停机。
initial 状态中一共有两个 Action。第一个 Action 的类型为 accept,该 Action 会导致 Peach 阻塞等待系统中的 TCP Listener Publisher 接受一个传入的 TCP 连接请求。当传入的 TCP 连接请求被接受后,第一个 Action 结束,Peach 继续执行第二个 Action。第二个 Action 的类型是 changeState,执行该 Action 会导致 Peach 状态机切换到由 ref 属性指定的状态,即 receiving 状态。
receiving 状态中一共有两个 Action。第一个 Action 的类型为 input,该 Action 会令 Peach 从 Publisher 中读取数据,并使用指定的 data model 检查读取到的数据的合法性。当读取到满足给定 data model 的条件的数据后,该 Action 结束。接下来,Peach 状态机将切换到 sending 状态。
sending 状态中仅有一个 Action,这个 Action 的类型为 output,该 Action 会导致 Peach 根据指定的 data model 生成符合条件的数据并写入 Publisher。数据生成完毕、写入完毕后,该 Action 结束。此时已经没有任何 Action 可供执行,因此 Peach 状态机停机,本轮测试结束。
State model 是模糊测试器与目标程序进行交互的行为模板。由于计算机的任何行为都可以使用状态机进行抽象,因此理论上 state model 能够实现对任意程序交互行为的表示。

延迟执行

上一节提供的例子是对 TCP 客户端进行测试的例子。在这种测试场景中,在执行 Peach 自动机之前,Peach 会自动通知 Agent 启动目标程序。Peach 自动机的执行与目标程序的执行可以是并发的。但在一些传统的测试场景下,例如在测试通过文件读取输入数据的程序时,Peach 自动机的执行与程序的执行则有同步的关系。具体地来说,在 Peach 自动机通过 output Action 向连接到文件的 Publisher 写入数据完毕之前,Agent 都不能启动目标程序,否则目标程序将很有可能无法读出任何有效的输入数据。为了解决这个问题,许多 Agent 中的 Monitor 均支持延迟执行的机制。
具体地来说,许多 Monitor 均支持 StartOnCall 这一属性。当为一个支持该属性的 Monitor 配置了该属性时,该 Monitor 将采用延迟执行的方式,在收到从 state model 中通过 call Action 显式地发送执行信号后才会启动目标程序的执行。例如:
<StateModel name="TheState" initialState="initial">
    <State name="initial">
        <Action type="output">
            <DataModel ref="InputDataModel" />
        </Action>
        <Action type="call" method="StartTarget" />
    </State>
</StateModel>

<Agent name="Local">
    <Monitor class="WindowsDebugger">
        <Param name="CommandLine" value="PuT.exe" />
        <Param name="WinDbgPath" value="C:\WinDbg" >
        <Param name="StartOnCall" value="StartTarget" />
    </Monitor>
</Agent>
在上述的例子中,我们为 Monitor 配置了一个值为 StartTargetStartOnCall 属性。因此,该 Monitor 会等待接收到 state model 中具有值为 StartTargetmethod 属性的 call Action 发送的信号后,才会启动目标程序。在执行 call Action 之前,state model 已经将生成的测试例通过 output Action 写入到了连接到输入文件的 Publisher 中,因此测试能够正常进行。