1.8.2 模糊测试 Windows DLL

构建与模糊测试动态链接库

译文 · 基于 Rust Fuzz Book

模糊测试 Windows DLL

原文链接: https://rust-fuzz.github.io/book/cargo-fuzz/windows/dll-fuzzing.html

在 Windows 系统上,共享库称为动态链接库 (DLL)。代码可编译为 .dll 文件,随后由系统上的其他可执行文件在运行时加载。

你可能希望对 Windows DLL 进行模糊测试。与其他软件一样,共享库也适合接受模糊测试。目前对 Windows DLL 进行模糊测试是可行的,但稍微麻烦一些。请继续阅读!

如何构建并模糊测试 DLL?

按以下步骤构建用于模糊测试的 DLL,并构建调用它的模糊测试目标。

配置模糊测试 Cargo.toml

将 DLL 的 Cargo 项目作为可选依赖添加到模糊测试 Cargo.toml:

1
2
3
4
[dependencies]
# ...
your_dll = { path = "..", optional = true }
# ...

在 Cargo.toml 中添加一个需要该 DLL 作为依赖的特性。我们希望 DLL 仅在此特性启用时构建:

1
2
3
4
5
6
[features]
# ...
build_your_dll = [
    "dep:your_dll"
]
# ...

最后,在 Cargo.toml 中创建一个需要此特性的模糊测试目标。这将是一个「虚拟」模糊测试目标,其唯一目的是构建你的 DLL。它不会真正进行模糊测试;只是让 cargo-fuzz 使用 AddressSanitizer(及其他)插桩来构建 DLL 的一种方式。

1
2
3
4
5
6
7
[[bin]]
name = "build_your_dll"
path = "fuzz_targets/build_your_dll.rs"
required-features = ["build_your_dll"]
test = false
doc = false
bench = false

创建「虚拟」模糊测试目标

接下来,需要为这个「虚拟」模糊测试目标创建源代码(fuzz_targets/build_your_dll.rs)。最简单的情况下,只需创建一个简单的 main 函数:

1
2
3
4
pub fn main()
{
    println!("DLL build complete!");
}

这个「虚拟」目标会在 DLL 构建完成之后执行其 main 函数,因此如有需要,可在此添加额外代码以执行构建后安装或配置。(例如,你可能需要将构建好的 DLL 复制到系统其他位置,以便模糊测试目标能找到它。)

创建模糊测试目标

之后就是 cargo-fuzz 的常规流程:在 Cargo.toml 中创建模糊测试目标,让它们加载并调用你的 DLL:

1
2
3
4
5
6
[[bin]]
name = "fuzz_your_dll_1"
path = "fuzz_targets/fuzz_your_dll_1.rs"
test = false
doc = false
bench = false

构建 DLL 并运行

要构建 DLL 再运行模糊测试目标,需要执行两条独立命令:

1
2
# 使用「虚拟」目标构建 DLL
cargo fuzz run --features=build_your_dll --no-include-main-msvc --strip-dead-code build_your_dll

(有关为何需要这些选项的更多信息,请参阅「技术细节」。)

1
2
# DLL 构建完成后运行模糊测试目标
cargo fuzz run fuzz_your_dll_1

技术细节

(为何必须这样模糊测试 DLL?点击此处查看详情。)

通过 cargo-fuzz 进行模糊测试的代码必须插入额外插桩进行编译。生成的二进制文件行为正常,但会执行 cargo-fuzz(底层使用 LibFuzzer)可利用的额外代码,以获取模糊器输入下目标程序行为的反馈。由此建立模糊测试「反馈循环」,模糊器可逐步生成在目标程序中产生新行为的更「有趣」输入。

