时隔这么多天终于有时间把当时数字经济第二道区块链题目拿来复现。感觉第二题更偏向逻辑方面的漏洞,说白了就是来考察做题人对合约的逆向能力。总结来说,以太坊的漏洞相对于其他类型问题来说还算是非常少的,所以逆合约是一个非常重要的手段,只要能完美的逆出来合约,剩下的就相对容易许多。
比赛包括两道题目,这里我们分析一下第二道题目,第一题我们见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]应该就是记录该用户是否已经调用过空投函数