Hi 游客

更多精彩,请登录!

比特池塘 区块链技术 正文

以太坊智能合约潜在风险

吃瓜围观小分队
79 0 0
最近作者一直在思考 EVM 和 Solidity 对不可支付合约(non-payable contracts)的错误概念,以及其可能蕴含的针对智能合约的攻击(attack vectors),该类型攻击在现有 EVM 中几乎不可修复。EVM 是以太坊、Qtum 量子链和其他一些区块链合约运行的基础。基本上写 Solidity 代码就等同于使用 EVM。这篇文章只以以太坊(ETH) 为例,不过该问题同样适用于用 QTUM 等所有兼容 EVM 的项目。
, _. ~( C7 N9 o; {/ r
1 L# P1 I) R  r对没有经验的人来说,EVM 似乎并没有只发送 ETH 到合约,但不潜在地执行任何合约代码的方法(这样的交易会失败/抛出异常)。但事实上,确实存在比较不为人知的强制合约地址接收 ETH 的方法。2 a! F  d0 `+ D/ I& E, a
! {$ ^  {- u' N% u* N: r, v! W
强制接收 ETH 攻击
% q% p. f7 P) z+ ^; S/ B: S' B& ]0 b9 X
异常的余额改变并不会影响所有合约,有些时候只会导致合约中 ETH 被锁定而永远无法使用。但有些合约可能会因此完全失效,故需要加入expectedBalance 状态字段,或者添加余额异常处理方法来避免这一问题。% B% d- b- z4 w: o% Y. u1 C. x( c( w

/ x7 L& `  A  d* k' j人为例子:/ S7 U  n+ c3 V& O
pragma solidity 0.4.18;
5 F7 F+ t( D5 C* }' W; D; ?7 ^0 econtract ForceEther {0 H+ g: `# g1 H! V! Z" d
bool youWin = false;
$ h2 C4 `2 p) U+ g- Y) v  [$ O function onlyNonZeroBalance() {
3 }0 W! [$ N+ N2 X  {     require(this.balance > 0);
( ^' V3 s, O) l& X7 u* Y5 {     youWin = true;
' e/ `& U6 ], ~ }5 ]. A3 L. R! N; H" K
// throw if any ether is received
! w* p) i# `/ W% P) M function() payable {( n- G% l/ x; ?
   revert();0 T! [) e/ A+ [
}
( D. X6 \5 f- |}
: l; ?( p  Y3 T$ G' c( E' n另一个看上去更真实的例子:
3 b# O' e8 g  r) n; S& @: x8 g& Ocontract EtherGame {   [  s8 j3 I& b( m) O0 R0 G
   uint public payoutMileStone1 = 3 ether;
+ @" e2 Q5 F: [. N   uint public mileStone1Reward = 2 ether;! y& L" O4 D8 d
   uint public payoutMileStone2 = 5 ether;
; v9 J; x$ F3 r5 i$ u& F1 R   uint public mileStone2Reward = 3 ether;
: y2 j( N' M$ g6 c' C   uint public finalMileStone = 10 ether;
/ Q: F$ S. ?5 \3 y7 w7 l1 e' Z   uint public finalReward = 5 ether; + |+ W. t/ W! C- g
   mapping(address => uint) redeemableEther;1 F( c5 B, u9 a! [
   // users pay 0.5 ether. At specific milestones, credit their accounts
- u+ l2 n) L/ o5 z. d6 Z   function play() public payable {
, C" X. ^) Z! B( u  Z       require(msg.value == 0.5 ether); // each play is 0.5 ether: \: Z, ^2 ~- [+ i' R7 n6 Z
       uint currentBalance = this.balance + msg.value;' }  c7 v2 K6 t' T4 D, Q
       // ensure no players after the game as finished
: ?0 G9 ^. E8 ~# W, w       require(currentBalance
3 X, d8 s9 D+ H/ ^       // if at a milestone credit the players account
+ ^4 l* m9 F5 }. I       if (currentBalance == payoutMileStone1) {  C  P" H! }6 g0 j7 m
           redeemableEther[msg.sender] += mileStone1Reward;
& T1 T0 K9 R- Z1 U5 }5 X       }+ R! Z/ \) d! ~! z
       else if (currentBalance == payoutMileStone2) {
5 [4 C7 Z# ]# v) a0 W( t           redeemableEther[msg.sender] += mileStone2Reward;2 m& N- |' l# R/ R/ M$ e
       }
- C5 V7 e9 d8 L       else if (currentBalance == finalMileStone ) {
$ g( S% Q( Q8 a: L           redeemableEther[msg.sender] += finalReward;
3 W1 k5 [* I% i0 |       }4 P- w! G/ q1 M7 a) D" u; d0 d+ C4 F
       return;
( Z7 L/ {; A/ ^; U   }, e  W2 J! u  W; i9 }9 u
   function claimReward() public {
9 s" C, h) J4 ]0 e0 k       // ensure the game is complete. S' C- ~2 U  D( t4 a( @0 x! Q
       require(this.balance == finalMileStone);
" Y# _& P6 X/ \& u1 ~) b# l       // ensure there is a reward to give) n0 C, f7 E/ Z& c
       require(redeemableEther[msg.sender] > 0); ; y  x$ Z& y( D% U) D" F. `
       redeemableEther[msg.sender] = 0;7 p9 {; h0 }' ^( y, d& b! s" N
       msg.sender.transfer(redeemableEther[msg.sender]);
, H* E1 G( V8 I: t/ }   }6 a4 h/ F/ R9 X9 E( u: j! b) j
}
- U1 L; J+ U+ y! T用户每次给合约发送 0.5 ETH,依次达到每个 milestone。攻击者可以强行将ETH发送给该合约,令其超过 finalMileStone 值,从而使合约中的所有资金都被锁定并且无法访问。 当然,这个例子也是人为设计出来的。0 \9 }1 c/ G/ X

6 y# J& S2 Y! J3 E; p1 W! x不幸的是,我还无法找到任何现实生活中的例子。不过,如果有目的地把代码隐藏在Solidity合约中,这些类型的漏洞可能会非常狡猾。这种性质的攻击是Underhanded Solidity比赛的第二和第三名获胜者的获胜关键。
- X5 `% X% [! m1 v6 z( K( m
$ m0 q2 x4 r2 _( {: K7 u强制发送 ETH 到合约
8 h; |& ]9 _) p  g7 |* t9 l1 v首先介绍一下交易是怎么发生的。假设有如下代码:3 x. N4 C# Y6 b  V3 Q) y* X
someAddress.transfer(self.balance() / 10);* F) ]( Z, q; ~+ n: C+ a+ b( v! V
这段代码有什么作用?1 y- J2 T. s! S# y" C. X: l1 N/ I
% J4 w/ w- b6 X# L5 w
someAddress 可以是非合约地址,这种情况没有合约执行,除标准交易费用外不会消耗任何 gas。这永远不会失败,但也无法保证地址是真实的和可访问的  ~/ h  {" o* v! n6 q
someAddress 可以是合约地址。合约可以成功交易并消耗足够的 gas 费用。, _! E& v7 s4 i4 S
+ B$ m3 R3 b/ ]% L* U
someAddress 可以是合约地址。合约交易如果失败,费用会返回,但会消费掉所有 gas,并且合约调用交易停止执行。
+ A  J/ m3 Y1 J* S' `& n; e* q' H- P% B8 V/ I' D  q4 x9 ?0 m
这看起来还行。如果合约不想收到 ETH,或者只想接收白名单上某些地址、合约的 ETH,那你对其发送 ETH 就会失败。( o8 o1 ^6 X' k% G/ @5 A) `

2 |9 Z: }: }0 x( q  X$ A然而,若以其他方式发送 ETH 的话,这种安全特性会被完全击破:合约可以在被创建时接收 ETH。然后,合约可以自毁,把它所有 ETH 发送给一个易受攻击的合约,而不用调用任何目标合约的代码。
( F# V  m  Q: Q/ z' N" ~: f3 c& r  B! o( _0 L  J& z
如果发送者和当前易受攻击的合约能在部署前被提前预测到,可以提前把 ETH 发送到那个地址。矿工/区块创建者能直接将区块奖励的 ETH 发送给任何合约,而不需要执行任何代码。
8 a; c& v  C) G7 T' T- w3 C3 l* H7 W
+ E# P0 F7 A  {7 O) R+ J- b4 {这些虽然都不是常规用法,但它们确实提供了一种方法来将 ETH 发送到合约并完全绕过任何阻止这一行为的代码。
' {/ p$ Y& q. u! F4 o: |" A: m  y$ x0 i$ M2 F" a) j
在没有特殊权限的情况下,能利用起来的唯一方法就是自毁。这个操作只需要你用点 ETH 来创建合约,以及支付额外的 gas 费用。7 ~( L; Z; j/ e0 p5 }9 Z

) \; x6 W! L7 F) \4 O另一个有趣的方法是在创建合约之前将ETH发送给合约。这样恶意开发者故意在合约代码里放漏洞。这类和合约余额有关的 bug 很难被发现,至少我已知的审核软件都没能明确指出错误,比如require(balance == whatever)。
2 t4 B. H) ]; |/ V5 N9 s, F' R/ f可以用下面这种代码阻止漏洞:
* a% d8 D9 I) _) v* Suint256 expectedBalance = 0;4 c% i# s& N! [! ~
function () public payable {# V5 S+ m5 m) _2 n7 }
   expectedBalance+=msg.value;" v, j9 ^8 z% `
}
/ k! x& S9 @% [/ q6 n1 D; O
3 \5 p" t/ `2 Z7 m  ]  ?( w0 q现在我们只用.transfer() 给它发送一些ETH
1 v( u+ a3 f9 I- @VM Exception while processing transaction: out of gas
5 S8 V" a" ~% x. q因为我们现在必须读写一个状态变量,这要昂贵得多。默认的 gas 费用是2300 gas。读取一个状态变量需要200 gas,并且写一个已经存在的变量需要 5000 gas(如果之前不存在则需要20,000 gas)
% h/ E- {6 n+ y& B所以,需要用下面代码:# h# D- K- q7 _* q, I
someAddress.transfer.value(whatever).gas(7000); //just a guess( u+ I- K6 f6 Z/ t, \9 e# _
大多数合约在交易时使用默认gas,但这里不是默认情况...不过,仔细考虑这个问题,既然我们需要更多 gas,我们可以写个更有趣的 payable 函数:
, {' e9 p7 L  f. g% \8 K1 t0 hfunction () public payable{3 A# O; U9 n3 {. W  u2 o$ F; v
   sender.call(....);
) \: w" n# V. V' k. W- {5 i}' Q5 |1 w4 r- q4 V+ q6 v
所以现在为了处理所调用合约的异常的余额改变问题,我们将合约暴露到一个全新形式的攻击下。默认值为2300只能调用一个非常小的外部函数,它不进行状态修改操作,基本上只能生成一个LOG。2 h& ^6 K& c5 ^: p1 Z

8 ~# ?1 O8 X, C" ?( Q# _4 M既然我们已经增加 gas 使得合约可以读取和存储状态,那么这个合约现在也有足够的 gas 来调用之前调用它的合约,于是就可能导致重入攻击(reentrancy attack)。我不会详细介绍所有细节,但基本上它需要一个非常谨慎的合约设计来防止这个“功能”的 bug,并且唯一100%处理掉它的方法是 gas 费用低到不能进行状态修改。没有100%的方法知道当前的合约执行是不是可重入的,EVM 并未公开这类信息。
7 ^! Q" c/ t7 f9 g- n4 T
: F8 G' Z; X# O9 m0 T$ h总结" u* B2 ]; v0 v: k% Y8 r& _3 G5 x$ D
对我来说,这部分 EVM 设计毫无意义。正常接受资金需要获得批准,这似乎被说成是一种安全功能,同时却有清晰且有记录的方法来绕过它,所以它没有提供任何安全性方面的价值。
  N$ W1 b, t. u* a7 ~
7 F/ Z! z! d+ l5 a& y3 K6 X此外,如果你想减少交易费用,那么这些方法在很大程度上是你无法访问的,它们只对攻击者有用。理论上,如果你只想将代币只发送到合约,而不关心合约的执行,gas 成本应该便宜33%。
* C# e8 _( r- C. o" X
: t% N  W5 ^4 R6 ~7 N为什么EVM没有暴露这样类似的方法呢?从历史上看,这种意外行为导致了许多合约DoS攻击和漏洞。幸运的是,Solidity让这些类型错误更难以出现。但显然它不能完全解决安全方面的问题,更不用说经济方面了。
" Q6 v. `( M% G' z7 C, w+ d* e8 `3 x
如果希望跟踪合约中确切的预期余额,那么需要支付比平常更多的 gas 并使用非标准接口,这实际上会带来更多的风险,比如重入攻击。
! z) ]4 j' F# |$ F' c  y# }5 n' a& o8 y* Q
我们可能永远不会知道为什么EVM这样设计,但这一问题将在 Qtum x86VM 中得到解决。将来会有一种在不执行合约的情况下向合约发送 QTUM 的方法,而且它比实际执行合约更加便宜。
BitMere.com 比特池塘系信息发布平台,比特池塘仅提供信息存储空间服务。
声明:该文观点仅代表作者本人,本文不代表比特池塘立场,且不构成建议,请谨慎对待。
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

成为第一个吐槽的人

吃瓜围观小分队 小学生
  • 粉丝

    0

  • 关注

    0

  • 主题

    3