智能合约游戏之殇——Dice2win安全分析

栏目: 数据库 · 发布时间: 6年前

内容简介:Dice2win是目前以太坊上很火爆的区块链博彩游戏,其最大的特点就是理论上的公平性保证,每天有超过1000以太币被人们投入到这个游戏中。

作者:LoRexxar'@知道创宇404区块链安全研究团队
时间:2018年10月18日

Dice2win是目前以太坊上很火爆的区块链博彩游戏,其最大的特点就是理论上的公平性保证,每天有超过1000以太币被人们投入到这个游戏中。

Dice2win官网

Dice2win合约代码

dice2win的游戏非常简单,就是一个赌概率的问题。

智能合约游戏之殇——Dice2win安全分析

就相当于猜硬币的正面和反面,只要你猜对了,就可以赢得相应概率的收获。

这就是一个最简单的依赖公平性的游戏合约,只要“庄家”可以保证绝对的公正,那么这个游戏就成立。

2018年9月21日,我在 《以太坊合约审计 CheckList 之“以太坊智能合约编码设计问题”影响分析报告》 中提到了以太坊智能合约中存在一个弱随机数问题,里面提到dice2win的合约中实现了一个很好的随机数生成方案 hash-commit-reveal

2018年10月12日,Zhiniang Peng from Qihoo 360 Core Security发表了 《Not a fair game, Dice2win 公平性分析》 ,里面提到了关于Dice2win的3个安全问题。

在阅读文章的时候,我重新审视了Dice2win的合约代码,发现在上次的阅读中对Dice2win的执行流程有所误解,而且Dice2win也在后面的代码中迭代更新了Merkle proof功能,这里我们就重点聊聊这几个问题。

Dice2win安全性分析

选择中止攻击

让我们来回顾一下dice2win的代码

function placeBet(uint betMask, uint modulo, uint commitLastBlock, uint commit, bytes32 r, bytes32 s) external payable {
        // Check that the bet is in 'clean' state.
        Bet storage bet = bets[commit];
        require (bet.gambler == address(0), "Bet should be in a 'clean' state.");

        // Validate input data ranges.
        uint amount = msg.value;
        require (modulo > 1 && modulo <= MAX_MODULO, "Modulo should be within range.");
        require (amount >= MIN_BET && amount <= MAX_AMOUNT, "Amount should be within range.");
        require (betMask > 0 && betMask < MAX_BET_MASK, "Mask should be within range.");

        // Check that commit is valid - it has not expired and its signature is valid.
        require (block.number <= commitLastBlock, "Commit has expired.");
        bytes32 signatureHash = keccak256(abi.encodePacked(uint40(commitLastBlock), commit));
        require (secretSigner == ecrecover(signatureHash, 27, r, s), "ECDSA signature is not valid.");

        uint rollUnder;
        uint mask;

        if (modulo <= MAX_MASK_MODULO) {
            // Small modulo games specify bet outcomes via bit mask.
            // rollUnder is a number of 1 bits in this mask (population count).
            // This magic looking formula is an efficient way to compute population
            // count on EVM for numbers below 2**40. For detailed proof consult
            // the dice2.win whitepaper.
            rollUnder = ((betMask * POPCNT_MULT) & POPCNT_MASK) % POPCNT_MODULO;
            mask = betMask;
        } else {
            // Larger modulos specify the right edge of half-open interval of
            // winning bet outcomes.
            require (betMask > 0 && betMask <= modulo, "High modulo range, betMask larger than modulo.");
            rollUnder = betMask;
        }

        // Winning amount and jackpot increase.
        uint possibleWinAmount;
        uint jackpotFee;

        (possibleWinAmount, jackpotFee) = getDiceWinAmount(amount, modulo, rollUnder);

        // Enforce max profit limit.
        require (possibleWinAmount <= amount + maxProfit, "maxProfit limit violation.");

        // Lock funds.
        lockedInBets += uint128(possibleWinAmount);
        jackpotSize += uint128(jackpotFee);

        // Check whether contract has enough funds to process this bet.
        require (jackpotSize + lockedInBets <= address(this).balance, "Cannot afford to lose this bet.");

        // Record commit in logs.
        emit Commit(commit);

        // Store bet parameters on blockchain.
        bet.amount = amount;
        bet.modulo = uint8(modulo);
        bet.rollUnder = uint8(rollUnder);
        bet.placeBlockNumber = uint40(block.number);
        bet.mask = uint40(mask);
        bet.gambler = msg.sender;
    }

    // This is the method used to settle 99% of bets. To process a bet with a specific
    // "commit", settleBet should supply a "reveal" number that would Keccak256-hash to
    // "commit". "blockHash" is the block hash of placeBet block as seen by croupier; it
    // is additionally asserted to prevent changing the bet outcomes on Ethereum reorgs.
    function settleBet(uint reveal, bytes32 blockHash) external onlyCroupier {
        uint commit = uint(keccak256(abi.encodePacked(reveal)));

        Bet storage bet = bets[commit];
        uint placeBlockNumber = bet.placeBlockNumber;

        // Check that bet has not expired yet (see comment to BET_EXPIRATION_BLOCKS).
        require (block.number > placeBlockNumber, "settleBet in the same block as placeBet, or before.");
        require (block.number <= placeBlockNumber + BET_EXPIRATION_BLOCKS, "Blockhash can't be queried by EVM.");
        require (blockhash(placeBlockNumber) == blockHash);

        // Settle bet using reveal and blockHash as entropy sources.
        settleBetCommon(bet, reveal, blockHash);
    }

