一、前言
前幾天全國大學(xué)生信息安全競賽初賽如期進行,在這次比賽中也看到了區(qū)塊鏈題目的身影。所以我將題目拿來進行分析,并為后續(xù)的比賽賽題提供一些分析思路。
由于本次比賽我并沒有參加,所以我并沒有Flag等相關(guān)信息,但是我拿到了比賽中的相關(guān)文件以及合約地址并在此基礎(chǔ)上進行的詳細分析,希望能幫助到進行研究的同學(xué)。
二、題目分析
拿到題目后,我們只得到了兩個內(nèi)容,一個是合約的地址,一個是broken.so。
pragma solidity ^0.4.24;
contract DaysBank {
mapping(address => uint) public balanceOf;
mapping(address => uint) public gift;
address owner;
constructor()public{
owner = msg.sender;
}
event SendFlag(uint256 flagnum, string b64email);
function payforflag(string b64email) public {
require(balanceOf[msg.sender] >= 10000);
emit SendFlag(1,b64email);
}
首先我們看這個合約文件。合約開始定義了兩個mapping變量——balanceOf 與gift,之后為構(gòu)造函數(shù),以及發(fā)送flag的事件。當(dāng)我們調(diào)用payforflag函數(shù)并傳入使用base64加密的郵件地址之后,需要滿足當(dāng)前賬戶的余額比10000多。
由這第一手信息我們可以進行一些簡單的猜想。這道題目需要領(lǐng)自己的余額大于10000,只有這樣才能購買flag。這也是很常見的題目類型。而這個題目十分設(shè)計的還是十分巧妙的,我們接著向下看。
根據(jù)上面的合約代碼,我們并不能得到更多的有用信息。然而此時我們就需要利用合約地址來進一步分析。
此處合約地址為:0x455541c3e9179a6cd8C418142855d894e11A288c。
我們訪問公鏈信息看看是否能夠訪問到有價值的信息:
https://ropsten.etherscan.io/address/0x455541c3e9179a6cd8c418142855d894e11a288c#code

發(fā)現(xiàn)出題人并沒有公開源代碼,只有ABI碼,此時我們只能根據(jù)此來進行合約逆向來尋找更有用的解題思路。
https://ethervm.io/decompile#func_profit
在此網(wǎng)站中進行逆向分析后,我們得到如下代碼:

contract Contract {
function main() {
memory[0x40:0x60] = 0x80;
if (msg.data.length < 0x04) { revert(memory[0x00:0x00]); }
var var0 = msg.data[0x00:0x20] / 0x0100000000000000000000000000000000000000000000000000000000 & 0xffffffff;
if (var0 == 0x652e9d91) {
// Dispatch table entry for 0x652e9d91 (unknown)
var var1 = msg.value;
if (var1) { revert(memory[0x00:0x00]); }
var1 = 0x009c;
func_01DC();
stop();
} else if (var0 == 0x66d16cc3) {
// Dispatch table entry for profit()
var1 = msg.value;
if (var1) { revert(memory[0x00:0x00]); }
var1 = 0x009c;
profit();
stop();
} else if (var0 == 0x6bc344bc) {
// Dispatch table entry for 0x6bc344bc (unknown)
var1 = msg.value;
if (var1) { revert(memory[0x00:0x00]); }
var temp0 = memory[0x40:0x60];
var temp1 = msg.data[0x04:0x24];
var temp2 = msg.data[temp1 + 0x04:temp1 + 0x04 + 0x20];
memory[0x40:0x60] = temp0 + (temp2 + 0x1f) / 0x20 * 0x20 + 0x20;
memory[temp0:temp0 + 0x20] = temp2;
var1 = 0x009c;
memory[temp0 + 0x20:temp0 + 0x20 + temp2] = msg.data[temp1 + 0x24:temp1 + 0x24 + temp2];
var var2 = temp0;
func_0278(var2);
stop();
} else if (var0 == 0x70a08231) {
// Dispatch table entry for balanceOf(address)
var1 = msg.value;
if (var1) { revert(memory[0x00:0x00]); }
var1 = 0x013a;
var2 = msg.data[0x04:0x24] & 0xffffffffffffffffffffffffffffffffffffffff;
var2 = balanceOf(var2);
label_013A:
var temp3 = memory[0x40:0x60];
memory[temp3:temp3 + 0x20] = var2;
var temp4 = memory[0x40:0x60];
return memory[temp4:temp4 + temp3 - temp4 + 0x20];
} else if (var0 == 0x7ce7c990) {
// Dispatch table entry for transfer2(address,uint256)
var1 = msg.value;
if (var1) { revert(memory[0x00:0x00]); }
var1 = 0x009c;
var2 = msg.data[0x04:0x24] & 0xffffffffffffffffffffffffffffffffffffffff;
var var3 = msg.data[0x24:0x44];
transfer2(var2, var3);
stop();
} else if (var0 == 0xa9059cbb) {
// Dispatch table entry for transfer(address,uint256)
var1 = msg.value;
if (var1) { revert(memory[0x00:0x00]); }
var1 = 0x009c;
var2 = msg.data[0x04:0x24] & 0xffffffffffffffffffffffffffffffffffffffff;
var3 = msg.data[0x24:0x44];
transfer(var2, var3);
stop();
} else if (var0 == 0xcbfc4bce) {
// Dispatch table entry for 0xcbfc4bce (unknown)
var1 = msg.value;
if (var1) { revert(memory[0x00:0x00]); }
var1 = 0x013a;
var2 = msg.data[0x04:0x24] & 0xffffffffffffffffffffffffffffffffffffffff;
var2 = func_0417(var2);
goto label_013A;
} else { revert(memory[0x00:0x00]); }
}
//0x66d16cc3函數(shù) 空投函數(shù)??
function func_01DC() {
memory[0x00:0x20] = msg.sender;
memory[0x20:0x40] = 0x01;
// 如果gift已經(jīng)存在,revert
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] + 0x01;
memory[0x20:0x40] = 0x01;
storage[keccak256(memory[0x00:0x40])] = 0x01;
}
// 利潤函數(shù):
function profit() {
memory[0x00:0x20] = msg.sender;
memory[0x20:0x40] = 0x00;
if (storage[keccak256(memory[0x00:0x40])] != 0x01) { revert(memory[0x00:0x00]); }
memory[0x00:0x20] = msg.sender;
memory[0x20:0x40] = 0x01;
if (storage[keccak256(memory[0x00:0x40])] != 0x01) { revert(memory[0x00:0x00]); }
memory[0x00:0x20] = msg.sender;
memory[0x20:0x40] = 0x00;
var temp0 = keccak256(memory[0x00:0x40]);
storage[temp0] = storage[temp0] + 0x01;
memory[0x20:0x40] = 0x01;
storage[keccak256(memory[0x00:0x40])] = 0x02;
}
function func_0278(var arg0) {
memory[0x00:0x20] = msg.sender;
memory[0x20:0x40] = 0x00;
if (0x2710 > storage[keccak256(memory[0x00:0x40])]) { revert(memory[0x00:0x00]); }
var var0 = 0xb1bc9a9c599feac73a94c3ba415fa0b75cbe44496bfda818a9b4a689efb7adba;
var var1 = 0x01;
var temp0 = arg0;
var var2 = temp0;
var temp1 = memory[0x40:0x60];
var var3 = temp1;
memory[var3:var3 + 0x20] = var1;
var temp2 = var3 + 0x20;
var var4 = temp2;
var temp3 = var4 + 0x20;
memory[var4:var4 + 0x20] = temp3 - var3;
memory[temp3:temp3 + 0x20] = memory[var2:var2 + 0x20];
var var5 = temp3 + 0x20;
var var7 = memory[var2:var2 + 0x20];
var var6 = var2 + 0x20;
var var8 = var7;
var var9 = var5;
var var10 = var6;
var var11 = 0x00;
if (var11 >= var8) {
label_02FD:
var temp4 = var7;
var5 = temp4 + var5;
var6 = temp4 & 0x1f;
if (!var6) {
var temp5 = memory[0x40:0x60];
log(memory[temp5:temp5 + var5 - temp5], [stack[-7]]);
return;
} else {
var temp6 = var6;
var temp7 = var5 - temp6;
memory[temp7:temp7 + 0x20] = ~(0x0100 ** (0x20 - temp6) - 0x01) & memory[temp7:temp7 + 0x20];
var temp8 = memory[0x40:0x60];
log(memory[temp8:temp8 + (temp7 + 0x20) - temp8], [stack[-7]]);
return;
}
} else {
label_02EE:
var temp9 = var11;
memory[temp9 + var9:temp9 + var9 + 0x20] = memory[temp9 + var10:temp9 + var10 + 0x20];
var11 = temp9 + 0x20;
if (var11 >= var8) { goto label_02FD; }
else { goto label_02EE; }
}
}
function balanceOf(var arg0) returns (var arg0) {
memory[0x20:0x40] = 0x00;
memory[0x00:0x20] = arg0;
return storage[keccak256(memory[0x00:0x40])];
}
function transfer2(var arg0, var arg1) {
if (arg1 <= 0x02) { revert(memory[0x00:0x00]); }
memory[0x00:0x20] = msg.sender;
memory[0x20:0x40] = 0x00;
if (0x02 >= storage[keccak256(memory[0x00:0x40])]) { revert(memory[0x00:0x00]); }
memory[0x00:0x20] = msg.sender;
memory[0x20:0x40] = 0x00;
if (storage[keccak256(memory[0x00:0x40])] - arg1 <= 0x00) { 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];
}
function transfer(var arg0, var arg1) {
if (arg1 <= 0x01) { revert(memory[0x00:0x00]); }
memory[0x00:0x20] = msg.sender;
memory[0x20:0x40] = 0x00;
if (0x01 >= storage[keccak256(memory[0x00:0x40])]) { revert(memory[0x00:0x00]); }
memory[0x00:0x20] = msg.sender;
memory[0x20:0x40] = 0x00;
// 如果arg1大于余額,revert
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;
// 地址arg0的余額增加arg1的個數(shù)
memory[0x00:0x20] = arg0 & 0xffffffffffffffffffffffffffffffffffffffff;
var temp2 = keccak256(memory[0x00:0x40]);
storage[temp2] = temp1 + storage[temp2];
}
function func_0417(var arg0) returns (var arg0) {
memory[0x20:0x40] = 0x01;
memory[0x00:0x20] = arg0;
return storage[keccak256(memory[0x00:0x40])];
}
}
之后我們針對此逆向后的代碼進行分析。
我們經(jīng)過分析發(fā)現(xiàn)了如下的public函數(shù):

