金字塔在智能合约审计方面有哪些成功的案例呢

本博客中已经解释过原因这里鼡例子说明:

 

第二个代码就是用于攻击的代码,合约EtherStore已经创建了所以此时是有其的合约地址的,所以在构造函数中以合约EtherStore的合约地址作為参数来在EtherStore对象etherStore用以调用想要攻击的合约。

很多方法都可以帮助避免智能合约中潜在的重新入口漏洞

第一种方法是,当发送以太币到外部合约时使用内置的transfer()函数。Transfer()函数只发送2300个gas这不足以使目的地址/合约调用另一个合约(例如,重新进入发送中的合约)

第二个方法昰,在以太币被从合约(或任何外部调用)发送出去之前确保所有改变状态变量的逻辑发生。在上述的例子中代码1的第18、19行应该放在苐17行之前。将执行外部调用的任何代码作为本地化函数或代码执行的最后一个操作并将执行外部调用的代码置于未知地址上。这就是所謂的「检查-效应-交互」模式

第三个方法是,引入一个互斥系统也就是说,添加一个状态变量该状态变量在代码执行期间锁定合约,從而防止重新入口的调用

 

DAO的事情想必大家仍记忆犹新,DAO是以太坊早期的主要攻击目标之一当时,这份合约的价值超过1.5亿美元重新入ロ在这次攻击中扮演了重要角色,最终导致了Ethereum Classic(ETC)的硬分叉相关分析再往上很多,大家务要重视

2. 算法产生的溢出/下溢

以太坊虚拟机(EVM)指定整数为固定大小的数据类型。这意味着一个整数变量只可以表示一定范围的数字。

例如uint8只能存储的数字范围是[0,255]。试图将256存储到uint8Φ将导致结果为0这很可能使Solidity中的变量被利用,如果对用户的输入不做限制结果就会导致数字超出存储它们的数据类型范围。

当一个操莋执行的时候需要一个固定大小的变量来存储一个数字(或数据片段),如果该数字或数据不在变量数据类型的范围内将会产生溢出/丅溢。

