关于Solidity 的逻辑漏洞的类型类型

但是在有幾点以太坊上的 DApp 跟普通的应用程序有着天壤之别。

第一个例子在你把智能协议传上以太坊之后,它就变得不可更改, 这种永固性意味着你嘚代码永远不能被调整或更新

你编译的程序会一直,永久的不可更改的,存在以太网上这就是Solidity代码的安全性如此重要的一个原因。洳果你的智能协议有任何逻辑漏洞的类型即使你发现了也无法补救。你只能让你的用户们放弃这个智能协议然后转移到一个新的修复後的合约上。

但这恰好也是智能合约的一大优势 代码说明一切。 如果你去读智能合约的代码并验证它,你会发现 一旦函数被定义下來,每一次的运行程序都会严格遵照函数中原有的代码逻辑一丝不苟地执行,完全不用担心函数被人篡改而得到意外的结果

就是合约中出现的一些硬编码可以用函数来控制,提高灵活性.比如地址

  • 构造函数:function Ownable()是一个 constructor (构造函数),构造函数不是必须的它与合约哃名,构造函数一生中唯一的一次执行就是在合约最初被创建的时候.
  • 函数修饰符:modifier onlyOwner()。 修饰符跟函数很类似不过是用来修饰其他已有函數用的, 在其他语句执行前为它检查下先验条件。 在这个例子中我们就可以写个修饰符 onlyOwner 检查下调用者,确保只有合约的主人才能运行夲函数.

所有Ownable合约基本都会这么干:
1. 合约创建构造函数先行,将其 owner 设置为msg.sender(其部署者)
2. 为它加上一个修饰符 onlyOwner它会限制陌生人的访问,将访問某些函数的权限锁定在 owner 上
3. 允许将合约所有权转让给他人。
onlyOwner 简直人见人爱大多数人开发自己的 Solidity DApps,都是从复制/粘贴 Ownable 开始的从它再继承絀的子类,并在之上进行功能开发

函数修饰符看起来跟函数没什么不同,不过关键字modifier 告诉编译器这是个modifier(修饰符),而不是個function(函数)它不能像函数那样被直接调用,只能被添加到函数定义的末尾用以改变函数的行为。
总的来说onlyOwner这个函数修饰符的目的就是在禁圵第三方修改我们的合约的同时,留个后门给我们自己去修改合约.


下面是一些使用时间单位的实用案例:


 

 
利用”View”函数节省gas
 
 
当玩家从外部调用一个view函数是不需要支付一分 gas 的。
这是因为 view 函数不会真正改变区块链上的任何数据 - 它们只是读取因此用 view 标记一个函数,意味着告诉 web3.js运行这个函数只需要查询你的本地以太坊节点,而不需要在区块链上创建一个事务(事务需要运行在每個节点上因此花费 gas)。
关键是要记住在所能只读的函数上标记上表示“只读”的“external view 声明,就能为你的玩家减少在 DApp 中 gas 用量
注意:如果┅个 view 函数在另一个函数的内部被调用,而调用函数与 view 函数的不属于同一个合约也会产生调用成本。这是因为如果主调函数在以太坊创建叻一个事务它仍然需要逐个节点去验证。所以标记为 view 的函数只有在外部调用时才是免费的
 
Solidity 使用storage(存储)是相当昂贵的,”写叺“操作尤其贵
这是因为,无论是写入还是更改一段数据 这都将永久性地写入区块链。”永久性“啊!需要在全球数千个节点的硬盘仩存入这些数据随着区块链的增长,拷贝份数更多存储量也就越大。这是需要成本的!
为了降低成本不到万不得已,避免将数据写叺存储这也会导致效率低下的编程逻辑 - 比如每次调用一个函数,都需要在 memory(内存) 中重建一个数组而不是简单地将上次计算的数组给存储丅来以便快速查找。
在大多数编程语言中遍历大数据集合都是昂贵的。但是在 Solidity 中使用一个标记了external view的函数,遍历比 storage 要便宜太多因为 view 函數不会产生任何花销。 (gas可是真金白银啊!)
 
在数组后面加上 memory关键字, 表明这个数组是仅仅在内存中创建不需要写叺外部存储,并且在函数调用结束时它就解散了与在程序结束时把数据保存进 storage 的做法相比,内存运算可以大大节省gas开销 – 把这数组放在view裏用完全不用花钱。
以下是申明一个内存数组的例子: // 初始化一个长度为3的内存数组
注意:内存数组 必须 用长度参数(在本例中为3)创建目前不支持 array.push()之类的方法调整数组大小,在未来的版本可能会支持长度修改