很明顯這是代幣合約,并且可以進行轉(zhuǎn)賬。而此代碼中擁有兩個轉(zhuǎn)賬函數(shù)。并且可以查看余額。
我們具體根據(jù)代碼對函數(shù)詳細分析:

首先我們分析編號為0x652e9d91的func_01DC()函數(shù)。
首先合約將內(nèi)存切換到0x01位置,此處為:mapping(address => uint) public gift;
memory[0x00:0x20] = msg.sender;
memory[0x20:0x40] = 0x01;
即合約首先要判斷該用戶的gift是否為0,若不為0則revert(也就是說這個函數(shù)要保證只能領(lǐng)取一次)。
之后內(nèi)存切換到mapping(address => uint) public balanceOf;。
對此變量進行操作,也就是將用戶的余額值+1。并將gift值加一。
profit()函數(shù)的分析如下:

根據(jù)函數(shù)的名稱我們也知道,此函數(shù)為利潤函數(shù),其目的也很明顯,根據(jù)我們的代幣背景知識,我們猜測這個函數(shù)是用來贈送代幣的。
函數(shù)要求balanceOf與gift必須==1,不然就會revert。當(dāng)調(diào)用此函數(shù)時,當(dāng)滿足上述條件后就會給用戶的余額+1,令用戶余額為2 。
balanceOf()函數(shù)
這個函數(shù)很簡單,就是返回用戶的余額情況。
下面我們來看兩個關(guān)鍵的轉(zhuǎn)賬函數(shù):
transfer()

