智能合约安全:重入攻击
智能合约安全:重入攻击
本文是视频 详解常见重入攻击手段与防范策略 和 WTF Solidity 合约安全:S01.重入攻击 的学习笔记。
1. 什么是重入攻击?有哪些经典的重入攻击安全事件
什么是重入攻击
重入,即重复进入,也就是“递归”的含义,本质是循环调用缺陷。
重入漏洞(或者叫做重入攻击),其产生的根源在于 solidity 智能合约的特性。重入漏洞本质是一种循环调用,类似于其他语言中的死循环调用代码缺陷。
当以太坊智能合约将 Ether 发送给未知地址(地址来源于输入或是调用者)时,可能会发生此攻击(触发 Fallback 函数)。
攻击者可以在地址对应合约的 Fallback 函数中,构建一段恶意代码。当易受攻击的合约将 Ether 发送给攻击者构建的恶意合约地址时,将执行 Fallback 函数,执行恶意代码。恶意代码可以是重新进入易受攻击的合约的相关代码,这样攻击者可以重新进入易受攻击合约,执行一些开发人员不希望执行的合约逻辑。
虽然重入攻击的历史比较久,但并没有随着时间的推移而逐渐较少。现在重入攻击事件也时常发生。
曾经遭受重入攻击的项目有哪些?
2022 年 7 月 11 日,OMNI 合约遭受黑客重入攻击,黑客获利约 425.5 ETH。
2022 年 4 月 30 日,Fei Protocol 官方的 Rari Fuse Pool 遭受黑客攻击,黑客获利约 28380 ETH,月 8034 万美元,本次攻击主要利用了 Rari Capital 的 cEther 实现合约中的重入漏洞。
重入漏洞导致的安全事件还有很多,比如 The DAO 事件。重入在攻击中发挥了作用,最终导致以太坊经典的硬分叉。
2. 重入(Re-Entrance)攻击详细讲解
1. 重入攻击具体案例解读
下面使用一些简单的例子回顾一下以太坊转账重入攻击。
1.1 含有以太坊转账重入漏洞的合约
攻击流程
可以看出,以太坊转账导致的重入攻击主要是由于在以太坊转账时,触发了目标的 fallback
函数,并且由于 fallback
函数可控,因此攻击者可以将回调逻辑写入 fallback
函数中,从而以开发者意想不到的执行顺序执行了代码。
此次事件也让无数开发者意识到 fallback 的负面作用。随着以太坊提案不断增多,重入的风险由以太坊转账引申到了其他标准中。
其他协议重入风险
ERC721、ERC777 和 ERC1155 相关标准代币中均实现了高级转账功能,即均有转账并通知功能。
当 ERC721、ERC1155 相关标准代币使用 safeTransferFrom
,_safeTransfer
,_safeMint
函数向合约转账或铸币均会调用通知函数(上图中给出),而 ERC777 使用 send
,transfer
,operatorSend
,transferFrom
,_send
,_mint
函数向合约转账或铸币也会调用目标合约的通知函数(上图中给出),如果攻击者再回调进目标业务合约,那么便有可能存在重入风险。
以 safeTransferFrom_safeMint
为例,为什么使用 _safeMint
反而不安全了呢?
步骤 2 会检查转账的目标合约里是否实现了接收函数。步骤 3 通过目标合约的 selector
判断合约是否实现了高级转账通知的功能。
这就满足了重入攻击的其中一个条件:调用目标合约的某个函数。
1.2 带有调用 ERC721 相关函数造成重入漏洞例子
攻击流程
可以看出 ERC721 导致的重入攻击主要是由于在 NFT 转账时,触发了目标的 onERC721Received
函数,并且由于 onERC721Received
函数可控,因此攻击者可以将回调逻辑写入 onERC721Received
函数中,当 NFT 转账目标为合约时均会触发。
所以尽管 ERC721、ERC777、ERC1155 等标准实现了转账并通知的高级调用,但也带来了重入的风险。
相关事件:idols NFT marketplace 重入漏洞
购买 buyGod()
使用了 safeTransferFrom
来转移 NFT(seller -> msg.sender
),并且删除记账 godBids[_godld]
发生在转账后。
而另一个接受出价的函数 acceptBidForBod
中,它将删除出价操作放在了 safeTransferFrom
调用之后,这是该合约能被重入攻击的另一必要条件:在 godBids[_godld]
还没被删除时,通过调用 safeTransferFrom
从而重入调用 acceptBidForGod
使得 pendingWithdrawals[msg.sender]
能不断累加,再提现即可盗走合约中的 ETH。
1.3 除此之外还有开发人员容易忽视的重入
将用户输入的代币参数完全信任。重入例子:
攻击流程
相关事件:DeFi 借贷协议 Akropolis
重入攻击一次 deposit
两次铸币。
通过分析代码发现,在调用 deposit
函数时,用户可指定 token 参数,如下图所示:
而 deposit
函数调用中的 depositToprotocol
函数,存在调用 tkn
地址的 safeTransferFrom
函数的方法,这就使得攻击者可以通过构造 “safeTransferFrom
”从而进行重入攻击。
2. 重入漏洞的总结
实际上发生重入在于:
- 合约设计时未严格按照检查-生效-交互(checks-effect-interaction)模式来设计函数实现
- 检查:即检查合约中账本变量的数值
- 生效:更改合约账本变量
- 交互:执行转账等操作
- 在相关合约中方法中调用了目标合约的某个函数,攻击者可以控制该函数进行回调
不仅是 Solidity 语言有重入漏洞的可能性,其它链也有可能因满足以上两个条件而有重入漏洞的风险。
3. 如何避免此类问题
- 严格按照上述的检查-生效-交互(checks-effect-interaction)模式的顺序实现合约的函数,即先检查状态变量是否符合要求,紧接着更新状态变量,最后再和别的合约交互;
- 对于合约中供用户输入的数据都进行“零信任”的检查和测试;
- 使用重入锁,重入锁是如下代码所示的一种防止重入函数的修饰器(modifier),包含一个默认
0
状态的_status
。第一次调用时会加锁,在调用结束后才会释放锁。这样当攻击合约在调用结束前的第二次调用就会报错,重入攻击失败。