Uncovering Smart Contract VM Bugs Via Differential Fuzzing
Maier D, Fäßler F, Seifert J P. Uncovering Smart Contract VM Bugs Via Differential Fuzzing[C]//Reversing and Offensive-oriented Trends Symposium. 2021: 11-22.
基于覆盖和状态对智能合约虚拟机的行为进行模糊测试,提出了NeoDiff——第一个反馈导向的智能合约虚拟机的模糊测试框架
除了对EVM进行模糊测试,NeoDiff发现了若干Neo区块链上的重要漏洞
通过高层的语义变异器,发现了Python编写的Neo智能合约和传统CPython编写的合约的不一致
- 发现了C#的Neo虚拟机中的内存损坏问题
Introduction
智能合约虚拟机类似于传统安全研究范畴内的操作系统,执行合约的虚拟机定义了如何进行交互的接口,没有文件系统、套接字和线程,而是直接访问区块链,因此出现了新的安全问题
智能合约虚拟机需要保证执行结果的一致性
2500行代码实现了NeoDiff
反馈部分不仅考虑了覆盖率,还考虑了虚拟机传回的状态传播
尽管对以太坊的两个虚拟机的测试仅返回了假阳的结果(可能是因为严格的手工测试),额外发现了很多其他生态系统中的漏洞,如Neo
Neo是一个被研究较少的区块链,2021年日均交易量达1.5亿美元。项目地址:Neo 智能经济
NEO (NEO) Definition (investopedia.com)
在Neo上NeoDiff发现了VM的不一致性、主链上的内存损坏,因为Neo提供了利用python等传统编程语言写智能合约的选项,因此可以利用python的语义进行模糊测试
和传统模糊测试不同的是,差分模糊测试想要实现的是输出或状态的不一致,NeoDiff支持对不同编程语言实现的系统进行模糊测试(neo-python VM 客户端和Neo VM C# 共识节点),
发现了CPython and neo-boa Python和智能合约上的语义差别
贡献如下:
开发并开源了NeoDiff,对智能合约虚拟机定制的模糊测试工具
实现了NeoDiff的后端,来模糊测试openethereum against the geth Ethereum VM和
Neo VM against neo-python.- 测试了CPython和Python实现的合约的语义,发现了语义的差距和对应的安全后果
- 讨论了如何利用智能合约虚拟机中的不一致性进行攻击(如攻击区块链网络上的应用
- NeoDiff帮助发现并修复了Neo智能合约生态系统中的重要bug
Background
2020年FC有一篇讨论NEo的BFT方案的论文,Neo采用PoS,参与共识的节点数量较少;目前支持8种编程语言编写合约(Python, Go, JavaScript, Java and C# /C C++ to be done);Neo的编译器调用对应语言的官方编译器,将IR转换成AVM 码,即虚拟机的字节码。
参与共识的节点运行C#编写的Neo VM构成区块链网络的核心部分,为了调用区块链,用户需要使用客户端程序,提供了SDK供开发者在NEO区块链上编写应用程序和合约交互;SDK需要能够理解所有交易并本地执行合约
资产:Neo区块链原生的资产是代币Neo和部署、执行合约需要的GAS;NeoGas随时间生成并根据拥有的Neo成比例地分配给钱包;每个节点本地 都能执行字节码形式的合约,消耗的gas和opcode以及syscall相关联。共识算法是基于PBFT的DelegatedBFT
虚拟机:负责执行上传的合约,每个参与者都必须有VM来执行合约,和传统的VM不同,Neo VM不提供硬件和文件系统访问,直接和区块链的数据交互。本地执行合约允许客户端从合约存储中直接读取数据,而不用通过缓慢的API进行查或相信中心化的节点
C#虚拟机:执行引擎是核心部分,包括函数和调用栈。执行合约时创建一个对应的上下文。上下文中包括代码和两个栈每个合约在隔离的上下文中执行,合约可以使用两个栈
- altstack:临时数据存储
- 执行栈:存储结果
- 每次调用生成新的调用栈
架构如图
持久存储:大多数数据(例如区块链的交易和合约的永久K-V存储)均可以通过syscall操作码访问;区块链本身仅存储交易,包括了调用合约的交易;为了得到当前存储的状态,所有交易都必须被重放(重新执行),存储可能存了认证消息或代币的余额。可以给予另一个合约访问存储的对象的权利
- 实现中的漏洞或不恰当的授权可能导致存储相关的问题,如delagatecall
- Johannes Krupp and Christian Rossow. 2018. teether: Gnawing at Ethereum to
Automatically Exploit Smart Contracts. Proceedings of the 27th USENIX Security
Symposium (2018), 1317–1333.
- Johannes Krupp and Christian Rossow. 2018. teether: Gnawing at Ethereum to
- 实现中的漏洞或不恰当的授权可能导致存储相关的问题,如delagatecall
Related work
在智能合约概念出现之前,差分模糊测试被用于测试编译器和代码库
- CSmith:自动生成C程序,寻找不同编译器和优化层级带来的程序行为的差异 2011
- Diffuzz:通过差分模糊测试进行侧信道分析
- NEZHA:对二进制代码库进行差分模糊测试,除了代码覆盖率之外,采用了多种反馈
- HyDiff:和NEZHA类似,关注覆盖率的同时,额外利用了静态分析和符号执行,采用了一系列的启发式推理改善对变异输出的
- EVMFuzzer:生成合约并提供给以太坊虚拟机发现不一致性,在solidity层面对合约进行变异
提到了EVMFuzzer,世界线收束
NEODIFF
对同一个虚拟机的多个可选实现进行差分模糊测试
Design
反馈导向,可以采用不同量级的反馈机制来对不同编程语言实现的VM进行测试
和EVMFuzz相比,NeoDiff更底层,会枚举所有可能的操作码序列,然而solidity编译器可能不会生成无意义的操作码(假设VM之前很少接受这种高级语言不会生成的序列的测试)
- Diff generator:采用基于反馈的变异器生成VM执行的字节码
- 拼接、添加高覆盖率的片段、随机翻转字节,能够生成高级编程语言无法生成的操作码序列
- mutator:可以针对特定目标定制不同变异器;默认NeoDiff循环采用下列的变异器,直到达到最小的合约长度;由于可能生成全新的合约,不容易陷入局部最优解;会将成功执行的字节码序列保留下来;方便定义其他特定链的变异器
- 随机1字节
- 全拼接:添加
部分第二个随机合约的代码,倾向于能够覆盖新分支的测试用例 - 部分拼接:添加部分第二个随机合约的代码,倾向于能够覆盖新分支的测试用例
- 字节插入:一个字节插入到当前合约的随机位置
- 特殊操作:特殊的字节码,比如ETH的PUSHBYTES
- Diff Analyzer:需要修改EVM的实现来生成trace,trace的内容包括执行的操作码的列表,对应的类型hash $\tau$和状态hash $\sigma$,分析器通过比较$\sigma$来发现不同,通过$\tau$判断是否到达了新的状态
- 假设当中间的$\sigma$不同时,认为出现了分叉
- 出现不一致时,导致问题产生的操作码和对应的$\tau$存储在结果中
- 分叉之前正确的操作码和对应的$\tau$会前向传播到minimizer
- minimizer试图寻找到能导致特定$\tau$的最短字节序列并且存储到$\tau$ map中去
- $\tau$ map随着模糊测试的进行不断增长,后续可以用于变异的反馈
state-hash
为了标识不同的执行,利用了状态哈希$\sigma$,取当前执行中的状态的子集,由研究者定义对于diff重要的信息,将该信息包括在哈希中
- 对于基于栈的虚拟机而言,包括对栈的概率性采样;
- 如果有寄存器应该包括寄存器的值
- 如果有额外的内存,需要考虑内存
执行速度和精度之前需要进行trade-off,可能的话对每个操作码都更新$\sigma$,利用它来检测不一致性
Type-Hash
思路:利用type-hash来作为一种轻量级的覆盖率的表示
也包含了状态信息,但是没有和之前的状态连接起来,仅标识当前操作码的状态
由于Neo VM支持多种类型(整数、字节数组等),以太坊仅支持256比特的整数作为类型,设立了虚类型方便执行
NeoDiff支持在传统的代码覆盖率下进行测试,如果采用了类型哈希,可以用来对差异进行排序和分类
类型哈希 $\tau $ 利用栈顶的2个类型,前缀为当前的操作码,执行加法命令,栈顶元素是整型1和字节数组2,对应的类型哈希 𝜏 为ADD_12
问题:类型哈希变化一定意味着覆盖率的变化吗?实验结果:86%的情况下,发现新的类型哈希 𝜏 同样标识到达了新的代码覆盖率。结果表明类型哈希能够表示所有的覆盖率变化,NeoDiff同样支持采用实际的覆盖率作为类型哈希
Minimization
利用测试用例再次执行VM,对于每个字节检查是否包含了之前的 𝜏 ,一旦发现了所有的类型哈希,认为当前长度是最短的
根据操作码导致的类型哈希来进行分类
Eval
工具已经开源,后续有空测试一下
fgsect/NeoDiff: Differential fuzzing for Smart Contract VMs (github.com)
- C# VM and neo-python VM
- 测试了4个变异策略
- random:字节全随机
- mut1p:将特定值压入栈,利用覆盖率反馈的概率较低
- mut20p:基于反馈的变异,概率是20倍
- coverage:默认变异策略,初始是随机的
- 然而当运行深度增加,随机策略丧失了多样性
- 测试了4个变异策略
- geth vs. openethereum
- 发现了6个不同,均是配置信息,未能在现有的区块链上复现
- cpy vs. Neo py:额外设计了py的语义变异器,不基于状态,能够产生有效的python脚本能够作为Neo智能合约运行
Discussion
合约虚拟机差异的安全影响
- 链上差异:可能导致算力分叉,大多数算力采用的实现将成为主流,可能导致链分叉
- 区块链应用差异:钱包和Dapp本地执行结果和主网不一致,导致出现本地分叉
- 合约语义差异:python写的合约和solidity写的合约行为不一致,恶意的开发者可能设计后门
对Neo VM差异的PoC
给了比较详细的PoC和脚本
Ethereum VM Differences
表明进行差分模糊测试时应该在空白的链上,采用相同的初始设置
或者手动删除这些假阳性
语言语义的差异
- 语义差异:
- 编译时报错:neo-boa不支持range()和float
- 字符串连接:
- +:neo-python编译成VMOP.ADD,整数加法
- ‘xxx’+’!!!’=’xxx!!!’, not ‘yyy’
- +:neo-python编译成VMOP.ADD,整数加法
- 字符串乘法
- ’x’21 和 ’x’20
- 潜在的漏洞:Neo VM会进行类型转换,之前的安全机制如给key加前缀可能不再安全
Neo VM的差异和漏洞
- 类型转换
- 执行引擎的差异
- 数学操作的不一致
- VM崩溃
实验
仓库地址:https://github.com/fgsect/NeoDiff
配置
1 | git submodule init |
openethereum经典下不下来,但是git checkout似乎没问题,先看看能不能编译出来
安装go
安装go,选择1.16.15,https://golang.google.cn/dl/
1 | sudo tar -C /usr/local -xzf go1.16.15.linux-amd64.tar.gz |
报错
1 | alleysira@LAPTOP-M4HO8L6S:~/NeoDiff$ make -C ./go-ethereum all |
挂代理用 source ../proxy
,curl www.google.com
检查,作用不大,卒
换国内的代理解决
1 | go env -w GO111MODULE=on |
build openethereum
1 | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh |
换ustc的源
1 | cd ./openethereum/bin/evmbin |
卡在441/442
安装虚拟环境
1 | echo "Installing reqs for us, creating virtualenv" |
轻松愉快
安装Neo python的环境
本来是轻松加愉快,缺一个leveldb,补上就行
1 | cd neo-python |
运行
在虚拟环境.env
中运行
1 | . .env/bin/activate |
运行100轮,测试geth和openEthereum查看结果
平均运行一轮消耗时间为76/25=3
运行了22小时,379轮
在utils目录下运行analyse_data.py得到对应的csv文件
问题和思考
- 变异策略生成的合约操作码能够执行的比例占多少
- 为什么说Ethereum VM没有类型
- 和EVMfuzzer相比,对于不一致性的对比更加细致(不再仅思考输出的不一致性,还考虑了中间过程)
- 状态哈希需要研究者手动定义对于diff来说哪些信息重要
- minimization认为最短的序列不一定是最短;代码可能有跳转
- python语义的对比:为什么是和python2 和 3对比,而不是直接比较
本文定义的变异策略显示coverage已经足够好了