主页 > imtoken手机版下载 > 两次被黑客入侵,以太坊重入攻击漏洞如何解决?

两次被黑客入侵,以太坊重入攻击漏洞如何解决?

imtoken手机版下载 2023-05-28 06:38:03

北京时间4月19日上午8点45分,国内DeFi借贷协议Lendf.Me被黑客曝光。 这是继 Uniswap 于 4 月 18 日被黑客攻击并损失 1,278 ETH(价值约 22 万美元)后,DeFi 生态的又一起重大安全事件。 近期,各类DeFi项目屡遭黑客攻击,损失惨重。 有的黑客利用DeFi产品设计和代码实现的漏洞进行入侵,有的利用以太坊一直未彻底解决的重入攻击问题窃取DeFi平台资产。 早在去年9月,Qtum联合创始人乔丹·厄尔斯就解读了以太坊对重入攻击问题的解决方案,并提出了自己的建议。

幻数和 STATICALL

在每一个使用转移函数汇款的现代智能合约(如果我没记错的话,在 Solidity 3.0 之后)中,都有一个硬编码常量 - 2300。例如在这个简单的例子中:

合同测试员{

外部函数{

应付地址 paymentAddress =

0x5A0b54D5dc17e0AadC383d2db43B0a0D3E029c4c ; paymentAddress.transfer(5);

}

}

transfer被翻译成EVM字节码后定位如下:

呼叫 2300,地址,....

为什么这个数字如此重要? 这个决定是有原因的。 这曾经是防止一类被归类为“可重入”的智能合约漏洞的最有效方法。 重入的概念是一个智能合约调用另一个智能合约,它最终(在同一次执行期间)再次调用原始智能合约。 重入是臭名昭著的 DAO 黑客攻击中利用的主要漏洞。 当时提出的解决方案并不是通过改变以太坊协议让合约来阻止这种行为,而是最终通过改变 Solidity 使得发送 ETH 到智能合约的默认行为使用极少量的 gas,从而使重入问题不能再被利用。 当然,也有副作用。 这种变化使得收到钱的智能合约只在日志中记录一个事件,而不能改变状态或做任何其他事情。

但最近以太坊引入了 STATICCALL 作为防止重入问题的灵丹妙药。 真的是灵丹妙药吗? 答案是——不完全是。

首先,我们尝试强制 Solidity 在测试合约的回退函数中使用 STATICCALL:

以太坊 协议_以太坊和以太币有什么区别_以太坊协议

pragma solidity ^0.5.9;

合同测试员{

功能外部视图{

}

函数 foo 外部视图 {

}

}

然后编译器用以下错误奖励我们的冒险精神:

browser/test.sol:4:5: TypeError: Fallback 函数必须是付费或非付费的,但是是“view”。

