Etherium智能合约中的漏洞。 代码示例

在这篇文章中,我将开始一系列有关以太坊智能合约安全性的文章。 我认为这个主题非常相关,因为开发人员的数量正像雪崩般增长,并且没有人可以从“耙子”中省钱。 再见-翻译...

1.扫描实时以太坊合约以查看未检查的发送错误


原始文件- 扫描实时以太坊合约中的“未经检查的发送...”


作者: Zikai Alex WenAndrew Miller

以太坊智能合约编程容易出错[1] 。 最近,我们看到
诸如以太坊之王DAO-1.0之类的高端智能合约包含由编程错误引起的漏洞。

自2015年3月以来,智能合约程序员已被警告当合约相互发送消息时可能会出现特定的编程危险[6]

一些编程指南推荐了如何避免常见错误(在以太坊官方文档[3]和UMD的独立指南[2]中 )。 尽管可以避免这些危险是可以理解的,但是这种错误的后果却是可怕的:金钱可能会被冻结,丢失或被盗。

这些危险导致的错误有多普遍? 除了活着的以太坊区块链合约之外,还有其他脆弱性吗? 在本文中,我们通过使用我们开发的新分析工具在以太坊实时区块链上分析合约来回答这个问题。


什么是未检查发送错误?


要将通话合同发送到另一个地址,最简单的方法是使用send关键字。 这充当为每个对象定义的方法。 例如,可以在实现棋盘游戏的智能合约中找到以下代码片段。


/*** Listing 1 ***/ if (gameHasEnded && !( prizePaidOut ) ) { winner.send(1000); //    prizePaidOut = True; } 

这里的问题是send方法可能会失败。 如果它不起作用,那么获胜者将不会收到钱,但是变量priestPaidOut将设置为True。

在两种情况下, winner.send()函数可能会失败。 我们稍后将分析它们之间的区别。 第一种情况是获胜者地址是合同(而不是用户帐户),并且该合同的代码引发异常(例如,如果使用过多的“气体”)。 如果是这样,那么在这种情况下也许是“赢家的错误”。 第二种情况不太明显。 以太坊虚拟机具有称为“ 调用堆栈 ”(调用堆栈深度)的有限资源,该资源可由先前在事务中执行的另一个合同代码使用。 如果在执行send命令时调用栈已用完,则该命令将失败,无论如何确定获胜者 。 获胜者的奖品将毫无瑕疵地销毁!



如何避免该错误?

以太坊文档包含有关此潜在危险的简短警告[3] :“使用send时存在一些危险-如果调用堆栈深度为1024(始终由调用者引起), 传输失败,如果接收方,则失败。 “ gas”结尾。因此,为确保广播安全,请始终检查send或更好的返回值:使用接收者取款的模板。

两个句子。 首先是检查send的返回值以查看其是否成功完成。 如果不是这种情况,则抛出异常以回滚状态。


  /*** Listing 2 ***/ if (gameHasEnded && !( prizePaidOut ) ) { if (winner.send(1000)) prizePaidOut = True; else throw; } 

对于当前示例,这是一个足够的解决方法,但并非总是正确的决定。 假设我们修改了示例,以便在游戏结束时,获胜者和失败者回滚他们的命运。 以下是“正式”解决方案的一个明显应用:


 /*** Listing 3 ***/ if (gameHasEnded && !( prizePaidOut ) ) { if (winner.send(1000) && loser.send(10)) prizePaidOut = True; else throw; } 

但是,这是一个错误,因为它引入了另一个漏洞。 虽然此代码可以保护获胜者免受呼叫堆栈攻击,但也使获胜者失败者容易受到攻击。 在这种情况下,我们希望防止调用堆栈攻击,但是如果send命令由于某种原因失败,则继续执行。

因此,即使是最佳的最佳实践(尽管它同样适用于Solidity,但在《以太坊和蛇编程指南》中也建议使用)也是检查呼叫堆栈资源。 我们可以定义一个宏callStackIsEmpty() ,当且仅当callstack为空时,它将返回错误。


 /*** Listing 4 ***/ if (gameHasEnded && !( prizePaidOut ) ) { if (callStackIsEmpty()) throw; winner.send(1000) loser.send(10) prizePaidOut = True; } 

更好的是,以太坊文档中的建议“使用收款人使用的模板”有点含糊,但有一个解释。 建议重新组织代码,以使发送失败的影响被隔离,并且一次仅影响一个接收者。 以下是此方法的示例。 但是,此技巧也是一种反模式。 他承担了检查接收者自己的呼叫栈的责任,这有可能陷入同一陷阱。


 /*** Listing 5 ***/ if (gameHasEnded && !( prizePaidOut ) ) { accounts[winner] += 1000 accounts[loser] += 10 prizePaidOut = True; } ... function withdraw(amount) { if (accounts[msg.sender] >= amount) { msg.sender.send(amount); accounts[msg.sender] -= amount; } } 

许多高度发达的智能合约易受攻击。 王位彩票之王是这一错误的最著名案例[4] 。 直到发现200个以太币(按今天的价格价值超过2000美元)才能使彩票的合法中奖者之前,这个错误才被发现。 以太之王中的相应代码与清单2中的代码相似。幸运的是,在这种情况下,合同开发者能够使用合同中不相关的功能作为“手动替代”来释放滞留的资金。 一个不太谨慎的管理员可以使用相同的功能来窃取广播!


继续扫描实时以太坊合约以检查未发送的错误。 第二部分

Source: https://habr.com/ru/post/zh-CN430602/


All Articles