CTI:以太坊蜜罐智能合约分析

链闻ChainNews:

本文将对文章《Thephenomenonofsmartcontracthoneypots》和其评论中提到的smart-contract-honeypots和Solidlity-Vulnerable项目中的各蜜罐智能合约进行分析,根据分析结果将蜜罐智能合约的手段分为四个方面。

作者|DrAdrianManning

0x00前言

在学习区块链相关知识的过程中,拜读过一篇很好的文章《Thephenomenonofsmartcontracthoneypots》,作者详细分析了他遇到的三种蜜罐智能合约,并将相关智能合约整理收集到Github项目smart-contract-honeypots。

本文将对文中和评论中提到的smart-contract-honeypots和Solidlity-Vulnerable项目中的各蜜罐智能合约进行分析,根据分析结果将蜜罐智能合约的手段分为以下四个方面:

古老的手段神奇的逻辑漏洞新颖的游戏黑客的漏洞利用基于已知的手段,我们通过内部的以太坊智能合约审计系统一共寻找到118个蜜罐智能合约地址,一共取了34

只要转账金额大于1ether,就可以取走该智能合约里所有的以太币。

但事实绝非如此,让我们做出错误判断的原因在于github在显示超长行时不会自动换行。下图是设置了自动换行的本地编辑器截图:

图中第21行和第29行就是蜜罐作者通过超长空格隐藏起来的代码。所以实际的脆弱点是这样的:

if(msg

先将账户余额转给合约的创立者,然后再将剩余的账户余额转给转账的用户

与之类似的智能合约还有TestToken,留待有兴趣的读者继续分析:

Github地址:smart-contract-honeypots/TestToken

}functionGetGift(bytespass)returns(bytes32){if(hashPass==sha3(pass)){msg

returnsha3(pass);}functionPassHasBeenSet(bytes32hash){if(hash==hashPass){passHasBeenSet=true;}}

}

整个智能合约的逻辑很简单,三个关键函数功能如下:

SetPass():在转账大于1ether并且passHasBeenSet为false(默认值就是false),就可以设置密码hashPass。

GetGift():在输入的密码加密后与hashPass相等的情况下,就可以取走合约里所有的以太币。

PassHasBeenSet():如果输入的hash与hashPass相等,则passHasBeenSet将会被设置成true。

如果我们想取走合约里所有的以太币,只需要按照如下流程进行操作:

推特用户AlexeyPertsev还为此写了一个获取礼物的EXP。

但实际场景中,受害者转入一个以太币后并没有获取到整个智能合约的余额,这是为什么呢?

这是因为在合约创立之后,任何人都可以对合约进行操作,包括合约的创建者:

合约创建者在合约被攻击前,设置一个只有创建者知道的密码并将passHasBeenSet置为True,将只有合约创建者可以取出智能合约中的以太币。

与之类似的智能合约还有NEW_YEARS_GIFT:

Github地址:Solidlity-Vulnerable/honeypots/NEW_YEARS_GIFT

}

对于multiplicate()而言,只要你转账的金额大于账户余额,就可以把账户余额和你本次转账的金额都转给一个可控的地址。

在这里我们需要知道:在调用multiplicate()时,账户余额=之前的账户余额本次转账的金额。所以msg

