Hi 游客

更多精彩,请登录!

比特池塘 区块链技术 正文

以太坊智能合约潜在风险

吃瓜围观小分队
84 0 0
最近作者一直在思考 EVM 和 Solidity 对不可支付合约(non-payable contracts)的错误概念,以及其可能蕴含的针对智能合约的攻击(attack vectors),该类型攻击在现有 EVM 中几乎不可修复。EVM 是以太坊、Qtum 量子链和其他一些区块链合约运行的基础。基本上写 Solidity 代码就等同于使用 EVM。这篇文章只以以太坊(ETH) 为例,不过该问题同样适用于用 QTUM 等所有兼容 EVM 的项目。
( q) Z+ }/ v- N6 \8 B7 n. Y% G* _* H' i2 l. P, I
对没有经验的人来说,EVM 似乎并没有只发送 ETH 到合约,但不潜在地执行任何合约代码的方法(这样的交易会失败/抛出异常)。但事实上,确实存在比较不为人知的强制合约地址接收 ETH 的方法。
* @% N6 G8 m; Q$ I1 ~
6 M1 C0 M' y* x' R( O强制接收 ETH 攻击; ~8 w0 h4 U! p5 Y4 Y. c

$ R8 Y+ D* z& a& D异常的余额改变并不会影响所有合约,有些时候只会导致合约中 ETH 被锁定而永远无法使用。但有些合约可能会因此完全失效,故需要加入expectedBalance 状态字段,或者添加余额异常处理方法来避免这一问题。
. U5 @& J' v" E# x
( n6 T$ `' d$ K" v4 r: r0 `% h人为例子:
: V& y) G) E" b; tpragma solidity 0.4.18;: ?3 s% k- @! s4 W7 k
contract ForceEther {6 H; ?8 D# @; d$ \1 m
bool youWin = false;
8 h. V2 G1 X- c+ ~5 v, G function onlyNonZeroBalance() {& O2 `( R2 X! C$ }& G( U; f
     require(this.balance > 0);
9 r5 m/ `+ Z: J, a     youWin = true;6 ^0 ~/ o4 t+ E( H' F. r2 L
}
. F. A6 D- x0 b& W# ]4 u" u# e // throw if any ether is received
( ~; `! z& e7 Z function() payable {( C- Y+ L( T$ Q/ i5 h
   revert();
5 J' c: }& d) C' U* L& i' E& t }
& r6 w) |6 I6 Z0 ^}. _# N4 O; B3 G" {% d
另一个看上去更真实的例子:( n$ H" x# o+ e  b9 _6 `! N2 c
contract EtherGame {
, Y0 G, |8 g* ~. I1 J   uint public payoutMileStone1 = 3 ether;3 H& n7 j+ y6 }" h; w' _* O( ], D
   uint public mileStone1Reward = 2 ether;
1 G; E/ b, P3 R0 x: y+ u9 _   uint public payoutMileStone2 = 5 ether;
, \4 }4 \3 v; [# M, ]& P   uint public mileStone2Reward = 3 ether; % y+ l9 ^: |- {% R, u& K* r
   uint public finalMileStone = 10 ether;
+ W5 L4 ]3 e7 ?1 X, x$ m* R2 B   uint public finalReward = 5 ether; & r0 L; k/ s3 h; v
   mapping(address => uint) redeemableEther;+ `3 W* j' j; d! k. K* L
   // users pay 0.5 ether. At specific milestones, credit their accounts- r: G! H: `1 s2 e0 }
   function play() public payable {
, V; k; k4 D5 Q) g8 w       require(msg.value == 0.5 ether); // each play is 0.5 ether* v/ j" Z  f; L' F
       uint currentBalance = this.balance + msg.value;: u0 P7 t- ^7 U# k. s% n
       // ensure no players after the game as finished5 ?5 O9 ^9 s' e3 z
       require(currentBalance . z4 G5 G1 h" y  ?3 s, p& d9 Y6 K. x7 T* G
       // if at a milestone credit the players account$ H' k' S4 F0 l2 S( y3 \& R/ q
       if (currentBalance == payoutMileStone1) {3 R6 |! t5 ?+ \7 r) H! E# m7 c! [% g
           redeemableEther[msg.sender] += mileStone1Reward;3 Q% d; I$ |. l' [9 D
       }; G( \& Z, {  Z# p3 G; N) ~7 b, D0 R
       else if (currentBalance == payoutMileStone2) {7 B6 n& k* e" E; u+ b2 e
           redeemableEther[msg.sender] += mileStone2Reward;
0 {/ x& G& S4 ]0 a6 q: t& i       }' [; O; v# o5 G  a. L& X
       else if (currentBalance == finalMileStone ) {$ I' Y9 ^3 h/ g3 e
           redeemableEther[msg.sender] += finalReward;, i  u, ]7 k; v+ O
       }( ]' s' l( {: H4 [) i
       return;- b7 N- @: P( Q! k: ]& d
   }% k' G( {% y' N; \: ^
   function claimReward() public {
& F0 k. a0 m8 Z% n5 }       // ensure the game is complete/ C; T/ n- v' ~# s3 |8 m$ T
       require(this.balance == finalMileStone);
4 }) I/ A: F: [! Y3 E  k       // ensure there is a reward to give. u; J/ _* b1 C4 g! Z1 F- J+ n8 W
       require(redeemableEther[msg.sender] > 0);
9 U, q+ s, F/ s) ^4 P  a       redeemableEther[msg.sender] = 0;7 }: L2 G& X* D
       msg.sender.transfer(redeemableEther[msg.sender]);4 j  `& Q7 E# P* I5 v# R; D
   }
) l) C; f! x+ ^. u* u}+ t) A6 D* x- W4 I% q
用户每次给合约发送 0.5 ETH,依次达到每个 milestone。攻击者可以强行将ETH发送给该合约,令其超过 finalMileStone 值,从而使合约中的所有资金都被锁定并且无法访问。 当然,这个例子也是人为设计出来的。
. s* K+ b: ?3 _1 \' p
/ \9 [# x3 x% U0 ?不幸的是,我还无法找到任何现实生活中的例子。不过,如果有目的地把代码隐藏在Solidity合约中,这些类型的漏洞可能会非常狡猾。这种性质的攻击是Underhanded Solidity比赛的第二和第三名获胜者的获胜关键。
9 ~* C1 e% Y( o2 A# C( |. }1 X" A4 T9 N7 }
强制发送 ETH 到合约
$ g* I$ a4 N# Y* n% t首先介绍一下交易是怎么发生的。假设有如下代码:
8 u! f. @( ^4 g6 e. IsomeAddress.transfer(self.balance() / 10);- g' d' l  M$ C3 N
这段代码有什么作用?
, k- j8 k2 ]$ e  R3 e' H# [+ x" s8 p4 {5 T# `5 b
someAddress 可以是非合约地址,这种情况没有合约执行,除标准交易费用外不会消耗任何 gas。这永远不会失败,但也无法保证地址是真实的和可访问的
) Y& {- f$ O* X9 w  n" N7 U! r, UsomeAddress 可以是合约地址。合约可以成功交易并消耗足够的 gas 费用。( L6 J/ g6 t; }7 K* z

2 p; f5 ?1 s" r, n6 isomeAddress 可以是合约地址。合约交易如果失败,费用会返回,但会消费掉所有 gas,并且合约调用交易停止执行。. S) N4 u& D+ B$ t4 o" E

& F( f; u( Y* w0 N( [这看起来还行。如果合约不想收到 ETH,或者只想接收白名单上某些地址、合约的 ETH,那你对其发送 ETH 就会失败。
" P7 U: A; p3 }; e8 k
- I/ v+ \  V* F+ ^* `/ U' r然而,若以其他方式发送 ETH 的话,这种安全特性会被完全击破:合约可以在被创建时接收 ETH。然后,合约可以自毁,把它所有 ETH 发送给一个易受攻击的合约,而不用调用任何目标合约的代码。
6 v$ H5 v1 i6 {3 @- U
0 W  W  k; \# X4 ^# c' _) \如果发送者和当前易受攻击的合约能在部署前被提前预测到,可以提前把 ETH 发送到那个地址。矿工/区块创建者能直接将区块奖励的 ETH 发送给任何合约,而不需要执行任何代码。4 b9 C+ d0 O- x8 b. P$ ?+ x6 C

7 E) R+ ]& |& G! d3 S6 H, p, v这些虽然都不是常规用法,但它们确实提供了一种方法来将 ETH 发送到合约并完全绕过任何阻止这一行为的代码。
& z( w2 ^% S7 H. P
  c) b) b& y: G6 o2 K8 _在没有特殊权限的情况下,能利用起来的唯一方法就是自毁。这个操作只需要你用点 ETH 来创建合约,以及支付额外的 gas 费用。+ I; s8 W5 h: O# O, J. W) k# v
+ y, f, e$ _. U7 ]" Q9 K5 X
另一个有趣的方法是在创建合约之前将ETH发送给合约。这样恶意开发者故意在合约代码里放漏洞。这类和合约余额有关的 bug 很难被发现,至少我已知的审核软件都没能明确指出错误,比如require(balance == whatever)。
4 K' o: U$ r+ F/ j$ q* O' f. R; X# E可以用下面这种代码阻止漏洞:
$ G7 X: b+ n7 cuint256 expectedBalance = 0;
& q( ?+ D7 I3 ?  Nfunction () public payable {
' Q7 [9 u0 J' ^; m1 g. n   expectedBalance+=msg.value;
! J$ C( ~5 X9 O8 T" N* B$ h}
! l: A, B' {1 X3 \2 f* a
4 X! n) h) R) b2 x# i现在我们只用.transfer() 给它发送一些ETH
2 M$ d4 `( @) Z) @" Q. b; HVM Exception while processing transaction: out of gas, }" [5 A( j4 b9 T# m& M
因为我们现在必须读写一个状态变量,这要昂贵得多。默认的 gas 费用是2300 gas。读取一个状态变量需要200 gas,并且写一个已经存在的变量需要 5000 gas(如果之前不存在则需要20,000 gas), E6 y4 j; X$ }; r6 ~
所以,需要用下面代码:
6 n/ ]3 I+ i/ n6 UsomeAddress.transfer.value(whatever).gas(7000); //just a guess4 c9 q5 w* L, T! y4 D; _5 h% r
大多数合约在交易时使用默认gas,但这里不是默认情况...不过,仔细考虑这个问题,既然我们需要更多 gas,我们可以写个更有趣的 payable 函数:- y. }+ x6 D# Q6 D' L: j& Z7 u6 N- J( n
function () public payable{
+ {7 N. Z- N* a4 i   sender.call(....);) _5 r9 `: s1 ?- K. K; u
}/ x" ~8 G/ ]3 J6 Q# f# T2 z# S
所以现在为了处理所调用合约的异常的余额改变问题,我们将合约暴露到一个全新形式的攻击下。默认值为2300只能调用一个非常小的外部函数,它不进行状态修改操作,基本上只能生成一个LOG。5 {) g3 L1 b0 t' |) K

" {( x6 f# V6 p7 r: ^: g: v* K既然我们已经增加 gas 使得合约可以读取和存储状态,那么这个合约现在也有足够的 gas 来调用之前调用它的合约,于是就可能导致重入攻击(reentrancy attack)。我不会详细介绍所有细节,但基本上它需要一个非常谨慎的合约设计来防止这个“功能”的 bug,并且唯一100%处理掉它的方法是 gas 费用低到不能进行状态修改。没有100%的方法知道当前的合约执行是不是可重入的,EVM 并未公开这类信息。
% n" S% S! e. V& K- U( `, q
& n* B' Z$ Y: p- L1 c8 ?. H' c& D总结
$ t/ }" i6 j* c7 S对我来说,这部分 EVM 设计毫无意义。正常接受资金需要获得批准,这似乎被说成是一种安全功能,同时却有清晰且有记录的方法来绕过它,所以它没有提供任何安全性方面的价值。* C) M+ S5 o- H, J% i( `' a7 ^

2 x0 m  {( j  T! O* t  f! Z此外,如果你想减少交易费用,那么这些方法在很大程度上是你无法访问的,它们只对攻击者有用。理论上,如果你只想将代币只发送到合约,而不关心合约的执行,gas 成本应该便宜33%。
' ^) |) ?, r/ V# |, a
2 W& f7 x! @& d+ }& O为什么EVM没有暴露这样类似的方法呢?从历史上看,这种意外行为导致了许多合约DoS攻击和漏洞。幸运的是,Solidity让这些类型错误更难以出现。但显然它不能完全解决安全方面的问题,更不用说经济方面了。
% D- X9 |3 e; H" R2 K0 T# H5 K* c# L9 O4 d5 G
如果希望跟踪合约中确切的预期余额,那么需要支付比平常更多的 gas 并使用非标准接口,这实际上会带来更多的风险,比如重入攻击。
5 I' r) I% c+ m/ C. ^8 @) p8 I6 Q7 x# H( J! R& `
我们可能永远不会知道为什么EVM这样设计,但这一问题将在 Qtum x86VM 中得到解决。将来会有一种在不执行合约的情况下向合约发送 QTUM 的方法,而且它比实际执行合约更加便宜。
BitMere.com 比特池塘系信息发布平台,比特池塘仅提供信息存储空间服务。
声明:该文观点仅代表作者本人,本文不代表比特池塘立场,且不构成建议,请谨慎对待。
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

成为第一个吐槽的人

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

    0

  • 关注

    0

  • 主题

    3