返回论坛

Solidity 安全:深入解析“不期而至的 Ether”攻击向量与防护策略

查找币 漏洞披露 安全研究 Web3安全 区块链安全

查找币安全研究院

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

查看研究院 研究报告中心
**作者:查找币安全团队** 在智能合约开发中,`this.balance` 常被开发者视为一个“不变量”——即合约中 Ether 余额应当仅通过 `payable` 函数进行变更。然而,这一认知存在重大安全隐患。本文将深入剖析两种绕过 `payable` 函数强制向合约发送 Ether 的攻击方式,并结合实际漏洞案例,提供切实可行的防御方案。 --- ## 攻击向量:强制发送 Ether 的两种途径 ### 1. 自毁操作(`selfdestruct`) 任何合约均可调用 `selfdestruct(address)` 函数。该函数会: - 删除合约地址上的所有字节码 - 将合约中存储的所有 Ether 强制发送至指定地址 **关键风险**:如果目标地址是合约,则不会触发其任何函数(包括 `fallback` 函数)。这意味着攻击者可以: 1. 创建一个包含 `selfdestruct()` 的恶意合约 2. 向该合约发送 Ether 3. 调用 `selfdestruct(targetContract)` 4. 无视目标合约的所有规则,强制将 Ether 注入 **技术细节**:Martin Swende 的博客详细描述了自毁操作码的诡异行为(Quirk #2),指出客户端节点若错误检查不变量,可能导致灾难性后果。 ### 2. 预先发送 Ether 合约地址是确定性的,计算公式为: ``` address = sha3(rlp.encode([account_address, transaction_nonce])) ``` 这意味着任何人可以在合约创建前计算其地址,并提前发送 Ether。当合约最终部署时,它将拥有非零余额,且未执行任何代码。 --- ## 漏洞案例分析:EtherGame 合约 考虑以下存在缺陷的合约: ```solidity contract EtherGame { uint public payoutMileStone1 = 3 ether; uint public mileStone1Reward = 2 ether; uint public payoutMileStone2 = 5 ether; uint public mileStone2Reward = 3 ether; uint public payoutMileStone3 = 10 ether; uint public mileStone3Reward = 5 ether; uint public lastDeposit; function deposit() public payable { require(msg.value > 0); lastDeposit = msg.value; } function claimReward() public { require(address(this).balance >= payoutMileStone3); // 发放奖励逻辑... } } ``` **漏洞分析**: - 合约使用 `this.balance` 作为奖励触发条件 - 攻击者可通过 `selfdestruct` 或预先发送方式,强制向合约注入 Ether - 一旦余额达到 `payoutMileStone3`,攻击者可调用 `claimReward()` 获取奖励,而实际并未进行任何有效存款 **更严重的场景**:若合约使用 `this.balance` 进行状态转换检查(如 `require(this.balance == 0)`),攻击者可通过强制注入 Ether 使合约陷入不可用状态。 --- ## 高级攻击:Parity 多签名钱包漏洞 Parity 多签名钱包漏洞是“不期而至的 Ether”攻击的经典案例,其核心设计存在致命缺陷: ### 合约架构 ```solidity contract Wallet { // 通过 delegatecall 将所有调用转发至 WalletLibrary function() payable { if (msg.value > 0) Deposit(msg.sender, msg.value); else if (msg.data.length > 0) _walletLibrary.delegatecall(msg.data); } address constant _walletLibrary = 0xcafecafecafecafecafecafecafecafecafecafe; } ``` ### 攻击路径 1. **Library 合约自身暴露**:`WalletLibrary` 合约本身也是一个合约,拥有独立状态 2. **初始化漏洞**:攻击者直接向 `WalletLibrary` 合约发送调用,执行 `initWallet()` 函数,成为其所有者 3. **自毁攻击**:作为所有者,攻击者调用 `kill()` 函数,导致 `WalletLibrary` 合约自毁 4. **连锁反应**:所有引用该 Library 的 Wallet 合约失去功能,包括资金提取能力 **后果**:大量 Parity 多签名钱包中的 Ether 永久丢失,无法恢复。 --- ## 防御策略 ### 1. 避免依赖 `this.balance` - 不要将 `this.balance` 作为状态转换或奖励发放的唯一条件 - 使用内部会计变量(如 `mapping(address => uint) public balances`)跟踪用户存款 ### 2. 实施显式检查 ```solidity function deposit() public payable { require(msg.value > 0); // 使用自定义变量而非 this.balance totalDeposited += msg.value; userBalances[msg.sender] += msg.value; } ``` ### 3. 合约地址验证 对于需要接收 Ether 的合约,可在构造函数中检查初始余额: ```solidity constructor() public { require(address(this).balance == 0, "Initial balance must be zero"); } ``` ### 4. 使用 OpenZeppelin 等经过审计的库 避免自行实现复杂逻辑,优先使用经过社区审计的标准库。 ### 5. 不变量检查的最佳实践 - 定义明确的不变量(如总量、用户余额总和) - 在关键操作前后进行断言检查 - 避免将 `this.balance` 作为不变量 --- ## 总结 “不期而至的 Ether”攻击揭示了智能合约安全中的一个常见误区:开发者往往高估了合约对 Ether 流入的控制能力。通过 `selfdestruct` 和预先发送两种方式,攻击者可以无视所有 `payable` 函数限制,强制向合约注入 Ether。 **核心教训**: - 永远不要假设合约余额仅通过 `payable` 函数变化 - 使用内部会计变量替代 `this.balance` - 对关键状态转换实施多重验证 - 采用经过审计的标准化合约库 --- **本文由查找币安全团队整理发布** *关注查找币安全技术论坛,获取更多智能合约安全深度解析。*
在论坛中查看和回复