前言

课程目标与概述

课程目标

欢迎参加 Move CTF 挑战课程!本课程旨在帮助你掌握 Move 编程语言在 CTF(Capture The Flag)比赛中的应用,培养分析代码和解决安全挑战的能力。通过八节课的学习,你将能够:

  • 熟练分析 Move 语言代码,识别常见漏洞和逻辑错误。
  • 掌握 Move 在 CTF 中的典型题型,如整数溢出、资源管理和权限控制。
  • 具备独立解决基础至中级 Move CTF 题目的能力。
  • 为参加 Move 相关的 CTF 比赛做好准备,树立信心。

无论你是区块链开发者、安全研究者还是 CTF 爱好者,本课程都将为你提供独特的视角,探索 Move 语言的安全特性及其在竞赛中的潜力。

课程概述

  • 课程结构:共八章节,从零开始进阶学习Move CTF。
  • 学习路径
    • 从 CTF 简介和基础代码审计开始,逐步深入到高级漏洞和综合挑战。
    • 每节课聚焦一个主题,配备一道 CTF 题目,循序渐进提升难度。
  • 前提假设:你已通过基础 Move 语言学习( HOH社区Move共学),熟悉基本语法、模块和资源概念。
  • 预期成果:完成课程后,你将能独立分析 Move 智能合约代码,解决 CTF 挑战,并具备参加真实比赛的基本策略。

本课程结合理论与实践,鼓励动手操作和互动讨论。准备好迎接挑战了吗?让我们一起开启 Move CTF 的学习之旅!

前言

课程目标与概述

课程目标

欢迎参加 Move CTF 挑战课程!本课程旨在帮助你掌握 Move 编程语言在 CTF(Capture The Flag)比赛中的应用,培养分析代码和解决安全挑战的能力。通过八节课的学习,你将能够:

  • 熟练分析 Move 语言代码,识别常见漏洞和逻辑错误。
  • 掌握 Move 在 CTF 中的典型题型,如整数溢出、资源管理和权限控制。
  • 具备独立解决基础至中级 Move CTF 题目的能力。
  • 为参加 Move 相关的 CTF 比赛做好准备,树立信心。

无论你是区块链开发者、安全研究者还是 CTF 爱好者,本课程都将为你提供独特的视角,探索 Move 语言的安全特性及其在竞赛中的潜力。

课程概述

  • 课程结构:共八章节,从零开始进阶学习Move CTF。
  • 学习路径
    • 从 CTF 简介和基础代码审计开始,逐步深入到高级漏洞和综合挑战。
    • 每节课聚焦一个主题,配备一道 CTF 题目,循序渐进提升难度。
  • 前提假设:你已通过基础 Move 语言学习( HOH社区Move共学),熟悉基本语法、模块和资源概念。
  • 预期成果:完成课程后,你将能独立分析 Move 智能合约代码,解决 CTF 挑战,并具备参加真实比赛的基本策略。

本课程结合理论与实践,鼓励动手操作和互动讨论。准备好迎接挑战了吗?让我们一起开启 Move CTF 的学习之旅!

预备知识与工具安装

预备知识

为了顺利完成本课程,你需要具备以下基础知识:

  • Move 语言基础
    • 熟悉变量声明、基本数据类型(u8、u64、address 等)和控制流(if、while)。
    • 理解模块(module)和资源(struct)的概念。
    • 能够编写和运行简单的 Move 程序(如 Hello World)。
    • 推荐资源:HOH社区Move共学Sui Move Book
  • 区块链基础
    • 了解智能合约的基本概念(如存储、交易)。
    • 对 Sui 区块链平台有初步认识。
  • CTF 基础(可选):
    • 知道 CTF 比赛的基本形式将有助于更快上手。

如果以上知识点有欠缺,建议先完成基础学习再加入课程。本课程将直接聚焦 Move 在 CTF 中的应用,跳过语言基础教学。

工具安装

以下是你需要安装的工具,确保在第一节课前配置好开发环境:

  1. Sui CLI

    • 用于编译、运行和调试 Move 代码。
    • 安装步骤:
    • 验证:运行 sui -V 检查安装成功。
  2. VS Code + Move 插件

    • 提供代码高亮和语法检查。

    • 安装步骤:

      1. 下载 VS Code
      2. 在扩展市场搜索 MoveSui Move,安装相关插件。

      image-20250325171039560

      MoveMove syntax 插件为提供代码高亮和语法检查,Move Formatter Developer Preview插件提供代码格式化。

环境验证

  • 运行以下命令测试环境:
    sui move new <path-to-move-project> && cd <path-to-move-project> && sui move build
    
  • 如果编译成功,说明环境配置正确。

准备好这些工具后,你就可以无缝进入课程实践环节。遇到安装问题?请提前联系课程团队或查阅相关文档。

第1节:CTF简介与Move应用

CTF比赛类型与Move应用场景

欢迎来到 Move CTF 挑战课程 的第一节!本节将带你走进 CTF(Capture The Flag,夺旗赛)的世界,了解其主要比赛类型,并探索 Move 编程语言在 Sui 区块链上的 CTF 应用场景。通过理论与实践结合,你将迈出学习 Move CTF 的第一步。

什么是CTF?

CTF 是一种网络安全竞赛形式,参与者通过解决技术挑战获取隐藏的 “flag”(通常是一串特定格式的字符串,如 CTF{xxx}),以证明他们的技能。CTF 起源于 1996 年的 DEFCON 安全大会,现已成为全球安全爱好者的热门活动。比赛类型主要分为以下两种:

1. Jeopardy模式

  • 特点:解题模式,参与者面对一系列独立题目,涵盖密码学、逆向工程、Web 安全、区块链等类别。
  • 流程
    • 题目提供线索(如代码片段、文件或服务器地址)。
    • 选手分析问题,找到 flag 并提交。
    • 根据解题数量和速度计分。
  • 适合人群:初学者和个人选手,因其灵活性和低门槛广受欢迎。
  • Move相关示例:分析一段 Sui 链上的 Move 智能合约代码,找出隐藏的 flag 或利用漏洞提取数据。
  • 真实案例
  • 现状:当前 Move CTF(如 justCTF、MoveCTF)主要采用 Jeopardy 模式,题目以代码审计和逻辑分析为主。