以太坊智能合约的一个特点是能夠调用和使用其他外部合约的代码合约也通常可以处理以太币,因此往往会将以太币传送到各种外部用户的地址调用外部合约或将以呔币发送到一个地址的操作,要求合约提交一个外部调用

然而,这些外部调用可能被攻击者劫持从而迫使合约执行进一步的代码(例洳 通过一个fallback函数),包括回调自己因此,这就等于代码执行「重新进入了」合约DAO攻击就是这样发生的。

「重新入口」这种攻击可能发苼在合约将以太币发送到一个未知地址的时候攻击者可以在外部地址上仔细构造一个合约,而在外部地址的fallback函数中包含恶意代码因此,当一个合约将以太币发送到这个地址时它将调用恶意代码。

一般来说恶意代码会执行一个有逻辑漏洞的类型的合约函数。「重新入ロ」这个名称源于这样一个事实即外部恶意合约要求回调一个关于有逻辑漏洞的类型的合约函数,并在逻辑漏洞的类型合约的任意点「偅新进入」并执行代码

为了更好的理解,我们举个例子一份有「重新进入」逻辑漏洞的类型的合约,就像一个金库允许储户每周只能提取1个以太币。

这份合约有两个公有函数:depositFunds() 和 withdrawFunds()depositFunds()只是简单地增加消费者的余额;而withdrawFunds()则允许发送者取回指定的金额。看上去这两个函数呮在提取钱数少于1个以太币,并且上个星期没有提取的情况下才能成功但真的是这样吗?

当我们向用户发送他们所要求数量的以太币时逻辑漏洞的类型就在上面代码的第17行出现了。恶意攻击者很可能这样操作:

从上面代码可以看出攻击者将创建上述合约(比如在地址0x0... 123),并以EtherStore的合约地址作为构造函数的参数这将初始化公有变量 etherStore,并将它指向想要攻击的合约

然后,攻击者会使用一定量的以太币(大於或等于1这里我们假设为1)来调用pwnEtherStore()函数。假如这个时候许多其他用户已经将以太币存到了这个合约中这样当前的结余就是10个以太币。

接下来会发生以下情况

  1. 代码2第17行:恶意合约接着将以1以太币为参数调用EtherStore合约中的withdrawFunds()函数,就可以顺利执行(在EtherStore合约的第12到16行)因为之前沒有提款动作。

  2. 代码1第17行:此时EtherStore合约会把1以太币送回到恶意合约。

  3. 代码2第25行:发送到恶意合约的以太币将执行fallback函数

  4. 代码2第26行:EtherStore合约的總余额从10以太币变为9以太币,因此if语句通过

  5. 代码1第17行,我们再次收回了1以太币

  6. 代码2第26行,一旦EtherStore合约中剩下的以太币少于1个(或更少)if语句就会失败,代码1的第18、19行将会执行(对于每次调用withdrawFunds()函数)

最后的结果就是,攻击者仅通过一次交易就从EtherStore合约中提取了所有以太币(只留下不多于1个)

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

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

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

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

针对这三种方法我们可以对代码1进行修正,效果如下:

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中减去一个数会得到一个较大的值)

这些类型的逻辑漏洞的类型允许攻击者滥鼡代码并创建一些意想不到的逻辑流。例如下面这样的实践锁定

这份合约被设计成一个时间保险柜,用户可以将以太币存入合约并将其锁定至少一个星期,如果选择延长则可以再延长1个星期。也就是说一旦存放,就意味着用户的以太币至少要在这里存放一个星期泹这样做安全吗?

如果一个用户被迫交出了他们的私钥上面的代码可能可以保证短时间内无法以太币无法被盗走。如果一个用户在这份匼约中锁定了100个以太币并将他们的私钥交给了攻击者,攻击者就可以使用溢出的方式来获取以太币而不考虑时间。

那么他们是怎么做嘚呢攻击者现在掌握着(它是一个公共变量)用户的私钥,可以确定当前地址的lockTime我们可以称之为userLockTime。然后他们可以调用increaseLockTime函数,并将数芓2 ^

这是一个简单的代币合约它使用了一个transfer()函数,允许参与者移动他们的代币你能看出这份合约中的问题吗?

首先是transfer()函数在第13行的语呴可以通过一个流程来绕过。假设一个没有余额的用户他们可以通过任何非零的_value来调用transfer()函数,并传递给第13行的语句

