Ethernaut_WP(11-15)
字数 1969 2025-08-29 22:41:24
Ethernaut 关卡11-15详细解析与教学文档
第11关:Elevator - 多次外部调用引发逻辑不一致
漏洞分析
该关卡展示了一个典型的"多次外部调用引发逻辑不一致"问题。合约通过接口多次调用外部合约的方法,但两次调用之间返回值可能不同,导致逻辑错误。
关键点:
- 合约实现了一个接口,定义了
isLastFloor函数签名但未实现 - 攻击者可控制外部合约,在两次调用间返回不同值
- 主合约未缓存第一次调用结果,直接进行第二次调用
攻击流程
- 攻击者部署恶意合约,实现
isLastFloor接口 - 第一次调用返回
false,第二次返回true - 绕过
building函数的楼层检查
修复建议
| 修复方法 | 描述 |
|---|---|
| 缓存第一次返回值 | 将第一次调用的结果存储在本地变量中 |
| 限制外部调用次数 | 避免一个逻辑流程中多次调用外部合约 |
| 检查返回值一致性 | 手动检查多次调用结果是否一致 |
| 使用可信源 | 避免依赖用户传入的地址做接口调用 |
第12关:Privacy - 存储布局与类型转换
存储布局分析
Solidity存储使用2²⁵⁶个32字节的槽位。关键概念:
静态变量存储:
- 小于32字节的变量会被打包到同一槽位
- 例如:
bool和uint8可共享一个槽位
动态变量存储:
- 使用标记槽的keccak256哈希作为指针
- 动态数组的标记槽存储数组长度
漏洞利用
- 目标:获取私有变量
data[2]的值 - 通过存储槽计算定位数据:
data数组从槽4开始(前3个槽被其他变量占用)data[2]位于槽6
- 类型转换漏洞:
bytes32转bytes16会截断高位数据
修复建议
- 不要将敏感信息保存在链上(即使是private)
- 使用加密哈希验证而非明文存储
第13关:Gatekeeper One - 多条件绕过
三个关卡分析
Gate One:
- 要求:
msg.sender != tx.origin - 绕过方法:通过中间合约调用
Gate Two:
- 要求:
gasleft() % 8191 == 0 - 解决方法:
- 使用循环尝试0-300偏移量
- 基准点设为
8191 * 3再微调
Gate Three:
- 涉及类型转换:
uint32→uint16→uint64 - 绕过方法:
- 使用
tx.origin推导合适值 - 确保转换后满足
0x11111111 == 0x1111
- 使用
攻击脚本关键点
for (uint256 i = 0; i < 300; i++) {
try attacker.attack{gas: 8191 * 3 + i}(...) {
break;
} catch {}
}
修复建议
| 问题 | 修复建议 |
|---|---|
| Gate One | 使用授权逻辑替代单纯合约检查 |
| Gate Two | 避免依赖gas相关条件 |
| Gate Three | 使用明确身份验证,避免危险的类型转换 |
第14关:Gatekeeper Two - 汇编与构造器利用
三个关卡分析
Gate One:
- 同13关:
msg.sender != tx.origin
Gate Two:
- 使用内联汇编检查
extcodesize - 绕过方法:在构造器中调用(此时代码未完全部署)
Gate Three:
- 要求:
keccak64(sender) ^ gateKey == type(uint64).max - 数学推导:
gateKey = keccak64(sender) ^ 0xFFFFFFFFFFFFFFFF
攻击合约示例
contract Attacker {
constructor(GatekeeperTwo target) {
// Gate Two绕过:构造器中extcodesize为0
bytes8 gateKey = bytes8(keccak64(msg.sender)) ^ type(uint64).max;
target.enter(bytes8(gateKey));
}
function keccak64(address addr) internal pure returns (uint64) {
return uint64(bytes8(keccak256(abi.encodePacked(addr))));
}
}
修复建议
| 问题 | 修复建议 |
|---|---|
| Gate Two | 改为检查msg.sender.code.length > 0 |
| Gate Three | 加入动态元素如block.timestamp增加预测难度 |
第15关:Naught Coin - ERC20转账限制绕过
漏洞分析
- 合约继承自OpenZeppelin ERC20实现
- 重写了
transfer函数添加限制 - 但未重写
transferFrom函数,留下绕过途径
攻击步骤
- 攻击者先调用
approve授权自己(或攻击合约)可支配代币 - 然后使用
transferFrom转移代币 - 绕过
transfer的时间锁限制
修复建议
- 同样对
transferFrom函数添加限制修饰符 - 关键修复代码:
function transferFrom(address sender, address recipient, uint256 amount)
public override lockTokens returns (bool) {
return super.transferFrom(sender, recipient, amount);
}
总结
这五个关卡涵盖了智能合约开发中的多个重要安全概念:
- 外部调用的一致性问题
- 存储布局与类型安全
- 多条件验证的绕过技巧
- 汇编代码与构造器的特殊行为
- ERC20标准实现的安全考虑
开发者应特别注意:
- 外部调用的不可预测性
- 私有数据的实际可见性
- 类型转换的数据丢失风险
- 合约在构造期间的独特状态
- 标准接口的完整实现要求