【精通以太坊】——第九章 智能合约安全
智能合约安全
安全最佳实践
最小化/简单化
代码重用
代码质量
因为你处在航天工程那样或其他类似的零容错的工程领域之中
可读性和可审计性
智能合约是公开的,任何人都可以获得其字节码并进行反向工程
因此智能合约很契合在开源社区协作开发
测试覆盖率
尽可能测试所有情况
安全风险
重入
漏洞
外部的恶意合约通过函数调用来重新进入合约代码执行过程。通过使用回退函数来重入合约函数。
防范技术
- 尽可能使用内置的transfer函数向外部合约发送以太币。transfer函数只会提供多余的2300gas,这些ETH只够用来用于执行外部合约发送ETH,没有多余的gas可以用来重入合约
- 所有对状态变量的修改都在向其他合约发送以太币之前来执行
- 引入互斥锁,增加一个状态变量来在代码执行中锁定合约,避免重入的调用
整数溢出
漏洞
比如unit8范围是[0,255],用来保存256时就溢出变为了0,257变为1,以此类推。类似于pwn中的整数溢出漏洞。在solidity中只有整数,因此要注意加减乘等等的溢出,包括上溢下溢。除法不会导致溢出,但除以0时会抛出异常
防范技术
使用或构建安全的算数运算的库合约来代替标准的算数操作,如使用OpenZeppelin的SafeMath.sol
意外的以太币
漏洞
并不是所有的向一个合约传ETH的动作都会调用合约函数来处理。两种方法:1.合约的析构函数执行时向外部转ETH不会引发任何函数的执行,包括回退函数。2.在合约注册生效之前,向合约地址发送ETH。这种手法的基础是,合约的地址是可以被确定的,可以被预知。攻击者向合约发送以太币,导致合约的初始数值非零,产生一些负面影响。
防范技术
避免依赖合约余额的具体数值(this.balance),想要储存一个合约的充值数额应该自定义变量在payable函数记录数额变动。
DELEGATECALL
DELEGATECALL是运行在调用合约的上下文,CALL则是运行在目标合约的上下文
漏洞
可能导致非预期的代码执行结果
防范技术
使用DELEGATECALL要非常仔细地注意库合约和主调用合约的可能的调用上下文
并尽可能构建无状态的库合约
默认的可见性
漏洞
solidity中,函数默认可见性是public,因此如果函数不指定可见性,函数就是可以被外部用户调用的
防范技术
对合约中的所有函数明确指定可见性
无序错觉
漏洞
区块链是一个确定性的系统,任何来自于区块链系统内部的随机数都是伪随机的。这意味着可以被人为控制。包括哈希值、时间戳、区块号或者gas上限。一个例子是,如果使用未来的区块哈希值判定赌局,矿工可以不发布不利于自己的区块,而是重新打包区块直到得到有利的新的区块哈希值再发布。如果使用过去的变量更加灾难,因为这些对所有人都是透明的,也就更容易被攻击者知晓
防范技术
无序性(随机性)必须来自区块链外部,如使用RandDAO
外部合约引用
漏洞
在部署时更改外部合约引用,从而运行攻击者自己的代码
防范技术
- 使用new创建引用的合约,这样以来部署的用户也无法在不更改代码的情况下替换改引用合约。一旦代码发生变化,合约字节码就发生变化,合作用户就能发现代码被篡改过
- 对外部合约地址进行硬编码。将外部合约地址设定为public,用户就可以轻松检查合约所引用的外部合约,否则则认为可能存在问题。
短地址/参数攻击
漏洞
智能合约中,如果传递的参数不符合ABI规范,EVM会自动使用0来补齐缺失的位。攻击者故意传一个位数少了的地址参数,让没有检查正确性的合约使用0自动补齐,进而产生不良影响。
防范技术
所有外部应用在把输入参数发送到区块链之前都应该对它们进行校验,包括参数的顺序等都同样扮演着重要角色
未检查的调用返回值
漏洞
Call和send函数会返回一个布尔值来指明调用是否成功。即使外部调用失败,执行了这些函数的那个交易也不会revert回滚
防范技术
调用call和send函数后检查布尔值,以分别处理成功以及失败的场景。另外尽量使用transfer函数来执行传输
竞争条件/预先交易
漏洞
攻击者监视交易池中的交易,如果交易中包含了对某个问题的答案,那么攻击者可以去修改或者撤销解决者的权限,或者把合约修改为对解决者不利的状态。然后,攻击者从解决者的交易中获取数据,并用更高的gasPrice创建他们自己的交易,这样他们的交易就会优先于原始交易被打包到区块中。
直白点:攻击者作弊偷了答案,并用更高的gasPrice让自己优先交卷。产生这种攻击的原因是交易打包按gasPrice排序,gas高者会被先打包。
防范技术
有两种角色可以发动攻击,用户(通过修改交易的gasPrice)和矿工(矿工可以按任意顺序随意打包交易,但只有在挖到矿的时候才能攻击成功)。
- 为了防范用户的攻击,可以将gasPrice设置为上限,这样可以防止其他用户提高gasPrice来竞争打包。但对矿工无效,因为矿工可以以任意顺序打包交易。
- 提示-揭示策略:发送带有隐秘信息的交易(通常是一个哈希值),当交易被包含到区块后再发送一个交易揭示先前发送的数据。如ENS智能合约允许用户先提交它们希望话费的以太币数量,同时附带任意数量的以太币,然后在揭示阶段,用户可以获得以太币返还
- 水底发送策略:NULL
拒绝服务
漏洞
- 基于可被外部操纵的映射或数组的循环:Distribute向多个账户分发代币时,攻击者创建过多的账户,使该合约的gas消耗超过gasLimit
- 主人的操作:必须经过拥有特权的主任来进行某些操作才能进入下一步状态,如果主人丢失了私钥,这个合约就进行不了下一步了
- 基于外部调用来修改状态:比如创建一个不接受转账的合约,而新的状态需要先将以太币被全部取走才能进入下一步
防范技术
- 第一个例子,合约不应该基于一个可以被外部用户人为操作的数据结构来执行循环
这里推荐使用取回模式,只能让取款人单独地调用withdraw函数来取回他们各自的代币 - 第二、三个例子,可以使用时间解锁机制。保证意外发生,也可在到期时自动进行下一步
区块时间戳操纵
漏洞
矿工是可以操纵时间戳的,但是时间戳必须单项正增长,同时矿工也不能指定过远的将来时间戳。
防范技术
时间戳不应该被用来作为无序数据或生成随机数,或者说不能用来作为重要的状态变动。如果确实有需要时间相关逻辑,则应该使用block.number和平均区块时间来估算(一个区块大约是10s)。或是简单指定一个区块号作为条件。
小心使用构造函数
漏洞
如果合约名称被改动后,构造函数忘记修改,就会变成普通的可被调用的public函数,从而引发攻击
防范技术
0.4.22版本的solidity编译器引入了constructor关键字来制定构造函数,因此此漏洞已经不存在了
未初始化的存储指针
漏洞
EVM是用存储或内存来保存数据的,不适当的变量初始化可能会产生有漏洞的合约
防范技术
注意为初始化的存储变量警告
在处理复杂数据类型时严格使用memory或storage标识符
努力让行为符合预期
浮点数和精度
漏洞
旧版solidity没有浮点型,需要特殊处理
防范技术
有时候先乘后除精度会更高
Tx.Origin验证
Solidity中的一个全局变量,它会回溯整个调用栈并返回最初发起这个调用或交易的账户地址
漏洞
如果受害者的合约A是通过tx.origin==owner来授权提取余额的,那攻击者可以编写攻击合约B,诱导受害者调用合约B(如在将合约B伪造成外部账户在社交活动中钓鱼),而实际上合约B调用了合约A(前提是转入了足够的gas),从而将合约A中的余额转入攻击者账户上
防范技术
智能合约不应该使用tx.origin来进行验证授权。Tx.origin的合理用法如:可以用来拒绝外部合约调用当前合约