Hi 游客

更多精彩,请登录!

比特池塘 区块链技术 正文

以太坊智能合约潜在风险

吃瓜围观小分队
117 0 0
最近作者一直在思考 EVM 和 Solidity 对不可支付合约(non-payable contracts)的错误概念,以及其可能蕴含的针对智能合约的攻击(attack vectors),该类型攻击在现有 EVM 中几乎不可修复。EVM 是以太坊、Qtum 量子链和其他一些区块链合约运行的基础。基本上写 Solidity 代码就等同于使用 EVM。这篇文章只以以太坊(ETH) 为例,不过该问题同样适用于用 QTUM 等所有兼容 EVM 的项目。
6 H* z, S, {! U/ X; c7 t, L  t& \/ q+ X* A% R: N0 s5 B) l
对没有经验的人来说,EVM 似乎并没有只发送 ETH 到合约,但不潜在地执行任何合约代码的方法(这样的交易会失败/抛出异常)。但事实上,确实存在比较不为人知的强制合约地址接收 ETH 的方法。7 n5 ]0 A& N1 W0 ~
6 S. _8 v9 n% s0 v/ k. N
强制接收 ETH 攻击
, f* J; J+ n* H6 C5 h# [1 ^  @0 T; A6 T7 J- p2 x9 s7 C5 ]
异常的余额改变并不会影响所有合约,有些时候只会导致合约中 ETH 被锁定而永远无法使用。但有些合约可能会因此完全失效,故需要加入expectedBalance 状态字段,或者添加余额异常处理方法来避免这一问题。
/ f3 F8 i" r! p; Q* |- w
" j, x& U& e6 B3 T. j+ H' e人为例子:1 m) i* t) l5 ~+ R$ D3 k
pragma solidity 0.4.18;
* `4 k3 {; e3 ^- @7 ]contract ForceEther {
$ h' g, H* {( M" G4 w, Z bool youWin = false;
4 W  ~5 ?3 h1 L) r/ n function onlyNonZeroBalance() {% U0 _( Y3 K' A+ D* _
     require(this.balance > 0);
5 e: b" ~8 d& ^     youWin = true;" a9 |# A# p1 \8 J, S
}) u. ^: y' \* z0 I* H% _% ^
// throw if any ether is received
( _7 X4 M8 d! B7 s+ B, H9 M function() payable {
" z  t7 H! B# d1 D4 G& I! o   revert();
  P- C0 i- \6 } }
& H# v. N( ~+ M, n0 O0 c/ k}
. ]5 m5 }8 N3 d9 j7 j7 k* V+ R6 A另一个看上去更真实的例子:
0 l! ^. D* E7 K+ g8 k. z/ b, ]contract EtherGame {
3 `3 Y& `" L2 e   uint public payoutMileStone1 = 3 ether;
1 L$ }0 u1 ?* V% ~! W7 R9 d1 ?+ v   uint public mileStone1Reward = 2 ether;& K# L& u# i0 K) N& x
   uint public payoutMileStone2 = 5 ether;' S' o# S- t0 I
   uint public mileStone2Reward = 3 ether; * N: o' y* t% I8 z5 F1 q
   uint public finalMileStone = 10 ether; % n1 F! p: j3 q& D7 }
   uint public finalReward = 5 ether; 5 l% r+ O$ u# `: y$ ~
   mapping(address => uint) redeemableEther;5 l  ^: j) Q) |+ A; y! I. c
   // users pay 0.5 ether. At specific milestones, credit their accounts  s4 n) A" r) N( b7 L; @3 W
   function play() public payable {
6 _1 R# R$ V4 \! J       require(msg.value == 0.5 ether); // each play is 0.5 ether
6 g& [6 N. a+ v+ A. ?       uint currentBalance = this.balance + msg.value;
8 u0 h" d7 d8 g) z' R" w6 L       // ensure no players after the game as finished
2 ~1 [  ^5 p" l& l6 O6 g' g& \+ {       require(currentBalance ) @' V6 B3 ^3 a7 m7 O
       // if at a milestone credit the players account
" _1 @1 P; |3 Q/ |, u; b       if (currentBalance == payoutMileStone1) {8 z' {9 b5 n2 g0 H9 A
           redeemableEther[msg.sender] += mileStone1Reward;
1 w6 {; H3 E3 i1 ~8 Y       }0 h: [* C: r! `2 L- W1 i
       else if (currentBalance == payoutMileStone2) {) I3 {1 I5 h% P. C% A
           redeemableEther[msg.sender] += mileStone2Reward;* w- ~0 u; D1 A2 r7 E8 g& V
       }3 I+ t2 ~( p2 y
       else if (currentBalance == finalMileStone ) {' @& t& P: f7 q6 y0 Q
           redeemableEther[msg.sender] += finalReward;
$ v6 L" b# m& w" ]& R! ~6 e3 }+ G       }& M; U* Y6 }6 A& `' m; @! d
       return;" ^2 y0 A- r6 l+ E8 r; i
   }
( _2 ^, Q) _# M! X1 }   function claimReward() public {, j. D1 G3 t) S; \2 @
       // ensure the game is complete3 `5 a) b5 K% g* Q
       require(this.balance == finalMileStone);
$ c* |4 [: \. b( ?1 n6 Z% r       // ensure there is a reward to give
9 f1 W! d  K6 V+ e       require(redeemableEther[msg.sender] > 0); ( Q3 }& W1 Q% \0 [
       redeemableEther[msg.sender] = 0;
. C( _, F9 T* t" w* E+ ?" `       msg.sender.transfer(redeemableEther[msg.sender]);: n- T! H& [) a# Y' A
   }+ Q! \! B0 R1 [* q
}
/ f; U& c4 ?  {8 L/ t; D' E用户每次给合约发送 0.5 ETH,依次达到每个 milestone。攻击者可以强行将ETH发送给该合约,令其超过 finalMileStone 值,从而使合约中的所有资金都被锁定并且无法访问。 当然,这个例子也是人为设计出来的。0 e# e# g% |$ e8 q
5 ~1 W, r3 Y4 M9 Z: S
不幸的是,我还无法找到任何现实生活中的例子。不过,如果有目的地把代码隐藏在Solidity合约中,这些类型的漏洞可能会非常狡猾。这种性质的攻击是Underhanded Solidity比赛的第二和第三名获胜者的获胜关键。
5 t/ d: a5 D/ F% s6 L9 v. e9 V6 z( c) e! ]* o: A
强制发送 ETH 到合约
3 u) }' e7 m4 d- V1 [首先介绍一下交易是怎么发生的。假设有如下代码:6 }9 L8 C; _% |4 L; |
someAddress.transfer(self.balance() / 10);
' C1 I3 e) t2 v) v这段代码有什么作用?
% |; _# l" N( u# C0 ~/ I  u1 R! b5 v7 f* w5 [' D" V: |
someAddress 可以是非合约地址,这种情况没有合约执行,除标准交易费用外不会消耗任何 gas。这永远不会失败,但也无法保证地址是真实的和可访问的
  o/ u) ~, M0 A* z, E6 ?0 K! X& OsomeAddress 可以是合约地址。合约可以成功交易并消耗足够的 gas 费用。8 j9 ?2 u7 N+ K) |% `" S
% n; f& ^! ?3 `9 t  i' a
someAddress 可以是合约地址。合约交易如果失败,费用会返回,但会消费掉所有 gas,并且合约调用交易停止执行。; O6 C9 }4 n6 `* n( `0 v8 n- ^& M& B
& K7 C& m9 K" h2 ]
这看起来还行。如果合约不想收到 ETH,或者只想接收白名单上某些地址、合约的 ETH,那你对其发送 ETH 就会失败。( d( u1 {! y  C/ u/ d3 k. ~

. h" ~2 [% u6 C! q: p8 X然而,若以其他方式发送 ETH 的话,这种安全特性会被完全击破:合约可以在被创建时接收 ETH。然后,合约可以自毁,把它所有 ETH 发送给一个易受攻击的合约,而不用调用任何目标合约的代码。' b2 K$ }" x# Z, q& X/ q0 U
) Z" U6 R( [% G, K9 [/ a. \
如果发送者和当前易受攻击的合约能在部署前被提前预测到,可以提前把 ETH 发送到那个地址。矿工/区块创建者能直接将区块奖励的 ETH 发送给任何合约,而不需要执行任何代码。- T* r6 U5 X/ o; m3 Z
+ O1 Y# e2 ~! q0 Z5 k% r
这些虽然都不是常规用法,但它们确实提供了一种方法来将 ETH 发送到合约并完全绕过任何阻止这一行为的代码。
# c: B; |. B1 t1 P7 Z
8 L3 }- p6 [0 g% J! M在没有特殊权限的情况下,能利用起来的唯一方法就是自毁。这个操作只需要你用点 ETH 来创建合约,以及支付额外的 gas 费用。8 Z2 a3 ?5 ^. j
( g7 y6 Q1 @5 }+ t& S2 Z
另一个有趣的方法是在创建合约之前将ETH发送给合约。这样恶意开发者故意在合约代码里放漏洞。这类和合约余额有关的 bug 很难被发现,至少我已知的审核软件都没能明确指出错误,比如require(balance == whatever)。
- V- |2 U. L5 R8 s  z  ?可以用下面这种代码阻止漏洞:+ O/ f+ P, E& ?' T2 b4 }) q1 L
uint256 expectedBalance = 0;. ?" {1 L5 a. J  p0 A
function () public payable {
6 d+ ?  j; O# J   expectedBalance+=msg.value;9 L( X% f: S8 F# |8 X. c  i6 ]6 v, d
}6 N9 X% ]/ j  |5 }7 ^3 _8 W. q* a
# c5 q7 T! p) p5 Y& }  x7 q* z
现在我们只用.transfer() 给它发送一些ETH% @0 T1 n- l2 F; P" h' `6 ?
VM Exception while processing transaction: out of gas3 ^8 E9 C& K2 y5 `, K% d
因为我们现在必须读写一个状态变量,这要昂贵得多。默认的 gas 费用是2300 gas。读取一个状态变量需要200 gas,并且写一个已经存在的变量需要 5000 gas(如果之前不存在则需要20,000 gas)
4 e6 X) l( V5 j5 C0 R4 C所以,需要用下面代码:
9 c3 s/ |( C4 ^/ jsomeAddress.transfer.value(whatever).gas(7000); //just a guess
7 f# L% Y& D3 r3 a8 L) U大多数合约在交易时使用默认gas,但这里不是默认情况...不过,仔细考虑这个问题,既然我们需要更多 gas,我们可以写个更有趣的 payable 函数:7 _. I5 [( y4 ?
function () public payable{. }9 M, v3 U  k
   sender.call(....);4 S$ Q" |/ U& W! J& w/ u( ~
}. g. X  p& C/ ?) U: l
所以现在为了处理所调用合约的异常的余额改变问题,我们将合约暴露到一个全新形式的攻击下。默认值为2300只能调用一个非常小的外部函数,它不进行状态修改操作,基本上只能生成一个LOG。
- f/ E7 K* `2 g( p; n# v' o' e  X$ t
既然我们已经增加 gas 使得合约可以读取和存储状态,那么这个合约现在也有足够的 gas 来调用之前调用它的合约,于是就可能导致重入攻击(reentrancy attack)。我不会详细介绍所有细节,但基本上它需要一个非常谨慎的合约设计来防止这个“功能”的 bug,并且唯一100%处理掉它的方法是 gas 费用低到不能进行状态修改。没有100%的方法知道当前的合约执行是不是可重入的,EVM 并未公开这类信息。
  }/ H7 [6 t! \. P& \
) I/ u4 n7 x" ?7 B* y( F1 @2 N2 p总结" Q* Y' r! j7 g; N: w5 i
对我来说,这部分 EVM 设计毫无意义。正常接受资金需要获得批准,这似乎被说成是一种安全功能,同时却有清晰且有记录的方法来绕过它,所以它没有提供任何安全性方面的价值。" x7 E: ]3 c% }7 I
' X1 U7 Z6 o0 L4 G6 v) B$ d  o. H
此外,如果你想减少交易费用,那么这些方法在很大程度上是你无法访问的,它们只对攻击者有用。理论上,如果你只想将代币只发送到合约,而不关心合约的执行,gas 成本应该便宜33%。
) ^# {: U% b% p1 r0 o/ C5 h: u" q
- L& w" p. q0 K( l为什么EVM没有暴露这样类似的方法呢?从历史上看,这种意外行为导致了许多合约DoS攻击和漏洞。幸运的是,Solidity让这些类型错误更难以出现。但显然它不能完全解决安全方面的问题,更不用说经济方面了。
: d8 I+ A3 R+ E
. F( o- ]* t: U" ^  j1 n4 V) s如果希望跟踪合约中确切的预期余额,那么需要支付比平常更多的 gas 并使用非标准接口,这实际上会带来更多的风险,比如重入攻击。/ S$ n; T$ L: d5 t' S. Z1 ^

2 o# T' H7 {" u+ }' h; b9 i我们可能永远不会知道为什么EVM这样设计,但这一问题将在 Qtum x86VM 中得到解决。将来会有一种在不执行合约的情况下向合约发送 QTUM 的方法,而且它比实际执行合约更加便宜。
BitMere.com 比特池塘系信息发布平台,比特池塘仅提供信息存储空间服务。
声明:该文观点仅代表作者本人,本文不代表比特池塘立场,且不构成建议,请谨慎对待。
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

成为第一个吐槽的人

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

    0

  • 关注

    0

  • 主题

    3