Ethernaut通关(智能合约漏洞)(有缘更新)

Ethernaut通关

参考文章:文章 - Ethernaut闯关录(上) - 先知社区

智能合约是什么?把智能合约想象成网络上的赛博自动售货机,每个人都可以写自己的智能合约,使用虚拟货币交易物品,并且网络区块链中的所有节点都在为你的交易记账不怕商家提桶跑路……当然,每次交易都要交大伙帮忙记账的手续费。

要开始您的冒险,请连接您的 MetaMask 钱包!我们的游戏直接与 Ethernaut 链上合约交互,这意味着您在游戏中的行为会记录在区块链上。通过链接您的 MetaMask 帐户,您可以安全地与这些智能合约互动,从而让您解决挑战并在游戏中取得进展。

image-20250321210720537

安装MetaMask插件,并且选择测试网络后即可开始挑战:

image-20250321210830327

为了能够愉快玩耍,去测试网络的水龙头领取免费的测试 ETH,否则会报错:

image-20250321211826751

在区块链领域,水龙头(Faucet) 指的是一个提供少量加密货币(通常是测试网代币)的服务,供开发者和测试人员使用。

在测试网络(如 Ropsten、Goerli、Sepolia 等)上,交易和智能合约部署需要使用测试代币,而这些代币无法通过常规挖矿或交易获得。因此,区块链社区会提供 Faucet(水龙头)网站,用户可以免费领取测试代币来进行开发和测试。

首先在咸鱼买了个Sepolia测试币

Holešky PoW Faucet中挖矿可以得到更多测试币https://faucets.pk910.de/(要至少两个测试币才可以开始)

或者每天领取(有一定活跃度后才可以领取):

https://goerlifaucet.com

https://docs.metamask.io/developer-tools/faucet/

Hello Ethernaut

这一关可以帮助你初步了解如何体验这个游戏

1. 配置MetaMask
如果你还没有,请安装MetaMask浏览器扩展(在电脑上的Chrome、Firefox、Brave或Opera浏览器中)。 配置好浏览器扩展的钱包,然后在扩展界面的左上角选择偏好的网络,或者你也可以用网页界面的按钮来切换网络。如果你选择了一个不支持的网络,这个游戏将提示你并且为你切换到默认的Sepolia测试网络。

2. 打开浏览器的控制台
打开浏览器控制台: Tools > Developer Tools.

你应该可以看到一些关于游戏的简单的信息,其中一个是你的玩家地址。这在游戏中很重要,你可以通过以下命令查看你的玩家地址:

player
请注意警告和报错,因为他们可能提供了有关游戏的重要信息。

3. 使用控制台协助
你可以通过以下命令来得知你的账户余额:

getBalance(player)
NOTE: 展开promise可以看到真实数值, 即使他显示的是"pending". 如果你使用的是Chrome v62, 你可以使用 await getBalance(player) 用起来更舒爽.
很好!可以使用以下命令获得更多功能:

help()
这在游戏中非常方便.

4. ethernaut 合约
在控制台中输入以下命令:

ethernaut
这个游戏的主要合约,你不需要通过控制台和他直接互动(因为这个应用已经替你做好了),但是如果你想,你还是可以做到。先尝试熟悉它,这是让你了解如何和游戏里其他合约互动的好方法。

然后让我们来展开 ethernaut 看看里面都有啥。

5. 和 ABI 互动
ethernaut 是一个 TruffleContract 对象, 它包装了部署在区块链上的 Ethernaut.sol 合约.

除此之外,合约的 ABI 还提供了所有的 Ethernaut.sol 公开方法, 比如 owner. 比如输入以下命令:

ethernaut.owner()
如果你使用的是 Chrome v62, 可以使用 await ethernaut.owner().
你可以看到这个 ethernaut 合约的所有者是谁, 当然不是你,哈哈 XD.

6. 获得测试以太币
你需要用测试以太币来体验这个游戏。获得测试网络以太币最容易的方法就是通过你选择的网络的有效的水龙头领取。

当你看到你的余额中有些币的时候继续到下一步。

7. 获得这个关卡的实例
当进行这个关卡的时候,你不直接和 ethernaut 合约互动。而是请求生成一个 level instance .为了完成这个步骤,你需要点击页面下方的蓝色按钮. 去点他点他点他,然后再回来!

