Ethernaut(16-20)详解
字数 1891 2025-08-30 06:50:27
Ethernaut 16-20关详细教学文档
第16关 Preservation
关键知识点
-
delegatecall机制:
- 允许合约执行另一个合约的代码,但保持调用合约的存储、msg.sender和msg.value不变
- 语法:
(bool success, bytes memory data) = target.delegatecall(abi.encodeWithSignature("functionName(uint256)", arg)); - 与call的区别:
- 代码执行:都使用目标合约的代码
- 存储:call使用目标合约的存储,delegatecall使用调用者合约的存储
- this值:call中是被调用者合约地址,delegatecall中是当前合约地址
-
漏洞分析:
- 合约使用delegatecall但未考虑存储布局兼容性
- LibraryContract的
setTime函数会修改slot 0,而Preservation合约的slot 0存储的是timeZone1Library地址
攻击步骤
- 部署攻击合约,其
setTime函数可以修改owner - 调用Preservation的
setFirstTime,将timeZone1Library地址覆盖为攻击合约地址 - 再次调用
setFirstTime,此时会执行攻击合约的代码修改owner
防御措施
- 使用library类型而非普通合约
- 避免对用户可控地址做delegatecall
- 使用OpenZeppelin的UpgradeableProxy模板
第17关 Recovery
关键知识点
-
合约地址计算:
- 常规创建:
keccak256(RLP_encode(address, nonce)) - CREATE2创建:
keccak256(0xff ++ address(this) ++ salt ++ keccak256(init_code))[12:] - 合约的nonce从1开始(创建后变为1)
- 常规创建:
-
RLP编码规则:
- 单字节值(0x00-0x7f):直接使用
- 短字符串(0-55字节):前缀0x80+长度
- 长字符串(>55字节):前缀0xb7+长度长度+长度+内容
- 短列表(总长度≤55):前缀0xc0+总长度
- 长列表(总长度>55):前缀0xf7+长度长度+长度+内容
解题方法
方法一:区块链浏览器查询
- 在etherscan上追踪合约创建交易
- 找到SimpleToken合约地址
- 调用其destroy函数
方法二:地址预计算
- 计算创建者合约地址和nonce(1)的RLP编码
- 计算keccak256哈希
- 取最后20字节作为合约地址
第18关 MagicNumber
关键知识点
-
EVM字节码要求:
- 合约必须返回42(0x2a)
- 代码大小限制为10字节
-
EVM操作码:
- PUSH1: 0x60
- MSTORE: 0x52
- RETURN: 0xF3
-
内存布局:
- 0x00-0x3f: 哈希暂存空间
- 0x40-0x5f: 空闲内存指针
- 0x60-0x7f: 零槽
- 0x80: 可用内存起始位置
解决方案
运行时字节码:
60 2A // PUSH1 0x2A (值42)
60 80 // PUSH1 0x80 (存储位置)
52 // MSTORE (存储)
60 20 // PUSH1 0x20 (返回长度32字节)
60 80 // PUSH1 0x80 (返回位置)
F3 // RETURN
部署方式:
- 直接部署:仅包含运行时代码
- 完整部署:包含初始化逻辑和运行时代码
第19关 AlienCodex
关键知识点
-
动态数组存储:
- 数组长度存储在声明的slot(本例为slot 1)
- 数组元素存储在:
keccak256(slot) + index - 整个存储空间可视为一个环形缓冲区
-
下溢漏洞:
- Solidity 0.6.0以下版本无自动溢出检查
codex.length--可能导致下溢,使length变为2^256-1
攻击步骤
- 调用makeContact()设置contact为true
- 调用retract()使codex.length下溢
- 计算覆盖slot 0所需的index:
2^256 - keccak256(1) - 调用revise()修改"owner"
第20关 Denial
关键知识点
-
call调用特性:
- 默认转发所有剩余gas
- 失败不会导致整个交易回滚
- 无gas限制可能导致DoS
-
攻击原理:
- 通过成为partner合约
- 在接收ETH时耗尽所有gas(无限循环或复杂计算)
- 导致withdraw()调用因gas不足失败
防御措施
- 设置gas限制:
partner.call{value:amountToSend, gas:10000}("") - 使用transfer或send(有固定gas限制)
- 采用pull支付模式而非push模式