在本例中,目标程序是 Windows DLL。由于是 DLL(共享库),必须与用于测试它的任何模糊测试可执行文件(.exe)完全分开构建并插桩为独立二进制文件(.dll)。你的模糊测试目标程序(..../fuzz/fuzz_target/*.rs)会调用该 DLL 中的函数,但这些函数实际加载到同一进程发生在运行时。

因此,构建时需要两个步骤(故上文列出两条独立的 cargo fuzz run ... 命令):

  1. 构建并安装 DLL。
  2. 构建模糊测试目标。

MSVC 与 LibFuzzer 的 main 函数

在 Windows 上,Rust 使用 MSVC 编译器与 链接器 进行构建。cargo-fuzz 的模糊测试目标不实现 main 函数; 而是使用 LibFuzzer 内置的 main 函数。(该函数实际启动模糊器。模糊器随后调用各模糊测试目标中定义的 fuzz_target!() 宏函数。)MSVC 链接器似乎无法识别 LibFuzzer 的 main 函数,因而需要一点帮助才能构建模糊测试目标。

为解决此问题,cargo-fuzz 在检测到使用 MSVC 构建的系统时,会向传给 cargo build 的构建参数添加 /include:main 链接器参数。该参数 强制 MSVC 生成的可执行文件包含外部 main 符号。(有关 /include 参数的更多信息见 此处。) 这样模糊测试目标即可成功构建。

添加 /include:main 会破坏 DLL 链接

但请稍等!DLL 本质是共享库,因此不应包含对 main 函数的引用。由加载 DLL 的可执行文件负责 main。因此,若尝试用 cargo fuzz build 构建 DLL,它会添加 /include:main,我们将得到链接器错误:

1
2
LINK : error LNK2001: unresolved external symbol main
C:/..../my_shared_library.dll : fatal error LNK1120: 1 unresolved externals

为避免此问题,我们使用 --no-include-main-msvc 参数,以控制是否将 /include:main 添加到 MSVC 链接器参数。

但移除 /include:main 会破坏模糊测试目标链接

然而……构建模糊测试目标可执行文件需要 /include:main。这使我们陷入两难:

  • 若添加 /include:main,模糊测试目标可以构建,但 DLL 不行。
  • 若移除 /include:main,DLL 可以构建,但模糊测试目标不行。

解决方案:两次独立构建

为解决此问题,需要调用 cargo fuzz ... 两次:一次构建 DLL(不带 /include:main),另一次构建模糊测试目标(带 /include:main)。为使用 cargo-fuzz 构建 DLL(我们希望如此,因为它会使用所有相关的 LLVM 覆盖率与 AddressSanitizer 编译器选项),我们实现一个提供自有 main 函数的小型「虚拟」模糊测试目标。

该「虚拟」目标不实现 fuzz_target!() 宏函数(因此不会真正进行模糊测试),但可作为构建用于模糊测试的 Windows DLL 的载体。此外,你还可以在此「虚拟」目标中添加额外代码,帮助将新构建的 DLL 安装到 Windows 系统上的正确位置。

为何使用 --strip-dead-code?

默认情况下,cargo-fuzz 调用 rustc 时会传入 -Clink-dead-code 参数。 如此处所述, 该选项控制是否指示链接器保留死代码。「死代码」指依赖(如 DLL)提供但导入该依赖的程序从未引用/使用的函数/符号。在某些情况下有用,在另一些情况下有害。

对某些 DLL 而言可能有害。使用 -Clink-dead-code 构建时,目标 DLL 所依赖的各种 Windows DLL 中未使用函数/符号的引用会被包含在用 cargo-fuzz 构建得到的二进制文件中。

例如:在 windows-rs 中, Cryptography 子 crate(windows::Win32::Security::Cryptography)包含来自 infocardapi.dll 的符号。该 DLL 似乎已不再受支持,甚至在 Windows 上也不再安装。若 -Clink-dead-code 导致这些符号被包含在你的 DLL 中,运行时加载会失败,因为这些符号引用终究无法被解析,而系统上根本没有 infocardapi.dll。(你的模糊测试目标程序将以 STATUS_DLL_NOT_FOUND 失败。)

可通过在 cargo-fuzz 命令中添加 --strip-dead-code 修复此问题,该选项会在构建时移除 -Clink-dead-code 的使用。

最后修改 August 23, 2026: 更新 (499855b16)