SharkTeam独家分析 | 智能合约中糟糕的伪随机数
区块链是信任机器(Trust Machine),其因公正、去中心化和透明的技术特性获得大众认可。图灵完备的智能合约则大大提高了区块链的灵活性和延展性,目前的智能合约以Solidity 编写的居多,而合约安全和用户信任至关重要。
然而,区块链本身是确定性的,因此它给那些选择编写自己的伪随机数生成器 (PRNG) 的人带来了一定的困难,这是任何带有随机性的应用程序的固有部分。本篇文章将评估用 Solidity 编写的 PRNG 的安全性,并介绍在 智能合约 中 对于伪随机的利用和预测,以及如何合理地做好链上随机性策略。
易受攻击的随机数实现
以下是四类易受攻击的 PRNG:
- 基于块变量的 PRNG
- 基于`blockhash`的PRNG
- 基于过去`blockhash`与被视为私有的种子相结合的 PRNG
- PRNG中的抢跑交易
1.基于块变量的 PRNG
有许多块变量可能被错误地用作熵的来源:
-
block.coinbase 代表挖掘当前区块的矿工的地址。
-
block.difficulty 是对找到块的难度的相对度量。
-
block.gaslimit 限制区块内交易的最大gas消耗。
-
block.number 是当前块的高度。
-
block.timestamp 是块被开采的时间。
首先,所有这些区块变量都可以被矿工操纵,因此由于矿工的激励,它们不能用作熵的来源。更重要的是,块变量显然在同一个块内共享。因此,如果攻击者的合约通过内部消息调用受害者合约,则两个合约中的相同 PRNG 将产生相同的结果。
示例1: 0x80ddae5251047d6ceb29765f38fed1c0013004b7
示例2:0xa11e4ed59dc94e69612f3111942626ed513cb172
示例3:0xcC88937F325d1C6B97da0AFDbb4cA542EFA70870
2.基于blockhash的PRNG
以太坊区块链中的每个区块都有一个验证哈希。以太坊虚拟机 (EVM) 可以通过 block.blockhash() 函数获取此类区块哈希。此函数需要一个数字参数来指定块的编号。在研究过程中,我们发现 block.blockhash() 的结果在 PRNG 实现中经常被误用。此类 PRNG 存在三个主要有缺陷的变体:
让我们逐一分析这些情况。
block.blockhash(block.number)
所述block.number状态变量允许获得所述当前块的高度。当矿工获得执行合约代码的交易时,block.number该交易的未来区块的 的 是已知的,因此合约可以可靠地访问其价值。但是,在 EVM 中执行交易的那一刻,由于显而易见的原因,正在创建的区块的区块哈希尚不可知,并且 EVM 将始终产生零。
一些合同误解了表达的意思block.blockhash(block.number)。在这些合约中,当前区块的区块哈希在运行时被认为是已知的,并被用作熵的来源。
示例1:0xa65d59708838581520511d98fb8b5d1f76a96cad
示例2:https://github.com/axiomzen/eth-random/issues/3
block.blockhash(block.number-1)
一定数量的合约使用另一种基于区块哈希的 PRNG,依赖于最后一个区块的区块哈希。不用说,这种方法也有缺陷:攻击者可以使用相同的 PRNG 代码制作漏洞利用合约,以便通过内部消息调用目标合约。两个合约的“随机”数字将相同。
示例1:0xF767fCA8e65d03fE16D4e38810f5E5376c3372A8
未来区块的blockhash
更好的方法是使用某个未来区块的blockhash。实现场景如下:
-
玩家下注,庄家存储交易的区块编号。
-
在第二次调用合约时,玩家要求庄家公布中奖号码。
-
房子从存储中检索保存的`block.number`并获取其块哈希,然后用于生成伪随机数。
这种方法只有在满足重要要求时才有效。Solidity 文档警告了 EVM 能够存储的已保存块哈希的限制:
因此,如果在 256 个区块内没有进行第二次调用,并且没有对区块哈希进行验证,那么伪随机数将事先知道——区块哈希将为零。
这个漏洞被利用的最著名案例是SmartBillions 彩票的黑客攻击(https://www.reddit.com/r/ethereum/comments/74d3dc/smartbillions_lottery_contract_just_got_hacked/)。该合约对block.number的验证不充分,导致 400 ETH 丢失给了一个未知玩家,该玩家等待了 256 个区块才显示可预测的中奖号码。
计算未来区块的blockhash是一种模式,它可以为当前交易产生真正未知的比特,但仍然容易受到矿工的攻击:矿工可以下注,然后挖掘多个版本的未来区块。因此,为了安全使用blockhash,期望值攻击者随机试验的回报应该低于挖掘区块的回报:攻击者永远不应该从扔掉未获胜的区块中获益。请注意,此预期值可能远低于随机性的总赌注。例如,以 1/1000 的概率奖励 1000 ETH 的赌注对攻击者来说仍然只值 1ETH。因此,这种随机性对于许多应用来说是非常实用的。
然而,在计算随机试验的预期值时,重要的是要记住赌注是复合的。如果单个区块包含 N 个投注(例如,在 N 个独立的交易中,可能由同一攻击者发起),每个投注为 1000ETH,并且每个投注的概率为 1/1000,则攻击者对该区块的期望值为 N ETH。此推理可用于限制同一交易中接受的最大投注数。不幸的是,单个合约无法知道单个区块中其他合约的交易采取了哪些其他赌注,攻击者很可能会针对多个合约进行复合赌注。因此,估计要么是近似的,要么过于保守,导致每次投注的预期值非常低。更糟糕的是,编码不当的合约会激励攻击者违反无关合约的随机性,至少是暂时的。攻击者/矿工有动机利用编码错误、易受攻击的合约,并有额外的机会押注一个本身不会盈利的合约。(攻击者可能无法更多地利用较弱的合约,例如,因为它对每个区块的赌注有限制,但可以在同一区块中容纳更多交易。)不过,这种攻击仅在严重-编码合同已耗尽。
对当前区块挖矿奖励 (3ETH) 和区块 gas 限制 (800 万) 的悲观值进行粗略计算表明,个人投注的预期价值低于每单位 3.75E-7 ETH- ofgas 对于稳态使用是安全的,即使暂时脆弱(直到其他合同耗尽)。例如,一笔消耗 100,000 gas 的交易应该会导致预期回报最多为 0.0375 ETH 的投注。(如果该区块充满了此类交易,攻击者-矿工将其扔掉仍然无利可图。)目前这大约是此类交易的 gas 成本的 50 倍,因此对于实际应用来说,下注价值并不是不切实际的低. 同样,这不会限制下注的回报,而是限制预期回报。成功下注可能会产生 100 万个 ETH,但如果这仅以 1/27,000,000 的概率发生,
更一般地说,这种推理激发了一种有趣的做法,但到目前为止我们还没有看到采用这种做法:根据预期价值按比例进行投注消耗 gas。例如,一个期望值很高的赌注,例如 2 ETH,应该是完全可能的,但应该需要几乎等于区块 gas 限制的 gas(即,调用者应该知道提供 gas 并且投注合约应该消耗它通过额外的计算),因此几乎没有其他交易可以成为同一块的一部分。
[注释:所有分析都假设攻击者仅被激励以基于智能合约执行最大化他/她在 ETH(或代币)中的利润。可能没有考虑攻击模型,尽管大多数传统攻击(例如,通过链重组进行双花)似乎并没有从扔掉一个块中受益。然而,值得注意的是,该假设不适用于愿意损失ETH 以进行攻击的攻击者(例如,为了对受害者造成损害,或破坏生态系统以操纵 ETH 汇率,或……)。此类攻击条件是另一篇文章的主题,但以太坊的大部分内容都容易受到此类攻击。]
3.基于过去blockhash与被视为私有的种子相结合的 PRNG
为了增加熵,一些被分析的合约使用了额外的被视为私有种子。Slotthereum彩票就是这样的一种情况。相关代码如下:
变量指针被声明为私有的,这意味着其他合约无法访问它的值。每场比赛结束后,将 1 到 9 之间的中奖号码分配给该变量,然后block.number在检索区块哈希时将其用作当前的偏移量。
区块链本质上是透明的,不得用于以明文形式存储秘密。尽管私有变量不受其他合约的保护,但可以在链外获取合约存储的内容。例如,流行的以太坊客户端 web3 具有 API 方法web3.eth.getStorageAt(),它允许检索指定索引处的存储条目。
鉴于这一事实,从合约存储中提取私有变量指针的值并将其作为参数(https://github.com/slotthereum/source/issues/1)提供给漏洞利用是很简单的:
4.PRNG中的抢跑交易
为了获得最大的奖励,矿工根据每笔交易使用的累计gas来选择交易来创建一个新的区块。一个区块中的交易执行顺序由gas价格决定。Gas 价格最高的交易将被首先执行。因此,通过操纵 gas 价格,可以在当前区块中的所有其他交易之前执行所需的交易。当合约的执行流程取决于其在区块中的位置时,这可能构成安全问题——通常称为抢先交易。
考虑以下示例。彩票使用外部预言机获取伪随机数,用于从每轮提交投注的玩家中确定获胜者。这些号码以未加密的方式发送。攻击者可能会观察待处理事务池并等待来自预言机的数字。一旦预言机的交易出现在交易池中,攻击者就会以更高的汽油价格下注。攻击者的交易是在本轮的最后进行的,但由于燃料价格较高,实际上在预言机的交易之前执行,使攻击者获胜。ZeroNights ICO Hacking Contest 中(https://blog.positive.com/zeronights-ico-hacking-contest-writeup-63afb996f1e3)出现了这样的任务。
另一个容易抢先的合同例子名为“Last is me!”。每次玩家买票时(https://etherscan.io/address/0x5d9b8fa00c16bcafae47deed872e919c8f6535bf),该玩家都会获得最后一个座位,计时器开始倒计时。如果在一定数量的街区内没有人买票,最后一个“坐下”的玩家将赢得头奖。当回合即将结束时,攻击者可能会观察其他参赛者交易的交易池,并通过更高的gas价格领取头奖。
Positive Hack Days
Positive Hack Days(PHDays)是每年在莫斯科举行的计算机安全会议。第一次会议于 2011 年举行。会 议讨论的主题包括零日攻击和数字调查、密码学和网络战、网络世界中个人和国家的安全。以下为第八届关于智能合约不安全的 随机数 赛 题。
Azino 777
第一个任务是基于伪随机数生成器 (PRNG),它依赖最后一个块的块哈希作为生成随机数的熵源:
由于block.blockhash(block.number-1)同一块内的任何交易的结果都是相同的,因此攻击假设使用具有相同`rand()`功能的漏洞利用合约,通过内部消息调用目标合约:
Private Ryan
We added a private seed, nobody will ever learn it!
这个任务比上一个任务更难一些。一个seed被视为私有的变量被用作 a 的偏移量,block.number这样块哈希就不会绑定到前一个块。每次下注后seed都会被一个新的“随机”值覆盖。这就是Slotthereum(https://github.com/slotthereum/source/issues/1)彩票的情况。
与之前的任务类似,成功的攻击者只需要将rand()函数复制到漏洞利用合约中,但这次私有变量的值`seed`应该是从链外获取的,然后作为参数提供给漏洞利用。为此,可以利用web3 库的web3.eth.getStorageAt()方法(https://github.com/ethereum/wiki/wiki/JavaScript-API#web3ethgetstorageat):
当我们获得种子后,所有需要做的就是将其提供给与第一个任务中几乎相同的漏洞利用:
Wheel of Fortune
This lottery uses blockhash of a future block, try to beat it!
根据任务描述,目标是预测Game在下注发生时其编号保存在结构中的块的块哈希。然后在进行后续投注时检索该区块哈希以生成随机数。
有两种可能的解决方案:
1.可以从漏洞利用合约中调用目标合约两次,第一次调用将导致block.blockhash(block.number)始终为零。
2.在进行第二次下注之前,可以等待 256 个区块被挖出,因此由于可用区块哈希数的EVM 限制(http://solidity.readthedocs.io/en/v0.4.21/units-and-global-variables.html#block-and-transaction-properties),保存的区块号的区块哈希值将为零。
在这两种情况下,获胜的赌注都是uint256(keccak256(bytes32(0))) % 100或47。
Call Me Maybe
This contract does not like when other contracts are calling it.
保护合约不被其他合约调用的一种方法是使用extcodesize EVM 汇编指令,该指令返回由其地址指定的合约的大小。该技术是在内联汇编中针对msg.sender的地址使用此操作码。如果与地址关联的大小大于零,则msg.sender是一个合约,因为以太坊中的普通地址没有任何关联代码。这个任务中的合约正是使用了这种方法来防止其他合约调用它。
Transaction 属性tx.origin是指交易的原始发行者,而msg.sender指向最后一个调用者。如果我们从普通地址发送交易,这些变量将是相等的,我们最终会得到一个revert()。这就是为什么为了解决需要绕过extcodesize检查的挑战,这样tx.origin和msg.sender会有所不同。幸运的是,有一个很好的 EVM 特性可以帮助实现这一点:
事实上,当一个新部署的合约在其构造函数中调用另一个合约时,它在区块链上尚不存在,它仅充当钱包。因此,它没有关联的代码并且extcodesize会产生零:
使PRNG更安全
有几种方法可以在以太坊区块链上实施更安全的 PRNG:
-
外部预言机
-
Signidice算法
-
Commit–reveal方法
-
限定条件下基于未来blockhash的策略
1.1.外部预言机Oraclize
Oraclize(http://www.oraclize.it/)是一种面向分布式应用程序的服务,它提供了区块链与外部环境(互联网)之间的桥梁。借助Oraclize,智能合约可以从 Web API 请求数据,例如货币汇率、天气预报和股票价格。最突出的用例之一是Oraclize作为 PRNG 的能力。一些被分析的合约使用Oraclize通过 URL 连接器从 random.org 获取随机数。该方案如图所示。
这种方法的主要缺点是它是中心化的。我们可以相信 Oraclize 守护进程不会篡改结果吗?我们可以信任 random.org 及其所有底层基础设施吗?尽管 Oraclize 提供了对结果的 TLSNotary 验证,但它只能在链下使用——在彩票的情况下,只有在选择获胜者之后才能使用。Oraclize 的一个更好的用途是作为“随机”数据源,使用可以在链上验证的账本证明(https://blog.oraclize.it/welcoming-our-brand-new-ledger-proof-649b9f098ccc)。
1.2.外部预言机BTCRelay
BTCRelay(http://btcrelay.org/)是以太坊和比特币区块链之间的桥梁。使用BTCRelay,以太坊区块链中的智能合约可以请求未来的比特币区块哈希并将它们用作熵的来源。一个使用BTCRelay作为 PRNG 的项目是The Ethereum Lottery。(https://etherscan.io/address/0x302fE87B56330BE266599FAB2A54747299B5aC5B)。
BTCRelay方法对于矿工激励问题并不安全。尽管与依赖以太坊区块相比,这种方法设置了更高的障碍,但它只是利用了比特币价格高于以太坊的事实,从而降低了但并不能消除矿工作弊的风险。
2.Signidice算法
Signidice(https://github.com/gluk256/misc/blob/master/rng4ethereum/signidice.md)是一种基于加密签名的算法,可在仅涉及两方的智能合约中用作 PRNG:玩家和房屋。该算法的工作原理如下:
-
玩家通过调用智能合约进行下注。
-
房子看到赌注,用它的私钥签名,然后将签名发送到智能合约。
-
智能合约使用已知的公钥验证签名。
-
然后使用此签名生成一个随机数。
以太坊具有ecrecover()用于验证链上 ECDSA 签名的内置功能。但是,ECDSA 不能在 Signidice中使用,因为房屋能够操纵输入参数(特别是参数k),从而影响生成的签名。一个[验证的概念](https://github.com/pertsev/web3_utilz/tree/master/ECDSA signature generating (cheating))这样的作弊已经由阿列克谢Pertsev创建。
幸运的是,随着Metropolis硬分叉的发布,引入了模幂运算符(https://github.com/ethereum/EIPs/pull/198)。这允许实现 RSA 签名验证,与 ECDSA 不同,它不允许操纵输入参数来找到合适的签名。
3.Commit–reveal方法
顾名思义,commit-reveal方法包括两个阶段:
commit阶段,当各方将其受密码保护的秘密提交给智能合约时。
reveal阶段,当各方宣布明文种子时,智能合约验证它们是否正确,并使用种子生成随机数。一个正确的 commit-reveal 实现不应该依赖于任何一方。虽然玩家不知道所有者提交的原始种子,并且他们的机会均等,但所有者也可能是玩家,因此玩家无法信任所有者。提交-显示方法的一个更好的实现是Randao(https://github.com/randao/randao)。这个 PRNG 从多方收集散列种子,每一方都因参与而获得奖励。没有人知道其他人的种子,所以结果是真正随机的。但是,拒绝透露种子的一方将导致拒绝服务。
Commit-reveal可以与未来的区块哈希相结合(https://blog.winsome.io/random-number-generation-on-winsome-io-future-blockhashes-fe44b1c61d35)。在这种情况下,熵有三个来源:
1. 所有者的 sha3(seed1)
2. 玩家的 sha3(seed2)
3. 未来的区块哈希
然后,将随机数产生如下: sha3(seed1, seed2, blockhash)。因此,commit-reveal 方法解决了矿工激励问题:矿工可以决定区块哈希,但不知道所有者和玩家的种子。它还解决了所有者激励问题:所有者只知道所有者自己的种子,而玩家的种子和未来的区块哈希是未知的。此外,这种方法解决了一个人既是所有者又是矿工的情况:那个人决定了区块哈希并且知道所有者的种子,但不知道玩家的种子。
4.限定条件下基于未来blockhash的策略
一个有效的基于未来blockhash的策略遵循以下模式:
1.接受投注,付款,登记投注交易的区块号。
2.投注者不仅要下注,还要在未来的交易中(在接下来的256个区块内)调用合约。合约将计算blockhash较早注册的区块号的 ,并使用它来确定下注的成功。
3.如果投注者太晚(或太早),结果应该有利于合同,而不是潜在的攻击者。
单个区块中所有投注的随机试验的期望值应低于挖掘区块的奖励。(你应该说服自己这个计算对你有利。)
这种方法的缺点是延迟直到显示投注结果、需要第二次交易以及对投注的预期价值设置严格的限制。然而,对于纯链上随机性,它是唯一已知的准可接受的技术。
结论
在以太坊区块链中安全实施 PRNG 仍然是一个挑战。正如我们的研究表明的那样,由于缺乏现成的解决方案,开发人员倾向于使用自己的实现。但是在创建这些实现时,很容易出错,因 为 区块链 的 熵源有限。在设计 PRNG 时,开发者应该确保首先了解各方的动机,然后再选择合适的方法。
参考材料
[1] Positive Hack Days 8 writeup(https://blog.positive.com/phdays-8-etherhack-contest-writeup-794523f01248)
[2] 预测以太坊智能合约中的随机数(https://blog.positive.com/predicting-random-numbers-in-ethereum-smart-contracts-e5358c6b8620)
[3] 糟糕的随机性比你想象的更危险(https://medium.com/dedaub/bad-randomness-is-even-dicier-than-you-think-7fa2c6e0c2cd)
==
添加TG群:鸵鸟中文社区 https://t.me/tuoniaox