2. Attack-Defense模式

  • 特点:攻防对抗模式,团队在虚拟环境中同时攻击对手服务并防御自身系统。
  • 流程
    • 每个团队维护一个包含漏洞的服务。
    • 攻击对手以获取 flag,同时修补自身漏洞。
    • 综合得分决定排名。
  • 适合人群:进阶选手和团队,因其更接近真实网络攻防场景。
  • Move相关示例:模拟 Sui 区块链网络,攻击对手的 Move 合约(如利用未授权访问漏洞窃取资源),同时保护自己的合约免受攻击。
  • 现状:目前 Move CTF 未见公开的攻防模式案例,更多聚焦于解题形式的挑战。

CTF的吸引力

  • 技能提升:涵盖编程、逆向、安全分析等多领域。
  • 实战性:模拟真实安全场景,如区块链漏洞利用。
  • 趣味性:解题过程如同解谜,充满成就感。

Move 在 CTF 中的应用

Move 是由 Facebook(现 Meta)为 Diem 区块链设计的一种编程语言,后被 Sui 公链采用并优化。它以资源导向和类型安全著称,灵感来源于 Rust。在 Sui 生态中,Move 用于编写智能合约和去中心化应用(DApp)。以下是 Move 在 CTF 中的主要应用场景:

1. 智能合约漏洞挖掘

  • 背景:Sui 链上的 Move 合约是 CTF 题目的常见素材,选手需分析代码,寻找漏洞。
  • 常见考点
    • 整数溢出/下溢:Move 未内置溢出检查,可能导致非法操作。例如,代币转移逻辑可能因溢出被绕过。
    • 逻辑错误:如条件判断失误,导致意外行为。
  • 示例:假设一个 Move 代币合约未检查整数加法的溢出,选手可通过构造大额输入转移超出余额的代币,flag 可能隐藏在交易事件中。
  • Move特点:Move 的类型系统减少了某些传统漏洞(如重入攻击),但仍需关注逻辑和边界问题。

2. 资源管理挑战

  • Move特性:Move 的资源(struct)具有线性类型特性,不可随意复制或丢弃,必须显式转移或销毁。这是 Sui 链上对象(Object)管理的核心机制。
  • CTF应用
    • 题目可能涉及资源管理不当(如未销毁资源)导致的漏洞。
    • 或通过资源转移逻辑的错误绕过限制。
  • 示例:设想一个 Move 合约中,开发者忘记销毁旧资源,导致资源被重复使用,选手可利用此漏洞执行双重花费。

3. 代码逻辑逆向

  • 背景:CTF 常要求选手理解代码意图,寻找隐藏信息。
  • Move场景
    • flag 可能藏在注释、变量名或 Sui 链上的事件输出中。
    • 或通过复杂逻辑(如循环、条件)计算得出。
  • 真实案例:在 justCTF 2024 Teaser 的“The Otter Scrolls”题目(justCTF 2024)中,选手需分析 Sui Move 合约 Spellbook,调用 cast_spell 函数并传入参数 vector[1, 0, 3, 3, 3],触发 flag 输出。
  • 总结:Move 的资源导向和类型安全特性为 CTF 提供了独特的技术挑战,目前通过 Jeopardy 模式吸引爱好者参与并提升 Sui 生态的安全意识。

学习目标

通过本节课,你将:

  • 理解 CTF 比赛的基本形式(Jeopardy 和 Attack-Defense)。
  • 认识 Move 在 Sui 链 CTF 中的应用场景。
  • 通过实践,初步体验 Move 代码分析,迈出 CTF 第一步。

准备好了吗?接下来,我们将通过一个简单实践,体验 Move CTF!

第1节:CTF简介与Move应用

CTF比赛类型与Move应用场景

欢迎来到 Move CTF 挑战课程 的第一节!本节将带你走进 CTF(Capture The Flag,夺旗赛)的世界,了解其主要比赛类型,并探索 Move 编程语言在 Sui 区块链上的 CTF 应用场景。通过理论与实践结合,你将迈出学习 Move CTF 的第一步。

什么是CTF?

CTF 是一种网络安全竞赛形式,参与者通过解决技术挑战获取隐藏的 “flag”(通常是一串特定格式的字符串,如 CTF{xxx}),以证明他们的技能。CTF 起源于 1996 年的 DEFCON 安全大会,现已成为全球安全爱好者的热门活动。比赛类型主要分为以下两种:

1. Jeopardy模式

  • 特点:解题模式,参与者面对一系列独立题目,涵盖密码学、逆向工程、Web 安全、区块链等类别。
  • 流程
    • 题目提供线索(如代码片段、文件或服务器地址)。
    • 选手分析问题,找到 flag 并提交。
    • 根据解题数量和速度计分。
  • 适合人群:初学者和个人选手,因其灵活性和低门槛广受欢迎。
  • Move相关示例:分析一段 Sui 链上的 Move 智能合约代码,找出隐藏的 flag 或利用漏洞提取数据。
  • 真实案例
  • 现状:当前 Move CTF(如 justCTF、MoveCTF)主要采用 Jeopardy 模式,题目以代码审计和逻辑分析为主。

2. Attack-Defense模式

  • 特点:攻防对抗模式,团队在虚拟环境中同时攻击对手服务并防御自身系统。
  • 流程
    • 每个团队维护一个包含漏洞的服务。
    • 攻击对手以获取 flag,同时修补自身漏洞。
    • 综合得分决定排名。
  • 适合人群:进阶选手和团队,因其更接近真实网络攻防场景。
  • Move相关示例:模拟 Sui 区块链网络,攻击对手的 Move 合约(如利用未授权访问漏洞窃取资源),同时保护自己的合约免受攻击。
  • 现状:目前 Move CTF 未见公开的攻防模式案例,更多聚焦于解题形式的挑战。

