我研究了V神的黄皮书,发现EVM里竟然藏着函数手册,虐哭你别怪我
作者:Paul Hauner
编译:老曹、Aholiab
「幂等性」是我们在编程中经常遇到的概念,在EVM的实现中也有一个类似的概念,叫做纯粹性。EVM中的纯粹性是为了明确具体操作码的安全性问题,它对合约的安全起到至关重要的影响,例如篡改合约调用的返回值、导致合约调用永久失效等。
然而,界定操作码的纯粹性并不简单,而界定操作码(虚拟机中的具体指令就是操作码,而智能合约是在EVM中运行的,最终都会映射到操作码)的非纯粹性要相对容易一些。
本文会将历数以太坊虚拟机中的所有非纯粹性操作码,以及其特性、非纯粹的理由,以及其可能造成的安全隐患呈现出来。初读下来,可能感觉像C语言的函数手册一样虐,但保存好之后却受益无穷。
在以太坊智能合约的一个调用中,提供给它的交易数据被视为”输入”,输入的纯粹性对智能合约有深远的影响。本文试图以一个智能合约为例,通过对其「纯粹性」(Purity)的界定,来让你看出如何写出「纯粹」的合约,并奉上20种「非纯粹性」操作码的特性目录。
这些隐藏在以太坊黄皮书附录H里的「函数」(操作码),稍不注意就会毁掉你的整个合约。不过在了解这些操作码之前,我们先要看看EVM的纯粹性到底指什么。
说到底,究竟是否纯粹,往往取决于具体的场景,不可一概而论。
我们通过对Vitalik Buterin和Alex Stokes的几段著名合约代码逆向,得到一些有意思的结果。GitHub项目如下:
Serpent Purity Checker in ethereum/research
LLL Port of the above Serpent Purity Checker
Serpent Purity Checker in ethereum/research
在探讨「纯粹性」这个概念前,我们需要对这个概念进行定义。那么,关于纯粹性,我能想到最好的定义是:如果给予足够的执行gas和相同的交易数据,一个合约能够总是返回相同的结果,则认为这个合约是「纯粹」的。
换句话说,纯粹的合约就是可以读取交易的数据字段,但无需交易的其他上下文;这种合约不必读取区块信息,也不必读写存储。
非纯粹性操作码的3种类型
定义了「纯粹」之后,我们就来看看什么是「非纯粹」(Impurity),我梳理了一张以太坊合约中非纯粹操作码(opcode)的列表(带*的操作码表示还尚未实现,但今后会予以实现):
操作码的值 |
助记符 |
非纯粹性类别 |
0x31 |
BALANCE |
常非纯粹性 |
0x32 |
ORIGIN |
常非纯粹性 |
0x33 |
CALLER |
常非纯粹性 |
0x3a |
GASPRICE |
常非纯粹性 |
0x3b |
EXTCODESIZE |
常非纯粹性 |
0x3c |
EXTCODECOPY |
常非纯粹性 |
0x40 |
BLOCKHASH |
常非纯粹性 |
0x41 |
COINBASE |
常非纯粹性 |
0x42 |
TIMESTAMP |
常非纯粹性 |
0x43 |
NUMBER |
常非纯粹性 |
0x44 |
DIFFICULTY |
常非纯粹性 |
0x45 |
GASLIMIT |
常非纯粹性 |
0x46 - 0x4F |
未来非纯粹性操作码的范围 |
未来非存粹性操作码 |
0x54 |
SLOAD |
常非纯粹性 |
0x55 |
SSTORE |
常非纯粹性 |
0xf0 |
CREATE |
常非纯粹性 |
0xff |
SELFDESTRUCT |
常非纯粹性 |
0xf1 |
CALL |
可能的非纯粹性调用类型 |
0xf2 |
CALLCODE |
可能的非纯粹性调用类型 |
0xf4 |
DELEGATECALL |
可能的非纯粹性调用类型 |
0xfa |
STATICCALL |
可能的非纯粹性调用类型 |
* 0xfb |
CREATE2 |
常非纯粹性 |
从上面表格可以看出,非纯粹性操作码有三种类别,分别为:常非纯粹性(always impure)、可能的非纯粹性调用(potentially impure call-type),以及未来的非纯粹性操作码(future impure opcodes)。
虽然看起来很绕口,但这三个概念却都不复杂,下面我们就一一来看看。
常非纯粹性操作码
这种操作码很容易分辨:除了可变状态、返回可变状态或提供关于执行环境的上下文(context)之外,这些操作码别无用途。任何合约一旦包括了一个「常非纯粹性」操作码,就可以马上被认为是「非纯粹」的。
未来的非纯粹性操作码
这种操作码现在还没出现,考虑到未来可能成为「非纯粹性」操作码。完全基于作者常年浸淫以太坊社区,对以太坊技术走向的推测。
现在可以不必太过关注。
可能的非纯粹性调用类型
与上述两种操作码相比,这种操作码的理解要复杂一点,它是指:调用类型的操作码(参见上面列表)可以在其他地址执行代码。根据调用指定的地址,外部调用可以是纯粹,也可以是非纯粹的。
不过,如果指定的地址是下面两种情况,调用类型的操作码就只会被认定为「纯粹」:
一个已经被确认的指定地址;
任何在
0x0000000000000000000000000000000000000001
到
0x0000000000000000000000000000000000000008
范围之间的预编译地址。下文中,我们会详细说这个部分。
除此之外,对外部拥有的(非合约)地址的任何调用都应被视为非纯粹的,因为这类地址中,很可能包括非纯粹性的操作码。
如何确定调用类型操作码地址?
上面讲到调用指定的地址对合约纯粹性的影响,那么,如何才能确定调用类型操作码的地址呢?
正如上面说的,只有调用特定的地址,调用类型的操作码才能被认为是纯粹的。因此,为了允许一些调用类型的操作码,有必要从字节码中确定所调用的地址。
不过,考虑到在调用类型的操作码堆栈上放置地址的代码可能是任意且复杂的,只有这些代码被执行时才能被发现。因此,为了在一个以太坊交易中允许纯粹性检查,找到地址的方法应该是简单的,并且能在误报时做出反应。
基于此,我总结的方法有以下几个。
便利函数法
首先声明两个便利函数:get_opcode(n) 和 get_last_opcode_param (n)。
get_opcode(n) 返回在目标字节码数组中声明的第n个操作码,如果n不在字节码数组的范围内,函数返回None。
例子如下:
get_last_opcode_param (n) 则返回提供给目标字节码数组中所声明第n个操作码的最终参数。如果n超出了字节码数组的边界,或者第n个操作码没有参数,函数返回 None。
地址检测函数法
简单来说,如果在处理调用类型的操作码之前发现了特定的操作码模式,现在声明四个函数来返回地址。如果所有这些函数都返回None,那么该合约则被认为是非纯粹的。
首先,每个函数都输入一个c,作为有问题调用类型操作码的索引。四个函数的运行结果如下。
结果1
结果2
结果3
结果4
非纯粹性操作码特性手册
这一部分就是我们的重头了,要想写出「纯粹的」代码,就要对非纯粹的操作码特性了如指掌,在这一部分中,我们会对文章开头列表中的所有非纯粹性操作码,也是目前所有已被定义的操作码的特性进行剖析。
每个操作码都从以下三个方面进行讲解:
概况,对该操作码的简要描述;
非纯粹性理由,证明其非纯粹性的原因;
潜在攻击点,假设了一些攻击者已经部署了一个合约,并希望能够对合约的返回结果进行一些预判或临时控制(没有详尽列出潜在的攻击,它只是为示范目的提供一个例子)。
当然,关于操作码的规范可以参考以太坊黄皮书的「附录H」。
BALANCE
概况:返回一些收件人的余额。
参考:
py-evm /evm/vm/logic/context.py:balance()
非纯粹性理由:读取状态。
潜在攻击点:攻击者可以通过改变一些外部账户的收支结余来影响合约调用的返回值。
ORIGIN
概况:返回触发执行的交易发送者的地址(在 Solidity 中是 tx.origin.)
参考:
py-evm/evm/vm/logic/context.py:origin()
非纯粹性理由:读取非法交易的上下文。
潜在攻击:攻击者可以通过改变签署交易的私钥来影响合约调用的返回值。
GASPRICE
概况:返回当前的gas价格。
参考:
py-evm/evm/vm/logic/context.py:gasprice
非纯粹性理由:读取非法交易上下文。
潜在攻击:攻击者可以通过使用某种方法来改变gas价格(例如直接控制区块提出者)来影响合约调用的返回值。
EXTCODESIZE
概况:返回一些收件人所保存的代码的大小。
参考:
py-evm/evm/vm/logic/context.py: extcodesize()
非纯粹性推理:读取状态。
潜在攻击:通过调用某些预先计算的地址上部署的代码,攻击者可能会影响合约的返回值。
EXTCODECOPY
概况:复在一些地址上,复制一些代码到内存中的某些位置
参考:
py-evm/evm/vm/logic/context.py: extcodecopy()
非纯粹性理由:读取状态。
潜在攻击:攻击者可以通过将代码部署到某些预先计算的地址来影响合同调用的返回值。
BLOCKHASH
概况:返回一些过去区块(在以前256个完整区块中)的哈希值。
参考:
py-evm/evm/vm/logic/block.py: blockhash()
非纯粹性理由:读取状态。
潜在攻击:攻击者可以通过控制一部分的区块提出者和选择区块哈希来影响合约调用的返回值。
COINBASE
概况:返回区块的受益人地址。
参考:
py-evm/evm/vm/logic/block.py: coinbase()
非纯粹性理由:读取状态。
潜在攻击:攻击者可以通过控制一部分的区块提出者和所宣称的受益人地址来影响合约调用的返回值,并影响合约的调用。
TIMESTAMP
概况:返回区块的时间戳。
参考:
py-evm/evm/vm/logic/block.py: timestamp()
非纯粹性理由:读取状态。
潜在攻击:攻击者可以通过控制一部分的区块提出人来影响合约调用的返回值,并根据其对合约调用的影响来声明时间戳。
NUMBER
概况:返回块的序数(自生成以来在链中的区块数)。
参考:
py-evm/evm/vm/logic/block.py: number()
非纯粹性理由:读取状态。
潜在攻击:攻击者可以通过选择「应该包含哪个区块」的交易来影响合约调用的返回值。
DIFFICULTY
概况:返回区块的难度值。
参考:
py-evm/evm/vm/logic/block.py: difficulty()
非纯粹性理由:读取状态。
潜在攻击:攻击者可以通过对集体哈希率的某种控制,并根据其如何影响合约的调用对其进行修改,从而影响合约调用的返回值。
GASLIMIT
概况:返回区块的gasLimit。
参考:
py-evm/evm/vm/logic/block.py: gaslimit()
非纯粹性理由: 读取状态。
潜在攻击:攻击者可以通过使用某种方法来改变gasLimit(例如直接控制区块提出这或者对网络进行扫描)来影响合约调用的返回值。
SLOAD
概况:从存储中返回一个字。
参考:
py-evm/evm/vm/logic/storage.py: sload()
非纯粹性理由:读取状态。
潜在攻击:如果遵循所有其他纯粹性指令,提交人不知道有任何使用SLOAD的攻击。然而,如果将攻击与SLOAD操作码(其他攻击可能是可能的)结合起来,就可以理解为攻击。
SSTORE
概况:从存储中返回一些字。
参考:
py-evm/evm/vm/logic/storage.py: sstore()
非纯粹性理由:读取和变异状态。
潜在攻击:如果遵循所有其他纯粹性指令,提交人并不知道使用 SSTORE的任何攻击。然而,如果将攻击与SSTORE或GAS操作码(也可能是其他攻击)结合起来,就可以理解为攻击。
CREATE
概况:创建给定代码的一个新帐户。
参考:
py-evm/evm/vm/logic/system.py: Create.__call__()
非纯粹性理由:读取和变异状态。
潜在攻击:在撰写本文时,如果遵循所有其他纯粹性指令,提交者就不知道使用CREATE的攻击。然而,如果将攻击与EXTCODESIZE操作码结合起来,就可以想象为攻击。
SELFDESTRUCT
概况:注册账户删除,将剩余的以太币发送到其他地址。
参考:
py-evm/evm/vm/logic/system.py: _selfdestruct()
非纯粹性理由:读取和变异状态。
潜在攻击:攻击者可能会自毁一个合约,导致未来所有的调用都会失败。
CALL
概况:对某些地址的消息调用。
参考:
py-evm/evm/vm/logic/call.py: Call()
可能的非纯粹性理由:从另一个帐户执行代码。
潜在攻击:攻击者可以调用非纯粹性合约并使用其返回的数据。
CALLCODE
概况:使用此帐户的状态来执行其他帐户的代码。
参考:
py-evm/evm/vm/logic/call.py: CallCode()
可能的非纯粹性理由: 从另一个帐户执行代码。
潜在攻击: 攻击者可能通过callcode来调用非纯粹性合约,并且读取或变异状态。
DELEGATECALL
概况:使用该帐户的状态执行其他帐户的代码,同时保留发件人和值
参考:
py-evm/evm/vm/logic/call.py: DelegateCall()
可能的非纯粹性理由:从另一个帐户执行代码。
潜在攻击:攻击者可能会委托一个非纯粹性的合约,并且读取或者变异状态。
STATICCALL
概况:在不持久化状态修改的情况下对某些地址进行消息调用。
参考:
py-evm/evm/vm/logic/call.py: StaticCall()
可能的非纯粹性理由:从另一个帐户执行代码。
潜在攻击:攻击者可以调用非纯粹性合约并使用其返回的数据。
CREATE2
概况:创建给定代码的一个新帐户给和一些Nonce,相对于使用当前帐户nonce 的CREATE(目前尚未出现)。
参考: EIP86。
非纯粹性理由:读取和变异状态。
潜在攻击:攻击者可以制作一个只在第一次调用会成功的合约,但是在其他任何时候都会失败。
最新热文:
扫码加入区块链大本营读者群,群满加微信 qk15732632926 入群
了解更多区块链技术及应用内容
敬请关注: