有趣的智能合约蜜罐(下)

本文深入分析了一系列基于区块链的赌博游戏智能合约,揭示了庄家如何通过精心设计的逻辑确保稳赢。文章介绍了加密轮盘赌轮、开放地址彩票、山丘之王和以太币竞争游戏四种合约,分析了其中的蜜罐陷阱,如变量覆盖、整数溢出和旧版编译器漏洞。此外,还探讨了黑客如何利用这些漏洞进行攻击,并通过代码复现实验验证了合约中的欺诈行为。
摘要由CSDN通过智能技术生成

1. 概述

在有趣的智能合约蜜罐(上)中我们对古老的欺骗手段和神奇的逻辑漏洞进行了讲解和复现,在下部分中我们将会对新颖的赌博游戏和黑客的漏洞利用进行讲解以及复现,从而进一步增加对智能合约蜜罐的了解。

同样的,所有的智能合约蜜罐代码都可以 GitHub 上找到,这里再次给出他们的网址:

  • smart-contract-honey
  • Solidlity-Vulnerable

2. 新颖的赌博游戏

赌博行业从古至今一直存在,而区块链的去中心化似乎给赌博行业带了新的机会,它的进入会让人们觉得赌博变得公平,然而我们都知道赌博结果往往都是必输,那么接下来就通过分析四个基于区块链的赌博游戏合约来介绍庄家是如何最后稳赢的。

2.1 加密轮盘赌轮:CryptoRoulette

2.1.1 蜜罐分析

第一个要介绍的是 CryptoRoulette,它译为「加密轮盘赌轮」。

蜜罐的完整代码如下:

// https://github.com/misterch0c/Solidlity-Vulnerable/blob/master/traps/CryptoRoulette.sol
// https://etherscan.io/address/0x94602b0E2512DdAd62a935763BF1277c973B2758

pragma solidity ^0.4.19;

// CryptoRoulette
//
// Guess the number secretly stored in the blockchain and win the whole contract balance!
// A new number is randomly chosen after each try.
//
// To play, call the play() method with the guessed number (1-20).  Bet price: 0.1 ether

contract CryptoRoulette {

    uint256 private secretNumber;
    uint256 public lastPlayed;
    uint256 public betPrice = 0.1 ether;
    address public ownerAddr;

    struct Game {
        address player;
        uint256 number;
    }
    Game[] public gamesPlayed;

    function CryptoRoulette() public {
        ownerAddr = msg.sender;
        shuffle();
    }

    function shuffle() internal {
        // randomly set secretNumber with a value between 1 and 20
        secretNumber = uint8(sha3(now, block.blockhash(block.number-1))) % 20 + 1;
    }

    function play(uint256 number) payable public {
        require(msg.value >= betPrice && number <= 10);

        Game game;
        game.player = msg.sender;
        game.number = number;
        gamesPlayed.push(game);

        if (number == secretNumber) {
            // win!
            msg.sender.transfer(this.balance);
        }

        shuffle();
        lastPlayed = now;
    }

    function kill() public {
        if (msg.sender == ownerAddr && now > lastPlayed + 1 days) {
            suicide(msg.sender);
        }
    }

    function() public payable { }
}

该合约设置了一个私有属性的随机数 secretNumber,在 shuffle() 函数中被指定范围在 1 - 20,玩家可以通过 play() 函数去盲猜这个随机数,如果猜对了就可以将合约中的所有钱取走,每次调用 play() 函数后都会重置随机数。这么看来这个合约好像没有什么问题,随着猜错的玩家越来越多,合约中的代币余额也会积累的越多,如果碰巧猜对了就可以获取所有的奖金,然而事实是这样的嘛?我们可以看到在这个蜜罐合约中,最重要的就是 shuffle()play() 这两个函数,下面就来分析下这两个函数。

初始的 secretNumber 是在构造函数 CryptoRoulette 中调用 shuffle() 函数,而 shuffle() 函数中只有一行代码,就是设置 secretNumber 的值,从代码中也可以看出 secretNumber 的值既和区块的数目有关,也和时间有关。函数代码如下:

    function shuffle() internal { // 设置随机数 secretNumber
        // randomly set secretNumber with a value between 1 and 20
        secretNumber = uint8(sha3(now, block.blockhash(block.number-1))) % 20 + 1; // 对 20 取余 再加 1,所以范围在 1 - 20
    }