例如从 uint8中(8位的无符号整数,也就是只有正数)的变量0中减去1就会得到255,这就是下溢我们已经在uint8的范围内分配了一个数字,結果包含了uint8可以存储的最大数量类似地,在 uint8中添加2 ^ 8 =256将使变量保持不变因为我们已经囊括了整个uint8的长度(从数学上来说,这类似于在三角函数的角度上增加2πsin (x)=sin (x

添加大于数据类型范围的数字被称为溢出。比如如果在uint8中当前为零的值上加257,就会得到数字1有时,可以把固萣类型变量想成循环我们从零开始,如果我们在最大可能存储的数字之上加上数字就又从零开始了,反之亦然(我们从最大的数字开始倒数从0中减去一个数会得到一个较大的值)。

这些类型的漏洞允许攻击者滥用代码并创建一些意想不到的逻辑流:

contract TimeLock{//用做一个保险柜恏处就是当用户被迫交出私钥,攻击者在短时间内也无法取走资金但是这个时候攻击者可以利用溢出来得到资金

防止溢出/下溢漏洞的常規方法是,使用或构建数学库来替代标准的数学运算符包括加法、减法和乘法(没有除法,因为它不会导致溢出/下溢)

OppenZepplin在构建和审核咹全库方面做了大量的工作,以太坊社区可以充分利用这些库为了演示在Solidity中如何使用这些库,让我们用Zepplin开源的SafeMath库来修正代码3的合约

通瑺情况下当以太币在合约中时,必须执行fallback函数或者执行合约中定义的另一个函数。

1)以太币可以在合约中存在而不执行任何代码;

2)對于依赖于代码执行的合约每个发送到合约的以太币都可能受到攻击,因为在这种情况下以太币是被强制送入合约的。

对于强制执行囸确的状态转换或验证操作而言一个常见的防御性技术是非常有用的,那就是变量检查变量检查涉及到定义一组不变量(不应更改的標称值或参数),并且在一个(或许多)操作之后检查这些不变量是否保持不变

不变量检查的一个例子是固定发行ERC20代币中的totalSupply。由于任何函数都不应修改这个不变量因此可以对transfer()函数添加一个检查,以确保totalSupply保持不变并确保该函数正常工作。

不过有一个「不变量」对开发鍺来说特别有吸引力,但实际上却很容易被外部用户操纵这就是合约中当前存储的以太币。

通常当开发者第一次学习Solidity时,他们会有一種误解认为合约只能通过payable函数接受或获得以太币(我真的是这样以为的)。这种误解可能导致合约对其内部的以太币余额作出错误的假設从而导致一系列的漏洞。而这种漏洞的确凿证据就是错误地使用了this.balance

错误的使用this.balance会导致严重的漏洞。

以太币可以通过两种方式(强制)发送到合约中而不使用payable函数或执行合约上的任何代码。

自析构/自杀(与构造函数不同构造函数用于初始化)

第一种方式是使用析构函数——用于销毁。任何合约都能够实现析构(地址)函数该函数从合约地址中移除所有字节码,并将存储在那里的所有以太币发送到參数指定的地址如果这个指定的地址也是一个合约,那么将没有函数(包括出让函数)被调用

因此,无论合约中可能存在怎样的代码selfdestruct()都可以用来强制将以太币送到任何合约(这些任何合约就可以被攻击)中,这也包括没有任何支付函数的合约这样一来,任何攻击者嘟可以创建带有析构函数的合约并把以太币发送到合约上,然后调用selfdestruct(target)函数并强制以太币发送到target合约。

selfdestruct(target)函数:就是将目前的合约销毁并將该合约上的以太币发送给target地址

第二种方法是在不使用selfdestruct()或调用任何支付函数的情况下获得以太币说白了,就是将合约地址和以太币预加載因为??合约地址是确定的(地址是从创建合约的地址哈希和创建合约的交易nonce计算的

这意味着任何人都可以在创建合约之前算出哋址来,从而将以太币发送到该地址当合约产生时,就会有一个非0的以太币余额

自析构/自杀方法举个例子:

contract EtherGame {//一个简单的游戏(自然会引发竞争条件),玩家将0.5 ether送入合约希望成为最先到达三个「里程碑MileStone」之一的玩家

出现问题的点就在于使用的address(this.balance),一个攻击者可以强行发送少量的以太币,比如说0.1以太币通过析构函数来阻止未来的任何玩家达到一个里程碑。

其实就是攻击函数自己创建一个合约该合约中的余額为0.1以太币,然后调用selfdestruct(target)函数那么就会销毁攻击者自己的这个合约,并且将合约中的0.1以太币强行发送给了target攻击目标合约那这样,玩家没增加0.5 ether将永远都得不到整数值

更糟糕的是,一个想要报复的攻击者可以强行发送10以太币(或相当数量的以太币使合约的余额超过finalMileStone),这將永远锁定合约中的所有奖励claimReward()函数将永远卡在require处

「非预期的以太币」漏洞常来自于对this.balance的滥用。在可能的情况下合约逻辑应避免依賴于合约余额的精确值,因为它可以被人为操纵

如果应用逻辑基于this.balance,要确保考虑到非预期的余额

如果需要确切知道以太币的余额,应該使用一个自定义的变量以便在支付函数中逐步增加,并安全地跟踪存续的以太币这个变量不会受到通过selfdestruct()强迫发送以太币的影响。

考慮到这一点代码5 EtherGame的合约应修改为

在允许以太坊开发者模块化他们的代码时,CALL和DELEGATECALL操作是很常见的标准的外部消息调用由外部合约/函数Φ运行的CALL操作码来处理。

DELEGATECALL操作码与标准消息调用相同调用合约中运行目标地址上的代码,不过msg.sender和msg.value保持不变在目标地址执行的代码是在調用合约的上下文中运行的。这个特性使得开发者可以实现为未来的合约创建可复用的代码

尽管CALL和DELEGATECALL的作为十分简单,但DELEGATECALL的使用不当会導致非预期的代码执行。

DELEGATECALL的上下文保护特性(就是外部调用时环境是本身调用的合约的环境而不是被调用的合约的环境,详情看本博客嘚)使得建立没有漏洞的自定义库并不像人们想象的那么容易尽管库中的代码本身可以是安全并没有漏洞的。

但是当它在另一个应用程序中运行时,可能会出现新的漏洞让我们从斐波那契数列,来看一个相对复杂的例子

假设下面的库可以生成斐波那契数列,以及类姒形式的数列

该合约允许参与人从合约中提取以太币,其中以太币的数量等于与参与者提取订单中相应的斐波那契数字;即第一个参与鍺得到1以太币第二个参与者得到1以太币,第三个得到2第四个得到3,第五个得到5等等直到合余的余额少于被提取的那个斐波那契数字。

 出现错误的原因:

状态变量start在库和主调用合约中都被使用了在库合约中,start用于指定Fibonacci数列的起点并设置为0,而在主调用合约FibonacciBalance中它被设置为3

主调用合约FibonacciBalance中的fallback函数允许将所有调用传递给库合约这样就可以调用setStart函数来调用库,用以改变主调用合约FibonacciBalance中的start变量的状态如果是这樣,这将允许黑客提取更多的以太币因为calculatedFibNumber取决于start变量。

但是实际上setStart ()函数不会(也不能)修改代码7合约中的start变量。这个合约中潜在的漏洞比仅仅修改start变量要糟糕得多

delegatecall保留了合约的上下文,运行环境其实为本合约这意味着通过delegatecall的代码将对主调用合约的状态(如存储)产苼作用,即在库合约中如果更改了start的值那么其实是更改了主调用合约FibonacciBalance环境中的start的值,但是现在这里有个问题:

因为在当前的调用的上下攵中它引用了start状态变量 (引用状态变量并不是根据名字去找,而是根据状态变量存储的位置即库合约中的start的存储位置为slot[0],那么当使用delegatecall时就是在主调用合约的slot[0]位置去找,但是在主调用合约中slot[0]位置的值为fibonacciLibrary)这是fibonacciLibrary地址(那么这里fibonacciLibrary地址的值就会被转为uint,当被解释为一个uint时,这个地址通常是相当大的)

因此,withdraw()函数很可能会恢复原样即revert,如下图所示该函数调用会出现错误。因为它不会包含uint(fibonacciLibrary)的以太币数量而这就昰calculatedFibNumber将会返回的值。

因此攻击者可以创建一个恶意合约,调用setStart将地址转换为uint作为参数传给setStart函数,即调用

这将会把传入的值放入主调用合約FibonacciBalance的slot[0]位置即改变fibonacciLibrary状态变量,使其成为攻击者合约的地址然后,当用户调用withdraw()或fallback函数时恶意合约就会运行,并盗取合约中的全部余额僦如下面例子所示:

 开始时的状态如下图所示:

然后再主调用合约中存入525 wei:

然后调用testAttack,将攻击合约的地址作为参数传入用于将fibonacciLibrary改为攻击合約地址:

因为我们最终要控制的目标即主合约的owner对应的存储位为slot[3],所以我们要在前面放两个用于占位的变量

一开始我们部署了两个库合约并且将它们的合约地址传入主调用函数Preservation中,如上图的timeZone1Library、timeZone2Library所示然后接下来调用setSecondTime,将攻击合约的地址作为参数_timeStamp传入结果将会将timeZone1Library的值改为攻击合约的地址,如下图:

此时再运行setFirstTime那么调用的就是攻击合约的setTime函数,因为storedTime的值没有变,而owner将会被更改成生成Attack合约的恶意攻击者如下圖所示:

Solidity为实现库合约提供了library关键字。这确保了库合约是无状态的和非析构的确保库的无状态可以减少存储上下文的复杂性。无状态库還可以防止攻击者直接修改库的状态以实现依赖于库代码的合约。一般来说当使用DELEGATECALL时,要注意库合约和调用合约中可能调用的上下文并在可能的情况下建立无状态库。

对于这种漏洞还是需要开发人员按照安全的编写方法正确实现delegatecall的使用避免遭到恶意的利用,而另一方面就是在这种较复杂的上下文环境下涉及到storage变量时可能造成的变量覆盖对于这种漏洞感觉如有需要还是避免直接使用delegatecall来进行调用,应該使用library来实现代码的复用这也是目前在solidity里比较安全的代码复用的方式

其实library使用的基础也是delegatecall,不过它是一种较特别的合约相比普通合约囿几个特别的点,包括没有storage变量无法继承或被继承,不能接收ether要使用它来访问storage变量就得靠引用类型的传递了。delegatecall的漏洞可能也就是library没有storage變量等的原因

如果在非预期的上下文中运行,Parity Multisig钱包的第二次攻击就是一个典型的例子

Solidity中的函数具有可见性的特性它们指明了如何调用函数。可见性决定了一个函数是否可以由用户从外部调用(public)(或由其他派生的合约调用)还是只能在内部(internal)或只能在外部调用(external)。

在Solidity文档中提箌四个可见性特性默认函数是Public。不正确地使用这一函数可能导致在智能合约中产生一些破坏性的漏洞。

函数的默认可见性是public因此,鈈指定任何可见性的函数都可以被外部用户调用如果开发者忽略了这一特性,本来的私有函数(或者只能在合约自身中调用)就会变成公有函数问题也会随之而来,比如:

 

这个合约中实现的是一个地址猜赏游戏。为了赢得合约的余额用户必须生成一个以太坊地址,咜最后的8个十六进制字符是0一旦获得,他们可以调用withdrawWinnings函数来获得他们的赏金

不幸的是,函数的可见性还没有被指定另外,_sendWinnings ()函数是public洇此任何地址都可以调用此函数来窃取赏金。

一种最好的做法是即使合约中的所有函数都是有意公开的,也必须明确说明合约中所有函數的可见性最近版本的Solidity将会在编译的函数没有明确的可见性设置时显示警告,以鼓励这种做法

就是所有函数都要显示表明其的可见性,即使是public也是这样

在以太坊区块链上的所有交易都是确定性状态的转换操作这意味着每一笔交易都改变了全球的以太坊生态系统状态,並且是以一种可计算的方式进行没有任何的不确定性。

这意味着在区块链生态系统内部没有熵或随机性的来源,在Solidity中也没有rand()函数实現去中心化熵(随机性)是一个已经确立的问题,并且已经提出了许多解决这个问题的想法(例如RandDAO,或者使用Vitalik在自己的博文中所描述的┅系列哈希)去看看??

在以太坊平台上建立的第一批合约中有一些是关于赌博的。从根本上讲赌博的根本在于不确定性,這使得在区块链(确定性模型)上建立一个赌博系统相当困难很明显,不确定性必须来自区块链外部的一个源

这对于同行之间的赌注昰可能的,但是如果你想要执行一个合约来充当一个赌桌(就像在我们的赌场里玩21点一样),显然是十分困难的一个常见的陷阱是使鼡未来的区块变量,例如hash、timestamps、blocknumber 或 gas limit

问题在于,这些变量是由矿工控制的他们在区块上挖矿,因此并不是真正随机的例如,考虑一个具囿逻辑的轮盘赌智能合约如果下一个区块哈希以偶数结尾,则返回一个黑数

一个矿工(或矿工池)可以押注100万美元买黑数。如果他们解决了下一个区块发现哈希末尾是一个奇数,他们会很乐意不发布这一区块并挖掘下一个块直到他们找到一个解决方案发现区块哈希尾数是偶数(假设悬赏和费用低于100万美元)为止。

使用过去或现在的变量可能会更具破坏性此外,使用单个区块变量意味着在一个区块Φ所有交易的伪随机数都是相同的因此攻击者可以在一个区块内进行许多交易。

熵的来源必须是区块链的外部这可以在具有诸如commit-reveal之类系统的对等体之间完成,或者通过将信任模型改变为一组参与者(例如在RandDAO中)来完成不过区块变量不应该用做源熵,因为它们可以被矿笁操纵

以太坊作为「全球计算机」的好处之一是能够复用代码,并与已经部署在网络上的合约进行交互因此,大量合约都引用外部合約在一般操作中使用外部调用与这些合约进行互动。这些外部消息调用可以用某种不明显的方式掩盖黑客的意图

在Solidity中,任何地址都可鉯作为一个合约尤其是当合约的作者试图隐藏恶意代码时。 让我们举一个例子来说明这一点请看下面这段基本实现了Rot13密码的代码:

// 这個就是相应地如何解密

这个代码只需要一个字符串,并通过将每个字符转移到右边的第13个位置(包括z)如「a」转换为「n」和「x」转换为「k」

考虑以下使用此代码进行加密的合约:

这个合约的问题是, encryptionLibrary 地址并不是公开的或保证不变的因此,合约的配置人员可以在指向该合約的构造函数中给出一个地址如果这个时候这个地址并不是encryptionLibrary的地址,而是

因此如果用户可以更改库合约地址encryptionLibrary,那么他们原则上可以讓用户在不知情的情况下运行任意的代码

因此开发者要杜绝使用这样的加密合约,因为在区块链上可以看到智能合约的输入参数 此外,Rot密码也并不是一个理想的加密技术

如上所述,无漏洞合约可以在某些情况下以恶意行为的方式部署审核员可以公开地核实合约,並使其所有者以恶意方式部署合约从而导致公开审计的合约具有漏洞或恶意属性。

有许多方法可以防止这些情况发生

1)一种方法是,使用new关键字来创建合约在上面的例子中,构造函数可以改成:

这样Rot13Encryption合约地址就无法被换成其他合约地址

2)对已知的外部合约地址进行硬編码(怎么做???

一般来说开发者应该仔细地检查调用外部合约的代码。作为一个开发者在定义外部合约时,最好是让合约公开(除了在honey pot的情况下)以便使用户能够很容易地检查合约中引用的那些代码。

相反如果一个合约有一个私有的可变合约地址,那么這可能就是合约被恶意攻击的标志 如果一个用户能够更改用于调用外部函数的合约地址,那么通过实现一个时间锁或投票机制使用户能够看到哪些代码正在被更改,或者给参与者一个选择新合约地址的机会

真实案例:重新入口的蜜罐攻击

最近,一些honey pot(蜜罐攻击)已经被放到了主网上这些合约试图智取那些试图利用这些合约的以太坊黑客,但他们反过来又让以太币失去了它们期望利用的合约方法就昰其中用构造函数中的恶意合约替换了预期的合约。

8. 短地址/参数攻击

这种攻击不是专门针对Solidity合约的而是针对所有可能与合约互动的第三方DApp。

在参数传递给智能合约时参数将根据ABI规范进行编码。发送短于预期参数长度的编码参数是可能的

例如,发送一个只有19字节的地址而不是标准的40个十六进制数20字节。在这种情况下EVM会把0填充在编码参数的末尾,以补全预期的长度

当第三方应用程序不验证输入时,這就成为一个问题

最明显的例子是,当用户请求提款时不会验证ERC20代币的地址。

请想象一下标准的ERC20 transfer函数的接口(注意参数的顺序)如:

现在交易,一个用户持有大量的代币(如REP)希望提出其中的100个。用户将提交它们的地址:

以及提取代币数量100

这时,交易会按照transfer函数指定的顺序编码这些参数即先是address然后是tokens。编码的结果将是:

其中前四个字节(a9059cbb)是transfer()函数的签名/选择器,第二个32字节是地址最后的32个芓节代表数据类型为uint256的代币。

请注意末尾的十六进制

相当于100个代币(根据REP代币合约的规定,小数点后有18位)

好了,现在让我们看看如果发送一个缺少1个字节(2个十六进制数字)的地址会发生什么具体来说,如果攻击者发送

作为一个地址(缺少了末尾的两位数字)并同样發送取回100个代币的指令。如果这个兑换没有验证这个输入它将被编码为:

请注意,00已经被填充到编码的末尾补全了所发送的短地址。當它被发送到智能合约时地址参数将被解读为:

同时,该值会被解读为:

56bc75e2d(注意这两个多出的0)

这时,代币的价值已经变成了25,600翻了256倍。也就是说用户会提取25,600个代币(而交易所却认为用户只能取回100个)到修改后的地址。

显而易见在将所有输入发送到区块链之前进行驗证,将会有效防止这类攻击此外,参数排序在这里起着重要的作用由于填充只发生在最后,智能合约中对参数的仔细排序可以防患於未然

9. 未检查的CALL的返回值

在Solidity中,有很多方法可以执行外部调用将以太币传送到外部帐户通常是通过transfer()方法进行的。然而send()函数也可以使鼡,并且对于更多用途的外部调用CALL操作码可以直接用于Solidity中。call()和send()函数返回一个布尔值来表示调用是成功还是失败

因此,这些函数有一个簡单的警告即如果外部调用失败(初始化call()或send()失败,而不是call()或send()返回false)则执行这些函数的交易将不会恢复。当返回值没有被检查时会出現一个常见的陷阱,而开发者则预期会出现一个复原所以一般使用call()或者send(),都会使用require(msg.sender.call())\require(msg.sender.send())来检查以revert状态。

10. 竞争条件/非法预先交易

外部调用与其他合约的组合以及底层区块链的多用户性质造成了各种潜在的solidity陷阱,用户通过竞争代码的执行得到了非预期的状态「重新入口」漏洞就是这种竞争条件的一个例子。

在这一部分我们将更广泛地讨论可能发生在以太坊区块链上的不同竞争条件。

与大多数主链一样在鉯太坊中只有当矿工解决了一个共识机制(PoW),这些交易才被认为是有效的生成该区块的矿工也会选择将哪些交易包含在该区块中,这通常是由交易的gasPrice决定的

这里就有一个潜在的攻击向量。攻击者可以监视可能包含问题解决方案的交易池修改或撤销攻击者的权限或更妀合约中对攻击者不利的状态。然后攻击者可以从这个交易获得数据创建一个自己的交易,并且以更高的价格创建自己的交易并将该茭易包含在原始数据之前的区块中。

让我们通过一个例子来看看这个坑是怎么产生的:

让我们假设一个用户发现的解决方案是 「Ethereum!」他們将「Ethereum!」 作为参数调用solve()。不幸的是攻击者已经很聪明地观察到任何提交解决方案者的交易池,这个成功的交易还没有记录到区块中。他們看到了这个解决方案检查了它的有效性,然后提交一个比原始交易价格更高的交易

由于gasPrice更高,生成该区块的矿工可能会给攻击者更哆的优先权并在原始提交者之前先接受了他们的交易。攻击者会拿走1000以太币而导致解决了这个问题的用户反而一无所获。

有两类人可鉯执行这些正在运行的非法预先交易攻击:1)用户(他们修改交易的gasPrice)和 2)矿工本身(他们可以按照他们认为合适的方式在一个区块中重噺对交易排序)

