返回论坛

智能合约安全审计:Contract Size Check 的技术攻防解析

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

查找币安全研究院

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

查看研究院 研究报告中心
## 背景概述:重入攻击的防御与绕过 在区块链安全领域,重入攻击一直是智能合约面临的主要威胁之一。在第一期技术分享中,我们讨论了多种防御重入攻击的策略,其中一种常见思路是通过验证调用者身份来禁止合约调用。本期,查找币安全团队将深入剖析 **Contract Size Check** 这一技术方案,揭示其原理、实现方式以及潜藏的安全隐患。 ## 核心技术原理:extcodesize() 的工作机制 在以太坊虚拟机(EVM)中,合约地址与外部账户(EOA)地址存在本质区别:合约地址在部署后会被写入字节码,而 EOA 地址则没有。基于这一特性,我们可以通过检测地址关联的字节码大小来判断调用者的身份。 Solidity 提供了内联汇编函数 `extcodesize()`,用于返回指定地址的代码大小。以下是基础实现示例: ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; contract CheckCodeSize { function Size(address account) public view returns (uint256) { uint size; assembly { size := extcodesize(account) } return size; } } ``` **关键测试结果:** - 传入 EOA 地址(如 `0x787...cabaB`)→ 返回 `0` - 传入合约自身地址(如 `0xC58...905F2`)→ 返回 `348`(合约字节码大小) ## 漏洞构造:看似安全的检查逻辑 基于上述原理,开发者可能会设计以下防护逻辑: ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; contract Target { bool public pwned = false; function isContract(address account) public view returns (bool) { uint size; assembly { size := extcodesize(account) } return size > 0; } function protected() external { require(!isContract(msg.sender), "no contract allowed"); pwned = true; } } ``` ### 漏洞分析 `isContract()` 函数通过检查字节码是否大于 0 来判断调用者是否为合约。`protected()` 函数则利用这一检查结果来限制合约调用。然而,这里存在一个关键漏洞:**合约部署过程中,构造函数(constructor)的执行发生在字节码写入之前**。 当合约正在部署时,其地址尚未存储任何字节码,此时在构造函数中调用 `extcodesize()` 检查自身地址,返回值将为 `0`。这意味着攻击者可以在构造函数中发起调用,从而绕过 `isContract()` 检查。 ## 攻击演示:从失败到成功 ### 正常调用(失败) ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; contract FailedAttack { function pwn(address _target) external { Target(_target).protected(); } } ``` 调用 `pwn()` 并传入 `Target` 合约地址 → 交易被 `revert`,提示 "no contract allowed"。这说明合约部署后,外部合约调用无法通过检查。 ### 构造函数攻击(成功) ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; contract Hack { bool public isContract; address public addr; constructor(address _target) { // 此时 extcodesize(this) 返回 0 Target(_target).protected(); // 调用成功后,pwned 被设置为 true } } ``` 部署 `Hack` 合约时,构造函数立即调用 `Target.protected()`。由于此时 `Hack` 合约的字节码尚未写入,`extcodesize(msg.sender)` 返回 0,`isContract()` 返回 `false`,`require` 条件通过,`pwned` 被成功设置为 `true`。 ## 安全加固方案:tx.origin 检查 针对上述绕过手法,推荐采用更严格的调用者身份验证方式——检查 `msg.sender` 与 `tx.origin` 是否相同: ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; contract SecureTarget { function isContract() private view returns (bool) { return (msg.sender == tx.origin); } bool public pwned = false; function protected() external { require(isContract(), "no contract allowed"); pwned = true; } } ``` ### 方案验证 ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; contract Hack { address public addr; constructor(address _target) { SecureTarget(_target).protected(); // 会被 revert } } ``` 部署 `Hack` 合约时,`msg.sender` 为 `Hack` 合约地址,而 `tx.origin` 为发起交易的 EOA 地址,两者不同,`isContract()` 返回 `true`,`require` 条件失败,交易被 revert。 ## 审计要点总结 作为智能合约审计人员,在审查涉及调用者身份验证的逻辑时,需重点关注以下方面: 1. **检查机制选择**:优先使用 `tx.origin` 与 `msg.sender` 的比较,而非 `extcodesize()` 检查 2. **业务场景适配**:评估是否需要完全禁止合约调用,或仅限制特定类型的合约交互 3. **构造函数风险**:警惕任何在构造函数中发起外部调用的潜在攻击路径 4. **多层防御**:结合重入锁、权限控制等多种安全机制,构建纵深防御体系 ## 结语 Contract Size Check 作为智能合约安全审计中的经典案例,揭示了看似严谨的防御逻辑可能存在的隐蔽漏洞。通过深入理解 EVM 执行机制和合约生命周期,我们能够更有效地识别和防范这类安全风险。查找币安全团队将持续分享前沿的区块链安全技术分析,助力行业构建更安全的数字资产生态。 --- *本文由查找币安全团队整理发布*
在论坛中查看和回复