Hi 游客

更多精彩,请登录!

比特池塘 区块链技术 正文

以太坊智能合约潜在风险

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

- c8 b: g9 X1 u& ]! s对没有经验的人来说,EVM 似乎并没有只发送 ETH 到合约,但不潜在地执行任何合约代码的方法(这样的交易会失败/抛出异常)。但事实上,确实存在比较不为人知的强制合约地址接收 ETH 的方法。
. Z! t- c3 u- b! S
3 B2 [/ W! Y. R强制接收 ETH 攻击, X- U; d0 o9 q* ~2 R& x

6 t* ^& y. u% }9 ^1 p异常的余额改变并不会影响所有合约,有些时候只会导致合约中 ETH 被锁定而永远无法使用。但有些合约可能会因此完全失效,故需要加入expectedBalance 状态字段,或者添加余额异常处理方法来避免这一问题。
) V/ _" h! d3 ^* d! T* k; W4 k
; L4 J7 J# c6 Z' m: Y人为例子:
) A! p' @  }2 K$ Upragma solidity 0.4.18;
% h- p8 c* {  g, l3 I2 E! u6 K. _1 zcontract ForceEther {
) t5 f) m5 ?, Z: Y, d bool youWin = false;
% W, }% d* u) G4 _4 m5 }' O function onlyNonZeroBalance() {" z& V1 W% v' o) M% g
     require(this.balance > 0);
