这篇讲述DASP TOP 10后面4-6类:未严格判断不安全函数调用返回值、拒绝服务、伪随机性

DASP Top 10,有个博主把这10类漏洞都大概描述了一下,写得真的是非常好,

repo大法!以太坊智能合约安全入门了解一下(上) 以太坊智能合约安全入门了解一下(下)

但是这篇文章还是偏向于代码向和实际用例,看心情翻牌讲

  1. Reentrancy - 重入
  2. Access Control - 访问控制
  3. Arithmetic Issues - 算术问题(整数溢出)
  4. Unchecked Return Values For Low Level Calls - 未严格判断不安全函数调用返回值
  5. Denial of Service - 拒绝服务
  6. Bad Randomness - 伪随机性
  7. Front Running - 提前交易
  8. Time manipulation - 时间操纵
  9. Short Address Attack - 短地址攻击
  10. Unknown Unknowns - 其他未知

未严格判断不安全函数调用返回值

这个还是很好理解的,感觉没啥特别好说的,后续在对Mythril的功能测评的时候,会提到这个漏洞的!不过现在如果你去用remix啊这类编译器,还是会提示你这个问题的,所以目前来说,它对以太坊的影响应该会慢慢消失,除非你,从不看warnings…

基本原理

引用DASP TOP 10的说法:

One of the deeper features of Solidity are the low level functions call(), callcode(), delegatecall() and send(). Their behavior in accounting for errors is quite different from other Solidity functions, as they will not propagate (or bubble up) and will not lead to a total reversion of the current execution. Instead, they will return a boolean value set to false, and the code will continue to run. This can surprise developers and, if the return value of such low-level calls are not checked, can lead to fail-opens and other unwanted outcomes. Remember, send can fail!

低级别的功能call()callcode()delegatecall()send(),它们解决错误的行为与其他Solidity函数完全不同,因为它们不会传播(或冒泡),也不会导致当前执行的全部还原。相反,它们将返回设置为的布尔值false,并且代码将继续运行。这可能会使开发人员感到吃惊,并且,如果不检查此类低级调用的返回值,可能会导致失败打开和其他不良后果。记住,发送可能失败!

实际例子

不详细说了

拒绝服务

基本原理

拒绝服务还是有点说头的,不过这篇文章已经说的非常清楚了,我在网上看了很多资料,感觉目前以太坊上拒绝服务也就是以下三种:

(转自: https://ethfans.org/posts/comprehensive-list-of-common-attacks-and-defense-part-6#1. 拒绝服务(DOS))

1.通过外部操纵映射或数组(Array)循环 ——在我的经历中,我看过此种模式的各种形式。通常情况下,它出现在 owner 希望在其投资者之间分配代币的情况下,以及,在合约中可以看到类似于 distribute() 函数的情况下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
contract DistributeTokens {
address public owner; // gets set somewhere
address[] investors; // array of investors
uint[] investorTokens; // the amount of tokens each investor gets

// ... extra functionality, including transfertoken()

function invest() public payable {
investors.push(msg.sender);
investorTokens.push(msg.value * 5); // 5 times the wei sent
}

function distribute() public {
require(msg.sender == owner); // only owner
for(uint i = 0; i < investors.length; i++) {
// here transferToken(to,amount) transfers "amount" of tokens to the address "to"
transferToken(investors[i],investorTokens[i]);
}
}
}

请注意,此合约中的循环遍历的数组可以被人为扩充。攻击者可以创建许多用户帐户,让 investor 数据变得更大。原则上来说,可以让执行 for 循环所需的 Gas 超过区块 Gas 上限,这会使 distribute() 函数变得无法操作。

2.所有者操作——另一种常见模式是所有者在合约中具有特定权限,并且必须执行一些任务才能使合约进入下一个状态。例如,ICO 合约要求所有者 finalize() 签订合约,然后才可以转让代币,即

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
bool public isFinalized = false;
address public owner; // gets set somewhere

function finalize() public {
require(msg.sender == owner);
isFinalized == true;
}

// ... extra ICO functionality

// overloaded transfer function
function transfer(address _to, uint _value) returns (bool) {
require(isFinalized);
super.transfer(_to,_value)
}

在这种情况下,如果权限用户丢失其私钥或变为非活动状态,则整个代币合约就变得无法操作。在这种情况下,如果 owner 无法调用 finalize() 则代币不可转让;即代币系统的全部运作都取决于一个地址。

3.基于外部调用的进展状态——有时候,合约被编写成为了进入新的状态需要将 Ether 发送到某个地址,或者等待来自外部来源的某些输入。这些模式也可能导致 DOS 攻击:当外部调用失败时,或由于外部原因而被阻止时。在发送 Ether 的例子中,用户可以创建一个不接受 Ether 的合约。如果合约需要将 Ether 发送到这个地址才能进入新的状态,那么合约将永远不会达到新的状态,因为 Ether 永远不会被发送到合约。

预防技术

在第一个例子中,合约不应该遍历可以被外部用户人为操纵的数据结构。建议使用 withdrawal 模式,每个投资者都会调用取出函数独立取出代币。

在第二个例子中,改变合约的状态需要权限用户参与。在这样的例子中(只要有可能),如果 owner 已经瘫痪,可以使用自动防故障模式。一种解决方案是将 owner 设为一个多签名合约。另一种解决方案是使用一个时间锁,其中 [13]行 上的需求可以包括在基于时间的机制中,例如 require(msg.sender == owner || now > unlockTime) ,那么在由 unlockTime 指定的一段时间后,任何用户都可以调用函数,完成合约。这种缓解技术也可以在第三个例子中使用。如果需要进行外部调用才能进入新状态,请考虑其可能的失败情况;并添加基于时间的状态进度,防止所需外部调用迟迟不到来。

注意:当然,这些建议都有中心化的替代方案,比如,可以添加 maintenanceUser ,它可以在有需要时出来解决基于 DOS 攻击向量的问题。通常,这类合约包含对这类权力实体的信任问题;不过这不是本节要探讨的内容

实际例子

GovernMental

GovernMental是一个很久以前的庞氏骗局,积累了相当多的 Ether。实际上,它曾经积累起 1100 个以太。不幸的是,它很容易受到本节提到的 DOS 漏洞的影响。这篇 Reddit 帖子描述了合约需要删除一个大的映射来取出以太。删除映射的 Gas 消耗量超过了当时的区块 Gas 上限,因此不可能撤回那 1100 个 Ether。合约地址为 0xF45717552f12Ef7cb65e95476F217Ea008167Ae3,您可以从交易0x0d80d67202bd9cb6773df8dd2020e7190a1b0793e8ec4fc105257e8128f0506b中看到,最后有人通过使用 250 万 Gas的交易取出了 1100 Ether 。

伪随机性

我觉得伪随机性很好理解,本来区块链就不存在随机熵

这篇文章我觉得写的非常好,基本上涵盖了当前伪随机数产生的所有问题:Predicting Random Numbers in Ethereum Smart Contracts

这篇文章的研究思路如下:

  1. 从etherscan.io和GitHub收集了3,649个智能合约。
  2. 然后将这些合同导入到Elasticsearch开源搜索引擎中。
  3. 使用Kibana Web UI进行丰富的搜索和过滤,发现了72种独特的PRNG实现。
  4. 根据对每份合同的人工评估,确定了43份易受伤害的合同。

然后分为以下四类脆弱的随机数发生器(PRNG):

  • 使用块变量作为熵源的PRNG
  • 基于过去某个区块的哈希的PRNG
  • 基于过去区块的区块哈希结合被视为私有的种子的PRNG
  • PRNG易于抢先

摘要部分该文章核心如下

使用块变量作为熵源的PRNG

这个主要是block.timestamp、block.coinbase、block.difficulty、block.gaslimit、block.number等等块变量引入的,这些变量可以被矿工操纵,所以就很不安全啊;而且这些都公开可查,同一个块内大家都能获取到这些信息,很危险的

基于过去某个区块的哈希的PRNG

block.blockhash()可以用来获取区块hash值,但是它有取值范围,只能适用于已经出块的且取值范围是最新的256个块。也就是说,如果超出范围,取值均为0。因此,block.blockhash(block.number) == 0, 因为block.number此时没有出块

预防技术

A better approach is to use the blockhash of some future block. The implementation scenario is as follows:

毅哥更好的方法是使用一些未来的区块,实现方案如下:

  • The player makes a bet and the house stores the block.number of the transaction.

    玩家下注,房屋将存储交易的block.number。

  • In a second call to the contract, the player requests that the house announces the winning number.

    在第二次致电合约时,玩家要求房主宣布中奖号码。

  • The house retrieves the saved block.number from storage and gets its blockhash, which is then used to generate a pseudo-random number.

    房子从存储中检索保存的block.number并获取其blockhash,然后将其用于生成伪随机数。

This approach works only if an important requirement is met. The Solidity documentation warns about the limit of saved blockhashes that the EVM is able to store: The block hashes are not available for all blocks for scalability reasons. You can only access the hashes of the most recent 256 blocks, all other values will be zero.

仅当满足重要要求时,此方法才有效。Solidity文档警告了EVM可以存储的已保存块哈希的限制:block hashes出于可扩展的考虑,并不对所有区块有效,你仅能获取最新的256个区块的hash值,其他区块hash值返回为0。

基于过去区块的区块哈希结合被视为私有的种子的PRNG

这个玩法不行,因为区块链没有真正意义上的私有变量,实际都可查。。。

PRNG易于抢先

这个应该也可以算作前置交易漏洞,以太坊的打包政策是,给的gas price高的优先,这就很有操作空间了

实际例子

Consider the following example. A lottery uses an external oracle to get pseudo-random numbers, which are used to determine the winner from among the players who submitted their bets in each round. These numbers are sent unencrypted. An attacker may observe the pool of pending transactions and wait for the number from the oracle. As soon as the oracle’s transaction appears in the transaction pool, an attacker sends a bet with a higher gas price. The attacker’s transaction was made last in the round, but thanks to the higher gas price, is actually executed before the oracle’s transaction, making the attacker victorious. Such a task was featured in the ZeroNights ICO Hacking Contest.

考虑以下示例。彩票使用外部预言机来获取伪随机数,该伪随机数用于在每个回合中提交赌注的玩家中确定赢家。这些数字未加密发送。攻击者可能会观察等待交易事务池,并等待来自oracle的数字。一旦oracle的交易出现在交易池中,攻击者便以更高的汽油价格发送赌注。由于汽油价格上涨,攻击者的交易虽然提交的晚,但是确可以在oracle的交易之前执行的,这使攻击者取得了胜利。在ZeroNights ICO黑客大赛中就有这样一个赌注。

Another example of a contract prone to front-running is the game called “Last is me!”. Every time a player buys a ticket, that player claims the last seat and the timer starts counting down. If nobody buys the ticket within a certain number of blocks, the last player to “take a seat” wins the jackpot. When the round is about to finish, an attacker may observe the transaction pool for other contestants’ transactions and claim the jackpot by means of a higher gas price.

容易发生抢占先机的另一个例子是名为“Last is me!Last ”的游戏。玩家每次购买彩票时,该玩家将获得最后一个席位,计时器开始倒计时。如果没有人在一定数量的区块内购买彩票,则最后一个“坐下来”的玩家将赢得大奖。当该回合即将结束时,攻击者可能会观察其他参赛者的交易的交易池,并通过更高的汽油价格索取大奖。

更好的随机数生成办法

文章共提到了三种:Oracle or BTCReply\Signidice\Commit–reveal

Oracle

就是外部预言机,老铁用图解释的很清楚

外部预言机流程

其中红色的就是链上的内容,Oraclize daemon和random.org则是链下内容,而且不可控,所以说除非你完全信任两者,不然这也是有风险的。

BTCReply

其实BTCReply就是把以太坊的矿工风险转移到了比特币矿工风险。BTCRelay是以太坊和比特币区块链之间的桥梁。使用BTCRelay,以太坊区块链中的智能合约可以请求将来的比特币区块哈希并将其用作熵的来源。将BTCRelay用作PRNG的一个项目是以太坊彩票

感觉只是提高了作弊代价阈值吧

Signidice

感觉Signidice就是利用算法签名来锁定下注者,也就是一旦出现下注者,就利用赌注所有者的私钥加签名锁定这个下注,然后合约用公钥解签来验证。摘录一下完整流程

该算法适用于那些基于以太坊的游戏,其中玩家的每一轮结果仅取决于RNG和(可选)玩家选择的数字,而不取决于其他玩家的动作。例如,它可能适用于轮盘赌,角子机等,但不适用于那些结果取决于其他玩家或仅取决于其人数的游戏(例如彩票业)。例如,轮盘游戏可以建模为多个回合,其中单个玩家与赌场对战。在这种情况下,可以使用以下算法生成伪随机数。

​ 1. 赌场为确定性签名算法(例如RSA)生成一对新的私钥/公钥(PrivKey和PubKey)。

​ 2. 赌场创建一个智能合约,其中包含公钥(PubKey),最大参与者数和以太坊赏金。(可选:赌场更改现有智能合约的PubKey)。

​ 3. 玩家选择要下注的数字(B)和某种格式(例如20字节)的随机数(R)。如果游戏规则允许,则玩家甚至可以指定数字B的范围(奇数与偶数等)。

​ 4. 玩家发送包含以太币投注以及数据B和R的交易(TX)。

​ 5. 合同检查数字B和R的有效性和格式。无效的TX被拒绝。

​ 6. 此外,合同还会检查该球员在之前的回合中是否已经使用过数字R,在这种情况下,TX被拒绝。(如果合同被重复用于多轮游戏,则此步骤是必需的)。

​ 7. 合同将随机数R与玩家的以太帐户的公共地址(A)串联在一起,从该地址发送TX:V = A +R。结果值V存储在合同中。V的大小始终相同:size(V)= size(A)+ size(R)。在这一点上,回合的结果(胜利或失败)成为确定性的。

​ 8. 娱乐场必须使用其PrivKey对结果值V进行签名,从而产生数字签名S = sign(PrivKey,V),并发送包含S的相应TX。

​ 9. 合同从数字签名S中恢复实际的公钥(K),并验证它是否等于先前发布的PubKey(K == PubKey)。如果APK与PubKey不匹配,或者娱乐场未能在预定义的时间范围内执行步骤8,则等同于作弊。在这种情况下,合同会将奖金和原始赌注一起发送给玩家,然后通过自杀关闭合同。(在多人游戏的情况下,所有玩家共享赏金)。

​ 10. 合同使用S作为预定义PRNG算法(例如,基于SHA-3)的种子,该算法会生成幸运数字(L),例如介于0到36之间。

​ 11. 如果B对应于L,则玩家获胜,否则赌场获胜。合同将赌注发送给获胜者。

​ 12. 现在,赌场可能会关闭合同并收回赏金,或启动新一轮游戏。或者,可以将合同编程为自动进行下一轮,除非赌场将其关闭。

赌场选择了PrivKey之后,其操作将变为确定性的。玩家无法预测数字签名的结果,因此,他对随机数R的选择只能以与在现实生活中掷骰子相同的方式影响结果(因此该算法的名称)。因此,没有一个参与者可以任何有意义的方式操纵结果。

需要注意的是,ECDSA算法不适合该算法,因为

A [proof-of-concept](https://github.com/pertsev/web3_utilz/tree/master/ECDSA signature generating (cheating)) of such cheating has been created by Alexey Pertsev.

Fortunately, with release of the Metropolis hardfork, a modular exponentiation operator has been introduced. This allows implementing RSA signature verification, which unlike ECDSA does not allow manipulating input parameters to find a suitable signature.

commit-reveal

所谓的提交-披露方法就是:

  • “提交”阶段,当事双方将其受密码保护的机密提交给智能合约。
  • 在“公开”阶段,当当事方宣布明文种子时,智能合约会验证它们是否正确,然后使用种子生成一个随机数。

举个例子,所有者提供一个随机数seed1,玩家提供一个随机数seed2,但是在『提交』阶段,仅透露sha3(seed1)和sha3(seed2)的值,到公开阶段,双方再披露seed1和seed2的值,然后验证两者,最后生成一个随机数sha3(seed1, seed3, blockhash),其中blockhash是未来区块hash值。

这个方法有个弊端就是,所有者也可以是玩家哦~所以,玩家无法信任所有者

文章有提到说Randao。该PRNG从多个方收集哈希种子,并向每个参与方奖励。没有人知道其他人的种子,因此结果确实是随机的。但是,单方拒绝透露种子将导致拒绝服务。

总结

所以这么看来,世上难得两全法啊~每个方法都有优势和弊端

区块链的熵源有限。设计PRNG时,开发人员应确保首先了解各方的动机,然后再选择适当的方法。