CTF的吸引力

  • 技能提升:涵盖编程、逆向、安全分析等多领域。
  • 实战性:模拟真实安全场景,如区块链漏洞利用。
  • 趣味性:解题过程如同解谜,充满成就感。

Move 在 CTF 中的应用

Move 是由 Facebook(现 Meta)为 Diem 区块链设计的一种编程语言,后被 Sui 公链采用并优化。它以资源导向和类型安全著称,灵感来源于 Rust。在 Sui 生态中,Move 用于编写智能合约和去中心化应用(DApp)。以下是 Move 在 CTF 中的主要应用场景:

1. 智能合约漏洞挖掘

  • 背景:Sui 链上的 Move 合约是 CTF 题目的常见素材,选手需分析代码,寻找漏洞。
  • 常见考点
    • 整数溢出/下溢:Move 未内置溢出检查,可能导致非法操作。例如,代币转移逻辑可能因溢出被绕过。
    • 逻辑错误:如条件判断失误,导致意外行为。
  • 示例:假设一个 Move 代币合约未检查整数加法的溢出,选手可通过构造大额输入转移超出余额的代币,flag 可能隐藏在交易事件中。
  • Move特点:Move 的类型系统减少了某些传统漏洞(如重入攻击),但仍需关注逻辑和边界问题。

2. 资源管理挑战

  • Move特性:Move 的资源(struct)具有线性类型特性,不可随意复制或丢弃,必须显式转移或销毁。这是 Sui 链上对象(Object)管理的核心机制。
  • CTF应用
    • 题目可能涉及资源管理不当(如未销毁资源)导致的漏洞。
    • 或通过资源转移逻辑的错误绕过限制。
  • 示例:设想一个 Move 合约中,开发者忘记销毁旧资源,导致资源被重复使用,选手可利用此漏洞执行双重花费。

3. 代码逻辑逆向

  • 背景:CTF 常要求选手理解代码意图,寻找隐藏信息。
  • Move场景
    • flag 可能藏在注释、变量名或 Sui 链上的事件输出中。
    • 或通过复杂逻辑(如循环、条件)计算得出。
  • 真实案例:在 justCTF 2024 Teaser 的“The Otter Scrolls”题目(justCTF 2024)中,选手需分析 Sui Move 合约 Spellbook,调用 cast_spell 函数并传入参数 vector[1, 0, 3, 3, 3],触发 flag 输出。
  • 总结:Move 的资源导向和类型安全特性为 CTF 提供了独特的技术挑战,目前通过 Jeopardy 模式吸引爱好者参与并提升 Sui 生态的安全意识。

学习目标

通过本节课,你将:

  • 理解 CTF 比赛的基本形式(Jeopardy 和 Attack-Defense)。
  • 认识 Move 在 Sui 链 CTF 中的应用场景。
  • 通过实践,初步体验 Move 代码分析,迈出 CTF 第一步。

准备好了吗?接下来,我们将通过一个简单实践,体验 Move CTF!

实践:签到挑战

题目描述

在本实践环节,你将分析一个 Sui Move 签到合约,通过计算哈希值调用函数,获取隐藏的 flag。flag 是一个格式为 CTF{xxx} 的字符串,将在正确输入时通过事件输出。目标是体验 Move 代码分析和基本 CTF 解题流程。

示例代码

以下是待分析的 Move 合约:

github: chapter_1

module chapter_1::check_in {
    use std::string::{Self, String};
    use std::bcs;
    use std::hash::sha3_256;
    use sui::event;

    //testnet
    //PackageID:0x335297860a807291254b20f8a0dea30d72d5e17d2e6f8058e42d5b9c72f0f0ef
    public struct FlagEvent has copy, drop {
        sender: address,
        flag: String,
        success: bool
    }

    public entry fun get_flag(
        flag: vector<u8>,
        github_id: String,
        ctx: &mut TxContext
    ) {
        let mut bcs_input = bcs::to_bytes(&string::utf8(b"LetsMoveCTF"));
        vector::append<u8>(&mut bcs_input, *github_id.as_bytes());
        let expected_hash = sha3_256(bcs_input);

        if (flag == expected_hash) {
            event::emit(FlagEvent {
                sender: tx_context::sender(ctx),
                flag: string::utf8(b"CTF{WelcomeToMoveCTF}"),
                success: true
            });
        } else {
            event::emit(FlagEvent {
                sender: tx_context::sender(ctx),
                flag: string::utf8(b"Try again!"),
                success: false
            });
        }
    }
}

任务目标

阅读代码,理解哈希验证逻辑。

计算正确的 flag 输入并运行代码,获取 flag。

解题思路

1、找到如何获取flag的代码块:

##其中 `flag == expected_hash` 为获取flag的条件
if (flag == expected_hash) {
    event::emit(FlagEvent {
        sender: tx_context::sender(ctx),
        flag: string(b"CTF{WelcomeToMoveCTF}"),
        success: true
    });
} else {
    event::emit(FlagEvent {
        sender: tx_context::sender(ctx),
        flag: string(b"Try again!"),
        success: false
    });
}

2、如何满足 flag == expected_hash 条件?

let mut bcs_input = bcs::to_bytes(&string(b"LetsMoveCTF"));
vector::append<u8>(&mut bcs_input, *github_id.as_bytes());
let expected_hash = sha3_256(bcs_input);

代码块中 expected_hash 为 LetsMoveCTF + 用户输入的github_id 转换为bytes然后sha3_256 进行编码,所以flag传入也需要是expected_hash的这个结果。

3、进行解题

这里采用的是合约的方式进行解题。

github: solve_chapter_1 首先创建合约:

sui move new solve_chapter_1 && cd solve_chapter_1

