Gatekeeper Two
1. 题目要求
1.1 题目:这个守门人带来了一些新的挑战, 同样的需要注册为参赛者来完成这一关
这可能有帮助:
- 想一想你从上一个守门人那学到了什么.
- 第二个门中的
assembly
关键词可以让一个合约访问非原生的 vanilla solidity 功能. 参见 here .extcodesize
函数可以用来得到给定地址合约的代码长度 - 你可以在这个页面学习到更多 yellow paper. ^
符号在第三个门里是位操作 (XOR), 在这里是代表另一个常见的位操作 (参见 here). Coin Flip 关卡也是一个很好的参考.
1.2 题目代码:
1 | // SPDX-License-Identifier: MIT |
2. 分析
- 2.1 与 Gatekeeper One 一样,我们必须成功通过函数的 3 次修饰符检查
enter()
才能创建entrant
我们的地址 - 2.2 交易必须从合约发送,以便合约地址 (
msg.sender
) 与合约调用者 (tx.origin
) 不同。 msg.sender
和tx.origin
- 在函数调用者上运行 solidity 汇编操作码的结果
extcodesize()
返回调用者合约代码的长度,但是,当我们从合约调用的构造函数执行外部调用时,extcodesize()
返回零,因为合约在构造期间没有可用的源代码. Consensys 智能合约最佳实践页面中的此页面详细介绍了它。因此,这extcodesize()
不是检查外部调用是由合约还是外部拥有的帐户执行的可靠方法。我们在这里要做的就是运行enter()
从调用合约的构造函数调用函数的代码。 - 按位
XOR
和通过它操作的每个元素都是它自己的逆,所以如果我们有如果:
1 | uint64(bytes8(keccak256(abi.encodePacked(msg.sender)))) ^ uint64(_gateKey) == uint64(0) - 1 |
为真,则:
1 | uint64(bytes8(keccak256(abi.encodePacked(msg.sender)))) ^ uint64(0) - 1 == uint64(_gateKey) |
也是如此。因此,*我们不需要_gateKey
*,我们可以简单地将结果uint64(bytes8(keccak256(abi.encodePacked(msg.sender)))) ^ uint64(0) - 1
作为参数传递,enter()
但强制转换为bytes8
as 这就是所enter()
需要的。
门 1:
msg.sender
和tx.origin
要打开这扇门,我们必须了解msg.sender
它们tx.origin
之间的区别。
msg.sender
(address
): 消息的发送者(当前通话)tx.origin
(address
): 交易的发送方(完整的调用链)
当交易由 EOA 进行并直接与智能合约交互时,这些变量将具有相同的值。但是,如果它与中间人合约交互A
,然后B
通过直接调用(而不是 a delegatecall
)与另一个合约交互,那么这些值将不同。
在这种情况下:
msg.sender
将有 EOA 地址tx.origin``A
将有合同的地址
因为为了gateOne
不恢复,我们需要让msg.sender != tx.origin
这意味着我们必须enter
从智能合约而不是直接从玩家的 EOA 调用。
这不是挑战的一部分,但我建议您阅读我在进一步阅读中列出的关于一些安全问题和最佳实践tx.orgin
以及何时不应使用它的内容。
关卡2:背后的玄机extcodesize
第二个门是了解更多关于合约如何部署以及合约在部署过程中的生命周期的绝好机会。
让我们看看函数的代码:
1 | 修饰符 gateTwo() { |
如果这是您第一次看到关键字assembly
,请不要害怕。这就是 Solidity 允许您使用称为Yul
. 这里不是讨论这个话题的地方,但如果您想了解更多, Solidity 文档网站上有大量关于 Yul 的内容。
让我们看看这两个操作码在执行时做了什么:
这个门要求的code
大小caller
必须是0
。
如果caller
是一个总是返回零的 EOA(外部拥有账户),但事实并非如此,因为正如我们所说,msg.sender
由于第一门要求,调用者 ( ) 必须是智能合约。
智能合约如何实现零代码?好吧,这是真的有一个特例。智能合约在编译时有两种不同的字节码。
- 创建字节码是以太坊创建合约和只执行一次构造函数所需的字节码
- 运行时字节码是合约的真实代码,存储在区块链中的代码将用于执行您的智能合约功能
当执行构造函数初始化合约存储时,它返回运行时字节码。直到构造函数的最后,合约本身没有任何运行时字节码,这意味着如果你调用address(contract).code.length
它会返回0!
如果您想在 EVM 级别阅读更多相关信息,可以深入阅读 OpenZeppelin 博客文章解构 Solidity 合约——第二部分:创建与运行时
因此,要通过第二道门,我们只需要从智能合约enter
中调用即可!Exploiter``constructor
Gate 3:铸造、向下铸造和位运算
最后一扇门是另一扇让你大吃一惊的门。你准备好了吗?
我们再次讨论类型和位运算之间的转换
来看看需求uint64(bytes8(keccak256(abi.encodePacked(msg.sender)))) ^ uint64(_gateKey) == uint64(0) - 1
合约是用0.8.x之前的Solidity版本编译的,所以在执行数学运算时不会回滚uint64(0) - 1
。此操作是表达“给我 auint64
可以容纳的最大数量”的“旧方法”。你可以通过做来表达同样的事情type(uint64).max
。
该部分从(在这种情况下是合同)中bytes8(keccak256(abi.encodePacked(msg.sender)))
获取不太重要的内容并将它们转换为8 bytes``msg.sender``Exploiter``uint64
该指令a ^ b
是按位XOR
操作。操作XOR
是这样的:如果 position 中的位相等,它将导致0
otherwise in a 1
。使a ^ b = type(uint64).max
(所以所有1
)b
必须是的倒数a
。
这意味着我们gateKey
必须是bytes8(keccak256(abi.encodePacked(msg.sender)))
在 solidity 中,没有“反向”操作,但我们可以通过XOR
在输入和值之间进行操作来重新创建它F
,其中只有 s。
这意味着我们可以gateKey
通过执行来计算正确的bytes8(keccak256(abi.encodePacked(address(this)))) ^ 0xFFFFFFFFFFFFFFFF
2.3 参考视频 写的攻击合约
contract Hack { constructor(GatekeeperTwo target) { uint64 s = uint64(bytes8(keccak256(abi.encodePacked(address(this))))); uint64 k = s ^ type(uint64).max; bytes8 key = bytes8(k); require(target.enter(key), "failed"); } }
3. 解题
3.1 获取关卡实例地址:0xf0f0bbA56D2035804D70D9Ed802e422195b62134
3.2 通过传入实例的地址部署攻击合约
3.3 提交案例
3.4 成功!!!!