一、前言
时隔这么多天终于有时间把当时数字经济第二道区块链题目拿来复现。感觉第二题更偏向逻辑方面的漏洞,说白了就是来考察做题人对合约的逆向能力。总结来说,以太坊的漏洞相对于其他类型问题来说还算是非常少的,所以逆合约是一个非常重要的手段,只要能完美的逆出来合约,剩下的就相对容易许多。
比赛包括两道题目,这里我们分析一下第二道题目,第一题我们见https://xz.aliyun.com/t/6602。
二、题目描述
如上图所述,拿到题目我们只能看到常规操作,即给了合约地址与发送flag的函数。为了加大难度,作者并没有给合约的源码信息,所以我们只能用最笨但是最有效的方法去逆合约。
读者可以访问这个网站来查询逆向合约信息:https://ethervm.io/decompile/ropsten/0xc9B91F149d3699474a0E680D55da62FBD3a51485
我们这里拿到了合约的函数信息,现在我们查看具体的函数代码,并尝试逆出来合约函数的具体含义。
这里我们放出关键代码:
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])];
}
上文为核心关键函数的具体代码,我们为了分析题目需要具体的看如何达到满足flag调用函数的要求的。
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。其中关键点为if (storage[keccak256(memory[0x00:0x40])] <= 0x0f4240) { revert(memory[0x00:0x00]); }
。我们发现要想调用该函数的最关键部分为满足memory[3]这个位置的书>0x0f4240,而0x0f4240为十进制的1000000。
即我们获得了我们的目标,即令我们的合约token>1000000即可。
三、解题步骤
我们对每个函数进行详细的分析。
首先我们来看:
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;
}
}
该函数需要满足用户的memory[3]的token>0,之后会对传入的参数arg0进行判定,如果该参数!=storage[1]的数,则进入,此时会赋予storage[3]为0,并将storage[2]为1 。
否则的话,storage[3]+=var0*storage[2](这里var0位传入的以太币数量)
我们下面看另一个函数:
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;
}
}
该函数同样需要满足用户余额有钱(storage[3]>0),之后如果传入参数0,则storage[0]赋值为msg.sender()、storage[1]= storage[3](将用户token赋值给storage[1]);
或者使得storage[arg0] = storage[3],并还原storage[3]=0。
下面我们来看空投函数。一般空投函数都是用来给用户送钱的。
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;
}
该函数要求用户的storage[4]不等于0,而这里的storage[4]应该就是记录该用户是否已经调用过空投函数(毕竟用户不能一直调用,否则不是薅羊毛了吗hhh)。
然而往下看我们会发现,调用了该函数后系统似乎并没有对storage[4]初始化,而是用storage[3]覆盖了storage[4],并且将storage[3]++。
这里其实有一个点可以利用,如果它没有对storage[4]进行操作,那么storage[4]就永远为0,此时该函数可以一直被调用,从而调用100000次令storage[3]=100000,从而获得flag。不过这个方法太笨重了,非常不切实际,所以我们还是正常去做。时间花费过多,难度很大。
function func_0860(var arg0) {
if (msg.sender != storage[0x05] & 0xffffffffffffffffffffffffffffffffffffffff) { revert(memory[0x00:0x00]); }
storage[0x01] = arg0;
}
该函数判断storage[5]是否为msg.sender,并将storage[1]任意赋值。
function func_08C6(var arg0) {
if (msg.sender != storage[0x00] & 0xffffffffffffffffffffffffffffffffffffffff) { revert(memory[0x00:0x00]); }
storage[0x02] = arg0;
}
同上函数,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函数传入arg0,这里arg0应该是一个地址,然后就可以返回该地址对应的storage[4]的值。
function balance(var arg0) returns (var arg0) {
memory[0x20:0x40] = 0x03;
memory[0x00:0x20] = arg0;
return storage[keccak256(memory[0x00:0x40])];
}
而balance返回对应地址的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
函数令storage[3]+value,即给合约的token充钱。
那么我们怎么利用上述的函数来使得我们的合约token>1000000呢?
我们注意到里面唯一能大量修改代币的函数为func_0293
中的else函数。如下图所示。
我们在这里给一个解决方案供读者参考。
- deposit() 传入value=1 ether
- func_03B2(0)
- func_08C6(1000000)
- deposit() 传入value=2 ether
- func_0293(1)
- payforflag(b64email)
下面我们来走一遍相关函数,并查看相关storage的数据变化情况。
首先初始化堆栈情况,如下图所示:
首先调用deposit(),传入1 ether(1000000000000000000):
之后调用 func_03B2(0),传入参数0:
函数要求token>0,我们满足,于是进入函数。
arg0=0所以进入第一个条件,最终得到:
之后为func_08C6(1000000)。
满足条件,进入函数,得到:
调用deposit() 传入value=2 ether:
最后调用:func_0293(1)
storage[0x01]=1,arg0参数=1,传入value=2,所以进行下面的条件语句:
所以storage[temp0] = storage[temp0] + var0 * storage[0x02]
=storage[3] = storage[3] + 2 * 1000000;
即我们得到storage[3]=2000002>1000000。满足题目条件,此时可以调用flag函数获取flag了。
为了验证自己是否真正调用获取flag函数,我们可以到event事件中查看是否调用成功:
https://ropsten.etherscan.io/address/0xc9b91f149d3699474a0e680d55da62fbd3a51485#events