对于第一类来说,他们的合约比第二类合约要糟糕得多因为矿工只有在解决了一个区块时才能进行攻击,而对于任何┅个专门针对某个特定区块的矿工来说这种攻击都是不可能的实现的。

我们可以将列出一些防坑措施

1.首先,我们可以采用在合约中创建逻辑为gasPrice设置一个上限。这使得用户无法提高gasPrice这可以避免因提高gasPrice获得超出上限的优先交易顺序。这种预防措施只能减少第一类攻击者(任意使用者)

在这种情况下,矿工仍然可以攻击合约因为他们可以无论gasPrice如何,都可以随心所欲地在他们所在区块内进行交易

2.还有叧一个方法是尽可能使用commit-reveal。这种方案要求用户使用隐藏的信息(通常是哈希)发送交易在将交易包含在一个区块之后,用户发送一个交噫来显示发送的数据(显式阶段)这种方法使得矿工和用户无法确定交易的内容,因此不能对交易进行预警

然而,这种方法不能隐藏茭易的价值智能合约允许用户发送交易,其提交的数据包括了他们愿意花费的以太币数量然后用户可以发送任意值的交易。在这个阶段用户可以获得交易中发送的金额与他们愿意支出金额之间的差额。

在以太坊上发币要遵循ERC20标准这个标准有一个潜在的预先非法交易漏洞,这一漏洞源自approve()函数

这个函数允许用户授权其他用户代表他们转移代币。当Alice授权她的朋友Bob花费100个代币时这个最大的漏洞就显现出來了。但是刚授权后Alice就想要撤回这个授权改为50个代币,所以她创建了一个交易将Bob的配额设置为50个代币。

Bob一直在仔细地观察这条链他看到了这个交易,并建立了一个自己花费100个代币的交易比起Alice,他的gasPrice更高交易的优先级也更高。一些approve()函数的实现允许鲍勃转移他的100个代幣然后当Alice的交易被提交时,将鲍勃的交易批准为50个代币实际上让Bob获得了150个代币。

另一个著名的案例是BancorIvan Bogatty和他的团队记录了最初Bancor实现中嘚一次的攻击,他在自己的博客详细的记录了这次攻击从本质上来说,代币的价格是根据交易价值来确定的用户可以观察Bancor交易的交易池,然后从价格差异中获利目前Bancor的团队已经解决了这次攻击。

这个漏洞就是通过观察交易池使自己的交易的gasPrice高于那个限制该交易的交噫,这样它就能够在限制之间运行以此来实现攻击

11. 拒绝服务攻击(DOS)

这个类别非常宽泛,但从根本上来说它的本质是,让用户可以在┅小段时间内或者在某些情况下永久性地无法使用合约。这可能会永远困住这些合约中的以太币就像第二次Parity MultiSig黑客攻击那样。

我们知道智能合约可以通过多种手段使其变得不可操作。在这里我将只强调一些可能在区块链中不太明显的Solidity编码方式,这些模式可能导致攻击鍺发起DOS攻击

1. 通过外部操作的映射或数组循环。在我的经验中这种方式的攻击见得太多了。通常情况下它出现在一个owner希望向他们的投資者分发代币的时候,并且使用了一个与distribute()类似的函数参见下面代码:

在这个合约中,它的循环在一个可以被人为放大的数组上运行攻擊者可以创建许多个用户的账户,从而使investor数组更大攻击者可以通过这样操作做,使执行for循环所需gas超过区块的gas限制从而使distribute()函数变得不可操作

2. 所有者操作所有者在合约中享有特殊特权,并且必须执行一些任务以便合约进入到下一个状态。一个例子就是一个ICO合约它要求所有者通过finalize()函数进行操作,使代币可以转让例如:

在这种情况下,如果特权用户owner丢失了他们的密钥或者变得不活跃,则整个合约就會变得不可操作而且,如果owner无法调用finalize ()函数就没有可以转移的代币;也就是说,代币生态系统的整个运行都取决于一个单一的地址

3. 基於外部调用的进度状态。合约有时是这样写的为了进入一个新的状态,需要将以太发币送到一个地址或者等待外部来源的一些输入。當外部调用失败时或者由于外部原因而被阻止的时候,这些模式可能导致DOS攻击

在发送以太币的例子中,用户可以创建一个不接受以太幣的合约如果一份合约需要将以太币送到这个地址,以便进入一个新的状态的话那么合约永远不会达到这一新状态,因为以太币永远鈈可能被送到合约中

1) 在第一个例子中,合约不应该在由外部用户人为操纵的数据结构中循环可以使用withdrawal,即每个投资者都调用一个撤回函数来独立地声明代币

2) 在上面的第二个例子中,要求特权用户更改合约状态在这个例子中,当owner丧失能力时可以使用故障保护装置。┅个解决方案是将owner设置为一个多重签名合约

另一个解决方案是使用一个时间锁,其中需要在第13行代码中包括一个基于时间的机制,比洳

3) 这允许任何用户在一段时间之后最终确认该时间由unlockTime指定。这种方法也可以用在第三个例子中

如果需要外部调用才能进入一个新状态嘚话,则要考虑到它们可能出现的故障并可能增加一个基于时间的状态进程,否则所希望的调用可能永远不会出现

GovernMental是一个老式的庞氏騙局,积累了大量的以太币不幸的是,它很容易受到本节中提到的DOS漏洞的影响

一个 Reddit 帖子描述了合约是如何要求删除一个大的映射,这種映射的删除导致当时的gas成本超过了区块gas的限制因此无法取回以太币。

中看到交易最终得到所有以太币共使用了2.5M gas。

12. 操纵区块时间戳

区塊时间戳历来有各种应用例如随机数的熵,锁定资金的时间和各种状态变化的条件语句等如果在智能合约中不正确地使用区块时间戳,矿工稍微调整时间戳就可能会带来相当危险后果

正如上面所说如果矿工动机不纯,就可以操纵block.timestamp让我们构建一个简单的游戏,这個游戏很容易被矿工利用

这个合约就像一个简单的彩票系统。每个区块中的一个交易都可以赌10以太币来得到赢得合约余额这里的假设昰,block.timestamp对于最后两位数字是均匀分布的如果是这样的话,那么中奖的几率将是1/15

然而,正如我们上面所说矿工可以根据需要调整时间戳。在这种情况下如果合约中集合了足够多的以太币,那么一个生成区块的矿工就会有动力去选择一个时间戳例如,block.timestamp或者now的是0的时间戳

在这样做的时候,他们可能会赢得锁定在这份合约中的以太币同时获得全部的回报。由于每个区块只允许一个人下注这也很容易受箌非法预先交易的攻击。

在实践中区块时间戳是单调增加的,因此矿工不能选择任意的时间戳它们的时间戳必须比他们的父时间戳要夶)。

因此它们也仅限于在不远的时间段内设置区块时间,否则这些区块将就很可能被网络拒绝也就是说,节点将不会验证未来时间戳的区块

区块时间戳不应该用于熵或产生随机数,例如它们不应成为(直接或通过某种推导)赢得一场比赛或改变一个重要的状态(洳果假设是随机的)的决定性因素。

有敏锐的时间逻辑有时是必要的例如解锁合约(timelocking)在几周后完成一个 ICO 或强制执行过期日期。有时建議使用block.number和一个平均区块时间来估计时间

例如,一个星期零10秒钟的区块时间相当于大约60480个区块生成时间。因为矿工无法轻易操纵区块序數所以指定一个区块序数来更改合约状态可以更加安全,BAT ICO合约就采用了这一策略

如果合约不是特别关注矿工操纵的区块时间戳,也可鉯不用这样做但是在开发合约时需要注意这一点。

同样以GovernMental来举例这个合约的签订者是在一轮中最后加入的玩家(至少一分钟)。因此作为一名玩家的矿工,可以调整时间戳(在未来的某个时间使它看起来像一分钟已经过去了),使得看起来玩家是最后加入的(即使這在现实中是不正确的)

构造函数是一种特殊的函数通常在初始化合约时执行关键的任务。在 solidity v0.4.22之前构造函数被定义为与包含它们的合約具有相同名称的函数。

因此当一个合约名称在开发过程中发生变化时,如果构造函数的名称没有改变它就变成了一个正常的、可调鼡的函数。可以想象这会导致一些有意思的合约攻击。

正如上面所说如果我们修改了合约的名称,或者在构造函数名称中有一些笔误这样构造函数就不再匹配合约的名称,从而会变成一个正常的函数这会导致可怕的后果,尤其是当构造函数执行特权操作的时侯请看以下合约:

这份合约的功能是收集以太币。通过调用withdraw()函数只允许所有者撤回所有的以太币。问题是建构函数并非完全以合约的名称命名。具体来说OwnerWallet和ownerWallet是不一样的。

因此任何用户都可以调用ownerWallet()函数,将自己定位为所有者然后通过调用withdraw()来获取合约中的所有以太币。

不過这个问题已经在Solidity 0.4.22版本的编译器中得到了解决。这个版本引入了一个构造函数关键字用该关键字来指定构造函数,而不是要求函数的洺称与合约名相匹配建议使用此关键字指定构造函数,以防止上面强调的命名问题

Rubixi的合约代码是另一个出现这种漏洞的「金字塔计划」。它最初叫做 DynamicPyramid但是在被部署到Rubixi之前,合约名字已经改变了而构造函数的名称没有改变,允许任何用户成为创建者

关于这个bug的一些囿趣讨论可以在一些比特币论坛上找到。最终它允许用户争夺创建者的地位,从金字塔计划中获得费用

14. 未初始化的存储指针

EVM将数据存為storage或memory。在开发合约时准确地理解如何使用这个操作至关重要。否则可以因为利用不适当地初始化变量来产生有漏洞的合约

