Hi 游客

更多精彩,请登录!

比特池塘 区块链技术 正文

以太坊智能合约潜在风险

吃瓜围观小分队
131 0 0
最近作者一直在思考 EVM 和 Solidity 对不可支付合约(non-payable contracts)的错误概念,以及其可能蕴含的针对智能合约的攻击(attack vectors),该类型攻击在现有 EVM 中几乎不可修复。EVM 是以太坊、Qtum 量子链和其他一些区块链合约运行的基础。基本上写 Solidity 代码就等同于使用 EVM。这篇文章只以以太坊(ETH) 为例,不过该问题同样适用于用 QTUM 等所有兼容 EVM 的项目。
1 W7 }. o+ D6 h2 c4 J8 g! o2 s% ]; I- @5 g2 [. U  Y* a: w
对没有经验的人来说,EVM 似乎并没有只发送 ETH 到合约,但不潜在地执行任何合约代码的方法(这样的交易会失败/抛出异常)。但事实上,确实存在比较不为人知的强制合约地址接收 ETH 的方法。
1 C. I0 _+ ]4 A' C* I! _4 g1 j- Q0 Y
: u' Y5 j. h! t1 `) y强制接收 ETH 攻击: q4 ~4 R* k0 b- y- \; M# _# P

) G: E( \3 Z# q! Q3 ~! W  b异常的余额改变并不会影响所有合约,有些时候只会导致合约中 ETH 被锁定而永远无法使用。但有些合约可能会因此完全失效,故需要加入expectedBalance 状态字段,或者添加余额异常处理方法来避免这一问题。# y) P2 g0 [( x: |' I5 d! S% `6 `" W
" B$ f: @* d- N  q1 H5 X; K
人为例子:
: u4 o* y* w9 y. h3 {0 Opragma solidity 0.4.18;& d' Y8 t8 `& \8 A" \
contract ForceEther {
" }8 u; D8 n  L* I3 ] bool youWin = false;
9 D; K+ l' N  S function onlyNonZeroBalance() {
9 [6 N! e' |2 Y( }: [     require(this.balance > 0); 4 e# Z) \- o" y7 C/ [$ E! ?* }) ]. N
     youWin = true;6 a3 n2 ]0 |7 z. q
}
- j3 v: E1 z) q1 }* @ // throw if any ether is received
+ ^* A1 u+ n: Q1 ]3 w6 X5 k0 ] function() payable {
9 U( q8 x' v4 u& K0 Z   revert();2 x6 Q+ t" a0 c# n7 ]$ p
}
% M! O$ p* a4 ?2 ]}
8 t, B+ w- ]9 q4 B0 \另一个看上去更真实的例子:4 ^8 ]- J( F7 s
contract EtherGame { 1 ]0 M  @7 i6 j6 E5 }
   uint public payoutMileStone1 = 3 ether;  @+ W* T& `2 m) }( n! M9 C
   uint public mileStone1Reward = 2 ether;
' N9 I. I7 m$ A2 \# z& k" B/ P/ V. d   uint public payoutMileStone2 = 5 ether;
5 F3 ?$ P/ b% \6 E! a   uint public mileStone2Reward = 3 ether; 2 J7 F& D. s) j. B) p# @& h  A
   uint public finalMileStone = 10 ether; 4 e/ {4 O' {1 P4 ?
   uint public finalReward = 5 ether; ; h4 ~, J# }; q1 P7 v5 L- ^; g
   mapping(address => uint) redeemableEther;6 t9 U/ A2 R' P7 o/ h# [2 D
   // users pay 0.5 ether. At specific milestones, credit their accounts0 {; z) W9 ^" @* s
   function play() public payable {
" b, M2 i$ x/ _8 n+ S3 I5 d, @       require(msg.value == 0.5 ether); // each play is 0.5 ether# f$ G/ |# c6 l4 |' w
       uint currentBalance = this.balance + msg.value;
4 q) H" K( h: X6 J) ^       // ensure no players after the game as finished/ C  w2 l( {/ O
       require(currentBalance
  ~4 W% K- V/ j3 o) ]- S7 R       // if at a milestone credit the players account
- w/ a. F, ]- C0 X- ~       if (currentBalance == payoutMileStone1) {. F3 K. n2 h) s6 m
           redeemableEther[msg.sender] += mileStone1Reward;
* G4 l' h6 C2 S2 ]4 r0 y6 I       }$ h( S2 K2 L9 G8 P3 @
       else if (currentBalance == payoutMileStone2) {
! d- j0 Z- b3 M           redeemableEther[msg.sender] += mileStone2Reward;3 F% Y! J" o: D! A1 ~( k
       }& b6 w4 t- b' v/ I  b  y) V1 B
       else if (currentBalance == finalMileStone ) {
1 l$ S6 Z& Q# D" a! p# A- A; }           redeemableEther[msg.sender] += finalReward;
6 s, c3 X( I% o; w       }
- ^+ X7 q# v. T+ o3 |: Q" y0 X       return;
- |3 A' V* E9 U0 |  d   }5 F- E$ N9 f0 n/ C! `" }4 Q. d
   function claimReward() public {3 N, F' A* {, x: A$ d
       // ensure the game is complete  c" V! ~! `6 [) Z  z
       require(this.balance == finalMileStone);) o3 P5 O! J& }& I- G0 c# n0 T
       // ensure there is a reward to give! E5 I8 Z9 i/ v
       require(redeemableEther[msg.sender] > 0); ; @1 m. a) h) m1 C6 t( D
       redeemableEther[msg.sender] = 0;
5 i0 A$ z% M5 _       msg.sender.transfer(redeemableEther[msg.sender]);$ Q) V5 F; a% \
   }