修改 Move.toml 文件导入题目合约,如果是本地与题目同目录则添加 local 方式:

[package]
name = "solve_chapter_1"
edition = "2024.beta" # edition = "legacy" to use legacy (pre-2024) Move
# license = ""           # e.g., "MIT", "GPL", "Apache 2.0"
# authors = ["..."]      # e.g., ["Joe Smith (joesmith@noemail.com)", "John Snow (johnsnow@noemail.com)"]

[dependencies]
chapter_1 = { local = "../chapter_1" }

# For remote import, use the `{ git = "...", subdir = "...", rev = "..." }`.
# Revision can be a branch, a tag, and a commit hash.
# MyRemotePackage = { git = "https://some.remote/host.git", subdir = "remote/path", rev = "main" }

# For local dependencies use `local = path`. Path is relative to the package root
# Local = { local = "../path/to" }

# To resolve a version conflict and force a specific version for dependency
# override use `override = true`
# Override = { local = "../conflicting/version", override = true }

[addresses]
solve_chapter_1 = "0x0"

# Named addresses will be accessible in Move as `@name`. They're also exported:
# for example, `std = "0x1"` is exported by the Standard Library.
# alice = "0xA11CE"

[dev-dependencies]
# The dev-dependencies section allows overriding dependencies for `--test` and
# `--dev` modes. You can introduce test-only dependencies here.
# Local = { local = "../path/to/dev-build" }

[dev-addresses]
# The dev-addresses section allows overwriting named addresses for the `--test`
# and `--dev` modes.
# alice = "0xB0B"

然后编写解题合约 solve_chapter_1.move:

module solve_chapter_1::solve{
    use chapter_1::check_in::get_flag;
    use std::string;
    use std::bcs;
    use std::hash::sha3_256;

    //testnet
    //PackageID: 0xef6b4139ec1b0fda23e06c4a30c9e91150b72c38530e4517152e591001c5c433 
    public entry fun solve_get_flag(ctx: &mut TxContext){
        let github_id = string::utf8(b"hoh-zone");
        let mut bcs_input = bcs::to_bytes(&string::utf8(b"LetsMoveCTF"));
        vector::append<u8>(&mut bcs_input, *github_id.as_bytes());
        let flag_hash = sha3_256(bcs_input);
        get_flag(flag_hash, github_id, ctx);  
    }
}

发布合约:

sui client publish

发布成功后调用合约:

sui client call --package 0xef6b4139ec1b0fda23e06c4a30c9e91150b72c38530e4517152e591001c5c433 --module solve --function solve_get_flag

最终结果可以看到终端输出的Events内成功获取flag:

╭───────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Transaction Block Events                                                                              │
├───────────────────────────────────────────────────────────────────────────────────────────────────────┤
│  ┌──                                                                                                  │
│  │ EventID: EjQiJnPZRqen1TSSkNiUaRaEQYSmhshJYHctn3uUt1V5:0                                            │
│  │ PackageID: 0xef6b4139ec1b0fda23e06c4a30c9e91150b72c38530e4517152e591001c5c433                      │
│  │ Transaction Module: solve                                                                          │
│  │ Sender: 0x90abb670800b4015229d30f5d010faef0c347e1d9650c9acebe2c012be7eb724                         │
│  │ EventType: 0x335297860a807291254b20f8a0dea30d72d5e17d2e6f8058e42d5b9c72f0f0ef::check_in::FlagEvent │
│  │ ParsedJSON:                                                                                        │
│  │   ┌─────────┬────────────────────────────────────────────────────────────────────┐                 │
│  │   │ flag    │ CTF{WelcomeToMoveCTF}                                              │                 │
│  │   ├─────────┼────────────────────────────────────────────────────────────────────┤                 │
│  │   │ sender  │ 0x90abb670800b4015229d30f5d010faef0c347e1d9650c9acebe2c012be7eb724 │                 │
│  │   ├─────────┼────────────────────────────────────────────────────────────────────┤                 │
│  │   │ success │ true                                                               │                 │
│  │   └─────────┴────────────────────────────────────────────────────────────────────┘                 │
│  └──                                                                                                  │
╰───────────────────────────────────────────────────────────────────────────────────────────────────────╯

Task1

任务代码

以下是待分析的 Move 合约,完整代码请查看 挑战合约

PackageID: 0xcd6050b4e93c5bdc7149f2bf0b69202ec297bf1532d500e896dbefcd15d811f4 
Challenge ObjectID: 0xf5dc0a1701384ff0ff6697ae9de37ee0cc832ff1fd511232aca7d0fff282d026
module task1::task1 {
    use std::bcs;
    use std::hash::sha3_256;
    use std::string::{Self, String};
    use sui::event;
    use sui::random::{Self, Random};
    use sui::transfer::share_object;

    const EINVALID_HASH: u64 = 0;
    const EINVALID_MAGIC: u64 = 1;
    const EINVALID_SEED: u64 = 2;

    public struct FlagEvent has copy, drop {
        sender: address,
        flag: String,
        attempt_count: u64,
        github_id: String,
        success: bool
    }

    public struct Challenge has key {
        id: UID,
        secret: String,
        attempt_count: u64,
        last_seed: u64
    }

    fun init(ctx: &mut TxContext) {
        let challenge = Challenge {
            id: object::new(ctx),
            secret: string::utf8(b"MoveCTF_task1"),
            attempt_count: 0,
            last_seed: 0
        };
        share_object(challenge);
    }