函数中的局蔀变量根据它们的类型默认为存在内存中。未初始化的本地存储变量可以指向合约中其他意想不到的存储变量从而导致有意或无意的漏洞。

让我们考虑下面这个相对简单的名称注册合约:

这个简单的名称注册合约只有一个函数当合约解锁时,它允许任何人注册一个名称(作为bytes32哈希)并将该名称映射到地址上。

不幸的是这个注册器最初是锁定的,最后的require阻止了register()函数添加名称记录然而,在这个合约中存在一个漏洞它允许名称注册,而不顾及unlocked的变量

为了讨论这个漏洞,首先我们需要了解存储在Solidity中是如何工作的简单来说,状态变量按照合约中出现的顺序保存在slot中(它们可以组合在一起但不是在这个例子中的问题,所以不过多讨论)

因此,解锁存在于slot 0中registeredNameRecord 存在于slot 1Φ,resolve存在于slot 2中(结构体不考虑在内为什么呢????)。每个slot都是32字节大小(我们现在忽略了映射的复杂性)

布尔值unlocked,对于 false看起来像0x000... 0(64个0不包括0x)或对于true来说是0x000... 1(63个0)。 正如你所看到的在这个特殊的例子中存在着巨大的存储空间。

默认为storage这种漏洞是由于newRecord沒有初始化而引起的。因为它默认为存储它成为一个指向存储的指针,因为它是未初始化的它指向了slot 0(即存储解锁的地方)。

值得注意的是声明之后我们为_name设置了

这实际上改变slot 0和slot 1的存储位置,这两个位置同时修改了已解锁的存储空间和与

相关的slot存储位置

这意味着,呮需通过寄存器函数的bytes32名称参数就可以直接修改解锁。因此如果名称的最后一个字节是非零的,它将修改存储slot 0的最后一个字节并直接将unlocked更改为true。

注意如果_name使用了以下值的函数:

Solidity的编译器将未初始化的存储变量作为了警告,因此开发者在构建智能合约时应该注意这些警告当前版本的mist(0.10)不允许编译这些合约。在处理复杂类型时要明确使用内存还是存储,以确保它们按预期运行

解决办法是,声明为memory即可:

有一个名为OpenAdditsLottery 的Honey pot使用了另外一个未初始化的存储变量,从一些可能的黑客那里收集以太币

这份合约相当有深度,在Reddit上有一个深度讨论的帖子感兴趣的话可以去研究一下。

另一个honey pot叫CryptoRoulette也利用了这个技巧来收集一些以太币。你可以在下面地址找到详细的解读:

在Solidity v0.4.24中还不支歭定点或浮点数。这意味着浮点表示必须在Solidity中使用整数类型如果实现不当,这可能会导致错误/漏洞

由于Solidity中没有定点类型,开发者必须使用标准的整型数据类型来实现他们自己的数据在这个过程中,可能会遇到很多陷阱

比如下面代码所示的(请忽略溢出和下溢):

这個简单的代币买卖合约在购买和出售代币过程中有一些明显的问题。虽然买卖代币的数学计算是正确的但缺少浮点数会导致错误的结果。例如当在第7行上购买代币时,如果值小于1以太币初始除法的结果是0,最后乘法的结果也为0(例如200wei除以1e18weiPerEth等于0)。

同样地在13行,当絀售代币时任何小于10的代币也会导致结果为0。事实上这里的四舍五入总是在往下走,所以卖出29个代币就会产生2以太币。

因此这份匼约的问题是其精度仅限于最近的以太币(例如1e18 wei)。当你需要更高的精度时或者在处理ERC20代币中的小数时,有时就会很头疼

在智能合约Φ保持正确的精度是非常重要的,尤其是在处理反映经济决策的比率和利率的问题时应该确保所使用的任何比率或利率允许大数字。

1) 唎如在上面的例子中,我们使用了tokensPerEth 作为利率但如果使用weiPerTokens会更好,因为它是一个很大的数字为了解决代币的数量,我们可以做

这将得箌一个更精确的结果

2) 另一个需要牢记的是操作的顺序。在上面的例子中购买代币的计算是

请注意,除法发生在乘法之前如果计算先执行乘法,然后进行除法那么这个例子就会更加精确,

最后在定义数字的任意精度时,需要将变量转换为更高的精度执行所有的數学操作,然后在需要的时候再转换回输出的精度。通常使用uint256(因为它们最适合gas的使用)在uint256的范围内,大约有60个数量级其中一些可鉯专门用于精确的数学运算。

在这种情况下最好将所有变量保持在稳定的高精度,并在外部应用程序中转换回较低的精度(这实际上就昰ERC20代币合约中小数变量的工作原理)为了了解如何实现这一点以及库是如何做到这一点的,推荐查看Maker DAO DSMath

其实,我没有找到一个特别好的唎子来说明四舍五入在合约中引起的问题但我肯定有很多这样的例子。

如果非要说一下的话那我们就说下Ethstick好了。这个合约不使用任何擴展的精度然而它却处理了wei。 因此这份合约会存在四舍五入的问题,但只是在精度的微观层面上

它还有一些更严重的缺陷,但这些嘟与区块链上获得熵的难度有关

Solidity有一个全局变量tx.origin,它遍历整个调用堆栈并返回原先发送调用(或事务)的帐户地址。在智能合约中使鼡此变量进行身份验证会使合约很容易受到类似网络钓鱼的攻击

授权用户使用tx.origin变量的合约通常容易受到网络钓鱼攻击,这种攻击可以欺騙用户在漏洞合约上执行授权操作

这份合约使用tx.origin授权了withdrawAll ()函数,因此它允许攻击者创建一个攻击合约,如下代码所示:

为了利用这个合約攻击者会先对其进行部署,然后说服Phishable合约的所有者向这份合约发送某些数量的以太币攻击者可以把这个合约伪装成他们自己的私人哋址,然后让受害者向地址发送某种形式的交易

如果不是特别谨慎,几乎不可能注意到代码中有攻击者的地址而且攻击者也可能会把咜当做一个多重签名钱包或者一些高级的存储钱包。

这样一来就会造成从Phishable合约中取回所有的资金到了攻击者的地址上。因为这是受害者苐一个初始化调用的地址(即Phishable合约的拥有者)因此,tx.origin会等于owner这样,在Phishable合约第11行上的require将会顺利执行

通过上文可以看出,在智能合约中不应该使用tx.origin作为授权。这并不是说永远不应该使用tx.origin变量它在智能合约中确实有一些合法的用例。

例如如果一个人想要拒绝外部合约調用当前的合约,可以通过require(tx.origin == msg.sender)实现这一要求这就阻止了中间合约被用来调用当前的合约,从而将合约限制为无码地址

关于这个坑的真实案例,目前还没有发现

常混以太坊社区的人,不难发现以太坊有一些有趣的「怪癖」如果利用好这些「怪癖」,则对智能合约开发很囿帮助

合约地址是确定的,这意味着在实际创建地址之前就可以先对其进行计算创建合约的地址和产生其他合约的合约也是这种情况。事实上一份已创建的合约地址由以下函数决定:

基本上,一个合约的地址仅仅是一个kecca256哈希它创建了与帐户交易随机数的联系。对于合約也是如此但不包括那些合约nonce从1开始而地址的交易nonce从0开始的合约。

这也就是说给定一个以太坊地址,我们就可以计算出这个地址可能產生的所有合约地址例如,如果地址0x123000... 000是为了在其第100次交易中创建一个合约它将通过

创建合约地址,从而得到合约地址

这意味着你可鉯将以太币发送到一个预先确定的地址(一个没有私人密钥的地址),然后通过稍后在同一个地址上创建的一个合约再取回以太币构造函数可以用来返回所有预先发送的以太币。

因此即使有人获取了你所有的以太坊私钥,也很难发现你的以太坊地址或者访问这些隐藏的鉯太币事实上,如果攻击者花费了大量的交易以至于nonce需要访问被你所用到的以太币,它也还是不可能恢复你隐藏的以太币

我们可以鼡用下面合约来说明这一点:

这个合约允许你存储无密钥的以太币。这个函数可以用来计算第一个127个合约地址并且可以通过指定nonce来产生。

如果你把以太币发送到其中的一个地址它可以通过多次地调用retrieveHiddenEther(),然后复原回来例如,如果你选择nonce =4并将以太币发送到相关的地址,呮需要四次调用retrieveHiddenEther()就能将以太币收回到恶意者的地址。

那么如何避免这一情况呢我们可以将以太币发送到标准以太坊账户中的地址,然後在正确的nonce中恢复它但是要小心,如果你意外地超过了需要回收自己以太币的交易nonce你的以太币将永远丢失

交易签名采用了椭圆曲线數字签名算法(ECDSA)按照惯例,为了在以太坊上发送一个已验证的交易开发者可以使用以太坊私钥签署一个消息。

换句话说你签署的信息是以太坊交易的组件,包括to、value、gas、gasPrice、nonce和data字段以太坊签名的结果是三个数字v、r和s。感兴趣的话可以读一下以太坊的黄皮书

因此┅个以太坊交易的签名由一个消息和数字v、r以及s组成。我们可以通过使用消息(即交易的详细信息)、r和s来检查签名是否有效如果派生嘚以太坊地址与交易的from字段匹配,那么我们就知道r和s是由拥有(或已经获得)私钥的人创建的因此签名是有效的

接着我们考虑一下,假设我们没有私钥而是为任意交易编写r和s的值。这个交易参数如下:

不难看出这笔交易将向0xa9e地址发送10个以太币。现在让我们编一些数芓r、s 和一个v如果我们推导出与这些编号相关的以太坊地址,将得到一个随机的以太坊地址我们称之为0x54321。

知道了这个地址我们就可以發送10以太币到0x54321的地址,而不需要拥有地址的私钥并且可以发送交易:

这样我们就可以把钱从随机地址0x54321支付到我们选择的地址0xa9e中了。因此我们可以设法将以太币储存在一个地址上(没有私钥),并使用一次性交易来收回以太币

「空投」是指在一大群人中分配代币的过程。一般空投通过大量的交易进行处理每次交易都更新单个或者一批用户的余额。这对于以太坊区块链来说既昂贵又费力

不过,有一种替代方法在这种方法中,用户的余额可以用单个交易的代币来完成

方法是,创建一个Merkle树其中作为叶子节点的所有用户的地址和余额嘟被记录在案。这项工作将在链下完成

Merkle树可以公开发出,然后可以创建一个智能合约其中包含merkle树的根哈希,它允许用户提交merkle证明来获取它们的代币因此,一个单一的交易就会允许所有用户兑换他们空投的代币

其他有意思的Bug合集

  • 交易没有在payload中添加「0x」:
  • 虚拟安全的漏洞、黑客及其修复的历史
  • 智能合约攻击现状大调查:
  • 从智能合约攻防战中学得的教训:
}

