0%

NeoDiff

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)

image-20230925160606443

在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:临时数据存储
    • 执行栈:存储结果
    • 每次调用生成新的调用栈
    • 架构如图image-20230925171307210

    • 持久存储:大多数数据(例如区块链的交易和合约的永久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.

Related work

在智能合约概念出现之前,差分模糊测试被用于测试编译器和代码库

  • CSmith:自动生成C程序,寻找不同编译器和优化层级带来的程序行为的差异 2011
  • Diffuzz:通过差分模糊测试进行侧信道分析
  • NEZHA:对二进制代码库进行差分模糊测试,除了代码覆盖率之外,采用了多种反馈
  • HyDiff:和NEZHA类似,关注覆盖率的同时,额外利用了静态分析和符号执行,采用了一系列的启发式推理改善对变异输出的
  • EVMFuzzer:生成合约并提供给以太坊虚拟机发现不一致性,在solidity层面对合约进行变异

提到了EVMFuzzer,世界线收束

NEODIFF

对同一个虚拟机的多个可选实现进行差分模糊测试

image-20230925185409445

Design

反馈导向,可以采用不同量级的反馈机制来对不同编程语言实现的VM进行测试

和EVMFuzz相比,NeoDiff更底层,会枚举所有可能的操作码序列,然而solidity编译器可能不会生成无意义的操作码(假设VM之前很少接受这种高级语言不会生成的序列的测试)

  • Diff generator:采用基于反馈的变异器生成VM执行的字节码
    • 拼接、添加高覆盖率的片段、随机翻转字节,能够生成高级编程语言无法生成的操作码序列
  • mutator:可以针对特定目标定制不同变异器;默认NeoDiff循环采用下列的变异器,直到达到最小的合约长度;由于可能生成全新的合约,不容易陷入局部最优解;会将成功执行的字节码序列保留下来;方便定义其他特定链的变异器
    • 随机1字节
    • 全拼接:添加部分第二个随机合约的代码,倾向于能够覆盖新分支的测试用例
    • 部分拼接:添加部分第二个随机合约的代码,倾向于能够覆盖新分支的测试用例
      • image-20230925193912440
    • 字节插入:一个字节插入到当前合约的随机位置
    • 特殊操作:特殊的字节码,比如ETH的PUSHBYTES
  • Diff Analyzer:需要修改EVM的实现来生成trace,trace的内容包括执行的操作码的列表,对应的类型hash $\tau$和状态hash $\sigma$,分析器通过比较$\sigma$来发现不同,通过$\tau$判断是否到达了新的状态
    • 假设当中间的$\sigma$不同时,认为出现了分叉
    • 出现不一致时,导致问题产生的操作码和对应的$\tau$存储在结果中
    • 分叉之前正确的操作码和对应的$\tau$会前向传播到minimizer
    • minimizer试图寻找到能导致特定$\tau$的最短字节序列并且存储到$\tau$ map中去
      • $\tau$ map随着模糊测试的进行不断增长,后续可以用于变异的反馈
    • image-20230925200033512

state-hash

为了标识不同的执行,利用了状态哈希$\sigma$,取当前执行中的状态的子集,由研究者定义对于diff重要的信息,将该信息包括在哈希中

  • 对于基于栈的虚拟机而言,包括对栈的概率性采样;
  • 如果有寄存器应该包括寄存器的值
  • 如果有额外的内存,需要考虑内存

执行速度和精度之前需要进行trade-off,可能的话对每个操作码都更新$\sigma$,利用它来检测不一致性

Type-Hash

思路:利用type-hash来作为一种轻量级的覆盖率的表示

也包含了状态信息,但是没有和之前的状态连接起来,仅标识当前操作码的状态

由于Neo VM支持多种类型(整数、字节数组等),以太坊仅支持256比特的整数作为类型,设立了虚类型方便执行

NeoDiff支持在传统的代码覆盖率下进行测试,如果采用了类型哈希,可以用来对差异进行排序和分类

类型哈希 $\tau $ 利用栈顶的2个类型,前缀为当前的操作码,执行加法命令,栈顶元素是整型1和字节数组2,对应的类型哈希 𝜏 为ADD_12

问题:类型哈希变化一定意味着覆盖率的变化吗?实验结果:86%的情况下,发现新的类型哈希 𝜏 同样标识到达了新的代码覆盖率。结果表明类型哈希能够表示所有的覆盖率变化,NeoDiff同样支持采用实际的覆盖率作为类型哈希

image-20231006192746686

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:默认变异策略,初始是随机的
      • image-20230928082042644
      • 然而当运行深度增加,随机策略丧失了多样性
  • geth vs. openethereum
    • 发现了6个不同,均是配置信息,未能在现有的区块链上复现
    • image-20230928084441953
  • cpy vs. Neo py:额外设计了py的语义变异器,不基于状态,能够产生有效的python脚本能够作为Neo智能合约运行
    • image-20231006200937631

Discussion

合约虚拟机差异的安全影响

  • 链上差异:可能导致算力分叉,大多数算力采用的实现将成为主流,可能导致链分叉
  • 区块链应用差异:钱包和Dapp本地执行结果和主网不一致,导致出现本地分叉
  • 合约语义差异:python写的合约和solidity写的合约行为不一致,恶意的开发者可能设计后门

对Neo VM差异的PoC

给了比较详细的PoC和脚本

Ethereum VM Differences

image-20231006202931015

表明进行差分模糊测试时应该在空白的链上,采用相同的初始设置

或者手动删除这些假阳性

语言语义的差异

  • 语义差异:
    • 编译时报错:neo-boa不支持range()和float
  • 字符串连接:
    • +:neo-python编译成VMOP.ADD,整数加法
      • ‘xxx’+’!!!’=’xxx!!!’, not ‘yyy’
  • 字符串乘法
    • ’x’21 和 ’x’20
  • 潜在的漏洞:Neo VM会进行类型转换,之前的安全机制如给key加前缀可能不再安全
    • image-20231006204246703

Neo VM的差异和漏洞

  • 类型转换
  • 执行引擎的差异
  • 数学操作的不一致
  • VM崩溃

实验

仓库地址:https://github.com/fgsect/NeoDiff

配置

1
2
git submodule init
git submodule update ## take some time

openethereum经典下不下来,但是git checkout似乎没问题,先看看能不能编译出来

安装go

安装go,选择1.16.15,https://golang.google.cn/dl/

1
2
sudo tar -C /usr/local -xzf go1.16.15.linux-amd64.tar.gz
build go ehtereum

报错

1
2
3
4
5
6
alleysira@LAPTOP-M4HO8L6S:~/NeoDiff$ make -C ./go-ethereum all
make: Entering directory '/home/alleysira/NeoDiff/go-ethereum'
env GO111MODULE=on go run build/ci.go install
go: github.com/Azure/azure-pipeline-go@v0.2.2: Get "https://proxy.golang.org/github.com/%21azure/azure-pipeline-go/@v/v0.2.2.mod": dial tcp 172.217.163.49:443: i/o timeout
make: *** [Makefile:21: all] Error 1
make: Leaving directory '/home/alleysira/NeoDiff/go-ethereum'

挂代理用 source ../proxy,curl www.google.com检查,作用不大,卒

换国内的代理解决

1
2
go env -w GO111MODULE=on
go env -w GOPROXY=https://goproxy.cn,direct

build openethereum

1
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

换ustc的源

1
2
cd ./openethereum/bin/evmbin
cargo build --release

卡在441/442

image-20231016212223747

安装虚拟环境

1
2
3
4
echo "Installing reqs for us, creating virtualenv"
virtualenv -p "$(command -v python3)" "$(pwd)/.env"
. .env/bin/activate
pip3 install -r requirements.txt

轻松愉快

安装Neo python的环境

本来是轻松加愉快,缺一个leveldb,补上就行

1
2
3
cd neo-python
pip install -r requirements.txt
sudo apt-get install libleveldb-dev

运行

在虚拟环境.env中运行

1
2
3
. .env/bin/activate
./utils/EVMrun.sh 1
./utils/EVMscale.sh [proc_count]

image-20231016215516748

运行100轮,测试geth和openEthereum查看结果

image-20231018180131426

平均运行一轮消耗时间为76/25=3

image-20231018195339302

运行了22小时,379轮

image-20231019162646551

在utils目录下运行analyse_data.py得到对应的csv文件

问题和思考

  • 变异策略生成的合约操作码能够执行的比例占多少
  • 为什么说Ethereum VM没有类型
  • 和EVMfuzzer相比,对于不一致性的对比更加细致(不再仅思考输出的不一致性,还考虑了中间过程)
  • 状态哈希需要研究者手动定义对于diff来说哪些信息重要
  • minimization认为最短的序列不一定是最短;代码可能有跳转
  • python语义的对比:为什么是和python2 和 3对比,而不是直接比较

本文定义的变异策略显示coverage已经足够好了