前言
课程目标与概述
课程目标
欢迎参加 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 中的应用,跳过语言基础教学。
工具安装
以下是你需要安装的工具,确保在第一节课前配置好开发环境:
-
Sui CLI:
- 用于编译、运行和调试 Move 代码。
- 安装步骤:
- Sui:参考 Sui CLI 安装指南。
- 验证:运行
sui -V检查安装成功。
-
VS Code + Move 插件:
-
提供代码高亮和语法检查。
-
安装步骤:
- 下载 VS Code。
- 在扩展市场搜索
Move或Sui Move,安装相关插件。

Move和Move 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 或利用漏洞提取数据。
- 真实案例:
- 在 CTFtime 上,HackTheBox Cyber Apocalypse CTF 2024 Blockchain Challenges 包含区块链题目,要求选手审计智能合约。
- Sui 社区在 2024 年推出了 MoveCTF 2024,其中包括基于 Move 的解题挑战。
- 现状:当前 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 或利用漏洞提取数据。
- 真实案例:
- 在 CTFtime 上,HackTheBox Cyber Apocalypse CTF 2024 Blockchain Challenges 包含区块链题目,要求选手审计智能合约。
- Sui 社区在 2024 年推出了 MoveCTF 2024,其中包括基于 Move 的解题挑战。
- 现状:当前 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声明,如key、store)。 - 线性类型:资源不可复制(
copy)或丢弃(drop),必须显式转移或销毁。 - 示例:
public struct Coin has key { id: UID, value: u64 }
- Move 的核心特性,使用
- 所有权管理:
- 创建资源后,可通过
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 的共享状态。
- 对象(Object):通过
3. 事件与输出
- 事件(Event):
- 通过
event::emit输出日志,用于记录状态变化或 CTF 中的 flag 输出。 - 需定义事件结构体,具备
copy和drop能力。 - 示例:
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. 变量与类型
- 基本类型:
u8、u64、u128:无符号整数。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声明,如key、store)。 - 线性类型:资源不可复制(
copy)或丢弃(drop),必须显式转移或销毁。 - 示例:
public struct Coin has key { id: UID, value: u64 }
- Move 的核心特性,使用
- 所有权管理:
- 创建资源后,可通过
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 的共享状态。
- 对象(Object):通过
3. 事件与输出
- 事件(Event):
- 通过
event::emit输出日志,用于记录状态变化或 CTF 中的 flag 输出。 - 需定义事件结构体,具备
copy和drop能力。 - 示例:
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. 变量与类型
- 基本类型:
u8、u64、u128:无符号整数。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
}
}