Hi 游客

更多精彩,请登录!

比特池塘 区块链技术 正文

以太坊智能合约潜在风险

吃瓜围观小分队
87 0 0
最近作者一直在思考 EVM 和 Solidity 对不可支付合约(non-payable contracts)的错误概念,以及其可能蕴含的针对智能合约的攻击(attack vectors),该类型攻击在现有 EVM 中几乎不可修复。EVM 是以太坊、Qtum 量子链和其他一些区块链合约运行的基础。基本上写 Solidity 代码就等同于使用 EVM。这篇文章只以以太坊(ETH) 为例,不过该问题同样适用于用 QTUM 等所有兼容 EVM 的项目。
3 ~. o1 P& [. f  p0 M) p
1 |. Z& F6 M: @* [9 D6 y8 I对没有经验的人来说,EVM 似乎并没有只发送 ETH 到合约,但不潜在地执行任何合约代码的方法(这样的交易会失败/抛出异常)。但事实上,确实存在比较不为人知的强制合约地址接收 ETH 的方法。
% B5 {1 K# p& o' V
5 I/ m- j+ r" ^. O0 m- N( c强制接收 ETH 攻击
' {5 Y3 p2 x' p4 x1 a6 Y+ M
3 n4 a( e2 i; n2 H/ O异常的余额改变并不会影响所有合约,有些时候只会导致合约中 ETH 被锁定而永远无法使用。但有些合约可能会因此完全失效,故需要加入expectedBalance 状态字段,或者添加余额异常处理方法来避免这一问题。
+ p) U- f: i8 ]: W# t& t9 I9 M2 W5 q1 ~* L  t  O; V3 S
人为例子:) Y0 u" m; _  K9 ?" m/ G
pragma solidity 0.4.18;7 X# r- K& D. [2 k% |
contract ForceEther {
' n5 o0 ?2 ^1 J9 z bool youWin = false;" R( H# ]- n5 U% H' b
function onlyNonZeroBalance() {4 U/ `; m& c* j
     require(this.balance > 0);
0 O3 u, C5 M4 e6 W/ q" i     youWin = true;3 n3 |: p( n. S/ K
}4 Q/ T4 s7 K: {# Q4 {
// throw if any ether is received
4 v- T  s1 p& p' X: o, p function() payable {
$ o. r+ N* l' {   revert();8 C2 x# a# H/ m  Q
}/ g. D* S" q6 V4 _+ T
}
. A% ^- L# ?# |% x: w  Q% q9 E另一个看上去更真实的例子:
$ U3 ^+ D) A& d2 I$ lcontract EtherGame {   ]$ q# a* N+ P: ^8 G
   uint public payoutMileStone1 = 3 ether;
. q# }; P, A' @1 R* f   uint public mileStone1Reward = 2 ether;( }+ i' `9 r2 w% ?" N/ ?
   uint public payoutMileStone2 = 5 ether;# Z: L; I5 B  w3 V( h, g
   uint public mileStone2Reward = 3 ether;
; R- [2 S4 m7 o   uint public finalMileStone = 10 ether; 0 _) D& q! S' d7 b$ U
   uint public finalReward = 5 ether;
# d' E6 k1 q. }: Z4 D* `9 a1 O0 V$ \   mapping(address => uint) redeemableEther;
# c6 D' g" ~8 d+ H( ~   // users pay 0.5 ether. At specific milestones, credit their accounts
$ S8 i& D  O, l6 T   function play() public payable {
$ T& S; Q* ?) \2 W       require(msg.value == 0.5 ether); // each play is 0.5 ether
1 v$ S6 P2 I- S. U" Z) s0 f1 H7 v8 x4 S       uint currentBalance = this.balance + msg.value;
3 z' a* R& x  @6 I. Z8 m       // ensure no players after the game as finished
, W* T7 K: a, C4 h  |$ E       require(currentBalance
, D6 n0 a8 m0 l( I0 o4 H& ]       // if at a milestone credit the players account4 u; G" K& o7 l7 h3 O* ]
       if (currentBalance == payoutMileStone1) {3 X+ j! M3 X; R. C4 `- i. H3 P6 w& P
           redeemableEther[msg.sender] += mileStone1Reward;
, A& o2 c) Z/ |- r9 R2 g3 H& ?4 f       }
1 F7 F) w. `6 Q& g$ O3 r) T       else if (currentBalance == payoutMileStone2) {1 v4 H, m4 G+ P8 u9 H7 G
           redeemableEther[msg.sender] += mileStone2Reward;
. N* z0 M6 g6 u- z+ N- b       }& x4 x6 c, C) N% J
       else if (currentBalance == finalMileStone ) {
* g- p% J. @, [% h           redeemableEther[msg.sender] += finalReward;
& D9 m7 Z4 \; E5 @5 [/ q' J       }. H) ]* r! D, a- x; q, X9 f5 M
       return;
& |: {9 D, f% N) S' s   }
+ G3 w) t, A1 o# c9 z0 `   function claimReward() public {
2 V* x7 |3 P7 s' w% T5 b       // ensure the game is complete- w; m; F4 O. w+ G
       require(this.balance == finalMileStone);  @- Q! \5 }0 U4 G1 v
       // ensure there is a reward to give$ ~) l, M  W, }; N) ?/ P/ Y: c
       require(redeemableEther[msg.sender] > 0);
1 y% H, \. L% }+ M& v( e       redeemableEther[msg.sender] = 0;
0 k! \! h9 O* \: y       msg.sender.transfer(redeemableEther[msg.sender]);: c. H' Z. e0 k* ]5 l2 j
   }
8 Q, ]  h) i0 q! U& c" ~+ y( u}' g; b7 r9 K  w- B) }0 z  F; r( C) Z
用户每次给合约发送 0.5 ETH,依次达到每个 milestone。攻击者可以强行将ETH发送给该合约,令其超过 finalMileStone 值,从而使合约中的所有资金都被锁定并且无法访问。 当然,这个例子也是人为设计出来的。/ J: G0 Z) S0 o& m; Q