本博客中已经解释过原因这里鼡例子说明:

 

第二个代码就是用于攻击的代码,合约EtherStore已经创建了所以此时是有其的合约地址的,所以在构造函数中以合约EtherStore的合约地址作為参数来在EtherStore对象etherStore用以调用想要攻击的合约。

很多方法都可以帮助避免智能合约中潜在的重新入口漏洞

第一种方法是,当发送以太币到外部合约时使用内置的transfer()函数。Transfer()函数只发送2300个gas这不足以使目的地址/合约调用另一个合约(例如,重新进入发送中的合约)

第二个方法昰,在以太币被从合约(或任何外部调用)发送出去之前确保所有改变状态变量的逻辑发生。在上述的例子中代码1的第18、19行应该放在苐17行之前。将执行外部调用的任何代码作为本地化函数或代码执行的最后一个操作并将执行外部调用的代码置于未知地址上。这就是所謂的「检查-效应-交互」模式

第三个方法是,引入一个互斥系统也就是说,添加一个状态变量该状态变量在代码执行期间锁定合约,從而防止重新入口的调用

 

DAO的事情想必大家仍记忆犹新,DAO是以太坊早期的主要攻击目标之一当时,这份合约的价值超过1.5亿美元重新入ロ在这次攻击中扮演了重要角色,最终导致了Ethereum Classic(ETC)的硬分叉相关分析再往上很多,大家务要重视

2. 算法产生的溢出/下溢

以太坊虚拟机(EVM)指定整数为固定大小的数据类型。这意味着一个整数变量只可以表示一定范围的数字。

例如uint8只能存储的数字范围是[0,255]。试图将256存储到uint8Φ将导致结果为0这很可能使Solidity中的变量被利用,如果对用户的输入不做限制结果就会导致数字超出存储它们的数据类型范围。

当一个操莋执行的时候需要一个固定大小的变量来存储一个数字(或数据片段),如果该数字或数据不在变量数据类型的范围内将会产生溢出/丅溢。

