关卡链接: Ethernaut Level 14 - Gatekeeper Two
攻击类型:extcodesize/constructor交互
难度: ⭐⭐⭐⭐☆
与上一关类似,我们需要再次通过三个 modifier 的检查,成为 entrant。

要通过此关卡,我们需要调用 enter(bytes8 _gateKey) 函数,并绕过它的三个 modifier。
gateOne1 | modifier gateOne() { |
与第13关完全相同。我们需要通过一个中间合约来调用 enter 函数,以确保 msg.sender 是合约地址,而 tx.origin 是我们的EOA地址。
gateTwo1 | modifier gateTwo() { |
这个 modifier 使用内联汇编检查 caller() 的 extcodesize 是否为0。caller() 返回的是直接调用者的地址(在我们的场景中,就是攻击合约的地址),而 extcodesize 返回该地址关联的代码大小。
extcodesize 会返回一个大于0的值。extcodesize 返回0。这与 gateOne 的要求(msg.sender 必须是合约)产生了矛盾。我们如何才能让一个合约地址的 extcodesize 为0呢?
答案在于合约的创建过程:当一个合约的 constructor 正在执行时,该合约的代码尚未完全部署到链上,因此此时对该合约地址调用 extcodesize 会返回0。
因此,我们必须在攻击合约的 constructor 内部调用目标合约的 enter 函数。
gateThree1 | modifier gateThree(bytes8 _gateKey) { |
这个 modifier 包含一个有趣的异或(XOR)逻辑。让我们简化一下:
A ^ B = C
其中:
A 是 uint64(bytes8(keccak256(abi.encodePacked(msg.sender))))B 是 uint64(_gateKey)C 是 type(uint64).max (即 0xFFFFFFFFFFFFFFFF)根据异或运算的性质,如果 A ^ B = C,那么 A ^ C = B。
因此,我们可以通过计算 A ^ C 来得到我们需要的 _gateKey。
_gateKey = uint64(bytes8(keccak256(abi.encodePacked(msg.sender)))) ^ type(uint64).max
由于 msg.sender 是我们的攻击合约地址,我们可以在攻击合约的 constructor 中计算出这个值。
我们的攻击合约非常简洁。它在 constructor 中完成所有的工作:计算 gateKey 并立即调用目标实例的 enter 函数。
1 | // SPDX-License-Identifier: Unlicense |
constructor 中。constructor 中调用: 这是关键。在 constructor 中调用 enter 函数,此时 extcodesize(address(this)) 为0,绕过了 gateTwo。_gateKey: 在 constructor 中,使用 address(this) 作为 msg.sender 来计算 keccak256 哈希,并通过XOR运算得到正确的 _gateKey,从而绕过 gateThree。entrant 将被设置为我们的EOA地址。extcodesize 进行合约检查: 正如本例所示,extcodesize 可以被 constructor 调用绕过。一个更可靠的检查方法是判断 address.balance > 0 或者 address.code.length > 0(在Solidity 0.8.10及更高版本中)。caller 的额外检查: 如果确实需要阻止合约调用,可以结合 tx.origin == msg.sender 的检查,但这会限制合约的可组合性。constructor: 合约的构造函数,仅在合约部署时执行一次。理解其在生命周期中的特殊性(如 extcodesize 为0)是解决此类挑战的关键。extcodesize: 一个EVM操作码,用于获取地址的代码大小。是区分EOA和合约的常用方法,但有其局限性。^): 一种位运算符,在密码学和哈希操作中很常见。理解其 A ^ B = C <=> A ^ C = B 的性质对于解决 gateThree 至关重要。核心概念:
constructor 执行期间的代码大小(extcodesize)为0。caller() 和 address(this) 在特定上下文中的区别和联系。攻击向量:
constructor 的特性绕过 extcodesize 检查。constructor 中完成所有攻击步骤,实现“部署即攻击”。防御策略:
extcodesize 来判断一个地址是否为合约。在智能合约的世界中,最简单的漏洞往往隐藏着最深刻的安全教训。 🎓