! t3 m5 @& |4 b; h4 T     youWin = true;/ S2 l+ A+ c. @' F
}
1 t; ]6 s3 Y4 u( V: O6 R; N // throw if any ether is received" ?5 W. _& _# o; {% Y
function() payable {1 {; B5 E8 F0 X% ^, S8 z
   revert();
& W" J3 S0 L- ]7 s5 I }
( R. z( H4 O! h0 a/ h8 R8 }}% e* _% f1 [# l) a6 Y
另一个看上去更真实的例子:: h3 ]7 q. T$ s( J2 Q6 W
contract EtherGame {
+ p0 g! B/ k: S9 U% W* l- u# |   uint public payoutMileStone1 = 3 ether;
/ V8 X, t' T: O/ H   uint public mileStone1Reward = 2 ether;
3 N$ ~: W- ?& O* S% {  r9 ^   uint public payoutMileStone2 = 5 ether;
' Q; J$ V8 w( Y% l( T   uint public mileStone2Reward = 3 ether;
7 C3 c: x7 `/ ^9 a   uint public finalMileStone = 10 ether;
% z5 c* h" f1 E* t5 T+ \7 d% Y   uint public finalReward = 5 ether;
0 `+ q, p6 \5 V9 b   mapping(address => uint) redeemableEther;2 ?- T; R  s+ F( p5 w
   // users pay 0.5 ether. At specific milestones, credit their accounts  w2 G' L8 S1 {9 a6 e1 ?
   function play() public payable {
! S0 {9 w2 @9 w4 I2 Y       require(msg.value == 0.5 ether); // each play is 0.5 ether
6 a" y1 i# }7 ^* O0 A: h! z# @' P' z       uint currentBalance = this.balance + msg.value;
, k) {8 E) g4 M! n5 J' R# @& D" b       // ensure no players after the game as finished
5 i4 l6 I& Z6 f3 L       require(currentBalance
1 |9 a# W# `# S3 H- Q       // if at a milestone credit the players account* L$ x3 Z# g3 i  x0 ~8 o
       if (currentBalance == payoutMileStone1) {
2 V) i8 n. h+ E: W           redeemableEther[msg.sender] += mileStone1Reward;6 X7 o; L5 M& W' k+ a9 @/ L' X
       }8 Z4 |4 Y0 S6 J2 E+ R! w8 V% ]
       else if (currentBalance == payoutMileStone2) {$ M) L! c5 d: J" }
           redeemableEther[msg.sender] += mileStone2Reward;
. I0 y( w' T& ]3 q       }
7 D/ }# U( _0 m, ^7 O- R       else if (currentBalance == finalMileStone ) {$ j' Q3 i2 _9 w- ?5 f+ ?
           redeemableEther[msg.sender] += finalReward;( U4 b2 A9 A5 j5 U
       }& J. X9 Q) Z2 \6 }- `) n  G8 m6 C0 Y
       return;  j; p' z0 s  d, L9 B( w, R
   }! V) X% }8 r) F4 L
   function claimReward() public {
! l7 {  h  ~2 D- ^+ h  S2 z* B       // ensure the game is complete. d: W- \6 x3 _/ L, K
       require(this.balance == finalMileStone);
+ o. ~; ~/ G9 `& [# F0 N7 ]* Y2 w1 h       // ensure there is a reward to give+ T- n* {- X7 a  Q2 ^, h4 N
       require(redeemableEther[msg.sender] > 0); ; Q$ H* k3 ]' J* y
       redeemableEther[msg.sender] = 0;) {& N( f1 B4 p
       msg.sender.transfer(redeemableEther[msg.sender]);4 I2 ]/ O& _# B1 t+ x+ t( U9 E' t
   }3 a, i0 G' D7 ]- d) U/ A% d( x6 J: o
}
, k" q, q+ @, q0 _用户每次给合约发送 0.5 ETH,依次达到每个 milestone。攻击者可以强行将ETH发送给该合约,令其超过 finalMileStone 值,从而使合约中的所有资金都被锁定并且无法访问。 当然,这个例子也是人为设计出来的。4 h5 `( n( U0 {4 B9 t  v- C# C
( p$ r' @6 w: V( J3 u1 m
不幸的是,我还无法找到任何现实生活中的例子。不过,如果有目的地把代码隐藏在Solidity合约中,这些类型的漏洞可能会非常狡猾。这种性质的攻击是Underhanded Solidity比赛的第二和第三名获胜者的获胜关键。3 G+ b: V: P$ j/ F$ q1 E4 Y

* {6 F4 R: A+ u# K强制发送 ETH 到合约6 X; J+ S1 E% e5 T
首先介绍一下交易是怎么发生的。假设有如下代码:" T0 w) P! ~, K* O
someAddress.transfer(self.balance() / 10);
3 h: ?) b+ t8 {9 C5 O" ], j这段代码有什么作用?7 ]: Q# u( J$ o9 F0 Y
# j! ^$ Y6 e: j( ]; e
someAddress 可以是非合约地址,这种情况没有合约执行,除标准交易费用外不会消耗任何 gas。这永远不会失败,但也无法保证地址是真实的和可访问的3 d% S, h% ~' D1 g0 L
someAddress 可以是合约地址。合约可以成功交易并消耗足够的 gas 费用。
& h+ s6 m" V0 G# J4 k( t, B9 B0 Q, m  @( _8 t& H
someAddress 可以是合约地址。合约交易如果失败,费用会返回,但会消费掉所有 gas,并且合约调用交易停止执行。& U7 d4 H/ T( t, ?* |! v, n
7 ~& P- z* D0 w0 `- r
这看起来还行。如果合约不想收到 ETH,或者只想接收白名单上某些地址、合约的 ETH,那你对其发送 ETH 就会失败。% {- W! i" t  {. ?' q; D$ |# t, p

% H# b2 X5 ^, e' H8 i' d然而,若以其他方式发送 ETH 的话,这种安全特性会被完全击破:合约可以在被创建时接收 ETH。然后,合约可以自毁,把它所有 ETH 发送给一个易受攻击的合约,而不用调用任何目标合约的代码。  h$ s) ~/ i6 N3 o
9 V9 ~5 K- |- ^' ~: o1 L
如果发送者和当前易受攻击的合约能在部署前被提前预测到,可以提前把 ETH 发送到那个地址。矿工/区块创建者能直接将区块奖励的 ETH 发送给任何合约,而不需要执行任何代码。
" t7 S* {$ {9 Q0 |4 v- I1 m- _+ G% c" f$ x) {4 p
这些虽然都不是常规用法,但它们确实提供了一种方法来将 ETH 发送到合约并完全绕过任何阻止这一行为的代码。$ Y6 i2 u  P7 t! Y2 ?6 F
" J6 X" j5 J# I+ U2 C9 R! B& a
在没有特殊权限的情况下,能利用起来的唯一方法就是自毁。这个操作只需要你用点 ETH 来创建合约,以及支付额外的 gas 费用。
5 K' q( m9 Z' D# V" G
% p- O2 Y5 B4 R另一个有趣的方法是在创建合约之前将ETH发送给合约。这样恶意开发者故意在合约代码里放漏洞。这类和合约余额有关的 bug 很难被发现,至少我已知的审核软件都没能明确指出错误,比如require(balance == whatever)。3 _( k8 F( T* y, i$ w
可以用下面这种代码阻止漏洞:" Q% d" n! O: l# ^; o" U
uint256 expectedBalance = 0;
, A4 I4 t* }' k6 u7 b" ~function () public payable {
0 n: s+ S( ?$ x4 a7 J2 E9 t$ l, c   expectedBalance+=msg.value;
2 `% D0 V8 T8 Z: @7 Y% \) U. r}0 W& k4 q1 P0 H4 m

5 |+ O+ d8 g: j. C- r现在我们只用.transfer() 给它发送一些ETH' d: j( M% ]; h
VM Exception while processing transaction: out of gas: i- f4 G9 A4 i4 b3 R: u
因为我们现在必须读写一个状态变量,这要昂贵得多。默认的 gas 费用是2300 gas。读取一个状态变量需要200 gas,并且写一个已经存在的变量需要 5000 gas(如果之前不存在则需要20,000 gas)
1 a- l4 u' X; t. m: K  }所以,需要用下面代码:& ?5 D( q1 |5 U/ ]
someAddress.transfer.value(whatever).gas(7000); //just a guess1 \: w" @1 _; a, ~5 n& u( D6 l
大多数合约在交易时使用默认gas,但这里不是默认情况...不过,仔细考虑这个问题,既然我们需要更多 gas,我们可以写个更有趣的 payable 函数:7 m1 S! m* _0 ~0 x
function () public payable{( l8 z) S( z' D# Z
   sender.call(....);
1 x* K, o" d- w8 ~" ]9 }}5 v! m+ d+ b  ]- |2 }/ B) r
所以现在为了处理所调用合约的异常的余额改变问题,我们将合约暴露到一个全新形式的攻击下。默认值为2300只能调用一个非常小的外部函数,它不进行状态修改操作,基本上只能生成一个LOG。
5 X! a# E7 e$ [; R1 z1 h; Q" }3 R3 |, Y4 D
既然我们已经增加 gas 使得合约可以读取和存储状态,那么这个合约现在也有足够的 gas 来调用之前调用它的合约,于是就可能导致重入攻击(reentrancy attack)。我不会详细介绍所有细节,但基本上它需要一个非常谨慎的合约设计来防止这个“功能”的 bug,并且唯一100%处理掉它的方法是 gas 费用低到不能进行状态修改。没有100%的方法知道当前的合约执行是不是可重入的,EVM 并未公开这类信息。; W+ s, X# G7 t; C1 Z
8 z7 ]/ |' L" r: L- O
总结. [6 d7 l# w- x, |  F" ]3 ]- [# U, ^
对我来说,这部分 EVM 设计毫无意义。正常接受资金需要获得批准,这似乎被说成是一种安全功能,同时却有清晰且有记录的方法来绕过它,所以它没有提供任何安全性方面的价值。
2 P$ `1 x9 c. P" n, P( U6 |* e  m/ m& j* K
此外,如果你想减少交易费用,那么这些方法在很大程度上是你无法访问的,它们只对攻击者有用。理论上,如果你只想将代币只发送到合约,而不关心合约的执行,gas 成本应该便宜33%。
/ ~# o, z: O5 z) ?$ H4 W! T- f* H1 p, S$ }8 v
为什么EVM没有暴露这样类似的方法呢?从历史上看,这种意外行为导致了许多合约DoS攻击和漏洞。幸运的是,Solidity让这些类型错误更难以出现。但显然它不能完全解决安全方面的问题,更不用说经济方面了。5 B' p9 R9 c. S* v, Q
8 m& B& S# p7 l. T. n
如果希望跟踪合约中确切的预期余额,那么需要支付比平常更多的 gas 并使用非标准接口,这实际上会带来更多的风险,比如重入攻击。; T( \7 _0 z9 p  s

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

本版积分规则

成为第一个吐槽的人

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

    0

  • 关注

    0

  • 主题

    3