: H; p/ n: a/ E5 N, o3 W. m}4 n# s6 r- y/ n
用户每次给合约发送 0.5 ETH,依次达到每个 milestone。攻击者可以强行将ETH发送给该合约,令其超过 finalMileStone 值,从而使合约中的所有资金都被锁定并且无法访问。 当然,这个例子也是人为设计出来的。
! x0 f. R: N  |9 Z5 D
: B! |4 C7 q$ A+ d不幸的是,我还无法找到任何现实生活中的例子。不过,如果有目的地把代码隐藏在Solidity合约中,这些类型的漏洞可能会非常狡猾。这种性质的攻击是Underhanded Solidity比赛的第二和第三名获胜者的获胜关键。& e- C$ g8 J% k3 U# @% w6 i
& |6 i% C7 h% X$ p+ ]( _( W
强制发送 ETH 到合约
0 m  G7 K: Z! C* i1 V: m8 ^& c首先介绍一下交易是怎么发生的。假设有如下代码:
7 f2 v% u2 |/ ~2 f6 O: }' xsomeAddress.transfer(self.balance() / 10);
0 X+ V$ [9 P6 T! z4 Q- C- m# f4 F这段代码有什么作用?9 H& L- i* x  X1 s1 G

8 ]( \7 o+ P% H8 O5 ZsomeAddress 可以是非合约地址,这种情况没有合约执行,除标准交易费用外不会消耗任何 gas。这永远不会失败,但也无法保证地址是真实的和可访问的
1 Z: M, z1 H9 f3 l1 v# W* HsomeAddress 可以是合约地址。合约可以成功交易并消耗足够的 gas 费用。1 G& _) T0 j. d) s/ N
$ J4 Z% e4 M  ]4 e
someAddress 可以是合约地址。合约交易如果失败,费用会返回,但会消费掉所有 gas,并且合约调用交易停止执行。. c9 s, u  o+ o- i  e9 w  N
/ _. [" Q6 ~4 r
这看起来还行。如果合约不想收到 ETH,或者只想接收白名单上某些地址、合约的 ETH,那你对其发送 ETH 就会失败。) K5 H2 W* e( t3 c% N0 P0 |5 m# a

0 l. y( V3 I$ v* c, J: ~- [7 W然而,若以其他方式发送 ETH 的话,这种安全特性会被完全击破:合约可以在被创建时接收 ETH。然后,合约可以自毁,把它所有 ETH 发送给一个易受攻击的合约,而不用调用任何目标合约的代码。% K4 |! ~. l/ L6 g
' M8 g7 E: E1 ~
如果发送者和当前易受攻击的合约能在部署前被提前预测到,可以提前把 ETH 发送到那个地址。矿工/区块创建者能直接将区块奖励的 ETH 发送给任何合约,而不需要执行任何代码。
# d+ L1 o2 U2 x6 e1 V" c4 F# n: e0 x: ^; Y  V' x* I( {$ x( P0 A
这些虽然都不是常规用法,但它们确实提供了一种方法来将 ETH 发送到合约并完全绕过任何阻止这一行为的代码。. A' W, E- y$ `4 A* M, R7 z

! X+ T* |7 W% M& d) N! A在没有特殊权限的情况下,能利用起来的唯一方法就是自毁。这个操作只需要你用点 ETH 来创建合约,以及支付额外的 gas 费用。
& t5 e  M: Z; x# q7 v5 p4 k! n' S; d- T
另一个有趣的方法是在创建合约之前将ETH发送给合约。这样恶意开发者故意在合约代码里放漏洞。这类和合约余额有关的 bug 很难被发现,至少我已知的审核软件都没能明确指出错误,比如require(balance == whatever)。$ w' F3 @4 P% |$ ?6 s: X/ q
可以用下面这种代码阻止漏洞:  D. R  F9 Q4 T* G
uint256 expectedBalance = 0;
# F8 `& N2 }( T' C3 K! yfunction () public payable {
& d2 c3 }' x( S/ U9 P0 n   expectedBalance+=msg.value;( g8 ?" O6 [: F9 _
}
" a) s+ m5 m, n0 `, F1 p
; R! e! d0 Z; }' s. W# m现在我们只用.transfer() 给它发送一些ETH: b6 }. m6 [, E
VM Exception while processing transaction: out of gas
' E7 v# x2 w, h因为我们现在必须读写一个状态变量,这要昂贵得多。默认的 gas 费用是2300 gas。读取一个状态变量需要200 gas,并且写一个已经存在的变量需要 5000 gas(如果之前不存在则需要20,000 gas)0 o$ J0 M% C" E6 P- H  ?
所以,需要用下面代码:/ \& P2 M8 y* ]! m1 ~0 o/ x
someAddress.transfer.value(whatever).gas(7000); //just a guess9 Z5 z( S% S/ B+ B; z, W
大多数合约在交易时使用默认gas,但这里不是默认情况...不过,仔细考虑这个问题,既然我们需要更多 gas,我们可以写个更有趣的 payable 函数:
+ {( [* U; k: F! x2 gfunction () public payable{
5 h- j: Q: I# ], Q; i2 l1 |   sender.call(....);& h& K% |0 V  s5 {  S- A
}
  D3 d$ C3 {' M. a: j所以现在为了处理所调用合约的异常的余额改变问题,我们将合约暴露到一个全新形式的攻击下。默认值为2300只能调用一个非常小的外部函数,它不进行状态修改操作,基本上只能生成一个LOG。
5 u6 c9 K$ S) S6 t- g4 L0 n' U6 m  ^1 h& ~: H# r6 m$ S3 G9 t
既然我们已经增加 gas 使得合约可以读取和存储状态,那么这个合约现在也有足够的 gas 来调用之前调用它的合约,于是就可能导致重入攻击(reentrancy attack)。我不会详细介绍所有细节,但基本上它需要一个非常谨慎的合约设计来防止这个“功能”的 bug,并且唯一100%处理掉它的方法是 gas 费用低到不能进行状态修改。没有100%的方法知道当前的合约执行是不是可重入的,EVM 并未公开这类信息。
! f) P, s/ a7 M$ H. D( G2 R5 }" G- B2 N. B9 i$ a1 E
总结& I1 [- A. F- l% t& ^) R' u
对我来说,这部分 EVM 设计毫无意义。正常接受资金需要获得批准,这似乎被说成是一种安全功能,同时却有清晰且有记录的方法来绕过它,所以它没有提供任何安全性方面的价值。* l  ]& t- [. L  |" T. m: h. C; I

4 i0 _- E" \: V- y此外,如果你想减少交易费用,那么这些方法在很大程度上是你无法访问的,它们只对攻击者有用。理论上,如果你只想将代币只发送到合约,而不关心合约的执行,gas 成本应该便宜33%。1 c) ^: |/ e; d6 O. x  D* W& E/ q
. h4 [, a* f. Y1 D, V+ D2 k' L
为什么EVM没有暴露这样类似的方法呢?从历史上看,这种意外行为导致了许多合约DoS攻击和漏洞。幸运的是,Solidity让这些类型错误更难以出现。但显然它不能完全解决安全方面的问题,更不用说经济方面了。6 i1 a% H- N! @0 n

2 P$ e( j- w% m# z' r如果希望跟踪合约中确切的预期余额,那么需要支付比平常更多的 gas 并使用非标准接口,这实际上会带来更多的风险,比如重入攻击。
* b9 s# k4 C) x7 H6 X% e% ~- I4 a& [0 }4 K
我们可能永远不会知道为什么EVM这样设计,但这一问题将在 Qtum x86VM 中得到解决。将来会有一种在不执行合约的情况下向合约发送 QTUM 的方法,而且它比实际执行合约更加便宜。
BitMere.com 比特池塘系信息发布平台,比特池塘仅提供信息存储空间服务。
声明:该文观点仅代表作者本人,本文不代表比特池塘立场,且不构成建议,请谨慎对待。
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

成为第一个吐槽的人

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

    0

  • 关注

    0

  • 主题

    3