一、前言
前几天全国大学生信息安全竞赛初赛如期进行,在这次比赛中也看到了区块链题目的身影。所以我将题目拿来进行分析,并为后续的比赛赛题提供一些分析思路。
由于本次比赛我并没有参加,所以我并没有Flag等相关信息,但是我拿到了比赛中的相关文件以及合约地址并在此基础上进行的详细分析,希望能帮助到进行研究的同学。
二、题目分析
拿到题目后,我们只得到了两个内容,一个是合约的地址,一个是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
,之后为构造函数,以及发送flag的事件。当我们调用payforflag
函数并传入使用base64加密的邮件地址之后,需要满足当前账户的余额比10000多。
由这第一手信息我们可以进行一些简单的猜想。这道题目需要领自己的余额大于10000,只有这样才能购买flag。这也是很常见的题目类型。而这个题目十分设计的还是十分巧妙的,我们接着向下看。
根据上面的合约代码,我们并不能得到更多的有用信息。然而此时我们就需要利用合约地址来进一步分析。
此处合约地址为:0x455541c3e9179a6cd8C418142855d894e11A288c
。
我们访问公链信息看看是否能够访问到有价值的信息:
https://ropsten.etherscan.io/address/0x455541c3e9179a6cd8c418142855d894e11a288c#code
image.png发现出题人并没有公开源代码,只有ABI码,此时我们只能根据此来进行合约逆向来寻找更有用的解题思路。
https://ethervm.io/decompile#func_profit
在此网站中进行逆向分析后,我们得到如下代码:
image.pngcontract 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函数 空投函数??
function func_01DC() {
memory[0x00:0x20] = msg.sender;
memory[0x20:0x40] = 0x01;
// 如果gift已经存在,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;
}
// 利润函数:
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的个数
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])];
}
}
之后我们针对此逆向后的代码进行分析。
我们经过分析发现了如下的public函数:
image.png很明显这是代币合约,并且可以进行转账。而此代码中拥有两个转账函数。并且可以查看余额。
我们具体根据代码对函数详细分析:
image.png首先我们分析编号为0x652e9d91
的func_01DC()
函数。
首先合约将内存切换到0x01位置,此处为:mapping(address => uint) public gift;
memory[0x00:0x20] = msg.sender;
memory[0x20:0x40] = 0x01;
即合约首先要判断该用户的gift
是否为0,若不为0则revert(也就是说这个函数要保证只能领取一次)。
之后内存切换到mapping(address => uint) public balanceOf;
。
对此变量进行操作,也就是将用户的余额值+1。并将gift值加一。
profit()函数的分析如下:
image.png根据函数的名称我们也知道,此函数为利润函数,其目的也很明显,根据我们的代币背景知识,我们猜测这个函数是用来赠送代币的。
函数要求balanceOf与gift
必须==1,不然就会revert。当调用此函数时,当满足上述条件后就会给用户的余额+1,令用户余额为2 。
balanceOf()函数
这个函数很简单,就是返回用户的余额情况。
下面我们来看两个关键的转账函数:
transfer()
函数同样比较简单。
首先需要判断用户的余额是否小于1 。之后判断转账的金额(arg1)是否大于余额,如果用户余额不足以进行转账,那么就会revert。
之后将当前用户的账面上减掉arg1代币数量,将收款方arg0的账户上增加arg1代币数量。
我们可以适当还原此函数:
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()函数。
在看到这个函数前我就疑问为何一个代币中有两个转账函数?后来在分析了源码后我了解到第二个转账函数中就存在漏洞。具体如下:
开始时函数判断arg1
需要>2,即转账数量要大于2. 。
之后判断用户余额需要大于等于2.
满足条件后需要令(余额 - arg1)大于零。即其本意是要用户余额大于转账金额。
之后进行转账后的余额更新。
我们分析该代码后将合约具体代码进行还原:
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;
}
不知用户是否发现,我们就看到了漏洞点了,这是一个典型的溢出漏洞。
image.png根据作者给出的代码,我们发现其具体余额是使用uint
定义的,由于uint的位数是有限的,并且其不支持负数。所以当其负数溢出时就会变成一个很大的正数。
而根据我们的transfer2函数内容,我们知道:require(balance(msg.sender) - arg1 >= 0);
。此句进行判断的时候是将用户余额减去一个arg1来判断是否大于0的。而如果arg1设置一个比较大的数,那么balance(msg.sender) - arg1
就会溢出为一个非常大的数,此时就成功绕过了检测并且转账大量的代币。
所以我们可以利用此处的整数溢出来进行题目求解,然而在分析的过程中我又发现了另一个解法。
如果做题人没有发现此处的漏洞点,我们可以利用常规做法来进行求解。
image.png根据给出的flag函数我们知道,我们只需要余额>10000即可,那么我们可以发现,我们的profit
函数可以给我们不断的新增钱。
根据我们的分析,我们需要令合约余额==1并且gitf==1,此时即可调用profit()
来将余额++,调用后余额为2,gift为1 。这时候将余额转给第二个账户,余额就又变成1了,就又可以调用profit()
函数。这样不断给第二个用户转账,转账10000次即可。(这里肯定是要用脚本去写,手动转账比较傻emmmm)
三、漏洞利用技巧
此处我们介绍漏洞利用的技巧。
首先我们需要拥有两个钱包地址(Addr1 Addr2)
。
-
此时我们令
Addr1
调用func_01DC()函数
领取1个代币以及1个gift。 -
之后我们调用
profit
领取一个代币。此时余额为2,gift为1 。
由于transfer2
需要余额大于2才能调用,所以我们首先令Addr2同样执行上面的两步。此时两个钱包均有余额为2 。
- 这时候Adde1调用
transfer
给Addr2转账两个代币,此时Addr余额为0,Addr2为4 。
之后Addr2就可以调用transfer2
给Adde1转账一个非常大的金额。达到溢出效果。此时Addr1与Addr2均拥有了大量的代币(Addr2为溢出得到,Addr1为转账得到)。任意地址均可以调用flag函数。
具体的交易日志如下:
image.png image.png image.png image.png此时flag就被调用发送到用户账户上了。
四、总结
本次题目非常巧妙,如果后面的同学想直接查看交易日志是非常难通过一个账户来进行跟踪的。并且本题目没有公布合约,所以考验逆向能力。但是只要逆出来后就是一道比较简单的题目,没有完全逆出来的同学也可以使用常规做法进行不断转账来使余额满足要求。希望本文对大家之后的研究有所帮助。欢迎讨论。
本稿为原创稿件,转载请标明出处。谢谢。
首发:[https://xz.aliyun.com/t/4982](https://xz.aliyun.com/t/4982)
网友评论