Hi Guest

More contents, please log on!

Bitmere.com 区块链技术 Content

以太坊智能合约潜在风险

吃瓜围观小分队
36 0 0
最近作者一直在思考 EVM 和 Solidity 对不可支付合约(non-payable contracts)的错误概念,以及其可能蕴含的针对智能合约的攻击(attack vectors),该类型攻击在现有 EVM 中几乎不可修复。EVM 是以太坊、Qtum 量子链和其他一些区块链合约运行的基础。基本上写 Solidity 代码就等同于使用 EVM。这篇文章只以以太坊(ETH) 为例,不过该问题同样适用于用 QTUM 等所有兼容 EVM 的项目。

对没有经验的人来说,EVM 似乎并没有只发送 ETH 到合约,但不潜在地执行任何合约代码的方法(这样的交易会失败/抛出异常)。但事实上,确实存在比较不为人知的强制合约地址接收 ETH 的方法。

强制接收 ETH 攻击

异常的余额改变并不会影响所有合约,有些时候只会导致合约中 ETH 被锁定而永远无法使用。但有些合约可能会因此完全失效,故需要加入expectedBalance 状态字段,或者添加余额异常处理方法来避免这一问题。

人为例子:
pragma solidity 0.4.18;
contract ForceEther {
bool youWin = false;
function onlyNonZeroBalance() {
     require(this.balance > 0);
     youWin = true;
}
// throw if any ether is received
function() payable {
   revert();
}
}
另一个看上去更真实的例子:
contract EtherGame {
   uint public payoutMileStone1 = 3 ether;
   uint public mileStone1Reward = 2 ether;
   uint public payoutMileStone2 = 5 ether;
   uint public mileStone2Reward = 3 ether;
   uint public finalMileStone = 10 ether;
   uint public finalReward = 5 ether;
   mapping(address => uint) redeemableEther;
   // users pay 0.5 ether. At specific milestones, credit their accounts
   function play() public payable {
       require(msg.value == 0.5 ether); // each play is 0.5 ether
       uint currentBalance = this.balance + msg.value;
       // ensure no players after the game as finished
       require(currentBalance
       // if at a milestone credit the players account
       if (currentBalance == payoutMileStone1) {
           redeemableEther[msg.sender] += mileStone1Reward;
       }
       else if (currentBalance == payoutMileStone2) {
           redeemableEther[msg.sender] += mileStone2Reward;
       }
       else if (currentBalance == finalMileStone ) {
           redeemableEther[msg.sender] += finalReward;
       }
       return;
   }
   function claimReward() public {
       // ensure the game is complete
       require(this.balance == finalMileStone);
       // ensure there is a reward to give
       require(redeemableEther[msg.sender] > 0);
       redeemableEther[msg.sender] = 0;
       msg.sender.transfer(redeemableEther[msg.sender]);
   }
}
用户每次给合约发送 0.5 ETH,依次达到每个 milestone。攻击者可以强行将ETH发送给该合约,令其超过 finalMileStone 值,从而使合约中的所有资金都被锁定并且无法访问。 当然,这个例子也是人为设计出来的。

不幸的是,我还无法找到任何现实生活中的例子。不过,如果有目的地把代码隐藏在Solidity合约中,这些类型的漏洞可能会非常狡猾。这种性质的攻击是Underhanded Solidity比赛的第二和第三名获胜者的获胜关键。

强制发送 ETH 到合约
首先介绍一下交易是怎么发生的。假设有如下代码:
someAddress.transfer(self.balance() / 10);
这段代码有什么作用?

someAddress 可以是非合约地址,这种情况没有合约执行,除标准交易费用外不会消耗任何 gas。这永远不会失败,但也无法保证地址是真实的和可访问的
someAddress 可以是合约地址。合约可以成功交易并消耗足够的 gas 费用。

someAddress 可以是合约地址。合约交易如果失败,费用会返回,但会消费掉所有 gas,并且合约调用交易停止执行。

这看起来还行。如果合约不想收到 ETH,或者只想接收白名单上某些地址、合约的 ETH,那你对其发送 ETH 就会失败。

然而,若以其他方式发送 ETH 的话,这种安全特性会被完全击破:合约可以在被创建时接收 ETH。然后,合约可以自毁,把它所有 ETH 发送给一个易受攻击的合约,而不用调用任何目标合约的代码。

如果发送者和当前易受攻击的合约能在部署前被提前预测到,可以提前把 ETH 发送到那个地址。矿工/区块创建者能直接将区块奖励的 ETH 发送给任何合约,而不需要执行任何代码。

这些虽然都不是常规用法,但它们确实提供了一种方法来将 ETH 发送到合约并完全绕过任何阻止这一行为的代码。

在没有特殊权限的情况下,能利用起来的唯一方法就是自毁。这个操作只需要你用点 ETH 来创建合约,以及支付额外的 gas 费用。

另一个有趣的方法是在创建合约之前将ETH发送给合约。这样恶意开发者故意在合约代码里放漏洞。这类和合约余额有关的 bug 很难被发现,至少我已知的审核软件都没能明确指出错误,比如require(balance == whatever)。
可以用下面这种代码阻止漏洞:
uint256 expectedBalance = 0;
function () public payable {
   expectedBalance+=msg.value;
}

现在我们只用.transfer() 给它发送一些ETH
VM Exception while processing transaction: out of gas
因为我们现在必须读写一个状态变量,这要昂贵得多。默认的 gas 费用是2300 gas。读取一个状态变量需要200 gas,并且写一个已经存在的变量需要 5000 gas(如果之前不存在则需要20,000 gas)
所以,需要用下面代码:
someAddress.transfer.value(whatever).gas(7000); //just a guess
大多数合约在交易时使用默认gas,但这里不是默认情况...不过,仔细考虑这个问题,既然我们需要更多 gas,我们可以写个更有趣的 payable 函数:
function () public payable{
   sender.call(....);
}
所以现在为了处理所调用合约的异常的余额改变问题,我们将合约暴露到一个全新形式的攻击下。默认值为2300只能调用一个非常小的外部函数,它不进行状态修改操作,基本上只能生成一个LOG。

既然我们已经增加 gas 使得合约可以读取和存储状态,那么这个合约现在也有足够的 gas 来调用之前调用它的合约,于是就可能导致重入攻击(reentrancy attack)。我不会详细介绍所有细节,但基本上它需要一个非常谨慎的合约设计来防止这个“功能”的 bug,并且唯一100%处理掉它的方法是 gas 费用低到不能进行状态修改。没有100%的方法知道当前的合约执行是不是可重入的,EVM 并未公开这类信息。

总结
对我来说,这部分 EVM 设计毫无意义。正常接受资金需要获得批准,这似乎被说成是一种安全功能,同时却有清晰且有记录的方法来绕过它,所以它没有提供任何安全性方面的价值。

此外,如果你想减少交易费用,那么这些方法在很大程度上是你无法访问的,它们只对攻击者有用。理论上,如果你只想将代币只发送到合约,而不关心合约的执行,gas 成本应该便宜33%。

为什么EVM没有暴露这样类似的方法呢?从历史上看,这种意外行为导致了许多合约DoS攻击和漏洞。幸运的是,Solidity让这些类型错误更难以出现。但显然它不能完全解决安全方面的问题,更不用说经济方面了。

如果希望跟踪合约中确切的预期余额,那么需要支付比平常更多的 gas 并使用非标准接口,这实际上会带来更多的风险,比如重入攻击。

我们可能永远不会知道为什么EVM这样设计,但这一问题将在 Qtum x86VM 中得到解决。将来会有一种在不执行合约的情况下向合约发送 QTUM 的方法,而且它比实际执行合约更加便宜。
BitMere.com is Information release platform,just provides information storage space services.
The opinions expressed are solely those of the author,Does not constitute advice, please treat with caution.
You have to log in before you can reply Login | 立即注册

Points Rules

Write the first review

吃瓜围观小分队 小学生
  • Follow

    0

  • Following

    0

  • Articles

    3

币圈江左盟
Promoted