OpenZeppelin 的智能合约攻防挑战题解
该靶场运行原理
在 Ethernaut(OpenZeppelin 的智能合约攻防挑战平台)中,玩家通过生成新实例、操作并提交实例来完成挑战。这种模式的原理涉及 智能合约工厂模式、状态验证与链上交互,以下是详细解析:
1. 生成新实例(Create Instance)
- 工厂合约部署
每个挑战都有一个工厂合约(如Level01.sol),玩家调用其createInstance()函数时:- 工厂会动态部署一个独立的挑战实例合约(如
Instance01.sol)。 - 实例合约的地址会返回给玩家,并记录在工厂的状态中(如
playerToInstance[msg.sender])。
- 工厂会动态部署一个独立的挑战实例合约(如
- 关键点
- 每个玩家获得的实例是专属的,避免多人操作冲突。
- 实例合约的初始状态包含预设的漏洞或挑战目标(如
bool public isComplete = false)。
2. 操作实例(Exploit)
- 玩家与实例交互
玩家需要分析实例合约的代码,找到漏洞或逻辑缺陷,并通过以下方式操作:- 调用实例的函数(如
hackMe())。 - 修改关键状态变量(如将
isComplete改为true)。 - 可能涉及:重入攻击、权限绕过、存储覆盖等以太坊常见漏洞。
- 调用实例的函数(如
- 链上执行
所有操作通过交易(Transaction)在区块链上执行,确保可验证性和不可篡改性。
3. 提交验证(Submit Instance)
- 工厂合约验证
玩家调用工厂的validateInstance(address instance)函数时:- 工厂检查
instance是否为该玩家生成的合约。 - 调用实例合约的预定义验证方法(如
isComplete())。 - 若验证通过(如返回
true),则标记挑战完成,并可能奖励玩家(如 NFT 徽章)。
- 工厂检查
- 防作弊设计
- 验证逻辑通常放在工厂合约中,而非实例合约,防止玩家直接篡改。
- 实例合约的状态必须通过链上交易修改,确保公平性。
1 Hello Ethernaut
这道题,主要考察的是控制台执行函数调用,通过语句contract.函数名来进行函数的调用,从而查看函数的返回值,然后进入下一个内容,层层递进,最终完成合约交互,提交最终的实例。
生成的实例是真实部署到测试网的合约
执行流程
1 | contract.info() |
一个合约调用另一个合约的变量时,其实是调用的这个变量的getter方法。只要将变量设置为public,那么合约会自动为该变量创建一个getter方法
合约源码:
1 | // SPDX-License-Identifier: MIT |
2 Fallback
仔细看下面的合约代码.
通过这关你需要
- 获得这个合约的所有权
- 把他的余额减到0
这可能有帮助
- 如何通过与ABI互动发送ether
- 如何在ABI之外发送ether
- 转换 wei/ether 单位 (参见
help()命令)- Fallback 方法
合约源码:
1 | // SPDX-License-Identifier: MIT |
漏洞点:
receive()函数在接收 ETH 时,若调用者有贡献记录(contributions[msg.sender] > 0),会将其设为owner。
攻击步骤:
- 调用
contribute()存入少量 ETH
1 | await contract.contribute({value: web3.utils.toWei('0.0001')}); |
- 作用:满足
contributions[msg.sender] > 0的条件。 - 关键限制:必须发送
< 0.001 ETH(因contribute()中有数值检查)。
- 直接发送 ETH 触发
receive()
1 | await contract.sendTransaction({value: web3.utils.toWei('0.0001')}); |
- 原理:
- 直接向合约地址转账(无 calldata)会触发
receive()。 - 由于攻击者已通过
contribute()存入过 ETH,满足contributions[msg.sender] > 0,此时owner被修改为攻击者地址。
- 直接向合约地址转账(无 calldata)会触发
- 调用
withdraw()提取所有 ETH
1 | await contract.withdraw(); |
- 前提:攻击者已成为
owner(通过receive()篡改成功)。 - 结果:合约余额被全部转移到攻击者地址。
修复建议
分离 ETH 接收和权限管理:
1
2
3receive() external payable {
contributions[msg.sender] += msg.value; // 仅更新贡献值
}使用明确的权限转移函数:
1
2
3function transferOwnership(address newOwner) public onlyOwner {
owner = newOwner;
}添加事件日志:
1
event OwnershipTransferred(address oldOwner, address newOwner);
3 Fallout
获得以下合约的所有权来完成这一关.
这可能有帮助
- Solidity Remix IDE
合约源码:
1 | // SPDX-License-Identifier: MIT |