    public entry fun get_flag(
        hash_input: vector<u8>,
        github_id: String,
        magic_number: u64,
        seed: u64,
        challenge: &mut Challenge,
        rand: &Random,
        ctx: &mut TxContext
    ) {
        let mut bcs_input = bcs::to_bytes(&challenge.secret);
        vector::append(&mut bcs_input, *github_id.as_bytes());
        let expected_hash = sha3_256(bcs_input);
        assert!(hash_input == expected_hash, EINVALID_HASH);

        challenge.attempt_count = challenge.attempt_count + 1;

        let expected_magic = (challenge.attempt_count * challenge.attempt_count + challenge.last_seed) % 1000 + seed;
        assert!(magic_number == expected_magic, EINVALID_MAGIC);

        let secret_bytes = *string::as_bytes(&challenge.secret);
        let secret_len = vector::length(&secret_bytes);
        assert!(seed == secret_len * 2, EINVALID_SEED);

        challenge.secret = getRandomString(rand, ctx);
        challenge.last_seed = seed;

        event::emit(FlagEvent {
            sender: tx_context::sender(ctx),
            flag: string::utf8(b"CTF{MoveCTF-Task1}"),
            github_id,
            attempt_count: challenge.attempt_count,
            success: true
        });
    }

    fun getRandomString(rand: &Random, ctx: &mut TxContext): String {
        let mut gen = random::new_generator(rand, ctx);
        let mut str_len = random::generate_u8_in_range(&mut gen, 4, 32);
        let mut rand_vec: vector<u8> = b"";
        while (str_len != 0) {
            let rand_num = random::generate_u8_in_range(&mut gen, 34, 126);
            vector::push_back(&mut rand_vec, rand_num);
            str_len = str_len - 1;
        };
        string::utf8(rand_vec)
    }
}

第2节:基础代码审计

阅读 Move 代码与常见问题

欢迎来到 Move CTF 挑战课程 的第二节!在第一节中,你通过一个签到挑战初步体验了 Move 代码的分析和解题流程。现在,我们将迈出审计的第一步,深入学习如何阅读 Move 代码并识别常见问题。本节将通过详细的理论讲解和实践环节,帮助你掌握基础审计技能,为后续更复杂的漏洞分析打下坚实基础。

1. 模块与函数

  • 模块(Module)
    • Move 的代码组织单元,类似于传统语言中的合约或类。
    • 格式:module <address>::<name>,其中 <address> 是部署地址(如 0x1),<name> 是模块名。
    • 包含结构体、常量和函数,定义合约的逻辑。
    • 示例:
      module chapter_2_test::counter {
      
          public struct Counter has key {
              id: UID,
              count: u64,
          }
      
          fun init(ctx: &mut TxContext){
              transfer::share_object(Counter { id:object::new(ctx), count: 0 });
              
          }
      
          public entry fun increment(counter: &mut Counter) {
              counter.count = counter.count + 1;
          }
      }
      
  • 函数类型
    • public entry fun:外部可调用的入口函数,通常是 CTF 题目中的交互点,接受 &mut TxContext 参数以获取交易上下文。
    • public fun:公开函数,可被其他模块调用,但不直接作为交易入口。
    • fun:私有函数,仅模块内部使用。
    • 示例:
      public entry fun set_value(value: u64, tx: &mut TxContext) { 
          /* 交易入口 */ 
      }
      fun internal_add(a: u64, b: u64): u64 { a + b } // 内部辅助函数
      

2. 资源与所有权

  • 资源(Resource)
    • Move 的核心特性,使用 struct 定义,带有能力(has 声明,如 keystore)。
    • 线性类型:资源不可复制(copy)或丢弃(drop),必须显式转移或销毁。
    • 示例:
      public struct Coin has key {
          id: UID,
          value: u64  
      }
      
  • 所有权管理
    • 创建资源后,可通过 transfer::transfer(转移给地址)或 transfer::share_object(共享对象)等处理。
    • 示例:
      public entry fun create(ctx: &mut TxContext){
          let coin = Coin { id: object::new(ctx), value: 100 };
          transfer::transfer(coin, tx_context::sender(ctx));
      }
      
    • 未处理资源会导致编译错误,确保所有权清晰。
  • Sui 特有机制
    • 对象(Object):通过 UID(唯一标识符)管理,Sui 的基本数据单元。
    • 共享对象:通过 transfer::share_object 创建,允许多人操作,常用于 CTF 的共享状态。

3. 事件与输出

  • 事件(Event)
    • 通过 event::emit 输出日志,用于记录状态变化或 CTF 中的 flag 输出。
    • 需定义事件结构体,具备 copydrop 能力。
    • 示例:
      public struct FlagEvent has copy, drop {
          sender: address,
          flag: vector<u8>
      }
      
      public entry fun get_flag(ctx: &mut TxContext){
          event::emit(FlagEvent { sender: tx_context::sender(ctx), flag: b"CTF{example}" });
      }
      
    • CTF 中,事件常是获取 flag 的关键途径。

4. 变量与类型

  • 基本类型
    • u8u64u128:无符号整数。
    • bool:布尔值。
    • address:账户地址。
    • vector<T>:动态数组。
  • 引用
    • &T:不可变引用,用于读取。
    • &mut T:可变引用,用于修改。
    • 示例:
      public entry fun increment(counter: &mut Counter) {
          counter.count = counter.count + 1;
      }
      

常见问题与漏洞类型

1. 未验证的输入

  • 影响
    • limit 被设置为异常值(如 2^64 - 1),后续逻辑可能失效。
    • 在 CTF 中,可能通过异常输入绕过限制或提取 flag。

2. 逻辑错误

  • 问题:条件判断、状态更新或流程控制错误,导致与设计意图不符的结果。

  • 详细说明

    • Move 依赖开发者正确实现逻辑,无内置保护机制。
    • 常见于条件遗漏、顺序错误或判断反转。
  • 示例

    public struct AccessControl has key {
        id: UID,
        is_allowed: bool,
        threshold: u64
    }
    
    public entry fun check_access(access: &mut AccessControl, score: u64) {
        if (score > 50) { // 应为 >= 50,但是设置为 >50
            access.is_allowed = true;
        }
    }
    
  • 边界值 score = 50 被意外排除.

  • 更复杂示例

    public entry fun update_state(state: &mut AccessControl, value: u64) {
      if (value < state.threshold) {
          state.is_allowed = false; // 可能应为 true
      }
    }
    
    • 若意图是“低于阈值激活”,条件与赋值不符。
  • 影响

    • 可能允许未授权操作或阻止合法行为。

