Hi 游客

更多精彩,请登录!

比特池塘 区块链技术 正文

以太坊智能合约潜在风险

吃瓜围观小分队
98 0 0
最近作者一直在思考 EVM 和 Solidity 对不可支付合约(non-payable contracts)的错误概念,以及其可能蕴含的针对智能合约的攻击(attack vectors),该类型攻击在现有 EVM 中几乎不可修复。EVM 是以太坊、Qtum 量子链和其他一些区块链合约运行的基础。基本上写 Solidity 代码就等同于使用 EVM。这篇文章只以以太坊(ETH) 为例,不过该问题同样适用于用 QTUM 等所有兼容 EVM 的项目。
1 ]2 w# n) k5 y# {) B
0 X# @  I2 Z; L  m3 Z1 l$ `7 |对没有经验的人来说,EVM 似乎并没有只发送 ETH 到合约,但不潜在地执行任何合约代码的方法(这样的交易会失败/抛出异常)。但事实上,确实存在比较不为人知的强制合约地址接收 ETH 的方法。
' ^) c! J  U# ~
4 t) m2 J6 b  s1 @% S7 q& x' r) v强制接收 ETH 攻击
: R6 D& f* H% s, A2 ]6 t2 ?
+ m7 {* E& d. f+ [1 w2 P异常的余额改变并不会影响所有合约,有些时候只会导致合约中 ETH 被锁定而永远无法使用。但有些合约可能会因此完全失效,故需要加入expectedBalance 状态字段,或者添加余额异常处理方法来避免这一问题。( ]& X( Y$ O. z
# ]7 K# I( k. @( K
人为例子:
- ]3 R2 n5 S' {5 kpragma solidity 0.4.18;. S; O$ p$ z3 v
contract ForceEther {/ ]- |( `* Y  w
bool youWin = false;+ R) D' v) G. o+ q% G! F
function onlyNonZeroBalance() {
3 n6 M, [* O1 q* i. E9 z     require(this.balance > 0);
/ Q; p8 v/ q) \# g- p  C5 U     youWin = true;1 O" @) E& x  p6 r
}+ m4 x6 F! ?# {
// throw if any ether is received
1 P) x2 x" C. k" l2 B: u" R+ j' ~ function() payable {
1 b3 H7 ?& q* i! G6 n) z  L   revert();
% T$ M4 F* m6 [* Q; P6 o }! U5 C, ]' I5 J* M8 O2 o5 l
}9 A: e! b2 S0 M6 |% A6 \
另一个看上去更真实的例子:5 I, e. I: M3 s$ _; Z& u
contract EtherGame {
6 e# I( ~5 i& [+ W, r* r2 s6 i   uint public payoutMileStone1 = 3 ether;
* ]% I  Q) J' ~) V5 ~) N   uint public mileStone1Reward = 2 ether;; T: }9 J: H& ~/ h
   uint public payoutMileStone2 = 5 ether;% ]2 F5 A% F* J& @
   uint public mileStone2Reward = 3 ether;
# A8 Y' O2 U/ J: g# q* g$ P   uint public finalMileStone = 10 ether;
' D& a0 v, W. C! Y   uint public finalReward = 5 ether; / v# }! `0 _- E  {# F6 Y4 |% A  |
   mapping(address => uint) redeemableEther;' g( I8 `9 l" F/ v
   // users pay 0.5 ether. At specific milestones, credit their accounts
# v/ w8 L! ^. ?, a   function play() public payable {  {' `  i, l% i
       require(msg.value == 0.5 ether); // each play is 0.5 ether
3 T) z, Z- J6 |! y) i8 R( }3 B: ~       uint currentBalance = this.balance + msg.value;8 e, n5 \# H  ]* ]" Q
       // ensure no players after the game as finished
, y: {5 j6 R. Y9 @: i       require(currentBalance
2 |2 \: T( I; G" V# r/ H6 ?       // if at a milestone credit the players account
  S% {5 ?3 e9 u  ?1 b3 F& @  D       if (currentBalance == payoutMileStone1) {9 [# _2 _1 b3 N! ?! ?$ Q0 j, u
           redeemableEther[msg.sender] += mileStone1Reward;# @4 @9 ^% u, H( g' e1 |
       }
. @6 c2 T* [. [4 r& O       else if (currentBalance == payoutMileStone2) {8 ~3 \5 y3 x6 a: a5 a4 U4 `
           redeemableEther[msg.sender] += mileStone2Reward;
$ Q+ O1 F: M. u4 k       }
) l  |4 S6 Y# g- u       else if (currentBalance == finalMileStone ) {
0 X& O7 e2 h( f" R& _* ]" t           redeemableEther[msg.sender] += finalReward;
. J# D6 C: e- E) E7 p2 t       }
7 o* O+ f2 P2 T  u3 e* Y8 p       return;' ~! h% l: L; M6 T! x4 i/ p% q% Z
   }: x; _) E! d" w+ T5 `
   function claimReward() public {6 k2 B( j8 Y0 m# l- s
       // ensure the game is complete
5 e1 ]) h- `/ L1 b  f& K       require(this.balance == finalMileStone);
1 y# p& h  Y1 f       // ensure there is a reward to give" ?0 l7 S/ X) d
       require(redeemableEther[msg.sender] > 0);
* Q2 _# t. K' ^& n* [" D       redeemableEther[msg.sender] = 0;. B, t- p) X5 l" c7 _5 w
       msg.sender.transfer(redeemableEther[msg.sender]);
. P" m' l9 E+ g# c! m2 y   }
" k! v7 P: p' C& n4 d% p# a4 r}( N& F& X2 a1 }! k" m& H
用户每次给合约发送 0.5 ETH,依次达到每个 milestone。攻击者可以强行将ETH发送给该合约,令其超过 finalMileStone 值,从而使合约中的所有资金都被锁定并且无法访问。 当然,这个例子也是人为设计出来的。0 r5 [% [4 d# y9 w0 I/ g6 N- t" }
0 L( |/ Y) g! L/ N4 P
不幸的是,我还无法找到任何现实生活中的例子。不过,如果有目的地把代码隐藏在Solidity合约中,这些类型的漏洞可能会非常狡猾。这种性质的攻击是Underhanded Solidity比赛的第二和第三名获胜者的获胜关键。
1 H* c5 y  b! {6 Q: E6 I
' y1 S  i; A$ x. G5 X. I# Y强制发送 ETH 到合约
/ D/ J8 Q, Q+ J' e- v2 \, s  f2 O首先介绍一下交易是怎么发生的。假设有如下代码:
: {3 v5 j9 q" `someAddress.transfer(self.balance() / 10);
# V7 o0 L* {# f/ x) R; i% ?# u这段代码有什么作用?
+ ~/ ~2 {9 U/ m3 X+ n: e# n! R1 b* w3 e2 C/ c- y/ S7 p
someAddress 可以是非合约地址,这种情况没有合约执行,除标准交易费用外不会消耗任何 gas。这永远不会失败,但也无法保证地址是真实的和可访问的
6 ]. I, B+ \" A: N% d( M0 asomeAddress 可以是合约地址。合约可以成功交易并消耗足够的 gas 费用。
6 `# {6 P2 Y- \" a$ o. {# v# k9 N* S  e9 p5 T- z; F
someAddress 可以是合约地址。合约交易如果失败,费用会返回,但会消费掉所有 gas,并且合约调用交易停止执行。
5 b  g# I& P8 L/ d. V! ^; c! \
' q$ ?; ~0 `' X8 z. ~: |这看起来还行。如果合约不想收到 ETH,或者只想接收白名单上某些地址、合约的 ETH,那你对其发送 ETH 就会失败。, [+ T2 c) Z4 W2 D! ?7 p' j

; `( X1 t" y+ J然而,若以其他方式发送 ETH 的话,这种安全特性会被完全击破:合约可以在被创建时接收 ETH。然后,合约可以自毁,把它所有 ETH 发送给一个易受攻击的合约,而不用调用任何目标合约的代码。
. j" u2 \- ~# Q+ J$ O1 A# r; m) ]3 ]6 |" M
如果发送者和当前易受攻击的合约能在部署前被提前预测到,可以提前把 ETH 发送到那个地址。矿工/区块创建者能直接将区块奖励的 ETH 发送给任何合约,而不需要执行任何代码。; T4 s7 l6 k- Z
! H9 \/ ?6 F! c7 b+ v% o2 y! M. C
这些虽然都不是常规用法,但它们确实提供了一种方法来将 ETH 发送到合约并完全绕过任何阻止这一行为的代码。9 x  G( N! s$ X7 X/ M0 B0 p
( o' v/ d% B8 Y$ i+ X/ ?
在没有特殊权限的情况下,能利用起来的唯一方法就是自毁。这个操作只需要你用点 ETH 来创建合约,以及支付额外的 gas 费用。* P! `' o& j( i9 U- q8 X2 [

& N: ~1 w9 ~# j; S" X, `另一个有趣的方法是在创建合约之前将ETH发送给合约。这样恶意开发者故意在合约代码里放漏洞。这类和合约余额有关的 bug 很难被发现,至少我已知的审核软件都没能明确指出错误,比如require(balance == whatever)。* E* P( A  J7 S6 o+ n
可以用下面这种代码阻止漏洞:
$ A5 q7 ]" @. L, J- C2 f, F& b( ~& kuint256 expectedBalance = 0;( j' J! J& E  B* H
function () public payable {
# I- y7 |: U4 B   expectedBalance+=msg.value;4 p! [1 Y8 m. h; d8 Z' l' V
}
6 m: i* V" Y/ N3 }' g# M: `; K* y. q" q  `
现在我们只用.transfer() 给它发送一些ETH
; ^# _/ T4 R/ tVM Exception while processing transaction: out of gas8 B5 u" ~. U. n. A4 _) P' r9 B
因为我们现在必须读写一个状态变量,这要昂贵得多。默认的 gas 费用是2300 gas。读取一个状态变量需要200 gas,并且写一个已经存在的变量需要 5000 gas(如果之前不存在则需要20,000 gas)# x  J. b  E& U# N3 L: y, ?
所以,需要用下面代码:. B) h# ^3 {. ~* I9 F/ l$ C
someAddress.transfer.value(whatever).gas(7000); //just a guess+ }+ ]2 q  N& ^/ j! M
大多数合约在交易时使用默认gas,但这里不是默认情况...不过,仔细考虑这个问题,既然我们需要更多 gas,我们可以写个更有趣的 payable 函数:
5 t8 s0 b0 e/ [/ f9 O: `function () public payable{
# Y0 a& C+ _+ x7 }   sender.call(....);
' g  j* q  @  x# ~}
( n+ r4 G: ?* \4 {7 f, o所以现在为了处理所调用合约的异常的余额改变问题,我们将合约暴露到一个全新形式的攻击下。默认值为2300只能调用一个非常小的外部函数,它不进行状态修改操作,基本上只能生成一个LOG。
- c. j0 G8 N+ e$ h) T' t
$ m3 |3 l. _8 d" P7 w& H既然我们已经增加 gas 使得合约可以读取和存储状态,那么这个合约现在也有足够的 gas 来调用之前调用它的合约,于是就可能导致重入攻击(reentrancy attack)。我不会详细介绍所有细节,但基本上它需要一个非常谨慎的合约设计来防止这个“功能”的 bug,并且唯一100%处理掉它的方法是 gas 费用低到不能进行状态修改。没有100%的方法知道当前的合约执行是不是可重入的,EVM 并未公开这类信息。' E2 ?1 E+ |: f

* w% E3 W; d% b, f7 s2 v' b! ~总结& w: F$ |) V5 n7 @$ P3 U
对我来说,这部分 EVM 设计毫无意义。正常接受资金需要获得批准,这似乎被说成是一种安全功能,同时却有清晰且有记录的方法来绕过它,所以它没有提供任何安全性方面的价值。- X' _0 j3 I( \! [- S" c0 i

0 @: J) H2 I0 c! j1 A$ h此外,如果你想减少交易费用,那么这些方法在很大程度上是你无法访问的,它们只对攻击者有用。理论上,如果你只想将代币只发送到合约,而不关心合约的执行,gas 成本应该便宜33%。
: T1 c# V' P9 F8 a! ?4 r. Y( L3 L6 \5 }# E3 n
为什么EVM没有暴露这样类似的方法呢?从历史上看,这种意外行为导致了许多合约DoS攻击和漏洞。幸运的是,Solidity让这些类型错误更难以出现。但显然它不能完全解决安全方面的问题,更不用说经济方面了。7 L. _- z( g3 P1 q8 G, e+ O

% q0 n* c: A! Q: n. S6 X如果希望跟踪合约中确切的预期余额,那么需要支付比平常更多的 gas 并使用非标准接口,这实际上会带来更多的风险,比如重入攻击。
' a4 L. q' g( S* l5 ~' F+ h5 C+ P+ F
我们可能永远不会知道为什么EVM这样设计,但这一问题将在 Qtum x86VM 中得到解决。将来会有一种在不执行合约的情况下向合约发送 QTUM 的方法,而且它比实际执行合约更加便宜。
BitMere.com 比特池塘系信息发布平台,比特池塘仅提供信息存储空间服务。
声明:该文观点仅代表作者本人,本文不代表比特池塘立场,且不构成建议,请谨慎对待。
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

成为第一个吐槽的人

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

    0

  • 关注

    0

  • 主题

    3