返回论坛

智能合约安全审计:自毁函数的攻击向量与防御策略

查找币 学术研究 安全研究 Web3安全 区块链安全

查找币安全研究院

钱包恢复评估 | 链上取证分析 | Web3 事件响应
以合法授权、证据保全、隐私保护和可复核流程为前提,不要求用户在线提交完整私钥或助记词。

查看研究院 研究报告中心
## 背景概述 在智能合约安全审计领域,我们通常关注三类常见的资金转移函数:`transfer`、`send` 和 `call.value().gas()()`。然而,有一类特殊的函数——`selfdestruct`(自毁函数),因其独特的强制转账特性,常常成为审计中容易被忽视的风险点。本文将深入剖析自毁函数的攻击原理,并通过实际案例展示其利用方式与防御方案。 ## 前置知识:Solidity 中的转账机制 在 Solidity 中,向合约或地址转账主要有以下三种方式: | 函数 | 行为特性 | 安全性 | |------|----------|--------| | `transfer` | 转账失败抛出异常,后续代码不执行 | 安全,但 Gas 限制为 2300 | | `send` | 转账失败返回 false,后续代码继续执行 | 需检查返回值 | | `call.value().gas()()` | 转账失败返回 false,后续代码继续执行 | 需防范重入攻击 | **关键点**:以上三种方式都需要目标地址具备接收转账的能力(如实现 `receive()` 或 `fallback()` 函数)。而 `selfdestruct` 函数则提供了一种**强制转账**的机制——无需目标合约配合,即可将合约余额发送到指定地址。 ## 自毁函数:双刃剑 自毁函数由以太坊虚拟机(EVM)原生提供,用于销毁合约并从区块链状态中移除其代码和存储。其核心特性包括: - **强制转账**:合约销毁时,剩余 ETH 会强制发送给指定目标地址,无需目标合约实现任何接收函数 - **状态清除**:合约的存储和代码将被永久移除 - **紧急避险**:可用于在紧急情况下转移资金 然而,攻击者可以利用这一特性向目标合约“强制注入”非预期资金,从而破坏合约的正常逻辑。典型攻击场景是:合约使用 `address(this).balance` 获取余额,而攻击者通过自毁函数向该合约打入额外资金,导致余额计算偏离预期。 ## 漏洞示例:幸运七游戏 ### 目标合约分析 ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.10; contract EtherGame { uint public targetAmount = 7 ether; address public winner; function deposit() public payable { require(msg.value == 1 ether, "You can only send 1 Ether"); uint balance = address(this).balance; require(balance <= targetAmount, "Game is over"); if (balance == targetAmount) { winner = msg.sender; } } function claimReward() public { require(msg.sender == winner, "Not winner"); (bool sent, ) = msg.sender.call{value: address(this).balance}(""); require(sent, "Failed to send Ether"); } } ``` ### 漏洞剖析 该合约实现了一个名为“幸运七”的游戏规则: 1. 玩家每次调用 `deposit()` 时需发送恰好 1 ETH 2. 合约通过 `address(this).balance` 获取当前余额 3. 当余额等于 7 ETH 时,当前调用者成为 winner 4. winner 可调用 `claimReward()` 提取全部余额 **漏洞核心**:`deposit()` 函数通过 `address(this).balance` 获取余额,而攻击者可以通过 `selfdestruct` 向合约强制转入任意数量的 ETH。假设攻击者在第 6 个玩家存入后,通过自毁函数向合约打入 1 ETH,此时合约余额变为 7 ETH,但第 7 个玩家调用 `deposit()` 时会因为 `balance <= targetAmount` 检查失败而无法参与游戏。更极端的情况,攻击者直接打入 7 ETH,导致游戏立即“结束”,但 winner 地址为 `address(0)`,永远无法产生 winner。 ### 攻击合约实现 ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.10; contract Attack { EtherGame etherGame; constructor(EtherGame _etherGame) { etherGame = EtherGame(_etherGame); } function attack() public payable { // 通过自毁强制向目标合约转账 selfdestruct(payable(address(etherGame))); } } ``` **攻击步骤**: 1. 部署 `Attack` 合约,并将目标 `EtherGame` 合约地址传入 2. 向 `Attack` 合约转入 1 ETH(或更多) 3. 调用 `attack()` 函数,触发 `selfdestruct` 4. `Attack` 合约销毁,其 ETH 强制转入 `EtherGame` 合约 5. 此时 `EtherGame` 余额变为非预期的 7 ETH(或更多),游戏逻辑被破坏 ## 防御方案 ### 方案一:使用状态变量跟踪余额 ```solidity contract EtherGameFixed { uint public targetAmount = 7 ether; address public winner; uint public balance; // 使用状态变量代替 address(this).balance function deposit() public payable { require(msg.value == 1 ether, "You can only send 1 Ether"); balance += msg.value; require(balance <= targetAmount, "Game is over"); if (balance == targetAmount) { winner = msg.sender; } } function claimReward() public { require(msg.sender == winner, "Not winner"); (bool sent, ) = msg.sender.call{value: balance}(""); require(sent, "Failed to send Ether"); } } ``` ### 方案二:审计检查清单 作为审计人员,在审查使用 `address(this).balance` 的合约时,应重点关注: 1. **业务逻辑依赖**:检查余额是否用于控制关键状态(如游戏结束条件、奖励分配等) 2. **强制转账风险**:评估是否存在通过 `selfdestruct` 或其他方式(如 `coinbase` 转账)注入非预期资金的可能性 3. **替代方案**:推荐使用内部状态变量跟踪余额,而非直接依赖合约地址余额 ## 总结 `selfdestruct` 函数虽然为紧急情况提供了资金转移的便利,但其强制转账特性可能被攻击者利用,破坏依赖 `address(this).balance` 的合约逻辑。审计时需特别注意: - 避免使用 `address(this).balance` 作为业务逻辑的核心判断依据 - 使用状态变量精确跟踪合约的预期资金流 - 在代码中显式处理非预期资金(如提供提现函数) 通过以上措施,可以有效防范自毁函数带来的安全风险,确保智能合约的稳健运行。 --- **本文由查找币安全团队整理发布**
在论坛中查看和回复