# Y" U' w  H* |* k不幸的是,我还无法找到任何现实生活中的例子。不过,如果有目的地把代码隐藏在Solidity合约中,这些类型的漏洞可能会非常狡猾。这种性质的攻击是Underhanded Solidity比赛的第二和第三名获胜者的获胜关键。
- R4 A+ s: T/ L! x
- O& T! Q7 f( ^) {0 x, j4 p强制发送 ETH 到合约
* }8 G* q; a2 j首先介绍一下交易是怎么发生的。假设有如下代码:) F7 K, J+ a- H4 Z1 h
someAddress.transfer(self.balance() / 10);
: Z) x) g2 P$ i  [这段代码有什么作用?
1 W8 x8 a& M* C. H" g( c7 E+ L4 l# b; R1 _. I* I
someAddress 可以是非合约地址,这种情况没有合约执行,除标准交易费用外不会消耗任何 gas。这永远不会失败,但也无法保证地址是真实的和可访问的, v! y1 |9 S9 [) p& R) A+ J, q
someAddress 可以是合约地址。合约可以成功交易并消耗足够的 gas 费用。
: z* v& ?# s: _( H2 ~8 U
) N6 Z% }7 {# MsomeAddress 可以是合约地址。合约交易如果失败,费用会返回,但会消费掉所有 gas,并且合约调用交易停止执行。
/ z: X) P5 q. w1 O0 Q8 A) N! Z2 H/ ~4 I6 W" d! _
这看起来还行。如果合约不想收到 ETH,或者只想接收白名单上某些地址、合约的 ETH,那你对其发送 ETH 就会失败。5 n; i  I( @( i, _
" b+ {4 V$ |7 L# S" g
然而,若以其他方式发送 ETH 的话,这种安全特性会被完全击破:合约可以在被创建时接收 ETH。然后,合约可以自毁,把它所有 ETH 发送给一个易受攻击的合约,而不用调用任何目标合约的代码。
6 e+ p" c& B0 F' ]) D
) P% ^$ Q3 w% f: e. z) \4 k如果发送者和当前易受攻击的合约能在部署前被提前预测到,可以提前把 ETH 发送到那个地址。矿工/区块创建者能直接将区块奖励的 ETH 发送给任何合约,而不需要执行任何代码。
; V, Z! M$ b2 M7 c& U# \* \; L  E/ k5 g4 k' d4 ?( H& @8 {
这些虽然都不是常规用法,但它们确实提供了一种方法来将 ETH 发送到合约并完全绕过任何阻止这一行为的代码。
. {# n0 Z( m- S8 @: H; m. R/ O- K  t! ^# c
在没有特殊权限的情况下,能利用起来的唯一方法就是自毁。这个操作只需要你用点 ETH 来创建合约,以及支付额外的 gas 费用。$ w# w$ V" t: B0 E9 P
) A: g! I7 B" f3 _
另一个有趣的方法是在创建合约之前将ETH发送给合约。这样恶意开发者故意在合约代码里放漏洞。这类和合约余额有关的 bug 很难被发现,至少我已知的审核软件都没能明确指出错误,比如require(balance == whatever)。
( m4 ^) \3 \8 P2 O可以用下面这种代码阻止漏洞:
/ d2 d; }0 ]7 {- i4 K! |, luint256 expectedBalance = 0;
, D) s3 M( y  |, Y: }! Xfunction () public payable {
9 `3 g5 V- z1 e. [  i% M. w' s   expectedBalance+=msg.value;+ ^3 ~' H2 R, U" R0 [2 \8 s& t
}2 {/ b: d/ |# F: ^7 h
$ y2 I8 V; L9 ?9 J$ `
现在我们只用.transfer() 给它发送一些ETH
! o- D- N( u: U7 iVM Exception while processing transaction: out of gas
# ?/ D* ^7 [, z) J6 k1 M) i9 M因为我们现在必须读写一个状态变量,这要昂贵得多。默认的 gas 费用是2300 gas。读取一个状态变量需要200 gas,并且写一个已经存在的变量需要 5000 gas(如果之前不存在则需要20,000 gas)2 y- t% q% m3 Z% g
所以,需要用下面代码:% Z% Y- V( R5 V& W
someAddress.transfer.value(whatever).gas(7000); //just a guess8 v  e+ j: r! n- _2 }
大多数合约在交易时使用默认gas,但这里不是默认情况...不过,仔细考虑这个问题,既然我们需要更多 gas,我们可以写个更有趣的 payable 函数:
8 K( p5 j* M7 u2 S/ afunction () public payable{, [; P5 k( ]3 F  s: g' j& ]
   sender.call(....);
- K; h, j. [/ s+ T. k9 R7 _8 [/ k4 e}/ M8 Z. b* ]/ y1 `
所以现在为了处理所调用合约的异常的余额改变问题,我们将合约暴露到一个全新形式的攻击下。默认值为2300只能调用一个非常小的外部函数,它不进行状态修改操作,基本上只能生成一个LOG。
) O: d& q$ K& V. ^5 k1 r# O3 [9 z% S
既然我们已经增加 gas 使得合约可以读取和存储状态,那么这个合约现在也有足够的 gas 来调用之前调用它的合约,于是就可能导致重入攻击(reentrancy attack)。我不会详细介绍所有细节,但基本上它需要一个非常谨慎的合约设计来防止这个“功能”的 bug,并且唯一100%处理掉它的方法是 gas 费用低到不能进行状态修改。没有100%的方法知道当前的合约执行是不是可重入的,EVM 并未公开这类信息。
/ ?# y: W, l2 H* \# x# e+ J% A) {0 \- P6 z! ~
总结
, w# r5 f- j0 z8 g3 T4 `4 @对我来说,这部分 EVM 设计毫无意义。正常接受资金需要获得批准,这似乎被说成是一种安全功能,同时却有清晰且有记录的方法来绕过它,所以它没有提供任何安全性方面的价值。% z( a0 T0 F! M
7 Q' t8 M; [8 w! g% t5 ?1 c
此外,如果你想减少交易费用,那么这些方法在很大程度上是你无法访问的,它们只对攻击者有用。理论上,如果你只想将代币只发送到合约,而不关心合约的执行,gas 成本应该便宜33%。: y' t1 w9 c( Y" }" H# r7 u9 y
# Z, I! c8 V% X1 j1 O
为什么EVM没有暴露这样类似的方法呢?从历史上看,这种意外行为导致了许多合约DoS攻击和漏洞。幸运的是,Solidity让这些类型错误更难以出现。但显然它不能完全解决安全方面的问题,更不用说经济方面了。7 w. r" X6 n% }! H: ?1 v* O

2 ?5 S/ j. `3 q+ q% w1 r如果希望跟踪合约中确切的预期余额,那么需要支付比平常更多的 gas 并使用非标准接口,这实际上会带来更多的风险,比如重入攻击。/ @( s% s: w/ B8 B! u) l1 Y! X6 q
  U& E' r! x: F, J' u& ^
我们可能永远不会知道为什么EVM这样设计,但这一问题将在 Qtum x86VM 中得到解决。将来会有一种在不执行合约的情况下向合约发送 QTUM 的方法,而且它比实际执行合约更加便宜。
BitMere.com 比特池塘系信息发布平台,比特池塘仅提供信息存储空间服务。
声明:该文观点仅代表作者本人,本文不代表比特池塘立场,且不构成建议,请谨慎对待。
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

成为第一个吐槽的人

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

    0

  • 关注

    0

  • 主题

    3