一、前言
時隔這么多天終于有時間把當(dāng)時數(shù)字經(jīng)濟(jì)第二道區(qū)塊鏈題目拿來復(fù)現(xiàn)。感覺第二題更偏向邏輯方面的漏洞,說白了就是來考察做題人對合約的逆向能力??偨Y(jié)來說,以太坊的漏洞相對于其他類型問題來說還算是非常少的,所以逆合約是一個非常重要的手段,只要能完美的逆出來合約,剩下的就相對容易許多。

比賽包括兩道題目,這里我們分析一下第二道題目,第一題我們見https://xz.aliyun.com/t/6602。
二、題目描述
如上圖所述,拿到題目我們只能看到常規(guī)操作,即給了合約地址與發(fā)送flag的函數(shù)。為了加大難度,作者并沒有給合約的源碼信息,所以我們只能用最笨但是最有效的方法去逆合約。
讀者可以訪問這個網(wǎng)站來查詢逆向合約信息:https://ethervm.io/decompile/ropsten/0xc9B91F149d3699474a0E680D55da62FBD3a51485
我們這里拿到了合約的函數(shù)信息,現(xiàn)在我們查看具體的函數(shù)代碼,并嘗試逆出來合約函數(shù)的具體含義。

這里我們放出關(guān)鍵代碼:
function func_0293(var arg0) {
var var0 = 0x00;
memory[0x00:0x20] = msg.sender;
memory[0x20:0x40] = 0x03;
if (storage[keccak256(memory[0x00:0x40])] <= var0) { revert(memory[0x00:0x00]); }
var var1 = 0x0de0b6b3a7640000;
var var2 = msg.value;
if (!var1) { assert(); }
var0 = var2 / var1;
if (arg0 != storage[0x01]) {
memory[0x00:0x20] = msg.sender;
memory[0x20:0x40] = 0x03;
storage[keccak256(memory[0x00:0x40])] = 0x00;
storage[0x02] = 0x01;
return;
} else {
memory[0x00:0x20] = msg.sender;
memory[0x20:0x40] = 0x03;
var temp0 = keccak256(memory[0x00:0x40]);
storage[temp0] = storage[temp0] + var0 * storage[0x02];
storage[0x02] = 0x01;
return;
}
}
function func_03B2(var arg0) {
var var0 = 0x00;
memory[0x00:0x20] = msg.sender;
memory[0x20:0x40] = 0x03;
if (storage[keccak256(memory[0x00:0x40])] <= var0) { revert(memory[0x00:0x00]); }
if (arg0 & 0xffffffffffffffffffffffffffffffffffffffff == 0x00) {
var temp0 = var0;
var temp1 = temp0;
storage[temp1] = msg.sender | (storage[temp1] & ~0xffffffffffffffffffffffffffffffffffffffff);
memory[0x00:0x20] = msg.sender;
memory[0x20:0x40] = 0x03;
storage[temp0 + 0x01] = storage[keccak256(memory[0x00:0x40])];
memory[0x00:0x20] = msg.sender;
memory[0x20:0x40] = 0x03;
storage[keccak256(memory[0x00:0x40])] = 0x00;
return;
} else {
memory[0x00:0x20] = msg.sender;
memory[0x20:0x40] = 0x03;
var temp2 = storage[keccak256(memory[0x00:0x40])];
memory[0x00:0x20] = arg0 & 0xffffffffffffffffffffffffffffffffffffffff;
memory[0x20:0x40] = 0x03;
storage[keccak256(memory[0x00:0x40])] = temp2;
memory[0x00:0x20] = msg.sender;
memory[0x20:0x40] = 0x03;
storage[keccak256(memory[0x00:0x40])] = 0x00;
return;
}
}
function airdrop() {
memory[0x00:0x20] = msg.sender;
memory[0x20:0x40] = 0x04;
if (storage[keccak256(memory[0x00:0x40])] != 0x00) { revert(memory[0x00:0x00]); }
memory[0x00:0x20] = msg.sender;
memory[0x20:0x40] = 0x04;
memory[0x00:0x20] = msg.sender;
memory[0x20:0x40] = 0x03;
var temp0 = keccak256(memory[0x00:0x40]);
storage[temp0] = storage[temp0] + 0x01;
}
function payforflag(var arg0) {
memory[0x00:0x20] = msg.sender;
memory[0x20:0x40] = 0x03;
if (storage[keccak256(memory[0x00:0x40])] <= 0x0f4240) { revert(memory[0x00:0x00]); }
memory[0x00:0x20] = msg.sender;
memory[0x20:0x40] = 0x03;
storage[keccak256(memory[0x00:0x40])] = 0x00;
storage[0x02] = 0x01;
var temp0 = address(address(this)).balance;
var temp1 = memory[0x40:0x60];
var temp2;
temp2, memory[temp1:temp1 + 0x00] = address(storage[0x05] & 0xffffffffffffffffffffffffffffffffffffffff).call.gas(!temp0 * 0x08fc).value(temp0)(memory[temp1:temp1 + memory[0x40:0x60] - temp1]);
var var0 = !temp2;
if (!var0) {
var0 = 0x7c2413bb49085e565f72ec50a1fb0460b69cf327e0b0d882980385b356239ea5;
var temp3 = arg0;
var var1 = temp3;
var temp4 = memory[0x40:0x60];
var var2 = temp4;
var var3 = var2;
var temp5 = var3 + 0x20;
memory[var3:var3 + 0x20] = temp5 - var3;
memory[temp5:temp5 + 0x20] = memory[var1:var1 + 0x20];
var var4 = temp5 + 0x20;
var var6 = memory[var1:var1 + 0x20];
var var5 = var1 + 0x20;
var var7 = var6;
var var8 = var4;
var var9 = var5;
var var10 = 0x00;
if (var10 >= var7) {
label_0823:
var temp6 = var6;
var4 = temp6 + var4;
var5 = temp6 & 0x1f;
if (!var5) {
var temp7 = memory[0x40:0x60];
log(memory[temp7:temp7 + var4 - temp7], [stack[-6]]);
return;
} else {
var temp8 = var5;
var temp9 = var4 - temp8;
memory[temp9:temp9 + 0x20] = ~(0x0100 ** (0x20 - temp8) - 0x01) & memory[temp9:temp9 + 0x20];
var temp10 = memory[0x40:0x60];
log(memory[temp10:temp10 + (temp9 + 0x20) - temp10], [stack[-6]]);
return;
}
} else {
label_0811:
var temp11 = var10;
memory[var8 + temp11:var8 + temp11 + 0x20] = memory[var9 + temp11:var9 + temp11 + 0x20];
var10 = temp11 + 0x20;
if (var10 >= var7) { goto label_0823; }
else { goto label_0811; }
}
} else {
var temp12 = returndata.length;
memory[0x00:0x00 + temp12] = returndata[0x00:0x00 + temp12];
revert(memory[0x00:0x00 + returndata.length]);
}
}
function func_0860(var arg0) {
if (msg.sender != storage[0x05] & 0xffffffffffffffffffffffffffffffffffffffff) { revert(memory[0x00:0x00]); }
storage[0x01] = arg0;
}
function func_08C6(var arg0) {
if (msg.sender != storage[0x00] & 0xffffffffffffffffffffffffffffffffffffffff) { revert(memory[0x00:0x00]); }
storage[0x02] = arg0;
}
function gift(var arg0) returns (var arg0) {
memory[0x20:0x40] = 0x04;
memory[0x00:0x20] = arg0;
return storage[keccak256(memory[0x00:0x40])];
}
function deposit() {
var var0 = 0x00;
var var1 = 0x0de0b6b3a7640000;
var var2 = msg.value;
if (!var1) { assert(); }
memory[0x00:0x20] = msg.sender;
memory[0x20:0x40] = 0x03;
var temp0 = keccak256(memory[0x00:0x40]);
storage[temp0] = storage[temp0] + var2 / var1;
}
function balance(var arg0) returns (var arg0) {
memory[0x20:0x40] = 0x03;
memory[0x00:0x20] = arg0;
return storage[keccak256(memory[0x00:0x40])];
}
上文為核心關(guān)鍵函數(shù)的具體代碼,我們?yōu)榱朔治鲱}目需要具體的看如何達(dá)到滿足flag調(diào)用函數(shù)的要求的。
function payforflag(var arg0) {
memory[0x00:0x20] = msg.sender;
memory[0x20:0x40] = 0x03;
if (storage[keccak256(memory[0x00:0x40])] <= 0x0f4240) { revert(memory[0x00:0x00]); }
memory[0x00:0x20] = msg.sender;
memory[0x20:0x40] = 0x03;
storage[keccak256(memory[0x00:0x40])] = 0x00;
storage[0x02] = 0x01;
var temp0 = address(address(this)).balance;
var temp1 = memory[0x40:0x60];
上述代碼作用為獲取flag。其中關(guān)鍵點(diǎn)為if (storage[keccak256(memory[0x00:0x40])] <= 0x0f4240) { revert(memory[0x00:0x00]); }。我們發(fā)現(xiàn)要想調(diào)用該函數(shù)的最關(guān)鍵部分為滿足memory[3]這個位置的書>0x0f4240,而0x0f4240為十進(jìn)制的1000000。
即我們獲得了我們的目標(biāo),即令我們的合約token>1000000即可。
三、解題步驟
我們對每個函數(shù)進(jìn)行詳細(xì)的分析。
首先我們來看:
function func_0293(var arg0) {
var var0 = 0x00;
memory[0x00:0x20] = msg.sender;
memory[0x20:0x40] = 0x03;
if (storage[keccak256(memory[0x00:0x40])] <= var0) { revert(memory[0x00:0x00]); }
var var1 = 0x0de0b6b3a7640000;
var var2 = msg.value;
if (!var1) { assert(); }
var0 = var2 / var1;
if (arg0 != storage[0x01]) {
memory[0x00:0x20] = msg.sender;
memory[0x20:0x40] = 0x03;
storage[keccak256(memory[0x00:0x40])] = 0x00;
storage[0x02] = 0x01;
return;
} else {
memory[0x00:0x20] = msg.sender;
memory[0x20:0x40] = 0x03;
var temp0 = keccak256(memory[0x00:0x40]);
storage[temp0] = storage[temp0] + var0 * storage[0x02];
storage[0x02] = 0x01;
return;
}
}
該函數(shù)需要滿足用戶的memory[3]的token>0,之后會對傳入的參數(shù)arg0進(jìn)行判定,如果該參數(shù)!=storage[1]的數(shù),則進(jìn)入,此時會賦予storage[3]為0,并將storage[2]為1 。
否則的話,storage[3]+=var0*storage[2](這里var0位傳入的以太幣數(shù)量)
我們下面看另一個函數(shù):
function func_03B2(var arg0) {
var var0 = 0x00;
memory[0x00:0x20] = msg.sender;
memory[0x20:0x40] = 0x03;
if (storage[keccak256(memory[0x00:0x40])] <= var0) { revert(memory[0x00:0x00]); }
if (arg0 & 0xffffffffffffffffffffffffffffffffffffffff == 0x00) {
var temp0 = var0;
var temp1 = temp0;
storage[temp1] = msg.sender | (storage[temp1] & ~0xffffffffffffffffffffffffffffffffffffffff);
memory[0x00:0x20] = msg.sender;
memory[0x20:0x40] = 0x03;
storage[temp0 + 0x01] = storage[keccak256(memory[0x00:0x40])];
memory[0x00:0x20] = msg.sender;
memory[0x20:0x40] = 0x03;
storage[keccak256(memory[0x00:0x40])] = 0x00;
return;
} else {
memory[0x00:0x20] = msg.sender;
memory[0x20:0x40] = 0x03;
var temp2 = storage[keccak256(memory[0x00:0x40])];
memory[0x00:0x20] = arg0 & 0xffffffffffffffffffffffffffffffffffffffff;
memory[0x20:0x40] = 0x03;
storage[keccak256(memory[0x00:0x40])] = temp2;
memory[0x00:0x20] = msg.sender;
memory[0x20:0x40] = 0x03;
storage[keccak256(memory[0x00:0x40])] = 0x00;
return;
}
}
該函數(shù)同樣需要滿足用戶余額有錢(storage[3]>0),之后如果傳入?yún)?shù)0,則storage[0]賦值為msg.sender()、storage[1]= storage[3](將用戶token賦值給storage[1]);
或者使得storage[arg0] = storage[3],并還原storage[3]=0。
下面我們來看空投函數(shù)。一般空投函數(shù)都是用來給用戶送錢的。
function airdrop() {
memory[0x00:0x20] = msg.sender;
memory[0x20:0x40] = 0x04;
if (storage[keccak256(memory[0x00:0x40])] != 0x00) { revert(memory[0x00:0x00]); }
memory[0x00:0x20] = msg.sender;
memory[0x20:0x40] = 0x04;
memory[0x00:0x20] = msg.sender;
memory[0x20:0x40] = 0x03;
var temp0 = keccak256(memory[0x00:0x40]);
storage[temp0] = storage[temp0] + 0x01;
}
該函數(shù)要求用戶的storage[4]不等于0,而這里的storage[4]應(yīng)該就是記錄該用戶是否已經(jīng)調(diào)用過空投函數(shù)(畢竟用戶不能一直調(diào)用,否則不是薅羊毛了嗎hhh)。
然而往下看我們會發(fā)現(xiàn),調(diào)用了該函數(shù)后系統(tǒng)似乎并沒有對storage[4]初始化,而是用storage[3]覆蓋了storage[4],并且將storage[3]++。
這里其實有一個點(diǎn)可以利用,如果它沒有對storage[4]進(jìn)行操作,那么storage[4]就永遠(yuǎn)為0,此時該函數(shù)可以一直被調(diào)用,從而調(diào)用100000次令storage[3]=100000,從而獲得flag。不過這個方法太笨重了,非常不切實際,所以我們還是正常去做。時間花費(fèi)過多,難度很大。
function func_0860(var arg0) {
if (msg.sender != storage[0x05] & 0xffffffffffffffffffffffffffffffffffffffff) { revert(memory[0x00:0x00]); }
storage[0x01] = arg0;
}
該函數(shù)判斷storage[5]是否為msg.sender,并將storage[1]任意賦值。
function func_08C6(var arg0) {
if (msg.sender != storage[0x00] & 0xffffffffffffffffffffffffffffffffffffffff) { revert(memory[0x00:0x00]); }
storage[0x02] = arg0;
}
同上函數(shù),storage[0]需要==msg.sender,之后storage[2]賦值為任意值。
function gift(var arg0) returns (var arg0) {
memory[0x20:0x40] = 0x04;
memory[0x00:0x20] = arg0;
return storage[keccak256(memory[0x00:0x40])];
}
gift函數(shù)傳入arg0,這里arg0應(yīng)該是一個地址,然后就可以返回該地址對應(yīng)的storage[4]的值。
function balance(var arg0) returns (var arg0) {
memory[0x20:0x40] = 0x03;
memory[0x00:0x20] = arg0;
return storage[keccak256(memory[0x00:0x40])];
}
而balance返回對應(yīng)地址的storage[3]的值 。
function deposit() {
var var0 = 0x00;
var var1 = 0x0de0b6b3a7640000;
var var2 = msg.value;
if (!var1) { assert(); }
memory[0x00:0x20] = msg.sender;
memory[0x20:0x40] = 0x03;
var temp0 = keccak256(memory[0x00:0x40]);
storage[temp0] = storage[temp0] + var2 / var1;
}
而deposit函數(shù)令storage[3]+value,即給合約的token充錢。
那么我們怎么利用上述的函數(shù)來使得我們的合約token>1000000呢?
我們注意到里面唯一能大量修改代幣的函數(shù)為func_0293中的else函數(shù)。如下圖所示。

