1.概述
在有趣的智能合约蜜罐中我们对古老的手段和神奇的逻辑漏洞进行了讲解和复现,在下部分中我们将会对新颖的游戏和黑客的漏洞利用进行讲解以及复现,从而进一步增加对智能合约蜜罐的了解。同样的,所有的智能合约蜜罐代码都可以GitHub上找到,这里再次给出他们的网址:smart-contract-honeySolidlity-Vulnerable2.新颖的游戏
行业从古至今一直存在,而区块链的去中心化似乎给行业带了新的机会,它的进入会让人们觉得变得公平,然而我们都知道结果往往都是必输,那么接下来就通过分析四个基于区块链的游戏合约来介绍庄家是如何最后稳赢的。2.1加密轮盘轮:CryptoRoulette
2.1.1蜜罐分析第一个要介绍的是CryptoRoulette,它译为「加密轮盘轮」。GutHub地址:smart-contract-honeypots/CryptoRoulette.solEtherscan地址:CryptoRoulette|0x94602b0E2512DdAd62a935763BF1277c973B2758蜜罐的完整代码如下:
该合约设置了一个私有属性的随机数secretNumber,在shuffle()函数中被指定范围在1-20,玩家可以通过play()函数去盲猜这个随机数,如果猜对了就可以将合约中的所有钱取走,每次调用play()函数后都会重置随机数。这么看来这个合约好像没有什么问题,随着猜错的玩家越来越多,合约中的代币余额也会积累的越多,如果碰巧猜对了就可以获取所有的奖金,然而事实是这样的嘛?我们可以看到在这个蜜罐合约中,最重要的就是shuffle()和play()这两个函数,下面就来分析下这两个函数。初始的secretNumber是在构造函数CryptoRoulette中调用shuffle()函数,而shuffle()函数中只有一行代码,就是设置secretNumber的值,从代码中也可以看出secretNumber的值既和区块的数目有关,也和时间有关。函数代码如下:
而play()函数就是提供给用户进行来猜这个随机数的,玩家携带不小于0.1eth并传入自己猜的数字number,玩家猜的这个数字number去和secretNumber进行比较,如果相等就可以获胜,转走合约中的所有以太币,但是在函数的开头中有一个检查require,其中后面要求玩家猜的数字不能大于10,而secretNumber我们在上面的函数中讲到范围是1-20,这样看来虽然加大了难度,但是也存在猜对可能性,然而事实是secretNumber一定会大于10,玩家永远都不可能猜对数字,合约所有者却可以通过调用kill()函数转走合约中的所有以太币。
这里会有人问了,secretNumber为啥一定会大于10呢?原因就是结构体game的初始化对存储数据secretNumber的覆盖,我们在函数里直接初始化结构体必须加memory关键字,因为memory是使用内存来进行存储,这样一来就可以避免占用storage的存储位,而蜜罐合约中并未使用memory关键字,从而导致了变量覆盖。该问题在Solidity0.5.0版本以前只是进行了提示,并没有做出错误警告,所以在老版本编译器中要注意该问题。在下面的代码复现中可以看到问题所在。2.1.2代码复现
将蜜罐合约的代码复制到RemixIDE中,为了方便我们查看secretNumber的值,我们将secretNumber的类型设置为public,这样就可以在RemixIDE中直接看到它的值了。甚至有些蜜罐部署者为了诱惑攻击者来攻击合约,也可以设置为public属性,因为就算告诉攻击者secretNumber的值他也不能猜对这个数字。使用地址0x5B3点击「Deploy」部署合约,调用secretNumber查看初始随机数为1,由于这里还没有初始化结构体也就不会覆盖随机数所以是正确的。
之后攻击者发现了该蜜罐合约,查看secretNumber为1并认为该合约可以进行攻击获利,所以在符合play()函数中的第一个判断条件情况下传入数字1和携带1个以太币进行函数调用,函数调用成功后查看账户余额发现账户余额不仅没有得到合约中的所有代币反而将刚才函数调用时携带的1个以太币也损失掉了。
为了探究具体原因我们对刚才的函数调用进行Debug。
调试点击下一步直到第一个条件判断,此时secretNumber仍然为1。
继续点击按钮进行下一步的调试,当进行到game.player=msg.sender时由于结构体game的初始化对存储数据secretNumber进行了覆盖,导致secretNumber变成了msg.sender的uint256内容,这样一来就使得后面的if判断条件不能成立,从而使得攻击者不能转走合约中的所有代币余额。
2.2开放地址彩票:OpenAddressLottery
2.2.1蜜罐分析第二个要介绍的是OpenAddressLottery,它译为「开发地址彩票」。GutHub地址:Solidlity-Vulnerable/OpenAddressLottery.solEtherscan地址:OpenAddressLottery|0xd1915A2bCC4B77794d64c4e483E43444193373Fa蜜罐的完整代码如下:
蜜罐合约OpenAddressLottery的游戏逻辑很简单,合约中有一个初始值为1的状态变量LuckyNumber,竞猜者每次竞猜时都会根据其地址随即生成0或者1,如果生成的值和LuckyNumber一样,那么竞猜者就可以获得1.9倍的奖金,且每个地址只能赢得一次游戏胜利,之后将无法继续参加竞猜。该蜜罐合约的重点就在于participate()、luckyNumberOfAddress()和forceReseed()函数,下面来对这3个函数进行依次讲解。首先是participate()函数,这是用户参与竞猜的函数:
接着是luckyNumberOfAddress()函数,将竞猜者的地址作为参数传入,通过n=uint(keccak256(uint(addr),secretSeed))%2;来计算竞彩时竞猜者对应的数字,由于是对2取余,所以得到的结果只能为0或者1。在计算这个数字时使用了变量secretSeed,而该变量总是通过reseed()函数得到的。
最后我们来讲下上面说到的reseed()函数,通过keccak256算法将传入的4个参数来生成secretSeed。
通过上面对合约的分析,看起来合约没有什么问题,中奖率也是50%,但其实是有陷阱的,这就要说到Solidity0.4.x结构体局部变量引起的变量覆盖漏洞,也就是给未初始化的结构体局部变量赋值时会直接覆盖掉智能合约中定义的前几个变量,这样就使得合约中forceReseed()函数被调用后,第四个定义的参数LuckyNumber会被s.component4=tx.gasprice*7给覆盖并将其设置为7,该蜜罐合约原理和上一个蜜罐合约类似。查看该合约的交易内容,可以发现OpenAddressLottery的交易数量很多,这也说明了蜜罐合约OpenAddressLottery的性。
2.2.2代码复现将蜜罐合约的代码复制到RemixIDE中,为了方便我们查看LuckyNumber的值,我们将LuckyNumber的类型设置为public,这样就可以在RemixIDE中就有获取其值的getter()函数了。同样的,蜜罐部署者也可以将该变量设置为public属性让攻击者误以为有利可图,因为LuckyNumber的值会被覆盖永远为7。使用地址0x5B3点击「Deploy」部署合约,调用LuckyNumber查看其值为1,由于这里还没有初始化SeedComponent结构体也就不会覆盖掉LuckyNumber的值,所以它还是1。
使用合约所有者0x5B3调用forceReseed()函数来初始化SeedComponent中的四个变量,可以看到LuckyNumber的值由于初始化已经变成了7。
攻击者0x4B2看到该合约后认为其存在漏洞,携带10eth调用participate()函数,调用后查看余额发现并没有增加。查看自己的地址对应的luckyNumberOfAddress的值为1,但是却没有得到奖励,再查看LuckyNumber的值发现一直为7。其原因就是在部署者调用forceReseed()函数初始化后LuckyNumber的值就被覆盖为了7,而攻击者地址生成的随机数只能是0或1,这就意味着永远不会有人获得胜利。这就是利用了编译器的漏洞,该问题已经在Solidity0.5.0中修复,所以这种蜜罐合约只有在Solidity0.4.x中才会生效。
2.3山丘之王:KingOfTheHill
2.3.1蜜罐分析第三个要介绍的是KingOfTheHill,它译为「山丘之王」。GutHub地址:Solidlity-Vulnerable/KingOfTheHill.solEtherscan地址:KingOfTheHill|0x4dc76cfc65b14b3fd83c8bc8b895482f3cbc150a蜜罐的完整代码如下:
蜜罐合约KingOfTheHill只有38行代码,逻辑很简单,有回退函数和takeAll()函数,其中jackpot变量是传入合约的所有代币之和,每次有用户调用回退函数后如果传入的mag.value比jackpot大,就将owner的值赋值为msg.sender。当用户获得了合约所有者权限后,就可以调用takeAll()函数在延期时间到后将合约中所有余额转走。接下来重点分析下这两个函数。首先是回退函数,这是用户参与合约「漏洞」的函数,其代码如下:
接着是takeAll()函数,这是能转走合约中所有余额的函数,其代码如下:
通过对上面两个函数的分析,感觉该合约并没有什么问题,但是我们说了这是个蜜罐,那么它的陷阱到底在哪儿呢?回看下「有趣的智能合约蜜罐」中的TestBank蜜罐合约就能知道原因了,它们的原理类似,都是「谁是合约主人」的问题。KingOfTheHill中存在着Owned和KingOfTheHill两个合约,KingOfTheHill继承了Owned,为了方便理解,我们将KingOfTheHill改写成一个单合约,代码如下:
在改写了合约代码后很容易就可以看出问题所在,用于权限判断的修饰器函数onlyOwner中判断的变量是owner1,而回退函数中修改的是原来子类新定义的owner,也就是owner2,这就说明了合约所有者是不会被更改的,调用takeAll()函数的人只能是合约创建者。接下来我们通过代码来复现一下。2.3.2代码复现将蜜罐合约的代码复制到RemixIDE中,为了方便我们复现,将回退函数中withdrawDelay=block.timestamp+5days;修改为withdrawDelay=block.timestamp+0days;,这样我们在测试的时候就不用等待5天后再去尝试取款操作了。使用地址0x5B3点击「Deploy」部署KingOfTheHill合约,点击owner查看当前值为0。
再使用0x5B3携带10eth调用回退函数,向合约中存入10个以太币,此时jackpot为10eth,查看owned为0x5B3。
攻击者0xAb8设置msg.value为20eth调用回退函数,查看owner为0xAb8。
攻击者发现此时owner为自己的地址,符合了takeAll()函数的要求,所以去调用takeAll()函数,结果发现交易失败,并且自己的余额仍然为80eth。
蜜罐部署者0x5B3发现有人上钩了,合约中已经有了30eth,此时虽然owner为攻击者地址0xAb8,但是0x5B3调用takeAll()函数仍然将合约中的所有余额全部转走,查看账户余额,的确增加了30eth。
与之类似的智能合约还有RichestTakeAll:GitHub地址:Solidlity-Vulnerable/RichestTakeAll.sol智能合约地址:RichestTakeAll|0xe65c53087e1a40b7c53b9a0ea3c2562ae2dfeb242.4以太币竞争游戏:RACEFORETH
2.4.1蜜罐分析第四个要介绍的是RACEFORETH,它译为「以太坊竞争游戏」。GutHub地址:Solidlity-Vulnerable/RACEFORETH.sol蜜罐的完整代码如下:
蜜罐合约RACEFORETH中有一个SCORE_TO_WIN参数,其值为100finney,字面意思我们也可以知道该参数的作用是胜利的分数,然后合约还有两个映射,其中racerScore是竞争者当前得分数,racerSpeedLimit是每步的限制。竞争者通过每次的转账金额来积累自己的分数racerScore,当自己的得分racerScore大于等于SCORE_TO_WIN时就能获得胜利,取走合约创建者一开始存入的奖励PRIZE。蜜罐合约的核心内容就是race()函数和endRace()函数,接下来我们分析下这两个函数。首先是race()函数,其代码如下:
用户每次调用race()函数都会带入msg.value,且msg.value需要大于1wei和小于步长限制,通过判断后加到自己的总得分数racerScore上,接着将新的步长限制设置为当前步长限制的一半,只要总得分数大于等于了获胜目标值就可以取走奖励,初看合约会觉得每次增加的步数在减少,但总有一天会追上,但事实是这样吗?接着是endRace()函数,其代码如下:
合约所有者在上一次竞赛的3天后就可以转走合约中所有的余额了。2.4.2代码复现将蜜罐合约的代码复制到RemixIDE中,为了方便我们复现,增加了一个publicnowScore,这样我们在测试的时候就可以看到每次竞赛后的分数了。使用地址0x5B3点击「Deploy」部署RACEFORETH合约。
使用0xAb8作为攻击者,根据代码的要求,第一次最大只能为50Finney,所以将msg.value也设置为50Finney,之后查看当前分数为50Finney。
攻击者0xAb8第二次尝试将msg.value设置为大于上一次竞赛的50Finney一半的26Finney,调用race()函数后发现调用失败,原因则是因为我们的26Finney不满足require中小于等于上一次竞赛一半的条件。
每次我们都传入上一次最大值的一半,执行多次后发现仍然未到100Finney。因为如下的公式只能无限趋于100却用于不能等于100。
其中:
永远是小于2的,那么50乘上这个式子就永远不可能等于100了,也就永远无法到达终点,所以对于该蜜罐合约,即使我们多次调用race()函数,每次都转入最大限制值,也不可能达到目标分数,那么我们就不能取出合约中的奖励了。
3.黑客的漏洞利用
3.1仅仅是测试?(整数溢出):For_Test
3.1.1蜜罐分析第五个要介绍的是For_Test,它译为「仅仅是测试?」。GutHub地址:Solidlity-Vulnerable/For_Test.solEtherscan地址:For_Test|0x2eCF8D1F46DD3C2098de9352683444A0B69Eb229蜜罐的完整代码如下:
蜜罐合约For_Test的逻辑很简单,核心函数只有Test()一个,在该函数中当传入的msg.value大于0.1eth时,根据for循环的内容,最终会得到amountToTransfer的值,也就是说函数调用者会获得4倍转入金额的奖励。接下来我们分析函数的主要内容。
仔细分析代码逻辑可以发现for循环中if判断中有个条件,当条件为真时会跳出循环,但是这个判断条件很诡异,因为amountToTransfer初始为0,在跳出之前amountToTransfer=multi,而在下一次循环时multi变为2倍的i,这就意味着multi是永远大于amountToTransfer的值,相应的这个判断条件不是会永远也不成立了吗?在最终揭秘这个蜜罐合约前我们还需要了解下几个知识。msg.value的单位是wei,而1eth=1018wei。当一个参数变量被定义为var时,其数据类型为uint8,其取值范围为。再次看到Test()函数中的循环,msg.value的最小值为0.1eth,而msg.value*2的值就会超过uint8的取值范围,也就是说此处会存在整形溢出,在i=255时再执行i++就会导致i上溢变为0,此时的multi为0从而小于amountToTransfer的值,这样就满足了if的判断条件,循环也会提前结束。根据代码内容,最终转给调用者的金额为amountToTransfer=255*2=510wei,无论调用者传入了大于0.1eth的任何金额,最后都只会得到510wei。3.1.2代码复现将蜜罐合约的代码复制到RemixIDE中,使用地址0x5B3点击「Deploy」部署For_Test合约,此时0x5B3的账户余额为100eth。
选择0xAb8作为攻击者,将msg.value设置为10eth,调用Test()函数,调用成功后发现账户余额不但没有增加反而减少了刚才传入的10eth。
当攻击者将代币转入合约后,合约所有者调用withdraw()函数进行取款,将刚才攻击者调用Test()函数传入的10eth转走,账户余额增加到110eth。
与之类似的智能合约还有Test1:Github地址:smart-contract-honeypots/Test1.sol3.2股息分配:DividendDistributor
3.2.1蜜罐分析最后一个要介绍的是DividendDistributor,它译为「股息分配」。GutHub地址:Solidlity-Vulnerable/DividendDistributor.solEtherscan地址:DividendDistributorv3|0x858c9eaf3ace37d2bedb4a1eb6b8805ffe801bba蜜罐的完整代码如下:
蜜罐合约DividendDistributor的逻辑不算太难,主要有投资、取钱、计算股息等功能,合约中有一个结构体类型的investor,其作用为存储投资人的投资信息包括投资额度和股息,并且该结构体通过mapping实现账户地址到investor的映射。通篇看来下合约并没有任何的问题,并且如果编译器版本设置正确的话合约也不会出现任何问题。看一下合约关键的函数,invest()、divest()、loggedTransfer()和payDividend(),接下来我们就对这4个函数进行详细分析。先是invest()函数,其函数功能为用户调用该函数进行投资,每次的投资数量不能小于要求的最低数量0.4eth,投资后更新相关的变量。完整代码如下:
divest()函数作为和上面的函数刚好相反,是取出自己投资的金额,函数中一开始就要检查调用者投资的数量或者调用函数传入的参数不为0,接着减去该次取钱操作的金额数量,最后从合约所有者账户中转走amount金额给调用者。完整代码如下:
loggedTransfer()函数的功能非常简单,就是转账和记录转账操作。完整代码如下:
payDividend()函数为获得由合约所有者设置的股息。完整代码如下:
通过分析上面的4个函数,我们发现该蜜罐合约的诱惑点在于投资者不仅能够随时存取投资,还可以通过payDividend()函数获取股息,这样的合约好像是有利可图的,然而事实是这是一个陷阱,它利用的就是旧版本编译器中的漏洞,在Solidity0.4.12之前存在一个漏洞,如果将空字符串作为函数调用时的参数那么编译器就会跳过该参数。
而在上面的几个核心函数中,divest()函数就是存在这样的问题,根据漏洞说明,调用this.loggedTransfer(amount,"",msg.sender,owner);后会变成loggedTransfer(uintamount,bytes32msg.sender,addressowner,address空)最终给owner用户转账owner.call.value(amount)()。下面我们就通过代码来复现这个蜜罐合约,揭开它的真面目。3.2.2代码复现
将蜜罐合约的代码复制到RemixIDE中,将编译器Solidity的版本设置为0.4.11。
选择0x5B3作为合约部署者和所有者,点击「Deploy」进行部署,随后将VALUE设置为10eth并调用distributeDividends函数设置股息。
将0xAb8作为攻击者,设置VALUE为10eth并调用invest()函数进行投资。
使用0xAb8调用下图中的函数获取该蜜罐合约的相关信息,包括计算股息,自己的投资数额,最小投资数额,合约所有者owner,总的股息和总的投资数额。
继续使用0xAb8调用divest()函数并设置其传入参数为5000000000000000000想要取出刚才投资的10eth的一半,发现该交易被确认,查看该交易的logs可以发现和上面我们分析的一样,target参数变成了owner的地址,第二个参数也被msg.sender所取代,返回查看账户当前余额,发现刚才调用divest()函数取出的5eth被转到了owner账户0x5B3中。
4.总结
通过对以太坊蜜罐智能合约的分析,我们可以发现在智能合约中这些有趣的蜜罐合约更像是钓鱼,通过各种手法诱使他人将代币转入合约中从而进一步获取这些代币。当然蜜罐合约也不是完全没有学习价值的,我们从蜜罐合约中可以看到合约的攻击思路以及Solidity的很多新旧特性。在平时的合约审计中也需要考虑这些问题,否则这些合约就可能被黑客攻击导致合约代币被盗取。即使是现在,同样有人编写蜜罐合约进行诱,只是他们的思路不再仅限于那些想要靠天上掉馅饼获取利益的人,各种机器人也成为了他们的诱目标。所以我们一定要重视合约的功能逻辑,防止合约因为功能逻辑被攻击的同时还要防止合约所有者跑路等各种因素。5.文献参考
蜜罐技术_百度百科(baidu.com)以太坊蜜罐智能合约分析(seebug.org)Solidity中文手册
郑重声明: 本文版权归原作者所有, 转载文章仅为传播更多信息之目的, 如作者信息标记有误, 请第一时间联系我们修改或删除, 多谢。