函數(shù)同樣比較簡單。
首先需要判斷用戶的余額是否小于1 。之后判斷轉(zhuǎn)賬的金額(arg1)是否大于余額,如果用戶余額不足以進行轉(zhuǎn)賬,那么就會revert。
之后將當(dāng)前用戶的賬面上減掉arg1代幣數(shù)量,將收款方arg0的賬戶上增加arg1代幣數(shù)量。
我們可以適當(dāng)還原此函數(shù):
function transfer(var arg0, var arg1){
if(arg1<=1) revert();
if(balance(msg.sender)<=1) revert();
if(balance(msg.sender)<arg1) revert();
balance(msg.sender) = balance(msg.sender) - arg1;
balance(arg0) = balance(arg0) + arg1;
}
此時我們看transfer2()函數(shù)。

在看到這個函數(shù)前我就疑問為何一個代幣中有兩個轉(zhuǎn)賬函數(shù)?后來在分析了源碼后我了解到第二個轉(zhuǎn)賬函數(shù)中就存在漏洞。具體如下:
開始時函數(shù)判斷arg1需要>2,即轉(zhuǎn)賬數(shù)量要大于2. 。
之后判斷用戶余額需要大于等于2.
滿足條件后需要令(余額 - arg1)大于零。即其本意是要用戶余額大于轉(zhuǎn)賬金額。
之后進行轉(zhuǎn)賬后的余額更新。
我們分析該代碼后將合約具體代碼進行還原:
function transfer2(var arg0, var arg1){
require(arg1>2);
require(balance(msg.sender) >= 2);
require(balance(msg.sender) - arg1 >= 0);
balance(msg.sender) = balance(msg.sender) - arg1;
balance(arg0) = balance(arg0) + arg1;
}
不知用戶是否發(fā)現(xiàn),我們就看到了漏洞點了,這是一個典型的溢出漏洞。

