EF/CF 是一种智能合约模糊测试的新方法:它不是使用一个全新的 自定义模糊测试器,而是将现有的 C/C++ 代码模糊测试基础设施重新用于 智能合约。目前,AFL++ 是主要支持的模糊测试器, 尽管对 libfuzzer 和 honggfuzz 也有一些非常基础的支持 。
为什么要使用现有的模糊测试基础设施?
在此过程中我们会遇到哪些问题?
./src/ethmutator/./src/evm2cpp/本仓库是 EF/CF 项目的主要入口点。它包含所有
相关代码,以子项目形式位于 ./src/ 中,并提供一些便利脚本,
用于安装、启动模糊测试活动,以及各种数据集,用以
测试模糊测试器(并与其他工具进行比较)。
./src/ - 包含构建和运行 EF/CF 所需的全部源码;为了
可重现性,所有直接依赖项都以 Git 子模块的形式添加。./data/ - 包含评估期间使用的数据集./scripts - 包含运行实验、安装等的脚本。./docker - 用于基于容器的工作流的 Dockerfile
./docker/tools/ 包含我们用于对比评估 EF/CF 的工具的 Dockerfile。
我们尽力在 Dockerfile 中固定我们在论文中评估所使用的
版本。./EXPERIMENTS.md - 包含一个指南,
用于复现我们论文中的实验。./examples - 包含 EF/CF 生成的示例输出我们在论文中描述了 EF/CF 的架构、实现,并总结了我们的评估 结果:arxiv.org 预印本
在学术工作中引用 EF/CF 时,请使用以下 bibtex 条目 进行引用:```bibtex @InProceedings{efcf2023, author = "Michael Rodler and David Paaßen and Wenting Li and Lukas Bernhard and Thorsten Holz and Ghassan Karame and Lucas Davi", title = "EF/CF: High Performance Smart Contract Fuzzing for Exploit Generation", booktitle = "{IEEE} European Symposium on Security and Privacy ({EuroS&P})", publisher = "{IEEE}", year = "2023", }
## 快速入门
推荐的方式是作为交互式 docker 容器运行 EF/CF。
1. 使用 shell 进入容器 ```
docker run --rm -it ghcr.io/uni-due-syssec/efcf-framework
或从克隆的存储库构建容器 ``` make gitmodules # to fetch the git submodules make container-enter
1. 编译并对 Solidity 合约进行模糊测试,直到第一个崩溃/错误
被发现: ```
efcfuzz --until-crash --out ./baby_bank_results/ --source ./data/examples/baby_bank.sol
没有 Git? 如果你使用 tarball/docker 版本,请忽略此项。
运行 git submodule update --init 以在已克隆的仓库中获取最新的子模块提交。
确保也在 ./src/eEVM 中运行此命令。```
git submodule update --init; cd src/eEVM/; git submodule update --init; cd ../../
*警告:* 运行 `git clone --recursive $repo` 或将 `--recursive` 参数传递给 `git sumbodule (update|init)` 会让 git 递归进入 AFL++ 仓库的子模块,而本项目不需要这些子模块。因此,为了节省空间,最好避免递归检出子模块。
### 容器
我们为基于容器的工作流提供以下便捷的 make 目标:```sh
make container-build # build default efcf container
make container-enter # enter default efcf container in current working dir
如果您想确保干净的构建,可以使用以下命令```sh make container-build CLEAN_CHECKOUT=1
或者,可以使用以下 docker 命令构建该容器:```sh
docker build \
-f docker/ubuntu.Dockerfile \
-t efcf:latest \
.
请注意,还有一个基于 Archlinux 和 Fedora 的 Dockerfile。它们应该 也能正常工作,但未经过同样充分的测试。
如需手动分发 Docker 镜像(例如,如果包含一些本地更改),请使用:``` make container-release docker load -i ./efcf*.tar
我们建议使用以下 Docker 选项启动:
* `--security-opt seccomp=unconfined` — 更好的模糊测试性能
* `--net=host` — 便于访问本地以太坊节点
* `--tmpfs "/tmp/efcf/":exec,size=6g` — 尽可能将 EF/CF 的临时文件放到内存盘上(减少磁盘磨损)
* `--privileged` — 用于运行 `afl-system-config` 或 `efcfuzz --configure-system`
* `-v` — 用于持久化 EF/CF 的输出数据
### 虚拟机 / 裸机
对于基于虚拟机或裸机的工作流程:```sh
make system-install # install efcf to current system (requires root or sudo rights)
请注意,许多脚本反正都基于相对目录布局运行,因此这一步主要是安装依赖项以及一些方便加入你 PATH 的工具。我们已在以下 Linux 发行版上测试过 EF/CF:
(发行版关系不大,我们测试了 LLVM 13 和 14,其中 14 是首选。LLVM 11 或 12 也许仍能用,但一如既往——越新越好。关键是要有一个与我们 fork 的 AFL++ 兼容的 LLVM。)
我们尚未在 Mac OS 上原生测试 EF/CF。很可能无法正常工作(例如,afl-clang-lto 在 Mac OS 上似乎无法运行)。最佳选择是使用 docker。```sh
make gitmodules
docker pull ubuntu:jammy --platform linux/amd64
docker build -t efcf:latest -f docker/ubuntu.Dockerfile --platform linux/amd64 .
docker run --tmpfs "/tmp/efcf/":exec,size=8g --platform linux/amd64 --rm -it -v $(pwd):$(pwd) -w $(pwd) efcf:latest
我们使用 docker desktop v4.21.1 进行了测试,基本的 EF/CF 用法可以正常工作。但请考虑以下事项:
* 如果在构建时看到段错误(segfaults):尝试增加 docker 在 Mac OS 上使用的虚拟机内存限制。
* 尝试在 docker 中使用 rosetta 启用加速——希望这样会快一点。
### 开发环境设置
这些工具通常无需安装。请按照 `system-install.sh` 脚本或 Dockerfiles 中的说明安装所需依赖。
为方便起见,我们提供了一些脚本来更新你的 `PATH`:```sh
# POSIX-like shells (i.e., bash, ...)
source ./scripts/env.sh
# for the fish shell
source ./scripts/env.fish
某些脚本需要 API 密钥来从 Etherscan 服务获取元数据(例如 ABI)。如果你有 API 密钥,则必须设置 ETHERSCAN_API_KEY 环境变量以将其传递给脚本。对于基于 docker 的工作流程,你可以使用 --env 标志启动 docker 容器,或者将你的 API 密钥放入 .etherscan_api_key 文件中,该文件会将 API 密钥嵌入 docker 容器中。
为方便起见,我们使用一个包装脚本,在启动 EF/CF 模糊器时为你处理所有细节:efcfuzz
你可以设置许多命令行选项来配置模糊器在构建和模糊测试过程中的行为。有关选项列表,请查看 efcfuzz --help。
示例
将 solidity 源代码编译为 EF/CF 原生代码,并开始模糊测试 5 分钟(即 300 秒)。```bash efcfuzz --timeout 300 --source ./data/examples/baby_bank.sol
或者,以减少的模糊测试输出启动(`--quiet` 会抑制基础
模糊测试器的输出,而 `--print-progress` 将打印模糊测试
进度的简短摘要),并在 4 个核心上启动模糊测试器。```bash
efcfuzz --quiet --print-progress --cores 4 --timeout 300 --source ./data/examples/baby_bank.sol
使用已编译的字节码,并将字节码编译为 EF/CF 原生代码,然后开始模糊测试。```bash
pushd ./data/examples/; make baby_bank.combined.json; popd efcfuzz --timeout 300 --bin-runtime ./data/examples/baby_bank.combined.json
pushd ./data/examples/; make baby_bank; popd
efcfuzz --timeout 300
--bin-runtime ./data/examples/baby_bank.bin-runtime
--bin-deploy ./data/examples/baby_bank.bin
--abi ./data/examples/baby_bank.abi
该封装器可以从 go-ethereum/erigon 节点导出合约状态,并从此处开始模糊测试。```bash
$ efcfuzz --timeout 300 --live-state 0xfffF8D17CB019E0825c478c666B251A7099df3FD
此外,你可以传递 --include-address-deps=y 来递归搜索导出合约存储中其他账户的地址,并将这些地址也包含到状态导出中。但是,这不会包含 Solidity mapping 类型中存储的其他合约。要真正递归导出整个状态,还需要同时传递 --include-mapping-deps=y 标志。
但请注意,这种递归查找可能导致较长的编译时间和较差的模糊测试性能。尤其是频繁使用的合约可能拥有大量内部状态,使用它们导出的状态会使模糊测试变慢。请检查模糊器是否能够达到超过 1k execs/sec 的执行速度。如果不能,你最好尝试构建一个人工的、更小的状态。尝试在 --dev 模式下运行本地 go-ethereum 节点,并在那里部署你的合约。然后从那里导出实时状态。
该包装器会缓存构建结果,因此第二次模糊测试运行应该会启动得快得多,因为不再需要初始编译时间。如果你只想构建并将结果放入缓存,可以传递 --build-only 参数。
示例:使用属性进行模糊测试
EF/CF 也支持基于属性的模糊测试,使用与 echidna 模糊器相同的属性定义。属性(或不变式)被表示为充当模糊器缺陷预言机的 Solidity 函数。例如,你可以添加一个 Solidity 函数:```solidity function test_property_balance() public view returns (bool) { return total_balance < 1000; }
这表示属性为:total_balance 应始终低于 1000。如果 EF/CF 通过某个交易序列设法违反了该属性,即预言机返回 `false`,它就会报告一个 bug。
要让 EF/CF 知道这是一个属性,你需要在文件中指定一组函数签名;EF/CF 会将其作为模糊测试期间要检查的属性列表读取。
最简单的方法是使用 Solidity 编译器的 `--hashes` 标志获取相关签名,例如:```
solc --hashes ./path/to/your.sol | grep test_property > property_list
现在你可以使用以下命令启动 fuzzer:``` efcfuzz --source ./path/to/your.sol --properties ./property_list -C
你还可以添加 `--disable-detectors` 来禁用内置的基于 ether 的缺陷预言机。
你可以尝试以下基于属性的模糊测试示例:```
efcfuzz \
--properties ./data/examples/harvey_baz_properties.signatures
--disable-detectors \
--until-crash --timeout 120 \
--source ./data/examples/harvey_baz.sol \
示例:对事件进行模糊测试
EF/CF 支持对通过事件表达的断言违规进行模糊测试。
实际上,我们也支持使用任意自定义事件作为漏洞预言机。
默认情况下,如果目标合约记录了以下事件之一,EF/CF 将识别为漏洞:
AssertionFailed()、AssertionFailed(uint256)、
AssertionFailed(string) 和 Panic(uint256)。```
efcfuzz --event-assertions
--timeout 120 --until-crash
--source ./data/properties-assertions-tests/verifyfunwithnumbers.sol
您还可以在文件中指定要关注的其他自定义事件主题/哈希,使用 `--event-assertions-list ./path/to/eventslist.txt`。与之前的属性列表一样,您可以通过使用 `solc --hashes` 获取格式,并将事件哈希和名称复制到事件列表文件中。
默认情况下,EF/CF 会忽略不是由目标合约发出的事件。如果您想改变这一点,请使用 `--event-assertions-target-only=n`。
(注意:您可以使用 `--assertions` 来同时启用事件和 Solidity 断言检查)
**示例:针对 Solidity ^0.8 断言的模糊测试**
目前,我们不支持对低于 0.8 的 Solidity 版本中的任意断言进行模糊测试。以前,Solidity 断言会简单地触发 `invalid` 操作码,导致一种相当强制的回退。Solidity 0.8 版本改变了这一行为,不再使用 `invalid` 操作码来回退交易,而是利用 `revert` 机制并向调用者返回错误信号。我们可以利用这种错误传播类型作为 EF/CF 中的漏洞预言机。目前,EF/CF 支持检查 Solidity 错误类型 `Panic(uint256)`。[有关 Solidity 错误的更多信息。](https://docs.soliditylang.org/en/v0.8.0/control-structures.html?highlight=assert#panic-via-assert-and-error-via-require)```
efcfuzz --sol-assertions \
--timeout 120 --until-crash \
--source ./data/assertions-tests/overflow.sol
(注意:你可以使用 --assertions 来同时启用事件和 Solidity 断言
检查)
系统规格与配置
我们建议分配 4 到 16 个核心,并为每个核心分配大约 1 GB 的内存。你
可以使用 --configure-system 标志将系统配置为高速
模糊测试模式,也可以自行配置。在 Docker 容器中,你还需要
配置宿主机以获得最佳性能。如果宿主机并非关键主机,你可以
以 --privileged 方式启动容器,并使用
/usr/local/bin/afl-system-config 将系统配置为高速
模糊测试模式(请注意,这实际上是以 root 身份运行容器)。```
docker run --rm -it --privileged efcf afl-system-config
docker run --rm -it
--security-opt seccomp=unconfined
--tmpfs "/tmp/efcf/":exec,size=6g
efcf
## 运行模糊测试实验
要在 `data/tests/` 数据集上运行实验,可以使用以下命令来构建合约及其模糊测试工具,然后使用不同的设置和多次重复等运行模糊测试器。由于这会花费相当长的时间,我们可以并行运行这些实验。我们将模糊测试实验拆分为构建步骤和模糊测试步骤。构建步骤将按顺序构建所有智能合约(不过构建本身会使用多个核心)。然后我们在后台启动 8 个模糊测试器实例,它们将获取构建步骤的构建产物并开始模糊测试运行。如果 `docker` 或 `podman` 可用,Makefile 将自动尝试在适当的容器中启动所有内容。```bash
make build-tests
make fuzz-tests CONTAINER_BACKGROUND=1 FUZZER_INSTANCES=8
我们在后台启动容器时,会禁用 seccomp 和网络沙箱。 禁用 seccomp 沙箱可提高模糊测试性能。 使用主机网络可让 EF/CF 无需额外配置即可访问本地网络中的以太坊节点。
我们使用脚本 ./scripts/run-tools-on-dataset.py 来在 docker 容器中于这些数据集上运行其他工具,例如针对 multi 数据集使用以下命令:```bash
python3 ./scripts/run-tools-on-dataset.py ./data/multi/
cd ./results/tools-multi/
python3 ../../scripts/get-tools-on-dataset-stats.py
head stats.csv
你需要调整脚本以配置工具和运行次数。
### 设置模糊测试实验
这里,我们以 `tests` 实验为例。只需在以下步骤中将字符串 `tests` 替换为实验名称:
1. 将你的数据集收集到 `./data/` 中,例如包含测试合约的 `./data/tests` 数据集。对于 solidity 合约,我们有一个通用的 `Makefile` 来构建合约:`sol.Makefile`。如果你愿意,可以复用该文件,参见 `./data/tests/Makefile` 中的示例。
2. 创建一个脚本来构建构建产物,包括所需的任何预处理/爬取步骤。例如,对于 `tests` 数据集,我们有 `./scripts/build-tests.sh` 脚本。构建产物应存储在 `./builds/tests/${contract}.build.tar.xz` 中。
3. 创建一个脚本来启动模糊测试活动,例如为 `tests` 数据集创建一个名为 `./scripts/fuzz-tests.sh` 的脚本。通常你可以使用 `./scripts/common.sh` 中的通用模糊测试活动函数。查看 `fuzz-tests.sh` 作为模板。
4. `fuzz-tests.sh` 的结果将存储在 `./results/run-fuzz-tests/` 中。
5. 为了汇总结果,我们为基于 bash 的启动脚本提供了 `./scripts/summarize.py`,为 python 启动脚本提供了 `./scripts/summarize_l.py`(`efcfuzz` 工具)。
你可能需要根据第 3 步调整这些脚本。
### 现有的模糊测试实验
#### 基准测试
* <a href="./data/multi/">`./data/multi`</a> 包含可扩展性基准测试,我们用它来评估分析工具在更长的交易序列上的扩展能力。它由三种类型的合约组成:
* `multi_gen_*.sol` - 自动合成的合约,会执行一系列 `require(input <= MAGIC)`,然后设置一个内部状态变量。如果所有状态变量都被设置,那么可以触发 `selfdestruct`(或 echidna 预言机)。
* `multi_man_complex_*.sol` - 手动创建的变体,其工作方式与 `multi_gen` 类型的合约类似,但约束条件更具技巧性(例如,除与魔法值的相等和不相等之外的其他情况)。
* `justlen_*.sol` - 这些取自 [echidna-parade 示例](https://github.com/crytic/echidna-parade/blob/main/examples/justlen.sol)
* `multi_simple_*.sol` - 健全性检查,用于验证模糊器/工具理论上能否发现需要 9 或 10 笔交易才能发现的错误。这里分析器只需在没有任何参数的情况下以正确的顺序调用 10 个函数。这对大多数分析工具来说相当容易。
* <a href="./data/throughput/">`./data/throughput`</a> 包含我们用于评估吞吐量的合约。这些合约是不同大小的选择。请注意,我们修补了这些合约中的所有漏洞,因此发现的漏洞不会影响吞吐量测量。
* <a href="./data/cov-max-testset">`./data/cov-max-testset`</a> 包含我们用于基于代码覆盖率比较模糊器的合约。
#### 漏洞检测
* <a href="./data/ethbmc-vuln">`./data/ethbmc-vuln`</a> EthBMC 检测为存在漏洞的合约列表。
* <a href="./data/ethbmc-timeouts">`./data/ethbmc-timeouts`</a> EthBMC 因超时而停止分析的合约列表。
* <a href="./data/reentrancy">`./data/reentrancy`</a> 一组易受重入攻击的合约。
* <a href="./data/sailfish-dao-tp">`./data/sailfish-dao-tp`</a> 一组已被验证包含重入漏洞的合约,属于 [sailfish 研究](https://github.com/ucsb-seclab/sailfish/tree/master/data/ground-truth) 的一部分。
* <a href="./data/sailfish-dao">`./data/sailfish-dao`</a> [sailfish](https://github.com/ucsb-seclab/sailfish/tree/master/data/bugs) 发现重入漏洞的所有合约列表。
* <a href="./data/sereum">`./data/sereum`</a> 根据 [Sereum](https://github.com/uni-due-syssec/sereum-results) 容易受到重入攻击的合约列表。
* <a href="./data/smartbugs-curated-accesscontrol">`./data/smartbugs-curated-accesscontrol`</a> 来自精选 smartbugs 的合约,归类为“访问控制”漏洞([smartbugs GitHub](https://github.com/smartbugs/smartbugs/tree/master/dataset/access_control))
* <a href="./data/smartbugs-curated-reentrancy">`./data/smartbugs-curated-reentrancy`</a> 来自精选 smartbugs 的合约,归类为“重入”漏洞([smartbugs GitHub](https://github.com/smartbugs/smartbugs/tree/master/dataset/reentrancy))
#### 测试
以下数据集包含用于测试模糊器能力的基本合成测试合约:
* <a href="./data/tests">`./data/tests`</a> 从多个来源收集的基础测试,用于检查模糊器的基本能力。全部使用 selfdestruct 预言机。
* <a href="./data/tests-not-vuln">`./data/tests-not-vuln`</a> 与 tests 相同,但不应被检测为存在漏洞。
* <a href="./data/properties-tests">`./data/properties-tests`</a> 用于基于属性的模糊测试的测试
* <a href="./data/assertions-tests">`./data/assertions-tests`</a> 用于断言模糊测试的测试。
## 模糊测试详解
我们使用包装脚本启动实际的模糊器(在我们的例子中是 AFL++)。使用 `efcfuzz` 启动器时会自动完成此操作。```bash
$ cd data/tests
$ make SimpleDAO.evm2cpp
$ cd ../../src/eEVM/
$ env AFL_BENCH_UNTIL_CRASH=1 ./fuzz/launch-aflfuzz.sh SimpleDAO
如果你已安装 tmux 和 tmuxp,那么出于开发和检查的目的,该脚本的交互式版本可能会很有用:```bash $ ./fuzz/interactive-aflfuzz.sh -b SimpleDAO
这将随后编译并进行相当长时间的模糊测试。然后你可以
`cd ./fuzz/out/SimpleDAO*` 查看模糊测试结果。我们的包装脚本在启动
`afl-fuzz` 程序之外还执行一些额外工作,即主要是
对结果进行后处理。此外,它还会生成几个便捷
脚本,用于分析生成的测试用例。
* `./a.sh` - 打印测试用例的人类可读形式,是
`efuzzcaseanalyzer` 的包装器。
* `./r.sh` - 使用与运行模糊测试器时相同的设置运行测试用例。
* `./m.sh` - 使用与运行模糊测试器时相同的设置
最小化测试用例。
* `./c.sh` - 分析导致给定测试用例的测试用例"链"。
适用于分析/优化模糊测试器。你可以快速查看,
哪个测试用例是由哪个队列条目上的哪条变异链产生的。
需要 `fzf`。
还有其他一些便捷报告,例如
* `./bugs` 和 `./bugtypes`,用于汇总已识别的任何 bug。
* `./crashes_min`,其中包含所有 `afl-fuzz`
实例的最小化崩溃。
**查看 EVM 基本块代码覆盖率**```bash
$ cat coverage-percent-all.evmcov
70.73170731707317
The script fuzz/evm-bb-coverage.sh will compute the basic block coverage
given an AFL output directory. The harness can optionally dump a trace of basic
blocks, which are then compared to a list of basic blocks that is output by
evm2cpp (i.e., the .bb_list files in eEVM/contracts/).
By default we also compute the coverage that our default generic seeds (see
eEVM/fuzz/generic_seeds produce:
脚本 fuzz/evm-bb-coverage.sh 将根据 AFL 输出目录计算基本块覆盖率。该测试工具可以选择性地转储基本块跟踪信息,然后将其与 evm2cpp 输出的基本块列表(即 eEVM/contracts/ 中的 .bb_list 文件)进行比较。
默认情况下,我们还会计算默认通用种子(参见 eEVM/fuzz/generic_seeds)所产生的覆盖率:```bash
$ cat coverage-percent-seeds.evmcov
10.5890
覆盖的基本块列表存储在文件 `all.evmcov` 中。
**查看生成的测试用例摘要**
`efuzzcaseanalyzer` 可用于查看/汇总生成的测试用例,
例如,```
$ efuzzcaseanalyzer -a ./contract.abi -s ./crashes_min/
Transactions Sequences:
--------------------------------------------------------------
TX [🪙]
deposit()[🪙];
withdraw(uint256)[↕️ ↩️ ];
withdraw(uint256)[];
--------------------------------------------------------------
Number of fuzzcases: 1
Average number of TXs: 3
Number of unique TX sequences: 1
Number of unique TX sequences (consecutive deduplicated): 1
摘要通常存储在文件 crashes_tx_summary 和
queue_tx_summary 中,但后者可能有些冗长。
分析单个崩溃的测试用例``` $ ./a.sh default/crashes/id:000000,...
$ efuzzcaseanalyzer -a ./contract.abi default/crashes/id:000000,... Block header: number: 0 difficulty: 0 gas_limit: 0 timestamp: 0 initial_ether: 0
TX with tx_sender: 54 (selector); call_value: 0x0; length: 36; block+=1; #returns=0 func: withdraw(uint256) input: { Uint(80), } TX with tx_sender: 238 (selector); call_value: 0x246ddf979; length: 4; block+=1; #returns=0 func: deposit() input: { } TX with tx_sender: 153 (selector); call_value: 0x3860e6373; length: 4; block+=1; #returns=0 func: deposit() input: { } TX with tx_sender: 166 (selector); call_value: 0x0; length: 36; block+=1; #returns=1 func: withdraw(uint256) input: { Uint(37000000000000000000), } returns: return val: 1; allows reenter: 2; data: 0x0000000000000000000000000000000000000000000000000000000000000001 TX with tx_sender: 166 (selector); call_value: 0x0; length: 36; block+=1; #returns=0 func: withdraw(uint256) input: { Uint(37000000000000000000), }
而要获取模糊测试目标的实际结果,你可以执行:```
$ ./r.sh default/crashes/id:000000,sig:06,src:000000+000010,time:1584,EM-________SAO_______AD
# roughly equivalent to running
$ env EVM_DEBUG_PRINT=1 ./build/fuzz_multitx default/crashes/id:000000,sig:06,src:000000+000010,time:1584,EM-________SAO_______AD
[...]
account 0xc4b803ea8bc30894cc4672a9159ca000d377d9a3 has balance 0x100000000000000000000000000000001bc16d67562e80000( > 0x1000000000000000000000000000000000000000000000000)
Aborted (core dumped)
这会提供大量详细输出,包括合约执行跟踪的 部分内容,以及 harness 执行的余额检查结果。
最小化崩溃输入
由于采用随机化测试方法,崩溃输入中通常会包含不相关的
交易。可以通过对崩溃输入执行最小化来缓解这一问题,
即在输入仍会导致崩溃的情况下,尽可能缩减输入。
如果你想最小化非崩溃输入,则可以使用 -M 标志,
以覆盖率作为最小化标准来启用最小化。
以下命令将缩减测试用例并覆盖该文件:``` $ efuzzcaseminimizer -oa ./contract.abi ./build/fuzz_multitx ./default/crashes/id:000000,sig:06,src:000000+000010,time:1584,EM-________SAO_______AD
[..]
=== Before minimizing: === Block header: number: 0 difficulty: 0 gas_limit: 0 timestamp: 0 initial_ether: 0
TX with tx_sender: 54 (selector); call_value: 0x0; length: 36; block+=1; #returns=0 func: withdraw(uint256) input: { Uint(80), } TX with tx_sender: 238 (selector); call_value: 0x246ddf979; length: 4; block+=1; #returns=0 func: deposit() input: { } TX with tx_sender: 153 (selector); call_value: 0x3860e6373; length: 4; block+=1; #returns=0 func: deposit() input: { } TX with tx_sender: 166 (selector); call_value: 0x0; length: 36; block+=1; #returns=1 func: withdraw(uint256) input: { Uint(37000000000000000000), } returns: return val: 1; allows reenter: 2; data: 0x0000000000000000000000000000000000000000000000000000000000000001 TX with tx_sender: 166 (selector); call_value: 0x0; length: 36; block+=1; #returns=0 func: withdraw(uint256) input: { Uint(37000000000000000000), }
=== After minimizing: === Block header: number: 0 difficulty: 0 gas_limit: 0 timestamp: 0 initial_ether: 15133991795
TX with tx_sender: 4 (selector); call_value: 0x246ddf979; length: 4; block+=0; #returns=0 func: deposit() input: { } TX with tx_sender: 4 (selector); call_value: 0x0; length: 36; block+=1; #returns=1 func: withdraw(uint256) input: { Uint(37000000000000000000), } returns: return val: 1; allows reenter: 2; data: 0x TX with tx_sender: 0 (selector); call_value: 0x0; length: 36; block+=1; #returns=0 func: withdraw(uint256) input: { Uint(37000000000000000000), }
## 阅读测试用例格式
测试用例格式专为模糊测试设计,阅读起来并非一目了然。你需要了解几个微妙之处。
* 测试用例格式被视为一个可执行交易的“队列”。一旦遇到任何问题,测试用例的处理就会停止。这包括:
* 交易发生回滚(revert)时。
* 测试辅助(harnessing)代码遇到任何错误时。
* 任何漏洞被触发并检测到时。
因此,打印出的测试用例不一定与实际执行的内容对应——末尾可能存在未执行的交易。请检查详细输出并使用最小化器(minimizer)移除这些交易!
* 同样,可能存在多余的 `returns` 或虚假的 `reenter` 标志。请使用测试用例最小化器将其移除。
* 仅当队列中存在位于应执行重入的交易之后的另一笔交易(即队列中有另一个后续条目)时,合约才会被重入。
* 即使 `reenter` 标志被设置为某个值,也不一定意味着合约会被重入,只是测试辅助代码会在可能的情况下尝试重入。例如,如果合约没有执行调用(call),则 `reenter` 标志会被忽略,因为没有重入的可能性。通常最小化器会移除任何虚假的 `reenter` 标志。
一般来说,使用测试用例最小化器可以消除许多此类问题,因此在分析生成的测试用例之前使用它始终是一个好主意。
## 已知的误报
我们观察到,在使用 EF/CF 对合约进行模糊测试时,有几种误报似乎反复出现。
* 设计上就会支付 Ether 的合约。EF/CF 的 Ether 收益漏洞预言机会将这些合约标记为易受攻击,尽管它们按设计正常运行:
* 赌博合约:许多赌博合约具有某种形式的随机性,这在以太坊中本就是一种不良实践。然而,有些赌博合约的实现方式迫使你必须猜测,例如下一个区块哈希的最后两位数字或类似内容。这可以通过承诺方案(commitment scheme)实现,即第一笔交易将用户承诺到某个值,第二笔交易触发猜测并在获胜时支付。这些合约在真实区块链上通常无法被利用。然而,在 EF/CF 的模拟区块链中,模糊器可以在观察到第二笔交易中的值之后调整承诺。这一事实对 EF/CF 实现更好的代码覆盖率很重要。不过,这也让 EF/CF 很容易识别出让模糊器在赌博合约中确定性获胜的交易序列。
* 支付利息的合约:有许多小型合约允许你投资 Ether,然后每 `N` 个区块支付一定百分比的利息。EF/CF 的模拟攻击者能够等待 `N` 个区块,然后收到利息支付,这同样会被 Ether 收益漏洞预言机捕获。
* 空投(Airdrops):一些代币合约启用空投,即向任何请求者发放代币,直到达到某些限制。例如,空投通常只在短时间内启用。如果这样的合约部署在 EF/CF 中,时间限制很可能被设置为空投仍然生效的状态。如果空投的代币可以再次出售,EF/CF 随后会将其标记为 Ether 收益。
* 急切报告可控的 `DELEGATECALL`:目前,一旦可控的 delegatecall 被调用,我们就会报告。然而,有许多合约具有一些函数,它们有意允许调用者对任意地址执行 delegatecall。但是,这些函数会在 delegatecall 之后立即无条件回滚交易。这会阻止任何状态更新或 Ether 转账持久化。通常这类函数的名称中包含“simulate”等词语,因此很容易识别。
* 这个问题可以在 EF/CF 中通过将报告推迟到执行结束来解决。然而,这会使漏洞预言机变得相当复杂。
* 目前没有修复计划。
* 初始化器可调用:我们观察到,在对从区块链导出的合约进行模糊测试时,EF/CF 有时能够调用初始化器函数,即使合约已经被初始化。正常情况下,这应该触发回滚,但在 EF/CF 的 EVM 环境中却不会。再次调用初始化器通常会导致微不足道的 Ether 收益,因为例如初始化器会设置一个 *owner* 变量或类似内容。
* 我们还不确定这个问题的根本原因是什么。不过,它通常很容易被发现,因为初始化器函数通常被命名为 `initializer`、`init` 或类似名称。
## 常见陷阱
我们尽力使其在一定程度上可用,但它仍然是一个研究原型。请做好出现故障的心理准备。以下是我们观察到的一些常见问题:
* *问:我遇到一个由 `TOKENPASTE` 宏导致的奇怪编译错误。*
答:这通常发生在 `efcfuzz` 猜测了错误的合约名称时(即它猜测的是一个抽象合约),请尝试传递 `--name YourContract` 来指定目标合约。