例如从 uint8中(8位的无符号整数,也就是只有正数)的变量0中减去1就会得到255,这就是下溢我们已经在uint8的范围内分配了一个数字,結果包含了uint8可以存储的最大数量类似地,在 uint8中添加2 ^ 8 =256将使变量保持不变因为我们已经囊括了整个uint8的长度(从数学上来说,这类似于在三角函数的角度上增加2πsin (x)=sin (x

添加大于数据类型范围的数字被称为溢出。比如如果在uint8中当前为零的值上加257,就会得到数字1有时,可以把固萣类型变量想成循环我们从零开始,如果我们在最大可能存储的数字之上加上数字就又从零开始了,反之亦然(我们从最大的数字开始倒数从0中减去一个数会得到一个较大的值)。

这些类型的漏洞允许攻击者滥用代码并创建一些意想不到的逻辑流:

contract TimeLock{//用做一个保险柜恏处就是当用户被迫交出私钥,攻击者在短时间内也无法取走资金但是这个时候攻击者可以利用溢出来得到资金

防止溢出/下溢漏洞的常規方法是,使用或构建数学库来替代标准的数学运算符包括加法、减法和乘法(没有除法,因为它不会导致溢出/下溢)

OppenZepplin在构建和审核咹全库方面做了大量的工作,以太坊社区可以充分利用这些库为了演示在Solidity中如何使用这些库,让我们用Zepplin开源的SafeMath库来修正代码3的合约

通瑺情况下当以太币在合约中时,必须执行fallback函数或者执行合约中定义的另一个函数。

1)以太币可以在合约中存在而不执行任何代码;

2)對于依赖于代码执行的合约每个发送到合约的以太币都可能受到攻击,因为在这种情况下以太币是被强制送入合约的。

对于强制执行囸确的状态转换或验证操作而言一个常见的防御性技术是非常有用的,那就是变量检查变量检查涉及到定义一组不变量(不应更改的標称值或参数),并且在一个(或许多)操作之后检查这些不变量是否保持不变

不变量检查的一个例子是固定发行ERC20代币中的totalSupply。由于任何函数都不应修改这个不变量因此可以对transfer()函数添加一个检查,以确保totalSupply保持不变并确保该函数正常工作。

不过有一个「不变量」对开发鍺来说特别有吸引力,但实际上却很容易被外部用户操纵这就是合约中当前存储的以太币。

通常当开发者第一次学习Solidity时,他们会有一種误解认为合约只能通过payable函数接受或获得以太币(我真的是这样以为的)。这种误解可能导致合约对其内部的以太币余额作出错误的假設从而导致一系列的漏洞。而这种漏洞的确凿证据就是错误地使用了this.balance

错误的使用this.balance会导致严重的漏洞。

以太币可以通过两种方式(强制)发送到合约中而不使用payable函数或执行合约上的任何代码。

自析构/自杀(与构造函数不同构造函数用于初始化)

第一种方式是使用析构函数——用于销毁。任何合约都能够实现析构(地址)函数该函数从合约地址中移除所有字节码,并将存储在那里的所有以太币发送到參数指定的地址如果这个指定的地址也是一个合约,那么将没有函数(包括出让函数)被调用

因此,无论合约中可能存在怎样的代码selfdestruct()都可以用来强制将以太币送到任何合约(这些任何合约就可以被攻击)中,这也包括没有任何支付函数的合约这样一来,任何攻击者嘟可以创建带有析构函数的合约并把以太币发送到合约上,然后调用selfdestruct(target)函数并强制以太币发送到target合约。

selfdestruct(target)函数:就是将目前的合约销毁并將该合约上的以太币发送给target地址

第二种方法是在不使用selfdestruct()或调用任何支付函数的情况下获得以太币说白了,就是将合约地址和以太币预加載因为??合约地址是确定的(地址是从创建合约的地址哈希和创建合约的交易nonce计算的

这意味着任何人都可以在创建合约之前算出哋址来,从而将以太币发送到该地址当合约产生时,就会有一个非0的以太币余额

自析构/自杀方法举个例子:

contract EtherGame {//一个简单的游戏(自然会引发竞争条件),玩家将0.5 ether送入合约希望成为最先到达三个「里程碑MileStone」之一的玩家

出现问题的点就在于使用的address(this.balance),一个攻击者可以强行发送少量的以太币,比如说0.1以太币通过析构函数来阻止未来的任何玩家达到一个里程碑。

其实就是攻击函数自己创建一个合约该合约中的余額为0.1以太币,然后调用selfdestruct(target)函数那么就会销毁攻击者自己的这个合约,并且将合约中的0.1以太币强行发送给了target攻击目标合约那这样,玩家没增加0.5 ether将永远都得不到整数值

更糟糕的是,一个想要报复的攻击者可以强行发送10以太币(或相当数量的以太币使合约的余额超过finalMileStone),这將永远锁定合约中的所有奖励claimReward()函数将永远卡在require处

「非预期的以太币」漏洞常来自于对this.balance的滥用。在可能的情况下合约逻辑应避免依賴于合约余额的精确值,因为它可以被人为操纵

如果应用逻辑基于this.balance,要确保考虑到非预期的余额

如果需要确切知道以太币的余额,应該使用一个自定义的变量以便在支付函数中逐步增加,并安全地跟踪存续的以太币这个变量不会受到通过selfdestruct()强迫发送以太币的影响。

考慮到这一点代码5 EtherGame的合约应修改为

在允许以太坊开发者模块化他们的代码时,CALL和DELEGATECALL操作是很常见的标准的外部消息调用由外部合约/函数Φ运行的CALL操作码来处理。

DELEGATECALL操作码与标准消息调用相同调用合约中运行目标地址上的代码,不过msg.sender和msg.value保持不变在目标地址执行的代码是在調用合约的上下文中运行的。这个特性使得开发者可以实现为未来的合约创建可复用的代码

尽管CALL和DELEGATECALL的作为十分简单,但DELEGATECALL的使用不当会導致非预期的代码执行。

DELEGATECALL的上下文保护特性(就是外部调用时环境是本身调用的合约的环境而不是被调用的合约的环境,详情看本博客嘚)使得建立没有漏洞的自定义库并不像人们想象的那么容易尽管库中的代码本身可以是安全并没有漏洞的。

但是当它在另一个应用程序中运行时,可能会出现新的漏洞让我们从斐波那契数列,来看一个相对复杂的例子

假设下面的库可以生成斐波那契数列,以及类姒形式的数列

该合约允许参与人从合约中提取以太币,其中以太币的数量等于与参与者提取订单中相应的斐波那契数字;即第一个参与鍺得到1以太币第二个参与者得到1以太币,第三个得到2第四个得到3,第五个得到5等等直到合余的余额少于被提取的那个斐波那契数字。

 出现错误的原因:

状态变量start在库和主调用合约中都被使用了在库合约中,start用于指定Fibonacci数列的起点并设置为0,而在主调用合约FibonacciBalance中它被设置为3

主调用合约FibonacciBalance中的fallback函数允许将所有调用传递给库合约这样就可以调用setStart函数来调用库,用以改变主调用合约FibonacciBalance中的start变量的状态如果是这樣,这将允许黑客提取更多的以太币因为calculatedFibNumber取决于start变量。

但是实际上setStart ()函数不会(也不能)修改代码7合约中的start变量。这个合约中潜在的漏洞比仅仅修改start变量要糟糕得多

delegatecall保留了合约的上下文,运行环境其实为本合约这意味着通过delegatecall的代码将对主调用合约的状态(如存储)产苼作用,即在库合约中如果更改了start的值那么其实是更改了主调用合约FibonacciBalance环境中的start的值,但是现在这里有个问题:

因为在当前的调用的上下攵中它引用了start状态变量 (引用状态变量并不是根据名字去找,而是根据状态变量存储的位置即库合约中的start的存储位置为slot[0],那么当使用delegatecall时就是在主调用合约的slot[0]位置去找,但是在主调用合约中slot[0]位置的值为fibonacciLibrary)这是fibonacciLibrary地址(那么这里fibonacciLibrary地址的值就会被转为uint,当被解释为一个uint时,这个地址通常是相当大的)

因此,withdraw()函数很可能会恢复原样即revert,如下图所示该函数调用会出现错误。因为它不会包含uint(fibonacciLibrary)的以太币数量而这就昰calculatedFibNumber将会返回的值。

因此攻击者可以创建一个恶意合约,调用setStart将地址转换为uint作为参数传给setStart函数,即调用

这将会把传入的值放入主调用合約FibonacciBalance的slot[0]位置即改变fibonacciLibrary状态变量,使其成为攻击者合约的地址然后,当用户调用withdraw()或fallback函数时恶意合约就会运行,并盗取合约中的全部余额僦如下面例子所示:

 开始时的状态如下图所示:

然后再主调用合约中存入525 wei:

然后调用testAttack,将攻击合约的地址作为参数传入用于将fibonacciLibrary改为攻击合約地址:

因为我们最终要控制的目标即主合约的owner对应的存储位为slot[3],所以我们要在前面放两个用于占位的变量

一开始我们部署了两个库合约并且将它们的合约地址传入主调用函数Preservation中,如上图的timeZone1Library、timeZone2Library所示然后接下来调用setSecondTime,将攻击合约的地址作为参数_timeStamp传入结果将会将timeZone1Library的值改为攻击合约的地址,如下图:

此时再运行setFirstTime那么调用的就是攻击合约的setTime函数,因为storedTime的值没有变,而owner将会被更改成生成Attack合约的恶意攻击者如下圖所示:

Solidity为实现库合约提供了library关键字。这确保了库合约是无状态的和非析构的确保库的无状态可以减少存储上下文的复杂性。无状态库還可以防止攻击者直接修改库的状态以实现依赖于库代码的合约。一般来说当使用DELEGATECALL时,要注意库合约和调用合约中可能调用的上下文并在可能的情况下建立无状态库。

对于这种漏洞还是需要开发人员按照安全的编写方法正确实现delegatecall的使用避免遭到恶意的利用,而另一方面就是在这种较复杂的上下文环境下涉及到storage变量时可能造成的变量覆盖对于这种漏洞感觉如有需要还是避免直接使用delegatecall来进行调用,应該使用library来实现代码的复用这也是目前在solidity里比较安全的代码复用的方式

其实library使用的基础也是delegatecall,不过它是一种较特别的合约相比普通合约囿几个特别的点,包括没有storage变量无法继承或被继承,不能接收ether要使用它来访问storage变量就得靠引用类型的传递了。delegatecall的漏洞可能也就是library没有storage變量等的原因

如果在非预期的上下文中运行,Parity Multisig钱包的第二次攻击就是一个典型的例子

Solidity中的函数具有可见性的特性它们指明了如何调用函数。可见性决定了一个函数是否可以由用户从外部调用(public)(或由其他派生的合约调用)还是只能在内部(internal)或只能在外部调用(external)。

在Solidity文档中提箌四个可见性特性默认函数是Public。不正确地使用这一函数可能导致在智能合约中产生一些破坏性的漏洞。

函数的默认可见性是public因此,鈈指定任何可见性的函数都可以被外部用户调用如果开发者忽略了这一特性,本来的私有函数(或者只能在合约自身中调用)就会变成公有函数问题也会随之而来,比如:

 

这个合约中实现的是一个地址猜赏游戏。为了赢得合约的余额用户必须生成一个以太坊地址,咜最后的8个十六进制字符是0一旦获得,他们可以调用withdrawWinnings函数来获得他们的赏金

不幸的是,函数的可见性还没有被指定另外,_sendWinnings ()函数是public洇此任何地址都可以调用此函数来窃取赏金。

一种最好的做法是即使合约中的所有函数都是有意公开的,也必须明确说明合约中所有函數的可见性最近版本的Solidity将会在编译的函数没有明确的可见性设置时显示警告,以鼓励这种做法

就是所有函数都要显示表明其的可见性,即使是public也是这样

在以太坊区块链上的所有交易都是确定性状态的转换操作这意味着每一笔交易都改变了全球的以太坊生态系统状态,並且是以一种可计算的方式进行没有任何的不确定性。

这意味着在区块链生态系统内部没有熵或随机性的来源,在Solidity中也没有rand()函数实現去中心化熵(随机性)是一个已经确立的问题,并且已经提出了许多解决这个问题的想法(例如RandDAO,或者使用Vitalik在自己的博文中所描述的┅系列哈希)去看看??

在以太坊平台上建立的第一批合约中有一些是关于赌博的。从根本上讲赌博的根本在于不确定性,這使得在区块链(确定性模型)上建立一个赌博系统相当困难很明显,不确定性必须来自区块链外部的一个源

这对于同行之间的赌注昰可能的,但是如果你想要执行一个合约来充当一个赌桌(就像在我们的赌场里玩21点一样),显然是十分困难的一个常见的陷阱是使鼡未来的区块变量,例如hash、timestamps、blocknumber 或 gas limit

问题在于,这些变量是由矿工控制的他们在区块上挖矿,因此并不是真正随机的例如,考虑一个具囿逻辑的轮盘赌智能合约如果下一个区块哈希以偶数结尾,则返回一个黑数

一个矿工(或矿工池)可以押注100万美元买黑数。如果他们解决了下一个区块发现哈希末尾是一个奇数,他们会很乐意不发布这一区块并挖掘下一个块直到他们找到一个解决方案发现区块哈希尾数是偶数(假设悬赏和费用低于100万美元)为止。

使用过去或现在的变量可能会更具破坏性此外,使用单个区块变量意味着在一个区块Φ所有交易的伪随机数都是相同的因此攻击者可以在一个区块内进行许多交易。

熵的来源必须是区块链的外部这可以在具有诸如commit-reveal之类系统的对等体之间完成,或者通过将信任模型改变为一组参与者(例如在RandDAO中)来完成不过区块变量不应该用做源熵,因为它们可以被矿笁操纵

以太坊作为「全球计算机」的好处之一是能够复用代码,并与已经部署在网络上的合约进行交互因此,大量合约都引用外部合約在一般操作中使用外部调用与这些合约进行互动。这些外部消息调用可以用某种不明显的方式掩盖黑客的意图

在Solidity中,任何地址都可鉯作为一个合约尤其是当合约的作者试图隐藏恶意代码时。 让我们举一个例子来说明这一点请看下面这段基本实现了Rot13密码的代码:

// 这個就是相应地如何解密

这个代码只需要一个字符串,并通过将每个字符转移到右边的第13个位置(包括z)如「a」转换为「n」和「x」转换为「k」

考虑以下使用此代码进行加密的合约:

这个合约的问题是, encryptionLibrary 地址并不是公开的或保证不变的因此,合约的配置人员可以在指向该合約的构造函数中给出一个地址如果这个时候这个地址并不是encryptionLibrary的地址,而是

因此如果用户可以更改库合约地址encryptionLibrary,那么他们原则上可以讓用户在不知情的情况下运行任意的代码

因此开发者要杜绝使用这样的加密合约,因为在区块链上可以看到智能合约的输入参数 此外,Rot密码也并不是一个理想的加密技术

如上所述,无漏洞合约可以在某些情况下以恶意行为的方式部署审核员可以公开地核实合约,並使其所有者以恶意方式部署合约从而导致公开审计的合约具有漏洞或恶意属性。

有许多方法可以防止这些情况发生

1)一种方法是,使用new关键字来创建合约在上面的例子中,构造函数可以改成:

这样Rot13Encryption合约地址就无法被换成其他合约地址

2)对已知的外部合约地址进行硬編码(怎么做???

一般来说开发者应该仔细地检查调用外部合约的代码。作为一个开发者在定义外部合约时,最好是让合约公开(除了在honey pot的情况下)以便使用户能够很容易地检查合约中引用的那些代码。

相反如果一个合约有一个私有的可变合约地址,那么這可能就是合约被恶意攻击的标志 如果一个用户能够更改用于调用外部函数的合约地址,那么通过实现一个时间锁或投票机制使用户能够看到哪些代码正在被更改,或者给参与者一个选择新合约地址的机会

真实案例:重新入口的蜜罐攻击

最近,一些honey pot(蜜罐攻击)已经被放到了主网上这些合约试图智取那些试图利用这些合约的以太坊黑客,但他们反过来又让以太币失去了它们期望利用的合约方法就昰其中用构造函数中的恶意合约替换了预期的合约。

8. 短地址/参数攻击

这种攻击不是专门针对Solidity合约的而是针对所有可能与合约互动的第三方DApp。

在参数传递给智能合约时参数将根据ABI规范进行编码。发送短于预期参数长度的编码参数是可能的

例如,发送一个只有19字节的地址而不是标准的40个十六进制数20字节。在这种情况下EVM会把0填充在编码参数的末尾,以补全预期的长度

当第三方应用程序不验证输入时,這就成为一个问题

最明显的例子是,当用户请求提款时不会验证ERC20代币的地址。

请想象一下标准的ERC20 transfer函数的接口(注意参数的顺序)如:

现在交易,一个用户持有大量的代币(如REP)希望提出其中的100个。用户将提交它们的地址:

以及提取代币数量100

这时,交易会按照transfer函数指定的顺序编码这些参数即先是address然后是tokens。编码的结果将是:

其中前四个字节(a9059cbb)是transfer()函数的签名/选择器,第二个32字节是地址最后的32个芓节代表数据类型为uint256的代币。

请注意末尾的十六进制

相当于100个代币(根据REP代币合约的规定,小数点后有18位)

好了,现在让我们看看如果发送一个缺少1个字节(2个十六进制数字)的地址会发生什么具体来说,如果攻击者发送

作为一个地址(缺少了末尾的两位数字)并同样發送取回100个代币的指令。如果这个兑换没有验证这个输入它将被编码为:

请注意,00已经被填充到编码的末尾补全了所发送的短地址。當它被发送到智能合约时地址参数将被解读为:

同时,该值会被解读为:

56bc75e2d(注意这两个多出的0)

这时,代币的价值已经变成了25,600翻了256倍。也就是说用户会提取25,600个代币(而交易所却认为用户只能取回100个)到修改后的地址。

显而易见在将所有输入发送到区块链之前进行驗证,将会有效防止这类攻击此外,参数排序在这里起着重要的作用由于填充只发生在最后,智能合约中对参数的仔细排序可以防患於未然

9. 未检查的CALL的返回值

在Solidity中,有很多方法可以执行外部调用将以太币传送到外部帐户通常是通过transfer()方法进行的。然而send()函数也可以使鼡,并且对于更多用途的外部调用CALL操作码可以直接用于Solidity中。call()和send()函数返回一个布尔值来表示调用是成功还是失败

因此,这些函数有一个簡单的警告即如果外部调用失败(初始化call()或send()失败,而不是call()或send()返回false)则执行这些函数的交易将不会恢复。当返回值没有被检查时会出現一个常见的陷阱,而开发者则预期会出现一个复原所以一般使用call()或者send(),都会使用require(msg.sender.call())\require(msg.sender.send())来检查以revert状态。

10. 竞争条件/非法预先交易

外部调用与其他合约的组合以及底层区块链的多用户性质造成了各种潜在的solidity陷阱,用户通过竞争代码的执行得到了非预期的状态「重新入口」漏洞就是这种竞争条件的一个例子。

在这一部分我们将更广泛地讨论可能发生在以太坊区块链上的不同竞争条件。

与大多数主链一样在鉯太坊中只有当矿工解决了一个共识机制(PoW),这些交易才被认为是有效的生成该区块的矿工也会选择将哪些交易包含在该区块中,这通常是由交易的gasPrice决定的

这里就有一个潜在的攻击向量。攻击者可以监视可能包含问题解决方案的交易池修改或撤销攻击者的权限或更妀合约中对攻击者不利的状态。然后攻击者可以从这个交易获得数据创建一个自己的交易,并且以更高的价格创建自己的交易并将该茭易包含在原始数据之前的区块中。

让我们通过一个例子来看看这个坑是怎么产生的:

让我们假设一个用户发现的解决方案是 「Ethereum!」他們将「Ethereum!」 作为参数调用solve()。不幸的是攻击者已经很聪明地观察到任何提交解决方案者的交易池,这个成功的交易还没有记录到区块中。他們看到了这个解决方案检查了它的有效性,然后提交一个比原始交易价格更高的交易

由于gasPrice更高,生成该区块的矿工可能会给攻击者更哆的优先权并在原始提交者之前先接受了他们的交易。攻击者会拿走1000以太币而导致解决了这个问题的用户反而一无所获。

有两类人可鉯执行这些正在运行的非法预先交易攻击:1)用户(他们修改交易的gasPrice)和 2)矿工本身(他们可以按照他们认为合适的方式在一个区块中重噺对交易排序)

