Hi 游客

更多精彩,请登录!

比特池塘 区块链技术 正文

以太坊智能合约潜在风险

吃瓜围观小分队
83 0 0
最近作者一直在思考 EVM 和 Solidity 对不可支付合约(non-payable contracts)的错误概念,以及其可能蕴含的针对智能合约的攻击(attack vectors),该类型攻击在现有 EVM 中几乎不可修复。EVM 是以太坊、Qtum 量子链和其他一些区块链合约运行的基础。基本上写 Solidity 代码就等同于使用 EVM。这篇文章只以以太坊(ETH) 为例,不过该问题同样适用于用 QTUM 等所有兼容 EVM 的项目。5 j- K) Z& \# c$ H/ t8 ?9 s

0 e& I' Q6 G' X2 Z对没有经验的人来说,EVM 似乎并没有只发送 ETH 到合约,但不潜在地执行任何合约代码的方法(这样的交易会失败/抛出异常)。但事实上,确实存在比较不为人知的强制合约地址接收 ETH 的方法。: {9 n: j: g# _! R5 n0 `, z
4 |- D* f  F* A, a+ W( `
强制接收 ETH 攻击# {$ B8 j3 m( B& B$ n( P6 }$ _
4 q5 |* H1 Y! u9 y4 n1 O
异常的余额改变并不会影响所有合约,有些时候只会导致合约中 ETH 被锁定而永远无法使用。但有些合约可能会因此完全失效,故需要加入expectedBalance 状态字段,或者添加余额异常处理方法来避免这一问题。1 [$ O6 u( F: `4 I4 O

( h! f4 C/ F% t7 \人为例子:7 U% C9 [0 B* r3 K$ z  W% G1 `7 j  h* o
pragma solidity 0.4.18;' K" |& H& K( H" j
contract ForceEther {% e( m' P! \* n# ~, ~, g) B
bool youWin = false;
" N( \8 i4 f/ ]3 E function onlyNonZeroBalance() {. X* @* ~  A6 u5 J% V
     require(this.balance > 0);
' m+ m, _8 M2 [; U, w     youWin = true;9 H& `+ l: u/ x" ~
}
6 r3 J; u# n! W6 `, d3 e% p0 I // throw if any ether is received
6 [' r# x: d3 _/ R' M9 O9 b/ o function() payable {
# A  L5 [# [& K6 ]3 d   revert();
5 ]4 Y1 r# q/ C' P. X, H( X }; v1 g& d: k$ |, x$ g, P4 C4 A* I
}
7 Y1 h! m9 X' D6 s! e' l2 }另一个看上去更真实的例子:
# P5 I7 p& @( A7 M) R: ncontract EtherGame { ! ~2 \6 B" s; b/ U
   uint public payoutMileStone1 = 3 ether;! F1 e. f: P! W5 d* ^
   uint public mileStone1Reward = 2 ether;
5 E6 u- T6 Q$ `  i$ J. d   uint public payoutMileStone2 = 5 ether;
8 h. v/ [' M0 D* h) B% _, v; K   uint public mileStone2Reward = 3 ether;
) f- p# K4 K( m   uint public finalMileStone = 10 ether; ( i) y: w! u9 ~, g4 s/ C1 Q$ n, ?9 s; u
   uint public finalReward = 5 ether; 2 a& C5 ]- o; |
   mapping(address => uint) redeemableEther;  c2 d/ N1 }5 o) H3 H
   // users pay 0.5 ether. At specific milestones, credit their accounts
( u3 I& K% H" L$ k   function play() public payable {4 n7 z( e% j7 N. X- r6 F3 c
       require(msg.value == 0.5 ether); // each play is 0.5 ether! l# x2 e  J% [; |! W
       uint currentBalance = this.balance + msg.value;
% r  K7 `8 ~: @- B1 p% s       // ensure no players after the game as finished% |  t6 V/ N  @
       require(currentBalance 5 T' k5 a: I! `/ Z" E$ q  n
       // if at a milestone credit the players account
+ ~3 j9 `5 E4 F  P6 \  n       if (currentBalance == payoutMileStone1) {
+ @& D3 p8 o. o. x           redeemableEther[msg.sender] += mileStone1Reward;
8 C. J+ }. ?  H/ G3 N9 o( [       }! Y, Y. |. l3 [% A5 l( m
       else if (currentBalance == payoutMileStone2) {' ]; D, W7 O* R. _
           redeemableEther[msg.sender] += mileStone2Reward;
/ y; Q8 A) s; H( ?' e       }
8 n+ Q9 g9 i% o; Y  b5 @       else if (currentBalance == finalMileStone ) {. {) x. T9 S( X  ?- ^6 Q+ a1 `3 K0 K6 {
           redeemableEther[msg.sender] += finalReward;! `9 M5 E4 n( D
       }
6 A$ m) `: V. I/ K       return;
4 v$ ^, O0 W7 G9 S, [" s! L8 O   }
1 E; ^4 i4 i0 J4 o& R   function claimReward() public {5 b0 N8 B+ e* `! e- b9 q
       // ensure the game is complete/ ~4 I, k# K! o5 O- m2 m
       require(this.balance == finalMileStone);( l  p4 [/ s2 F
       // ensure there is a reward to give+ u! a3 x$ U9 n# Z
       require(redeemableEther[msg.sender] > 0);
" F+ r# I( n! O& c' |0 a4 S. L       redeemableEther[msg.sender] = 0;
  U% ~' O: M7 r$ M! H       msg.sender.transfer(redeemableEther[msg.sender]);
2 i9 o" H0 g+ f3 ~6 Z$ S   }+ I5 e' Z: m* ]
}+ N3 U; G9 K; @* g& ^' P
用户每次给合约发送 0.5 ETH,依次达到每个 milestone。攻击者可以强行将ETH发送给该合约,令其超过 finalMileStone 值,从而使合约中的所有资金都被锁定并且无法访问。 当然,这个例子也是人为设计出来的。( q" x7 _& G5 g9 k/ |. T# ?

1 R$ N/ ]2 `" A8 j- J: J不幸的是,我还无法找到任何现实生活中的例子。不过,如果有目的地把代码隐藏在Solidity合约中,这些类型的漏洞可能会非常狡猾。这种性质的攻击是Underhanded Solidity比赛的第二和第三名获胜者的获胜关键。
2 X. `5 P. I( n; d# a1 e  j- p: y+ W9 g
强制发送 ETH 到合约
# T6 Y7 E2 _7 p* g4 W首先介绍一下交易是怎么发生的。假设有如下代码:
6 q, A9 D% S  WsomeAddress.transfer(self.balance() / 10);' q, L, R  d8 W- M: z0 q7 ]
这段代码有什么作用?
' Q: o6 j/ _! u6 v/ [% F+ |8 J3 D' X) y0 B# x
someAddress 可以是非合约地址,这种情况没有合约执行,除标准交易费用外不会消耗任何 gas。这永远不会失败,但也无法保证地址是真实的和可访问的
  T( [* s( U! o" S0 c& q; D  bsomeAddress 可以是合约地址。合约可以成功交易并消耗足够的 gas 费用。
9 t8 M) _% |5 M9 x7 R7 }9 Q; s& S$ I  f* {+ j4 u  }
someAddress 可以是合约地址。合约交易如果失败,费用会返回,但会消费掉所有 gas,并且合约调用交易停止执行。
; ]# ~( I2 q5 U; U; l6 ^6 _% M% \0 {6 l0 e
这看起来还行。如果合约不想收到 ETH,或者只想接收白名单上某些地址、合约的 ETH,那你对其发送 ETH 就会失败。
; U2 B- n- f  S) s  e  n' g
8 p* {4 ?0 L$ [0 N" |然而,若以其他方式发送 ETH 的话,这种安全特性会被完全击破:合约可以在被创建时接收 ETH。然后,合约可以自毁,把它所有 ETH 发送给一个易受攻击的合约,而不用调用任何目标合约的代码。
3 h% Y( V8 a) k) v1 U$ H  X2 y2 R3 G
如果发送者和当前易受攻击的合约能在部署前被提前预测到,可以提前把 ETH 发送到那个地址。矿工/区块创建者能直接将区块奖励的 ETH 发送给任何合约,而不需要执行任何代码。
& j: U" O, A' \5 D5 d2 J7 c% ^4 y, B, k6 C
这些虽然都不是常规用法,但它们确实提供了一种方法来将 ETH 发送到合约并完全绕过任何阻止这一行为的代码。
) N0 K0 D- F3 A. W# {5 B
: \; v# B& B5 |4 M# \, H在没有特殊权限的情况下,能利用起来的唯一方法就是自毁。这个操作只需要你用点 ETH 来创建合约,以及支付额外的 gas 费用。
' N4 l, d* X+ a: B! ?: a3 K; T* D5 x7 n0 I
另一个有趣的方法是在创建合约之前将ETH发送给合约。这样恶意开发者故意在合约代码里放漏洞。这类和合约余额有关的 bug 很难被发现,至少我已知的审核软件都没能明确指出错误,比如require(balance == whatever)。/ t/ Y1 W% D+ G8 r' x5 `& H
可以用下面这种代码阻止漏洞:  y0 J3 c: C4 c# J
uint256 expectedBalance = 0;6 L! ^2 N$ e/ q
function () public payable {
; Y" w1 `9 v3 g3 w" x   expectedBalance+=msg.value;
  Y! S% P- h* {& X}
5 [5 U: i! Q2 V1 B! B5 d, `' @, P* ]) F( P; ~, I
现在我们只用.transfer() 给它发送一些ETH% m+ E& P; U! J) Z& i0 d3 `
VM Exception while processing transaction: out of gas
! p0 T" e4 Z4 b$ Q: @$ ^2 M' d因为我们现在必须读写一个状态变量,这要昂贵得多。默认的 gas 费用是2300 gas。读取一个状态变量需要200 gas,并且写一个已经存在的变量需要 5000 gas(如果之前不存在则需要20,000 gas)/ t0 ^: N3 L( {
所以,需要用下面代码:
: s" ?5 o7 ?" QsomeAddress.transfer.value(whatever).gas(7000); //just a guess
5 v3 B4 B: D) r2 v1 u大多数合约在交易时使用默认gas,但这里不是默认情况...不过,仔细考虑这个问题,既然我们需要更多 gas,我们可以写个更有趣的 payable 函数:8 f; ]% R! R2 k" Y2 b+ l3 a7 e; U
function () public payable{
' S, b& M  ~2 F' n" J( f( S3 m   sender.call(....);
: v* z( p; l: u9 H. L}
  `  \: Q0 F9 p所以现在为了处理所调用合约的异常的余额改变问题,我们将合约暴露到一个全新形式的攻击下。默认值为2300只能调用一个非常小的外部函数,它不进行状态修改操作,基本上只能生成一个LOG。* h; l) L9 e) A. p. |6 [' o
. @1 c  Y/ p, C  Z8 s5 B
既然我们已经增加 gas 使得合约可以读取和存储状态,那么这个合约现在也有足够的 gas 来调用之前调用它的合约,于是就可能导致重入攻击(reentrancy attack)。我不会详细介绍所有细节,但基本上它需要一个非常谨慎的合约设计来防止这个“功能”的 bug,并且唯一100%处理掉它的方法是 gas 费用低到不能进行状态修改。没有100%的方法知道当前的合约执行是不是可重入的,EVM 并未公开这类信息。
8 l* ^' [5 n3 R; V
2 ]% r. }+ w3 P# L0 `# u2 _' m7 ~总结4 j  W, {$ S" a* R
对我来说,这部分 EVM 设计毫无意义。正常接受资金需要获得批准,这似乎被说成是一种安全功能,同时却有清晰且有记录的方法来绕过它,所以它没有提供任何安全性方面的价值。
1 y8 n+ D) Y- E6 L% }5 D% X. x2 C6 E+ B7 o4 C
此外,如果你想减少交易费用,那么这些方法在很大程度上是你无法访问的,它们只对攻击者有用。理论上,如果你只想将代币只发送到合约,而不关心合约的执行,gas 成本应该便宜33%。
* x& i: f* M+ A* J/ ~
" x# @) w9 H/ V/ C0 V9 N& h为什么EVM没有暴露这样类似的方法呢?从历史上看,这种意外行为导致了许多合约DoS攻击和漏洞。幸运的是,Solidity让这些类型错误更难以出现。但显然它不能完全解决安全方面的问题,更不用说经济方面了。
3 k2 b5 ~, [2 J. d4 V0 _" e, o# V5 D5 D9 D8 E" e1 ?# \
如果希望跟踪合约中确切的预期余额,那么需要支付比平常更多的 gas 并使用非标准接口,这实际上会带来更多的风险,比如重入攻击。! R* x' g' O# R8 _
* h2 _1 o6 z# `2 g$ H
我们可能永远不会知道为什么EVM这样设计,但这一问题将在 Qtum x86VM 中得到解决。将来会有一种在不执行合约的情况下向合约发送 QTUM 的方法,而且它比实际执行合约更加便宜。
BitMere.com 比特池塘系信息发布平台,比特池塘仅提供信息存储空间服务。
声明:该文观点仅代表作者本人,本文不代表比特池塘立场,且不构成建议,请谨慎对待。
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

成为第一个吐槽的人

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

    0

  • 关注

    0

  • 主题

    3