Hi 游客

更多精彩,请登录!

比特池塘 区块链技术 正文

以太坊智能合约潜在风险

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

* A+ X, [' E* Z- |对没有经验的人来说,EVM 似乎并没有只发送 ETH 到合约,但不潜在地执行任何合约代码的方法(这样的交易会失败/抛出异常)。但事实上,确实存在比较不为人知的强制合约地址接收 ETH 的方法。9 B) |. l% G/ t" C' x

" x. H, Q1 l$ e' S+ P. w# g强制接收 ETH 攻击
1 v4 I# `+ _% q/ p# Z% N; i. h! T
& f/ K" k0 q$ f2 E1 i3 z异常的余额改变并不会影响所有合约,有些时候只会导致合约中 ETH 被锁定而永远无法使用。但有些合约可能会因此完全失效,故需要加入expectedBalance 状态字段,或者添加余额异常处理方法来避免这一问题。
' K7 w8 u7 ?  A9 K4 O! z; P
5 }' Z) \4 v% V' U. K+ K) r人为例子:- [& V* L1 U8 {" y& g1 g
pragma solidity 0.4.18;2 g7 e( }; c6 s/ L* s7 B
contract ForceEther {5 s% l  V! {) L  c8 ]4 x& ~- ^& ?
bool youWin = false;, V( N' H- N7 n, @& t8 r
function onlyNonZeroBalance() {1 K- k2 z+ v1 U( a
     require(this.balance > 0);
7 e3 i0 V0 }, p9 A     youWin = true;# V* E. G* T, v
}* I$ r2 g! Z+ f- z7 H+ g
// throw if any ether is received2 x7 W+ U3 ~+ E! }5 u
function() payable {! K# W% |% G7 D5 [# ?
   revert();% \1 F* O* Y. `
}
& P5 p' F" t: N& f4 V' g! i& O3 f}9 {1 O9 i" d8 n! D, Q; W$ N5 H
另一个看上去更真实的例子:, X! [) |" @  u1 h
contract EtherGame {
# L6 V) s' @2 L9 ]/ O+ u; s6 H( W   uint public payoutMileStone1 = 3 ether;( k0 v9 [' b2 p
   uint public mileStone1Reward = 2 ether;8 U% \/ `; r* p* K7 P! R
   uint public payoutMileStone2 = 5 ether;4 C2 L) q2 R' `8 `/ V# V
   uint public mileStone2Reward = 3 ether;
% f2 P4 k/ N2 O8 F   uint public finalMileStone = 10 ether; 6 o& J  R& H/ ^; Q1 }
   uint public finalReward = 5 ether; 4 |% L% Y! w4 n6 J4 R" S5 X
   mapping(address => uint) redeemableEther;
4 n% g+ j. g. E   // users pay 0.5 ether. At specific milestones, credit their accounts' |. t1 c0 |+ B' A& b& N
   function play() public payable {
* o" F7 N/ g( W2 z8 B; I       require(msg.value == 0.5 ether); // each play is 0.5 ether* r, A1 |. I' F5 N: q
       uint currentBalance = this.balance + msg.value;. V+ z, L+ E" \* Z+ R
       // ensure no players after the game as finished+ i2 T% |2 |8 Y; @
       require(currentBalance
3 m9 L, c9 ^! P6 P! a- G/ g2 f; c. E. ~       // if at a milestone credit the players account
! `- c: F; m1 s! l       if (currentBalance == payoutMileStone1) {
  L: T) p; e" I           redeemableEther[msg.sender] += mileStone1Reward;& ]! [) W' c1 s! }
       }
7 {2 i' v# Q  J) [5 K$ p, ~       else if (currentBalance == payoutMileStone2) {
+ D" P/ D; @4 C, `$ H% U* _           redeemableEther[msg.sender] += mileStone2Reward;3 B( Y& e9 R7 ~5 P, }4 w0 x
       }
  b* T5 J  ~5 O8 q       else if (currentBalance == finalMileStone ) {
: n' ~; K6 d3 t/ ~2 M0 c( n           redeemableEther[msg.sender] += finalReward;
* ]# x# j. h3 M7 q4 N2 l       }
' i% ]- S1 X% ?3 G1 I1 f* p       return;- ?+ r; u1 Q- S9 K) g
   }* r- ?) j& B3 f% J. l, H
   function claimReward() public {
( J% N  c+ V; J- j0 C4 m       // ensure the game is complete  |5 L7 Z# e. ?1 F
       require(this.balance == finalMileStone);
- R1 P% P. f1 q7 s1 D8 H: [7 Y' m' X       // ensure there is a reward to give; B0 N: a0 @1 X" Z- V9 H% `  s
       require(redeemableEther[msg.sender] > 0); ( f  v" @) T/ p/ Q( w+ Z
       redeemableEther[msg.sender] = 0;
; [% H) F/ V. T+ Y5 G' B       msg.sender.transfer(redeemableEther[msg.sender]);
& R5 g: h" m% S8 Z* }% m! G7 |* l- Z   }
7 N: l) O6 b+ i* @( Z  b}/ @5 i5 A& H' e4 N
用户每次给合约发送 0.5 ETH,依次达到每个 milestone。攻击者可以强行将ETH发送给该合约,令其超过 finalMileStone 值,从而使合约中的所有资金都被锁定并且无法访问。 当然,这个例子也是人为设计出来的。) ]& V2 i8 h7 V) z2 z8 n7 C" p1 ]% K6 C

6 a$ E" K: g: ]- z- p, f) b  \- J不幸的是,我还无法找到任何现实生活中的例子。不过,如果有目的地把代码隐藏在Solidity合约中,这些类型的漏洞可能会非常狡猾。这种性质的攻击是Underhanded Solidity比赛的第二和第三名获胜者的获胜关键。6 a& D! H/ Y: ^
! |- y/ h  p( Z, S- e# R3 e
强制发送 ETH 到合约
) x, d3 L$ e+ ~9 x9 w$ B首先介绍一下交易是怎么发生的。假设有如下代码:
6 e0 K; p% o/ J  M5 V# k5 ]4 W& fsomeAddress.transfer(self.balance() / 10);
$ s* J- ~( X$ Y1 C2 U这段代码有什么作用?
9 h! ^/ b! }( \3 }1 C0 r$ z
8 h2 @$ s: n" X* VsomeAddress 可以是非合约地址,这种情况没有合约执行,除标准交易费用外不会消耗任何 gas。这永远不会失败,但也无法保证地址是真实的和可访问的
* {+ O" j' h4 u4 W% V0 ^someAddress 可以是合约地址。合约可以成功交易并消耗足够的 gas 费用。
* i, }5 C' Z3 @- _' L' k7 a
3 b8 h# b4 P: d- ?someAddress 可以是合约地址。合约交易如果失败,费用会返回,但会消费掉所有 gas,并且合约调用交易停止执行。  T* F2 {( C6 O9 A

  b3 r$ B. l. ~* ?# w5 \$ ~这看起来还行。如果合约不想收到 ETH,或者只想接收白名单上某些地址、合约的 ETH,那你对其发送 ETH 就会失败。/ H0 \/ D) r7 z4 E- z

9 a2 ^+ W0 q# H& a& }! ^3 Z然而,若以其他方式发送 ETH 的话,这种安全特性会被完全击破:合约可以在被创建时接收 ETH。然后,合约可以自毁,把它所有 ETH 发送给一个易受攻击的合约,而不用调用任何目标合约的代码。
& x$ X3 ]6 j( \/ W, _
' [' `, U: {/ i4 Z如果发送者和当前易受攻击的合约能在部署前被提前预测到,可以提前把 ETH 发送到那个地址。矿工/区块创建者能直接将区块奖励的 ETH 发送给任何合约,而不需要执行任何代码。# i3 e0 s  g4 l5 n2 U1 H: d
+ h7 J8 U7 {+ \) y) Z: `/ h
这些虽然都不是常规用法,但它们确实提供了一种方法来将 ETH 发送到合约并完全绕过任何阻止这一行为的代码。
+ t& @6 Q) e* Y! C8 h) L2 g7 f& s2 b: Y2 q
在没有特殊权限的情况下,能利用起来的唯一方法就是自毁。这个操作只需要你用点 ETH 来创建合约,以及支付额外的 gas 费用。# x( P# h+ T7 d' H
8 M7 S0 k& u7 Z9 j- d" g! m
另一个有趣的方法是在创建合约之前将ETH发送给合约。这样恶意开发者故意在合约代码里放漏洞。这类和合约余额有关的 bug 很难被发现,至少我已知的审核软件都没能明确指出错误,比如require(balance == whatever)。* d6 M0 R8 U+ r  F4 o. I
可以用下面这种代码阻止漏洞:* y1 u" V# J) L& P: L  I
uint256 expectedBalance = 0;' f1 R2 q- h9 O3 ]0 w
function () public payable {
$ z6 ^' s6 {- g" B, F6 D* i   expectedBalance+=msg.value;6 q. B/ V5 j: c5 [2 x" n
}9 Y! b, i. o$ s1 s+ `; }- V1 ?
# L6 f  c0 V; c$ {0 a7 y
现在我们只用.transfer() 给它发送一些ETH
6 E) R. B! D- e& b1 E/ g. y# iVM Exception while processing transaction: out of gas! C/ N8 G6 q$ `- K1 I8 p7 \
因为我们现在必须读写一个状态变量,这要昂贵得多。默认的 gas 费用是2300 gas。读取一个状态变量需要200 gas,并且写一个已经存在的变量需要 5000 gas(如果之前不存在则需要20,000 gas)- @9 P# \& G5 g/ D, S
所以,需要用下面代码:2 ~) I6 N, m/ T3 X0 T/ I
someAddress.transfer.value(whatever).gas(7000); //just a guess
$ _2 P* L7 A) K9 b5 u大多数合约在交易时使用默认gas,但这里不是默认情况...不过,仔细考虑这个问题,既然我们需要更多 gas,我们可以写个更有趣的 payable 函数:
/ r7 I& S7 |- i* o9 B( v: afunction () public payable{
/ p. j8 s  v+ K0 G8 w6 z: C   sender.call(....);2 g5 E& j) [# N/ E* t- d; i
}$ P, Z& ^  R/ ~
所以现在为了处理所调用合约的异常的余额改变问题,我们将合约暴露到一个全新形式的攻击下。默认值为2300只能调用一个非常小的外部函数,它不进行状态修改操作,基本上只能生成一个LOG。
0 N9 V- s9 z8 H* b% k3 y% j8 J/ M, v4 b
既然我们已经增加 gas 使得合约可以读取和存储状态,那么这个合约现在也有足够的 gas 来调用之前调用它的合约,于是就可能导致重入攻击(reentrancy attack)。我不会详细介绍所有细节,但基本上它需要一个非常谨慎的合约设计来防止这个“功能”的 bug,并且唯一100%处理掉它的方法是 gas 费用低到不能进行状态修改。没有100%的方法知道当前的合约执行是不是可重入的,EVM 并未公开这类信息。
# q& T3 @  ~6 e9 `
$ `0 W/ h$ p9 s, v总结
9 w. i/ y/ M" A4 r" a' `. A对我来说,这部分 EVM 设计毫无意义。正常接受资金需要获得批准,这似乎被说成是一种安全功能,同时却有清晰且有记录的方法来绕过它,所以它没有提供任何安全性方面的价值。# ^8 ^3 U/ V2 z

9 G- f3 z* o8 D2 g此外,如果你想减少交易费用,那么这些方法在很大程度上是你无法访问的,它们只对攻击者有用。理论上,如果你只想将代币只发送到合约,而不关心合约的执行,gas 成本应该便宜33%。
( b! T5 F; C. A6 n/ |+ n, O# w+ ~
* J3 P# T) Z4 y$ D: U5 ]为什么EVM没有暴露这样类似的方法呢?从历史上看,这种意外行为导致了许多合约DoS攻击和漏洞。幸运的是,Solidity让这些类型错误更难以出现。但显然它不能完全解决安全方面的问题,更不用说经济方面了。
* h( T4 [0 ]- Q$ f  _. c1 c- z# a; ?% o5 H* q2 F6 a. S
如果希望跟踪合约中确切的预期余额,那么需要支付比平常更多的 gas 并使用非标准接口,这实际上会带来更多的风险,比如重入攻击。
' ~8 j* g) q% c4 ^1 \
0 C: U  r7 [5 {/ D6 d, J我们可能永远不会知道为什么EVM这样设计,但这一问题将在 Qtum x86VM 中得到解决。将来会有一种在不执行合约的情况下向合约发送 QTUM 的方法,而且它比实际执行合约更加便宜。
BitMere.com 比特池塘系信息发布平台,比特池塘仅提供信息存储空间服务。
声明:该文观点仅代表作者本人,本文不代表比特池塘立场,且不构成建议,请谨慎对待。
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

成为第一个吐槽的人

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

    0

  • 关注

    0

  • 主题

    3