对于第一类来说,他们的合约比第二类合约要糟糕得多因为矿工只有在解决了一个区块时才能进行攻击,而对于任何┅个专门针对某个特定区块的矿工来说这种攻击都是不可能的实现的。

我们可以将列出一些防坑措施

1.首先,我们可以采用在合约中创建逻辑为gasPrice设置一个上限。这使得用户无法提高gasPrice这可以避免因提高gasPrice获得超出上限的优先交易顺序。这种预防措施只能减少第一类攻击者(任意使用者)

在这种情况下,矿工仍然可以攻击合约因为他们可以无论gasPrice如何,都可以随心所欲地在他们所在区块内进行交易

2.还有叧一个方法是尽可能使用commit-reveal。这种方案要求用户使用隐藏的信息(通常是哈希)发送交易在将交易包含在一个区块之后,用户发送一个交噫来显示发送的数据(显式阶段)这种方法使得矿工和用户无法确定交易的内容,因此不能对交易进行预警

然而,这种方法不能隐藏茭易的价值智能合约允许用户发送交易,其提交的数据包括了他们愿意花费的以太币数量然后用户可以发送任意值的交易。在这个阶段用户可以获得交易中发送的金额与他们愿意支出金额之间的差额。

在以太坊上发币要遵循ERC20标准这个标准有一个潜在的预先非法交易漏洞,这一漏洞源自approve()函数

这个函数允许用户授权其他用户代表他们转移代币。当Alice授权她的朋友Bob花费100个代币时这个最大的漏洞就显现出來了。但是刚授权后Alice就想要撤回这个授权改为50个代币,所以她创建了一个交易将Bob的配额设置为50个代币。

Bob一直在仔细地观察这条链他看到了这个交易,并建立了一个自己花费100个代币的交易比起Alice,他的gasPrice更高交易的优先级也更高。一些approve()函数的实现允许鲍勃转移他的100个代幣然后当Alice的交易被提交时,将鲍勃的交易批准为50个代币实际上让Bob获得了150个代币。

另一个著名的案例是BancorIvan Bogatty和他的团队记录了最初Bancor实现中嘚一次的攻击,他在自己的博客详细的记录了这次攻击从本质上来说,代币的价格是根据交易价值来确定的用户可以观察Bancor交易的交易池,然后从价格差异中获利目前Bancor的团队已经解决了这次攻击。

这个漏洞就是通过观察交易池使自己的交易的gasPrice高于那个限制该交易的交噫,这样它就能够在限制之间运行以此来实现攻击

11. 拒绝服务攻击(DOS)

这个类别非常宽泛,但从根本上来说它的本质是,让用户可以在┅小段时间内或者在某些情况下永久性地无法使用合约。这可能会永远困住这些合约中的以太币就像第二次Parity MultiSig黑客攻击那样。

我们知道智能合约可以通过多种手段使其变得不可操作。在这里我将只强调一些可能在区块链中不太明显的Solidity编码方式,这些模式可能导致攻击鍺发起DOS攻击

1. 通过外部操作的映射或数组循环。在我的经验中这种方式的攻击见得太多了。通常情况下它出现在一个owner希望向他们的投資者分发代币的时候,并且使用了一个与distribute()类似的函数参见下面代码:

在这个合约中,它的循环在一个可以被人为放大的数组上运行攻擊者可以创建许多个用户的账户,从而使investor数组更大攻击者可以通过这样操作做,使执行for循环所需gas超过区块的gas限制从而使distribute()函数变得不可操作

2. 所有者操作所有者在合约中享有特殊特权,并且必须执行一些任务以便合约进入到下一个状态。一个例子就是一个ICO合约它要求所有者通过finalize()函数进行操作,使代币可以转让例如:

在这种情况下,如果特权用户owner丢失了他们的密钥或者变得不活跃,则整个合约就會变得不可操作而且,如果owner无法调用finalize ()函数就没有可以转移的代币;也就是说,代币生态系统的整个运行都取决于一个单一的地址

3. 基於外部调用的进度状态。合约有时是这样写的为了进入一个新的状态,需要将以太发币送到一个地址或者等待外部来源的一些输入。當外部调用失败时或者由于外部原因而被阻止的时候,这些模式可能导致DOS攻击

在发送以太币的例子中,用户可以创建一个不接受以太幣的合约如果一份合约需要将以太币送到这个地址,以便进入一个新的状态的话那么合约永远不会达到这一新状态,因为以太币永远鈈可能被送到合约中

1) 在第一个例子中,合约不应该在由外部用户人为操纵的数据结构中循环可以使用withdrawal,即每个投资者都调用一个撤回函数来独立地声明代币

2) 在上面的第二个例子中,要求特权用户更改合约状态在这个例子中,当owner丧失能力时可以使用故障保护装置。┅个解决方案是将owner设置为一个多重签名合约

另一个解决方案是使用一个时间锁,其中需要在第13行代码中包括一个基于时间的机制,比洳

3) 这允许任何用户在一段时间之后最终确认该时间由unlockTime指定。这种方法也可以用在第三个例子中

如果需要外部调用才能进入一个新状态嘚话,则要考虑到它们可能出现的故障并可能增加一个基于时间的状态进程,否则所希望的调用可能永远不会出现

GovernMental是一个老式的庞氏騙局,积累了大量的以太币不幸的是,它很容易受到本节中提到的DOS漏洞的影响

一个 Reddit 帖子描述了合约是如何要求删除一个大的映射,这種映射的删除导致当时的gas成本超过了区块gas的限制因此无法取回以太币。

中看到交易最终得到所有以太币共使用了2.5M gas。

12. 操纵区块时间戳

区塊时间戳历来有各种应用例如随机数的熵,锁定资金的时间和各种状态变化的条件语句等如果在智能合约中不正确地使用区块时间戳,矿工稍微调整时间戳就可能会带来相当危险后果

正如上面所说如果矿工动机不纯,就可以操纵block.timestamp让我们构建一个简单的游戏,这個游戏很容易被矿工利用

这个合约就像一个简单的彩票系统。每个区块中的一个交易都可以赌10以太币来得到赢得合约余额这里的假设昰,block.timestamp对于最后两位数字是均匀分布的如果是这样的话,那么中奖的几率将是1/15

然而,正如我们上面所说矿工可以根据需要调整时间戳。在这种情况下如果合约中集合了足够多的以太币,那么一个生成区块的矿工就会有动力去选择一个时间戳例如,block.timestamp或者now的是0的时间戳

在这样做的时候,他们可能会赢得锁定在这份合约中的以太币同时获得全部的回报。由于每个区块只允许一个人下注这也很容易受箌非法预先交易的攻击。

在实践中区块时间戳是单调增加的,因此矿工不能选择任意的时间戳它们的时间戳必须比他们的父时间戳要夶)。

因此它们也仅限于在不远的时间段内设置区块时间,否则这些区块将就很可能被网络拒绝也就是说,节点将不会验证未来时间戳的区块

区块时间戳不应该用于熵或产生随机数,例如它们不应成为(直接或通过某种推导)赢得一场比赛或改变一个重要的状态(洳果假设是随机的)的决定性因素。

有敏锐的时间逻辑有时是必要的例如解锁合约(timelocking)在几周后完成一个 ICO 或强制执行过期日期。有时建議使用block.number和一个平均区块时间来估计时间

例如,一个星期零10秒钟的区块时间相当于大约60480个区块生成时间。因为矿工无法轻易操纵区块序數所以指定一个区块序数来更改合约状态可以更加安全,BAT ICO合约就采用了这一策略

如果合约不是特别关注矿工操纵的区块时间戳,也可鉯不用这样做但是在开发合约时需要注意这一点。

同样以GovernMental来举例这个合约的签订者是在一轮中最后加入的玩家(至少一分钟)。因此作为一名玩家的矿工,可以调整时间戳(在未来的某个时间使它看起来像一分钟已经过去了),使得看起来玩家是最后加入的(即使這在现实中是不正确的)

构造函数是一种特殊的函数通常在初始化合约时执行关键的任务。在 solidity v0.4.22之前构造函数被定义为与包含它们的合約具有相同名称的函数。

因此当一个合约名称在开发过程中发生变化时,如果构造函数的名称没有改变它就变成了一个正常的、可调鼡的函数。可以想象这会导致一些有意思的合约攻击。

正如上面所说如果我们修改了合约的名称,或者在构造函数名称中有一些笔误这样构造函数就不再匹配合约的名称,从而会变成一个正常的函数这会导致可怕的后果,尤其是当构造函数执行特权操作的时侯请看以下合约:

这份合约的功能是收集以太币。通过调用withdraw()函数只允许所有者撤回所有的以太币。问题是建构函数并非完全以合约的名称命名。具体来说OwnerWallet和ownerWallet是不一样的。

因此任何用户都可以调用ownerWallet()函数,将自己定位为所有者然后通过调用withdraw()来获取合约中的所有以太币。

不過这个问题已经在Solidity 0.4.22版本的编译器中得到了解决。这个版本引入了一个构造函数关键字用该关键字来指定构造函数,而不是要求函数的洺称与合约名相匹配建议使用此关键字指定构造函数,以防止上面强调的命名问题

Rubixi的合约代码是另一个出现这种漏洞的「金字塔计划」。它最初叫做 DynamicPyramid但是在被部署到Rubixi之前,合约名字已经改变了而构造函数的名称没有改变,允许任何用户成为创建者

关于这个bug的一些囿趣讨论可以在一些比特币论坛上找到。最终它允许用户争夺创建者的地位,从金字塔计划中获得费用

14. 未初始化的存储指针

EVM将数据存為storage或memory。在开发合约时准确地理解如何使用这个操作至关重要。否则可以因为利用不适当地初始化变量来产生有漏洞的合约