3. 权限控制不足

  • 问题:函数未限制调用者身份,允许任何人执行敏感操作。
  • 详细说明
    • Move 的 public entry fun 默认对所有地址开放,需手动验证 tx_context::sender.
    • Sui 的共享对象尤其需注意权限。
  • 示例
    public entry fun reset_counter(counter: &mut Counter) { // 未验证调用者
      counter.count = 0; 
    }
    
    • 任何人都可重置计数器。
  • 更实际示例
    public struct SuiPool has key {
        id: UID,
        suiBalance: Balance<0x2::sui::SUI>,
    }
    
    public entry fun withdraw_commision(
        suipool: &mut SuiPool,
        amount: u64,
        to: address,
        ctx: &mut TxContext,
    ) {
        assert!(suipool.suiBalance.value() > amount, 1);
        let coin_balance = suipool.suiBalance.split(amount);
        let coin = from_balance(coin_balance, ctx);
        public_transfer(coin, to);
    }
    
    • 非管理者可提取余额。
  • 影响
    • 未授权用户可能破坏合约状态或窃取资源。

4. 整数溢出/下溢

  • 问题:在 Sui 中,Move 的整数运算(如 u64 的加法、减法)默认启用溢出检查,溢出或下溢会导致交易失败.
  • 示例
    module counter::counter{
        use sui::event;
    
        public struct Counter has key {
            id: UID,
            count: u64,
        }
    
        public struct CounterEmit has copy, drop{
            count: u64,
        }
    
        fun init(ctx: &mut TxContext){
            transfer::share_object(Counter { id:object::new(ctx), count: 0 });
        }
    
        public entry fun add(counter: &mut Counter,amount: u64){
            counter.count = counter.count + amount;
            event::emit(CounterEmit { count: counter.count })
        }
    
        public entry fun reduce(counter: &mut Counter,amount: u64){
            counter.count = counter.count - amount;
            event::emit(CounterEmit { count: counter.count })
        }
    }
    
  • 影响
    • 若 counter + amount > 2^64 - 1 或 counter - amount < 0 会抛出 MovePrimitiveRuntimeError,交易失败,无法继续执行。

5. 资源管理不当

  • 问题:资源未正确转移或销毁,导致编译错误。
  • 示例
    public entry fun create_coin(ctx: &mut TxContext) {
      let coin = Coin { id: object::new(ctx), value: 100 };
    }
    
  • 影响
    • 编译错误。

第2节:基础代码审计

阅读 Move 代码与常见问题

欢迎来到 Move CTF 挑战课程 的第二节!在第一节中,你通过一个签到挑战初步体验了 Move 代码的分析和解题流程。现在,我们将迈出审计的第一步,深入学习如何阅读 Move 代码并识别常见问题。本节将通过详细的理论讲解和实践环节,帮助你掌握基础审计技能,为后续更复杂的漏洞分析打下坚实基础。

1. 模块与函数

  • 模块(Module)
    • Move 的代码组织单元,类似于传统语言中的合约或类。
    • 格式:module <address>::<name>,其中 <address> 是部署地址(如 0x1),<name> 是模块名。
    • 包含结构体、常量和函数,定义合约的逻辑。
    • 示例:
      module chapter_2_test::counter {
      
          public struct Counter has key {
              id: UID,
              count: u64,
          }
      
          fun init(ctx: &mut TxContext){
              transfer::share_object(Counter { id:object::new(ctx), count: 0 });
              
          }
      
          public entry fun increment(counter: &mut Counter) {
              counter.count = counter.count + 1;
          }
      }
      
  • 函数类型
    • public entry fun:外部可调用的入口函数,通常是 CTF 题目中的交互点,接受 &mut TxContext 参数以获取交易上下文。
    • public fun:公开函数,可被其他模块调用,但不直接作为交易入口。
    • fun:私有函数,仅模块内部使用。
    • 示例:
      public entry fun set_value(value: u64, tx: &mut TxContext) { 
          /* 交易入口 */ 
      }
      fun internal_add(a: u64, b: u64): u64 { a + b } // 内部辅助函数
      

2. 资源与所有权

  • 资源(Resource)
    • Move 的核心特性,使用 struct 定义,带有能力(has 声明,如 keystore)。
    • 线性类型:资源不可复制(copy)或丢弃(drop),必须显式转移或销毁。
    • 示例:
      public struct Coin has key {
          id: UID,
          value: u64  
      }
      
  • 所有权管理
    • 创建资源后,可通过 transfer::transfer(转移给地址)或 transfer::share_object(共享对象)等处理。
    • 示例:
      public entry fun create(ctx: &mut TxContext){
          let coin = Coin { id: object::new(ctx), value: 100 };
          transfer::transfer(coin, tx_context::sender(ctx));
      }
      
    • 未处理资源会导致编译错误,确保所有权清晰。
  • Sui 特有机制
    • 对象(Object):通过 UID(唯一标识符)管理,Sui 的基本数据单元。
    • 共享对象:通过 transfer::share_object 创建,允许多人操作,常用于 CTF 的共享状态。

3. 事件与输出

  • 事件(Event)
    • 通过 event::emit 输出日志,用于记录状态变化或 CTF 中的 flag 输出。
    • 需定义事件结构体,具备 copydrop 能力。
    • 示例:
      public struct FlagEvent has copy, drop {
          sender: address,
          flag: vector<u8>
      }
      
      public entry fun get_flag(ctx: &mut TxContext){
          event::emit(FlagEvent { sender: tx_context::sender(ctx), flag: b"CTF{example}" });
      }
      
    • CTF 中,事件常是获取 flag 的关键途径。

