Hi 游客

更多精彩,请登录!

比特池塘 区块链技术 正文

以太坊智能合约潜在风险

吃瓜围观小分队
132 0 0
最近作者一直在思考 EVM 和 Solidity 对不可支付合约(non-payable contracts)的错误概念,以及其可能蕴含的针对智能合约的攻击(attack vectors),该类型攻击在现有 EVM 中几乎不可修复。EVM 是以太坊、Qtum 量子链和其他一些区块链合约运行的基础。基本上写 Solidity 代码就等同于使用 EVM。这篇文章只以以太坊(ETH) 为例,不过该问题同样适用于用 QTUM 等所有兼容 EVM 的项目。# a5 s; r, U( z/ ]6 s
; f+ Y  j$ p4 {# P/ R
对没有经验的人来说,EVM 似乎并没有只发送 ETH 到合约,但不潜在地执行任何合约代码的方法(这样的交易会失败/抛出异常)。但事实上,确实存在比较不为人知的强制合约地址接收 ETH 的方法。
9 q; O- }9 [1 z/ y4 s
& Z- @1 h: h7 j, }强制接收 ETH 攻击; |. B# |: h" Z3 A- d3 H
- k; |# V/ p; ?0 _
异常的余额改变并不会影响所有合约,有些时候只会导致合约中 ETH 被锁定而永远无法使用。但有些合约可能会因此完全失效,故需要加入expectedBalance 状态字段,或者添加余额异常处理方法来避免这一问题。
0 P4 J- f& f# N
. \7 N+ T% u" j% V人为例子:. _4 |  w+ a9 N! P* q: n2 u/ [
pragma solidity 0.4.18;
2 \9 w0 i0 _% A' B& {8 J8 r$ hcontract ForceEther {
% A: J0 y0 p9 x" F# [3 y bool youWin = false;9 u  o8 U, j& q9 S
function onlyNonZeroBalance() {
9 A% C. S/ M4 _$ Y     require(this.balance > 0); 8 M5 j2 H5 v  `# C& a, w0 ^! T
     youWin = true;4 u: B2 w+ I2 p  D' E) q9 ]) o* v
}
, O$ f* V1 E2 | // throw if any ether is received: ^1 M& Z$ }; v$ O) Q
function() payable {2 s$ H' a) B5 d
   revert();$ F2 y8 F5 _1 w! w9 o
}0 H  V" n3 O; J9 g- |9 k0 ?5 Y
}
/ t6 u5 Y  I: s9 Q& [  P0 m另一个看上去更真实的例子:7 w2 F* t, b. f; d: `( c
contract EtherGame {
( P& T# f1 C' T2 }   uint public payoutMileStone1 = 3 ether;
) g* s1 j* O' R* M; F   uint public mileStone1Reward = 2 ether;
6 x' y& b( z0 d   uint public payoutMileStone2 = 5 ether;
- B  T6 o" H0 J- }3 o! T   uint public mileStone2Reward = 3 ether; / o) Q) r9 R  E4 @7 t+ a
   uint public finalMileStone = 10 ether; 8 C8 `5 H7 m0 H" m
   uint public finalReward = 5 ether;
- q6 b) b+ P0 i) @2 ]8 m! |1 h   mapping(address => uint) redeemableEther;
! K& ^) {3 v; x% K: ?2 J" M/ M1 ^   // users pay 0.5 ether. At specific milestones, credit their accounts% k5 X' e& M9 u# H8 r+ a# `
   function play() public payable {+ F8 t0 i4 P3 g. @& x$ c
       require(msg.value == 0.5 ether); // each play is 0.5 ether
  n4 g9 U2 O" C! g. X. U$ m4 b       uint currentBalance = this.balance + msg.value;  `8 G8 ~9 \) O0 W; \  y1 X
       // ensure no players after the game as finished: n# w6 H5 [6 M# ~% C4 q8 o
       require(currentBalance
5 ?1 E# B' ]0 P9 V       // if at a milestone credit the players account/ y/ x, o* M; p  c+ l
       if (currentBalance == payoutMileStone1) {
9 N1 x/ n: d# L: C) Q7 w) i           redeemableEther[msg.sender] += mileStone1Reward;
, f' T5 h; c; E  ~! j       }% j* ~0 s5 v9 ]5 w$ j
       else if (currentBalance == payoutMileStone2) {
$ Z  o  f* v0 l+ U2 a5 Z  R           redeemableEther[msg.sender] += mileStone2Reward;0 N0 Z, s; T* c# S6 I; x0 C
       }
( @8 s  I8 [  `       else if (currentBalance == finalMileStone ) {
; g: T: @. q5 T4 T           redeemableEther[msg.sender] += finalReward;
+ ^+ i8 `' n9 Q- _5 J       }+ m( D9 e+ o2 |2 U
       return;
2 @+ p; C( Y# z- I   }) U0 {% q  s# ?! q* R& L
   function claimReward() public {  [0 c4 `' o/ _  B* P
       // ensure the game is complete! n4 {% F0 O- b: ?( g4 A
       require(this.balance == finalMileStone);' K* J5 Q) n* P8 J* g+ W- d+ S4 J! \
       // ensure there is a reward to give9 X" A, G# K: C6 j; D/ J: c
       require(redeemableEther[msg.sender] > 0);
" N7 W. g# ]' y8 ~5 K+ y7 v: Z: x       redeemableEther[msg.sender] = 0;
7 M7 N0 f# a. c* g       msg.sender.transfer(redeemableEther[msg.sender]);
1 e$ e7 k) U- K. e  |* f   }
: q4 a2 v1 j$ D6 |  l8 b7 d4 z5 g}
" Z- Y0 c  p" {- j) Y% r. w用户每次给合约发送 0.5 ETH,依次达到每个 milestone。攻击者可以强行将ETH发送给该合约,令其超过 finalMileStone 值,从而使合约中的所有资金都被锁定并且无法访问。 当然,这个例子也是人为设计出来的。
2 o$ S  |% {# N; K5 Z8 G/ I
1 I& h9 M5 E2 U: L* m# ]2 R不幸的是,我还无法找到任何现实生活中的例子。不过,如果有目的地把代码隐藏在Solidity合约中,这些类型的漏洞可能会非常狡猾。这种性质的攻击是Underhanded Solidity比赛的第二和第三名获胜者的获胜关键。3 E  _7 H# }% M# f3 ~% w
$ N: ^/ a! Y. D  v, w
强制发送 ETH 到合约
4 {$ e" L8 `9 }" y/ o首先介绍一下交易是怎么发生的。假设有如下代码:% S: X' @' ]! n+ l- r
someAddress.transfer(self.balance() / 10);- _/ q; R, D' e! p* d7 M7 P6 L$ c& _
这段代码有什么作用?
" e" {: l% ^  f* z/ u) l3 V4 s; ^# Q/ s( m$ [7 W/ ^4 a4 {$ L  _
someAddress 可以是非合约地址,这种情况没有合约执行,除标准交易费用外不会消耗任何 gas。这永远不会失败,但也无法保证地址是真实的和可访问的
2 x! a. v3 p" I! `' T5 usomeAddress 可以是合约地址。合约可以成功交易并消耗足够的 gas 费用。( Z4 H  B1 A* ]; B# ^- {
% F0 v) l( s; B% K
someAddress 可以是合约地址。合约交易如果失败,费用会返回,但会消费掉所有 gas,并且合约调用交易停止执行。* y2 `7 s- r  ?/ C! ]! K+ g4 @

" }0 d  ~5 o+ w$ g5 S7 T这看起来还行。如果合约不想收到 ETH,或者只想接收白名单上某些地址、合约的 ETH,那你对其发送 ETH 就会失败。
0 p% T2 t9 Y, O5 i' ?4 w: u# ^9 X
0 s; V# @$ X- E, [+ E+ H然而,若以其他方式发送 ETH 的话,这种安全特性会被完全击破:合约可以在被创建时接收 ETH。然后,合约可以自毁,把它所有 ETH 发送给一个易受攻击的合约,而不用调用任何目标合约的代码。
! \. Y% t4 u$ P0 W$ d! }; s! ^, S9 I1 N( b+ {
如果发送者和当前易受攻击的合约能在部署前被提前预测到,可以提前把 ETH 发送到那个地址。矿工/区块创建者能直接将区块奖励的 ETH 发送给任何合约,而不需要执行任何代码。
. @' {% _% I7 J  }; s, n4 Q, N& I  R( C
  Z& L2 I% D1 h) \9 t这些虽然都不是常规用法,但它们确实提供了一种方法来将 ETH 发送到合约并完全绕过任何阻止这一行为的代码。, P( D5 Q8 ^# t/ b# V7 Q3 n; `

& L* x$ o6 p( f% ?, _6 [7 M在没有特殊权限的情况下,能利用起来的唯一方法就是自毁。这个操作只需要你用点 ETH 来创建合约,以及支付额外的 gas 费用。' r9 G" Q1 h# @4 u4 X
: S" v: e9 Z8 C
另一个有趣的方法是在创建合约之前将ETH发送给合约。这样恶意开发者故意在合约代码里放漏洞。这类和合约余额有关的 bug 很难被发现,至少我已知的审核软件都没能明确指出错误,比如require(balance == whatever)。3 R. S4 v2 H, M2 R
可以用下面这种代码阻止漏洞:/ J- P. U& K- `' N) r& a7 f6 j
uint256 expectedBalance = 0;
, }+ Q; S2 d+ b4 |) Dfunction () public payable {" B2 D$ v/ B$ G+ O- o# P7 I0 K
   expectedBalance+=msg.value;
1 w$ ?2 u* x6 u: h}* y4 U# R% G$ V7 N- d
$ r! c$ N$ Z% Q5 B) q7 z- a
现在我们只用.transfer() 给它发送一些ETH7 E0 b, n. Q4 p% o9 H
VM Exception while processing transaction: out of gas$ o+ j2 Y; `' F/ ?7 s; j" o, s
因为我们现在必须读写一个状态变量,这要昂贵得多。默认的 gas 费用是2300 gas。读取一个状态变量需要200 gas,并且写一个已经存在的变量需要 5000 gas(如果之前不存在则需要20,000 gas)! ?5 n# m5 _; I2 X
所以,需要用下面代码:0 r! ~/ \" F, Q8 Y1 ^- D
someAddress.transfer.value(whatever).gas(7000); //just a guess
5 U% z/ a  Y$ G' H1 |大多数合约在交易时使用默认gas,但这里不是默认情况...不过,仔细考虑这个问题,既然我们需要更多 gas,我们可以写个更有趣的 payable 函数:
0 [  d% n. t+ c4 \. g1 A( pfunction () public payable{
! \2 x  \4 C5 {( E- E   sender.call(....);
/ s) r9 n, k8 i6 [; O}
9 Q* m$ X  _' }) a. z所以现在为了处理所调用合约的异常的余额改变问题,我们将合约暴露到一个全新形式的攻击下。默认值为2300只能调用一个非常小的外部函数,它不进行状态修改操作,基本上只能生成一个LOG。# s0 r5 `# @. U/ q& {/ ]
$ O- X7 o/ t7 s3 s: }% U6 Y
既然我们已经增加 gas 使得合约可以读取和存储状态,那么这个合约现在也有足够的 gas 来调用之前调用它的合约,于是就可能导致重入攻击(reentrancy attack)。我不会详细介绍所有细节,但基本上它需要一个非常谨慎的合约设计来防止这个“功能”的 bug,并且唯一100%处理掉它的方法是 gas 费用低到不能进行状态修改。没有100%的方法知道当前的合约执行是不是可重入的,EVM 并未公开这类信息。5 e. r: C! _( ~$ e5 a2 r  v
; o, `. V- r3 n0 F* `) j- `
总结: O, v+ S2 h5 C; M6 [
对我来说,这部分 EVM 设计毫无意义。正常接受资金需要获得批准,这似乎被说成是一种安全功能,同时却有清晰且有记录的方法来绕过它,所以它没有提供任何安全性方面的价值。
) k( b& i# R0 R
0 Z! c3 u' h0 d1 p; D: {/ Y" G9 ]此外,如果你想减少交易费用,那么这些方法在很大程度上是你无法访问的,它们只对攻击者有用。理论上,如果你只想将代币只发送到合约,而不关心合约的执行,gas 成本应该便宜33%。# {( O6 c* F+ J
: ?7 d- Q% B  o# k4 z, Y$ q& w
为什么EVM没有暴露这样类似的方法呢?从历史上看,这种意外行为导致了许多合约DoS攻击和漏洞。幸运的是,Solidity让这些类型错误更难以出现。但显然它不能完全解决安全方面的问题,更不用说经济方面了。
0 `% r1 F8 [# o' ]: f
9 c/ M8 M% w+ t7 D4 q- H, [/ h如果希望跟踪合约中确切的预期余额,那么需要支付比平常更多的 gas 并使用非标准接口,这实际上会带来更多的风险,比如重入攻击。
5 m- A3 f( c) P" O$ M- @, z# @; A, o, U2 D
我们可能永远不会知道为什么EVM这样设计,但这一问题将在 Qtum x86VM 中得到解决。将来会有一种在不执行合约的情况下向合约发送 QTUM 的方法,而且它比实际执行合约更加便宜。
BitMere.com 比特池塘系信息发布平台,比特池塘仅提供信息存储空间服务。
声明:该文观点仅代表作者本人,本文不代表比特池塘立场,且不构成建议,请谨慎对待。
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

成为第一个吐槽的人

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

    0

  • 关注

    0

  • 主题

    3