play() 函数就是提供给用户进行赌博来猜这个随机数的,玩家携带不小于 0.1 eth 并传入自己猜的数字 number,玩家猜的这个数字 number 去和 secretNumber 进行比较,如果相等就可以获胜,转走合约中的所有以太币,但是在函数的开头中有一个检查 require,其中后面要求玩家猜的数字不能大于 10,而 secretNumber 我们在上面的函数中讲到范围是 1 - 20,这样看来虽然加大了难度,但是也存在猜对可能性,然而事实是 secretNumber 一定会大于 10,玩家永远都不可能猜对数字,合约所有者却可以通过调用 kill() 函数转走合约中的所有以太币。函数代码如下:

    function play(uint256 number) payable public { // 玩游戏竞猜数字
        require(msg.value >= betPrice && number <= 10); // 要求 msg.value 不小于 0.1 eth 且 number 要不大于 10

        Game game;
        game.player = msg.sender; // 游戏玩家为调用者
        game.number = number; // 游戏的数字为 number
        gamesPlayed.push(game); // 加入游戏列表

        if (number == secretNumber) { // 如果 number 为 secretNumber 则游戏胜利
            // win!
            msg.sender.transfer(this.balance); // 将合约中所有的以太币转给调用者
        }

        shuffle(); // 执行 shuffle 重置随机数
        lastPlayed = now; // 设置最后一个玩的时间为现在
    }

这里会有人问了,secretNumber 为啥一定会大于 10 呢?原因就是结构体 game 的初始化对存储数据 secretNumber 的覆盖,我们在函数里直接初始化结构体必须加 memory 关键字,因为 memory 是使用内存来进行存储,这样一来就可以避免占用 storage 的存储位,而蜜罐合约中并未使用 memory 关键字,从而导致了变量覆盖。该问题在 Solidity 0.5.0 版本以前只是进行了提示,并没有做出错误警告,所以在老版本编译器中要注意该问题。在下面的代码复现中可以看到问题所在。

2.1.2 代码复现

将蜜罐合约的代码复制到 Remix IDE 中,为了方便我们查看 secretNumber 的值,我们将 secretNumber 的类型设置为 public,这样就可以在 Remix IDE 中直接看到它的值了。甚至有些蜜罐部署者为了诱惑攻击者来攻击合约,也可以设置为 public 属性,因为就算告诉攻击者 secretNumber 的值他也不能猜对这个数字。

使用地址 0x5B3 点击「Deploy」部署合约,调用 secretNumber 查看初始随机数为 1,由于这里还没有初始化结构体也就不会覆盖随机数所以是正确的。

在这里插入图片描述
之后攻击者发现了该蜜罐合约,查看 secretNumber 为 1 并认为该合约可以进行攻击获利,所以在符合 play() 函数中的第一个判断条件情况下传入数字 1 和携带 1 个以太币进行函数调用,函数调用成功后查看账户余额发现账户余额不仅没有得到合约中的所有代币反而将刚才函数调用时携带的 1 个以太币也损失掉了。

在这里插入图片描述
为了探究具体原因我们对刚才的函数调用进行 Debug。

在这里插入图片描述
调试点击下一步直到第一个条件判断,此时 secretNumber 仍然为 1。

继续点击按钮进行下一步的调试,当进行到 game.player = msg.sender 时由于结构体 game 的初始化对存储数据 secretNumber 进行了覆盖,导致 secretNumber 变成了 msg.sender 的 uint256 内容,这样一来就使得后面的 if 判断条件不能成立,从而使得攻击者不能转走合约中的所有代币余额。

在这里插入图片描述

2.2 开放地址彩票:OpenAddressLottery

2.2.1 蜜罐分析

第二个要介绍的是 OpenAddressLottery,它译为「开发地址彩票」。

蜜罐的完整代码如下:

// https://etherscan.io/address/0xd1915A2bCC4B77794d64c4e483E43444193373Fa