4. 变量与类型

  • 基本类型
    • u8u64u128:无符号整数。
    • bool:布尔值。
    • address:账户地址。
    • vector<T>:动态数组。
  • 引用
    • &T:不可变引用,用于读取。
    • &mut T:可变引用,用于修改。
    • 示例:
      public entry fun increment(counter: &mut Counter) {
          counter.count = counter.count + 1;
      }
      

常见问题与漏洞类型

1. 未验证的输入

  • 影响
    • limit 被设置为异常值(如 2^64 - 1),后续逻辑可能失效。
    • 在 CTF 中,可能通过异常输入绕过限制或提取 flag。

2. 逻辑错误

  • 问题:条件判断、状态更新或流程控制错误,导致与设计意图不符的结果。

  • 详细说明

    • Move 依赖开发者正确实现逻辑,无内置保护机制。
    • 常见于条件遗漏、顺序错误或判断反转。
  • 示例

    public struct AccessControl has key {
        id: UID,
        is_allowed: bool,
        threshold: u64
    }
    
    public entry fun check_access(access: &mut AccessControl, score: u64) {
        if (score > 50) { // 应为 >= 50,但是设置为 >50
            access.is_allowed = true;
        }
    }
    
  • 边界值 score = 50 被意外排除.

  • 更复杂示例

    public entry fun update_state(state: &mut AccessControl, value: u64) {
      if (value < state.threshold) {
          state.is_allowed = false; // 可能应为 true
      }
    }
    
    • 若意图是“低于阈值激活”,条件与赋值不符。
  • 影响

    • 可能允许未授权操作或阻止合法行为。

3. 权限控制不足

  • 问题:函数未限制调用者身份,允许任何人执行敏感操作。
  • 详细说明
    • Move 的 public entry fun 默认对所有地址开放,需手动验证 tx_context::sender.
    • Sui 的共享对象尤其需注意权限。
  • 示例
    public entry fun reset_counter(counter: &mut Counter) { // 未验证调用者
      counter.count = 0; 
    }
    
    • 任何人都可重置计数器。
  • 更实际示例
    public struct SuiPool has key {
        id: UID,
        suiBalance: Balance<0x2::sui::SUI>,
    }
    
    public entry fun withdraw_commision(
        suipool: &mut SuiPool,
        amount: u64,
        to: address,
        ctx: &mut TxContext,
    ) {
        assert!(suipool.suiBalance.value() > amount, 1);
        let coin_balance = suipool.suiBalance.split(amount);
        let coin = from_balance(coin_balance, ctx);
        public_transfer(coin, to);
    }
    
    • 非管理者可提取余额。
  • 影响
    • 未授权用户可能破坏合约状态或窃取资源。

4. 整数溢出/下溢

  • 问题:在 Sui 中,Move 的整数运算(如 u64 的加法、减法)默认启用溢出检查,溢出或下溢会导致交易失败.
  • 示例
    module counter::counter{
        use sui::event;
    
        public struct Counter has key {
            id: UID,
            count: u64,
        }
    
        public struct CounterEmit has copy, drop{
            count: u64,
        }
    
        fun init(ctx: &mut TxContext){
            transfer::share_object(Counter { id:object::new(ctx), count: 0 });
        }
    
        public entry fun add(counter: &mut Counter,amount: u64){
            counter.count = counter.count + amount;
            event::emit(CounterEmit { count: counter.count })
        }
    
        public entry fun reduce(counter: &mut Counter,amount: u64){
            counter.count = counter.count - amount;
            event::emit(CounterEmit { count: counter.count })
        }
    }
    
  • 影响
    • 若 counter + amount > 2^64 - 1 或 counter - amount < 0 会抛出 MovePrimitiveRuntimeError,交易失败,无法继续执行。

5. 资源管理不当

  • 问题:资源未正确转移或销毁,导致编译错误。
  • 示例
    public entry fun create_coin(ctx: &mut TxContext) {
      let coin = Coin { id: object::new(ctx), value: 100 };
    }
    
  • 影响
    • 编译错误。

实践:识别与修复简单漏洞

题目描述

在本实践环节,你将审计一个简单的 Sui Move 合约 chapter_2::simple_challenge,目标是识别其中的漏洞并提出修复建议。合约实现了一个简单的“计数挑战”:用户可以通过提交计数(increment_count)来增加计数器,达到目标值后领取奖励(claim_reward)。奖励是共享的,任何人都可以领取。然而,合约存在一些隐藏漏洞,其中一个可能导致运行时报错,你需要找到这些漏洞,分析其影响,并提出修复建议。

示例代码

以下是 chapter_2::simple_challenge 模块的代码:

move

module chapter_2::simple_challenge {
    use sui::object::{Self, UID};
    use sui::transfer;
    use sui::tx_context::{Self, TxContext};
    use sui::event;

    public struct Challenge has key {
        id: UID,
        owner: address,
        count: u64,
        target_count: u64,
        reward: u64,
        total_rewards_claimed: u64,
        total_attempts: u64,
    }

    public struct RewardEvent has copy, drop {
        reward: u64,
    }

    fun init(ctx: &mut TxContext) {
        let challenge = Challenge {
            id: object::new(ctx),
            owner: tx_context::sender(ctx),
            count: 0,
            target_count: 10,
            reward: 1000,
            total_rewards_claimed: 0,
            total_attempts: 0,
        };
        transfer::share_object(challenge);
    }

    public entry fun increment_count(challenge: &mut Challenge) {
        challenge.total_attempts = challenge.total_attempts + 1;
        challenge.count = challenge.count + 1;
    }

    public entry fun claim_reward(challenge: &mut Challenge, ctx: &mut TxContext) {
        if (challenge.count >= challenge.target_count) {
            challenge.total_rewards_claimed = challenge.total_rewards_claimed + challenge.reward;
            event::emit(RewardEvent { reward: challenge.reward });
            challenge.count = 0;
        };
    }
}

Task2

任务代码

以下是待分析的 Move 合约,完整代码请查看 挑战合约