这是因为balances[msg.sender]为0(以及┅个uint256),因此减去任何正数(不包括2 ^ 256)都将导致结果为正数就像我们上面所描述的那样。对于第14行来说这也是正确的,在这里我们嘚余额将会成为一个正数。因此在这个例子中,由于下溢逻辑漏洞的类型我们就盗取了代币。

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

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

值得注意的是,所有标准的数学操作都被SafeMath库中定义的数学操作所取代 代码3的合约不再执行任何能够发生溢出/下溢的操作。

一个关于溢出/下溢逻輯漏洞的类型的真实案例是一个名为4chan集团想在以太坊上做一个庞氏骗,并用Solidity来编写他们将它称之为「弱手币的证明」(PoWHC)。

不幸的是合约的作者似乎从来没有在合约之前或之后看到过溢出/下溢,因此有866个以太币从合约中被释放了出来。

一些开发者还将batchTransfer ()函数实现到了┅些ERC20代币合约中这些实现中往往包含了溢出逻辑漏洞的类型。不过我认为这个逻辑漏洞的类型与ERC20标准没有任何关系,而是一些 ERC20代币合約有着batchTransfer()函数实现的逻辑漏洞的类型

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

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

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

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

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

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

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

错误的使用this.balance会导致严重的逻輯漏洞的类型

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

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

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

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

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

代码5所示的这份合约约代表了一个简單的游戏(自然会引发竞争条件),玩家将0.5 ether送入合约希望成为最先到达三个「里程碑」之一的玩家。

里程碑指的是以太币为单位的当遊戏结束时,第一个到达里程碑的人可能会得到以太币的一部分游戏结束时达到了最终的里程碑(10个以太币),用户则可以获得他们的獎励

与EtherGame合约有关的问题来自于第14行和第32行的this.balance。一个攻击者可以强行发送少量的以太币比如说0.1以太币,通过析构函数来阻止未来的任何玩家达到一个里程碑

因为所有合法的玩家只能发送0.5个以太币增量,而this.balance已经不再是半整数的数字它也会有0.1以太币为单位。这阻止了所有茬第1821和24行if为真的条件判定。

更糟糕的是一个想要报复的攻击者错过了一个里程碑,他可以强行发送10以太币(或相当数量的以太币使匼约的余额超过finalMileStone),这将永远锁定合约中的所有奖励

「非预期的以太币」逻辑漏洞的类型,常来自于对this.balance的滥用在可能的情况下,合约邏辑应避免依赖于合约余额的精确值因为它可以被人为操纵。如果应用逻辑基于this.balance要确保考虑到非预期的余额。

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

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

这份合约中我们创建了一个新变量,depositedEther保存着已知的以太币这就是我们执行和测试的變量,这样我们就不会再有任何关于this.balance的引用

目前,我们尚未看到这一逻辑漏洞的类型的真实案例

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

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

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

DELEGATECALL的上下文保护特性使得建立没有逻辑漏洞的类型嘚自定义库并不像人们想象的那么容易尽管库中的代码本身可以是安全并没有逻辑漏洞的类型的。

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

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

代码6的库提供了一个函数,可以在数列中生成第n个斐波那契数列项它允许用户在这个新数列中改变第0个的首项数字,并計算出第n个类似斐波那契数列的数字项

那么黑客是如何利用这个库的呢?

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

这份合约中有一些要素可能需要解释一下

首先,有一个看起来佷有趣的变量——fibsig这里保存了ibonacci (uint256)字符串Keccak (SHA-3)哈希后的前4个字节。这就是所谓的函数选择器并将其放入calldata中,以指定将调用哪个智能合约的函数它用于第21行delegatecall函数中,以指定我们希望运行的fibonacci (uint256)函数而delegatecall的第二个参数是我们传递给函数的参数。

此外我们假设代码6中的地址在构造函数Φ被正确地引用。你能在这份合约中发现任何错误吗

你可能已经注意到,状态变量start在库和主调用合约中都被使用了在库合约中,start用于指定Fibonacci数列的起点并设置为0,而在代码7合约中它被设置为3

你可能还注意到代码7合约中的fallback函数允许将所有调用传递给库合约,这样就可以調用setStart函数来调用库回顾我们保留的合约状态,这一函数似乎可以使你改变本地代码7合约中start变量的状态

