智能合约安全:重入攻击

智能合约安全:重入攻击

本文是视频 详解常见重入攻击手段与防范策略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 含有以太坊转账重入漏洞的合约

1.1 含有以太坊转账重入漏洞的合约

攻击流程

1.1 攻击流程

可以看出,以太坊转账导致的重入攻击主要是由于在以太坊转账时,触发了目标的 fallback​ 函数,并且由于 fallback​ 函数可控,因此攻击者可以将回调逻辑写入 fallback​ 函数中,从而以开发者意想不到的执行顺序执行了代码。

此次事件也让无数开发者意识到 fallback 的负面作用。随着以太坊提案不断增多,重入的风险由以太坊转账引申到了其他标准中。

其他协议重入风险

1.1 其他协议重入风险

ERC721、ERC777 和 ERC1155 相关标准代币中均实现了高级转账功能,即均有转账并通知功能。

当 ERC721、ERC1155 相关标准代币使用 safeTransferFrom​,_safeTransfer​,_safeMint​ 函数向合约转账或铸币均会调用通知函数(上图中给出),而 ERC777 使用 send​,transfer​,operatorSend​,transferFrom​,_send​,_mint​ 函数向合约转账或铸币也会调用目标合约的通知函数(上图中给出),如果攻击者再回调进目标业务合约,那么便有可能存在重入风险。

safeTransferFrom_safeMint​ 为例,为什么使用 _safeMint​ 反而不安全了呢?

1.1 其他协议重入风险示例

步骤 2 会检查转账的目标合约里是否实现了接收函数。步骤 3 通过目标合约的 selector​ 判断合约是否实现了高级转账通知的功能。

这就满足了重入攻击的其中一个条件:调用目标合约的某个函数。

1.2 带有调用 ERC721 相关函数造成重入漏洞例子

1.2 带有调用 ERC721 相关函数造成重入漏洞例子

攻击流程

1.2 攻击流程

可以看出 ERC721 导致的重入攻击主要是由于在 NFT 转账时,触发了目标的 onERC721Received​ 函数,并且由于 onERC721Received​ 函数可控,因此攻击者可以将回调逻辑写入 onERC721Received​ 函数中,当 NFT 转账目标为合约时均会触发。

所以尽管 ERC721、ERC777、ERC1155 等标准实现了转账并通知的高级调用,但也带来了重入的风险。

相关事件:idols NFT marketplace 重入漏洞

1.2 idols NFT marketplace 重入漏洞-1

购买 buyGod()​ 使用了 safeTransferFrom​ 来转移 NFT(seller -> msg.sender​),并且删除记账 godBids[_godld]​ 发生在转账后。

而另一个接受出价的函数 acceptBidForBod​ 中,它将删除出价操作放在了 safeTransferFrom​ 调用之后,这是该合约能被重入攻击的另一必要条件:在 godBids[_godld]​ 还没被删除时,通过调用 safeTransferFrom​ 从而重入调用 acceptBidForGod​ 使得 pendingWithdrawals[msg.sender]​ 能不断累加,再提现即可盗走合约中的 ETH。

1.2 idols NFT marketplace 重入漏洞-2

1.3 除此之外还有开发人员容易忽视的重入

将用户输入的代币参数完全信任。重入例子:

1.3 除此之外还有开发人员容易忽视的重入

攻击流程

1.3 攻击流程

相关事件:DeFi 借贷协议 Akropolis

1.3 DeFi 借贷协议 Akropolis-1

重入攻击一次 deposit​ 两次铸币。

1.3 DeFi 借贷协议 Akropolis-2

通过分析代码发现,在调用 deposit​ 函数时,用户可指定 token 参数,如下图所示:

1.3 DeFi 借贷协议 Akropolis-3

deposit​ 函数调用中的 depositToprotocol​ 函数,存在调用 tkn​ 地址的 safeTransferFrom​ 函数的方法,这就使得攻击者可以通过构造 “safeTransferFrom​”从而进行重入攻击。

2. 重入漏洞的总结

实际上发生重入在于:

  1. 合约设计时未严格按照检查-生效-交互(checks-effect-interaction)模式来设计函数实现
    • 检查:即检查合约中账本变量的数值
    • 生效:更改合约账本变量
    • 交互:执行转账等操作
  2. 在相关合约中方法中调用了目标合约的某个函数,攻击者可以控制该函数进行回调

不仅是 Solidity 语言有重入漏洞的可能性,其它链也有可能因满足以上两个条件而有重入漏洞的风险。

3. 如何避免此类问题

  1. 严格按照上述的检查-生效-交互(checks-effect-interaction)模式的顺序实现合约的函数,即先检查状态变量是否符合要求,紧接着更新状态变量,最后再和别的合约交互;
  2. 对于合约中供用户输入的数据都进行“零信任”的检查和测试;
  3. 使用重入锁,重入锁是如下代码所示的一种防止重入函数的修饰器(modifier),包含一个默认 0​ 状态的 _status​。第一次调用时会加锁,在调用结束后才会释放锁。这样当攻击合约在调用结束前的第二次调用就会报错,重入攻击失败。
// 重入锁
uint256 private _status; // 重入锁

// 重入锁
modifier nonReentrant() {
    // 在第一次调用 nonReentrant 时,_status 将是 0
    require(_status == 0, "ReentrancyGuard: reentrant call");
    // 在此之后对 nonReentrant 的任何调用都将失败
    _status = 1;
    _;
    // 调用结束,将 _status 恢复为0
    _status = 0;
}
// 只需要用 `nonReentrant` 重入锁修饰 `withdraw()` 函数即可预防重入攻击

智能合约安全:重入攻击
https://alphafitz.com/2022/10/24/security-reentrancy-attack/
作者
alphafitz
发布于
2022年10月24日
许可协议