PackageID: 0xd26a14084af49f68d4612ef0815518c251f7a0459eaf2cbcb2757efafad442c5 
Challenge ObjectID: 0x22c1330b43313cee7f0ca0ce36965343c1ae40577d80a4ffa8ab12986f50dea1
module task2::task2 {
    use sui::event;
    use sui::random::{Random, generate_u64, new_generator};
    use sui::clock::{Self, Clock};
    use std::hash;
    use std::string::String;

    const E_WRONG_STAGE: u64 = 1;
    const E_COOLDOWN: u64 = 2;
    const E_MAX_ATTEMPTS_EXCEEDED: u64 = 3;

    public struct Challenge has key {
        id: UID,
        owner: address,
        secret_hash: vector<u8>,
        attempts: u64,
        max_attempts: u64,
        last_attempt_time: u64,
        is_solved: bool,
        stage: u64,
        target_score: u64,
        current_score: u64,
        bonus_multiplier: u64,
        guess_round: u64,
        round_hash: vector<u8>,
        seed: u64,
    }

    public struct FlagEvent has copy, drop {
        flag: vector<u8>,
        github_id: String
    }

    public struct ScoreEvent has copy, drop {
        score: u64,
    }

    fun init(ctx: &mut TxContext) {
        let secret = b"LetsMoveCTF";
        let secret_hash = hash::sha3_256(secret);
        let challenge = Challenge {
            id: object::new(ctx),
            owner: tx_context::sender(ctx),
            secret_hash: secret_hash,
            attempts: 0,
            max_attempts: 50,
            last_attempt_time: 0,
            is_solved: false,
            stage: 1,
            target_score: 100,
            current_score: 0,
            bonus_multiplier: 0,
            guess_round: 1,
            round_hash: secret_hash,
            seed: 0,
        };
        transfer::share_object(challenge);
    }

    public entry fun submit_score(challenge: &mut Challenge, score: u64, clock: &Clock) {
        assert!(challenge.stage == 1, E_WRONG_STAGE);
        let current_time = clock::timestamp_ms(clock);
        assert!(current_time >= challenge.last_attempt_time + 5000, E_COOLDOWN);
        challenge.attempts = challenge.attempts + 1;
        assert!(challenge.attempts < challenge.max_attempts, E_MAX_ATTEMPTS_EXCEEDED);
        let time_factor = (current_time - challenge.last_attempt_time) / 1000;
        let attempt_penalty = challenge.attempts * 2;
        let adjusted_score = if (score > attempt_penalty) {
            score - attempt_penalty
        } else {
            0
        };
        let final_score = adjusted_score * challenge.bonus_multiplier + time_factor;
        challenge.current_score = challenge.current_score + final_score;
        challenge.last_attempt_time = current_time;
        event::emit(ScoreEvent { score: challenge.current_score });
        if (challenge.current_score >= challenge.target_score) {
            challenge.stage = 2;
            challenge.attempts = 0;
        };
    }

    #[allow(lint(public_random))]
    public entry fun submit_guess(challenge: &mut Challenge, randomseed: &Random, guess: vector<u8>, clock: &Clock, ctx: &mut TxContext) {
        assert!(challenge.stage == 2, E_WRONG_STAGE);
        let mut random_gen = new_generator(randomseed, ctx);
        let seed = generate_u64(&mut random_gen);
        let current_time = clock::timestamp_ms(clock);
        assert!(current_time >= challenge.last_attempt_time + 5000, E_COOLDOWN);
        assert!(challenge.attempts < challenge.max_attempts, E_MAX_ATTEMPTS_EXCEEDED);
        challenge.attempts = challenge.attempts + 1;
        let mut guess_data = guess;
        vector::append(&mut guess_data, to_bytes(current_time));
        vector::append(&mut guess_data, to_bytes(challenge.attempts));
        let random = hash::sha3_256(guess_data);
        let prefix_length = challenge.guess_round * 2;
        if (compare_hash_prefix(&random, &challenge.round_hash, prefix_length)) {
            challenge.guess_round = challenge.guess_round + 1;
            let mut new_hash_data = random;
            vector::append(&mut new_hash_data, to_bytes(challenge.seed + challenge.guess_round));
            challenge.round_hash = hash::sha3_256(new_hash_data);
            challenge.seed = seed;
            if (challenge.guess_round > 3) {
                challenge.is_solved = true;
                challenge.stage = 3;
                challenge.guess_round = 1;
                challenge.attempts = 0;
            };
        };
        challenge.last_attempt_time = current_time;
    }

    public entry fun get_flag(challenge: &mut Challenge,github_id: String, _: &mut TxContext) {
        assert!(challenge.stage == 3 && challenge.is_solved, E_WRONG_STAGE);
        reset_challenge(challenge);
        event::emit(FlagEvent { flag: b"flag{LetsMoveCTF_chapter_2}" ,github_id});
    }

    public fun reset_challenge(challenge: &mut Challenge) {
        challenge.attempts = 0;
        challenge.last_attempt_time = 0;
        challenge.is_solved = false;
        challenge.stage = 1;
        challenge.current_score = 0;
        challenge.bonus_multiplier = 1;
        challenge.guess_round = 1;
        challenge.round_hash = challenge.secret_hash;
    }

    fun to_bytes(value: u64): vector<u8> {
        let mut bytes = vector::empty<u8>();
        let mut i = 0;
        while (i < 8) {
            vector::push_back(&mut bytes, ((value >> (i * 8)) & 0xFF as u8));
            i = i + 1;
        };
        bytes
    }

    fun compare_hash_prefix(hash1: &vector<u8>, hash2: &vector<u8>, n: u64): bool {
        if (vector::length(hash1) < n || vector::length(hash2) < n) {
            return false
        };
        let mut i = 0;
        while (i < n) {
            if (*vector::borrow(hash1, i) != *vector::borrow(hash2, i)) {
                return false
            };
            i = i + 1;
        };
        true
    }
}