pragma solidity ^0.4.19;
/*
 * This is a distributed lottery that chooses random addresses as lucky addresses. If these
 * participate, they get the jackpot: 1.9 times the price of their bet.
 * Of course one address can only win once. The owner regularly reseeds the secret
 * seed of the contract (based on which the lucky addresses are chosen), so if you did not win,
 * just wait for a reseed and try again!
 *
 * Jackpot chance:   50%
 * Ticket price: Anything larger than (or equal to) 0.1 ETH
 * Jackpot size: 1.9 times the ticket price
 *
 * HOW TO PARTICIPATE: Just send any amount greater than (or equal to) 0.1 ETH to the contract's address
 * Keep in mind that your address can only win once
 *
 * If the contract doesn't have enough ETH to pay the jackpot, it sends the whole balance.
 *
 * Example: For each address, a random number is generated, either 0 or 1. This number is then compared
 * with the LuckyNumber - a constant 1. If they are equal, the contract will instantly send you the jackpot:
 * your bet multiplied by 1.9 (House edge of 0.1)
*/

contract OpenAddressLottery{
    struct SeedComponents{
        uint component1;
        uint component2;
        uint component3;
        uint component4;
    }
    
    address owner; //address of the owner
    uint private secretSeed; //seed used to calculate number of an address
    uint private lastReseed; //last reseed - used to automatically reseed the contract every 1000 blocks
    uint LuckyNumber = 1; //if the number of an address equals 1, it wins
        
    mapping (address => bool) winner; //keeping track of addresses that have already won
    
    function OpenAddressLottery() {
        owner = msg.sender;
        reseed(SeedComponents((uint)(block.coinbase), block.difficulty, block.gaslimit, block.timestamp)); //generate a quality random seed
    }
    
    function participate() payable {
        if(msg.value<0.1 ether)
            return; //verify ticket price
        
        // make sure he hasn't won already
        require(winner[msg.sender] == false);
        
        if(luckyNumberOfAddress(msg.sender) == LuckyNumber){ //check if it equals 1
            winner[msg.sender] = true; // every address can only win once
            
            uint win=(msg.value/10)*19; //win = 1.9 times the ticket price
            
            if(win>this.balance) //if the balance isnt sufficient...
                win=this.balance; //...send everything we've got
            msg.sender.transfer(win);
        }
        
        if(block.number-lastReseed>1000) //reseed if needed
            reseed(SeedComponents((uint)(block.coinbase), block.difficulty, block.gaslimit, block.timestamp)); //generate a quality random seed
    }
    
    function luckyNumberOfAddress(address addr) constant returns(uint n){
        // calculate the number of current address - 50% chance
        n = uint(keccak256(uint(addr), secretSeed)[0]) % 2; //mod 2 returns either 0 or 1
    }
    
    function reseed(SeedComponents components) internal {
        secretSeed = uint256(keccak256(
            components.component1,
            components.component2,
            components.component3,
            components.component4
        )); //hash the incoming parameters and use the hash to (re)initialize the seed
        lastReseed = block.number;
    }
    
    function kill() {
        require(msg.sender==owner);
        
        selfdestruct(msg.sender);
    }
    
    function forceReseed() { //reseed initiated by the owner - for testing purposes
        require(msg.sender==owner);
        
        SeedComponents s;
        s.component1 = uint(msg.sender);
        s.component2 = uint256(block.blockhash(block.number - 1));
        s.component3 = block.difficulty*(uint)(block.coinbase);
        s.component4 = tx.gasprice * 7;
        
        reseed(s); //reseed
    }
    
    function () payable { //if someone sends money without any function call, just assume he wanted to participate
        if(msg.value>=0.1 ether && msg.sender!=owner) //owner can't participate, he can only fund the jackpot
            participate();
    }

}

蜜罐合约 OpenAddressLottery 的游戏逻辑很简单,合约中有一个初始值为 1 的状态变量 LuckyNumber,竞猜者每次竞猜时都会根据其地址随即生成 0 或者 1,如果生成的值和 LuckyNumber 一样,那么竞猜者就可以获得 1.9 倍的奖金,且每个地址只能赢得一次游戏胜利,之后将无法继续参加竞猜。该蜜罐合约的重点就在于 participate()luckyNumberOfAddress()forceReseed() 函数,下面来对这 3 个函数进行依次讲解。

首先是 participate() 函数,这是用户参与竞猜的函数:


                
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值