作者:Pinging(先知社区)
在前面的稿件中我们更多的会去选择分析如何在已知合约中寻找存在的漏洞,并利用漏洞以达到获取非法token的目的或者利用漏洞进行作恶。
研究安全的读者应该都清楚,在进行安全防御的时候,我们除了会对已经发生的安全事件进行跟踪之外,我们还会自行设置一个陷阱,让攻击者自己掉入我们布置好的陷阱中以便能让我们更好的分析作恶者的手法。而这个陷阱又被称为蜜罐。
在本文中,我们就针对智能合约的蜜罐进行分析。而这里的蜜罐不同于上面的传统的web中的蜜罐概念。在这里我们的蜜罐通常是攻击者编写的某种合约并部署在网络上,面向的对象是那些对Solidity语言不能够深入理解的一类投机用户。这些用户以为合约出现了严重漏洞就想通过合约去盗取token,然而不仅没有成功,反而把自己的本钱都赔进去了。
在讲述这个问题之前,我们首先看一个例子,代码如下:
pragma solidity ^0.4.18; contract Owned { address public owner; function Owned() { owner = msg.sender; } modifier onlyOwner{ if (msg.sender != owner) revert(); _; } } contract TestBank is Owned { event BankDeposit(address from, uint amount); event BankWithdrawal(address from, uint amount); address public owner = msg.sender; uint256 ecode; uint256 evalue; function() public payable { deposit(); } function deposit() public payable { require(msg.value > 0); BankDeposit(msg.sender, msg.value); } function setEmergencyCode(uint256 code, uint256 value) public onlyOwner { ecode = code; evalue = value; } function useEmergencyCode(uint256 code) public payable { if ((code == ecode) && (msg.value == evalue)) owner = msg.sender; } function withdraw(uint amount) public onlyOwner { require(amount <= this.balance); msg.sender.transfer(amount); } }
我们先简单的对合约进行分析。
首先这里定义了一个父类合约Owned()
,而在合约中我们只定义了一个构造函数与一个修饰器。
function Owned() { owner = msg.sender; }
而构造函数用于将owner
赋值为msg.sender
。而修饰器则是为了用于判断当前调用合约的用户是否为owner
。
之后我们分析子合约。
子合约继承于Owned
。关键函数如下:
function setEmergencyCode(uint256 code, uint256 value) public onlyOwner { ecode = code; evalue = value; }
核心内容首先为设置函数,此函数要求调用函数的人为owner
,并且可以设置ecode、evalue
的值。
当我们将这两个值设定完成后,普通用户就可以调用useEmergencyCode()
函数来完成更换owner
操作。
function useEmergencyCode(uint256 code) public payable { if ((code == ecode) && (msg.value == evalue)) owner = msg.sender; }
那用户更换完owner后会有什么好处吗?
这里我们就需要查看withdraw
函数了。
function withdraw(uint amount) public onlyOwner { require(amount <= this.balance); msg.sender.transfer(amount); }
这个函数为转账函数,调用此函数可以将合约中的token转账于msg.sender
(不过需要转账的金额不大于此合约的余额)。也就是说如果我们能够猜对了code
以及evalue
的值,我们就可以更换owner
了。
是不是听起来很美妙???我们似乎发现了合约中的bug。然而这就是蜜罐的核心代码。
我们分析起来是很简单的,然而我们下面进行一下测试,来看看是否跟我们分析的结果一样?
首先我们在测试账户下部署TestBank
合约。
然后设定这两个参数为100,100
。
然后我们在当前owner下向主合约存入10 eth
的钱。
然后我们模拟用户进行操作,我们更换账号:0x14723a09acff6d2a60dcdf7aa4aff308fddc160c
。
然后尝试直接调用取钱函数:
发现调用失败,失败的原因很明显,就是因为我们的owner不是0x14723a09acff6d2a60dcdf7aa4aff308fddc160c
,而是先前的0xCA35b7d915458EF540aDe6068dFe2F44E8fa733c
。
此时我们用户通过对合约的分析发现了可以通过useEmergencyCode()
来更改owner
,此时模拟用户调用。
传入我们设定的100,100参数。并且调用成功,此时我们继续查看owner
:
发现owner
已经被更改。用户心理窃喜,似乎我们发现了合约中存在的漏洞,并且可以调用withdraw
了!!!
然而事情没有想象中那么简单。
当我的用户开开心心的去调用withdraw
的时候,发现事情不妙。
用户仍然无法取出合约的钱?不仅如此,用户在调用useEmergencyCode()
时传入的value也无法取出。也就是说,合约把用户的钱全部吃掉了。
至此,蜜罐的钓鱼成功,不仅用户无法取出合约的钱,连自己的钱也赔进去了。
下面我们就来分析一下这种情况发生的原因。
此蜜罐能够成功的原理完全利用了用户对继承的理解不到位。关于继承,下面的参考链接中列举了详细的继承情况,并给出了分析结果。在这里我们针对本蜜罐进行分析。
在继承中无非设计两种,一是将父合约完整的继承下来,第二是父类合约的函数进行修改。针对这两个情况,在EVM中有不同的对待方法。
我们来看下面的例子:
contract A{ uint variable = 0; function test1(uint a) returns(uint){ variable++; return variable; } function test2(uint a) returns(uint){ variable += a; return variable; } } contract B is A{ uint variable = 0; function test2(uint a) returns(uint){ variable++; return variable; } }
A中拥有variable
变量,然而B中也拥有。然而对EVM来说,每个storage variable都会有一个唯一标识的slot id。虽然这两个AB中的变量名相同,但是他们有不同的slot id
,也就说明他们不是同一个变量,在底层是有所区别的。
对于函数test1 ()
来说,B可以完整的基础A中的这个函数,然而对于test12()
,由于B将此函数修改,所以可以算做多态的感觉。也就是说A中的test2
被B代替了。
于是B合约可以等价为:
contract B{ uint variable1 = 0; uint variable2 = 0; function test1(uint a) returns(uint v){ variable1++; return variable1; } function test2(uint a) returns(uint v){ variable2++; return variable2; } }
也就是说我的test1
与test2
修改的变量完全不是同一个。他们找寻的地址是不同的。
这对我们的蜜罐合约有没有启发呢?我们的蜜罐合约也为此。
由于owned
合约是父合约
所以onlyOwner
控制的owner为父合约的内存。也就是说此处的owner
是子合约中的,而onlyOwner
需要修改父类中的owner
。
然而我们并没有接口对其进行修改,也就意味着所有人都无法修改!
那么表面的都是虚假的,所以这个合约真正的目的就是为了骗取钱财。
下面我们再来看一个蜜罐合约。
pragma solidity ^0.4.18; contract MultiplicatorX3 { address public Owner = msg.sender; function() public payable{} function withdraw() payable public { require(msg.sender == Owner); Owner.transfer(this.balance); } function Command(address adr,bytes data) payable public { require(msg.sender == Owner); adr.call.value(msg.value)(data); } function multiplicate(address adr) public payable { if(msg.value>=this.balance) { adr.transfer(this.balance+msg.value); } } }
与往常一样,我们首先需要对这个合约进行一个简单的分析。
这个合约有个withdraw()
函数,这个函数用于使合约的拥有者将合约的所有余额进行提取。而这个函数目前对于我们来说无法用到,因为我们无法改变owner
的值,所以对于普通用户来说是无法使用这个函数的。
下面是Command
函数,这个函数同样是owner使用的。所以这里不再分析。
最重要的函数是multiplicate
函数,我们来看看:
function multiplicate(address adr) public payable { if(msg.value>=this.balance) { adr.transfer(this.balance+msg.value); } }
在这个函数中,我们受害者首先看到函数内容就会以为:用户可以调用此函数,并赋值value一个大于合约余额的数,然后就会满足if条件,之后合约就会向adr地址进行转账操作。
然而真正的情况会有用户所想的这么美好吗?我们来做个实验。
首先我们将合约部署:
为了方便我们查看合约余额,我们写入查看余额的函数。
此时余额为0,并可以查看到owner的地址。
之后我们更换账户信息为0xca35b7d915458ef540ade6068dfe2f44e8fa733c
。
然后我们将value的值设置为1 eth,然后调用multiplicate
。
此时我们注意账户的金额:
并且观察此时合约中账户的余额:
因为我们之前向合约转账了3 eth,所以此时里面有余额。
此时我们更换用户,模拟被害用户,此时用户为了投机向合约中转账4 eth。目的是收到合约转回来的7 eth。
然而调用函数后我们发现账户余额只减少却没有增加。
再次查看合约发现其内部金额只增加反而没有减少。用户的钱白白成为了“战利品”。
根据我们的实验,我们发现,代码中的this.balance
是原来的余额+value
,而此处的msg.value>=this.balance
可以等价为msg.value>=this.originBalance + msg.value
,所以是用户不可能满足的。
这也就是用户对solidity语法了解的不清楚导致的,应该引以为戒。