Hi 游客

更多精彩,请登录!

比特池塘 区块链技术 正文

以太坊智能合约潜在风险

吃瓜围观小分队
86 0 0
最近作者一直在思考 EVM 和 Solidity 对不可支付合约(non-payable contracts)的错误概念,以及其可能蕴含的针对智能合约的攻击(attack vectors),该类型攻击在现有 EVM 中几乎不可修复。EVM 是以太坊、Qtum 量子链和其他一些区块链合约运行的基础。基本上写 Solidity 代码就等同于使用 EVM。这篇文章只以以太坊(ETH) 为例,不过该问题同样适用于用 QTUM 等所有兼容 EVM 的项目。
! m% @7 K; c1 K( F; b' X' j+ p' t6 R" _% S$ e8 n
对没有经验的人来说,EVM 似乎并没有只发送 ETH 到合约,但不潜在地执行任何合约代码的方法(这样的交易会失败/抛出异常)。但事实上,确实存在比较不为人知的强制合约地址接收 ETH 的方法。! r* }. a' A9 A0 f7 X; O
" B' ?1 v* N/ M$ A3 H) v
强制接收 ETH 攻击0 [, C: H! g" D- A% A/ V+ V

7 D& j5 d2 y% B, G# i% A4 u异常的余额改变并不会影响所有合约,有些时候只会导致合约中 ETH 被锁定而永远无法使用。但有些合约可能会因此完全失效,故需要加入expectedBalance 状态字段,或者添加余额异常处理方法来避免这一问题。
* j7 {8 \; u! u" H) O, ~8 a* |) N9 M: w" l" K$ W% H
人为例子:
3 K: @. N8 K) N- s& A2 G# c& |# }pragma solidity 0.4.18;7 o4 @# |, Z& a! U0 m8 E
contract ForceEther {$ f& a$ q, _0 z  h+ [
bool youWin = false;& J( \# I! @3 S! f( ]
function onlyNonZeroBalance() {1 K2 z4 }5 w. C, f( z- V
     require(this.balance > 0);
5 h7 S: n. w) K1 h. @# j     youWin = true;7 p; N  g5 z( p* H
}
! L8 M) {1 T9 M+ E3 [" s/ G // throw if any ether is received. E7 H& u0 d# k/ K
function() payable {
% H: M4 I8 Q3 w/ I   revert();
8 O8 r+ I4 I* Z% v( N( h* e }
! n3 l; G3 H- m0 W& U4 G/ U}
/ f7 M& A9 Z! [. K7 Z- O* U7 S8 T* B另一个看上去更真实的例子:
- C5 ^: @" a2 Q1 b6 C" bcontract EtherGame {
' ?0 E" h; X7 Q4 y% X& h   uint public payoutMileStone1 = 3 ether;$ S  _0 I5 e) j8 N
   uint public mileStone1Reward = 2 ether;
1 q/ q9 x0 z; J0 j" r8 f- @   uint public payoutMileStone2 = 5 ether;
, Q2 c+ q2 f9 z* |2 S* r) O   uint public mileStone2Reward = 3 ether;
" d! m8 ]2 a% o+ L6 Y   uint public finalMileStone = 10 ether;
4 F/ Q4 ^: a( U- R! u6 F- ~$ W; J4 ~   uint public finalReward = 5 ether; 4 B- W1 q& \% U) j3 O# v
   mapping(address => uint) redeemableEther;% {) E/ y6 g5 k
   // users pay 0.5 ether. At specific milestones, credit their accounts! C  a1 I4 m! F2 T/ ?
   function play() public payable {
! u' X% l8 b, u6 }+ Y0 Z* |       require(msg.value == 0.5 ether); // each play is 0.5 ether
2 n% E$ a5 P1 ?4 Q( v. z7 h* d# W       uint currentBalance = this.balance + msg.value;0 X& O5 h, X8 k0 ~( d: i5 }
       // ensure no players after the game as finished
) ?$ j( Y/ L' n" Y' s" o       require(currentBalance
: H( B9 [! Y# E       // if at a milestone credit the players account
, b1 N' l- s2 t% w- g% A) D       if (currentBalance == payoutMileStone1) {( e( R; H1 [- ~# f" I( d
           redeemableEther[msg.sender] += mileStone1Reward;
) n8 N) U" s% X' W       }
3 H, v$ k" c6 j8 D  Z+ p+ s       else if (currentBalance == payoutMileStone2) {
+ z0 m5 ?- U% E1 d& u$ v           redeemableEther[msg.sender] += mileStone2Reward;
/ r5 O; h' X/ a! O& ]$ w' ^% ~       }
8 ^. |0 S0 _( R9 |* e* G       else if (currentBalance == finalMileStone ) {7 ~4 I- x. N, a; D
           redeemableEther[msg.sender] += finalReward;" L2 |/ n% J6 L0 u3 x
       }& ?  g8 j% G! w! N* m* a) B6 ?: s
       return;0 R) S$ o+ |/ Y0 G9 ]/ e" i+ @
   }6 h5 f. Y% f1 w1 O1 L5 Q( L* i
   function claimReward() public {! F0 {: s! x' z* g3 c
       // ensure the game is complete
' \) V' U6 }4 l' T- C$ n       require(this.balance == finalMileStone);; H3 D0 |% c5 ]  k/ C" A% I. F  u
       // ensure there is a reward to give
; J: _5 Z  ]8 V$ m       require(redeemableEther[msg.sender] > 0); 6 L- a& s5 N7 Y0 ?
       redeemableEther[msg.sender] = 0;
1 _! g7 l- i! K: _6 W! o       msg.sender.transfer(redeemableEther[msg.sender]);6 j* j; ^0 k  d5 m
   }
/ }  O0 x8 r  x! C5 f}; l+ j3 f; W/ \- b7 \% c2 b
用户每次给合约发送 0.5 ETH,依次达到每个 milestone。攻击者可以强行将ETH发送给该合约,令其超过 finalMileStone 值,从而使合约中的所有资金都被锁定并且无法访问。 当然,这个例子也是人为设计出来的。5 B+ k* F2 o8 |* j* |; k

& O% ]) o8 R- J, `8 s不幸的是,我还无法找到任何现实生活中的例子。不过,如果有目的地把代码隐藏在Solidity合约中,这些类型的漏洞可能会非常狡猾。这种性质的攻击是Underhanded Solidity比赛的第二和第三名获胜者的获胜关键。
% o6 r' w: ^- O6 }4 R$ z
. F3 N9 |; b: s$ \0 N3 S- o强制发送 ETH 到合约. H$ _( o% g, z. ?) F" [
首先介绍一下交易是怎么发生的。假设有如下代码:1 R: Q4 e6 x9 t. l
someAddress.transfer(self.balance() / 10);
& h/ h% i; }- ]) z' ?这段代码有什么作用?+ T! A! I* H- Q& J

, p9 Y; c; m. X1 K1 T& hsomeAddress 可以是非合约地址,这种情况没有合约执行,除标准交易费用外不会消耗任何 gas。这永远不会失败,但也无法保证地址是真实的和可访问的
9 w  U2 [$ k" E6 R; K! D. AsomeAddress 可以是合约地址。合约可以成功交易并消耗足够的 gas 费用。
2 q3 C% L; D+ |/ W2 p0 R! ~- Q% o- ?% e2 d% X( o7 F; F
someAddress 可以是合约地址。合约交易如果失败,费用会返回,但会消费掉所有 gas,并且合约调用交易停止执行。) `7 O( o5 @4 U# [/ b( r8 ?
. l# C: k+ v, R" z( g
这看起来还行。如果合约不想收到 ETH,或者只想接收白名单上某些地址、合约的 ETH,那你对其发送 ETH 就会失败。
) }( i' m" R" A6 [$ Y, t: S% \, J' Y0 d; T
然而,若以其他方式发送 ETH 的话,这种安全特性会被完全击破:合约可以在被创建时接收 ETH。然后,合约可以自毁,把它所有 ETH 发送给一个易受攻击的合约,而不用调用任何目标合约的代码。
, E+ X: _$ C+ M' s2 `: G/ U  t, @, W# `  _  A% p; }
如果发送者和当前易受攻击的合约能在部署前被提前预测到,可以提前把 ETH 发送到那个地址。矿工/区块创建者能直接将区块奖励的 ETH 发送给任何合约,而不需要执行任何代码。
, z) T- a, A! j6 L9 Y0 d4 R  J7 U+ e: k
这些虽然都不是常规用法,但它们确实提供了一种方法来将 ETH 发送到合约并完全绕过任何阻止这一行为的代码。
0 u6 {9 N4 j4 v& [: V1 N5 d: Q3 x/ w3 Q) _* C. ]2 @0 T7 ^1 K
在没有特殊权限的情况下,能利用起来的唯一方法就是自毁。这个操作只需要你用点 ETH 来创建合约,以及支付额外的 gas 费用。* ?- k) ?5 i& a& `0 n* j. f

% B0 ~& i4 a$ V( h* g另一个有趣的方法是在创建合约之前将ETH发送给合约。这样恶意开发者故意在合约代码里放漏洞。这类和合约余额有关的 bug 很难被发现,至少我已知的审核软件都没能明确指出错误,比如require(balance == whatever)。
% r/ _& N+ ~% F7 g' }可以用下面这种代码阻止漏洞:
8 H& S  ]. o" l, _8 T" D& ]; buint256 expectedBalance = 0;
' v9 P" a% p' l/ e! M' q' n' Ffunction () public payable {0 L, ?: x& Q% ^7 H1 p8 j& f
   expectedBalance+=msg.value;
8 e; _, c' ~1 R$ U% y  N}
+ F& y. L8 i& n4 F6 r  o; H) g. b  T7 J) s; w+ ]) F, J
现在我们只用.transfer() 给它发送一些ETH/ _7 O6 Y) `# Z
VM Exception while processing transaction: out of gas
2 r7 U% H/ J3 p: E因为我们现在必须读写一个状态变量,这要昂贵得多。默认的 gas 费用是2300 gas。读取一个状态变量需要200 gas,并且写一个已经存在的变量需要 5000 gas(如果之前不存在则需要20,000 gas)! O( }8 ^, m, |" h
所以,需要用下面代码:2 X4 W  G$ h- \
someAddress.transfer.value(whatever).gas(7000); //just a guess  \9 N9 Z& k% T  F. b% }4 _
大多数合约在交易时使用默认gas,但这里不是默认情况...不过,仔细考虑这个问题,既然我们需要更多 gas,我们可以写个更有趣的 payable 函数:2 x; I1 _4 }" v7 A. s' q
function () public payable{0 j4 K3 u. F+ ?
   sender.call(....);
7 R3 }3 @- p$ Y6 `$ L. Q}
6 c& H& o' z' p4 D* y7 @0 y所以现在为了处理所调用合约的异常的余额改变问题,我们将合约暴露到一个全新形式的攻击下。默认值为2300只能调用一个非常小的外部函数,它不进行状态修改操作,基本上只能生成一个LOG。
! o3 o9 `/ x2 H& S( x. R7 N
) x. Z( ]2 ]5 v# m) A既然我们已经增加 gas 使得合约可以读取和存储状态,那么这个合约现在也有足够的 gas 来调用之前调用它的合约,于是就可能导致重入攻击(reentrancy attack)。我不会详细介绍所有细节,但基本上它需要一个非常谨慎的合约设计来防止这个“功能”的 bug,并且唯一100%处理掉它的方法是 gas 费用低到不能进行状态修改。没有100%的方法知道当前的合约执行是不是可重入的,EVM 并未公开这类信息。" r: \8 f: g$ ^; P+ p" T
0 ?& W- r  u' _$ I6 l" E$ p4 s
总结
8 w" J4 [" ]' n对我来说,这部分 EVM 设计毫无意义。正常接受资金需要获得批准,这似乎被说成是一种安全功能,同时却有清晰且有记录的方法来绕过它,所以它没有提供任何安全性方面的价值。- o' @  s. u9 M4 i* g) F( d3 R' g

0 j3 w( p  y6 F# |: t5 A此外,如果你想减少交易费用,那么这些方法在很大程度上是你无法访问的,它们只对攻击者有用。理论上,如果你只想将代币只发送到合约,而不关心合约的执行,gas 成本应该便宜33%。
; W- w2 f0 E& O+ w/ K& Y  M' G  Z
( [7 X3 B! v/ O' x为什么EVM没有暴露这样类似的方法呢?从历史上看,这种意外行为导致了许多合约DoS攻击和漏洞。幸运的是,Solidity让这些类型错误更难以出现。但显然它不能完全解决安全方面的问题,更不用说经济方面了。! \) {' `# p( |8 s5 G

% m1 F) }4 E1 {0 S2 [如果希望跟踪合约中确切的预期余额,那么需要支付比平常更多的 gas 并使用非标准接口,这实际上会带来更多的风险,比如重入攻击。
2 r6 q( V* E2 i5 i% Q0 k/ V( n4 Z0 {. A1 P3 k. Z# }
我们可能永远不会知道为什么EVM这样设计,但这一问题将在 Qtum x86VM 中得到解决。将来会有一种在不执行合约的情况下向合约发送 QTUM 的方法,而且它比实际执行合约更加便宜。
BitMere.com 比特池塘系信息发布平台,比特池塘仅提供信息存储空间服务。
声明:该文观点仅代表作者本人,本文不代表比特池塘立场,且不构成建议,请谨慎对待。
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

成为第一个吐槽的人

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

    0

  • 关注

    0

  • 主题

    3