Metamask会发送请求通过交易授权. 请通过请求, 然后你会在控制台看到一些信息. 注意这会在网络上部署一个新的合约,并且这需要一些时间, 所以请耐心等待!

8. 检查合约
就像你刚才和 ethernaut 合约互动的那样, 你可以通过控制台输入 contract 变量名来观察这个合约的ABI.

9. 和这个合约互动来完成关卡
来看看这个合约的 info 方法

contract.info()
如果你使用的是 Chrome v62, 可以使用 await contract.info() .
你应该已经在合约内找到帮你通过关卡的东西了。 当你知道你已经完成了这个关卡,通过这个页面的橙色按钮提交合约。 这会将你的实例发送回给 ethernaut, 然后来判断你是否完成了任务。

提示: 别忘了你总是可以查看 ABI!
(索引)
player '当前玩家地址'
ethernaut '主游戏合约'
level '当前关卡合约地址'
contract '当前关卡合约实例 (如果已创建)'
instance '当前关卡合约实例地址 (如果已创建)'
version '当前游戏版本'
getBalance(address) '获知地址可用ether数'
getBlockNumber() '获取当前网络区块数'
sendTransaction({options}) '发送交易到'
getNetworkId() '获得以太网id'
toWei(ether) '从ether转换到wei'
fromWei(wei) '从wei转换到ether'
deployAllContracts() 'Deploy all the remaining contracts on the current network

ABI(Application Binary Interface) 是智能合约与外部应用(如 Web3.js、Ethers.js、Truffle)进行交互的接口。它定义了智能合约的方法和数据结构,使得 DApp 可以调用合约的函数、读取状态变量等。

DApp(Decentralized Application,去中心化应用) 是一种基于 区块链智能合约 运行的应用,区别于传统的 Web 应用,它没有中心化的服务器,所有数据和逻辑都存储在 去中心化网络(如以太坊、Solana、BSC 等)上。

Ethernaut.sol 合约(核心协议)

角色:游戏主合约,负责全局管理和关卡逻辑校验
关键功能

  • createLevelInstance():部署新的关卡实例合约
  • submit():验证玩家提交的关卡实例
  • registerLevel():允许管理员注册新关卡模板
  • claim():测试网络代币分发水龙头

Level Instance(动态生成的子合约)

