关卡链接: Ethernaut Level 13 - Gatekeeper One
攻击类型: Gas计算 / 类型转换
难度: ⭐⭐⭐⭐☆
通过三个 modifier
的检测,成功调用 enter
函数,成为 entrant
。
要通过此关卡,我们需要调用 enter(bytes8 _gateKey)
函数,但必须绕过它的三个 modifier
。让我们逐一分析。
gateOne
1 | modifier gateOne() { |
这个 modifier
要求 msg.sender
不等于 tx.origin
。这是一种常见的检查,用于防止直接从外部账户(EOA)调用。为了绕过它,我们必须通过一个中间合约来调用 enter
函数。这样,tx.origin
将是我们的EOA地址,而 msg.sender
将是攻击合约的地址。
gateTwo
1 | modifier gateTwo() { |
这个 modifier
要求在执行到这里时,剩余的 gas
必须是 8191
的倍数。这是一个棘手的约束,因为 gas
的消耗会因操作码、Solidity版本和优化器设置而异。
最直接的方法是进行暴力破解:通过一个循环,在调用 enter
函数时尝试不同的 gas
值,直到找到一个满足 gasleft() % 8191 == 0
的值。
gateThree
1 | modifier gateThree(bytes8 _gateKey) { |
这个 modifier
对我们传入的 _gateKey
(一个 bytes8
类型的值) 进行了三项检查:
uint32(uint64(_gateKey)) == uint16(uint64(_gateKey))
uint64(_gateKey)
将 bytes8
转换为 uint64
。uint32(...)
会截断,只保留低32位。uint16(...)
会截断,只保留低16位。_gateKey
的第17位到第32位必须全为0。例如,0x????????0000????
。uint32(uint64(_gateKey)) != uint64(_gateKey)
_gateKey
的高32位不全为0。uint32(uint64(_gateKey)) == uint16(uint160(tx.origin))
uint16(uint160(tx.origin))
获取 tx.origin
地址的最低16位。_gateKey
的低32位(经过第一次检查后,其实就是低16位)必须等于 tx.origin
的低16位。综合这三个条件,我们可以构造出 _gateKey
:
tx.origin
(即我们的EOA地址) 的低16位作为 _gateKey
的低16位。_gateKey
的17-32位为0。_gateKey
的高32位中设置至少一个非零位。这是我们的Foundry测试合约,它将部署攻击合约并调用 enter
函数。
1 | // SPDX-License-Identifier: Unlicense |
gateOne
(msg.sender != tx.origin
)。_gateKey
:tx.origin
的低16位。bytes8
值,满足 gateThree
的所有 require
条件。gas
:gas
值来调用 enter
函数。Foundry
测试中,我们可以通过 try/catch
捕获失败的调用,直到找到一个成功的 gas
值(例如,gas
偏移量为 268
)。gas
值和构造的 _gateKey
从攻击合约中调用 enter
函数。gas
检查: gasleft()
的值是不可预测的,并且会随着EVM的更新而改变。不应将其用于关键的访问控制逻辑。tx.origin
或 gas
技巧。可以考虑使用数字签名、Merkle树或预言机等更强大的验证机制。try/catch
: 用于在测试中捕获和处理预期的 revert
,非常适合暴力破解 gas
等场景。|
, &
): 在构造 _gateKey
时用于精确控制字节内容。uint16
, uint32
, uint64
)和字节类型(bytes8
)之间的转换规则至关重要。核心概念:
tx.origin
vs msg.sender
的区别是许多合约攻击的基础。gasleft()
的值是动态的,依赖它进行验证是脆弱的。攻击向量:
tx.origin
检查。gasleft()
模运算的 gas
值。require
条件来构造一个有效的输入。防御策略:
gas
消耗作为安全机制。在智能合约的世界中,最简单的漏洞往往隐藏着最深刻的安全教训。 🎓