功能外部视图{

^(相关源代码部分从这里开始并跨越多行)。

另外,有趣的是,没有明确的“non-payable”关键字来明确地将一个函数标识为non-payable。 让我们通过从回退函数中删除 view 关键字来检查此 ABI:

以太坊协议_以太坊 协议_以太坊和以太币有什么区别

[

{

“常数”:真实的,

“输入”:[],

“名称”:“富”,

“输出”:[],

“应付”:假的,

“stateMutability”:“视图”,

“类型”:“功能”

},

{

“应付”:假的,

以太坊和以太币有什么区别_以太坊 协议_以太坊协议

"stateMutability": "不可支付",

“类型”:“回退”

}

]

所以Solidity决定了一个fallback函数的stateMutability只能是non-payable或者payable,不允许“view”。

但是让我们假装 Solidity 并没有那么糟糕并允许它。 然后你可以让 Solidity 使用 STATICCALL 将资金转移到合约的回退。 一切正常吗? 或不。 STATICCALL 的设计比较特别。 当然,你可以在逻辑上使用fallback函数,但是STATICCALL的设计目的是让外部合约调用没有副作用,只返回计算结果的数据。 回退函数实际上没有返回数据的概念,尽管如果您下降到调用者和被调用者的汇编级别就可以实现这一点。 因此,STATICCALL 在这种情况下是无用的,除非您正在执行罕见的操作。 但是,如果您打算做一些不寻常的事情,为什么不直接编写一个常规函数调用并使用回退函数呢? 好吧,我离题了一点。

STATICCALL 强调没有副作用。 这意味着您不能执行以下操作:

· 改变状态

调用另一个改变状态的合约

· 创建合同

· 自毁合约

· 在日志中记录事件

以太坊协议_以太坊 协议_以太坊和以太币有什么区别

· 发送ETH到另一个合约,或者改变一个合约的余额

为被 STATICCALLed 的合约接收 ETH

你可以期望不能改变状态,因为正是从这个角度直接防止了重入错误的发生。 虽然,我没想到日志记录事件也会被禁用。 记录事件对智能合约没有可见的真正副作用。 一旦记录了事件,外部(或内部)智能合约就无法看到状态已记录。 这是完全空的输出。 您将数据发送到虚空,但再也不会收到该数据,甚至不会观察到该数据已发送。 这种副作用只有在区块链之外的世界才能看到。 事件通常用于通知外部接口进入区块链,比如说“这里发生了一些你可能感兴趣的事情”。

因此,假设以技术纯度的名义,STATICCALL 非常严格地执行“无副作用”规则。 无副作用中的副作用包括仅在外部可见的副作用。 另一个影响当然是不能在 STATICCALL 中转移 ETH。 这有效地打破了它作为解决重入问题的竞争者的地位。 一般来说,调用 fallback 函数时,要么是你出错了,要么是你想将 ETH 发送到一个合约。 当合约收到 ETH 时,它通常会记录一个事件来告诉外部程序“嘿,我收到了一笔钱,你可能想做点什么,给那个用户发个消息什么的”。 当有神奇的 2300 gas limit 时,不可能将部分 ETH 发送到另一个合约,也无法改变合约中的状态,比如更新“估计余额”变量等。 STATICCALL 的唯一用途是防止您不想发送 ETH 的合同的通用功能发生重入攻击。

这意味着,要想有效防止重入攻击、发送ETH、允许创建事件,唯一的方法还是之前的方法,使用神奇的gas limit常数——2300。为什么这是一个这么大的问题? 这并不是说硬编码数字在计算机科学中被认为是不好的(提示:它是),而是硬编码数字实际上让一些合约在需要以太坊周围的生态系统做某事时做一些事情。 更改后无法使用。

动态区块链中的常量

这个 reddit 线程 [1] 提供了一些有用的细节,指出以太坊之前在升级中增加了 CALL 指令和其他一些指令的最低成本(如果我没记错的话最低成本是 100,现在是 500)。 大多数这些担忧仍然适用于所有未来的天然气价格调整。 Nick Johnson 指出“带有明确 gas 限制的调用非常罕见”。 当这句话被提出时,Solidity在执行相当于转移的操作时会将所有可用的gas转移到一个合约中,从而留下了使用可重入操作进行攻击的可能性。 Solidity 在 DAO 攻击后引入了 2300 的气体限制,以防止类似事件的发生。 现在,公平地说,当默认调用外部合约函数(没有转移)时,Solidity 仍然会默认发送所有气体。 并且在文档中有无数关于这种操作隐患的警告。 神奇的 2300 gas limit 常量只会强化那个 reddit 帖子中指出的问题。

例如,想象一个带有支付回退函数的合约,该函数使用一些足够便宜的操作码,可以在部署时在 2300 gas 限制内执行。 但是,为了应对攻击或一些以前未发现的问题,这些操作码的价格已大大提高。 然后合约将变得不可用,无法从未明确将气体限制提高到 2300 以上的合约中接受 ETH(Solidity 警告的行为)。 更糟糕的是,明确提高气体限制会使调用合约暴露于重入攻击。 因此,这个合同可能不得不被弃用。 按照实际接收ETH的逻辑(比如依赖某个特定的合约向其发送ETH),很可能被堵在了墙里,里面的钱是提不出来的。

这个 2300 gas limit 假设不仅不利于向后兼容。 它还会损害以太坊协议中潜在的未来创新。 例如,SSTORE 的净燃气计量 EIP-1293 [2] 是一种创新的协议改进,它将降低许多智能合约操作的成本,包括存储。 它使存储 gas 成本能够反映区块链上的实际成本,这意味着在一次执行中第二次写入存储密钥时,将花费更少的 gas。 这是有道理的,因为从区块链的角度来看,第二次状态修改几乎没有成本,而第一次状态修改几乎已经足够支付给区块链了。 该提案曾被包含在君士坦丁堡分叉中,但在最后一刻被删除 [3],因为它被发现对大量现有的智能合约有危险 [4]。 这个判断是正确的,这个改进会降低状态存储的成本,从而带来重入攻击的隐患,即使是非常保守的gas limit 2300点。 这个事件的讽刺之处在于,提案的设计实际上会降低智能合约重入保护的gas成本,而这正是它的主要应用场景。

现在,针对 EIP-1283 的重入问题提出的解决方案是 EIP-1706 [5]。 此次提案的变化可以总结为:当当前执行gas limit低于2300时,gas净值计量带来的手续费减免将不会实施。所以,现在这个神奇的常数在以太坊共识中更加根深蒂固协议。 这将有效地强制任何未来的 EVM 语言也对合约调用使用硬编码的 2300 gas 限制,以防止重入攻击的危险。

这个神奇的假设基本上扼杀了存储变得更便宜的可能性,更不用说以太坊将解决可扩展性问题等等。 即使有一天发明了一种神奇的方法,可以把所有的存储都移到链下,让存储基本免费,存储的实际gas成本仍然不能低于2300,否则就会暴露在重入攻击的危险之中。

可能的解决方案

以太坊协议_以太坊和以太币有什么区别_以太坊 协议

我说了很多,但这真的是一个很难的问题,不是吗? 我们在谈论区块链,而关于区块链的一切都很困难。 这也是事实,但同时,我倾向于不同意以太坊团队在改进共识协议时刻意强调的技术纯度。 其实我觉得EIP-1706因为硬编码的问题不会被以太坊主网接受,不够纯粹。 我个人预测 EIP-1283 会无限期推迟,最终可能会加入到以太坊 2.0 中。

我将如何解决重入问题? 有两种可能的情况。

第一个更简单:添加另一个操作码以太坊协议,但添加一个有用的操作码。 我的建议是添加这个操作码:

没有重新进入权限的魔法呼叫

确实是一个简洁的名字,读起来需要很长时间。 但说真的,这个操作码的工作原理与 CALL 基本相同,除了以下变化:

· 允许 SSTORE(状态改变),如 STATICCALL

· 其他任何事情都是允许的

老实说,我不太喜欢这个解决方案。 我更喜欢解决本质问题,允许重新进入任何智能合约。 理想情况下,可能存在一个非常简单的操作码,如下所示:

杀了我

如果当前合约已经在调用堆栈中,这将停止执行。 这个功能只需要一个经验丰富的开发人员通宵工作,然后需要一天的时间进行安全测试。 这将允许不涉及存储来防止重入攻击,并且可以使用以下简单代码来实现 if callstack.exists(currentAddress) then throw 非常便宜。 但话又说回来,我认为这样的操作码不够“纯粹” 永远不会考虑采用到以太坊中。

还有更纯粹的替代方案,例如将整个调用堆栈暴露给智能合约。 如果你知道调用堆栈中有什么,你可以简单地编写一个遍历堆栈的 Solidity 函数,检查它自己的地址是否包含在其中,以证明现有执行是可重入的。 当然,如果不是预期的,合约将抛出异常以防止任何不需要或意外的行为。 这也将允许将其他功能添加到智能合约中。 例如,假设您运行智能合约可以参与的众筹。但是以太坊协议,您将一些与恐怖分子有关的智能合约列入黑名单。 恐怖分子可以简单地部署一个“通证”智能合约,然后让被黑名单屏蔽的智能合约调用“通证”合约,最后调用你的众筹合约。 如果有关于调用堆栈的信息,则可以发现此行为。 现在在以太坊中,绝对没有办法用链上的智能合约逻辑检测到这一点。

现在几乎所有防止以太坊重入攻击的设计本身都存在安全隐患。

通常在合约执行时,一个变量会被置1,表示正在执行。 当执行完成时,这个变量被重置为0。这样,如果你在中间执行一个合约调用,如果外部合约想重新进入现有合约,合约会看到这个变量被设置为1,并且然后中止执行。 但是,如果某些逻辑问题导致变量在执行结束时没有重置回 0 怎么办? 基本上,智能合约是孤立的,不能再被操纵,因为它一直认为自己受到了攻击。

区块链安全咨询公司Warp Future表示:可重入是以太坊生态中的头号问题,也是讨论最多的问题。 它也被一些网站列为创建智能合约时应注意的头号安全问题。 这就是导致 DAO 攻击和其他一些攻击和异常的原因。 这也是智能合约开发者最难搞定的问题之一,而大多数智能合约只是从根本上消除了这种可能性。 以太坊没有实施任何直接的方法来防止重入,这对我来说似乎令人难以置信。 相反,依赖严重受限的 STATICCALL 机制或神奇的 2300 gas 限制常数似乎具有更高的优先级。 在我看来,这值得一流的解决方案,而不是基于假设的丑陋修复。

本文内容由曲速未来安全咨询公司整理整理,转载请注明。 Warp Future提供相关的区块链安全咨询服务,包括主链安全、交易所安全、交易所钱包安全、DAPP开发安全、智能合约开发安全等。