角色:每个关卡的具体逻辑容器
关键特性

  • 通过new关键字动态部署(每个实例唯一地址)
  • 继承自基础关卡模板(如LevelInstance.sol
  • 包含待解决的谜题逻辑和验证方法

TruffleContract(开发工具封装)

角色:前端与智能合约交互的桥梁
核心功能

  • ABI解析:自动将ABI转换为JavaScript方法
  • 交易处理:签名、Gas估算、重播保护
  • 事件监听:自动解码合约事件

ethernaut(TruffleContract实例化对象)

关系图ethernaut = new TruffleContract(ethernautABI, ethernautAddress)
功能特性

  • 已部署合约的本地引用
  • 包含所有可调用方法的快捷方式
  • 自动处理交易签名和网络切换
[玩家] → (创建关卡实例) → [TruffleContract(ethernaut)]
           │
           ▼
┌───────────────────────┐
│   Ethernaut.sol      	│
│ (合约源码逻辑)        	│
│ - createLevelInstance │
│ - submit()            │
│ - 验证逻辑             │
└───────────────────────┘
        │
        ▼
┌───────────────────────┐
│   Eternaut主合约       │
│ (部署在区块链上的实例)	  │
│ - 地址:0xd9Db...      │
│ - 存储游戏状态          │
└───────────────────────┘
        │
        ▼
┌───────────────────────┐
│ LevelInstance合约      │
│ (动态部署的子合约)       │
│ - 继承自Eternaut.sol   │
│ - 包含谜题逻辑          │
└───────────────────────┘
        │
        ▼
┌───────────────────────┐
│   谜题解决方案          │
│ (玩家提交的证明)        │
└───────────────────────┘
        │
        ▼
┌───────────────────────┐
│   Eternaut主合约       │
│ - 验证解决方案          │
│ - 更新游戏状态          │
└───────────────────────┘
        │
        ▼
┌───────────────────────┐
│   区块链数据库          │ 
│ (交易记录与状态持久化)    │
└───────────────────────┘

点击生成实例,触发createLevelInstance方法

image-20250322143219300

实例生成后按照提示查看这个合约的 info 方法:await contract.info()

image-20250322153048503

回显你将会在info1()中找到我们想要的

然后提示在info2()函数中传入"hello"作为参数

再提示属性infoNum中保存着下一个要调用的info方法的编号,查看了一下发现编号是42因此调用info42()

又提示theMethodName与下一个调用的方法(函数)是一样的

image-20250322153822461

提示为函数名为method7123949,然后让你提交密码作为参数到authenticate()

别忘了可以输入contract查看当前合约实例的所有可调用函数(ABI)

image-20250322154041542

可以看到有password,我们执行即可:

image-20250322154159687

完成之后点击提交实例即可,等待的时间比较久

image-20250322155420209

恭喜你! 你完成了这一关. 在下面你可以看到刚才与你交互对合约的代码.

现在你已经可以去攻破后面对所有关卡了, 从现在起, 你需要独自战斗.

冲啊!!!
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

// 定义一个名为 `Instance` 的智能合约
contract Instance {
    // `password` 变量存储合约的密码(用于身份验证)
    string public password;

    // `infoNum` 变量存储一个数字 42,提示用户要调用哪个 info 方法
    uint8 public infoNum = 42;

    // `theMethodName` 变量存储一个方法名称的提示信息
    string public theMethodName = "The method name is method7123949.";

    // `cleared` 变量表示是否已通过身份验证,默认为 false
    bool private cleared = false;

    // 构造函数,在合约部署时初始化 `password`
    constructor(string memory _password) {
        password = _password;
    }

    // info() 方法,返回提示信息,告诉用户下一步要调用 info1()
    function info() public pure returns (string memory) {
        return "You will find what you need in info1().";
    }

    // info1() 方法,返回提示信息,告诉用户需要调用 info2() 并传递参数 "hello"
    function info1() public pure returns (string memory) {
        return 'Try info2(), but with "hello" as a parameter.';
    }

    // info2() 方法,接受一个字符串参数 `param`
    // 如果参数值是 "hello",则返回提示信息,告诉用户 `infoNum` 变量的作用
    function info2(string memory param) public pure returns (string memory) {
        if (keccak256(abi.encodePacked(param)) == keccak256(abi.encodePacked("hello"))) {
            return "The property infoNum holds the number of the next info method to call.";
        }
        return "Wrong parameter.";
    }

    // info42() 方法,只有当用户知道 `infoNum = 42` 时才会调用
    // 返回 `theMethodName` 变量的提示信息
    function info42() public pure returns (string memory) {
        return "theMethodName is the name of the next method.";
    }

    // method7123949() 方法,根据 `theMethodName` 变量的提示,用户需要调用此方法
    // 返回信息,告诉用户需要知道密码并提交到 `authenticate()` 方法
    function method7123949() public pure returns (string memory) {
        return "If you know the password, submit it to authenticate().";
    }

    // authenticate() 方法,接受一个字符串 `passkey` 作为参数
    // 如果 `passkey` 和存储的 `password` 相匹配,则将 `cleared` 设置为 true
    function authenticate(string memory passkey) public {
        if (keccak256(abi.encodePacked(passkey)) == keccak256(abi.encodePacked(password))) {
            cleared = true;
        }
    }

    // getCleared() 方法,返回 `cleared` 变量的值
    // 如果 `cleared` 为 true,说明身份验证成功
    function getCleared() public view returns (bool) {
        return cleared;
    }
}

Fallback

仔细看下面的合约代码.

通过这关你需要

  1. 获得这个合约的所有权
  2. 把他的余额减到0

这可能能够教会你:

  • 如何通过与ABI互动发送ether
  • 如何在ABI之外发送ether
  • 转换 wei/ether 单位 (参见 help() 命令)
  • Fallback 方法
mapping 声明映射类型(类似字典/哈希表)
address => uint256 定义键类型(地址)和值类型(256位无符号整数)
public 允许外部直接读取映射内容(如 contract.contributions(user)
contributions 变量名,用于存储地址与贡献值的对应关系
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

// 定义一个名为 `Fallback` 的智能合约
contract Fallback {
    // `contributions` 变量存储每个地址的贡献金额
    mapping(address => uint256) public contributions;

    // `owner` 变量存储当前合约的所有者地址
    address public owner;

    // 构造函数,在合约部署时执行
    constructor() {
        // 设置合约部署者为初始所有者
        owner = msg.sender;
        // 给予部署者 1000 ether 的贡献值
        contributions[msg.sender] = 1000 * (1 ether);
    }

    // 定义一个 onlyOwner 修饰符,只有所有者才能执行特定的函数
    modifier onlyOwner() {
        require(msg.sender == owner, "caller is not the owner");
        _;
    }

    // `contribute` 方法允许用户进行贡献(捐款)
    function contribute() public payable {
        // 限制每次贡献的金额必须小于 0.001 ether
        require(msg.value < 0.001 ether);

        // 更新贡献值
        contributions[msg.sender] += msg.value;

        // 如果贡献者的贡献值超过当前所有者的贡献值,则变更所有者
        if (contributions[msg.sender] > contributions[owner]) {
            owner = msg.sender;
        }
    }

    // `getContribution` 方法,返回调用者的贡献值
    function getContribution() public view returns (uint256) {
        return contributions[msg.sender];
    }

    // `withdraw` 方法,只有所有者可以调用,提取合约中的全部余额
    function withdraw() public onlyOwner {
        payable(owner).transfer(address(this).balance);
    }

    // `receive` 以太坊回退函数,当合约接收到 ETH 时被触发
    receive() external payable {
        // 仅允许已捐款的地址发送 ETH
        require(msg.value > 0 && contributions[msg.sender] > 0);

        // 触发后,发送 ETH 的用户会成为新的所有者
        owner = msg.sender;
    }
}

通过源代码我们可以了解到要想改变合约的owner可以通过两种方法实现:
1、贡献1000ETH成为合约的owner(虽然在测试网络中我们可以不断的申请测试eth,但由于每次贡献数量需要小于0.001,完成需要1000/0.001次,这显然很不现实~)
2、通过调用回退函数fallback()调用receive()来实现

在receive()函数中,任何向合约发送 ETH(即使不调用任何函数)且满足contributions[msg.sender] > 0的地址都可以成为新所有者
合约只能有一个 receive() 函数(重载会导致编译错误)。
如果同时定义 receive() 和 fallback(),优先执行 receive()。

显然我们这里需要通过第二种方法来获取合约的owner,而触发fallback()函数也有下面两种方式:

  • 没有其他函数与给定函数标识符匹配
  • 合约接收没有数据的纯ether(例如:转账函数))

因此我们可以调用转账函数"await contract.sendTransaction({value:1})"或者使用matemask的转账功能(注意转账地址是合约地址也就是说instance的地址)来触发fallback()函数调用receive()

用户通过sendTransaction或 MetaMask 转账(无数据),也会触发receive(),因为receive()不需要参数。

那么分析到这里我们从理论上就可以获取合约的owner了,那么我们如何转走合约中的eth呢?很明显,答案就是——调用withdraw()函数来实现。

我们首先看看合约地址的资产总量await getbalance(instance)

image-20250322170104215

合约地址资产总量为0,相当于我们没有给自动售货机投币,因此我们先投入测试币增加我们的贡献值await contract.contribute()

image-20250322170248743

有钱了,我们通过调用sendTransaction函数来触发fallback函数并获取合约的ownerawait contract.sendTransaction(Transaction{value:1}),之后再使用await contract.owner()查看合约的owner可以发现成了我们的钱包地址:

image-20250322172008477

在以太坊交易中,value 是必须显式指定的字段,表示交易发送的 ETH 金额(单位:wei)。即使转账金额为 0,也必须显式传递 {value: 0}

此时就可以调用await contract.withdraw()将合约的balance清零,相当于把自动售货机的钱全收走了:

image-20250322172142974

posted @ 2025-03-29 18:53  Super_Snow_Sword  阅读(157)  评论(0)    收藏  举报