主要函数为placeBet和settleBet,其中placeBet函数主要为建立赌博,而settleBet为开奖。最重要的一点就是,这里完全遵守 hash-commit-reveal 方案实现,随机数生成过程在服务端,整个过程如下。

  1. 用户选择好自己的下注方式,确认好后点击下注按钮。
  2. 服务端生成随机数reveal,生成本次赌博的随机数hash信息,有效最大blockNumber,并将这些数据进行签名,并将commit和信息签名传给用户。
  3. 用户将获取到的随机数hash以及lastBlockNumber等信息和下注信息打包,通过Metamask执行placebet函数交易。
  4. 服务端在一段时间之后,将带有随机数和服务端执行settlebet开奖

在原文中提到,庄家(服务端)接收到用户猜测的数字,可以选择是否中奖,选择部分对自己不利的中止,以使庄家获得更大的利润。

这的确是这类型合约最容易出现的问题,庄家依赖这种方式放大庄家获胜的概率。

上面的流程如下

智能合约游戏之殇——Dice2win安全分析

而上面提到的选择中止攻击就是上面图的右边可能会出现的问题

智能合约游戏之殇——Dice2win安全分析

整个流程最大的问题,就在于placebet和settlebet有强制的执行先后顺序,否则其中的一项block.number将取不到正确的数字,也正是应为如此,当用户下注,placebet函数执行时,用户的下注信息就可以被服务端获得了,此时服务端有随机数、打包placebet的block.number、下注信息,服务端可以提前计算用户是否中奖,也就可以选择是否中止这次交易。

选择开奖攻击

在原文中,提到了一个很有趣的攻击方式,在了解这种攻击方式之前,首先我们需要对区块链共识算法有所了解。

比特币区块链采用Proof of Work(PoW)的机制,这是一个叫做工作量证明的机制,提案者需要经过大量的计算才能找到满足条件的hash,当寻找到满足条件的hash反过来也证明了提案者付出的工作量。但这种情况下,可能会有多个提案者,那么就有可能出现链的分叉。区块链对这种结果的做法是,会选取最长的一条链作为最终结果。

当你计算出来的块被抛弃时,也就意味着你付出的成本白费了。所以矿工会选择更容易被保留的链继续计算下去。这也就意味着如果有人破坏,需要付出大量的经济成本。

借用一张原文中的图 智能合约游戏之殇——Dice2win安全分析

在链上,计算出的b2、c5、b5、b6打包的交易都会回退,交易失败,该块不被认可。

回到Dice2win合约上,Dice2win是一个不希望可逆的交易过程,对于赌博来说,单向不可逆是一个很重要的原则。所以Dice2win新添加了MerikleProof方法来解决这个问题。

MerikleProofi方法核心在于,无论是否分叉,该分块是否会被废弃,Dice2win都认可这次交易。当服务端接收到一个下注交易(placebet)时,立刻对该区块开奖。

MerikleProofi 的commit

上面这种方法的原理和以太坊的区块结构有关,具体可以看 《Not a fair game, Dice2win 公平性分析》 一文中的分析,但这种方法一定程度的确解决了开奖速度的问题,甚至还减少了上面提到的选择中止攻击的难度。

但却出现了新的问题,当placebet交易被打包到分叉的多个区块中,服务端可以通过选择获利更多的那个区块接受,这样可以最大化获得的利益。但这种攻击方式效果有效,主要有几个原因:

  1. Dice2win需要有一定算力的矿池才能主动影响链上的区块打包,但大部分算力仍然掌握在公开的矿池手中。
  2. 被动的遇到分叉情况并不会太多,尤其是遇到了打包了placebet的区块,该区块的hash只是多了选择,仍然是不可控的,大概率多种情况结果都是一致的。
  3. Dice2win无法控制矿工打包什么交易,可能会打包多个交易,不同的区块可能打包的交易都不一致,在这种情况下很难去取舍该选择那个区块的结果。

