1.8.2 模糊测试 Windows DLL
5 分钟阅读
译文 · 基于 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:
| |
在 Cargo.toml 中添加一个需要该 DLL 作为依赖的特性。我们希望 DLL 仅在此特性启用时构建:
| |
最后,在 Cargo.toml 中创建一个需要此特性的模糊测试目标。这将是一个「虚拟」模糊测试目标,其唯一目的是构建你的 DLL。它不会真正进行模糊测试;只是让 cargo-fuzz 使用 AddressSanitizer(及其他)插桩来构建 DLL 的一种方式。
| |
创建「虚拟」模糊测试目标
接下来,需要为这个「虚拟」模糊测试目标创建源代码(fuzz_targets/build_your_dll.rs)。最简单的情况下,只需创建一个简单的 main 函数:
| |
这个「虚拟」目标会在 DLL 构建完成之后执行其 main 函数,因此如有需要,可在此添加额外代码以执行构建后安装或配置。(例如,你可能需要将构建好的 DLL 复制到系统其他位置,以便模糊测试目标能找到它。)
创建模糊测试目标
之后就是 cargo-fuzz 的常规流程:在 Cargo.toml 中创建模糊测试目标,让它们加载并调用你的 DLL:
| |
构建 DLL 并运行
要构建 DLL 再运行模糊测试目标,需要执行两条独立命令:
| |
(有关为何需要这些选项的更多信息,请参阅「技术细节」。)
| |
技术细节
(为何必须这样模糊测试 DLL?点击此处查看详情。)
通过 cargo-fuzz 进行模糊测试的代码必须插入额外插桩进行编译。生成的二进制文件行为正常,但会执行 cargo-fuzz(底层使用 LibFuzzer)可利用的额外代码,以获取模糊器输入下目标程序行为的反馈。由此建立模糊测试「反馈循环」,模糊器可逐步生成在目标程序中产生新行为的更「有趣」输入。
在本例中,目标程序是 Windows DLL。由于是 DLL(共享库),必须与用于测试它的任何模糊测试可执行文件(.exe)完全分开构建并插桩为独立二进制文件(.dll)。你的模糊测试目标程序(..../fuzz/fuzz_target/*.rs)会调用该 DLL 中的函数,但这些函数实际加载到同一进程发生在运行时。
因此,构建时需要两个步骤(故上文列出两条独立的 cargo fuzz run ... 命令):
- 构建并安装 DLL。
- 构建模糊测试目标。
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,我们将得到链接器错误:
| |
为避免此问题,我们使用 --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 的使用。