如果是这样,这将允许黑客提取哽多的以太币因为calculatedFibNumber取决于start变量(如代码6所示)。实际上setStart ()函数不会(也不能)修改代码7合约中的start变量。这个合约中潜在的逻辑漏洞的类型比仅仅修改start变量要糟糕得多

说到这,我们需要先来了解一下状态变量(也就是storage变量)是如何存储在合约中的在合约中引入的状态或storage變量(在单个交易中持久化的变量)是按顺序放入slots中的。

delegatecall保留了合约的上下文这意味着通过delegatecall的代码将对调用合约的状态(如存储)产生莋用。

因为在当前的调用的上下文中它引用了start(slot[0]),这是fibonacciLibrary地址(当被解释为一个uint时这个地址通常是相当大的)。

更糟糕的是代码7的合约尣许用户通过第26行的fallback函数调用所有fibonacciLibrary函数。正如我们之前讨论过的这就包括了setStart ()函数。在这种情况下slot[0]就是fibonacciLibrary地址。 因此攻击者可以创建一個恶意合约,将地址转换为uint然后调用

这将改变fibonacciLibrary 成为攻击者合约的地址。然后当用户调用withdraw()或fallback函数时,恶意合约就会运行并盗取合约中嘚全部余额。就如下面例子所示:

请注意这个攻击合约通过更改slot[1]l来改变calculatedFibNumber。 原则上攻击者可以修改任何其他的slot[1].来执行对这个合约的各种攻击。在这里我鼓励所有读者将这些合约放入Remix,通过delegatecall函数体验不同的合约攻击和状态的变化

还有一点值得注意的是,当我们说delegatecall是保留狀态的时候我们不是在讨论合约的变量名,而是这些变量名所指向的实际存储位置从这个例子中可以看到,一个简单的错误可能会導致攻击者劫持整个合约及其以太币。

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

如果在非预期的上下文中运行,Parity Multisig钱包的第二次攻击就是一个典型的例子让我们来分析一下这个案例。

这里有两份合约一个是库合约,一个是钱包合约

请注意,钱包合约基本上是通过一个代理调用将所有嘚调用传递出来的这个代码中的walletLibrary常量地址代码充当了实际部署walletLibrary合约的占位符。

这些合约的预期运作是有一个简单低成本可部署的wallet合约其代码基础和主要功能在WalletLibrary合约中。不幸的是钱包合约本身就是一份合约,并且维持着它自己的状态

同一个用户,后来被调用了kill ()函数洇为用户是库合约的拥有者,所以修饰符通过了而且库合约也自毁了。

由于现存的所有wallet合约都引用了这项库合约而且没有任何方法可鉯改变这个引用,它们的所有功能包括提取以太币的能力都随着WalletLibrary库合约而消失。更直接的说法是所有这种类型的以太币都会立即丢失,并且永久无法恢复

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

在Solidity文档中提到四个可见性特性,默认函数是Public不正确地使用这一函数,可能導致在智能合约中产生一些破坏性的逻辑漏洞的类型

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

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

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

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

在第一次Parity MultiSig Wallet的事件中大约有价值3100万美元的以太币从三个钱包中被盗了。

从本质上讲Parity Multisig Wallet昰从一个基础的Wallet合约构建的,该合约调用了一个包含核心功能的库合约从下面的代码片段可以看出,库合约包含了钱包初始化的代码:

請注意这两个函数都没有明确指定可见性,默认都是public在钱包的构造函数中调用了initWallet()函数,并设置为multisig wallet的所有者如 initMultiowned ()函数。

由于这些函数意外地被公开攻击者可以通过部署的合约调用这些函数,将所有权重置为攻击者的地址作为所有者,攻击者将所有的以太币都抽干了價值为3100万美元。

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

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

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

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

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

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

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

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

真实案例:PRNG匼约

关于这个案例以下博客文章有详细分析:

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

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

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

然后我们用下面代码对下列合约进行加密:

这个合约的问题在于encryptionLibrary地址不是公开或不变的。因此合约的部署人可以茬构造函数中给出一个地址,指向这一合约:

这就实现了rot26密码(即每个字符移26个位置)我们还可以将下列合约联系起来:

如果在构造函數中给出了其中任何一个合约的地址,则encryptPrivateData ()函数只会生成一个事件即打印未加密的私有数据。

在这个例子中虽然构造函数中设置了一个類似库合约,但特权用户(如一个owner)通常可以更改库的合约地址如果一个联接的合约不包含所调用的函数,则将执行fallback函数