modifieronlyOwner{if(msg

}

contractTestBankisOwned{addresspublicowner=msg

functionwithdraw(uintamount)publiconlyOwner{require(amount<=this

根据关键代码的内容,如果我们可以通过useEmergencyCode()中的判断,那就可以将owner设置为我们的地址,然后通过withdraw()函数就可以取出合约中的以太币。

如果你也有了上述的分析,那么就需要学习一下Solidity中继承的相关知识参考链接5:

该部分引用自参考链接5重点:Solidity的继承原理是代码拷贝,因此换句话说,继承的写法总是能够写成一个单独的合约。情况五:子类父类有相同名字的变量。父类A的test1操纵父类中的variable,子类B中的test2操纵子类中的variable,父类中的test2因为没被调用所以不存在。解释:对EVM来说,每个storagevariable都会有一个唯一标识的slotid。在下面的例子说,虽然都叫做variable,但是从bytecode角度来看,他们是由不同的slotid来确定的,因此也和变量叫什么没有关系。*contractA{uintvariable=0;functiontest1(uinta)returns(uint){variable;returnvariable;}functiontest2(uinta)returns(uint){variable=a;returnvariable;}}contractBisA{uintvariable=0;functiontest2(uinta)returns(uint){variable;returnvariable;}}====================contractB{uintvariable1=0;uintvariable2=0;functiontest1(uinta)returns(uintv){variable1;returnvariable1;}functiontest2(uinta)returns(uintv){variable2;returnvariable2;}}

根据样例中的代码,我们将该合约的核心代码修改如下:

contractTestBankisOwned{addresspublicowner1=msg

addresspublicowner2=msg

functionwithdraw(uintamount)publiconlyOwner{require(amount<=this

变量owner1是父类Owner中的owner变量,而owner2是子类TestBank中的变量。useEmergencyCode()函数只会修改owner2,而非owner1,自然无法调用withdraw()。由于调用useEmergencyCode()时需要转作者设置的evaluewei的以太币,所以只会造成以太币白白丢失。

0x03新颖的游戏

区块链的去中心化给行业带来了新的机遇,然而久必输这句话也不无道理。本章将会给介绍四个基于区块链的游戏并分析庄家如何赢钱的。*

3

functionshuffle()internal{//randomlysetsecretNumberwithavaluebetween1and20secretNumber=uint8(sha3(now,block

functionplay(uint256number)payablepublic{require(msg

shuffle();lastPlayed=now;}functionkill()public{if(msg

}

}

该合约设置了一个1-20的随机数:secretNumber,玩家通过调用play()去尝试竞猜这个数字,如果猜对,就可以取走合约中所有的钱并重新设置随机数secretNumber。

这里存在两层猫腻。第一层猫腻就出在这个play()。play()需要满足两个条件才会运行:

msg

addressowner;//addressoftheowneruintprivatesecretSeed;//seedusedtocalculatenumberofanaddressuintprivatelastReseed;//lastreseed-usedtoautomaticallyreseedthecontractevery1000blocksuintLuckyNumber=1;//ifthenumberofanaddressequals1,itwinsfunctionforceReseed(){//reseedinitiatedbytheowner-fortestingpurposesrequire(msg

functiontest()public{SeedComponentss;s

}

在运行test()之前,addr、b、c、d的值如下图所示:

在运行了test()之后,各值均被覆盖。

这个bug已经被提交给官方,并将在Solidity0

functionTest(){owner=msg

functionfake_foo(uint256n)public{Seeds;s

}

如图所示,攻击者0x583031d1113ad414f02576bd6afabfb302140225在调用fake_foo()之后,成功将owner修改成自己。

在2

}

contractTestisOwner{structSeed{addressx;}

functionTest(){owner=msg

functionfake_foo()public{Seeds;s

}

相比于示例代码1,示例代码2更容易出现在现实生活中。由于示例代码2配合复杂的逻辑隐蔽性较高,更容易被不良合约发布者利用。比如利用这种特性留后门。

在参考链接10中,开发者认为由于某些原因,让编译器通过警告的方式通知用户更合适。所以在目前0

modifieronlyOwner{if(msg

}

contractKingOfTheHillisOwned{addresspublicowner;

function()publicpayable{if(msg

jackpot=msg

functiontakeAll()publiconlyOwner{require(block

}

这个合约的逻辑是:每次请求fallback(),变量jackopt就是加上本次传入的金额。如果你传入的金额大于之前的jackopt,那么owner就会变成你的地址。

看到这个代码逻辑,你是否感觉和2

require(msg

}function()publicpayable{race();}

}

这个智能合约有趣的地方在于它设置了最大转账上限是50finney,最小转账下限是2wei(条件是大于1wei,也就是最小2wei)。每次转账之后,最大转账上限都会缩小成原来的一半,当总转账数量大于等于100finney,那就可以取出庄家在初始化智能合约时放进的钱。

假设我们转账了x次,那我们最多可以转的金额如下:

5050(1/2)^150(1/2)^250(1/2)^3.....

}

}

了解过DAO事件以及重入漏洞可以很明显地看出,CashOut()存在重入漏洞。

在了解重入漏洞之前,让我们先了解三个知识点:

Solidity的代码执行限制。为了防止以太坊网络被攻击或滥用,智能合约执行的每一步都需要消耗gas,俗称燃料。如果燃料消耗完了但合约没有执行完成,合约状态会回滚。addr

functionsetVictim(addresstarget){victim=target;}functionstep1(uint256amount)payable{if(this

}functionstep2(uint256amount){victim

//selfdestruct,sendallbalancetoownerfunctionstopAttack(){selfdestruct(owner);}functionstartAttack(uint256amount){step1(amount);step2(amount/2);}function()payable{victim

}

模拟的攻击步骤如下:

正常用户A向该合约存入50ether。

恶意攻击者B(地址:0x583031d1113ad414f02576bd6afabfb302140225)新建恶意智能合约Attack,实施攻击。不仅取出了自己存入的10ether,还取出了A存入的50ether。用户A的余额还是50ether,而恶意攻击者B的余额也因为发生溢出变成115792089237316195423570985008687907853269984665640564039407584007913129639936。

虽然此时用户A的余额仍然存在,但由于合约中已经没有以太币了,所以A将无法取出其存入的50个以太币

根据以上的案例可以得出如下结论:当普通用户将以太币存取该蜜罐智能合约地址,他的代币将会被恶意攻击者通过重入攻击取出,虽然他依旧能查到在该智能合约中存入的代币数量,但将无法取出相应的代币。

4

}

}

逻辑看起去很简单,只要在调用withdrawal()时发送超过1ether,该合约就会把余额全部转给发送者。至于通过delegatecall()调用的logEvent(),谁在意呢?

在DASPTOP10的漏洞中,排名第二的就是访问控制漏洞,其中就说到delegatecall()。

delegatecall()和call()功能类似,区别仅在于delegatecall()仅使用给定地址的代码,其它信息则使用当前合约(如存储,余额等等)。这也就意味着调用的logEvent()也可以修改该合约中的参数,包括adr。

举个例子,在第一个合约中,我们定义了一个变量adr,在第二个合约中通过delegatecall()调用第一个合约中的logEvent()。第二个合约中的第一个变量就变成了0x1111。这也就意味着攻击者完全有能力在logEvent()里面修改adr的值。

为了验证我们的猜测,使用evmdis逆向0x25df6e3da49f41ef5b99e139c87abc12c3583d13地址处的opcode。logEvent()处的关键逻辑如下:

翻译成Solidity的伪代码大致是:

functionlogEvent(){if(storage==0x46FEEB381E90F7E30635B4F33CE3F6FA8EA6ED9B){storage=addressofcurrentcontract;}}

这也就意味着,在调用蜜罐智能合约firstTest中的withdrawal()时,emails

functiondivest(uintamount)public{if(investors

该智能合约大致有存钱、计算利息、取钱等操作。在最开始的分析中,笔者并未在整个合约中找到任何存在漏洞、不正常的地方,使用Remix模拟也没有出现任何问题,一度怀疑该合约是否真的是蜜罐。直到打开了智能合约地址对应的页面:

在Solidity0

functiondivest()public{this

}

在Remix中将编译器版本修改为0.4.11commit.68ef5810.Emscripten.clang后,执行divest()函数结果如下:

在这个智能合约中也是如此。当我们需要调用divest()取出我们存进去的钱,最终将会调用this.loggedTransfer(amount,"",msg.sender,owner);。

因为编译器的bug,最终调用的是this.loggedTransfer(amount,msg.sender,owner);,具体的转账函数处就是owner.call.value(amount)。成功的将原本要转给msg.sender()的以太币转给合约的拥有者。合约拥有者成功盗币!

0x05后记

在分析过程中,我愈发认识到这些蜜罐智能合约与原始的蜜罐概念是有一定差别的。相较于蜜罐是诱导攻击者进行攻击,智能合约蜜罐的目的变成了诱导别人转账到合约地址。在手法上,也有了更多的方式,部分方式具有强烈的参考价值,值得学习。

这些蜜罐智能合约的目的性更强,显著区别与普通的钓鱼行为。相较于钓鱼行为面向大众,蜜罐智能合约主要面向的是智能合约开发者、智能合约代码审计人员或拥有一定技术背景的黑客。因为蜜罐智能合约门槛更高,需要能够看懂智能合约才可能会上当,非常有针对性,所以使用「蜜罐」这个词,我认为是非常贴切的。

这也对智能合约代码审计人员提出了更高的要求,不能只看懂代码,要了解代码潜在的逻辑和威胁、了解外部可能的影响面,才能知其然也知其所以然。

对于智能合约代码开发者来说,先知攻才能在代码写出前就拥有一定的警惕心理,从源头上减少存在漏洞的代码。

目前智能合约正处于新生阶段,流行的solidity语言也还没有发布正式1.0版本,很多语?的特性还需要发掘和完善;同时,区块链的相关业务也暂时没有出现完善的流水线操作。正因如此,在当前这个阶段智能合约代码审计更是相当的重要,合约的部署一定要经过严格的代码审计。

最后感谢404实验室的每一位小伙伴,分析过程中的无数次沟通交流,让这篇文章羽翼渐丰。

更多精彩内容,关注链闻ChainNews公众号,或者来微博@链闻ChainNews与我们互动!转载请注明版权和原文链接!

来源链接:blog.sigmaprime.io

本文来源于非小号媒体平台:

链闻研究院

现已在非小号资讯平台发布1篇作品,

非小号开放平台欢迎币圈作者入驻

入驻指南:

/apply_guide/

本文网址:

/news/3630315.html

以太坊ETH

免责声明:

1.资讯内容不构成投资建议,投资者应独立决策并自行承担风险

2.本文版权归属原作所有,仅代表作者本人观点,不代表非小号的观点或立场

上一篇:

细节!EOS抵押漏洞分析

郑重声明: 本文版权归原作者所有, 转载文章仅为传播更多信息之目的, 如作者信息标记有误, 请第一时间联系我们修改或删除, 多谢。

链链资讯

[0:15ms0-5:661ms