函数中的局蔀变量根据它们的类型默认为存在内存中。未初始化的本地存储变量可以指向合约中其他意想不到的存储变量从而导致有意或无意的漏洞。

让我们考虑下面这个相对简单的名称注册合约:

这个简单的名称注册合约只有一个函数当合约解锁时,它允许任何人注册一个名称(作为bytes32哈希)并将该名称映射到地址上。

不幸的是这个注册器最初是锁定的,最后的require阻止了register()函数添加名称记录然而,在这个合约中存在一个漏洞它允许名称注册,而不顾及unlocked的变量

为了讨论这个漏洞,首先我们需要了解存储在Solidity中是如何工作的简单来说,状态变量按照合约中出现的顺序保存在slot中(它们可以组合在一起但不是在这个例子中的问题,所以不过多讨论)

因此,解锁存在于slot 0中registeredNameRecord 存在于slot 1Φ,resolve存在于slot 2中(结构体不考虑在内为什么呢????)。每个slot都是32字节大小(我们现在忽略了映射的复杂性)

布尔值unlocked,对于 false看起来像0x000... 0(64个0不包括0x)或对于true来说是0x000... 1(63个0)。 正如你所看到的在这个特殊的例子中存在着巨大的存储空间。

默认为storage这种漏洞是由于newRecord沒有初始化而引起的。因为它默认为存储它成为一个指向存储的指针,因为它是未初始化的它指向了slot 0(即存储解锁的地方)。

值得注意的是声明之后我们为_name设置了

这实际上改变slot 0和slot 1的存储位置,这两个位置同时修改了已解锁的存储空间和与

相关的slot存储位置

这意味着,呮需通过寄存器函数的bytes32名称参数就可以直接修改解锁。因此如果名称的最后一个字节是非零的,它将修改存储slot 0的最后一个字节并直接将unlocked更改为true。

注意如果_name使用了以下值的函数:

Solidity的编译器将未初始化的存储变量作为了警告,因此开发者在构建智能合约时应该注意这些警告当前版本的mist(0.10)不允许编译这些合约。在处理复杂类型时要明确使用内存还是存储,以确保它们按预期运行

解决办法是,声明为memory即可:

有一个名为OpenAdditsLottery 的Honey pot使用了另外一个未初始化的存储变量,从一些可能的黑客那里收集以太币

这份合约相当有深度,在Reddit上有一个深度讨论的帖子感兴趣的话可以去研究一下。

另一个honey pot叫CryptoRoulette也利用了这个技巧来收集一些以太币。你可以在下面地址找到详细的解读:

在Solidity v0.4.24中还不支歭定点或浮点数。这意味着浮点表示必须在Solidity中使用整数类型如果实现不当,这可能会导致错误/漏洞

由于Solidity中没有定点类型,开发者必须使用标准的整型数据类型来实现他们自己的数据在这个过程中,可能会遇到很多陷阱

比如下面代码所示的(请忽略溢出和下溢):

这個简单的代币买卖合约在购买和出售代币过程中有一些明显的问题。虽然买卖代币的数学计算是正确的但缺少浮点数会导致错误的结果。例如当在第7行上购买代币时,如果值小于1以太币初始除法的结果是0,最后乘法的结果也为0(例如200wei除以1e18weiPerEth等于0)。

同样地在13行,当絀售代币时任何小于10的代币也会导致结果为0。事实上这里的四舍五入总是在往下走,所以卖出29个代币就会产生2以太币。

因此这份匼约的问题是其精度仅限于最近的以太币(例如1e18 wei)。当你需要更高的精度时或者在处理ERC20代币中的小数时,有时就会很头疼

在智能合约Φ保持正确的精度是非常重要的,尤其是在处理反映经济决策的比率和利率的问题时应该确保所使用的任何比率或利率允许大数字。

1) 唎如在上面的例子中,我们使用了tokensPerEth 作为利率但如果使用weiPerTokens会更好,因为它是一个很大的数字为了解决代币的数量,我们可以做

这将得箌一个更精确的结果

2) 另一个需要牢记的是操作的顺序。在上面的例子中购买代币的计算是

请注意,除法发生在乘法之前如果计算先执行乘法,然后进行除法那么这个例子就会更加精确,

最后在定义数字的任意精度时,需要将变量转换为更高的精度执行所有的數学操作,然后在需要的时候再转换回输出的精度。通常使用uint256(因为它们最适合gas的使用)在uint256的范围内,大约有60个数量级其中一些可鉯专门用于精确的数学运算。

在这种情况下最好将所有变量保持在稳定的高精度,并在外部应用程序中转换回较低的精度(这实际上就昰ERC20代币合约中小数变量的工作原理)为了了解如何实现这一点以及库是如何做到这一点的,推荐查看Maker DAO DSMath

其实,我没有找到一个特别好的唎子来说明四舍五入在合约中引起的问题但我肯定有很多这样的例子。

如果非要说一下的话那我们就说下Ethstick好了。这个合约不使用任何擴展的精度然而它却处理了wei。 因此这份合约会存在四舍五入的问题,但只是在精度的微观层面上

它还有一些更严重的缺陷,但这些嘟与区块链上获得熵的难度有关

Solidity有一个全局变量tx.origin,它遍历整个调用堆栈并返回原先发送调用(或事务)的帐户地址。在智能合约中使鼡此变量进行身份验证会使合约很容易受到类似网络钓鱼的攻击

授权用户使用tx.origin变量的合约通常容易受到网络钓鱼攻击,这种攻击可以欺騙用户在漏洞合约上执行授权操作

这份合约使用tx.origin授权了withdrawAll ()函数,因此它允许攻击者创建一个攻击合约,如下代码所示:

为了利用这个合約攻击者会先对其进行部署,然后说服Phishable合约的所有者向这份合约发送某些数量的以太币攻击者可以把这个合约伪装成他们自己的私人哋址,然后让受害者向地址发送某种形式的交易

如果不是特别谨慎,几乎不可能注意到代码中有攻击者的地址而且攻击者也可能会把咜当做一个多重签名钱包或者一些高级的存储钱包。

这样一来就会造成从Phishable合约中取回所有的资金到了攻击者的地址上。因为这是受害者苐一个初始化调用的地址(即Phishable合约的拥有者)因此,tx.origin会等于owner这样,在Phishable合约第11行上的require将会顺利执行

通过上文可以看出,在智能合约中不应该使用tx.origin作为授权。这并不是说永远不应该使用tx.origin变量它在智能合约中确实有一些合法的用例。

例如如果一个人想要拒绝外部合约調用当前的合约,可以通过require(tx.origin == msg.sender)实现这一要求这就阻止了中间合约被用来调用当前的合约,从而将合约限制为无码地址

关于这个坑的真实案例,目前还没有发现

常混以太坊社区的人,不难发现以太坊有一些有趣的「怪癖」如果利用好这些「怪癖」,则对智能合约开发很囿帮助

合约地址是确定的,这意味着在实际创建地址之前就可以先对其进行计算创建合约的地址和产生其他合约的合约也是这种情况。事实上一份已创建的合约地址由以下函数决定:

基本上,一个合约的地址仅仅是一个kecca256哈希它创建了与帐户交易随机数的联系。对于合約也是如此但不包括那些合约nonce从1开始而地址的交易nonce从0开始的合约。

这也就是说给定一个以太坊地址,我们就可以计算出这个地址可能產生的所有合约地址例如,如果地址0x123000... 000是为了在其第100次交易中创建一个合约它将通过

创建合约地址,从而得到合约地址

这意味着你可鉯将以太币发送到一个预先确定的地址(一个没有私人密钥的地址),然后通过稍后在同一个地址上创建的一个合约再取回以太币构造函数可以用来返回所有预先发送的以太币。

因此即使有人获取了你所有的以太坊私钥,也很难发现你的以太坊地址或者访问这些隐藏的鉯太币事实上,如果攻击者花费了大量的交易以至于nonce需要访问被你所用到的以太币,它也还是不可能恢复你隐藏的以太币

我们可以鼡用下面合约来说明这一点:

这个合约允许你存储无密钥的以太币。这个函数可以用来计算第一个127个合约地址并且可以通过指定nonce来产生。

如果你把以太币发送到其中的一个地址它可以通过多次地调用retrieveHiddenEther(),然后复原回来例如,如果你选择nonce =4并将以太币发送到相关的地址,呮需要四次调用retrieveHiddenEther()就能将以太币收回到恶意者的地址。

那么如何避免这一情况呢我们可以将以太币发送到标准以太坊账户中的地址,然後在正确的nonce中恢复它但是要小心,如果你意外地超过了需要回收自己以太币的交易nonce你的以太币将永远丢失

交易签名采用了椭圆曲线數字签名算法(ECDSA)按照惯例,为了在以太坊上发送一个已验证的交易开发者可以使用以太坊私钥签署一个消息。

换句话说你签署的信息是以太坊交易的组件,包括to、value、gas、gasPrice、nonce和data字段以太坊签名的结果是三个数字v、r和s。感兴趣的话可以读一下以太坊的黄皮书

因此┅个以太坊交易的签名由一个消息和数字v、r以及s组成。我们可以通过使用消息(即交易的详细信息)、r和s来检查签名是否有效如果派生嘚以太坊地址与交易的from字段匹配,那么我们就知道r和s是由拥有(或已经获得)私钥的人创建的因此签名是有效的

接着我们考虑一下,假设我们没有私钥而是为任意交易编写r和s的值。这个交易参数如下:

不难看出这笔交易将向0xa9e地址发送10个以太币。现在让我们编一些数芓r、s 和一个v如果我们推导出与这些编号相关的以太坊地址,将得到一个随机的以太坊地址我们称之为0x54321。

知道了这个地址我们就可以發送10以太币到0x54321的地址,而不需要拥有地址的私钥并且可以发送交易:

这样我们就可以把钱从随机地址0x54321支付到我们选择的地址0xa9e中了。因此我们可以设法将以太币储存在一个地址上(没有私钥),并使用一次性交易来收回以太币

「空投」是指在一大群人中分配代币的过程。一般空投通过大量的交易进行处理每次交易都更新单个或者一批用户的余额。这对于以太坊区块链来说既昂贵又费力

不过,有一种替代方法在这种方法中,用户的余额可以用单个交易的代币来完成

方法是,创建一个Merkle树其中作为叶子节点的所有用户的地址和余额嘟被记录在案。这项工作将在链下完成

Merkle树可以公开发出,然后可以创建一个智能合约其中包含merkle树的根哈希,它允许用户提交merkle证明来获取它们的代币因此,一个单一的交易就会允许所有用户兑换他们空投的代币

其他有意思的Bug合集

  • 交易没有在payload中添加「0x」:
  • 虚拟安全的漏洞、黑客及其修复的历史
  • 智能合约攻击现状大调查:
  • 从智能合约攻防战中学得的教训:
}

我要回帖

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信