一、理论介绍
1. 模糊测试介绍
什么是模糊测试,先来看一下维基百科关于模糊测试的介绍。
模糊测试(fuzz testing, fuzzing)是一种软件测试技术。 其核心思想是将自动或半自动生成的随机数据输入到一个程序中,并监视程序异常,如崩溃,断言(assertion)失败,以发现可能的程序错误,比如内存泄漏。 模糊测试常常用于检测软件或计算机系统的安全漏洞。
在工作实践中,模糊测试通常是一种辅助测试手段,在完成功能测试之后作为补充测试来执行,对发现边界值问题、异常情况、安全漏洞,有不错的辅助效果,用于发现技术人员“意料之外”的软件缺陷。
2. Echidna介绍
这是一款用于对区块链智能合约进行模糊测试的安全检测工具,它可以基于用户自定义的属性,或者断言语句,进行自动的测试,通常用于对智能合约进行安全审计,起到辅助人工审计的作用。
在Echidna的论文中,介绍Echidna支持3种属性类型,分别为:
- user-defined properties
- assertion checking
- gas use estimation
在Echidna的github项目介绍里,Echidna的运行模式有5种:
- property
- assertion
- overflow
- exploration
- optimization
其中在进行安全测试时,最常用的是“自定义属性模式”,并且有调查显示使用此模式最多可以检测到被测合约中63%的的严重的可利用漏洞。
此外,Echidna支持对Solidity和Vyper类型的智能合约进行模糊测试,并且支持大多数主流的智能合约框架。
Echidna的架构设计如上图所示,工具的输入是被测的合约或合约集合,在这些合约中有用户自定义的属性用例,一般来讲就是找到一些属性,这些属性有在某些情况下恒为真的特点,比如用户钱包余额永远小于等于发币总量。Echidna的运行过程可以分为两个阶段:
-
第一阶段:启动Slither,对合约进行静态扫描,扫描结果将作为第二阶段的输入
-
第二阶段:模糊测试阶段。这一阶段是一个迭代过程,模糊测试将依赖被测合约的ABI、合约中定义的属性常量以及corpus语料库来生成随机交易。当运行过程中发现了违背自定义属性规律的交易时,会自动生成一个触发此场景的最小化交易序列的失败报告。Echidna会自动生成一系列交易来完成对被测合约的最大化覆盖。
二、工具使用
1.工具安装及使用方法
在对Echidna的工作原理有大概了解之后,进入到工具实践部分。
首先,工具的安装及使用,参考github上的使用说明即可完成。笔者本地环境是Mac M1芯片,安装了0.9.5版本的Slither和2.0.5版本的Echidna
环境OK之后,对Echidna源代码中的测试用例进行试验,比如:
在代码根目录下运行
echidna-test tests/solidity/basic/allContracts.sol --contract B --config tests/solidity/basic/allContracts.yaml
工具正常运行,运行时界面如下:
2.Echidna的五种工作模式
以下例子均来自Echidna的github(github.com/crytic/echi…)中的示例代码,默认在此项目的根目录下执行运行命令
(1)自定义属性
运行命令:echidna-test tests/solidity/basic/array-mutation.sol
(2)断言模式
断言模式的示例代码如下
contract Test {
TestAssert ta;
constructor() public {
ta = new TestAssert();
}
function f() public {}
}
contract TestAssert {
event AssertionFailed(string message);
function fail(uint val) public {
if(val > 128)
emit AssertionFailed("error");
}
function g() public { }
}
运行命令:
echidna-test tests/solidity/assert/multi.sol --contract TestAssert --config tests/solidity/assert/multi.yaml
运行结果:
(3)overflow模式
在示例中有一个overflow.sol的示例代码,在运行前增加运行模式的配置:
testMode: overflow
运行结果:
(4)exploration
关于这个模式,我没有找到特别明确的介绍,个人理解是任何一个智能合约都可以用此模式跑起来,不需要添加特别的测试方法,此模式的设计目的是为了收集覆盖率数据。
在github的issue里有两个相关的issue:
Benchmark mode to run without tests (#420, #409)
当中的描述如下:
We need a special mode to let echidna collect coverage without adding new code (e.g. an echidna_something function). This mode should be enabled using a config keyword (e.g. benchmarkMode: true) but disable by default.
先尝试运行:
echidna-test tests/solidity/basic/allContracts.sol --config tests/solidity/basic/benchmark.yaml
但是不知道coverage data在哪里,于是再增加一个配置:
corpusDir: "./output/”
就可以在output路径下看到覆盖率数据了
(5)optimization
运行命令:
echidna-test tests/solidity/optimize/linear.sol --config tests/solidity/optimize/config.yaml
运行结果:
三、使用经验小结
1. 最常用的模式
- Property,即默认的运行模式
2. Property模式
-
格式要求:
echidna_something() returns(bool)
- 函数必须以“echidna_”开头
- 不需要对echidna_something函数传入参数
- 比较容易找到不变量恒为真的条件,并以此来完成echidna_something函数的返回语句
-
运行特点:
- 不会对合约产生副作用
- 如果运行过程中发生了Revert,就会认为运行失败
- 在统计覆盖率时,添加的echidna_something函数不会有覆盖率统计数据
- 如果被测的不止一个合约,而是一系列合约,需要在配置文件里加入“multi-abi: true”,或者在运行命令里加上“- -multi-abi”
- 如果被测的合约中引入了外部依赖module,比如依赖了“@openzeppelin/contracts/token/ERC20/ERC20.sol”,此时需要在配置文件里加入“cryticArgs: [‘–solc-remaps’, ‘@=node_modules/@‘]”,否则会出现编译时找不到依赖项的错误
- 实践时,可以参考echidna提供的default.yaml的配置说明,按需调节echidna运行配置,以达到更好的运行效果
3. Assertion模式
- 通过在被测合约代码里加入assert断言语句,然后指定运行模式为assertion,才能进入到此模式运行
- Assertion模式的特点
- 提供了简单的函数内检方式
- 增加的断言语句会被统计到代码覆盖率中去
- 新增的断言不应该影响原本的代码
- 容易因为错误的使用断言而影响了原本函数的运行逻辑