根據(jù)作者給出的代碼,我們發(fā)現(xiàn)其具體余額是使用uint定義的,由于uint的位數(shù)是有限的,并且其不支持負(fù)數(shù)。所以當(dāng)其負(fù)數(shù)溢出時就會變成一個很大的正數(shù)。
而根據(jù)我們的transfer2函數(shù)內(nèi)容,我們知道:require(balance(msg.sender) - arg1 >= 0);。此句進行判斷的時候是將用戶余額減去一個arg1來判斷是否大于0的。而如果arg1設(shè)置一個比較大的數(shù),那么balance(msg.sender) - arg1就會溢出為一個非常大的數(shù),此時就成功繞過了檢測并且轉(zhuǎn)賬大量的代幣。
所以我們可以利用此處的整數(shù)溢出來進行題目求解,然而在分析的過程中我又發(fā)現(xiàn)了另一個解法。
如果做題人沒有發(fā)現(xiàn)此處的漏洞點,我們可以利用常規(guī)做法來進行求解。

根據(jù)給出的flag函數(shù)我們知道,我們只需要余額>10000即可,那么我們可以發(fā)現(xiàn),我們的profit函數(shù)可以給我們不斷的新增錢。
根據(jù)我們的分析,我們需要令合約余額==1并且gitf==1,此時即可調(diào)用profit()來將余額++,調(diào)用后余額為2,gift為1 。這時候?qū)⒂囝~轉(zhuǎn)給第二個賬戶,余額就又變成1了,就又可以調(diào)用profit()函數(shù)。這樣不斷給第二個用戶轉(zhuǎn)賬,轉(zhuǎn)賬10000次即可。(這里肯定是要用腳本去寫,手動轉(zhuǎn)賬比較傻emmmm)
三、漏洞利用技巧
此處我們介紹漏洞利用的技巧。
首先我們需要擁有兩個錢包地址(Addr1 Addr2)。
此時我們令
Addr1調(diào)用func_01DC()函數(shù)領(lǐng)取1個代幣以及1個gift。之后我們調(diào)用
profit領(lǐng)取一個代幣。此時余額為2,gift為1 。
由于transfer2需要余額大于2才能調(diào)用,所以我們首先令A(yù)ddr2同樣執(zhí)行上面的兩步。此時兩個錢包均有余額為2 。
- 這時候Adde1調(diào)用
transfer給Addr2轉(zhuǎn)賬兩個代幣,此時Addr余額為0,Addr2為4 。
之后Addr2就可以調(diào)用transfer2給Adde1轉(zhuǎn)賬一個非常大的金額。達到溢出效果。此時Addr1與Addr2均擁有了大量的代幣(Addr2為溢出得到,Addr1為轉(zhuǎn)賬得到)。任意地址均可以調(diào)用flag函數(shù)。
具體的交易日志如下:




此時flag就被調(diào)用發(fā)送到用戶賬戶上了。
四、總結(jié)
本次題目非常巧妙,如果后面的同學(xué)想直接查看交易日志是非常難通過一個賬戶來進行跟蹤的。并且本題目沒有公布合約,所以考驗?zāi)嫦蚰芰?。但是只要逆出來后就是一道比較簡單的題目,沒有完全逆出來的同學(xué)也可以使用常規(guī)做法進行不斷轉(zhuǎn)賬來使余額滿足要求。希望本文對大家之后的研究有所幫助。歡迎討論。
本稿為原創(chuàng)稿件,轉(zhuǎn)載請標(biāo)明出處。謝謝。
首發(fā):[https://xz.aliyun.com/t/4982](https://xz.aliyun.com/t/4982)