美文网首页区块链故事
区块链安全—守株待兔的蜜罐合约(二)

区块链安全—守株待兔的蜜罐合约(二)

作者: CPinging | 来源:发表于2019-10-30 19:17 被阅读0次

    一、前言

    在前一篇的蜜罐合约中,我们介绍并测试了部分由于继承等问题而搭建的蜜罐合约。蜜罐合约顾名思义,就是利用了受害者的投机想法,从而另普通用户自行进行转账的行为。在我们文章中演示的相关合约对owner友好,即普通用户很难从合约中获得利益,所以读者如果看到类似的合约请不要轻易的使用以太币进行尝试。

    而本文中,我们在蜜罐合约之上分析由Solidity的结构体产生的漏洞,而此漏洞危害性极大,倘若合约开发不到位会导致owner的篡改,即普通用户的提权操作。

    二、由合约漏洞而导致的蜜罐

    1 蜜罐合约介绍

    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();
        }
    
    }
    

    下面我们简单的分析一下这个类彩票合约。

    为何称这个合约为蜜罐合约么?我们根据合约内容可以知道,合约在起始时赋值LuckyNumber为1,而在参与函数中根据参与者的地址生成随机数0 or 1,之后如果为1,那么就返还value * 1.9的赌金。看似0.5的高概率,但是合约利用了一种以太坊的bug,从而导致用户永远不可能取到钱。下面请看我们的分析。

    首先,合约定义了一个结构体。(我认为本来不需要结构体这样的类型来进行随机数的生成,所以我觉得这里的结构体是为了触发合约的漏洞)

        struct SeedComponents{
            uint component1;
            uint component2;
            uint component3;
            uint component4;
        }
    

    之后定义了五个变量,分别代表合约的owner、随机数种子、上一次的记录值、幸运数、竞猜获胜者集合

        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
        }
    

    构造函数将owner赋初值为合约创建者,之后调用reseed函数。而我们下面就看一看这个函数的作用。

        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;
        }
    

    在这个函数中,我们会传入components结构体,并使用keccak256 ()哈希函数更新secretSeed的值,并初始化lastReseed

    也就是说,我们在构造函数中调用此函数来更新secretSeed的值。

    之后,我们来看participate(),此函数是用户调用参与接口,用于竞猜的环节。

        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
        }
    

    在函数中,我们看到用户必须传入value >= 0.1 eth,并且用户还未赢得过奖励。之后合约会将LuckyNumberluckyNumberOfAddress(msg.sender)进行比较。倘若两者的值相等,那么记录下该用户的中奖记录并进行【value * 1.9】的转账奖励(余额不足的将所有余额转入)。

    而我们在看luckyNumberOfAddress函数。

        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
        }
    

    传入一个地址,之后根据传入的地址产生随机数,并%2,得到1或0 。

    然后是一个测试函数forceReseed

        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
        }
    

    合约创建者在这个函数后面添加了注释//reseed initiated by the owner - for testing purposes。表达用于测试的目的。

    然而问题就是出在这个地方。

    整体来看,这个合约并没有什么问题。gamble的过程也十分清晰。

    image.png

    然而我们进行一个合约测试。

    2 攻击手段分析

    我们先看一个测试合约:

    pragma solidity ^0.4.24;
    
    contract test
    {
        address public addr = 0xa;
        uint    public b    = 555;
        uint256 public c    = 666;
        bytes   public d    = "abcd";
    
        struct Seed{
            uint256 component1;
            uint256 component2;
            uint256 component3;
            uint256 component4;
        }
    
        function change() public{
            Seed s;
            s.component1 = 1;
            s.component2 = 2;
            s.component3 = 3;
            s.component4 = 4;
        }
    }
    

    在这个合约中,我们设置了4个变量,而这四个变量均有初始值。之后我们又设置了结构体Seed。在这个结构体中拥有四个变量,而我们在test()函数中初始化结构体并赋初值,之后我们看看效果。

    部署合约:


    image.png

    查看变量内容:

    image.png

    之后我们调用change函数。并查看,发现我们的变量被修改了,而修改的内容就是结构体中的内容。

    image.png

    这就是我们的漏洞所在。

    我们的合约中并没有修改变量的值,但是由于solidity机制的问题而导致了变量修改问题。

    而这个漏洞对我们上述介绍的蜜罐合约有什么影响呢?我们进行一下测试。

    为了方便我们查看测试效果,我们为LuckyNumber添加查看函数。

    image.png image.png

    倘若此时owner不进行任何操作,任凭用户进行下一步的赌博,那么用户还是有很大的概率获得奖励的。例如:(为了方便演示,我在函数中添加了event事件)

    emit back(msg.sender,win,true);

    此时我们能够看到,LuckyNumber是初始值1 。

    image.png

    之后,我们更换用户进行参与。我们投入1 eth进行竞猜。

    第一次:

    image.png

    没有获得奖励,所以1 eth赔进去了。

    继续更换用户参与:

    image.png

    直到最后一个用户:

    image.png

    我们得到了奖励金1900000000000000000 wei,所以竞猜成功。

    而我们大致能够发现,其实我们是拥有很大的概率获得奖励的。这个合约真的就是拼概率的传统赌博合约吗?然而事实并非如此。

    根据我们前文所测试的漏洞,这个合约中同样存在恶意篡改的行为。我们发现了合约中其实存在着结构体

    image.png

    而这个结构体在合约中存在修改函数:

    image.png

    所以,如果owner调用了此函数,那么会不会发起漏洞从而将竞猜值恶意修改呢?

    我们更换地址为owner,并且调用此函数。

    image.png

    我们惊奇的发现,果然此时的竞猜值从1变成了7 。

    而我们合约中的判断条件是luckyNumberOfAddress(msg.sender) == LuckyNumber。而我们函数中luckyNumberOfAddress(msg.sender)只能是0或者1两种可能。这里的LuckNumber是7,也就是说无论我们如何竞猜,永远都不会成功。

    image.png

    三、赌博?庄家永远更胜一筹

    在看完上述的高级蜜罐后,我们来看一下常规的蜜罐合约。

    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 { }
    }
    

    为什么说蜜罐的owner更胜一筹呢?我们在阅读了合约的所有函数内容后就知道,在合约中的shuffle()函数%20,也就意味着它最后的范围是0~19,而用户能够传入的数是多少呢?在play()函数中,用户需要传入一个number,而其规定值<=10。

    image.png

    其概率值相对来说还是极低的。并且在一天之后,倘若用户还未猜对那么owner便可以调用kill()函数进行自杀操作。将余额转入到自己的账户中。

    四、参考链接

    本稿为原创稿件,转载请标明出处。谢谢。

    相关文章

      网友评论

        本文标题:区块链安全—守株待兔的蜜罐合约(二)

        本文链接:https://www.haomeiwen.com/subject/vbpasqtx.html