我們在這里給一個解決方案供讀者參考。
- deposit() 傳入value=1 ether
- func_03B2(0)
- func_08C6(1000000)
- deposit() 傳入value=2 ether
- func_0293(1)
- payforflag(b64email)
下面我們來走一遍相關(guān)函數(shù),并查看相關(guān)storage的數(shù)據(jù)變化情況。
首先初始化堆棧情況,如下圖所示:

首先調(diào)用deposit(),傳入1 ether(1000000000000000000):

之后調(diào)用 func_03B2(0),傳入?yún)?shù)0:
函數(shù)要求token>0,我們滿足,于是進(jìn)入函數(shù)。
arg0=0所以進(jìn)入第一個條件,最終得到:

之后為func_08C6(1000000)。
滿足條件,進(jìn)入函數(shù),得到:

調(diào)用deposit() 傳入value=2 ether:

最后調(diào)用:func_0293(1)
storage[0x01]=1,arg0參數(shù)=1,傳入value=2,所以進(jìn)行下面的條件語句:
所以storage[temp0] = storage[temp0] + var0 * storage[0x02]=storage[3] = storage[3] + 2 * 1000000;
即我們得到storage[3]=2000002>1000000。滿足題目條件,此時可以調(diào)用flag函數(shù)獲取flag了。
為了驗證自己是否真正調(diào)用獲取flag函數(shù),我們可以到event事件中查看是否調(diào)用成功:
https://ropsten.etherscan.io/address/0xc9b91f149d3699474a0e680d55da62fbd3a51485#events