从这种角度来看,这种攻击并没有太大的实际价值,可以说对Dice2win没有什么优势可言。

任意开奖攻击(Merkle proof验证绕过)

在上面的分析中,我们详细分析了我们Merkle proof的好处以及问题所在。但如果Merkle proof机制从根本上被绕过,那么是不是就有更大的问题了。

Dice2win在之前已经出现了这类攻击 https://etherscan.io/tx/0xd3b1069b63c1393b160c65481bd48c77f1d6f2b9f4bde0fe74627e42a4fc8f81

攻击者成功构造攻击合约,通过合约调用placeBet来下赌注,并伪造Merkle proof并调用settleBetUncleMerkleProof开奖,以100%的几率控制赌博成功。

分析攻击合约可以发现该合约中的多个安全问题:

1、Dice2win是一个不断更新的合约,存在多个版本。但其中决定庄家身份的secretSigner值存在多个版本相同的问题,导致同一个签名可以在多个合约中使用。

2、placebet中对于最后一个commitlaskblock的check存在问题

智能合约游戏之殇——Dice2win安全分析

用作签名的commitlastblock定义是uint256,但用作签名的只有uint40,也就是说,我们在执行placeBet的时候,可以修改高位的数字,导致某个签名信息始终有效。

3、Merkle proof边界检查不严格。

在最近的一次commit中,dice2win修复了一个漏洞是关于Merkle proofcheck的范围。

https://github.com/dice2-win/contracts/commit/b0a0412f0301623dc3af2743dcace8e86cc6036b

智能合约游戏之殇——Dice2win安全分析

这里检查使Merkle proof更严格了

4、settleBet 权限问题

经过我的研究,实际上在Dice2win的游戏逻辑中,settleBet应该是只有服务端才能调用的(只有庄家才能开奖),但在之前的版本中,并没有这样的设置。

在新版本中,settleBet加入了这个限制。

智能合约游戏之殇——Dice2win安全分析

这里绕过Merkle proof的方法就不再赘述了,有兴趣可以看看原文。

refundBet下溢

原文中最后提到了一个refundBet函数的下溢,让我们来看看这个函数的代码

智能合约游戏之殇——Dice2win安全分析

跟入getDiceWinAmount函数,发现jackpotFee并不可控

智能合约游戏之殇——Dice2win安全分析

其中 JACKPOT_FEE = 0.001 ether ,且要保证amount大于0.1 ether,amount来自bet变量

智能合约游戏之殇——Dice2win安全分析

而bet变量只有在placebet中可以被设置。

智能合约游戏之殇——Dice2win安全分析

但可惜的是,placebet中会进行一次相同的调用

智能合约游戏之殇——Dice2win安全分析

所以这里并不会存在溢出问题。

总结

在回溯分析完整个Dice2win合约之后,我们不难发现,由于智能合约和传统的服务端逻辑不同,导致许多我们惯用的安全思路遇到了更多问题,区块链的不可信原则直接导致了随机数生成方式的难度加深。目前最为成熟的 hash-commit-reveal 方法是属于通过服务端与智能合约交互实现的,在随机数保密方面完成度很高,可惜的是无法避免服务端获取过多信息的问题。

hash-commit-reveal 方法的基础上,只要服务端不能即时响应开奖,选择中止攻击就始终存在。有趣的是Dice2win合约中试图实现的Merkle proof功能初衷是为了更快的开奖,但反而却在一定程度上减少了选择中止攻击的可能性。

任意开奖攻击,是一个针对Merkle proof的攻击方式,应验了所谓的功能越多漏洞越多的问题。攻击方式精巧,是一种很有趣的利用方式。

就目前为止,无论是底层的机制也好,又或是随机数的生成方式也好,智能合约的安全还有很长的路要走。


以上所述就是小编给大家介绍的《智能合约游戏之殇——Dice2win安全分析》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

数据结构与算法JavaScript描述

数据结构与算法JavaScript描述

[美] Michael McMillan / 王群锋、杜 欢 / 人民邮电出版社 / 2014-8 / 49.00元

通过本书的学习,读者将能自如地选择最合适的数据结构与算法,并在JavaScript开发中懂得权衡使用。此外,本书也概述了与数据结构与算法相关的JavaScript特性。 本书主要内容如下。 数组和列表:最常用的数据结构。 栈和队列:与列表类似但更复杂的数据结构。 链表:如何通过它们克服数组的不足。 字典:将数据以键-值对的形式存储。 散列:适用于快速查找和检索。......一起来看看 《数据结构与算法JavaScript描述》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具