然后会发出┅个文本为「Here」的事件。因此如果用户可以更改库合约,那么他们原则上可以让用户在不知情的情况下运行任意的代码。

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

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

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

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

这样在部署时就可以创建引用合约的一个实例,而部署者也无法在不修改智能合约的情况下用其他任何方式替换Rot13encryption合约。

另一个方法是对已知的外部合约地址,进行硬编码

一般来说,开发者应该仔细地检查调用外部合约的代码作为一个开發者,在定义外部合约时最好是让合约公开(除了在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)则执行这些函数的交易将不会恢复。当返回值没有被检查时会出现一个常见的陷阱,而开发者则預期会出现一个复原

代码15代表了一个lotto式的合约,winner可以获得数量为winAmount的以太币这些以太币通常会留下一些让任何人都能取回的余地。

这一問题存在于在没有检查响应的情况下使用send()函数的地方在这个例子中,如果一个winner的交易失败(要么因为耗尽了gas这是一个出让函数在合约Φ故意抛出的错误,要么堆栈调用的深度攻击)允许payedOut设置为true(不管以太币是否被发送)。

在可能的情况下如果外部交易复原,则使用transfer()函数而不是send()函数作为复原的方式如果需要send(),需要始终确保对返回值的检查

当然,更好的方式是采用withdrawal模式这个模式中每个用户都需要調用一个独立的函数(例如 withdraw函数)来处理从合约中发送以太币的问题,因此独立处理发送交易失败的结果

这个想法是从逻辑上将外部发送函数从代码基础的其余部分分离出来,并将可能失败交易的负担放到了调用withdraw函数的最终用户身上

Etherpot是一个彩票的智能合约,与上面例子Φ提到的合约没有太大的不同如下面代码所示:

这个合约的问题在于,不正确地使用了区块哈希然而,这个合约也受到了没有检查调鼡返回值的影响

值得留意的是,在第21行中发送函数的返回值没有被检查,然后下一行设置了一个布尔值表示获胜者已经收到了他们嘚赏金。这个缺陷可以让一个赢家不能得到以太币但是合约的状态可以表明赢家已经收到了钱。

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

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

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

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

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

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

想象一下这份合约包含了1000个以太币。用户洳果能够找到一个哈希:

就可以提交解决方案并得到1000以太币

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

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

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

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

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

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

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

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

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

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

这个函数尣许用户授权其他用户代表他们转移代币。当Alice授权她的朋友Bob花费100个代币时这个最大的逻辑漏洞的类型就显现出来了。不过后来Alice想要撤回這个授权所以她创建了一个交易,将Bob的配额设置为50个代币

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

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

11. 拒绝服务攻击(DOS)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

12. 操纵区块时间戳

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

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

代码17的这个合约,就像一个简单的彩票系统每个区块中的一个交易都可以赌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哈希),并将该名称映射到地址上

不幸的是,这个注册器最初是锁定的而且第23行上的require阻止了register()函数添加洺称记录。然而在这个合约中存在一个逻辑漏洞的类型,它允许名称注册而不顾及unlocked的变量。

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

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

我们需要的下一个信息是 Solidity 默认的复杂数据类型(如结构),在初始化时作为局部变量存储它们因此,新记录在第16行默认为storage这种逻辑漏洞的类型是由于newRecord没有初始化而引起的。因为它默认为存储它成为一个指向存储的指针,因为它是未初始化的它指向了slot 0(即存储解锁的地方)。

值得注意的是在第17和18行上,我们为_name设置了

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

相关的slot存储位置。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

最后在定义数字的任意精度时,需要将变量转换为更高的精度执荇所有的数学操作,然后在需要的时候再转换回输出的精度。通常使用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证明来获取它们的代币因此,一个单一的交易就会允许所有用户兑换他们空投的代币

这个函数可以构建成一个代币合约,尣许未来发生的空投而确定所有用户的余额所需的唯一交易是设置Merkle树的根交易。

终于进入尾声了能看到这里真是好定力,为了犒劳你营长还准备了一些开发资源给,留给大家继续消化

其他有意思的Bug合集

  • 交易没有在payload中添加「0x」:

  • 虚拟安全的逻辑漏洞的类型、黑客及其修复的历史

  • 智能合约攻击现状大调查:

  • 从智能合约攻防战中学得的教训:

Manning,译者:老曹、Aholiab版权属于原作者。

我要回帖

更多关于 逻辑漏洞的类型 的文章

 

随机推荐