├── .gitignore ├── README.md ├── book.toml ├── move ctfer └── 001 │ └── README.md ├── package.json ├── src ├── 01_check_in │ └── check_in │ │ ├── Move.lock │ │ ├── Move.toml │ │ ├── sources │ │ └── check_in.move │ │ └── tests │ │ └── check_in_tests.move ├── 02_lets_move │ └── lets_move │ │ ├── Move.lock │ │ ├── Move.toml │ │ ├── sources │ │ └── lets_move.move │ │ └── tests │ │ └── lets_move_tests.move ├── SUMMARY.md ├── chapter_1.md ├── ctfbook │ ├── appendix │ │ ├── faq.md │ │ └── resources.md │ ├── chapter_1 │ │ ├── challenge │ │ │ ├── chapter_1 │ │ │ │ ├── .gitignore │ │ │ │ ├── Move.lock │ │ │ │ ├── Move.toml │ │ │ │ ├── sources │ │ │ │ │ └── chapter_1.move │ │ │ │ └── tests │ │ │ │ │ └── chapter_1_tests.move │ │ │ └── solve_chapter_1 │ │ │ │ ├── .gitignore │ │ │ │ ├── Move.lock │ │ │ │ ├── Move.toml │ │ │ │ ├── sources │ │ │ │ └── solve_chapter_1.move │ │ │ │ └── tests │ │ │ │ └── solve_chapter_1_tests.move │ │ ├── intro.md │ │ ├── move_applications.md │ │ ├── practice.md │ │ ├── task1.md │ │ └── task1 │ │ │ ├── .gitignore │ │ │ ├── Move.lock │ │ │ ├── Move.toml │ │ │ ├── sources │ │ │ └── task1.move │ │ │ └── tests │ │ │ └── task1_tests.move │ ├── chapter_2 │ │ ├── code_reading.md │ │ ├── intro.md │ │ ├── practice.md │ │ ├── task2.md │ │ └── task2 │ │ │ ├── .gitignore │ │ │ ├── Move.lock │ │ │ ├── Move.toml │ │ │ ├── sources │ │ │ └── task2.move │ │ │ └── tests │ │ │ └── task2_tests.move │ ├── chapter_3 │ │ ├── intro.md │ │ ├── practice.md │ │ └── type_safety.md │ ├── chapter_4 │ │ ├── global_storage.md │ │ ├── intro.md │ │ └── practice.md │ ├── chapter_5 │ │ ├── access_control.md │ │ ├── intro.md │ │ └── practice.md │ ├── chapter_6 │ │ ├── intro.md │ │ ├── practice.md │ │ └── state_management.md │ ├── chapter_7 │ │ ├── cross_module.md │ │ ├── intro.md │ │ └── practice.md │ ├── chapter_8 │ │ ├── intro.md │ │ ├── practice.md │ │ └── vulnerability_analysis.md │ └── preface │ │ ├── goals.md │ │ ├── intro.md │ │ ├── prerequisites.assets │ │ └── image-20250325171129301.png │ │ └── prerequisites.md └── week1 │ ├── .gitignore │ ├── Move.toml │ ├── sources │ └── week1.move │ └── tests │ └── week1_tests.move ├── vercel.json └── writeup └── justctf2024 ├── README.md ├── db ├── .dockerignore ├── Dockerfile ├── db_docker.tar.gz ├── docker-compose.yml ├── solve.move └── sources │ ├── framework-solve │ ├── Cargo.lock │ ├── Cargo.toml │ ├── dependency │ │ ├── Move.toml │ │ └── sources │ │ │ └── dark_brotterhood.move │ ├── solve │ │ ├── Move.toml │ │ └── sources │ │ │ └── solve.move │ └── src │ │ └── main.rs │ ├── framework │ ├── Cargo.lock │ ├── Cargo.toml │ ├── chall │ │ ├── Move.lock │ │ ├── Move.toml │ │ └── sources │ │ │ └── dark_brotterhood.move │ ├── run.sh │ ├── rust-toolchain.toml │ └── src │ │ └── main.rs │ ├── run_client.sh │ └── run_server.sh ├── tos ├── .dockerignore ├── Dockerfile ├── docker-compose.yml ├── solve.move ├── sources │ ├── framework-solve │ │ ├── Cargo.lock │ │ ├── Cargo.toml │ │ ├── dependency │ │ │ ├── Move.toml │ │ │ └── sources │ │ │ │ └── the_otter_scrolls.move │ │ ├── solve │ │ │ ├── Move.toml │ │ │ └── sources │ │ │ │ └── solve.move │ │ └── src │ │ │ └── main.rs │ ├── framework │ │ ├── Cargo.lock │ │ ├── Cargo.toml │ │ ├── chall │ │ │ ├── Move.lock │ │ │ ├── Move.toml │ │ │ └── sources │ │ │ │ └── the_otter_scrolls.move │ │ ├── run.sh │ │ ├── rust-toolchain.toml │ │ └── src │ │ │ └── main.rs │ ├── run_client.sh │ └── run_server.sh └── tos_docker.tar.gz └── woo ├── .dockerignore ├── Dockerfile ├── docker-compose.yml ├── solve.move ├── sources ├── framework-solve │ ├── Cargo.lock │ ├── Cargo.toml │ ├── dependency │ │ ├── Move.toml │ │ └── sources │ │ │ └── quest.move │ ├── solve │ │ ├── Move.toml │ │ └── sources │ │ │ └── solve.move │ └── src │ │ └── main.rs ├── framework │ ├── Cargo.lock │ ├── Cargo.toml │ ├── chall │ │ ├── Move.lock │ │ ├── Move.toml │ │ └── sources │ │ │ └── quest.move │ ├── run.sh │ ├── rust-toolchain.toml │ └── src │ │ └── main.rs ├── run_client.sh └── run_server.sh └── woo_docker.tar.gz /.gitignore: -------------------------------------------------------------------------------- 1 | book 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # letsmove-ctf 2 | 3 | Let's Move CTF 是一个专注于 Move 智能合约安全的 CTF(Capture The Flag)学习项目,通过实践性的挑战来帮助开发者深入理解 Move 语言的安全特性和常见漏洞。 4 | 5 | ## 学习任务 6 | 7 | 1. **CTF简介与Move应用** 8 | - CTF比赛类型与Move应用场景 9 | - 签到挑战实践 10 | - 基础任务练习 11 | - [Task 1: 签到挑战](src/01_check_in/check_in/) 12 | 13 | 2. **基础代码审计** 14 | - Move代码阅读技巧 15 | - 常见问题识别 16 | - 漏洞分析实践 17 | - [Task 2: Let's Move](src/02_lets_move/lets_move/) 18 | 19 | ## 学习资源 20 | 21 | ### 视频教程 22 | 23 | | 期数 | 视频 | 源码 | 24 | | --- | --- | --- | 25 | | 2024年最新课程 | [B站合集](https://space.bilibili.com/29742457/lists/4655046?type=season) | [课程源码](https://github.com/move-cn/letsmove) | 26 | 27 | ### 书籍 28 | 29 | | 书名 | 中文 | 英文 | 30 | | --- | --- | --- | 31 | | Move Book | [中文](https://move.sui-book.com/index.html) | [英文](https://move-book.com/) | 32 | | Move Reference | [中文](https://reference.sui-book.com/index.html) | [英文](https://move-book.com/reference/) | 33 | | Move 导论 | [中文](https://intro-zh.sui-book.com/) | [Sui Move Intro Course](https://intro.sui-book.com/) | 34 | | Let's Move Sui | [中文](https://movesui.sui-book.com/) | [let's move](https://letsmovesui.com/) | 35 | | Sui Book | [中文](https://sui-book.com) | | 36 | | Sui Move by Example | [中文](https://examples.sui-book.com/) | [Sui Move by Example](https://examples.sui.io/) | 37 | | 轻松入门Move | [轻松入门Move](https://easy.sui-book.com/) | | 38 | | Move Master | [中文](https://master.sui-book.com/) | [move master](https://metaschool.so/sui) | 39 | | move castle | [中文](https://movecastle.sui-book.com/) | [move castle](https://learn.movecastle.info/courses/move-on-sui) | 40 | 41 | ## 社区 & 问答 42 | 43 | - [Move 中文社区](https://t.me/move_cn) 44 | - [Sui 中文社区](https://t.me/sui_cn) 45 | 46 | ## 项目概述 47 | 48 | 本项目采用循序渐进的方式,从基础概念到高级漏洞利用,通过实际的 CTF 挑战来学习 Move 智能合约安全。每个章节都包含理论知识和实践练习,帮助学习者掌握 Move 安全审计的核心技能。 49 | 50 | ## 目录结构 51 | 52 | ``` 53 | . 54 | ├── src/ # 源代码目录 55 | │ ├── ctfbook/ # CTF 教程内容 56 | │ ├── chapter_1/ # 第1章:CTF简介与Move应用 57 | │ ├── chapter_2/ # 第2章:基础代码审计 58 | │ └── SUMMARY.md # 目录导航 59 | ├── book.toml # 项目配置文件 60 | └── README.md # 项目说明文档 61 | ``` 62 | 63 | ## 学习路线 64 | 65 | 1. **CTF简介与Move应用** 66 | - CTF比赛类型与Move应用场景 67 | - 签到挑战实践 68 | - 基础任务练习 69 | - [Task 1: 签到挑战](src/01_check_in/check_in/) 70 | 71 | 2. **基础代码审计** 72 | - Move代码阅读技巧 73 | - 常见问题识别 74 | - 漏洞分析实践 75 | - [Task 2: Let's Move](src/02_lets_move/lets_move/) 76 | 77 | 78 | -------------------------------------------------------------------------------- /book.toml: -------------------------------------------------------------------------------- 1 | [book] 2 | authors = ["wsc"] 3 | language = "en" 4 | multilingual = false 5 | src = "src" 6 | title = "lets move ctf" 7 | -------------------------------------------------------------------------------- /move ctfer/001/README.md: -------------------------------------------------------------------------------- 1 | ## 参与方式 2 | 3 | 在move ctfer文件夹下复制001文件夹并更名为自己的github id 4 | 5 | ## 个人简介 6 | 7 | 工作经验: x年 8 | 技术栈: Rust C++ 9 | 重要提示 请认真写自己的简介 10 | 11 | 多年web2开发经验,对Move特别感兴趣,想通过Move入门区块链 12 | 联系方式: tg: xxx 13 | 14 | ## 任务 15 | 16 | ### 01 Move CTF Check In 17 | - [ ] CLI call 截图: 截图 18 | - [ ] flag hash: 19 | 20 | ### 02 Move CTF Lets Move 21 | - [ ] proof: 22 | - [ ] flag hash: -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mdbook-vercel", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "install-mdbook": "curl -Lo mdbook.tar.gz https://github.com/rust-lang/mdBook/releases/download/v0.4.32/mdbook-v0.4.32-x86_64-unknown-linux-musl.tar.gz && tar -xvzf mdbook.tar.gz && rm mdbook.tar.gz", 6 | "build": "npm run install-mdbook && ./mdbook build" 7 | } 8 | } -------------------------------------------------------------------------------- /src/01_check_in/check_in/Move.lock: -------------------------------------------------------------------------------- 1 | # @generated by Move, please check-in and do not edit manually. 2 | 3 | [move] 4 | version = 2 5 | manifest_digest = "60ED2B08562863E03B32E589CB9D813832DBE08E722E777386028AACAA9C2B4A" 6 | deps_digest = "F8BBB0CCB2491CA29A3DF03D6F92277A4F3574266507ACD77214D37ECA3F3082" 7 | dependencies = [ 8 | { name = "Sui" }, 9 | ] 10 | 11 | [[move.package]] 12 | name = "MoveStdlib" 13 | source = { git = "https://gitee.com/MystenLabs/sui.git", rev = "framework/testnet", subdir = "crates/sui-framework/packages/move-stdlib" } 14 | 15 | [[move.package]] 16 | name = "Sui" 17 | source = { git = "https://gitee.com/MystenLabs/sui.git", rev = "framework/testnet", subdir = "crates/sui-framework/packages/sui-framework" } 18 | 19 | dependencies = [ 20 | { name = "MoveStdlib" }, 21 | ] 22 | 23 | [move.toolchain-version] 24 | compiler-version = "1.27.2" 25 | edition = "2024.beta" 26 | flavor = "sui" 27 | 28 | [env] 29 | 30 | [env.testnet] 31 | chain-id = "4c78adac" 32 | original-published-id = "0x914099b4d1b4f5513acc8aaa4fdc1f67578522b81d818f61bae527d590c6d87d" 33 | latest-published-id = "0x914099b4d1b4f5513acc8aaa4fdc1f67578522b81d818f61bae527d590c6d87d" 34 | published-version = "1" 35 | -------------------------------------------------------------------------------- /src/01_check_in/check_in/Move.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "check_in" 3 | edition = "2024.beta" # edition = "legacy" to use legacy (pre-2024) Move 4 | # license = "" # e.g., "MIT", "GPL", "Apache 2.0" 5 | # authors = ["..."] # e.g., ["Joe Smith (joesmith@noemail.com)", "John Snow (johnsnow@noemail.com)"] 6 | 7 | [dependencies] 8 | Sui = { git = "https://gitee.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "framework/testnet" } 9 | 10 | # For remote import, use the `{ git = "...", subdir = "...", rev = "..." }`. 11 | # Revision can be a branch, a tag, and a commit hash. 12 | # MyRemotePackage = { git = "https://some.remote/host.git", subdir = "remote/path", rev = "main" } 13 | 14 | # For local dependencies use `local = path`. Path is relative to the package root 15 | # Local = { local = "../path/to" } 16 | 17 | # To resolve a version conflict and force a specific version for dependency 18 | # override use `override = true` 19 | # Override = { local = "../conflicting/version", override = true } 20 | 21 | [addresses] 22 | check_in = "0x0" 23 | 24 | # Named addresses will be accessible in Move as `@name`. They're also exported: 25 | # for example, `std = "0x1"` is exported by the Standard Library. 26 | # alice = "0xA11CE" 27 | 28 | [dev-dependencies] 29 | # The dev-dependencies section allows overriding dependencies for `--test` and 30 | # `--dev` modes. You can introduce test-only dependencies here. 31 | # Local = { local = "../path/to/dev-build" } 32 | 33 | [dev-addresses] 34 | # The dev-addresses section allows overwriting named addresses for the `--test` 35 | # and `--dev` modes. 36 | # alice = "0xB0B" 37 | 38 | -------------------------------------------------------------------------------- /src/01_check_in/check_in/sources/check_in.move: -------------------------------------------------------------------------------- 1 | module check_in::check_in { 2 | use std::ascii::{String, string}; 3 | use std::bcs; 4 | use std::hash::sha3_256; 5 | use std::vector; 6 | use sui::event; 7 | use sui::object; 8 | use sui::random; 9 | use sui::random::Random; 10 | use sui::transfer::share_object; 11 | use sui::tx_context::{Self, TxContext}; 12 | 13 | const ESTRING: u64 = 0; 14 | 15 | public struct Flag has copy, drop { 16 | sender: address, 17 | flag: bool, 18 | ture_num: u64, 19 | github_id: String 20 | } 21 | 22 | public struct FlagString has key { 23 | id: UID, 24 | str: String, 25 | ture_num: u64 26 | } 27 | 28 | fun init(ctx: &mut TxContext) { 29 | let flag_str = FlagString { 30 | id: object::new(ctx), 31 | str: string(b"LetsMoveCTF"), 32 | ture_num: 0 33 | }; 34 | share_object(flag_str); 35 | } 36 | 37 | 38 | entry fun get_flag( 39 | flag: vector, 40 | github_id: String, 41 | flag_str: &mut FlagString, 42 | rand: &Random, 43 | ctx: &mut TxContext 44 | ) { 45 | let mut bcs_flag = bcs::to_bytes(&flag_str.str); 46 | vector::append(&mut bcs_flag, *github_id.as_bytes()); 47 | 48 | assert!(flag == sha3_256(bcs_flag), ESTRING); 49 | 50 | flag_str.str = getRandomString(rand, ctx); 51 | 52 | flag_str.ture_num = flag_str.ture_num + 1; 53 | 54 | event::emit(Flag { 55 | sender: tx_context::sender(ctx), 56 | flag: true, 57 | ture_num: flag_str.ture_num, 58 | github_id 59 | }); 60 | } 61 | 62 | 63 | fun getRandomString(rand: &Random, ctx: &mut TxContext): String { 64 | let mut gen = random::new_generator(rand, ctx); 65 | 66 | let mut str_len = random::generate_u8_in_range(&mut gen, 4, 30); 67 | 68 | let mut rand: vector = b""; 69 | while (str_len != 0) { 70 | let rand_num = random::generate_u8_in_range(&mut gen, 34, 126); 71 | vector::push_back(&mut rand, rand_num); 72 | str_len = str_len - 1; 73 | }; 74 | 75 | string(rand) 76 | } 77 | } 78 | 79 | -------------------------------------------------------------------------------- /src/01_check_in/check_in/tests/check_in_tests.move: -------------------------------------------------------------------------------- 1 | /* 2 | #[test_only] 3 | module check_in::check_in_tests { 4 | // uncomment this line to import the module 5 | // use check_in::check_in; 6 | 7 | const ENotImplemented: u64 = 0; 8 | 9 | #[test] 10 | fun test_check_in() { 11 | // pass 12 | } 13 | 14 | #[test, expected_failure(abort_code = ::check_in::check_in_tests::ENotImplemented)] 15 | fun test_check_in_fail() { 16 | abort ENotImplemented 17 | } 18 | } 19 | */ 20 | -------------------------------------------------------------------------------- /src/02_lets_move/lets_move/Move.lock: -------------------------------------------------------------------------------- 1 | # @generated by Move, please check-in and do not edit manually. 2 | 3 | [move] 4 | version = 2 5 | manifest_digest = "786B91A5B97E30CFE4109DB806C7FDAA208AD34906C77AB899770D4D0AEB5DB1" 6 | deps_digest = "F8BBB0CCB2491CA29A3DF03D6F92277A4F3574266507ACD77214D37ECA3F3082" 7 | dependencies = [ 8 | { name = "Sui" }, 9 | ] 10 | 11 | [[move.package]] 12 | name = "MoveStdlib" 13 | source = { git = "https://github.com/MystenLabs/sui.git", rev = "framework/testnet", subdir = "crates/sui-framework/packages/move-stdlib" } 14 | 15 | [[move.package]] 16 | name = "Sui" 17 | source = { git = "https://github.com/MystenLabs/sui.git", rev = "framework/testnet", subdir = "crates/sui-framework/packages/sui-framework" } 18 | 19 | dependencies = [ 20 | { name = "MoveStdlib" }, 21 | ] 22 | 23 | [move.toolchain-version] 24 | compiler-version = "1.26.2" 25 | edition = "2024.beta" 26 | flavor = "sui" 27 | 28 | [env] 29 | 30 | [env.testnet] 31 | chain-id = "4c78adac" 32 | original-published-id = "0x097a3833b6b5c62ca6ad10f0509dffdadff7ce31e1d86e63e884a14860cedc0f" 33 | latest-published-id = "0x097a3833b6b5c62ca6ad10f0509dffdadff7ce31e1d86e63e884a14860cedc0f" 34 | published-version = "1" 35 | -------------------------------------------------------------------------------- /src/02_lets_move/lets_move/Move.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "lets_move" 3 | edition = "2024.beta" # edition = "legacy" to use legacy (pre-2024) Move 4 | # license = "" # e.g., "MIT", "GPL", "Apache 2.0" 5 | # authors = ["..."] # e.g., ["Joe Smith (joesmith@noemail.com)", "John Snow (johnsnow@noemail.com)"] 6 | 7 | [dependencies] 8 | Sui = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "framework/testnet" } 9 | 10 | # For remote import, use the `{ git = "...", subdir = "...", rev = "..." }`. 11 | # Revision can be a branch, a tag, and a commit hash. 12 | # MyRemotePackage = { git = "https://some.remote/host.git", subdir = "remote/path", rev = "main" } 13 | 14 | # For local dependencies use `local = path`. Path is relative to the package root 15 | # Local = { local = "../path/to" } 16 | 17 | # To resolve a version conflict and force a specific version for dependency 18 | # override use `override = true` 19 | # Override = { local = "../conflicting/version", override = true } 20 | 21 | [addresses] 22 | lets_move = "0x0" 23 | 24 | # Named addresses will be accessible in Move as `@name`. They're also exported: 25 | # for example, `std = "0x1"` is exported by the Standard Library. 26 | # alice = "0xA11CE" 27 | 28 | [dev-dependencies] 29 | # The dev-dependencies section allows overriding dependencies for `--test` and 30 | # `--dev` modes. You can introduce test-only dependencies here. 31 | # Local = { local = "../path/to/dev-build" } 32 | 33 | [dev-addresses] 34 | # The dev-addresses section allows overwriting named addresses for the `--test` 35 | # and `--dev` modes. 36 | # alice = "0xB0B" 37 | 38 | -------------------------------------------------------------------------------- /src/02_lets_move/lets_move/sources/lets_move.move: -------------------------------------------------------------------------------- 1 | module lets_move::lets_move { 2 | use std::ascii::{String, string}; 3 | use std::hash; 4 | use sui::event; 5 | use sui::bcs; 6 | use sui::random; 7 | use sui::random::Random; 8 | use sui::transfer::share_object; 9 | 10 | const EPROOF: u64 = 0; 11 | 12 | public struct Flag has copy, drop { 13 | sender: address, 14 | flag: bool, 15 | ture_num: u64, 16 | github_id: String 17 | } 18 | 19 | public struct Challenge has key { 20 | id: UID, 21 | str: String, 22 | difficulity: u64, 23 | ture_num: u64 24 | } 25 | 26 | fun init(ctx: &mut TxContext) { 27 | let flag_str = Challenge { 28 | id: object::new(ctx), 29 | str: string(b"LetsMoveCTF"), 30 | difficulity: 3, 31 | ture_num: 0, 32 | }; 33 | share_object(flag_str); 34 | } 35 | 36 | 37 | entry fun get_flag( 38 | proof: vector, 39 | github_id: String, 40 | challenge: &mut Challenge, 41 | rand: &Random, 42 | ctx: &mut TxContext 43 | ) { 44 | let mut full_proof: vector = vector::empty(); 45 | vector::append(&mut full_proof, proof); 46 | vector::append(&mut full_proof, tx_context::sender(ctx).to_bytes()); 47 | vector::append(&mut full_proof, bcs::to_bytes(challenge)); 48 | 49 | let hash: vector = hash::sha3_256(full_proof); 50 | 51 | let mut prefix_sum: u32 = 0; 52 | let mut i: u64 = 0; 53 | while (i < challenge.difficulity) { 54 | prefix_sum = prefix_sum + (*vector::borrow(&hash, i) as u32); 55 | i = i + 1; 56 | }; 57 | 58 | assert!(prefix_sum == 0, EPROOF); 59 | 60 | challenge.str = getRandomString(rand, ctx); 61 | challenge.ture_num = challenge.ture_num + 1; 62 | 63 | event::emit(Flag { 64 | sender: tx_context::sender(ctx), 65 | flag: true, 66 | ture_num: challenge.ture_num, 67 | github_id 68 | }); 69 | } 70 | 71 | 72 | fun getRandomString(rand: &Random, ctx: &mut TxContext): String { 73 | let mut gen = random::new_generator(rand, ctx); 74 | 75 | let mut str_len = random::generate_u8_in_range(&mut gen, 4, 30); 76 | 77 | let mut rand: vector = b""; 78 | while (str_len != 0) { 79 | let rand_num = random::generate_u8_in_range(&mut gen, 34, 126); 80 | vector::push_back(&mut rand, rand_num); 81 | str_len = str_len - 1; 82 | }; 83 | 84 | string(rand) 85 | } 86 | } 87 | 88 | -------------------------------------------------------------------------------- /src/02_lets_move/lets_move/tests/lets_move_tests.move: -------------------------------------------------------------------------------- 1 | /* 2 | #[test_only] 3 | module lets_move::lets_move_tests { 4 | // uncomment this line to import the module 5 | // use lets_move::lets_move; 6 | 7 | const ENotImplemented: u64 = 0; 8 | 9 | #[test] 10 | fun test_lets_move() { 11 | // pass 12 | } 13 | 14 | #[test, expected_failure(abort_code = ::lets_move::lets_move_tests::ENotImplemented)] 15 | fun test_lets_move_fail() { 16 | abort ENotImplemented 17 | } 18 | } 19 | */ 20 | -------------------------------------------------------------------------------- /src/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | - [前言](ctfbook/preface/intro.md) 4 | - [课程目标与概述](ctfbook/preface/goals.md) 5 | - [预备知识与工具安装](ctfbook/preface/prerequisites.md) 6 | - [第1节:CTF简介与Move应用](ctfbook/chapter_1/intro.md) 7 | - [CTF比赛类型与Move应用场景](ctfbook/chapter_1/move_applications.md) 8 | - [实践:签到挑战](ctfbook/chapter_1/practice.md) 9 | 10 | - [第2节:基础代码审计](ctfbook/chapter_2/intro.md) 11 | - [阅读Move代码与常见问题](ctfbook/chapter_2/code_reading.md) 12 | - [实践:识别简单漏洞](ctfbook/chapter_2/practice.md) 13 | 14 | - [第3节:泛型类型安全](ctfbook/chapter_3/intro.md) 15 | - [了解泛型类型安全](ctfbook/chapter_3/type_safety.md) 16 | - [实践:利用泛型漏洞伪造投票凭证](ctfbook/chapter_3/practice.md) 17 | - [第4节:资源管理与所有权](ctfbook/chapter_4/intro.md) 18 | - [了解资源管理问题](ctfbook/chapter_4/global_storage.md) 19 | - [实践:资源管理不当分析](ctfbook/chapter_4/practice.md) 20 | - [第5节:权限与访问控制](ctfbook/chapter_5/intro.md) 21 | - [权限控制与 TxContext 验证](ctfbook/chapter_5/access_control.md) 22 | - [实践:分析权限控制漏洞](ctfbook/chapter_5/practice.md) 23 | - [第6节:逻辑漏洞与状态管理](ctfbook/chapter_6/intro.md) 24 | - [了解逻辑漏洞与状态管理](ctfbook/chapter_6/state_management.md) 25 | - [实践:分析状态管理逻辑漏洞](ctfbook/chapter_6/practice.md) 26 | - [第7节:跨合约交互安全](ctfbook/chapter_7/intro.md) 27 | - [跨合约安全与溢出漏洞](ctfbook/chapter_7/cross_module.md) 28 | - [实践:利用漏洞造成溢出实现攻击](ctfbook/chapter_7/practice.md) 29 | - [第8节:综合CTF挑战](ctfbook/chapter_8/intro.md) 30 | - [综合漏洞分析与解题策略](ctfbook/chapter_8/vulnerability_analysis.md) 31 | - [实践:攻破综合漏洞](ctfbook/chapter_8/practice.md) 32 | -------------------------------------------------------------------------------- /src/chapter_1.md: -------------------------------------------------------------------------------- 1 | # Chapter 1 2 | -------------------------------------------------------------------------------- /src/ctfbook/appendix/faq.md: -------------------------------------------------------------------------------- 1 | # 常见问题解答 2 | -------------------------------------------------------------------------------- /src/ctfbook/appendix/resources.md: -------------------------------------------------------------------------------- 1 | # 附录 2 | -------------------------------------------------------------------------------- /src/ctfbook/chapter_1/challenge/chapter_1/.gitignore: -------------------------------------------------------------------------------- 1 | build/* 2 | -------------------------------------------------------------------------------- /src/ctfbook/chapter_1/challenge/chapter_1/Move.lock: -------------------------------------------------------------------------------- 1 | # @generated by Move, please check-in and do not edit manually. 2 | 3 | [move] 4 | version = 3 5 | manifest_digest = "CC0C38C35F7FCE14DAF394AE0DC084EC5BDD368B39D5943FD2A619C8D1854EEE" 6 | deps_digest = "397E6A9F7A624706DBDFEE056CE88391A15876868FD18A88504DA74EB458D697" 7 | dependencies = [ 8 | { id = "Bridge", name = "Bridge" }, 9 | { id = "DeepBook", name = "DeepBook" }, 10 | { id = "MoveStdlib", name = "MoveStdlib" }, 11 | { id = "Sui", name = "Sui" }, 12 | { id = "SuiSystem", name = "SuiSystem" }, 13 | ] 14 | 15 | [[move.package]] 16 | id = "Bridge" 17 | source = { git = "https://github.com/MystenLabs/sui.git", rev = "9c04e1840eb5", subdir = "crates/sui-framework/packages/bridge" } 18 | 19 | dependencies = [ 20 | { id = "MoveStdlib", name = "MoveStdlib" }, 21 | { id = "Sui", name = "Sui" }, 22 | { id = "SuiSystem", name = "SuiSystem" }, 23 | ] 24 | 25 | [[move.package]] 26 | id = "DeepBook" 27 | source = { git = "https://github.com/MystenLabs/sui.git", rev = "9c04e1840eb5", subdir = "crates/sui-framework/packages/deepbook" } 28 | 29 | dependencies = [ 30 | { id = "MoveStdlib", name = "MoveStdlib" }, 31 | { id = "Sui", name = "Sui" }, 32 | ] 33 | 34 | [[move.package]] 35 | id = "MoveStdlib" 36 | source = { git = "https://github.com/MystenLabs/sui.git", rev = "9c04e1840eb5", subdir = "crates/sui-framework/packages/move-stdlib" } 37 | 38 | [[move.package]] 39 | id = "Sui" 40 | source = { git = "https://github.com/MystenLabs/sui.git", rev = "9c04e1840eb5", subdir = "crates/sui-framework/packages/sui-framework" } 41 | 42 | dependencies = [ 43 | { id = "MoveStdlib", name = "MoveStdlib" }, 44 | ] 45 | 46 | [[move.package]] 47 | id = "SuiSystem" 48 | source = { git = "https://github.com/MystenLabs/sui.git", rev = "9c04e1840eb5", subdir = "crates/sui-framework/packages/sui-system" } 49 | 50 | dependencies = [ 51 | { id = "MoveStdlib", name = "MoveStdlib" }, 52 | { id = "Sui", name = "Sui" }, 53 | ] 54 | 55 | [move.toolchain-version] 56 | compiler-version = "1.45.2" 57 | edition = "2024.beta" 58 | flavor = "sui" 59 | 60 | [env] 61 | 62 | [env.testnet] 63 | chain-id = "4c78adac" 64 | original-published-id = "0x335297860a807291254b20f8a0dea30d72d5e17d2e6f8058e42d5b9c72f0f0ef" 65 | latest-published-id = "0x335297860a807291254b20f8a0dea30d72d5e17d2e6f8058e42d5b9c72f0f0ef" 66 | published-version = "1" 67 | -------------------------------------------------------------------------------- /src/ctfbook/chapter_1/challenge/chapter_1/Move.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "chapter_1" 3 | edition = "2024.beta" # edition = "legacy" to use legacy (pre-2024) Move 4 | # license = "" # e.g., "MIT", "GPL", "Apache 2.0" 5 | # authors = ["..."] # e.g., ["Joe Smith (joesmith@noemail.com)", "John Snow (johnsnow@noemail.com)"] 6 | 7 | [dependencies] 8 | 9 | # For remote import, use the `{ git = "...", subdir = "...", rev = "..." }`. 10 | # Revision can be a branch, a tag, and a commit hash. 11 | # MyRemotePackage = { git = "https://some.remote/host.git", subdir = "remote/path", rev = "main" } 12 | 13 | # For local dependencies use `local = path`. Path is relative to the package root 14 | # Local = { local = "../path/to" } 15 | 16 | # To resolve a version conflict and force a specific version for dependency 17 | # override use `override = true` 18 | # Override = { local = "../conflicting/version", override = true } 19 | 20 | [addresses] 21 | chapter_1 = "0x0" 22 | 23 | # Named addresses will be accessible in Move as `@name`. They're also exported: 24 | # for example, `std = "0x1"` is exported by the Standard Library. 25 | # alice = "0xA11CE" 26 | 27 | [dev-dependencies] 28 | # The dev-dependencies section allows overriding dependencies for `--test` and 29 | # `--dev` modes. You can introduce test-only dependencies here. 30 | # Local = { local = "../path/to/dev-build" } 31 | 32 | [dev-addresses] 33 | # The dev-addresses section allows overwriting named addresses for the `--test` 34 | # and `--dev` modes. 35 | # alice = "0xB0B" 36 | 37 | -------------------------------------------------------------------------------- /src/ctfbook/chapter_1/challenge/chapter_1/sources/chapter_1.move: -------------------------------------------------------------------------------- 1 | module chapter_1::check_in { 2 | use std::string::{Self, String}; 3 | use std::bcs; 4 | use std::hash::sha3_256; 5 | use sui::event; 6 | 7 | //testnet 8 | //PackageID:0x335297860a807291254b20f8a0dea30d72d5e17d2e6f8058e42d5b9c72f0f0ef 9 | public struct FlagEvent has copy, drop { 10 | sender: address, 11 | flag: String, 12 | success: bool 13 | } 14 | 15 | public entry fun get_flag( 16 | flag: vector, 17 | github_id: String, 18 | ctx: &mut TxContext 19 | ) { 20 | let mut bcs_input = bcs::to_bytes(&string::utf8(b"LetsMoveCTF")); 21 | vector::append(&mut bcs_input, *github_id.as_bytes()); 22 | let expected_hash = sha3_256(bcs_input); 23 | 24 | if (flag == expected_hash) { 25 | event::emit(FlagEvent { 26 | sender: tx_context::sender(ctx), 27 | flag: string::utf8(b"CTF{WelcomeToMoveCTF}"), 28 | success: true 29 | }); 30 | } else { 31 | event::emit(FlagEvent { 32 | sender: tx_context::sender(ctx), 33 | flag: string::utf8(b"Try again!"), 34 | success: false 35 | }); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/ctfbook/chapter_1/challenge/chapter_1/tests/chapter_1_tests.move: -------------------------------------------------------------------------------- 1 | /* 2 | #[test_only] 3 | module chapter_1::chapter_1_tests; 4 | // uncomment this line to import the module 5 | // use chapter_1::chapter_1; 6 | 7 | const ENotImplemented: u64 = 0; 8 | 9 | #[test] 10 | fun test_chapter_1() { 11 | // pass 12 | } 13 | 14 | #[test, expected_failure(abort_code = ::chapter_1::chapter_1_tests::ENotImplemented)] 15 | fun test_chapter_1_fail() { 16 | abort ENotImplemented 17 | } 18 | */ 19 | -------------------------------------------------------------------------------- /src/ctfbook/chapter_1/challenge/solve_chapter_1/.gitignore: -------------------------------------------------------------------------------- 1 | build/* 2 | -------------------------------------------------------------------------------- /src/ctfbook/chapter_1/challenge/solve_chapter_1/Move.lock: -------------------------------------------------------------------------------- 1 | # @generated by Move, please check-in and do not edit manually. 2 | 3 | [move] 4 | version = 3 5 | manifest_digest = "976FF0886C8BD08DD7CAAC740C08450BBB9C2E7511E88BCC2A8F19B29F8539B7" 6 | deps_digest = "52B406A7A21811BEF51751CF88DA0E76DAEFFEAC888D4F4060B1A72BBE7D8D35" 7 | dependencies = [ 8 | { id = "Bridge", name = "Bridge" }, 9 | { id = "DeepBook", name = "DeepBook" }, 10 | { id = "MoveStdlib", name = "MoveStdlib" }, 11 | { id = "Sui", name = "Sui" }, 12 | { id = "SuiSystem", name = "SuiSystem" }, 13 | { id = "chapter_1", name = "chapter_1" }, 14 | ] 15 | 16 | [[move.package]] 17 | id = "Bridge" 18 | source = { git = "https://github.com/MystenLabs/sui.git", rev = "9c04e1840eb5", subdir = "crates/sui-framework/packages/bridge" } 19 | 20 | dependencies = [ 21 | { id = "MoveStdlib", name = "MoveStdlib" }, 22 | { id = "Sui", name = "Sui" }, 23 | { id = "SuiSystem", name = "SuiSystem" }, 24 | ] 25 | 26 | [[move.package]] 27 | id = "DeepBook" 28 | source = { git = "https://github.com/MystenLabs/sui.git", rev = "9c04e1840eb5", subdir = "crates/sui-framework/packages/deepbook" } 29 | 30 | dependencies = [ 31 | { id = "MoveStdlib", name = "MoveStdlib" }, 32 | { id = "Sui", name = "Sui" }, 33 | ] 34 | 35 | [[move.package]] 36 | id = "MoveStdlib" 37 | source = { git = "https://github.com/MystenLabs/sui.git", rev = "9c04e1840eb5", subdir = "crates/sui-framework/packages/move-stdlib" } 38 | 39 | [[move.package]] 40 | id = "Sui" 41 | source = { git = "https://github.com/MystenLabs/sui.git", rev = "9c04e1840eb5", subdir = "crates/sui-framework/packages/sui-framework" } 42 | 43 | dependencies = [ 44 | { id = "MoveStdlib", name = "MoveStdlib" }, 45 | ] 46 | 47 | [[move.package]] 48 | id = "SuiSystem" 49 | source = { git = "https://github.com/MystenLabs/sui.git", rev = "9c04e1840eb5", subdir = "crates/sui-framework/packages/sui-system" } 50 | 51 | dependencies = [ 52 | { id = "MoveStdlib", name = "MoveStdlib" }, 53 | { id = "Sui", name = "Sui" }, 54 | ] 55 | 56 | [[move.package]] 57 | id = "chapter_1" 58 | source = { local = "../chapter_1" } 59 | 60 | dependencies = [ 61 | { id = "Bridge", name = "Bridge" }, 62 | { id = "DeepBook", name = "DeepBook" }, 63 | { id = "MoveStdlib", name = "MoveStdlib" }, 64 | { id = "Sui", name = "Sui" }, 65 | { id = "SuiSystem", name = "SuiSystem" }, 66 | ] 67 | 68 | [move.toolchain-version] 69 | compiler-version = "1.45.2" 70 | edition = "2024.beta" 71 | flavor = "sui" 72 | 73 | [env] 74 | 75 | [env.testnet] 76 | chain-id = "4c78adac" 77 | original-published-id = "0xef6b4139ec1b0fda23e06c4a30c9e91150b72c38530e4517152e591001c5c433" 78 | latest-published-id = "0xef6b4139ec1b0fda23e06c4a30c9e91150b72c38530e4517152e591001c5c433" 79 | published-version = "1" 80 | -------------------------------------------------------------------------------- /src/ctfbook/chapter_1/challenge/solve_chapter_1/Move.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "solve_chapter_1" 3 | edition = "2024.beta" # edition = "legacy" to use legacy (pre-2024) Move 4 | # license = "" # e.g., "MIT", "GPL", "Apache 2.0" 5 | # authors = ["..."] # e.g., ["Joe Smith (joesmith@noemail.com)", "John Snow (johnsnow@noemail.com)"] 6 | 7 | [dependencies] 8 | chapter_1 = { local = "../chapter_1" } 9 | 10 | # For remote import, use the `{ git = "...", subdir = "...", rev = "..." }`. 11 | # Revision can be a branch, a tag, and a commit hash. 12 | # MyRemotePackage = { git = "https://some.remote/host.git", subdir = "remote/path", rev = "main" } 13 | 14 | # For local dependencies use `local = path`. Path is relative to the package root 15 | # Local = { local = "../path/to" } 16 | 17 | # To resolve a version conflict and force a specific version for dependency 18 | # override use `override = true` 19 | # Override = { local = "../conflicting/version", override = true } 20 | 21 | [addresses] 22 | solve_chapter_1 = "0x0" 23 | 24 | # Named addresses will be accessible in Move as `@name`. They're also exported: 25 | # for example, `std = "0x1"` is exported by the Standard Library. 26 | # alice = "0xA11CE" 27 | 28 | [dev-dependencies] 29 | # The dev-dependencies section allows overriding dependencies for `--test` and 30 | # `--dev` modes. You can introduce test-only dependencies here. 31 | # Local = { local = "../path/to/dev-build" } 32 | 33 | [dev-addresses] 34 | # The dev-addresses section allows overwriting named addresses for the `--test` 35 | # and `--dev` modes. 36 | # alice = "0xB0B" 37 | 38 | -------------------------------------------------------------------------------- /src/ctfbook/chapter_1/challenge/solve_chapter_1/sources/solve_chapter_1.move: -------------------------------------------------------------------------------- 1 | module solve_chapter_1::solve{ 2 | use chapter_1::check_in::get_flag; 3 | use std::string; 4 | use std::bcs; 5 | use std::hash::sha3_256; 6 | 7 | //testnet 8 | //PackageID: 0xef6b4139ec1b0fda23e06c4a30c9e91150b72c38530e4517152e591001c5c433 9 | public entry fun solve_get_flag(ctx: &mut TxContext){ 10 | let github_id = string::utf8(b"hoh-zone"); 11 | let mut bcs_input = bcs::to_bytes(&string::utf8(b"LetsMoveCTF")); 12 | vector::append(&mut bcs_input, *github_id.as_bytes()); 13 | let flag_hash = sha3_256(bcs_input); 14 | get_flag(flag_hash, github_id, ctx); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/ctfbook/chapter_1/challenge/solve_chapter_1/tests/solve_chapter_1_tests.move: -------------------------------------------------------------------------------- 1 | /* 2 | #[test_only] 3 | module solve_chapter_1::solve_chapter_1_tests; 4 | // uncomment this line to import the module 5 | // use solve_chapter_1::solve_chapter_1; 6 | 7 | const ENotImplemented: u64 = 0; 8 | 9 | #[test] 10 | fun test_solve_chapter_1() { 11 | // pass 12 | } 13 | 14 | #[test, expected_failure(abort_code = ::solve_chapter_1::solve_chapter_1_tests::ENotImplemented)] 15 | fun test_solve_chapter_1_fail() { 16 | abort ENotImplemented 17 | } 18 | */ 19 | -------------------------------------------------------------------------------- /src/ctfbook/chapter_1/intro.md: -------------------------------------------------------------------------------- 1 | # 第1节:CTF简介与Move应用 2 | -------------------------------------------------------------------------------- /src/ctfbook/chapter_1/move_applications.md: -------------------------------------------------------------------------------- 1 | ## CTF比赛类型与Move应用场景 2 | 3 | 欢迎来到 **Move CTF 挑战课程** 的第一节!本节将带你走进 CTF(Capture The Flag,夺旗赛)的世界,了解其主要比赛类型,并探索 Move 编程语言在 Sui 区块链上的 CTF 应用场景。通过理论与实践结合,你将迈出学习 Move CTF 的第一步。 4 | 5 | ### 什么是CTF? 6 | CTF 是一种网络安全竞赛形式,参与者通过解决技术挑战获取隐藏的 “flag”(通常是一串特定格式的字符串,如 `CTF{xxx}`),以证明他们的技能。CTF 起源于 1996 年的 DEFCON 安全大会,现已成为全球安全爱好者的热门活动。比赛类型主要分为以下两种: 7 | 8 | #### 1. Jeopardy模式 9 | - **特点**:解题模式,参与者面对一系列独立题目,涵盖密码学、逆向工程、Web 安全、区块链等类别。 10 | - **流程**: 11 | - 题目提供线索(如代码片段、文件或服务器地址)。 12 | - 选手分析问题,找到 flag 并提交。 13 | - 根据解题数量和速度计分。 14 | - **适合人群**:初学者和个人选手,因其灵活性和低门槛广受欢迎。 15 | - **Move相关示例**:分析一段 Sui 链上的 Move 智能合约代码,找出隐藏的 flag 或利用漏洞提取数据。 16 | - **真实案例**: 17 | - 在 [CTFtime](https://ctftime.org/) 上,[HackTheBox Cyber Apocalypse CTF 2024 Blockchain Challenges](https://www.hackthebox.com/events/cyber-apocalypse-2024) 包含区块链题目,要求选手审计智能合约。 18 | - Sui 社区在 2024 年推出了 [MoveCTF 2024](https://movectf2024.movebit.xyz/),其中包括基于 Move 的解题挑战。 19 | - **现状**:当前 Move CTF(如 justCTF、MoveCTF)主要采用 Jeopardy 模式,题目以代码审计和逻辑分析为主。 20 | 21 | #### 2. Attack-Defense模式 22 | - **特点**:攻防对抗模式,团队在虚拟环境中同时攻击对手服务并防御自身系统。 23 | - **流程**: 24 | - 每个团队维护一个包含漏洞的服务。 25 | - 攻击对手以获取 flag,同时修补自身漏洞。 26 | - 综合得分决定排名。 27 | - **适合人群**:进阶选手和团队,因其更接近真实网络攻防场景。 28 | - **Move相关示例**:模拟 Sui 区块链网络,攻击对手的 Move 合约(如利用未授权访问漏洞窃取资源),同时保护自己的合约免受攻击。 29 | - **现状**:目前 Move CTF 未见公开的攻防模式案例,更多聚焦于解题形式的挑战。 30 | 31 | #### CTF的吸引力 32 | - **技能提升**:涵盖编程、逆向、安全分析等多领域。 33 | - **实战性**:模拟真实安全场景,如区块链漏洞利用。 34 | - **趣味性**:解题过程如同解谜,充满成就感。 35 | 36 | ### Move 在 CTF 中的应用 37 | Move 是由 Facebook(现 Meta)为 Diem 区块链设计的一种编程语言,后被 Sui 公链采用并优化。它以资源导向和类型安全著称,灵感来源于 Rust。在 Sui 生态中,Move 用于编写智能合约和去中心化应用(DApp)。以下是 Move 在 CTF 中的主要应用场景: 38 | 39 | #### 1. 智能合约漏洞挖掘 40 | - **背景**:Sui 链上的 Move 合约是 CTF 题目的常见素材,选手需分析代码,寻找漏洞。 41 | - **常见考点**: 42 | - **整数溢出/下溢**:Move 未内置溢出检查,可能导致非法操作。例如,代币转移逻辑可能因溢出被绕过。 43 | - **逻辑错误**:如条件判断失误,导致意外行为。 44 | - **示例**:假设一个 Move 代币合约未检查整数加法的溢出,选手可通过构造大额输入转移超出余额的代币,flag 可能隐藏在交易事件中。 45 | - **Move特点**:Move 的类型系统减少了某些传统漏洞(如重入攻击),但仍需关注逻辑和边界问题。 46 | 47 | #### 2. 资源管理挑战 48 | - **Move特性**:Move 的资源(struct)具有线性类型特性,不可随意复制或丢弃,必须显式转移或销毁。这是 Sui 链上对象(Object)管理的核心机制。 49 | - **CTF应用**: 50 | - 题目可能涉及资源管理不当(如未销毁资源)导致的漏洞。 51 | - 或通过资源转移逻辑的错误绕过限制。 52 | - **示例**:设想一个 Move 合约中,开发者忘记销毁旧资源,导致资源被重复使用,选手可利用此漏洞执行双重花费。 53 | 54 | #### 3. 代码逻辑逆向 55 | - **背景**:CTF 常要求选手理解代码意图,寻找隐藏信息。 56 | - **Move场景**: 57 | - flag 可能藏在注释、变量名或 Sui 链上的事件输出中。 58 | - 或通过复杂逻辑(如循环、条件)计算得出。 59 | - **真实案例**:在 justCTF 2024 Teaser 的“The Otter Scrolls”题目([justCTF 2024](https://2024.justctf.team/challenges/11))中,选手需分析 Sui Move 合约 `Spellbook`,调用 `cast_spell` 函数并传入参数 `vector[1, 0, 3, 3, 3]`,触发 flag 输出。 60 | - **总结**:Move 的资源导向和类型安全特性为 CTF 提供了独特的技术挑战,目前通过 Jeopardy 模式吸引爱好者参与并提升 Sui 生态的安全意识。 61 | 62 | ### 学习目标 63 | 通过本节课,你将: 64 | - 理解 CTF 比赛的基本形式(Jeopardy 和 Attack-Defense)。 65 | - 认识 Move 在 Sui 链 CTF 中的应用场景。 66 | - 通过实践,初步体验 Move 代码分析,迈出 CTF 第一步。 67 | 68 | 准备好了吗?接下来,我们将通过一个简单实践,体验 Move CTF! -------------------------------------------------------------------------------- /src/ctfbook/chapter_1/practice.md: -------------------------------------------------------------------------------- 1 | # 实践:签到挑战 2 | 3 | ## 题目描述 4 | 在本实践环节,你将分析一个 Sui Move 签到合约,通过计算哈希值调用函数,获取隐藏的 flag。flag 是一个格式为 `CTF{xxx}` 的字符串,将在正确输入时通过事件输出。目标是体验 Move 代码分析和基本 CTF 解题流程。 5 | 6 | ## 示例代码 7 | 以下是待分析的 Move 合约: 8 | 9 | github: [chapter_1](https://github.com/hoh-zone/lets-ctf/tree/main/src/ctfbook/chapter_1/challenge/chapter_1) 10 | 11 | ```move 12 | module chapter_1::check_in { 13 | use std::string::{Self, String}; 14 | use std::bcs; 15 | use std::hash::sha3_256; 16 | use sui::event; 17 | 18 | //testnet 19 | //PackageID:0x335297860a807291254b20f8a0dea30d72d5e17d2e6f8058e42d5b9c72f0f0ef 20 | public struct FlagEvent has copy, drop { 21 | sender: address, 22 | flag: String, 23 | success: bool 24 | } 25 | 26 | public entry fun get_flag( 27 | flag: vector, 28 | github_id: String, 29 | ctx: &mut TxContext 30 | ) { 31 | let mut bcs_input = bcs::to_bytes(&string::utf8(b"LetsMoveCTF")); 32 | vector::append(&mut bcs_input, *github_id.as_bytes()); 33 | let expected_hash = sha3_256(bcs_input); 34 | 35 | if (flag == expected_hash) { 36 | event::emit(FlagEvent { 37 | sender: tx_context::sender(ctx), 38 | flag: string::utf8(b"CTF{WelcomeToMoveCTF}"), 39 | success: true 40 | }); 41 | } else { 42 | event::emit(FlagEvent { 43 | sender: tx_context::sender(ctx), 44 | flag: string::utf8(b"Try again!"), 45 | success: false 46 | }); 47 | } 48 | } 49 | } 50 | ``` 51 | 52 | ## 任务目标 53 | 阅读代码,理解哈希验证逻辑。 54 | 55 | 计算正确的 flag 输入并运行代码,获取 flag。 56 | 57 | ## 解题思路 58 | 1、找到如何获取flag的代码块: 59 | ```move 60 | ##其中 `flag == expected_hash` 为获取flag的条件 61 | if (flag == expected_hash) { 62 | event::emit(FlagEvent { 63 | sender: tx_context::sender(ctx), 64 | flag: string(b"CTF{WelcomeToMoveCTF}"), 65 | success: true 66 | }); 67 | } else { 68 | event::emit(FlagEvent { 69 | sender: tx_context::sender(ctx), 70 | flag: string(b"Try again!"), 71 | success: false 72 | }); 73 | } 74 | ``` 75 | 2、如何满足 `flag == expected_hash` 条件? 76 | ```move 77 | let mut bcs_input = bcs::to_bytes(&string(b"LetsMoveCTF")); 78 | vector::append(&mut bcs_input, *github_id.as_bytes()); 79 | let expected_hash = sha3_256(bcs_input); 80 | ``` 81 | 代码块中 expected_hash 为 `LetsMoveCTF` + 用户输入的`github_id` 转换为bytes然后sha3_256 进行编码,所以flag传入也需要是expected_hash的这个结果。 82 | 83 | 3、进行解题 84 | 85 | 这里采用的是合约的方式进行解题。 86 | 87 | github: [solve_chapter_1](https://github.com/hoh-zone/lets-ctf/tree/main/src/ctfbook/chapter_1/challenge/solve_chapter_1) 88 | 首先创建合约: 89 | 90 | `sui move new solve_chapter_1 && cd solve_chapter_1` 91 | 92 | 修改 `Move.toml` 文件导入题目合约,如果是本地与题目同目录则添加 `local` 方式: 93 | ``` 94 | [package] 95 | name = "solve_chapter_1" 96 | edition = "2024.beta" # edition = "legacy" to use legacy (pre-2024) Move 97 | # license = "" # e.g., "MIT", "GPL", "Apache 2.0" 98 | # authors = ["..."] # e.g., ["Joe Smith (joesmith@noemail.com)", "John Snow (johnsnow@noemail.com)"] 99 | 100 | [dependencies] 101 | chapter_1 = { local = "../chapter_1" } 102 | 103 | # For remote import, use the `{ git = "...", subdir = "...", rev = "..." }`. 104 | # Revision can be a branch, a tag, and a commit hash. 105 | # MyRemotePackage = { git = "https://some.remote/host.git", subdir = "remote/path", rev = "main" } 106 | 107 | # For local dependencies use `local = path`. Path is relative to the package root 108 | # Local = { local = "../path/to" } 109 | 110 | # To resolve a version conflict and force a specific version for dependency 111 | # override use `override = true` 112 | # Override = { local = "../conflicting/version", override = true } 113 | 114 | [addresses] 115 | solve_chapter_1 = "0x0" 116 | 117 | # Named addresses will be accessible in Move as `@name`. They're also exported: 118 | # for example, `std = "0x1"` is exported by the Standard Library. 119 | # alice = "0xA11CE" 120 | 121 | [dev-dependencies] 122 | # The dev-dependencies section allows overriding dependencies for `--test` and 123 | # `--dev` modes. You can introduce test-only dependencies here. 124 | # Local = { local = "../path/to/dev-build" } 125 | 126 | [dev-addresses] 127 | # The dev-addresses section allows overwriting named addresses for the `--test` 128 | # and `--dev` modes. 129 | # alice = "0xB0B" 130 | 131 | ``` 132 | 133 | 然后编写解题合约 `solve_chapter_1.move`: 134 | 135 | ```move 136 | module solve_chapter_1::solve{ 137 | use chapter_1::check_in::get_flag; 138 | use std::string; 139 | use std::bcs; 140 | use std::hash::sha3_256; 141 | 142 | //testnet 143 | //PackageID: 0xef6b4139ec1b0fda23e06c4a30c9e91150b72c38530e4517152e591001c5c433 144 | public entry fun solve_get_flag(ctx: &mut TxContext){ 145 | let github_id = string::utf8(b"hoh-zone"); 146 | let mut bcs_input = bcs::to_bytes(&string::utf8(b"LetsMoveCTF")); 147 | vector::append(&mut bcs_input, *github_id.as_bytes()); 148 | let flag_hash = sha3_256(bcs_input); 149 | get_flag(flag_hash, github_id, ctx); 150 | } 151 | } 152 | ``` 153 | 154 | 发布合约: 155 | 156 | `sui client publish` 157 | 158 | 发布成功后调用合约: 159 | ```bash 160 | sui client call --package 0xef6b4139ec1b0fda23e06c4a30c9e91150b72c38530e4517152e591001c5c433 --module solve --function solve_get_flag 161 | ``` 162 | 163 | 最终结果可以看到终端输出的Events内成功获取flag: 164 | ```bash 165 | ╭───────────────────────────────────────────────────────────────────────────────────────────────────────╮ 166 | │ Transaction Block Events │ 167 | ├───────────────────────────────────────────────────────────────────────────────────────────────────────┤ 168 | │ ┌── │ 169 | │ │ EventID: EjQiJnPZRqen1TSSkNiUaRaEQYSmhshJYHctn3uUt1V5:0 │ 170 | │ │ PackageID: 0xef6b4139ec1b0fda23e06c4a30c9e91150b72c38530e4517152e591001c5c433 │ 171 | │ │ Transaction Module: solve │ 172 | │ │ Sender: 0x90abb670800b4015229d30f5d010faef0c347e1d9650c9acebe2c012be7eb724 │ 173 | │ │ EventType: 0x335297860a807291254b20f8a0dea30d72d5e17d2e6f8058e42d5b9c72f0f0ef::check_in::FlagEvent │ 174 | │ │ ParsedJSON: │ 175 | │ │ ┌─────────┬────────────────────────────────────────────────────────────────────┐ │ 176 | │ │ │ flag │ CTF{WelcomeToMoveCTF} │ │ 177 | │ │ ├─────────┼────────────────────────────────────────────────────────────────────┤ │ 178 | │ │ │ sender │ 0x90abb670800b4015229d30f5d010faef0c347e1d9650c9acebe2c012be7eb724 │ │ 179 | │ │ ├─────────┼────────────────────────────────────────────────────────────────────┤ │ 180 | │ │ │ success │ true │ │ 181 | │ │ └─────────┴────────────────────────────────────────────────────────────────────┘ │ 182 | │ └── │ 183 | ╰───────────────────────────────────────────────────────────────────────────────────────────────────────╯ 184 | ``` -------------------------------------------------------------------------------- /src/ctfbook/chapter_1/task1.md: -------------------------------------------------------------------------------- 1 | # Task1 2 | 3 | ## 任务代码 4 | 以下是待分析的 Move 合约,完整代码请查看 [挑战合约](https://github.com/hoh-zone/lets-ctf/tree/main/src/ctfbook/chapter_1/task1): 5 | ``` 6 | PackageID: 0xcd6050b4e93c5bdc7149f2bf0b69202ec297bf1532d500e896dbefcd15d811f4 7 | Challenge ObjectID: 0xf5dc0a1701384ff0ff6697ae9de37ee0cc832ff1fd511232aca7d0fff282d026 8 | ``` 9 | 10 | ```move 11 | module task1::task1 { 12 | use std::bcs; 13 | use std::hash::sha3_256; 14 | use std::string::{Self, String}; 15 | use sui::event; 16 | use sui::random::{Self, Random}; 17 | use sui::transfer::share_object; 18 | 19 | const EINVALID_HASH: u64 = 0; 20 | const EINVALID_MAGIC: u64 = 1; 21 | const EINVALID_SEED: u64 = 2; 22 | 23 | public struct FlagEvent has copy, drop { 24 | sender: address, 25 | flag: String, 26 | attempt_count: u64, 27 | github_id: String, 28 | success: bool 29 | } 30 | 31 | public struct Challenge has key { 32 | id: UID, 33 | secret: String, 34 | attempt_count: u64, 35 | last_seed: u64 36 | } 37 | 38 | fun init(ctx: &mut TxContext) { 39 | let challenge = Challenge { 40 | id: object::new(ctx), 41 | secret: string::utf8(b"MoveCTF_task1"), 42 | attempt_count: 0, 43 | last_seed: 0 44 | }; 45 | share_object(challenge); 46 | } 47 | 48 | public entry fun get_flag( 49 | hash_input: vector, 50 | github_id: String, 51 | magic_number: u64, 52 | seed: u64, 53 | challenge: &mut Challenge, 54 | rand: &Random, 55 | ctx: &mut TxContext 56 | ) { 57 | let mut bcs_input = bcs::to_bytes(&challenge.secret); 58 | vector::append(&mut bcs_input, *github_id.as_bytes()); 59 | let expected_hash = sha3_256(bcs_input); 60 | assert!(hash_input == expected_hash, EINVALID_HASH); 61 | 62 | challenge.attempt_count = challenge.attempt_count + 1; 63 | 64 | let expected_magic = (challenge.attempt_count * challenge.attempt_count + challenge.last_seed) % 1000 + seed; 65 | assert!(magic_number == expected_magic, EINVALID_MAGIC); 66 | 67 | let secret_bytes = *string::as_bytes(&challenge.secret); 68 | let secret_len = vector::length(&secret_bytes); 69 | assert!(seed == secret_len * 2, EINVALID_SEED); 70 | 71 | challenge.secret = getRandomString(rand, ctx); 72 | challenge.last_seed = seed; 73 | 74 | event::emit(FlagEvent { 75 | sender: tx_context::sender(ctx), 76 | flag: string::utf8(b"CTF{MoveCTF-Task1}"), 77 | github_id, 78 | attempt_count: challenge.attempt_count, 79 | success: true 80 | }); 81 | } 82 | 83 | fun getRandomString(rand: &Random, ctx: &mut TxContext): String { 84 | let mut gen = random::new_generator(rand, ctx); 85 | let mut str_len = random::generate_u8_in_range(&mut gen, 4, 32); 86 | let mut rand_vec: vector = b""; 87 | while (str_len != 0) { 88 | let rand_num = random::generate_u8_in_range(&mut gen, 34, 126); 89 | vector::push_back(&mut rand_vec, rand_num); 90 | str_len = str_len - 1; 91 | }; 92 | string::utf8(rand_vec) 93 | } 94 | } 95 | ``` -------------------------------------------------------------------------------- /src/ctfbook/chapter_1/task1/.gitignore: -------------------------------------------------------------------------------- 1 | build/* 2 | -------------------------------------------------------------------------------- /src/ctfbook/chapter_1/task1/Move.lock: -------------------------------------------------------------------------------- 1 | # @generated by Move, please check-in and do not edit manually. 2 | 3 | [move] 4 | version = 3 5 | manifest_digest = "F6DCF04755C04940D70A8F78F4CDE24C55FD62A8F72E70CE5477B61ED8FC5184" 6 | deps_digest = "397E6A9F7A624706DBDFEE056CE88391A15876868FD18A88504DA74EB458D697" 7 | dependencies = [ 8 | { id = "Bridge", name = "Bridge" }, 9 | { id = "DeepBook", name = "DeepBook" }, 10 | { id = "MoveStdlib", name = "MoveStdlib" }, 11 | { id = "Sui", name = "Sui" }, 12 | { id = "SuiSystem", name = "SuiSystem" }, 13 | ] 14 | 15 | [[move.package]] 16 | id = "Bridge" 17 | source = { git = "https://github.com/MystenLabs/sui.git", rev = "9c04e1840eb5", subdir = "crates/sui-framework/packages/bridge" } 18 | 19 | dependencies = [ 20 | { id = "MoveStdlib", name = "MoveStdlib" }, 21 | { id = "Sui", name = "Sui" }, 22 | { id = "SuiSystem", name = "SuiSystem" }, 23 | ] 24 | 25 | [[move.package]] 26 | id = "DeepBook" 27 | source = { git = "https://github.com/MystenLabs/sui.git", rev = "9c04e1840eb5", subdir = "crates/sui-framework/packages/deepbook" } 28 | 29 | dependencies = [ 30 | { id = "MoveStdlib", name = "MoveStdlib" }, 31 | { id = "Sui", name = "Sui" }, 32 | ] 33 | 34 | [[move.package]] 35 | id = "MoveStdlib" 36 | source = { git = "https://github.com/MystenLabs/sui.git", rev = "9c04e1840eb5", subdir = "crates/sui-framework/packages/move-stdlib" } 37 | 38 | [[move.package]] 39 | id = "Sui" 40 | source = { git = "https://github.com/MystenLabs/sui.git", rev = "9c04e1840eb5", subdir = "crates/sui-framework/packages/sui-framework" } 41 | 42 | dependencies = [ 43 | { id = "MoveStdlib", name = "MoveStdlib" }, 44 | ] 45 | 46 | [[move.package]] 47 | id = "SuiSystem" 48 | source = { git = "https://github.com/MystenLabs/sui.git", rev = "9c04e1840eb5", subdir = "crates/sui-framework/packages/sui-system" } 49 | 50 | dependencies = [ 51 | { id = "MoveStdlib", name = "MoveStdlib" }, 52 | { id = "Sui", name = "Sui" }, 53 | ] 54 | 55 | [move.toolchain-version] 56 | compiler-version = "1.45.2" 57 | edition = "2024.beta" 58 | flavor = "sui" 59 | 60 | [env] 61 | 62 | [env.testnet] 63 | chain-id = "4c78adac" 64 | original-published-id = "0xcd6050b4e93c5bdc7149f2bf0b69202ec297bf1532d500e896dbefcd15d811f4" 65 | latest-published-id = "0xcd6050b4e93c5bdc7149f2bf0b69202ec297bf1532d500e896dbefcd15d811f4" 66 | published-version = "1" 67 | -------------------------------------------------------------------------------- /src/ctfbook/chapter_1/task1/Move.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "task1" 3 | edition = "2024.beta" # edition = "legacy" to use legacy (pre-2024) Move 4 | # license = "" # e.g., "MIT", "GPL", "Apache 2.0" 5 | # authors = ["..."] # e.g., ["Joe Smith (joesmith@noemail.com)", "John Snow (johnsnow@noemail.com)"] 6 | 7 | [dependencies] 8 | 9 | # For remote import, use the `{ git = "...", subdir = "...", rev = "..." }`. 10 | # Revision can be a branch, a tag, and a commit hash. 11 | # MyRemotePackage = { git = "https://some.remote/host.git", subdir = "remote/path", rev = "main" } 12 | 13 | # For local dependencies use `local = path`. Path is relative to the package root 14 | # Local = { local = "../path/to" } 15 | 16 | # To resolve a version conflict and force a specific version for dependency 17 | # override use `override = true` 18 | # Override = { local = "../conflicting/version", override = true } 19 | 20 | [addresses] 21 | task1 = "0x0" 22 | 23 | # Named addresses will be accessible in Move as `@name`. They're also exported: 24 | # for example, `std = "0x1"` is exported by the Standard Library. 25 | # alice = "0xA11CE" 26 | 27 | [dev-dependencies] 28 | # The dev-dependencies section allows overriding dependencies for `--test` and 29 | # `--dev` modes. You can introduce test-only dependencies here. 30 | # Local = { local = "../path/to/dev-build" } 31 | 32 | [dev-addresses] 33 | # The dev-addresses section allows overwriting named addresses for the `--test` 34 | # and `--dev` modes. 35 | # alice = "0xB0B" 36 | 37 | -------------------------------------------------------------------------------- /src/ctfbook/chapter_1/task1/sources/task1.move: -------------------------------------------------------------------------------- 1 | module task1::task1 { 2 | use std::bcs; 3 | use std::hash::sha3_256; 4 | use std::string::{Self, String}; 5 | use sui::event; 6 | use sui::random::{Self, Random}; 7 | use sui::transfer::share_object; 8 | 9 | const EINVALID_HASH: u64 = 0; 10 | const EINVALID_MAGIC: u64 = 1; 11 | const EINVALID_SEED: u64 = 2; 12 | 13 | public struct FlagEvent has copy, drop { 14 | sender: address, 15 | flag: String, 16 | attempt_count: u64, 17 | github_id: String, 18 | success: bool 19 | } 20 | 21 | public struct Challenge has key { 22 | id: UID, 23 | secret: String, 24 | attempt_count: u64, 25 | last_seed: u64 26 | } 27 | 28 | fun init(ctx: &mut TxContext) { 29 | let challenge = Challenge { 30 | id: object::new(ctx), 31 | secret: string::utf8(b"MoveCTF_task1"), 32 | attempt_count: 0, 33 | last_seed: 0 34 | }; 35 | share_object(challenge); 36 | } 37 | 38 | public entry fun get_flag( 39 | hash_input: vector, 40 | github_id: String, 41 | magic_number: u64, 42 | seed: u64, 43 | challenge: &mut Challenge, 44 | rand: &Random, 45 | ctx: &mut TxContext 46 | ) { 47 | let mut bcs_input = bcs::to_bytes(&challenge.secret); 48 | vector::append(&mut bcs_input, *github_id.as_bytes()); 49 | let expected_hash = sha3_256(bcs_input); 50 | assert!(hash_input == expected_hash, EINVALID_HASH); 51 | 52 | challenge.attempt_count = challenge.attempt_count + 1; 53 | 54 | let expected_magic = (challenge.attempt_count * challenge.attempt_count + challenge.last_seed) % 1000 + seed; 55 | assert!(magic_number == expected_magic, EINVALID_MAGIC); 56 | 57 | let secret_bytes = *string::as_bytes(&challenge.secret); 58 | let secret_len = vector::length(&secret_bytes); 59 | assert!(seed == secret_len * 2, EINVALID_SEED); 60 | 61 | challenge.secret = getRandomString(rand, ctx); 62 | challenge.last_seed = seed; 63 | 64 | event::emit(FlagEvent { 65 | sender: tx_context::sender(ctx), 66 | flag: string::utf8(b"CTF{MoveCTF-Task1}"), 67 | github_id, 68 | attempt_count: challenge.attempt_count, 69 | success: true 70 | }); 71 | } 72 | 73 | fun getRandomString(rand: &Random, ctx: &mut TxContext): String { 74 | let mut gen = random::new_generator(rand, ctx); 75 | let mut str_len = random::generate_u8_in_range(&mut gen, 4, 32); 76 | let mut rand_vec: vector = b""; 77 | while (str_len != 0) { 78 | let rand_num = random::generate_u8_in_range(&mut gen, 34, 126); 79 | vector::push_back(&mut rand_vec, rand_num); 80 | str_len = str_len - 1; 81 | }; 82 | string::utf8(rand_vec) 83 | } 84 | } -------------------------------------------------------------------------------- /src/ctfbook/chapter_1/task1/tests/task1_tests.move: -------------------------------------------------------------------------------- 1 | /* 2 | #[test_only] 3 | module task1::task1_tests; 4 | // uncomment this line to import the module 5 | // use task1::task1; 6 | 7 | const ENotImplemented: u64 = 0; 8 | 9 | #[test] 10 | fun test_task1() { 11 | // pass 12 | } 13 | 14 | #[test, expected_failure(abort_code = ::task1::task1_tests::ENotImplemented)] 15 | fun test_task1_fail() { 16 | abort ENotImplemented 17 | } 18 | */ 19 | -------------------------------------------------------------------------------- /src/ctfbook/chapter_2/code_reading.md: -------------------------------------------------------------------------------- 1 | # 阅读Move代码与常见问题 2 | ## 阅读 Move 代码与常见问题 3 | 4 | 欢迎来到 **Move CTF 挑战课程** 的第二节!在第一节中,你通过一个签到挑战初步体验了 Move 代码的分析和解题流程。现在,我们将迈出审计的第一步,深入学习如何阅读 Move 代码并识别常见问题。本节将通过详细的理论讲解和实践环节,帮助你掌握基础审计技能,为后续更复杂的漏洞分析打下坚实基础。 5 | 6 | #### 1. 模块与函数 7 | - **模块(Module)**: 8 | - Move 的代码组织单元,类似于传统语言中的合约或类。 9 | - 格式:`module
::`,其中 `
` 是部署地址(如 `0x1`),`` 是模块名。 10 | - 包含结构体、常量和函数,定义合约的逻辑。 11 | - 示例: 12 | ``` 13 | module chapter_2_test::counter { 14 | 15 | public struct Counter has key { 16 | id: UID, 17 | count: u64, 18 | } 19 | 20 | fun init(ctx: &mut TxContext){ 21 | transfer::share_object(Counter { id:object::new(ctx), count: 0 }); 22 | 23 | } 24 | 25 | public entry fun increment(counter: &mut Counter) { 26 | counter.count = counter.count + 1; 27 | } 28 | } 29 | ``` 30 | - **函数类型**: 31 | - `public entry fun`:外部可调用的入口函数,通常是 CTF 题目中的交互点,接受 `&mut TxContext` 参数以获取交易上下文。 32 | - `public fun`:公开函数,可被其他模块调用,但不直接作为交易入口。 33 | - `fun`:私有函数,仅模块内部使用。 34 | - 示例: 35 | ``` 36 | public entry fun set_value(value: u64, tx: &mut TxContext) { 37 | /* 交易入口 */ 38 | } 39 | fun internal_add(a: u64, b: u64): u64 { a + b } // 内部辅助函数 40 | ``` 41 | #### 2. 资源与所有权 42 | - **资源(Resource)**: 43 | - Move 的核心特性,使用 `struct` 定义,带有能力(`has` 声明,如 `key`、`store`)。 44 | - 线性类型:资源不可复制(`copy`)或丢弃(`drop`),必须显式转移或销毁。 45 | - 示例: 46 | ``` 47 | public struct Coin has key { 48 | id: UID, 49 | value: u64 50 | } 51 | ``` 52 | - **所有权管理**: 53 | - 创建资源后,可通过 `transfer::transfer`(转移给地址)或 `transfer::share_object`(共享对象)等处理。 54 | - 示例: 55 | ``` 56 | public entry fun create(ctx: &mut TxContext){ 57 | let coin = Coin { id: object::new(ctx), value: 100 }; 58 | transfer::transfer(coin, tx_context::sender(ctx)); 59 | } 60 | ``` 61 | - 未处理资源会导致编译错误,确保所有权清晰。 62 | - **Sui 特有机制**: 63 | - **对象(Object)**:通过 `UID`(唯一标识符)管理,Sui 的基本数据单元。 64 | - **共享对象**:通过 `transfer::share_object` 创建,允许多人操作,常用于 CTF 的共享状态。 65 | 66 | #### 3. 事件与输出 67 | - **事件(Event)**: 68 | - 通过 `event::emit` 输出日志,用于记录状态变化或 CTF 中的 flag 输出。 69 | - 需定义事件结构体,具备 `copy` 和 `drop` 能力。 70 | - 示例: 71 | ``` 72 | public struct FlagEvent has copy, drop { 73 | sender: address, 74 | flag: vector 75 | } 76 | 77 | public entry fun get_flag(ctx: &mut TxContext){ 78 | event::emit(FlagEvent { sender: tx_context::sender(ctx), flag: b"CTF{example}" }); 79 | } 80 | ``` 81 | - CTF 中,事件常是获取 flag 的关键途径。 82 | 83 | #### 4. 变量与类型 84 | - **基本类型**: 85 | - `u8`、`u64`、`u128`:无符号整数。 86 | - `bool`:布尔值。 87 | - `address`:账户地址。 88 | - `vector`:动态数组。 89 | - **引用**: 90 | - `&T`:不可变引用,用于读取。 91 | - `&mut T`:可变引用,用于修改。 92 | - 示例: 93 | ``` 94 | public entry fun increment(counter: &mut Counter) { 95 | counter.count = counter.count + 1; 96 | } 97 | ``` 98 | 99 | ### 常见问题与漏洞类型 100 | 101 | #### 1. 未验证的输入 102 | - **影响**: 103 | - 若 `limit` 被设置为异常值(如 `2^64 - 1`),后续逻辑可能失效。 104 | - 在 CTF 中,可能通过异常输入绕过限制或提取 flag。 105 | 106 | #### 2. 逻辑错误 107 | - **问题**:条件判断、状态更新或流程控制错误,导致与设计意图不符的结果。 108 | - **详细说明**: 109 | - Move 依赖开发者正确实现逻辑,无内置保护机制。 110 | - 常见于条件遗漏、顺序错误或判断反转。 111 | - **示例**: 112 | ``` 113 | public struct AccessControl has key { 114 | id: UID, 115 | is_allowed: bool, 116 | threshold: u64 117 | } 118 | 119 | public entry fun check_access(access: &mut AccessControl, score: u64) { 120 | if (score > 50) { // 应为 >= 50,但是设置为 >50 121 | access.is_allowed = true; 122 | } 123 | } 124 | ``` 125 | - 边界值 score = 50 被意外排除. 126 | 127 | - **更复杂示例**: 128 | ``` 129 | public entry fun update_state(state: &mut AccessControl, value: u64) { 130 | if (value < state.threshold) { 131 | state.is_allowed = false; // 可能应为 true 132 | } 133 | } 134 | ``` 135 | - 若意图是“低于阈值激活”,条件与赋值不符。 136 | 137 | - **影响**: 138 | - 可能允许未授权操作或阻止合法行为。 139 | 140 | #### 3. 权限控制不足 141 | - **问题**:函数未限制调用者身份,允许任何人执行敏感操作。 142 | - **详细说明**: 143 | - Move 的 public entry fun 默认对所有地址开放,需手动验证 tx_context::sender. 144 | - Sui 的共享对象尤其需注意权限。 145 | - **示例**: 146 | ``` 147 | public entry fun reset_counter(counter: &mut Counter) { // 未验证调用者 148 | counter.count = 0; 149 | } 150 | ``` 151 | - 任何人都可重置计数器。 152 | - **更实际示例**: 153 | ``` 154 | public struct SuiPool has key { 155 | id: UID, 156 | suiBalance: Balance<0x2::sui::SUI>, 157 | } 158 | 159 | public entry fun withdraw_commision( 160 | suipool: &mut SuiPool, 161 | amount: u64, 162 | to: address, 163 | ctx: &mut TxContext, 164 | ) { 165 | assert!(suipool.suiBalance.value() > amount, 1); 166 | let coin_balance = suipool.suiBalance.split(amount); 167 | let coin = from_balance(coin_balance, ctx); 168 | public_transfer(coin, to); 169 | } 170 | ``` 171 | - 非管理者可提取余额。 172 | - **影响**: 173 | - 未授权用户可能破坏合约状态或窃取资源。 174 | 175 | #### 4. 整数溢出/下溢 176 | - **问题**:在 Sui 中,Move 的整数运算(如 u64 的加法、减法)默认启用溢出检查,溢出或下溢会导致交易失败. 177 | - **示例**: 178 | ``` 179 | module counter::counter{ 180 | use sui::event; 181 | 182 | public struct Counter has key { 183 | id: UID, 184 | count: u64, 185 | } 186 | 187 | public struct CounterEmit has copy, drop{ 188 | count: u64, 189 | } 190 | 191 | fun init(ctx: &mut TxContext){ 192 | transfer::share_object(Counter { id:object::new(ctx), count: 0 }); 193 | } 194 | 195 | public entry fun add(counter: &mut Counter,amount: u64){ 196 | counter.count = counter.count + amount; 197 | event::emit(CounterEmit { count: counter.count }) 198 | } 199 | 200 | public entry fun reduce(counter: &mut Counter,amount: u64){ 201 | counter.count = counter.count - amount; 202 | event::emit(CounterEmit { count: counter.count }) 203 | } 204 | } 205 | ``` 206 | - **影响**: 207 | - 若 counter + amount > 2^64 - 1 或 counter - amount < 0 会抛出 MovePrimitiveRuntimeError,交易失败,无法继续执行。 208 | 209 | #### 5. 资源管理不当 210 | - **问题**:资源未正确转移或销毁,导致编译错误。 211 | - **示例**: 212 | ``` 213 | public entry fun create_coin(ctx: &mut TxContext) { 214 | let coin = Coin { id: object::new(ctx), value: 100 }; 215 | } 216 | ``` 217 | - **影响**: 218 | - 编译错误。 -------------------------------------------------------------------------------- /src/ctfbook/chapter_2/intro.md: -------------------------------------------------------------------------------- 1 | # 第2节:基础代码审计 2 | -------------------------------------------------------------------------------- /src/ctfbook/chapter_2/practice.md: -------------------------------------------------------------------------------- 1 | # 实践:识别与修复简单漏洞 2 | 3 | ## 题目描述 4 | 5 | 在本实践环节,你将审计一个简单的 Sui Move 合约 chapter_2::simple_challenge,目标是识别其中的漏洞并提出修复建议。合约实现了一个简单的“计数挑战”:用户可以通过提交计数(increment_count)来增加计数器,达到目标值后领取奖励(claim_reward)。奖励是共享的,任何人都可以领取。然而,合约存在一些隐藏漏洞,其中一个可能导致运行时报错,你需要找到这些漏洞,分析其影响,并提出修复建议。 6 | 7 | ## 示例代码 8 | 9 | 以下是 chapter_2::simple_challenge 模块的代码: 10 | 11 | move 12 | 13 | ```text 14 | module chapter_2::simple_challenge { 15 | use sui::object::{Self, UID}; 16 | use sui::transfer; 17 | use sui::tx_context::{Self, TxContext}; 18 | use sui::event; 19 | 20 | public struct Challenge has key { 21 | id: UID, 22 | owner: address, 23 | count: u64, 24 | target_count: u64, 25 | reward: u64, 26 | total_rewards_claimed: u64, 27 | total_attempts: u64, 28 | } 29 | 30 | public struct RewardEvent has copy, drop { 31 | reward: u64, 32 | } 33 | 34 | fun init(ctx: &mut TxContext) { 35 | let challenge = Challenge { 36 | id: object::new(ctx), 37 | owner: tx_context::sender(ctx), 38 | count: 0, 39 | target_count: 10, 40 | reward: 1000, 41 | total_rewards_claimed: 0, 42 | total_attempts: 0, 43 | }; 44 | transfer::share_object(challenge); 45 | } 46 | 47 | public entry fun increment_count(challenge: &mut Challenge) { 48 | challenge.total_attempts = challenge.total_attempts + 1; 49 | challenge.count = challenge.count + 1; 50 | } 51 | 52 | public entry fun claim_reward(challenge: &mut Challenge, ctx: &mut TxContext) { 53 | if (challenge.count >= challenge.target_count) { 54 | challenge.total_rewards_claimed = challenge.total_rewards_claimed + challenge.reward; 55 | event::emit(RewardEvent { reward: challenge.reward }); 56 | challenge.count = 0; 57 | }; 58 | } 59 | } 60 | ``` -------------------------------------------------------------------------------- /src/ctfbook/chapter_2/task2.md: -------------------------------------------------------------------------------- 1 | # Task2 2 | 3 | ## 任务代码 4 | 以下是待分析的 Move 合约,完整代码请查看 [挑战合约](https://github.com/hoh-zone/lets-ctf/tree/main/src/ctfbook/chapter_2/task2): 5 | ``` 6 | PackageID: 0xd26a14084af49f68d4612ef0815518c251f7a0459eaf2cbcb2757efafad442c5 7 | Challenge ObjectID: 0x22c1330b43313cee7f0ca0ce36965343c1ae40577d80a4ffa8ab12986f50dea1 8 | ``` 9 | 10 | ```move 11 | module task2::task2 { 12 | use sui::event; 13 | use sui::random::{Random, generate_u64, new_generator}; 14 | use sui::clock::{Self, Clock}; 15 | use std::hash; 16 | use std::string::String; 17 | 18 | const E_WRONG_STAGE: u64 = 1; 19 | const E_COOLDOWN: u64 = 2; 20 | const E_MAX_ATTEMPTS_EXCEEDED: u64 = 3; 21 | 22 | public struct Challenge has key { 23 | id: UID, 24 | owner: address, 25 | secret_hash: vector, 26 | attempts: u64, 27 | max_attempts: u64, 28 | last_attempt_time: u64, 29 | is_solved: bool, 30 | stage: u64, 31 | target_score: u64, 32 | current_score: u64, 33 | bonus_multiplier: u64, 34 | guess_round: u64, 35 | round_hash: vector, 36 | seed: u64, 37 | } 38 | 39 | public struct FlagEvent has copy, drop { 40 | flag: vector, 41 | github_id: String 42 | } 43 | 44 | public struct ScoreEvent has copy, drop { 45 | score: u64, 46 | } 47 | 48 | fun init(ctx: &mut TxContext) { 49 | let secret = b"LetsMoveCTF"; 50 | let secret_hash = hash::sha3_256(secret); 51 | let challenge = Challenge { 52 | id: object::new(ctx), 53 | owner: tx_context::sender(ctx), 54 | secret_hash: secret_hash, 55 | attempts: 0, 56 | max_attempts: 50, 57 | last_attempt_time: 0, 58 | is_solved: false, 59 | stage: 1, 60 | target_score: 100, 61 | current_score: 0, 62 | bonus_multiplier: 0, 63 | guess_round: 1, 64 | round_hash: secret_hash, 65 | seed: 0, 66 | }; 67 | transfer::share_object(challenge); 68 | } 69 | 70 | public entry fun submit_score(challenge: &mut Challenge, score: u64, clock: &Clock) { 71 | assert!(challenge.stage == 1, E_WRONG_STAGE); 72 | let current_time = clock::timestamp_ms(clock); 73 | assert!(current_time >= challenge.last_attempt_time + 5000, E_COOLDOWN); 74 | challenge.attempts = challenge.attempts + 1; 75 | assert!(challenge.attempts < challenge.max_attempts, E_MAX_ATTEMPTS_EXCEEDED); 76 | let time_factor = (current_time - challenge.last_attempt_time) / 1000; 77 | let attempt_penalty = challenge.attempts * 2; 78 | let adjusted_score = if (score > attempt_penalty) { 79 | score - attempt_penalty 80 | } else { 81 | 0 82 | }; 83 | let final_score = adjusted_score * challenge.bonus_multiplier + time_factor; 84 | challenge.current_score = challenge.current_score + final_score; 85 | challenge.last_attempt_time = current_time; 86 | event::emit(ScoreEvent { score: challenge.current_score }); 87 | if (challenge.current_score >= challenge.target_score) { 88 | challenge.stage = 2; 89 | challenge.attempts = 0; 90 | }; 91 | } 92 | 93 | #[allow(lint(public_random))] 94 | public entry fun submit_guess(challenge: &mut Challenge, randomseed: &Random, guess: vector, clock: &Clock, ctx: &mut TxContext) { 95 | assert!(challenge.stage == 2, E_WRONG_STAGE); 96 | let mut random_gen = new_generator(randomseed, ctx); 97 | let seed = generate_u64(&mut random_gen); 98 | let current_time = clock::timestamp_ms(clock); 99 | assert!(current_time >= challenge.last_attempt_time + 5000, E_COOLDOWN); 100 | assert!(challenge.attempts < challenge.max_attempts, E_MAX_ATTEMPTS_EXCEEDED); 101 | challenge.attempts = challenge.attempts + 1; 102 | let mut guess_data = guess; 103 | vector::append(&mut guess_data, to_bytes(current_time)); 104 | vector::append(&mut guess_data, to_bytes(challenge.attempts)); 105 | let random = hash::sha3_256(guess_data); 106 | let prefix_length = challenge.guess_round * 2; 107 | if (compare_hash_prefix(&random, &challenge.round_hash, prefix_length)) { 108 | challenge.guess_round = challenge.guess_round + 1; 109 | let mut new_hash_data = random; 110 | vector::append(&mut new_hash_data, to_bytes(challenge.seed + challenge.guess_round)); 111 | challenge.round_hash = hash::sha3_256(new_hash_data); 112 | challenge.seed = seed; 113 | if (challenge.guess_round > 3) { 114 | challenge.is_solved = true; 115 | challenge.stage = 3; 116 | challenge.guess_round = 1; 117 | challenge.attempts = 0; 118 | }; 119 | }; 120 | challenge.last_attempt_time = current_time; 121 | } 122 | 123 | public entry fun get_flag(challenge: &mut Challenge,github_id: String, _: &mut TxContext) { 124 | assert!(challenge.stage == 3 && challenge.is_solved, E_WRONG_STAGE); 125 | reset_challenge(challenge); 126 | event::emit(FlagEvent { flag: b"flag{LetsMoveCTF_chapter_2}" ,github_id}); 127 | } 128 | 129 | public fun reset_challenge(challenge: &mut Challenge) { 130 | challenge.attempts = 0; 131 | challenge.last_attempt_time = 0; 132 | challenge.is_solved = false; 133 | challenge.stage = 1; 134 | challenge.current_score = 0; 135 | challenge.bonus_multiplier = 1; 136 | challenge.guess_round = 1; 137 | challenge.round_hash = challenge.secret_hash; 138 | } 139 | 140 | fun to_bytes(value: u64): vector { 141 | let mut bytes = vector::empty(); 142 | let mut i = 0; 143 | while (i < 8) { 144 | vector::push_back(&mut bytes, ((value >> (i * 8)) & 0xFF as u8)); 145 | i = i + 1; 146 | }; 147 | bytes 148 | } 149 | 150 | fun compare_hash_prefix(hash1: &vector, hash2: &vector, n: u64): bool { 151 | if (vector::length(hash1) < n || vector::length(hash2) < n) { 152 | return false 153 | }; 154 | let mut i = 0; 155 | while (i < n) { 156 | if (*vector::borrow(hash1, i) != *vector::borrow(hash2, i)) { 157 | return false 158 | }; 159 | i = i + 1; 160 | }; 161 | true 162 | } 163 | } 164 | ``` -------------------------------------------------------------------------------- /src/ctfbook/chapter_2/task2/.gitignore: -------------------------------------------------------------------------------- 1 | build/* 2 | -------------------------------------------------------------------------------- /src/ctfbook/chapter_2/task2/Move.lock: -------------------------------------------------------------------------------- 1 | # @generated by Move, please check-in and do not edit manually. 2 | 3 | [move] 4 | version = 3 5 | manifest_digest = "B18FCA2316E4FA78C97884AA0C32188EC4E6D1A52AAB564857E31BDFE42004D5" 6 | deps_digest = "397E6A9F7A624706DBDFEE056CE88391A15876868FD18A88504DA74EB458D697" 7 | dependencies = [ 8 | { id = "Bridge", name = "Bridge" }, 9 | { id = "DeepBook", name = "DeepBook" }, 10 | { id = "MoveStdlib", name = "MoveStdlib" }, 11 | { id = "Sui", name = "Sui" }, 12 | { id = "SuiSystem", name = "SuiSystem" }, 13 | ] 14 | 15 | [[move.package]] 16 | id = "Bridge" 17 | source = { git = "https://github.com/MystenLabs/sui.git", rev = "9c04e1840eb5", subdir = "crates/sui-framework/packages/bridge" } 18 | 19 | dependencies = [ 20 | { id = "MoveStdlib", name = "MoveStdlib" }, 21 | { id = "Sui", name = "Sui" }, 22 | { id = "SuiSystem", name = "SuiSystem" }, 23 | ] 24 | 25 | [[move.package]] 26 | id = "DeepBook" 27 | source = { git = "https://github.com/MystenLabs/sui.git", rev = "9c04e1840eb5", subdir = "crates/sui-framework/packages/deepbook" } 28 | 29 | dependencies = [ 30 | { id = "MoveStdlib", name = "MoveStdlib" }, 31 | { id = "Sui", name = "Sui" }, 32 | ] 33 | 34 | [[move.package]] 35 | id = "MoveStdlib" 36 | source = { git = "https://github.com/MystenLabs/sui.git", rev = "9c04e1840eb5", subdir = "crates/sui-framework/packages/move-stdlib" } 37 | 38 | [[move.package]] 39 | id = "Sui" 40 | source = { git = "https://github.com/MystenLabs/sui.git", rev = "9c04e1840eb5", subdir = "crates/sui-framework/packages/sui-framework" } 41 | 42 | dependencies = [ 43 | { id = "MoveStdlib", name = "MoveStdlib" }, 44 | ] 45 | 46 | [[move.package]] 47 | id = "SuiSystem" 48 | source = { git = "https://github.com/MystenLabs/sui.git", rev = "9c04e1840eb5", subdir = "crates/sui-framework/packages/sui-system" } 49 | 50 | dependencies = [ 51 | { id = "MoveStdlib", name = "MoveStdlib" }, 52 | { id = "Sui", name = "Sui" }, 53 | ] 54 | 55 | [move.toolchain-version] 56 | compiler-version = "1.45.2" 57 | edition = "2024.beta" 58 | flavor = "sui" 59 | 60 | [env] 61 | 62 | [env.mainnet] 63 | chain-id = "35834a8a" 64 | original-published-id = "0xd26a14084af49f68d4612ef0815518c251f7a0459eaf2cbcb2757efafad442c5" 65 | latest-published-id = "0xd26a14084af49f68d4612ef0815518c251f7a0459eaf2cbcb2757efafad442c5" 66 | published-version = "1" 67 | -------------------------------------------------------------------------------- /src/ctfbook/chapter_2/task2/Move.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "task2" 3 | edition = "2024.beta" # edition = "legacy" to use legacy (pre-2024) Move 4 | # license = "" # e.g., "MIT", "GPL", "Apache 2.0" 5 | # authors = ["..."] # e.g., ["Joe Smith (joesmith@noemail.com)", "John Snow (johnsnow@noemail.com)"] 6 | 7 | [dependencies] 8 | 9 | # For remote import, use the `{ git = "...", subdir = "...", rev = "..." }`. 10 | # Revision can be a branch, a tag, and a commit hash. 11 | # MyRemotePackage = { git = "https://some.remote/host.git", subdir = "remote/path", rev = "main" } 12 | 13 | # For local dependencies use `local = path`. Path is relative to the package root 14 | # Local = { local = "../path/to" } 15 | 16 | # To resolve a version conflict and force a specific version for dependency 17 | # override use `override = true` 18 | # Override = { local = "../conflicting/version", override = true } 19 | 20 | [addresses] 21 | task2 = "0x0" 22 | 23 | # Named addresses will be accessible in Move as `@name`. They're also exported: 24 | # for example, `std = "0x1"` is exported by the Standard Library. 25 | # alice = "0xA11CE" 26 | 27 | [dev-dependencies] 28 | # The dev-dependencies section allows overriding dependencies for `--test` and 29 | # `--dev` modes. You can introduce test-only dependencies here. 30 | # Local = { local = "../path/to/dev-build" } 31 | 32 | [dev-addresses] 33 | # The dev-addresses section allows overwriting named addresses for the `--test` 34 | # and `--dev` modes. 35 | # alice = "0xB0B" 36 | 37 | -------------------------------------------------------------------------------- /src/ctfbook/chapter_2/task2/sources/task2.move: -------------------------------------------------------------------------------- 1 | module task2::task2 { 2 | use sui::event; 3 | use sui::random::{Random, generate_u64, new_generator}; 4 | use sui::clock::{Self, Clock}; 5 | use std::hash; 6 | use std::string::String; 7 | 8 | const E_WRONG_STAGE: u64 = 1; 9 | const E_COOLDOWN: u64 = 2; 10 | const E_MAX_ATTEMPTS_EXCEEDED: u64 = 3; 11 | 12 | public struct Challenge has key { 13 | id: UID, 14 | owner: address, 15 | secret_hash: vector, 16 | attempts: u64, 17 | max_attempts: u64, 18 | last_attempt_time: u64, 19 | is_solved: bool, 20 | stage: u64, 21 | target_score: u64, 22 | current_score: u64, 23 | bonus_multiplier: u64, 24 | guess_round: u64, 25 | round_hash: vector, 26 | seed: u64, 27 | } 28 | 29 | public struct FlagEvent has copy, drop { 30 | flag: vector, 31 | github_id: String 32 | } 33 | 34 | public struct ScoreEvent has copy, drop { 35 | score: u64, 36 | } 37 | 38 | fun init(ctx: &mut TxContext) { 39 | let secret = b"LetsMoveCTF"; 40 | let secret_hash = hash::sha3_256(secret); 41 | let challenge = Challenge { 42 | id: object::new(ctx), 43 | owner: tx_context::sender(ctx), 44 | secret_hash: secret_hash, 45 | attempts: 0, 46 | max_attempts: 50, 47 | last_attempt_time: 0, 48 | is_solved: false, 49 | stage: 1, 50 | target_score: 100, 51 | current_score: 0, 52 | bonus_multiplier: 0, 53 | guess_round: 1, 54 | round_hash: secret_hash, 55 | seed: 0, 56 | }; 57 | transfer::share_object(challenge); 58 | } 59 | 60 | public entry fun submit_score(challenge: &mut Challenge, score: u64, clock: &Clock) { 61 | assert!(challenge.stage == 1, E_WRONG_STAGE); 62 | let current_time = clock::timestamp_ms(clock); 63 | assert!(current_time >= challenge.last_attempt_time + 5000, E_COOLDOWN); 64 | challenge.attempts = challenge.attempts + 1; 65 | assert!(challenge.attempts < challenge.max_attempts, E_MAX_ATTEMPTS_EXCEEDED); 66 | let time_factor = (current_time - challenge.last_attempt_time) / 1000; 67 | let attempt_penalty = challenge.attempts * 2; 68 | let adjusted_score = if (score > attempt_penalty) { 69 | score - attempt_penalty 70 | } else { 71 | 0 72 | }; 73 | let final_score = adjusted_score * challenge.bonus_multiplier + time_factor; 74 | challenge.current_score = challenge.current_score + final_score; 75 | challenge.last_attempt_time = current_time; 76 | event::emit(ScoreEvent { score: challenge.current_score }); 77 | if (challenge.current_score >= challenge.target_score) { 78 | challenge.stage = 2; 79 | challenge.attempts = 0; 80 | }; 81 | } 82 | 83 | #[allow(lint(public_random))] 84 | public entry fun submit_guess(challenge: &mut Challenge, randomseed: &Random, guess: vector, clock: &Clock, ctx: &mut TxContext) { 85 | assert!(challenge.stage == 2, E_WRONG_STAGE); 86 | let mut random_gen = new_generator(randomseed, ctx); 87 | let seed = generate_u64(&mut random_gen); 88 | let current_time = clock::timestamp_ms(clock); 89 | assert!(current_time >= challenge.last_attempt_time + 5000, E_COOLDOWN); 90 | assert!(challenge.attempts < challenge.max_attempts, E_MAX_ATTEMPTS_EXCEEDED); 91 | challenge.attempts = challenge.attempts + 1; 92 | let mut guess_data = guess; 93 | vector::append(&mut guess_data, to_bytes(challenge.last_attempt_time)); 94 | vector::append(&mut guess_data, to_bytes(challenge.attempts)); 95 | let random = hash::sha3_256(guess_data); 96 | let prefix_length = challenge.guess_round * 2; 97 | if (compare_hash_prefix(&random, &challenge.round_hash, prefix_length)) { 98 | challenge.guess_round = challenge.guess_round + 1; 99 | let mut new_hash_data = random; 100 | vector::append(&mut new_hash_data, to_bytes(challenge.seed + challenge.guess_round)); 101 | challenge.round_hash = hash::sha3_256(new_hash_data); 102 | challenge.seed = seed; 103 | if (challenge.guess_round > 3) { 104 | challenge.is_solved = true; 105 | challenge.stage = 3; 106 | challenge.guess_round = 1; 107 | challenge.attempts = 0; 108 | }; 109 | }; 110 | challenge.last_attempt_time = current_time; 111 | } 112 | 113 | public entry fun get_flag(challenge: &mut Challenge,github_id: String, _: &mut TxContext) { 114 | assert!(challenge.stage == 3 && challenge.is_solved, E_WRONG_STAGE); 115 | reset_challenge(challenge); 116 | event::emit(FlagEvent { flag: b"flag{LetsMoveCTF_chapter_2}" ,github_id}); 117 | } 118 | 119 | public fun reset_challenge(challenge: &mut Challenge) { 120 | challenge.attempts = 0; 121 | challenge.last_attempt_time = 0; 122 | challenge.is_solved = false; 123 | challenge.stage = 1; 124 | challenge.current_score = 0; 125 | challenge.bonus_multiplier = 1; 126 | challenge.guess_round = 1; 127 | challenge.round_hash = challenge.secret_hash; 128 | } 129 | 130 | fun to_bytes(value: u64): vector { 131 | let mut bytes = vector::empty(); 132 | let mut i = 0; 133 | while (i < 8) { 134 | vector::push_back(&mut bytes, ((value >> (i * 8)) & 0xFF as u8)); 135 | i = i + 1; 136 | }; 137 | bytes 138 | } 139 | 140 | fun compare_hash_prefix(hash1: &vector, hash2: &vector, n: u64): bool { 141 | if (vector::length(hash1) < n || vector::length(hash2) < n) { 142 | return false 143 | }; 144 | let mut i = 0; 145 | while (i < n) { 146 | if (*vector::borrow(hash1, i) != *vector::borrow(hash2, i)) { 147 | return false 148 | }; 149 | i = i + 1; 150 | }; 151 | true 152 | } 153 | } -------------------------------------------------------------------------------- /src/ctfbook/chapter_2/task2/tests/task2_tests.move: -------------------------------------------------------------------------------- 1 | /* 2 | #[test_only] 3 | module task2::task2_tests; 4 | // uncomment this line to import the module 5 | // use task2::task2; 6 | 7 | const ENotImplemented: u64 = 0; 8 | 9 | #[test] 10 | fun test_task2() { 11 | // pass 12 | } 13 | 14 | #[test, expected_failure(abort_code = ::task2::task2_tests::ENotImplemented)] 15 | fun test_task2_fail() { 16 | abort ENotImplemented 17 | } 18 | */ 19 | -------------------------------------------------------------------------------- /src/ctfbook/chapter_3/intro.md: -------------------------------------------------------------------------------- 1 | # 第3章:泛型类型安全 2 | 3 | ## 引言 4 | 在区块链智能合约开发中,安全性是核心关注点。Sui Move 作为一种专为高性能区块链设计的语言,通过其强大的类型系统为开发者提供了安全保障。然而,某些特性如果使用不当,可能引入严重漏洞。本章将聚焦 **Sui Move 的泛型(Generics)**,深入探讨其在智能合约中的应用以及未检查泛型类型带来的安全风险。 5 | 6 | --- 7 | 8 | ## 1. Sui Move 中的泛型 9 | 10 | ### 1.1 什么是泛型? 11 | 泛型是编程语言中的一种机制,允许开发者编写可以处理多种类型的通用代码,从而提高代码的灵活性和复用性。在 Sui Move 中,泛型通过类型参数(如 ``)实现,类型参数可以在结构体、函数或模块定义中使用,指定在运行时替换的具体类型,类型参数通过尖括号 `` 定义。 12 | 13 | 例如,在合约中,`VoteToken` 结构体使用泛型 `` 表示投票凭证的类型: 14 | 15 | ``` 16 | public struct VoteToken has key, store { 17 | id: UID, 18 | amount: u64, 19 | } 20 | ``` 21 | 22 | - `` 是类型参数,可以代表任何类型(如 `OfficialToken` 或攻击者定义的 `FakeToken`)。 23 | - `phantom` 关键字表示 `` 仅用于类型标记,不影响 `VoteToken` 的存储结构(即不会在链上存储 `` 的实例)。 24 | - `has key, store` 表明 `VoteToken` 是一个链上对象,可以存储和转移。 25 | 26 | 泛型的核心优势是允许开发者编写通用的逻辑,而无需为每种类型重复实现代码。 27 | 28 | ### 1.2 泛型的语法与能力 29 | 在 Sui Move 中,泛型的使用受到类型能力的约束: 30 | - **结构体泛型**:如 `VoteToken`,类型参数 `` 通常需要满足特定能力(如 `drop` 或 `store`)。例如,`phantom` 参数通常要求 `drop` 能力。 31 | - **函数泛型**:函数可以声明泛型参数,限制调用时传入的类型。例如: 32 | 33 | ``` 34 | public entry fun register_voter(ctx: &mut TxContext) { 35 | let sender = tx_context::sender(ctx); 36 | let token = VoteToken { 37 | id: object::new(ctx), 38 | amount: 100, 39 | }; 40 | public_transfer(token, sender); 41 | } 42 | ``` 43 | 44 | - `` 允许函数为任意类型创建 `VoteToken`。 45 | - 调用者可以在调用时指定具体类型,如 `register_voter<0x1::Token::Token>`。 46 | 47 | - **多类型参数**:支持多个类型参数,顺序重要。例如: 48 | 49 | ``` 50 | public struct Pair { 51 | first: T, 52 | second: U, 53 | } 54 | public fun new_pair(first: T, second: U): Pair { 55 | Pair { first, second } 56 | } 57 | ``` 58 | 59 | - `` 的顺序决定类型签名,`Pair` 和 `Pair` 是不同类型,无法直接比较。 60 | 61 | - **幻影类型参数**:未在字段或方法中使用的类型参数,用于区分类型。例如: 62 | 63 | ``` 64 | public struct Coin { 65 | value: u64 66 | } 67 | 68 | public struct USD {} 69 | public struct EUR {} 70 | 71 | #[test] 72 | fun test_phantom_type() { 73 | let coin1: Coin = Coin { value: 10 }; 74 | let coin2: Coin = Coin { value: 20 }; 75 | 76 | // Unpacking is identical because the phantom type parameter is not used. 77 | let Coin { value: _ } = coin1; 78 | let Coin { value: _ } = coin2; 79 | } 80 | ``` 81 | 82 | - `Coin` 和 `Coin` 使用 `` 区分不同货币,防止混淆。 83 | 84 | - **能力约束**:类型参数可通过能力约束(如 `T: drop`)限制行为。例如: 85 | 86 | ``` 87 | public struct Droppable { 88 | value: T, 89 | } 90 | public struct CopyableDroppable { 91 | value: T, 92 | } 93 | ``` 94 | 95 | - `` 必须具有指定能力,否则编译器报错。例如,`NoAbilities` 结构体无能力,无法用于 `Droppable`. 96 | 97 | ### 1.3 泛型的应用场景 98 | 在 Sui Move 智能合约中,泛型广泛应用于: 99 | - **资源标识**:如 `VoteToken`,通过 `` 区分不同类型的凭证(如治理代币、投票权)。 100 | - **模块复用**:编写通用逻辑,适配多种类型。例如,`vote` 函数处理不同类型的 `VoteToken`。 101 | - **跨模块交互**:泛型支持模块与外部类型交互,增加灵活性。 102 | - **标准库**:如 `vector`(动态数组)和 `Option`(可选值),分别存储任意类型序列和表示可能缺失的值。 103 | - **抽象实现**:定义通用接口或行为,允许不同类型共享逻辑。 104 | 105 | 在样例合约中,泛型用于: 106 | - `VoteToken`:标记投票凭证的合法性。 107 | - `register_voter` 和 `vote`:支持不同类型凭证的分配和使用。 108 | 109 | 然而,这种灵活性可能被攻击者利用,导致安全漏洞。 110 | 111 | --- 112 | 113 | ## 2. 未检查泛型类型的安全风险 114 | 115 | ### 2.1 泛型漏洞的本质 116 | 在 Sui Move 中,泛型类型是由调用者在运行时提供的“用户输入”。如果合约未验证泛型类型 `` 是否符合预期,攻击者可以传入任意类型,导致以下安全问题: 117 | - **伪造凭证**:攻击者创建非法类型的对象(如 `VoteToken`)绕过权限检查。 118 | - **逻辑破坏**:非预期类型导致合约状态异常,影响核心功能(如投票结果错误)。 119 | - **资源滥用**:攻击者利用伪造类型创建无效资源,干扰合约运行或耗尽 Gas. 120 | 121 | 在 `VoteChain` 合约中,`register_voter` 和 `vote` 函数未检查 `` 类型,存在严重漏洞: 122 | 123 | ``` 124 | public entry fun register_voter(ctx: &mut TxContext) { 125 | let sender = tx_context::sender(ctx); 126 | let token = VoteToken { 127 | id: object::new(ctx), 128 | amount: 100, 129 | }; 130 | public_transfer(token, sender); 131 | } 132 | 133 | public entry fun vote(store: &mut VoteStore, token: &VoteToken, proposal_id: u64) { 134 | assert!(token.amount > 0, 1); 135 | assert!(object_table::contains(&store.proposals, proposal_id), 2); 136 | let proposal = object_table::borrow_mut(&mut store.proposals, proposal_id); 137 | proposal.votes = proposal.votes + token.amount; 138 | } 139 | ``` 140 | 141 | - **漏洞**:`register_voter` 允许任何 `` 创建 `VoteToken`,`vote` 未验证 `token` 的类型。 142 | - **后果**:攻击者可以伪造 `VoteToken` 并投票,非法影响提案结果. 143 | 144 | ### 2.2 漏洞案例分析 145 | 假设 `VoteChain` 是一个去中心化投票系统,设计意图是只有持有 `OfficialToken` 类型凭证的用户才能投票。然而,由于泛 type漏洞,攻击者可以: 146 | 1. 定义一个伪造类型 `FakeToken`: 147 | 148 | ``` 149 | module attacker::fake_token { 150 | public struct FakeToken has drop {} 151 | } 152 | ``` 153 | 2. 调用 `register_voter` 获取 `VoteToken`. 154 | 3. 使用伪造的 `VoteToken` 调用 `vote`,增加任意提案的票数。 155 | 156 | 这种攻击在区块链 CTF 中非常常见,因为泛型类型作为“隐形输入”,容易被开发者忽视。在现实世界的治理合约或 DeFi 协议中,类似漏洞可能导致: 157 | - 非法用户控制投票结果。 158 | - 资金分配错误。 159 | - 协议治理被恶意操纵。 160 | 161 | ### 2.3 漏洞的影响 162 | 未检查泛型类型的漏洞可能导致: 163 | - **完整性破坏**:投票系统等依赖权限控制的合约可能被非法操作。 164 | - **经济损失**:在 DeFi 或 DAO 中,伪造凭证可能导致资金被窃取或错误分配。 165 | - **拒绝服务**:攻击者可能创建大量伪造对象,增加 Gas 消耗或干扰正常功能。 166 | 167 | 在 `VoteChain` 的场景中,攻击者通过伪造 `VoteToken` 可以: 168 | - 使无效用户参与投票,破坏提案的公平性。 169 | - 操纵提案结果,影响治理决策。 170 | - 降低系统的可信度,损害用户信任。 171 | 172 | --- 173 | 174 | ## 3. 防御泛型漏洞 175 | 176 | ### 3.1 使用 `std::type_name` 进行类型检查 177 | Sui Move 提供了 `std::type_name` 模块,用于在运行时检查类型的名称。开发者可以在合约中添加断言,确保泛型类型 `` 符合预期: 178 | 179 | ``` 180 | use std::type_name; 181 | 182 | public entry fun register_voter(ctx: &mut TxContext) { 183 | assert!(type_name::get() == type_name::get(), 3); 184 | let sender = tx_context::sender(ctx); 185 | let token = VoteToken { 186 | id: object::new(ctx), 187 | amount: 100, 188 | }; 189 | public_transfer(token, sender); 190 | } 191 | ``` 192 | 193 | - `type_name::get()` 返回 `` 的完整类型名称(包括模块和结构体名称,如 `votechain::OfficialToken`)。 194 | - 断言确保 `` 是 `votechain::OfficialToken`,否则中止交易。 195 | - 错误码 `3`(建议定义为常量,如 `const E_INVALID_TYPE: u64 = 3;`)便于调试。 196 | 197 | 这种方法通过限制 `` 到白名单类型,有效防止伪造凭证。 198 | 199 | ### 3.2 设计安全合约的注意事项 200 | - **最小化泛型使用**:仅在必要时使用泛型,避免过度灵活性。 201 | - **显式验证**:对所有泛型参数进行运行时检查。 202 | - **错误处理**:定义清晰的错误码,便于调试和审计。 203 | - **测试覆盖**:编写测试用例,模拟攻击者伪造类型的情景。 204 | -------------------------------------------------------------------------------- /src/ctfbook/chapter_3/practice.md: -------------------------------------------------------------------------------- 1 | # 实践:利用泛型漏洞伪造投票凭证。 2 | 3 | ## 引言 4 | 5 | 以下是一个简单的投票系统的合约,每个地址可以领取100数量的coin然后进行投票,但是存在一些漏洞问题请尝试找出问题。 6 | 7 | ``` 8 | module votechain::vote { 9 | use sui::object_table::{Self, ObjectTable}; 10 | use sui::coin::{Self,TreasuryCap}; 11 | use sui::transfer::{public_transfer, share_object, public_freeze_object}; 12 | use std::string::String; 13 | use sui::table::{Self, Table}; 14 | 15 | public struct VOTE has drop {} 16 | 17 | public struct Votecap has key { 18 | id: UID, 19 | cap: TreasuryCap 20 | } 21 | 22 | public struct Mintlist has key { 23 | id: UID, 24 | mintlist: Table 25 | } 26 | 27 | public struct VoteToken has key, store { 28 | id: UID, 29 | amount: u64, 30 | } 31 | 32 | public struct VoteStore has key { 33 | id: UID, 34 | proposals: ObjectTable, 35 | } 36 | 37 | public struct Proposal has key, store { 38 | id: UID, 39 | votes: u64, 40 | } 41 | 42 | 43 | fun init(waitness: VOTE,ctx: &mut TxContext) { 44 | let name = std::string::utf8(b"letsctf"); 45 | 46 | let mintlist = Mintlist { id: object::new(ctx) , mintlist: table::new(ctx) }; 47 | let mut store = VoteStore { 48 | id: object::new(ctx), 49 | proposals: object_table::new(ctx), 50 | }; 51 | 52 | let proposal = Proposal { 53 | id: object::new(ctx), 54 | votes: 0, 55 | }; 56 | 57 | let (treasury_cap, meta) = coin::create_currency(waitness,6,b"VOTE", b"VOTE", b"", option::none(), ctx); 58 | 59 | let vote_cap = Votecap { id: object::new(ctx), cap: treasury_cap }; 60 | 61 | object_table::add(&mut store.proposals, name, proposal); 62 | public_freeze_object(meta); 63 | share_object(vote_cap); 64 | share_object(mintlist); 65 | share_object(store); 66 | } 67 | 68 | public entry fun mint( 69 | vote_cap: &mut Votecap, 70 | mint_list: &mut Mintlist, 71 | ctx: &mut TxContext 72 | ){ 73 | let addr = ctx.sender(); 74 | assert!(!table::contains(&mint_list.mintlist, addr), 1); 75 | let coin = coin::mint(&mut vote_cap.cap, 100, ctx); 76 | table::add(&mut mint_list.mintlist, addr, 100); 77 | public_transfer(coin, addr); 78 | } 79 | 80 | public entry fun register_voter(vote_coin: coin::Coin, ctx: &mut TxContext) { 81 | let amount = vote_coin.value(); 82 | assert!(amount == 100,1); 83 | let sender = tx_context::sender(ctx); 84 | let token = VoteToken { 85 | id: object::new(ctx), 86 | amount: 100, 87 | }; 88 | public_transfer(token, sender); 89 | public_transfer(vote_coin, @0x0); 90 | } 91 | 92 | public entry fun vote(token: &VoteToken, store: &mut VoteStore, proposal_name: String) { 93 | assert!(token.amount > 0, 1); 94 | assert!(object_table::contains(&store.proposals, proposal_name), 2); 95 | 96 | let proposal = object_table::borrow_mut(&mut store.proposals, proposal_name); 97 | proposal.votes = proposal.votes + token.amount; 98 | } 99 | } 100 | ``` 101 | 102 | -------------------------------------------------------------------------------- /src/ctfbook/chapter_3/type_safety.md: -------------------------------------------------------------------------------- 1 | # 了解泛型类型安全 2 | 3 | ## 引言 4 | 在区块链智能合约开发中,安全性是核心关注点。Sui Move 作为一种专为高性能区块链设计的语言,通过其强大的类型系统为开发者提供了安全保障。然而,某些特性如果使用不当,可能引入严重漏洞。本章将聚焦 **Sui Move 的泛型(Generics)**,深入探讨其在智能合约中的应用以及未检查泛型类型带来的安全风险。 5 | 6 | --- 7 | 8 | ## 1. Sui Move 中的泛型 9 | 10 | ### 1.1 什么是泛型? 11 | 泛型是编程语言中的一种机制,允许开发者编写可以处理多种类型的通用代码,从而提高代码的灵活性和复用性。在 Sui Move 中,泛型通过类型参数(如 ``)实现,类型参数可以在结构体、函数或模块定义中使用,指定在运行时替换的具体类型,类型参数通过尖括号 `` 定义。 12 | 13 | 例如,在合约中,`VoteToken` 结构体使用泛型 `` 表示投票凭证的类型: 14 | 15 | ``` 16 | public struct VoteToken has key, store { 17 | id: UID, 18 | amount: u64, 19 | } 20 | ``` 21 | 22 | - `` 是类型参数,可以代表任何类型(如 `OfficialToken` 或攻击者定义的 `FakeToken`)。 23 | - `phantom` 关键字表示 `` 仅用于类型标记,不影响 `VoteToken` 的存储结构(即不会在链上存储 `` 的实例)。 24 | - `has key, store` 表明 `VoteToken` 是一个链上对象,可以存储和转移。 25 | 26 | 泛型的核心优势是允许开发者编写通用的逻辑,而无需为每种类型重复实现代码。 27 | 28 | ### 1.2 泛型的语法与能力 29 | 在 Sui Move 中,泛型的使用受到类型能力的约束: 30 | - **结构体泛型**:如 `VoteToken`,类型参数 `` 通常需要满足特定能力(如 `drop` 或 `store`)。例如,`phantom` 参数通常要求 `drop` 能力。 31 | - **函数泛型**:函数可以声明泛型参数,限制调用时传入的类型。例如: 32 | 33 | ``` 34 | public entry fun register_voter(ctx: &mut TxContext) { 35 | let sender = tx_context::sender(ctx); 36 | let token = VoteToken { 37 | id: object::new(ctx), 38 | amount: 100, 39 | }; 40 | public_transfer(token, sender); 41 | } 42 | ``` 43 | 44 | - `` 允许函数为任意类型创建 `VoteToken`。 45 | - 调用者可以在调用时指定具体类型,如 `register_voter<0x1::Token::Token>`。 46 | 47 | - **多类型参数**:支持多个类型参数,顺序重要。例如: 48 | 49 | ``` 50 | public struct Pair { 51 | first: T, 52 | second: U, 53 | } 54 | public fun new_pair(first: T, second: U): Pair { 55 | Pair { first, second } 56 | } 57 | ``` 58 | 59 | - `` 的顺序决定类型签名,`Pair` 和 `Pair` 是不同类型,无法直接比较。 60 | 61 | - **幻影类型参数**:未在字段或方法中使用的类型参数,用于区分类型。例如: 62 | 63 | ``` 64 | public struct Coin { 65 | value: u64 66 | } 67 | 68 | public struct USD {} 69 | public struct EUR {} 70 | 71 | #[test] 72 | fun test_phantom_type() { 73 | let coin1: Coin = Coin { value: 10 }; 74 | let coin2: Coin = Coin { value: 20 }; 75 | 76 | // Unpacking is identical because the phantom type parameter is not used. 77 | let Coin { value: _ } = coin1; 78 | let Coin { value: _ } = coin2; 79 | } 80 | ``` 81 | 82 | - `Coin` 和 `Coin` 使用 `` 区分不同货币,防止混淆。 83 | 84 | - **能力约束**:类型参数可通过能力约束(如 `T: drop`)限制行为。例如: 85 | 86 | ``` 87 | public struct Droppable { 88 | value: T, 89 | } 90 | public struct CopyableDroppable { 91 | value: T, 92 | } 93 | ``` 94 | 95 | - `` 必须具有指定能力,否则编译器报错。例如,`NoAbilities` 结构体无能力,无法用于 `Droppable`. 96 | 97 | ### 1.3 泛型的应用场景 98 | 在 Sui Move 智能合约中,泛型广泛应用于: 99 | - **资源标识**:如 `VoteToken`,通过 `` 区分不同类型的凭证(如治理代币、投票权)。 100 | - **模块复用**:编写通用逻辑,适配多种类型。例如,`vote` 函数处理不同类型的 `VoteToken`。 101 | - **跨模块交互**:泛型支持模块与外部类型交互,增加灵活性。 102 | - **标准库**:如 `vector`(动态数组)和 `Option`(可选值),分别存储任意类型序列和表示可能缺失的值。 103 | - **抽象实现**:定义通用接口或行为,允许不同类型共享逻辑。 104 | 105 | 在样例合约中,泛型用于: 106 | - `VoteToken`:标记投票凭证的合法性。 107 | - `register_voter` 和 `vote`:支持不同类型凭证的分配和使用。 108 | 109 | 然而,这种灵活性可能被攻击者利用,导致安全漏洞。 110 | 111 | --- 112 | 113 | ## 2. 未检查泛型类型的安全风险 114 | 115 | ### 2.1 泛型漏洞的本质 116 | 在 Sui Move 中,泛型类型是由调用者在运行时提供的“用户输入”。如果合约未验证泛型类型 `` 是否符合预期,攻击者可以传入任意类型,导致以下安全问题: 117 | - **伪造凭证**:攻击者创建非法类型的对象(如 `VoteToken`)绕过权限检查。 118 | - **逻辑破坏**:非预期类型导致合约状态异常,影响核心功能(如投票结果错误)。 119 | - **资源滥用**:攻击者利用伪造类型创建无效资源,干扰合约运行或耗尽 Gas. 120 | 121 | 在 `VoteChain` 合约中,`register_voter` 和 `vote` 函数未检查 `` 类型,存在严重漏洞: 122 | 123 | ``` 124 | public entry fun register_voter(ctx: &mut TxContext) { 125 | let sender = tx_context::sender(ctx); 126 | let token = VoteToken { 127 | id: object::new(ctx), 128 | amount: 100, 129 | }; 130 | public_transfer(token, sender); 131 | } 132 | 133 | public entry fun vote(store: &mut VoteStore, token: &VoteToken, proposal_id: u64) { 134 | assert!(token.amount > 0, 1); 135 | assert!(object_table::contains(&store.proposals, proposal_id), 2); 136 | let proposal = object_table::borrow_mut(&mut store.proposals, proposal_id); 137 | proposal.votes = proposal.votes + token.amount; 138 | } 139 | ``` 140 | 141 | - **漏洞**:`register_voter` 允许任何 `` 创建 `VoteToken`,`vote` 未验证 `token` 的类型。 142 | - **后果**:攻击者可以伪造 `VoteToken` 并投票,非法影响提案结果. 143 | 144 | ### 2.2 漏洞案例分析 145 | 假设 `VoteChain` 是一个去中心化投票系统,设计意图是只有持有 `OfficialToken` 类型凭证的用户才能投票。然而,由于泛 type漏洞,攻击者可以: 146 | 1. 定义一个伪造类型 `FakeToken`: 147 | 148 | ``` 149 | module attacker::fake_token { 150 | public struct FakeToken has drop {} 151 | } 152 | ``` 153 | 2. 调用 `register_voter` 获取 `VoteToken`. 154 | 3. 使用伪造的 `VoteToken` 调用 `vote`,增加任意提案的票数。 155 | 156 | 这种攻击在区块链 CTF 中非常常见,因为泛型类型作为“隐形输入”,容易被开发者忽视。在现实世界的治理合约或 DeFi 协议中,类似漏洞可能导致: 157 | - 非法用户控制投票结果。 158 | - 资金分配错误。 159 | - 协议治理被恶意操纵。 160 | 161 | ### 2.3 漏洞的影响 162 | 未检查泛型类型的漏洞可能导致: 163 | - **完整性破坏**:投票系统等依赖权限控制的合约可能被非法操作。 164 | - **经济损失**:在 DeFi 或 DAO 中,伪造凭证可能导致资金被窃取或错误分配。 165 | - **拒绝服务**:攻击者可能创建大量伪造对象,增加 Gas 消耗或干扰正常功能。 166 | 167 | 在 `VoteChain` 的场景中,攻击者通过伪造 `VoteToken` 可以: 168 | - 使无效用户参与投票,破坏提案的公平性。 169 | - 操纵提案结果,影响治理决策。 170 | - 降低系统的可信度,损害用户信任。 171 | 172 | --- 173 | 174 | ## 3. 防御泛型漏洞 175 | 176 | ### 3.1 使用 `std::type_name` 进行类型检查 177 | Sui Move 提供了 `std::type_name` 模块,用于在运行时检查类型的名称。开发者可以在合约中添加断言,确保泛型类型 `` 符合预期: 178 | 179 | ``` 180 | use std::type_name; 181 | 182 | public entry fun register_voter(ctx: &mut TxContext) { 183 | assert!(type_name::get() == type_name::get(), 3); 184 | let sender = tx_context::sender(ctx); 185 | let token = VoteToken { 186 | id: object::new(ctx), 187 | amount: 100, 188 | }; 189 | public_transfer(token, sender); 190 | } 191 | ``` 192 | 193 | - `type_name::get()` 返回 `` 的完整类型名称(包括模块和结构体名称,如 `votechain::OfficialToken`)。 194 | - 断言确保 `` 是 `votechain::OfficialToken`,否则中止交易。 195 | - 错误码 `3`(建议定义为常量,如 `const E_INVALID_TYPE: u64 = 3;`)便于调试。 196 | 197 | 这种方法通过限制 `` 到白名单类型,有效防止伪造凭证。 198 | 199 | ### 3.2 设计安全合约的注意事项 200 | - **最小化泛型使用**:仅在必要时使用泛型,避免过度灵活性。 201 | - **显式验证**:对所有泛型参数进行运行时检查。 202 | - **错误处理**:定义清晰的错误码,便于调试和审计。 203 | - **测试覆盖**:编写测试用例,模拟攻击者伪造类型的情景。 204 | -------------------------------------------------------------------------------- /src/ctfbook/chapter_4/global_storage.md: -------------------------------------------------------------------------------- 1 | # 了解资源管理问题 2 | 3 | ## 引言 4 | 在区块链智能合约中,资源管理是确保系统安全的关键。Sui Move 使用对象(如代币、共享状态)来管理资源,通过 has key、Coin 和 share_object 提供强大的控制能力。然而,错误的资源管理可能导致漏洞,如代币重复使用。本章将介绍 Sui Move 的资源管理,以 VoteChain 投票系统为例,探讨未正确销毁投票代币的漏洞,学习如何利用和修复此类问题。 5 | 6 | 通过本章,你将: 7 | - 了解 Sui Move 的对象和代币管理。 8 | - 掌握资源管理漏洞,如未销毁代币导致重复投票。 9 | - 学会在 CTF 中发现和修复资源管理问题。 10 | 11 | 我们将分析 chapter_4::vote 中的重复投票漏洞,实践操纵投票结果,并在任务中修复代币管理。 12 | 13 | --- 14 | 15 | ## 1. Sui Move 的资源管理 16 | 17 | ### 1.1 对象与代币 18 | Sui Move 的资源以对象形式存储,具备唯一标识(UID): 19 | - 对象类型:需 has key(唯一性)和 has store(可存储)。 20 | - 所有权:通过 public_transfer 转移,share_object 公开共享。 21 | - 代币:Coin 表示代币,需销毁(coin::burn)或分割(coin::split)以防止重复使用。 22 | 23 | 在 VoteChain 中: 24 | - Coin 是投票代币,代表投票权。 25 | - VoteStore 是共享对象,存储提案票数。 26 | 27 | ### 1.2 资源管理漏洞 28 | 常见的资源管理漏洞包括: 29 | - 未销毁代币:投票后未销毁 Coin,允许重复使用。 30 | - 错误转移:对象转移到错误地址,丢失控制。 31 | - 共享对象滥用:未限制 share_object 的访问(第5章讨论)。 32 | 33 | 这些漏洞可能导致投票操纵或资源滥用。 34 | 35 | ### 1.3 漏洞案例 36 | 在 VoteChain 的 chapter_4::vote 中: 37 | - vote 函数记录投票者,但未销毁或分割 Coin。 38 | - 攻击者可多次调用 vote,用同一代币增加票数,操纵提案结果。 39 | - 危害:投票系统公平性丧失。 40 | 41 | --- 42 | 43 | ## 2. 防御资源管理漏洞 44 | 45 | ### 2.1 销毁或分割代币 46 | - 在 vote 中销毁 Coin(coin::burn)或分割已用部分(coin::split)。 47 | - 示例:coin::burn(vote_coin); 48 | 49 | ### 2.2 验证状态 50 | - 检查代币是否有效(如 vote_coin.value() > 0)。 51 | - 记录投票状态(如 voters 列表),防止重复投票。 52 | 53 | ### 2.3 最小化共享对象 54 | - 减少 share_object 的敏感数据,仅存储必要信息(如票数)。 55 | - 使用用户拥有的对象(如代币)管理权限。 56 | 57 | --- -------------------------------------------------------------------------------- /src/ctfbook/chapter_4/intro.md: -------------------------------------------------------------------------------- 1 | # 第4章:资源管理 2 | 3 | ## 引言 4 | 在区块链智能合约中,资源管理是确保系统安全的关键。Sui Move 使用对象(如代币、共享状态)来管理资源,通过 has key、Coin 和 share_object 提供强大的控制能力。然而,错误的资源管理可能导致漏洞,如代币重复使用。本章将介绍 Sui Move 的资源管理,以 VoteChain 投票系统为例,探讨未正确销毁投票代币的漏洞,学习如何利用和修复此类问题。 5 | 6 | 通过本章,你将: 7 | - 了解 Sui Move 的对象和代币管理。 8 | - 掌握资源管理漏洞,如未销毁代币导致重复投票。 9 | - 学会在 CTF 中发现和修复资源管理问题。 10 | 11 | 我们将分析 chapter_4::vote 中的重复投票漏洞,实践操纵投票结果,并在任务中修复代币管理。 12 | 13 | --- 14 | 15 | ## 1. Sui Move 的资源管理 16 | 17 | ### 1.1 对象与代币 18 | Sui Move 的资源以对象形式存储,具备唯一标识(UID): 19 | - 对象类型:需 has key(唯一性)和 has store(可存储)。 20 | - 所有权:通过 public_transfer 转移,share_object 公开共享。 21 | - 代币:Coin 表示代币,需销毁(coin::burn)或分割(coin::split)以防止重复使用。 22 | 23 | 在 VoteChain 中: 24 | - Coin 是投票代币,代表投票权。 25 | - VoteStore 是共享对象,存储提案票数。 26 | 27 | ### 1.2 资源管理漏洞 28 | 常见的资源管理漏洞包括: 29 | - 未销毁代币:投票后未销毁 Coin,允许重复使用。 30 | - 错误转移:对象转移到错误地址,丢失控制。 31 | - 共享对象滥用:未限制 share_object 的访问(第5章讨论)。 32 | 33 | 这些漏洞可能导致投票操纵或资源滥用。 34 | 35 | ### 1.3 漏洞案例 36 | 在 VoteChain 的 chapter_4::vote 中: 37 | - vote 函数记录投票者,但未销毁或分割 Coin。 38 | - 攻击者可多次调用 vote,用同一代币增加票数,操纵提案结果。 39 | - 危害:投票系统公平性丧失。 40 | 41 | --- 42 | 43 | ## 2. 防御资源管理漏洞 44 | 45 | ### 2.1 销毁或分割代币 46 | - 在 vote 中销毁 Coin(coin::burn)或分割已用部分(coin::split)。 47 | - 示例:coin::burn(vote_coin); 48 | 49 | ### 2.2 验证状态 50 | - 检查代币是否有效(如 vote_coin.value() > 0)。 51 | - 记录投票状态(如 voters 列表),防止重复投票。 52 | 53 | ### 2.3 最小化共享对象 54 | - 减少 share_object 的敏感数据,仅存储必要信息(如票数)。 55 | - 使用用户拥有的对象(如代币)管理权限。 56 | 57 | --- -------------------------------------------------------------------------------- /src/ctfbook/chapter_4/practice.md: -------------------------------------------------------------------------------- 1 | # 实践:分析利用资源管理漏洞 2 | 3 | ``` 4 | module chapter_4::vote { 5 | use sui::coin::{Self, Coin, TreasuryCap}; 6 | use sui::transfer::{public_transfer, share_object, public_freeze_object}; 7 | use sui::vec_map::{Self, VecMap}; 8 | use sui::object_table::{Self, ObjectTable}; 9 | use std::string::String; 10 | 11 | public struct VOTE has drop {} 12 | 13 | public struct Votecap has key { 14 | id: UID, 15 | cap: TreasuryCap, 16 | } 17 | 18 | public struct Mintlist has key { 19 | id: UID, 20 | mintlist: VecMap, 21 | } 22 | 23 | public struct VoteStore has key { 24 | id: UID, 25 | proposals: ObjectTable, 26 | voters: VecMap, 27 | } 28 | 29 | public struct Proposal has key, store { 30 | id: UID, 31 | votes: u64, 32 | } 33 | 34 | const E_INVALID_AMOUNT: u64 = 1; 35 | const E_INVALID_PROPOSAL: u64 = 2; 36 | const E_ALREADY_MINTED: u64 = 3; 37 | 38 | fun init(witness: VOTE, ctx: &mut TxContext) { 39 | let name = std::string::utf8(b"letsctf"); 40 | let (treasury_cap, meta) = coin::create_currency( 41 | witness, 42 | 6, 43 | b"VOTE", 44 | b"VOTE", 45 | b"", 46 | option::none(), 47 | ctx 48 | ); 49 | 50 | let vote_cap = Votecap { id: object::new(ctx), cap: treasury_cap }; 51 | let mintlist = Mintlist { id: object::new(ctx), mintlist: vec_map::empty() }; 52 | let mut store = VoteStore { 53 | id: object::new(ctx), 54 | proposals: object_table::new(ctx), 55 | voters: vec_map::empty(), 56 | }; 57 | 58 | let proposal = Proposal { 59 | id: object::new(ctx), 60 | votes: 0, 61 | }; 62 | 63 | object_table::add(&mut store.proposals, name, proposal); 64 | public_freeze_object(meta); 65 | share_object(vote_cap); 66 | share_object(mintlist); 67 | share_object(store); 68 | } 69 | 70 | public entry fun mint( 71 | vote_cap: &mut Votecap, 72 | mint_list: &mut Mintlist, 73 | ctx: &mut TxContext 74 | ) { 75 | let addr = tx_context::sender(ctx); 76 | assert!(!vec_map::contains(&mint_list.mintlist, &addr), E_ALREADY_MINTED); 77 | let coin = coin::mint(&mut vote_cap.cap, 100, ctx); 78 | vec_map::insert(&mut mint_list.mintlist, addr, 100); 79 | public_transfer(coin, addr); 80 | } 81 | 82 | public entry fun vote( 83 | store: &mut VoteStore, 84 | vote_coin: &Coin, 85 | proposal_name: String, 86 | ctx: &mut TxContext 87 | ) { 88 | assert!(vote_coin.value() > 0, E_INVALID_AMOUNT); 89 | assert!(object_table::contains(&store.proposals, proposal_name), E_INVALID_PROPOSAL); 90 | let sender = tx_context::sender(ctx); 91 | if (!vec_map::contains(&store.voters, &sender)) { 92 | vec_map::insert(&mut store.voters, sender, true); 93 | }; 94 | let proposal = object_table::borrow_mut(&mut store.proposals, proposal_name); 95 | proposal.votes = proposal.votes + vote_coin.value(); 96 | } 97 | } 98 | ``` 99 | 100 | -------------------------------------------------------------------------------- /src/ctfbook/chapter_5/access_control.md: -------------------------------------------------------------------------------- 1 | # 了解权限与访问控制 2 | 3 | ## 引言 4 | 权限控制是区块链智能合约安全的核心。Sui Move 使用 `TxContext` 的 `tx_context::sender` 获取调用者地址,验证用户权限。然而,遗漏身份验证可能导致未授权访问,如篡改他人提案。本章介绍 Sui Move 的权限控制机制,以 VoteChain 投票系统为例,分析未验证调用者身份的漏洞,学习如何利用和修复此类问题。 5 | 6 | 通过本章,你将: 7 | - 了解 Sui Move 的 `TxContext` 权限验证。 8 | - 掌握未授权访问漏洞,如提案篡改。 9 | - 学会在 CTF 中发现和修复权限控制问题。 10 | 11 | 我们将分析 `chapter_5::vote` 中的提案关闭漏洞,实践窃取投票权,并在任务中修复权限管理。 12 | 13 | --- 14 | 15 | ## 1. Sui Move 的权限控制 16 | 17 | ### 1.1 使用 TxContext 验证身份 18 | Sui Move 通过 `TxContext` 管理权限: 19 | - `tx_context::sender(ctx)`:返回交易调用者地址,用于验证身份。 20 | - 断言(如 `assert!`)确保调用者是授权用户,如提案创建者。 21 | 22 | ### 1.2 未授权访问的危害 23 | 常见漏洞包括: 24 | - 未验证调用者:允许任意用户执行敏感操作,如关闭提案。 25 | - 错误验证:逻辑错误导致权限绕过。 26 | - 共享对象滥用:未限制 `share_object` 访问。 27 | 28 | 这些漏洞可能导致资产窃取或系统篡改。 29 | 30 | ### 1.3 VoteChain 的提案关闭漏洞 31 | 在 `chapter_5::vote` 中: 32 | - `close_proposal` 未验证 `tx_context::sender` 是否为 `proposal.owner`。 33 | - 攻击者可关闭他人提案,窃取锁定的 `Coin`。 34 | - 危害:投票系统公平性受损,提案创建者损失投票权。 35 | 36 | --- 37 | 38 | ## 2. 防御权限控制漏洞 39 | 40 | ### 2.1 实现权限检查 41 | - 使用 `assert!` 检查 `tx_context::sender` 是否为预期用户。 42 | - 示例:assert!(proposal.owner == tx_context::sender(ctx), E_UNAUTHORIZED); 43 | 44 | ### 2.2 限制敏感操作 45 | - 限制函数访问,仅授权必要用户(如提案创建者)。 46 | - 避免公开敏感操作。 47 | 48 | ### 2.3 结合状态增强安全 49 | - 结合状态验证(如提案是否关闭)增强权限控制。 50 | 51 | --- 52 | 53 | ## 3. CTF 中的权限漏洞 54 | 55 | ### 3.1 CTF 中的权限漏洞 56 | 权限控制漏洞在 CTF 中常见: 57 | - 未授权访问:关闭他人提案,窃取资产。 58 | - 权限绕过:利用逻辑错误执行敏感操作。 59 | 60 | 在 VoteChain,攻击者可窃取提案的 `Coin`,破坏系统。 61 | 62 | ### 3.2 利用与修复权限漏洞 63 | 在 **实践**(`practice.md`),你将: 64 | - 利用 `close_proposal` 漏洞,关闭他人提案,窃取 `Coin`。 65 | - 分析漏洞危害,理解权限控制的重要性。 66 | 67 | 在 **任务5**(`task5.md`),你将: 68 | - 修复 `close_proposal`,添加调用者验证。 69 | - 测试修复,确保未授权访问失效。 70 | -------------------------------------------------------------------------------- /src/ctfbook/chapter_5/intro.md: -------------------------------------------------------------------------------- 1 | # 第5节:权限与访问控制 2 | -------------------------------------------------------------------------------- /src/ctfbook/chapter_5/practice.md: -------------------------------------------------------------------------------- 1 | # 实践:分析权限控制漏洞 2 | 3 | ``` 4 | module chapter_5::vote { 5 | use sui::coin::{Self, Coin, TreasuryCap}; 6 | use sui::transfer::{public_transfer, share_object, public_freeze_object}; 7 | use sui::vec_map::{Self, VecMap}; 8 | use sui::object_table::{Self, ObjectTable}; 9 | use std::string::String; 10 | use sui::balance::Balance; 11 | use sui::balance::zero; 12 | use sui::coin::into_balance; 13 | use sui::coin::from_balance; 14 | 15 | public struct VOTE has drop {} 16 | 17 | public struct Votecap has key { 18 | id: UID, 19 | cap: TreasuryCap, 20 | } 21 | 22 | public struct Mintlist has key { 23 | id: UID, 24 | mintlist: VecMap, 25 | } 26 | 27 | public struct VoteStore has key { 28 | id: UID, 29 | proposals: ObjectTable, 30 | voters: VecMap, 31 | } 32 | 33 | public struct Proposal has key, store { 34 | id: UID, 35 | owner: address, 36 | votes: u64, 37 | locked_tokens: Balance, 38 | closed: bool, 39 | } 40 | 41 | const E_INVALID_AMOUNT: u64 = 1; 42 | const E_INVALID_PROPOSAL: u64 = 2; 43 | const E_ALREADY_MINTED: u64 = 3; 44 | const E_ALREADY_VOTE: u64 = 4; 45 | const E_PROPOSAL_CLOSED: u64 = 5; 46 | 47 | fun init(witness: VOTE, ctx: &mut TxContext) { 48 | let (treasury_cap, meta) = coin::create_currency( 49 | witness, 6, b"VOTE", b"VOTE", b"", option::none(), ctx 50 | ); 51 | 52 | let vote_cap = Votecap { id: object::new(ctx), cap: treasury_cap }; 53 | let mintlist = Mintlist { id: object::new(ctx), mintlist: vec_map::empty() }; 54 | let store = VoteStore { 55 | id: object::new(ctx), 56 | proposals: object_table::new(ctx), 57 | voters: vec_map::empty(), 58 | }; 59 | 60 | public_freeze_object(meta); 61 | share_object(vote_cap); 62 | share_object(mintlist); 63 | share_object(store); 64 | } 65 | 66 | public entry fun mint( 67 | vote_cap: &mut Votecap, 68 | mint_list: &mut Mintlist, 69 | ctx: &mut TxContext 70 | ) { 71 | let addr = tx_context::sender(ctx); 72 | assert!(!vec_map::contains(&mint_list.mintlist, &addr), E_ALREADY_MINTED); 73 | let coin = coin::mint(&mut vote_cap.cap, 100, ctx); 74 | vec_map::insert(&mut mint_list.mintlist, addr, 100); 75 | public_transfer(coin, addr); 76 | } 77 | 78 | public entry fun create_proposal( 79 | store: &mut VoteStore, 80 | name: String, 81 | ctx: &mut TxContext 82 | ) { 83 | assert!(!object_table::contains(&store.proposals, name), E_INVALID_PROPOSAL); 84 | let proposal = Proposal { 85 | id: object::new(ctx), 86 | owner: tx_context::sender(ctx), 87 | votes: 0, 88 | locked_tokens: zero(), 89 | closed: false, 90 | }; 91 | object_table::add(&mut store.proposals, name, proposal); 92 | } 93 | 94 | public entry fun vote( 95 | store: &mut VoteStore, 96 | vote_coin: Coin, 97 | proposal_name: String, 98 | ctx: &mut TxContext 99 | ) { 100 | assert!(vote_coin.value() > 0, E_INVALID_AMOUNT); 101 | assert!(object_table::contains(&store.proposals, proposal_name), E_INVALID_PROPOSAL); 102 | let proposal = object_table::borrow_mut(&mut store.proposals, proposal_name); 103 | assert!(!proposal.closed, E_PROPOSAL_CLOSED); 104 | let sender = tx_context::sender(ctx); 105 | assert!(!vec_map::contains(&store.voters, &sender),E_ALREADY_VOTE); 106 | vec_map::insert(&mut store.voters, sender, vote_coin.value()); 107 | let amount = into_balance(vote_coin); 108 | proposal.votes = proposal.votes + amount.value(); 109 | proposal.locked_tokens.join(amount); 110 | } 111 | 112 | public entry fun close_proposal( 113 | store: &mut VoteStore, 114 | proposal_name: String, 115 | ctx: &mut TxContext 116 | ) { 117 | assert!(object_table::contains(&store.proposals, proposal_name), E_INVALID_PROPOSAL); 118 | let proposal = object_table::borrow_mut(&mut store.proposals, proposal_name); 119 | assert!(!proposal.closed, E_PROPOSAL_CLOSED); 120 | proposal.closed = true; 121 | let coin = from_balance(proposal.locked_tokens.withdraw_all(), ctx); 122 | public_transfer(coin, tx_context::sender(ctx)); 123 | } 124 | } 125 | ``` -------------------------------------------------------------------------------- /src/ctfbook/chapter_6/intro.md: -------------------------------------------------------------------------------- 1 | # 第6节:高级Move特性 2 | -------------------------------------------------------------------------------- /src/ctfbook/chapter_6/practice.md: -------------------------------------------------------------------------------- 1 | # 实践:分析状态管理逻辑漏洞 2 | 3 | ``` 4 | module chapter_6::vote { 5 | use sui::coin::{Self, Coin, TreasuryCap}; 6 | use sui::transfer::{public_transfer, share_object, public_freeze_object}; 7 | use sui::vec_map::{Self, VecMap}; 8 | use sui::object_table::{Self, ObjectTable}; 9 | use std::string::String; 10 | use sui::balance::Balance; 11 | 12 | public struct VOTE has drop {} 13 | 14 | public struct Votecap has key { 15 | id: UID, 16 | cap: TreasuryCap, 17 | } 18 | 19 | public struct Mintlist has key { 20 | id: UID, 21 | mintlist: VecMap, 22 | } 23 | 24 | public struct VoteStore has key { 25 | id: UID, 26 | proposals: ObjectTable, 27 | voters: VecMap, 28 | } 29 | 30 | public struct Proposal has key, store { 31 | id: UID, 32 | owner: address, 33 | votes: u64, 34 | locked_tokens: Balance, 35 | closed: bool, 36 | } 37 | 38 | const E_INVALID_AMOUNT: u64 = 1; 39 | const E_INVALID_PROPOSAL: u64 = 2; 40 | const E_ALREADY_MINTED: u64 = 3; 41 | const E_UNAUTHORIZED: u64 = 4; 42 | const E_PROPOSAL_CLOSED: u64 = 5; 43 | 44 | fun init(witness: VOTE, ctx: &mut TxContext) { 45 | let (treasury_cap, meta) = coin::create_currency( 46 | witness, 6, b"VOTE", b"VOTE", b"", option::none(), ctx 47 | ); 48 | 49 | let vote_cap = Votecap { id: object::new(ctx), cap: treasury_cap }; 50 | let mintlist = Mintlist { id: object::new(ctx), mintlist: vec_map::empty() }; 51 | let store = VoteStore { 52 | id: object::new(ctx), 53 | proposals: object_table::new(ctx), 54 | voters: vec_map::empty(), 55 | }; 56 | 57 | 58 | public_freeze_object(meta); 59 | share_object(vote_cap); 60 | share_object(mintlist); 61 | share_object(store); 62 | } 63 | 64 | public entry fun mint( 65 | vote_cap: &mut Votecap, 66 | mint_list: &mut Mintlist, 67 | ctx: &mut TxContext 68 | ) { 69 | let addr = tx_context::sender(ctx); 70 | assert!(!vec_map::contains(&mint_list.mintlist, &addr), E_ALREADY_MINTED); 71 | let coin = coin::mint(&mut vote_cap.cap, 100, ctx); 72 | vec_map::insert(&mut mint_list.mintlist, addr, 100); 73 | public_transfer(coin, addr); 74 | } 75 | 76 | public entry fun create_proposal( 77 | store: &mut VoteStore, 78 | name: String, 79 | ctx: &mut TxContext 80 | ) { 81 | assert!(!object_table::contains(&store.proposals, name), E_INVALID_PROPOSAL); 82 | let proposal = Proposal { 83 | id: object::new(ctx), 84 | owner: tx_context::sender(ctx), 85 | votes: 0, 86 | locked_tokens: zero(), 87 | closed: false, 88 | }; 89 | object_table::add(&mut store.proposals, name, proposal); 90 | } 91 | 92 | public entry fun vote( 93 | store: &mut VoteStore, 94 | vote_coin: Coin, 95 | proposal_name: String, 96 | ctx: &mut TxContext 97 | ) { 98 | assert!(vote_coin.value() > 0, E_INVALID_AMOUNT); 99 | assert!(object_table::contains(&store.proposals, proposal_name), E_INVALID_PROPOSAL); 100 | let proposal = object_table::borrow_mut(&mut store.proposals, proposal_name); 101 | let sender = tx_context::sender(ctx); 102 | let amount = coin::into_balance(vote_coin); 103 | if (vec_map::contains(&store.voters, &sender)) { 104 | let voter_amount = vec_map::get_mut(&mut store.voters, &sender); 105 | *voter_amount = *voter_amount + amount.value(); 106 | } else { 107 | vec_map::insert(&mut store.voters, sender, amount.value()); 108 | }; 109 | proposal.votes = proposal.votes + amount.value(); 110 | proposal.locked_tokens.join(amount); 111 | } 112 | 113 | public entry fun close_proposal( 114 | store: &mut VoteStore, 115 | proposal_name: String, 116 | ctx: &mut TxContext 117 | ) { 118 | assert!(object_table::contains(&store.proposals, proposal_name), E_INVALID_PROPOSAL); 119 | let proposal = object_table::borrow_mut(&mut store.proposals, proposal_name); 120 | let sender = tx_context::sender(ctx); 121 | assert!(sender == proposal.owner, E_UNAUTHORIZED); 122 | assert!(!proposal.closed, E_PROPOSAL_CLOSED); 123 | proposal.closed = true; 124 | let coin = coin::from_balance(proposal.locked_tokens.withdraw_all(), ctx); 125 | public_transfer(coin, sender); 126 | } 127 | } 128 | ``` -------------------------------------------------------------------------------- /src/ctfbook/chapter_6/state_management.md: -------------------------------------------------------------------------------- 1 | # 逻辑漏洞与状态管理 2 | 3 | ## 引言 4 | 逻辑漏洞源于智能合约的状态管理不当,可能导致意外行为,如重复投票。Sui Move 使用 `share_object` 和 `VecMap` 管理状态,但错误的状态更新或检查可能破坏系统。本章介绍 Sui Move 的状态管理机制,以 VoteChain 投票系统为例,分析未正确管理投票状态的逻辑漏洞,学习如何利用和修复此类问题。 5 | 6 | 通过本章,你将: 7 | - 了解 Sui Move 的状态管理(如 `VecMap` 和 `ObjectTable`)。 8 | - 掌握逻辑漏洞,如重复投票。 9 | - 学会在 CTF 中发现和修复状态管理问题。 10 | 11 | 我们将分析 `chapter_6::vote` 中的重复投票漏洞,实践操纵投票结果,并在任务中修复状态逻辑。 12 | 13 | --- 14 | 15 | ## 1. Sui Move 的状态管理 16 | 17 | ### 1.1 状态管理机制 18 | Sui Move 通过对象和集合管理状态: 19 | - `share_object`:存储共享状态,如 `VoteStore`。 20 | - `ObjectTable` 和 `VecMap`:管理提案和投票记录。 21 | - `TxContext`:提供交易上下文,确保状态一致性。 22 | 23 | ### 1.2 逻辑漏洞的来源 24 | 常见逻辑漏洞包括: 25 | - 未检查状态:如允许关闭的提案继续投票。 26 | - 错误更新状态:如重复累加投票记录。 27 | - 状态重置错误:如投票记录未正确清理。 28 | 29 | 这些漏洞可能导致投票操纵或系统失效。 30 | 31 | ### 1.3 VoteChain 的重复投票漏洞 32 | 在 `chapter_6::vote` 中: 33 | - `vote` 未检查 `proposal.closed`,允许关闭后投票。 34 | - `voters` 记录错误累加投票金额,未防止重复投票。 35 | - 危害:攻击者可反复投票,操纵提案结果。 36 | 37 | --- 38 | 39 | ## 2. 防御逻辑漏洞 40 | 41 | ### 2.1 验证状态一致性 42 | - 检查关键状态(如 `proposal.closed`)防止非法操作。 43 | - 示例:assert!(!proposal.closed, E_PROPOSAL_CLOSED); 44 | 45 | ### 2.2 正确更新状态 46 | - 使用 `VecMap` 或 `VecSet` 精确记录投票状态。 47 | - 示例:assert!(!vec_map::contains(&store.voters, &sender),E_ALREADY_VOTE);防止重复投票。 48 | 49 | ### 2.3 最小化状态暴露 50 | - 减少公开状态(如 `share_object` 的敏感数据)。 51 | - 定期清理无用状态(如关闭提案)。 52 | 53 | --- 54 | 55 | ## 3. CTF 中的逻辑漏洞 56 | 57 | ### 3.1 CTF 中的逻辑漏洞 58 | 逻辑漏洞在 CTF 中常见: 59 | - 重复操作:如重复投票或提取资产。 60 | - 状态绕过:如在非法状态下执行操作。 61 | 62 | 在 VoteChain,攻击者可通过重复投票操纵结果。 63 | 64 | ### 3.2 利用与修复逻辑漏洞 65 | 在 **实践**(`practice.md`),你将: 66 | - 利用 `vote` 漏洞,在提案关闭后重复投票。 67 | - 分析漏洞危害,理解状态管理。 68 | -------------------------------------------------------------------------------- /src/ctfbook/chapter_7/cross_module.md: -------------------------------------------------------------------------------- 1 | ### 跨合约安全与溢出漏洞 2 | 3 | #### 1. 课程概述 4 | 欢迎来到第 7 章!本章将深入探讨区块链智能合约中的“跨合约安全”问题,重点关注多个合约之间的交互如何引入漏洞。我们设计了一个投票系统(`vote::vote`)、一个数学工具模块(`math::math_utils`)和一个标志存储库(`vote::flag_vault`),并提供了一个求解脚本(`solve_chapter_7::solve_chapter_7`)作为参考。你的目标是通过累积 `VOTE` 代币、利用溢出漏洞触发权限,并获取隐藏的 `FLAG{CTF{Letsctf_chapter_7}}`。 5 | 6 | #### 2. 系统架构 7 | - **投票系统 (`vote::vote`)**: 8 | - 初始化时创建 `VotingSystem`、`Votecap` 和 `Mintlist`,分配 10,000 `VOTE` 作为初始余额。 9 | - `mint` 函数允许每个地址首次铸造 100 `VOTE`,限制重复铸造。 10 | - `vote` 函数累积用户的投票金额(`vote_list`),通过 `math_utils::calculate_weight` 计算权重,权重超过 1,000,000 时授权。 11 | - `withdraw` 函数允许提取累积的 `VOTE`。 12 | 13 | - **数学工具 (`math::math_utils`)**: 14 | - `calculate_weight` 函数检查输入是否超过 `mask = 0xff << 1` ,若超过返回 `999999999`,否则返回输入值。 15 | 16 | - **标志存储库 (`vote::flag_vault`)**: 17 | - `get_flag` 函数检查 `VotingSystem` 的 `is_authorized` 状态,授权后触发 `FlagEvent`。 18 | 19 | 20 | #### 3. 漏洞与挑战 21 | 本章设计了跨合约安全漏洞,供你探索和利用: 22 | - **有限铸造限制**:`mint` 限制每个地址一次 100 `VOTE`,但通过 `withdraw` 和重复投票可累积。 23 | - **溢出漏洞**:`math_utils::calculate_weight` 的阈值 `510` 未有效限制大输入,累积 `amount > 510` 可返回 `999999999 > 1000000`,触发授权。 24 | - **跨合约信任**:`vote` 依赖 `math_utils` 的计算,但未验证权重合理性,导致权限误判。 25 | - **目标**:累积足够 `VOTE`(至少 800),触发溢出,获取 `FLAG{CTF{Letsctf_chapter_7}}`。 26 | 27 | #### 4. 学习目标 28 | - 理解跨合约交互中的信任边界问题。 29 | - 掌握溢出漏洞的检测和利用技术。 30 | - 学习状态操纵(`withdraw` 和重复投票)绕过限制。 31 | - 实践 CTF 挑战,分析和修复智能合约漏洞。 32 | 33 | #### 5. 实践指南 34 | 1. **初始化**: 35 | - 调用 `vote::vote::mint` 获取 100 `VOTE`。 36 | - 注意:`mint` 仅对新地址生效。 37 | 38 | 2. **累积代币**: 39 | - 使用 `vote::vote::vote` 将 100 `VOTE` 加入 `vote_list`。 40 | - 调用 `vote::vote::withdraw` 提取当前 `vote_list` 值(100)。 41 | - 重复 `vote` 和 `withdraw`,每次金额翻倍(200, 400, 800...)。 42 | - 目标:累积 `amount > 510`。 43 | 44 | 3. **触发漏洞**: 45 | - 当 `total_amount > 510`,`math_utils::calculate_weight` 返回 `999999999 > 1000000`,设置 `is_authorized = true`。 46 | - 参考 `solve_chapter_7::solve_chapter_7` 实现: 47 | - `mint` 获取 100 `VOTE`。 48 | - `vote` 第一次,`vote_list = 100`。 49 | - `withdraw` 提取 100,`vote` 第二次,`vote_list = 200`。 50 | - `withdraw` 提取 200,`vote` 第三次,`vote_list = 400`。 51 | - `withdraw` 提取 400,`vote` 第四次,`vote_list = 800 > 510`。 52 | 53 | 4. **获取 FLAG**: 54 | - 调用 `vote::flag_vault::get_flag`,捕获 `FlagEvent`。 -------------------------------------------------------------------------------- /src/ctfbook/chapter_7/intro.md: -------------------------------------------------------------------------------- 1 | # 第7节:跨合约交互安全 2 | -------------------------------------------------------------------------------- /src/ctfbook/chapter_7/practice.md: -------------------------------------------------------------------------------- 1 | # 实践:利用漏洞造成溢出实现攻击 2 | 3 | 4 | ## 示例合约部分 5 | `math::math_utils` 合约 6 | ``` 7 | module math::math_utils { 8 | 9 | public fun calculate_weight( amount: u256): u256 { 10 | let (weight, overflow) = check(amount); 11 | if (overflow) { 12 | 999999999 13 | } else { 14 | weight 15 | } 16 | } 17 | 18 | 19 | fun check(n: u256): (u256, bool) { 20 | let num = 0xff << 1; 21 | if (n > num) { 22 | (0, true) 23 | } else { 24 | (n, false) 25 | } 26 | } 27 | } 28 | ``` 29 | 30 | `vote::vote` 合约 31 | ``` 32 | module vote::vote { 33 | use sui::coin::{Self, Coin}; 34 | use sui::transfer::share_object; 35 | use sui::balance::{Balance}; 36 | use math::math_utils; 37 | use sui::table; 38 | use sui::table::Table; 39 | use sui::transfer::public_freeze_object; 40 | use sui::coin::TreasuryCap; 41 | use sui::vec_map::{Self, VecMap}; 42 | 43 | public struct VOTE has drop {} 44 | 45 | public struct Votecap has key { 46 | id: UID, 47 | cap: TreasuryCap, 48 | } 49 | 50 | public struct Mintlist has key { 51 | id: UID, 52 | mintlist: VecMap, 53 | } 54 | 55 | public struct VotingSystem has key { 56 | id: UID, 57 | balance: Balance, 58 | vote_list: Table, 59 | is_authorized: bool, 60 | } 61 | 62 | fun init(witness: VOTE, ctx: &mut TxContext) { 63 | let (treasury_cap, meta) = coin::create_currency( 64 | witness, 6, b"VOTE", b"VOTE", b"", option::none(), ctx 65 | ); 66 | 67 | let mut vote_cap = Votecap { id: object::new(ctx), cap: treasury_cap }; 68 | let mintlist = Mintlist { id: object::new(ctx), mintlist: vec_map::empty() }; 69 | let coin = coin::mint(&mut vote_cap.cap, 10000, ctx); 70 | let balance = coin::into_balance(coin); 71 | 72 | let system = VotingSystem { 73 | id: object::new(ctx), 74 | balance: balance, 75 | vote_list: table::new(ctx), 76 | is_authorized: false, 77 | }; 78 | public_freeze_object(meta); 79 | share_object(system); 80 | share_object(vote_cap); 81 | share_object(mintlist); 82 | } 83 | 84 | public fun mint( 85 | vote_cap: &mut Votecap, 86 | mint_list: &mut Mintlist, 87 | ctx: &mut TxContext 88 | ): Coin { 89 | let addr = tx_context::sender(ctx); 90 | assert!(!vec_map::contains(&mint_list.mintlist, &addr), 1); 91 | let coin = coin::mint(&mut vote_cap.cap, 100, ctx); 92 | vec_map::insert(&mut mint_list.mintlist, addr, 100); 93 | coin 94 | } 95 | 96 | public entry fun vote( 97 | system: &mut VotingSystem, 98 | vote_coin: Coin, 99 | ctx: &mut TxContext 100 | ) { 101 | let sender = tx_context::sender(ctx); 102 | let amount = coin::value(&vote_coin); 103 | 104 | system.balance.join(coin::into_balance(vote_coin)); 105 | 106 | if (table::contains(&system.vote_list, sender)) { 107 | let current_amount = table::borrow_mut(&mut system.vote_list, sender); 108 | *current_amount = *current_amount + amount; 109 | } else { 110 | table::add(&mut system.vote_list, sender, amount); 111 | }; 112 | 113 | let total_amount = *table::borrow(&system.vote_list, sender); 114 | let weight = math_utils::calculate_weight(total_amount as u256); 115 | if (weight > 1000000) { 116 | system.is_authorized = true; 117 | } 118 | } 119 | 120 | public fun withdraw( 121 | system: &mut VotingSystem, 122 | ctx: &mut TxContext 123 | ) : Coin { 124 | let amount = table::borrow(& system.vote_list, ctx.sender()); 125 | let coin = coin::from_balance(system.balance.split(*amount), ctx); 126 | coin 127 | } 128 | 129 | public fun is_authorized(system: &VotingSystem): bool { 130 | system.is_authorized 131 | } 132 | } 133 | ``` 134 | 135 | `vote::flag_vault` 合约 136 | ``` 137 | module vote::flag_vault { 138 | use sui::event::emit; 139 | use vote::vote::{Self, VotingSystem}; 140 | use std::string::{Self, String}; 141 | 142 | public struct FlagEvent has copy, drop { 143 | flag: String, 144 | sender: address 145 | } 146 | 147 | public entry fun get_flag( 148 | system: &VotingSystem, 149 | ctx: &mut TxContext 150 | ) { 151 | assert!(vote::is_authorized(system), 1); 152 | let sender = tx_context::sender(ctx); 153 | emit(FlagEvent{flag: string::utf8(b"CTF{Letsctf_chapter_7}"),sender: sender}); 154 | } 155 | } 156 | ``` 157 | 158 | ## 解题合约部分 159 | 160 | ``` 161 | module solve_chapter_7::solve_chapter_7{ 162 | 163 | use vote::vote::{mint, vote, withdraw, Votecap, Mintlist, VotingSystem}; 164 | use vote::flag_vault::get_flag; 165 | 166 | public fun solve( 167 | vote_cap: &mut Votecap, 168 | mint_list: &mut Mintlist, 169 | system: &mut VotingSystem, 170 | ctx: &mut TxContext 171 | ){ 172 | let coin = mint(vote_cap, mint_list, ctx); 173 | //100 174 | vote(system,coin,ctx); 175 | let coin1 = withdraw(system, ctx); 176 | //200 177 | vote(system,coin1,ctx); 178 | let coin2 = withdraw(system, ctx); 179 | //400 180 | vote(system,coin2,ctx); 181 | let coin3 = withdraw(system, ctx); 182 | //800 183 | vote(system,coin3,ctx); 184 | get_flag(system,ctx); 185 | } 186 | } 187 | ``` -------------------------------------------------------------------------------- /src/ctfbook/chapter_8/intro.md: -------------------------------------------------------------------------------- 1 | # 第8节:综合CTF挑战 2 | -------------------------------------------------------------------------------- /src/ctfbook/chapter_8/practice.md: -------------------------------------------------------------------------------- 1 | # 实践:多步骤综合挑战 2 | 3 | ## 示例合约 4 | 5 | `chapter_8::auth` 代码 6 | ``` 7 | module chapter_8::auth { 8 | use sui::transfer::{share_object}; 9 | use sui::balance::{Self, Balance}; 10 | use sui::coin::{Self, Coin}; 11 | use chapter_8::vote::{VOTE, VotingSystem, is_authorized as vote_authorized}; 12 | use std::type_name::{Self, TypeName}; 13 | use sui::transfer::public_transfer; 14 | 15 | public struct Credential has key, store { 16 | id: UID, 17 | verified: bool, 18 | fee_paid: Balance, 19 | coin_type: TypeName, 20 | } 21 | 22 | public struct Auth has key { 23 | id: UID, 24 | authorized: bool, 25 | admin: address, 26 | min_fee: u64, 27 | vote_dependency: Option
, 28 | valid_coin_type: TypeName, 29 | } 30 | 31 | fun init(ctx: &mut TxContext) { 32 | let auth = Auth { 33 | id: object::new(ctx), 34 | admin: ctx.sender(), 35 | authorized: false, 36 | min_fee: 50, 37 | vote_dependency: option::none(), 38 | valid_coin_type: type_name::get>(), 39 | }; 40 | share_object(auth); 41 | } 42 | 43 | public fun register( 44 | auth: &mut Auth, 45 | payment: Coin, 46 | vote_system: &mut VotingSystem, 47 | ctx: &mut TxContext 48 | ): Credential { 49 | let amount = coin::value(&payment); 50 | 51 | assert!(amount >= auth.min_fee, 1); 52 | 53 | if (option::is_some(&auth.vote_dependency)) { 54 | let dep_addr = option::borrow(&auth.vote_dependency); 55 | assert!(vote_authorized(vote_system) && tx_context::sender(ctx) == *dep_addr, 2); 56 | }; 57 | 58 | let coin_type = type_name::get>(); 59 | let fee_balance = coin::into_balance(payment); 60 | let credential = Credential { 61 | id: object::new(ctx), 62 | verified: false, 63 | fee_paid: fee_balance, 64 | coin_type, 65 | }; 66 | 67 | credential 68 | } 69 | 70 | #[allow(lint(self_transfer))] 71 | public fun verify( 72 | auth: &mut Auth, 73 | credential: &mut Credential, 74 | vote_system: &mut VotingSystem, 75 | payment: Coin, 76 | ctx: &mut TxContext 77 | ) { 78 | assert!(balance::value(&credential.fee_paid) > auth.min_fee, 3); 79 | assert!(coin::value(&payment) >= auth.min_fee, 4); 80 | 81 | if (vote_authorized(vote_system) && type_name::get>() == credential.coin_type) { 82 | credential.verified = true; 83 | auth.authorized = true; 84 | balance::join(&mut credential.fee_paid, coin::into_balance(payment)); 85 | }else{ 86 | public_transfer(payment, ctx.sender()); 87 | } 88 | } 89 | 90 | public fun add_self_to_dependency(auth: &mut Auth, ctx: &mut TxContext) { 91 | if (option::is_none(&auth.vote_dependency)) { 92 | auth.vote_dependency = option::some(tx_context::sender(ctx)); 93 | } 94 | } 95 | 96 | public fun is_authorized(auth: &Auth): bool { 97 | auth.authorized 98 | } 99 | 100 | public fun is_verified(credential: &Credential): bool { 101 | credential.verified 102 | } 103 | } 104 | ``` 105 | 106 | `chapter_8::flag_vault` 代码` 107 | ``` 108 | module chapter_8::flag_vault { 109 | use sui::event::emit; 110 | use chapter_8::vote::VotingSystem; 111 | use chapter_8::auth::Auth; 112 | use std::string::{Self, String}; 113 | 114 | public struct FlagEvent has copy, drop { 115 | flag: String, 116 | sender: address, 117 | } 118 | 119 | public entry fun get_flag(system: &VotingSystem, auth: &Auth, ctx: &mut TxContext) { 120 | assert!(chapter_8::vote::is_authorized(system) && chapter_8::auth::is_authorized(auth), 1); 121 | let sender = tx_context::sender(ctx); 122 | emit(FlagEvent { flag: string::utf8(b"CTF{All_Chapters_Combined}"), sender }); 123 | } 124 | } 125 | ``` 126 | 127 | `chapter_8::math_utils` 代码 128 | ``` 129 | module chapter_8::math_utils { 130 | public fun calculate_weight(amount: u256): u256 { 131 | let (weight, overflow) = check(amount); 132 | if (overflow) { 133 | 999999999 134 | } else { 135 | weight 136 | } 137 | } 138 | 139 | fun check(n: u256): (u256, bool) { 140 | let mask = 0xff - 1; 141 | if (n > mask) { 142 | (0, true) 143 | } else { 144 | (n, false) 145 | } 146 | } 147 | } 148 | ``` 149 | 150 | `chapter_8::vote` 代码 151 | ``` 152 | module chapter_8::vote { 153 | use sui::coin::{Self, Coin, TreasuryCap}; 154 | use sui::transfer::{share_object, public_freeze_object}; 155 | use sui::balance::Balance; 156 | use sui::vec_map::{Self, VecMap}; 157 | use sui::table::{Self, Table}; 158 | use chapter_8::math_utils; 159 | 160 | public struct VOTE has drop {} 161 | 162 | public struct Votecap has key { 163 | id: UID, 164 | cap: TreasuryCap, 165 | } 166 | 167 | public struct Mintlist has key { 168 | id: UID, 169 | mintlist: VecMap, 170 | reset_count: u64, 171 | max_resets: u64, 172 | } 173 | 174 | public struct VotingSystem has key { 175 | id: UID, 176 | balance: Balance, 177 | vote_list: Table, 178 | is_authorized: bool, 179 | } 180 | 181 | fun init(witness: VOTE, ctx: &mut TxContext) { 182 | let (treasury_cap, meta) = coin::create_currency(witness, 6, b"VOTE", b"VOTE", b"", option::none(), ctx); 183 | let mut vote_cap = Votecap { id: object::new(ctx), cap: treasury_cap }; 184 | let mintlist = Mintlist { id: object::new(ctx), mintlist: vec_map::empty(), reset_count: 0, max_resets: 3 }; 185 | let coin = coin::mint(&mut vote_cap.cap, 10000, ctx); 186 | let balance = coin::into_balance(coin); 187 | let system = VotingSystem { id: object::new(ctx), balance, vote_list: table::new(ctx), is_authorized: false }; 188 | public_freeze_object(meta); 189 | share_object(vote_cap); 190 | share_object(mintlist); 191 | share_object(system); 192 | } 193 | 194 | public fun mint(vote_cap: &mut Votecap, mint_list: &mut Mintlist, ctx: &mut TxContext): Coin { 195 | let addr = tx_context::sender(ctx); 196 | assert!(!vec_map::contains(&mint_list.mintlist, &addr) || mint_list.reset_count < mint_list.max_resets, 1); 197 | if (vec_map::contains(&mint_list.mintlist, &addr)) { 198 | vec_map::remove(&mut mint_list.mintlist, &addr); 199 | mint_list.reset_count = mint_list.reset_count + 1; 200 | }; 201 | let coin = coin::mint(&mut vote_cap.cap, 100, ctx); 202 | vec_map::insert(&mut mint_list.mintlist, addr, 100); 203 | coin 204 | } 205 | 206 | public entry fun vote(system: &mut VotingSystem, vote_coin: Coin, ctx: &mut TxContext) { 207 | let sender = tx_context::sender(ctx); 208 | let amount = coin::value(&vote_coin); 209 | system.balance.join(coin::into_balance(vote_coin)); 210 | if (table::contains(&system.vote_list, sender)) { 211 | let current_amount = table::borrow_mut(&mut system.vote_list, sender); 212 | *current_amount = *current_amount + amount; 213 | } else { 214 | table::add(&mut system.vote_list, sender, amount); 215 | }; 216 | let total_amount = *table::borrow(&system.vote_list, sender); 217 | let weight = math_utils::calculate_weight(total_amount as u256); 218 | if (weight > 1000000) { 219 | system.is_authorized = true; 220 | } 221 | } 222 | 223 | public fun withdraw(system: &mut VotingSystem, ctx: &mut TxContext): Coin { 224 | let amount = *table::borrow(&system.vote_list, ctx.sender()); 225 | let coin = coin::from_balance(system.balance.split(amount), ctx); 226 | coin 227 | } 228 | 229 | public fun is_authorized(system: &VotingSystem): bool { 230 | system.is_authorized 231 | } 232 | } 233 | ``` 234 | 235 | 236 | ## 解题合约 237 | 238 | `solve_chapter_8::solve` 代码 239 | ``` 240 | module solve_chapter_8::solve{ 241 | 242 | use chapter_8::vote::{mint, vote, withdraw, Votecap, Mintlist, VotingSystem, VOTE}; 243 | use chapter_8::flag_vault::get_flag; 244 | use chapter_8::auth::{register, add_self_to_dependency , verify}; 245 | use chapter_8::auth::Auth; 246 | use sui::transfer::public_transfer; 247 | 248 | #[allow(lint(self_transfer))] 249 | public fun solve( 250 | vote_cap: &mut Votecap, 251 | mint_list: &mut Mintlist, 252 | system: &mut VotingSystem, 253 | auth: &mut Auth, 254 | ctx: &mut TxContext 255 | ){ 256 | let coin = mint(vote_cap, mint_list, ctx); 257 | //100 258 | vote(system,coin,ctx); 259 | let coin1 = withdraw(system, ctx); 260 | //200 261 | vote(system,coin1,ctx); 262 | let coin2 = withdraw(system, ctx); 263 | //400 264 | vote(system,coin2,ctx); 265 | let coin3 = withdraw(system, ctx); 266 | add_self_to_dependency(auth,ctx); 267 | 268 | let mut cet = register(auth,coin3, system,ctx); 269 | let coin4 = withdraw(system, ctx); 270 | verify(auth,&mut cet,system,coin4,ctx); 271 | 272 | get_flag(system, auth, ctx); 273 | 274 | public_transfer(cet, ctx.sender()); 275 | } 276 | } 277 | ``` -------------------------------------------------------------------------------- /src/ctfbook/chapter_8/vulnerability_analysis.md: -------------------------------------------------------------------------------- 1 | # 综合漏洞分析与解题策略 2 | 3 | ## 1. 课程概述 4 | 欢迎来到第 8 章!这是整个课程的终极综合 CTF 挑战,整合了前七章的核心知识点,包括代码审计、泛型类型安全、权限控制、逻辑漏洞和跨合约安全。我们设计了一个投票与认证系统,包含 `auth::auth`、`vote::vote` 和 `flag::flag_vault` 模块,引入了多种漏洞类型,供你分析和利用。你的任务是识别系统中的安全弱点,通过多步骤攻击路径,利用未授权写入和溢出漏洞,最终获取隐藏的 `FLAG{CTF{All_Chapters_Combined}}`。 5 | 6 | ## 2. 系统架构 7 | - **认证系统 (`auth::auth`)**: 8 | - 管理用户凭证 (`Credential`),要求支付费用并验证 `Coin` 类型,依赖投票系统的授权状态。 9 | - **投票系统 (`vote::vote`)**: 10 | - 管理 `VOTE` 代币的铸造、投票和状态,包含重置机制和权重计算。 11 | - **标志存储库 (`flag::flag_vault`)**: 12 | - 存储 `FLAG`,需认证系统和投票系统双重授权。 13 | - **数学工具 (`math::math_utils`)**: 14 | - 提供权重计算,包含溢出漏洞。 15 | 16 | ## 3. 学习目标 17 | - 掌握综合漏洞分析,涵盖权限控制、逻辑漏洞、资源管理和跨合约安全。 18 | - 学习利用未授权写入和溢出漏洞设计多步骤攻击路径。 19 | - 理解泛型类型安全和状态依赖的复杂性。 20 | - 实践 CTF 挑战,修复智能合约漏洞。 21 | 22 | ## 4. 挑战预览 23 | 本章设计了多重漏洞,需结合前七章技巧解决: 24 | - **权限控制漏洞**:`add_self_to_dependency` 允许未授权将调用者地址加入 `vote_dependency`,绕过正常验证。 25 | - **逻辑漏洞**:`vote::mint` 的 `reset_count` 机制允许多次铸造,`auth::verify` 依赖弱条件。 26 | - **泛型类型安全漏洞**:`auth::register` 接受任意 `T` 类型 `Coin`,`coin_type` 验证不足。 27 | - **跨合约安全漏洞**:`auth` 依赖 `vote` 的授权状态,易受溢出影响。 28 | - **目标**:累积足够 `VOTE`(至少 254),触发溢出并利用未授权写入,获取 `FLAG{CTF{All_Chapters_Combined}}`。 29 | 30 | ## 5. 实践指南 31 | 1. **初始化**: 32 | - 调用 `vote::vote::mint` 获取 100 `VOTE`。 33 | - 注意:`mint` 限制单地址 3 次重置。 34 | 35 | 2. **累积代币**: 36 | - 使用 `vote::vote::vote` 累积 `vote_list` 值。 37 | - 调用 `vote::vote::withdraw` 提取,重复至 `amount > 253`。 38 | - 提示:3 次重置可得 400 `VOTE`。 39 | 40 | 3. **触发溢出**: 41 | - 当 `total_amount > 253`(`mask = 0xff - 1`),`math_utils::calculate_weight` 返回 `0`,触发 `vote.is_authorized = true`。 42 | 43 | 4. **未授权写入**: 44 | - 调用 `auth::add_self_to_dependency`,将自己的地址加入 `vote_dependency`。 45 | 46 | 5. **注册凭证**: 47 | - 调用 `auth::register` with 50 `VOTE`,创建 `Credential`。 48 | 49 | 6. **验证凭证**: 50 | - 调用 `auth::verify` with `Credential` 和 50 `VOTE`,依赖 `vote` 状态。 51 | 52 | 7. **获取 FLAG**: 53 | - 调用 `flag::flag_vault::get_flag`。 54 | - 捕获 `FlagEvent` 获取 `FLAG{CTF{All_Chapters_Combined}}`。 55 | 56 | ## 6. 分析与思考 57 | - **漏洞点**:`add_self_to_dependency` 的未授权写入绕过 `vote_dependency` 验证,`math_utils` 的 `mask = 253` 易被溢出。 58 | - **安全隐患**:`auth` 信任 `vote` 状态,`register` 接受任意 `Coin` 类型。 59 | - **修复建议**:限制 `add_self_to_dependency` 调用,强化 `coin_type` 验证,清理 `fee_paid`。 60 | -------------------------------------------------------------------------------- /src/ctfbook/preface/goals.md: -------------------------------------------------------------------------------- 1 | # 课程目标与概述 2 | 3 | ### 课程目标 4 | 欢迎参加 **Move CTF 挑战课程**!本课程旨在帮助你掌握 Move 编程语言在 CTF(Capture The Flag)比赛中的应用,培养分析代码和解决安全挑战的能力。通过八节课的学习,你将能够: 5 | - 熟练分析 Move 语言代码,识别常见漏洞和逻辑错误。 6 | - 掌握 Move 在 CTF 中的典型题型,如整数溢出、资源管理和权限控制。 7 | - 具备独立解决基础至中级 Move CTF 题目的能力。 8 | - 为参加 Move 相关的 CTF 比赛做好准备,树立信心。 9 | 10 | 无论你是区块链开发者、安全研究者还是 CTF 爱好者,本课程都将为你提供独特的视角,探索 Move 语言的安全特性及其在竞赛中的潜力。 11 | 12 | ### 课程概述 13 | - **课程结构**:共八章节,从零开始进阶学习Move CTF。 14 | - **学习路径**: 15 | - 从 CTF 简介和基础代码审计开始,逐步深入到高级漏洞和综合挑战。 16 | - 每节课聚焦一个主题,配备一道 CTF 题目,循序渐进提升难度。 17 | - **前提假设**:你已通过基础 Move 语言学习( [HOH社区Move共学](https://github.com/move-cn/letsmove)),熟悉基本语法、模块和资源概念。 18 | - **预期成果**:完成课程后,你将能独立分析 Move 智能合约代码,解决 CTF 挑战,并具备参加真实比赛的基本策略。 19 | 20 | 本课程结合理论与实践,鼓励动手操作和互动讨论。准备好迎接挑战了吗?让我们一起开启 Move CTF 的学习之旅! -------------------------------------------------------------------------------- /src/ctfbook/preface/intro.md: -------------------------------------------------------------------------------- 1 | # 前言 2 | -------------------------------------------------------------------------------- /src/ctfbook/preface/prerequisites.assets/image-20250325171129301.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoh-zone/lets-ctf/d477c5b23ebce7fe235c9de154c6804006f6ed94/src/ctfbook/preface/prerequisites.assets/image-20250325171129301.png -------------------------------------------------------------------------------- /src/ctfbook/preface/prerequisites.md: -------------------------------------------------------------------------------- 1 | # 预备知识与工具安装 2 | 3 | ## 预备知识 4 | 为了顺利完成本课程,你需要具备以下基础知识: 5 | - **Move 语言基础**: 6 | - 熟悉变量声明、基本数据类型(u8、u64、address 等)和控制流(if、while)。 7 | - 理解模块(module)和资源(struct)的概念。 8 | - 能够编写和运行简单的 Move 程序(如 Hello World)。 9 | - 推荐资源:[HOH社区Move共学](https://github.com/move-cn/letsmove) 或 [Sui Move Book](https://move.sui-book.com/index.html)。 10 | - **区块链基础**: 11 | - 了解智能合约的基本概念(如存储、交易)。 12 | - 对 Sui 区块链平台有初步认识。 13 | - **CTF 基础**(可选): 14 | - 知道 CTF 比赛的基本形式将有助于更快上手。 15 | 16 | 如果以上知识点有欠缺,建议先完成基础学习再加入课程。本课程将直接聚焦 Move 在 CTF 中的应用,跳过语言基础教学。 17 | 18 | ## 工具安装 19 | 以下是你需要安装的工具,确保在第一节课前配置好开发环境: 20 | 1. **Sui CLI**: 21 | - 用于编译、运行和调试 Move 代码。 22 | - 安装步骤: 23 | - Sui:参考 [Sui CLI 安装指南](https://docs.sui.io/build/cli-client)。 24 | - 验证:运行 `sui -V` 检查安装成功。 25 | 26 | 2. **VS Code + Move 插件**: 27 | - 提供代码高亮和语法检查。 28 | 29 | - 安装步骤: 30 | 1. 下载 [VS Code](https://code.visualstudio.com/)。 31 | 2. 在扩展市场搜索 `Move` 或 `Sui Move`,安装相关插件。 32 | 33 | ![image-20250325171039560](./prerequisites.assets/image-20250325171129301.png) 34 | 35 | `Move` 和 `Move syntax` 插件为提供代码高亮和语法检查,`Move Formatter Developer Preview`插件提供代码格式化。 36 | 37 | 38 | 39 | ### 环境验证 40 | - 运行以下命令测试环境: 41 | ```bash 42 | sui move new && cd && sui move build 43 | ``` 44 | - 如果编译成功,说明环境配置正确。 45 | 46 | 准备好这些工具后,你就可以无缝进入课程实践环节。遇到安装问题?请提前联系课程团队或查阅相关文档。 47 | 48 | -------------------------------------------------------------------------------- /src/week1/.gitignore: -------------------------------------------------------------------------------- 1 | build/* 2 | -------------------------------------------------------------------------------- /src/week1/Move.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "week1" 3 | edition = "2024.beta" # edition = "legacy" to use legacy (pre-2024) Move 4 | # license = "" # e.g., "MIT", "GPL", "Apache 2.0" 5 | # authors = ["..."] # e.g., ["Joe Smith (joesmith@noemail.com)", "John Snow (johnsnow@noemail.com)"] 6 | 7 | [dependencies] 8 | 9 | # For remote import, use the `{ git = "...", subdir = "...", rev = "..." }`. 10 | # Revision can be a branch, a tag, and a commit hash. 11 | # MyRemotePackage = { git = "https://some.remote/host.git", subdir = "remote/path", rev = "main" } 12 | 13 | # For local dependencies use `local = path`. Path is relative to the package root 14 | # Local = { local = "../path/to" } 15 | 16 | # To resolve a version conflict and force a specific version for dependency 17 | # override use `override = true` 18 | # Override = { local = "../conflicting/version", override = true } 19 | 20 | [addresses] 21 | week1 = "0x0" 22 | 23 | # Named addresses will be accessible in Move as `@name`. They're also exported: 24 | # for example, `std = "0x1"` is exported by the Standard Library. 25 | # alice = "0xA11CE" 26 | 27 | [dev-dependencies] 28 | # The dev-dependencies section allows overriding dependencies for `--test` and 29 | # `--dev` modes. You can introduce test-only dependencies here. 30 | # Local = { local = "../path/to/dev-build" } 31 | 32 | [dev-addresses] 33 | # The dev-addresses section allows overwriting named addresses for the `--test` 34 | # and `--dev` modes. 35 | # alice = "0xB0B" 36 | 37 | -------------------------------------------------------------------------------- /src/week1/sources/week1.move: -------------------------------------------------------------------------------- 1 | module week1::challenge { 2 | use std::bcs; 3 | use std::hash::sha3_256; 4 | use std::string::{Self, String}; 5 | use sui::event; 6 | use sui::random::{Self, Random}; 7 | use sui::transfer::share_object; 8 | 9 | const EINVALID_GUESS_HASH: u64 = 0; 10 | const EINVALID_HASH: u64 = 1; 11 | const EINVALID_MAGIC: u64 = 2; 12 | const EINVALID_SEED: u64 = 3; 13 | const EINVALID_SCORE: u64 = 4; 14 | 15 | 16 | public struct Challenge has key { 17 | id: UID, 18 | secret: String, 19 | current_score: u64, 20 | round_hash: vector, 21 | finish: u64, 22 | } 23 | 24 | public struct FlagEvent has copy, drop { 25 | sender: address, 26 | flag: String, 27 | github_id: String, 28 | success: bool, 29 | rank: u64, 30 | } 31 | 32 | fun init(ctx: &mut TxContext) { 33 | let secret = b"Letsmovectf_week1"; 34 | let secret_hash = sha3_256(secret); 35 | let challenge = Challenge { 36 | id: object::new(ctx), 37 | secret: string::utf8(secret), 38 | current_score: 0, 39 | round_hash: secret_hash, 40 | finish: 0 41 | }; 42 | share_object(challenge); 43 | } 44 | 45 | #[allow(lint(public_random))] 46 | public entry fun get_flag( 47 | score: u64, 48 | guess: vector, 49 | hash_input: vector, 50 | github_id: String, 51 | magic_number: u64, 52 | seed: u64, 53 | challenge: &mut Challenge, 54 | rand: &Random, 55 | ctx: &mut TxContext 56 | ) { 57 | let secret_hash = sha3_256(*string::as_bytes(&challenge.secret)); 58 | let expected_score = (((*vector::borrow(&secret_hash, 0) as u64) << 24) | 59 | ((*vector::borrow(&secret_hash, 1) as u64) << 16) | 60 | ((*vector::borrow(&secret_hash, 2) as u64) << 8) | 61 | (*vector::borrow(&secret_hash, 3) as u64)); 62 | assert!(score == expected_score, EINVALID_SCORE); 63 | challenge.current_score = score; 64 | 65 | let mut guess_data = guess; 66 | vector::append(&mut guess_data, *string::as_bytes(&challenge.secret)); 67 | let random = sha3_256(guess_data); 68 | let prefix_length = 2; 69 | assert!(compare_hash_prefix(&random, &challenge.round_hash, prefix_length), EINVALID_GUESS_HASH); 70 | 71 | let mut bcs_input = bcs::to_bytes(&challenge.secret); 72 | vector::append(&mut bcs_input, *string::as_bytes(&github_id)); 73 | let expected_hash = sha3_256(bcs_input); 74 | assert!(hash_input == expected_hash, EINVALID_HASH); 75 | let expected_magic = challenge.current_score % 1000 + seed; 76 | assert!(magic_number == expected_magic, EINVALID_MAGIC); 77 | let secret_bytes = *string::as_bytes(&challenge.secret); 78 | let secret_len = vector::length(&secret_bytes); 79 | assert!(seed == secret_len * 2, EINVALID_SEED); 80 | 81 | challenge.secret = getRandomString(rand, ctx); 82 | challenge.round_hash = sha3_256(*string::as_bytes(&challenge.secret)); 83 | challenge.current_score = 0; 84 | challenge.finish = challenge.finish + 1; 85 | 86 | event::emit(FlagEvent { 87 | sender: tx_context::sender(ctx), 88 | flag: string::utf8(b"CTF{Letsmovectf_week1}"), 89 | github_id, 90 | success: true, 91 | rank: challenge.finish 92 | }); 93 | } 94 | 95 | fun getRandomString(rand: &Random, ctx: &mut TxContext): String { 96 | let mut gen = random::new_generator(rand, ctx); 97 | let mut str_len = random::generate_u8_in_range(&mut gen, 4, 32); 98 | let mut rand_vec: vector = b""; 99 | while (str_len != 0) { 100 | let rand_num = random::generate_u8_in_range(&mut gen, 34, 126); 101 | vector::push_back(&mut rand_vec, rand_num); 102 | str_len = str_len - 1; 103 | }; 104 | string::utf8(rand_vec) 105 | } 106 | 107 | fun compare_hash_prefix(hash1: &vector, hash2: &vector, n: u64): bool { 108 | if (vector::length(hash1) < n || vector::length(hash2) < n) { 109 | return false 110 | }; 111 | let mut i = 0; 112 | while (i < n) { 113 | if (*vector::borrow(hash1, i) != *vector::borrow(hash2, i)) { 114 | return false 115 | }; 116 | i = i + 1; 117 | }; 118 | true 119 | } 120 | } -------------------------------------------------------------------------------- /src/week1/tests/week1_tests.move: -------------------------------------------------------------------------------- 1 | /* 2 | #[test_only] 3 | module week1::week1_tests; 4 | // uncomment this line to import the module 5 | // use week1::week1; 6 | 7 | const ENotImplemented: u64 = 0; 8 | 9 | #[test] 10 | fun test_week1() { 11 | // pass 12 | } 13 | 14 | #[test, expected_failure(abort_code = ::week1::week1_tests::ENotImplemented)] 15 | fun test_week1_fail() { 16 | abort ENotImplemented 17 | } 18 | */ 19 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "builds": [ 4 | { 5 | "src": "package.json", 6 | "use": "@vercel/static-build", 7 | "config": { 8 | "distDir": "book" 9 | } 10 | } 11 | ], 12 | "routes": [ 13 | { 14 | "src": "/(.*)", 15 | "dest": "/$1" 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /writeup/justctf2024/README.md: -------------------------------------------------------------------------------- 1 | 2 | # JustCTF 2024 Writeup 3 | 4 | ## 题目 5 | https://2024.justctf.team/challenges 6 | 有三道sui move题目 7 | 8 | - TOS 9 | ``` 10 | The Otter Scrolls 11 | Points: 246 12 | zwalaczkonia12 13 | BLOCKCHAIN 14 | Behold the ancient Spellbook, a tome of arcane wisdom where spells cast by mystical Otters weave the threads of fate. Embark on this enchanted journey where the secrets of the blockchain will be revealed. Brave adventurers, your quest awaits; may your courage be as boundless as the magic that guides you. 15 | 16 | Challenge created by embe221ed & Darkstar49 from OtterSec 17 | 18 | nc tos.nc.jctf.pro 31337 19 | https://s3.cdn.justctf.team/a3cc5591-ad0a-47e5-bce0-78ff9bb7d2f3/tos_docker.tar.gz 20 | ``` 21 | 22 | - DB 23 | ``` 24 | Dark BrOTTERhood 25 | Points: 275 26 | CyKor 27 | BLOCKCHAIN 28 | In the shadowed corners of the Dark Brotterhood's secrets, lies a tavern where valiant Otters barter for swords and shields. Here, amidst whispers of hidden bounties, adventurers find the means to battle fearsome monsters for rich rewards. Join this clandestine fellowship, where the blockchain holds mysteries to uncover. Otters of Valor, your destiny calls; may your path be lined with both honor and gold. 29 | 30 | Challenge created by embe221ed & Darkstar49 from OtterSec 31 | 32 | nc db.nc.jctf.pro 31337 33 | https://s3.cdn.justctf.team/42840bf9-5734-42c6-9463-2b61238148e8/db_docker.tar.gz 34 | ``` 35 | 36 | 37 | - WOO 38 | ``` 39 | World of Ottercraft 40 | Points: 271 41 | zwalaczkonia12 42 | BLOCKCHAIN 43 | Welcome to the World of Ottercraft, where otters rule the blockchain! In this challenge, you'll dive deep into the blockchain to grab the mythical Otter Stone! Beware of the powerful monsters that will try to block your path! Can you outsmart them and fish out the Otter Stone, or will you just end up swimming in circles? 44 | 45 | Challenge created by embe221ed & Darkstar49 from OtterSec 46 | 47 | nc woo.nc.jctf.pro 31337 48 | https://s3.cdn.justctf.team/a951edfb-bd5f-40a0-b334-ad650d889ac3/woo_docker.tar.gz 49 | ``` 50 | 51 | 52 | ## 部署题目 53 | 54 | 比赛结束后服务器已经关了,可以自己把比赛环境搭起来. 55 | 实测至少需要买一台2核4G 、硬盘25G的vps 56 | 57 | 1.安装docker和compose 58 | 59 | https://docs.docker.com/engine/install/debian/ 60 | https://docs.docker.com/compose/install/linux/ 61 | 62 | 2.拉取镜像 63 | https://hub.docker.com/r/embe221ed/otter_template/tags 64 | sha256:1868755b24d06342766c54dd6e0516f41b62cec1e992a036f77a0b0401476a04 65 | 下载需要大概16G磁盘空间 66 | ``` 67 | docker pull embe221ed/otter_template:latest 68 | ``` 69 | 70 | 3.解开tos_docker.tar.gz并修改docker-compose.yml (非必须) 71 | 72 | 在本地测试时,我改了两个地方: 73 | - 添加flag 74 | - 把服务端口改成了127.0.0.1:31337 75 | ``` 76 | services: 77 | tos: 78 | environment: 79 | FLAG: justCTF{Th4t_sp3ll_looks_d4ngerous...keep_y0ur_distance} 80 | PORT: 31337 81 | build: 82 | context: ./ 83 | dockerfile: ./Dockerfile 84 | ports: 85 | - "127.0.0.1:31337:31337" 86 | restart: always 87 | ``` 88 | 4.最后执行docker compose up 或者 docker compose up -d即可 89 | 90 | 91 | ## The Otter Scrolls 92 | ``` 93 | 0 % sui --version 94 | sui 1.27.0-homebrew 95 | 96 | ``` 97 | 首先进入解题框架,把题目的地址(nc连接服务器获得)填入`dependency/Move.toml` 98 | 99 | ``` 100 | test@vps ~/justctf/tos/sources/framework-solve 101 | 0 % ls 102 | Cargo.lock Cargo.toml dependency solve src 103 | 104 | test@vps ~/justctf/tos/sources/framework-solve 105 | 0 % nc tos.movectf.com 31337 106 | 107 | [SERVER] Challenge modules published at: 542fe29e11d10314d3330e060c64f8fb9cd341981279432b03b2bd51cf5d489b% 108 | 109 | test@vps ~/justctf/tos/sources/framework-solve 110 | 0 % cat dependency/Move.toml 111 | [package] 112 | name = "challenge" 113 | version = "0.0.1" 114 | edition = "2024.beta" 115 | 116 | [dependencies] 117 | Sui = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "devnet-v1.27.0" } 118 | 119 | [addresses] 120 | admin = "0xfccc9a421bbb13c1a66a1aa98f0ad75029ede94857779c6915b44f94068b921e" 121 | #challenge = "" 122 | challenge = "0x542fe29e11d10314d3330e060c64f8fb9cd341981279432b03b2bd51cf5d489b" 123 | ``` 124 | 然后编写solve 125 | ``` 126 | test@vps ~/justctf/tos/sources/framework-solve 127 | 0 % ls solve 128 | build Move.lock Move.toml sources 129 | 130 | test@vps ~/justctf/tos/sources/framework-solve 131 | 0 % cat solve/sources/solve.move 132 | module solve::solve { 133 | 134 | // [*] Import dependencies 135 | use challenge::theotterscrolls; 136 | 137 | public fun solve( 138 | _spellbook: &mut theotterscrolls::Spellbook, 139 | _ctx: &mut TxContext 140 | ) { 141 | // Your code here... 142 | theotterscrolls::cast_spell(vector[1, 0, 3, 3, 3], _spellbook); 143 | } 144 | 145 | } 146 | ``` 147 | TOS这道题目比较简单,相当于一道签到题 148 | 按照指定顺序取出单词即可,解题代码只需要插入一行 149 | ``` 150 | theotterscrolls::cast_spell(vector[1, 0, 3, 3, 3], _spellbook); 151 | ``` 152 | 然后执行build,把编译后的字节码发送到服务器就能得到flag了 153 | ``` 154 | test@vps ~/justctf/tos/sources/framework-solve 155 | 0 % cd solve 156 | 157 | test@vps ~/justctf/tos/sources/framework-solve/solve 158 | 0 % sui move build 159 | INCLUDING DEPENDENCY challenge 160 | INCLUDING DEPENDENCY Sui 161 | INCLUDING DEPENDENCY MoveStdlib 162 | BUILDING solve 163 | 164 | test@vps ~/justctf/tos/sources/framework-solve/solve 165 | 0 % cat build/solve/bytecode_modules/solve.mv | nc 127.0.0.1 31337 166 | [SERVER] Challenge modules published at: 542fe29e11d10314d3330e060c64f8fb9cd341981279432b03b2bd51cf5d489b[SERVER] Solution published at cf07b5b91e5ea4b1c17442a0e626cbb77b6a1d9a3427e568f403a2c3eff95566[SERVER] Congrats, flag: justCTF{Th4t_sp3ll_looks_d4ngerous...keep_y0ur_distance}% 167 | ``` 168 | # DB 169 | 170 | 酒馆里有个任务榜单,里面有不超过25个怪兽,击杀可以获取奖励 171 | 获取奖励的函数存在逻辑漏洞,击杀榜单里第0个怪兽,可以领取所有怪兽的击杀奖金 172 | 173 | ``` 174 | #[allow(lint(self_transfer))] 175 | public fun get_the_reward( 176 | vault: &mut Vault, 177 | board: &mut QuestBoard, 178 | player: &mut Player, 179 | quest_id: u64, 180 | ctx: &mut TxContext, 181 | ) { 182 | let quest_to_claim = vector::borrow_mut(&mut board.quests, quest_id); 183 | assert!(quest_to_claim.fight_status == FINISHED, WRONG_STATE); 184 | 185 | 186 | let monster = vector::pop_back(&mut board.quests); 187 | 188 | 189 | let Monster { 190 | fight_status: _, 191 | reward: reward, 192 | power: _ 193 | } = monster; 194 | 195 | 196 | let coins = coin::split(&mut vault.cash, (reward as u64), ctx); 197 | coin::join(&mut player.coins, coins); 198 | } 199 | 200 | ``` 201 | # WOO 202 | 203 | 与上一道题目DB类似,问题还是出在获取奖励上 204 | 205 | 206 | ``` 207 | public fun get_the_reward(vault: &mut Vault, board: &mut QuestBoard, player: &mut Player, ctx: &mut TxContext) { 208 | assert!(player.status != RESTING && player.status != PREPARE_FOR_TROUBLE && player.status != ON_ADVENTURE, WRONG_PLAYER_STATE); 209 | 210 | 211 | let monster = vector::remove(&mut board.quests, player.quest_index); 212 | 213 | 214 | let Monster { 215 | reward: reward, 216 | power: _ 217 | } = monster; 218 | 219 | 220 | let coins = coin::split(&mut vault.cash, reward, ctx); 221 | let balance = coin::into_balance(coins); 222 | 223 | 224 | balance::join(&mut player.wallet, balance); 225 | 226 | 227 | player.status = RESTING; 228 | } 229 | ``` 230 | 231 | 设置黑名单防止重复获取奖励, 232 | ``` 233 | assert!(player.status != RESTING && player.status != PREPARE_FOR_TROUBLE && player.status != ON_ADVENTURE, WRONG_PLAYER_STATE); 234 | ``` 235 | 236 | 237 | 238 | 但是忘记了考虑玩家处于购物状态的情况 239 | 240 | ``` 241 | public fun enter_tavern(player: &mut Player): TawernTicket { 242 | assert!(player.status == RESTING, WRONG_PLAYER_STATE); 243 | 244 | player.status = SHOPPING; 245 | 246 | TawernTicket{ total: 0, flag_bought: false } 247 | } 248 | ``` 249 | 250 | 玩家可以领取奖励后购物,再领奖,再购物...... 251 | -------------------------------------------------------------------------------- /writeup/justctf2024/db/.dockerignore: -------------------------------------------------------------------------------- 1 | **/target 2 | **/build 3 | -------------------------------------------------------------------------------- /writeup/justctf2024/db/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM embe221ed/otter_template:latest 2 | 3 | ADD ./sources/framework/chall /work/framework/chall 4 | ADD ./sources/framework/src/main.rs /work/framework/src/ 5 | 6 | # build the challenge contracts 7 | WORKDIR /work/framework/chall 8 | RUN sui move build 9 | 10 | WORKDIR /work/framework 11 | 12 | # build the framework 13 | RUN touch src/main.rs 14 | RUN cargo build --locked --release 15 | 16 | CMD ./run.sh 17 | -------------------------------------------------------------------------------- /writeup/justctf2024/db/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | db: 3 | environment: 4 | FLAG: justCTF{REDACTED} 5 | PORT: 31337 6 | build: 7 | context: ./ 8 | dockerfile: ./Dockerfile 9 | ports: 10 | - "31337:31337" 11 | restart: always 12 | -------------------------------------------------------------------------------- /writeup/justctf2024/db/solve.move: -------------------------------------------------------------------------------- 1 | #https://ctftime.org/writeup/39193 2 | module solve::solve { 3 | 4 | // [*] Import dependencies 5 | use challenge::Otter::{Self, OTTER}; 6 | use sui::random::Random; 7 | 8 | #[allow(lint(public_random))] 9 | public fun solve( 10 | _vault: &mut Otter::Vault, 11 | _questboard: &mut Otter::QuestBoard, 12 | _player: &mut Otter::Player, 13 | _r: &Random, 14 | _ctx: &mut TxContext, 15 | ) { 16 | let mut i = 0; 17 | while (i < 10) { 18 | Otter::buy_sword(_vault, _player, _ctx); 19 | 20 | let mut j = 0; 21 | while (j < 25) { 22 | Otter::find_a_monster(_questboard, _r, _ctx); 23 | j = j + 1; 24 | }; 25 | 26 | Otter::fight_monster(_questboard, _player, 0); 27 | Otter::return_home(_questboard, 0); 28 | 29 | let mut j = 0; 30 | while (j < 25) { 31 | Otter::get_the_reward(_vault, _questboard, _player, 0, _ctx); 32 | j = j + 1; 33 | }; 34 | 35 | i = i + 1; 36 | }; 37 | 38 | let flag = Otter::buy_flag(_vault, _player, _ctx); 39 | Otter::prove(_questboard, flag); 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /writeup/justctf2024/db/sources/framework-solve/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "solve-framework" 7 | version = "0.1.0" 8 | -------------------------------------------------------------------------------- /writeup/justctf2024/db/sources/framework-solve/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "solve-framework" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | -------------------------------------------------------------------------------- /writeup/justctf2024/db/sources/framework-solve/dependency/Move.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "challenge" 3 | version = "0.0.1" 4 | edition = "2024.beta" 5 | 6 | [dependencies] 7 | Sui = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "devnet-v1.27.0" } 8 | 9 | [addresses] 10 | admin = "0xfccc9a421bbb13c1a66a1aa98f0ad75029ede94857779c6915b44f94068b921e" 11 | challenge = "" 12 | -------------------------------------------------------------------------------- /writeup/justctf2024/db/sources/framework-solve/dependency/sources/dark_brotterhood.move: -------------------------------------------------------------------------------- 1 | module challenge::Otter { 2 | 3 | // --------------------------------------------------- 4 | // DEPENDENCIES 5 | // --------------------------------------------------- 6 | 7 | use sui::coin::{Self, Coin}; 8 | use sui::balance::{Self, Supply}; 9 | use sui::url; 10 | use sui::random::{Self, Random}; 11 | use sui::table::{Self, Table}; 12 | 13 | // --------------------------------------------------- 14 | // CONST 15 | // --------------------------------------------------- 16 | 17 | const NEW: u64 = 1; 18 | const WON: u64 = 2; 19 | const FINISHED: u64 = 3; 20 | 21 | const WRONG_AMOUNT: u64 = 1337; 22 | const BETTER_BRING_A_KNIFE_TO_A_GUNFIGHT: u64 = 1338; 23 | const WRONG_STATE: u64 = 1339; 24 | const ALREADY_REGISTERED: u64 = 1340; 25 | const NOT_REGISTERED: u64 = 1341; 26 | const TOO_MUCH_MONSTERS: u64 = 1342; 27 | const NOT_SOLVED: u64 = 1343; 28 | 29 | const QUEST_LIMIT: u64 = 25; 30 | // --------------------------------------------------- 31 | // STRUCTS 32 | // --------------------------------------------------- 33 | 34 | public struct OTTER has drop {} 35 | 36 | public struct OsecSuply has key { 37 | id: UID, 38 | supply: Supply 39 | } 40 | 41 | public struct Vault has key { 42 | id: UID, 43 | cash: Coin 44 | } 45 | 46 | public struct Monster has store { 47 | fight_status: u64, 48 | reward: u8, 49 | power: u8 50 | } 51 | 52 | public struct QuestBoard has key, store { 53 | id: UID, 54 | quests: vector, 55 | players: Table 56 | } 57 | 58 | public struct Flag has key, store { 59 | id: UID, 60 | user: address, 61 | flag: bool 62 | } 63 | 64 | public struct Player has key, store { 65 | id: UID, 66 | user: address, 67 | coins: Coin, 68 | power: u8 69 | } 70 | 71 | // --------------------------------------------------- 72 | // MINT CASH 73 | // --------------------------------------------------- 74 | 75 | fun init(witness: OTTER, ctx: &mut TxContext) { 76 | let (mut treasury, metadata) = coin::create_currency( 77 | witness, 9, b"OSEC", b"Osec", b"Otter ca$h", option::some(url::new_unsafe_from_bytes(b"https://osec.io/")), ctx 78 | ); 79 | transfer::public_freeze_object(metadata); 80 | 81 | let pool_liquidity = coin::mint(&mut treasury, 50000, ctx); 82 | 83 | let vault = Vault { 84 | id: object::new(ctx), 85 | cash: pool_liquidity 86 | }; 87 | 88 | let supply = coin::treasury_into_supply(treasury); 89 | 90 | let osec_supply = OsecSuply { 91 | id: object::new(ctx), 92 | supply 93 | }; 94 | 95 | transfer::transfer(osec_supply, tx_context::sender(ctx)); 96 | 97 | transfer::share_object(QuestBoard { 98 | id: object::new(ctx), 99 | quests: vector::empty(), 100 | players: table::new(ctx) 101 | }); 102 | 103 | transfer::share_object(vault); 104 | } 105 | 106 | public fun mint(sup: &mut OsecSuply, amount: u64, ctx: &mut TxContext): Coin { 107 | let osecBalance = balance::increase_supply(&mut sup.supply, amount); 108 | coin::from_balance(osecBalance, ctx) 109 | } 110 | 111 | public entry fun mint_to(sup: &mut OsecSuply, amount: u64, to: address, ctx: &mut TxContext) { 112 | let osec = mint(sup, amount, ctx); 113 | transfer::public_transfer(osec, to); 114 | } 115 | 116 | public fun burn(sup: &mut OsecSuply, c: Coin): u64 { 117 | balance::decrease_supply(&mut sup.supply, coin::into_balance(c)) 118 | } 119 | 120 | // --------------------------------------------------- 121 | // REGISTER 122 | // --------------------------------------------------- 123 | 124 | public fun register(sup: &mut OsecSuply, board: &mut QuestBoard, player: address, ctx: &mut TxContext) { 125 | assert!(!table::contains(&board.players, player), ALREADY_REGISTERED); 126 | 127 | table::add(&mut board.players, player, false); 128 | 129 | transfer::transfer(Player { 130 | id: object::new(ctx), 131 | user: tx_context::sender(ctx), 132 | coins: mint(sup, 137, ctx), 133 | power: 10 134 | }, player); 135 | } 136 | 137 | // --------------------------------------------------- 138 | // SHOP 139 | // --------------------------------------------------- 140 | 141 | #[allow(lint(self_transfer))] 142 | public fun buy_flag(vault: &mut Vault, player: &mut Player, ctx: &mut TxContext): Flag { 143 | assert!(coin::value(&player.coins) >= 1337, WRONG_AMOUNT); 144 | 145 | let coins = coin::split(&mut player.coins, 1337, ctx); 146 | coin::join(&mut vault.cash, coins); 147 | 148 | Flag { 149 | id: object::new(ctx), 150 | user: tx_context::sender(ctx), 151 | flag: true 152 | } 153 | } 154 | 155 | public fun buy_sword(vault: &mut Vault, player: &mut Player, ctx: &mut TxContext) { 156 | assert!(coin::value(&player.coins) >= 137, WRONG_AMOUNT); 157 | 158 | let coins = coin::split(&mut player.coins, 137, ctx); 159 | coin::join(&mut vault.cash, coins); 160 | 161 | player.power = player.power + 100; 162 | } 163 | 164 | // --------------------------------------------------- 165 | // ADVENTURE TIME 166 | // --------------------------------------------------- 167 | 168 | #[allow(lint(public_random))] 169 | public fun find_a_monster(board: &mut QuestBoard, r: &Random, ctx: &mut TxContext) { 170 | assert!(vector::length(&board.quests) <= QUEST_LIMIT, TOO_MUCH_MONSTERS); 171 | 172 | let mut generator = random::new_generator(r, ctx); 173 | 174 | let quest = Monster { 175 | fight_status: NEW, 176 | reward: random::generate_u8_in_range(&mut generator, 13, 37), 177 | power: random::generate_u8_in_range(&mut generator, 13, 73) 178 | }; 179 | 180 | vector::push_back(&mut board.quests, quest); 181 | 182 | } 183 | 184 | public fun fight_monster(board: &mut QuestBoard, player: &mut Player, quest_id: u64) { 185 | let quest = vector::borrow_mut(&mut board.quests, quest_id); 186 | assert!(quest.fight_status == NEW, WRONG_STATE); 187 | assert!(player.power > quest.power, BETTER_BRING_A_KNIFE_TO_A_GUNFIGHT); 188 | 189 | player.power = 10; // sword breaks after fighting the monster :c 190 | 191 | quest.fight_status = WON; 192 | } 193 | 194 | public fun return_home(board: &mut QuestBoard, quest_id: u64) { 195 | let quest_to_finish = vector::borrow_mut(&mut board.quests, quest_id); 196 | assert!(quest_to_finish.fight_status == WON, WRONG_STATE); 197 | 198 | quest_to_finish.fight_status = FINISHED; 199 | } 200 | 201 | #[allow(lint(self_transfer))] 202 | public fun get_the_reward( 203 | vault: &mut Vault, 204 | board: &mut QuestBoard, 205 | player: &mut Player, 206 | quest_id: u64, 207 | ctx: &mut TxContext, 208 | ) { 209 | let quest_to_claim = vector::borrow_mut(&mut board.quests, quest_id); 210 | assert!(quest_to_claim.fight_status == FINISHED, WRONG_STATE); 211 | 212 | let monster = vector::pop_back(&mut board.quests); 213 | 214 | let Monster { 215 | fight_status: _, 216 | reward: reward, 217 | power: _ 218 | } = monster; 219 | 220 | let coins = coin::split(&mut vault.cash, (reward as u64), ctx); 221 | coin::join(&mut player.coins, coins); 222 | } 223 | 224 | // --------------------------------------------------- 225 | // PROVE SOLUTION 226 | // --------------------------------------------------- 227 | 228 | public fun prove(board: &mut QuestBoard, flag: Flag) { 229 | let Flag { 230 | id, 231 | user, 232 | flag 233 | } = flag; 234 | 235 | object::delete(id); 236 | 237 | assert!(table::contains(&board.players, user), NOT_REGISTERED); 238 | assert!(flag, NOT_SOLVED); 239 | *table::borrow_mut(&mut board.players, user) = true; 240 | } 241 | 242 | // --------------------------------------------------- 243 | // CHECK WINNER 244 | // --------------------------------------------------- 245 | 246 | public fun check_winner(board: &QuestBoard, player: address) { 247 | assert!(*table::borrow(&board.players, player) == true, NOT_SOLVED); 248 | } 249 | 250 | } 251 | -------------------------------------------------------------------------------- /writeup/justctf2024/db/sources/framework-solve/solve/Move.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "solve" 3 | version = "0.0.1" 4 | edition = "2024.alpha" 5 | 6 | [dependencies] 7 | Sui = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "devnet-v1.27.0" } 8 | 9 | [dependencies.challenge] 10 | version = '1.0.0' 11 | local = '../dependency' 12 | 13 | [addresses] 14 | solve = "0x0" 15 | -------------------------------------------------------------------------------- /writeup/justctf2024/db/sources/framework-solve/solve/sources/solve.move: -------------------------------------------------------------------------------- 1 | module solve::solve { 2 | 3 | // [*] Import dependencies 4 | use challenge::Otter::{Self, OTTER}; 5 | use sui::random::Random; 6 | 7 | #[allow(lint(public_random))] 8 | public fun solve( 9 | _vault: &mut Otter::Vault, 10 | _questboard: &mut Otter::QuestBoard, 11 | _player: &mut Otter::Player, 12 | _r: &Random, 13 | _ctx: &mut TxContext, 14 | ) { 15 | // Your code here ... 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /writeup/justctf2024/db/sources/framework-solve/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::net::TcpStream; 2 | use std::io::{Read, Write}; 3 | use std::str::from_utf8; 4 | use std::{error::Error, fs}; 5 | use std::env; 6 | 7 | fn main() -> Result<(), Box> { 8 | 9 | let host = env::var("HOST").unwrap_or_else(|_| "127.0.0.1".to_string()); // replace with remote ip 10 | let port = env::var("PORT").unwrap_or_else(|_| "31337".to_string()); 11 | 12 | match TcpStream::connect(format!("{}:{}", host, port)) { 13 | Ok(mut stream) => { 14 | println!(" - Connected!"); 15 | 16 | let mod_data : Vec = fs::read("./solve/build/solve/bytecode_modules/solve.mv").unwrap(); 17 | println!(" - Loaded solution!"); 18 | 19 | stream.write_all(&mod_data)?; 20 | stream.flush()?; 21 | println!(" - Sent solution!"); 22 | 23 | let mut return_data1 = [0 as u8; 200]; 24 | match stream.read(&mut return_data1) { 25 | Ok(_) => { 26 | println!(" - Connection Output: '{}'", from_utf8(&return_data1).unwrap()); // Get module address 27 | let mut return_data2 = [0 as u8; 200]; 28 | match stream.read(&mut return_data2) { 29 | Ok(_) => { 30 | println!(" - Connection Output: '{}'", from_utf8(&return_data2).unwrap()); // Get module address 31 | let mut flag = [0 as u8; 200]; 32 | match stream.read(&mut flag) { 33 | Ok(_) => { 34 | println!(" - Connection Output: '{}'", from_utf8(&flag).unwrap()); // Get flag 35 | 36 | }, 37 | Err(e) => { 38 | println!(" - Failed to receive data: {}", e); 39 | } 40 | } 41 | }, 42 | Err(e) => { 43 | println!(" - Failed to receive data: {}", e); 44 | } 45 | } 46 | }, 47 | Err(e) => { 48 | println!(" - Failed to connect: {}", e); 49 | } 50 | } 51 | }, 52 | Err(e) => { 53 | println!(" - Failed to connect: {}", e); 54 | } 55 | } 56 | println!(" - Terminated."); 57 | 58 | Ok(()) 59 | } 60 | -------------------------------------------------------------------------------- /writeup/justctf2024/db/sources/framework/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "framework" 3 | version = "0.1.0" 4 | edition = "2021" 5 | exclude = ["chall/"] 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | quote = "1.0.26" 11 | threadpool = "1.8.1" 12 | proc-macro2 = "1.0.66" 13 | 14 | tokio = { version = "1", features = ["full"] } 15 | 16 | move-core-types = { git = "https://github.com/MystenLabs/sui", tag = "devnet-v1.27.0", package = "move-core-types" } 17 | move-bytecode-source-map = { git = "https://github.com/MystenLabs/sui", tag = "devnet-v1.27.0", package = "move-bytecode-source-map" } 18 | move-binary-format = { git = "https://github.com/MystenLabs/sui", tag = "devnet-v1.27.0", package = "move-binary-format" } 19 | move-symbol-pool = { git = "https://github.com/MystenLabs/sui", tag = "devnet-v1.27.0", package = "move-symbol-pool" } 20 | move-transactional-test-runner = { git = "https://github.com/MystenLabs/sui", tag = "devnet-v1.27.0", package = "move-transactional-test-runner" } 21 | 22 | sui-types = { git = "https://github.com/MystenLabs/sui", tag = "devnet-v1.27.0", package = "sui-types"} 23 | sui-transactional-test-runner = { git = "https://github.com/MystenLabs/sui", tag = "devnet-v1.27.0", package = "sui-transactional-test-runner"} 24 | 25 | sui-ctf-framework = { git = "https://github.com/otter-sec/sui-ctf-framework", branch = "justctf2024" } 26 | -------------------------------------------------------------------------------- /writeup/justctf2024/db/sources/framework/chall/Move.lock: -------------------------------------------------------------------------------- 1 | # @generated by Move, please check-in and do not edit manually. 2 | 3 | [move] 4 | version = 2 5 | manifest_digest = "C1FECA3B112600A783F9F421CC2BF9CE00CE66320440DD4E76D8CD5B87B3AD2C" 6 | deps_digest = "F8BBB0CCB2491CA29A3DF03D6F92277A4F3574266507ACD77214D37ECA3F3082" 7 | dependencies = [ 8 | { name = "Sui" }, 9 | ] 10 | 11 | [[move.package]] 12 | name = "MoveStdlib" 13 | source = { git = "https://github.com/MystenLabs/sui.git", rev = "devnet-v1.27.0", subdir = "crates/sui-framework/packages/move-stdlib" } 14 | 15 | [[move.package]] 16 | name = "Sui" 17 | source = { git = "https://github.com/MystenLabs/sui.git", rev = "devnet-v1.27.0", subdir = "crates/sui-framework/packages/sui-framework" } 18 | 19 | dependencies = [ 20 | { name = "MoveStdlib" }, 21 | ] 22 | 23 | [move.toolchain-version] 24 | compiler-version = "1.27.0" 25 | edition = "2024.beta" 26 | flavor = "sui" 27 | -------------------------------------------------------------------------------- /writeup/justctf2024/db/sources/framework/chall/Move.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "challenge" 3 | version = "0.0.1" 4 | edition = "2024.beta" 5 | 6 | [dependencies] 7 | Sui = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "devnet-v1.27.0" } 8 | 9 | [addresses] 10 | challenge = "0x0" 11 | admin = "0xfccc9a421bbb13c1a66a1aa98f0ad75029ede94857779c6915b44f94068b921e" 12 | -------------------------------------------------------------------------------- /writeup/justctf2024/db/sources/framework/chall/sources/dark_brotterhood.move: -------------------------------------------------------------------------------- 1 | module challenge::Otter { 2 | 3 | // --------------------------------------------------- 4 | // DEPENDENCIES 5 | // --------------------------------------------------- 6 | 7 | use sui::coin::{Self, Coin}; 8 | use sui::balance::{Self, Supply}; 9 | use sui::url; 10 | use sui::random::{Self, Random}; 11 | use sui::table::{Self, Table}; 12 | 13 | // --------------------------------------------------- 14 | // CONST 15 | // --------------------------------------------------- 16 | 17 | const NEW: u64 = 1; 18 | const WON: u64 = 2; 19 | const FINISHED: u64 = 3; 20 | 21 | const WRONG_AMOUNT: u64 = 1337; 22 | const BETTER_BRING_A_KNIFE_TO_A_GUNFIGHT: u64 = 1338; 23 | const WRONG_STATE: u64 = 1339; 24 | const ALREADY_REGISTERED: u64 = 1340; 25 | const NOT_REGISTERED: u64 = 1341; 26 | const TOO_MUCH_MONSTERS: u64 = 1342; 27 | const NOT_SOLVED: u64 = 1343; 28 | 29 | const QUEST_LIMIT: u64 = 25; 30 | // --------------------------------------------------- 31 | // STRUCTS 32 | // --------------------------------------------------- 33 | 34 | public struct OTTER has drop {} 35 | 36 | public struct OsecSuply has key { 37 | id: UID, 38 | supply: Supply 39 | } 40 | 41 | public struct Vault has key { 42 | id: UID, 43 | cash: Coin 44 | } 45 | 46 | public struct Monster has store { 47 | fight_status: u64, 48 | reward: u8, 49 | power: u8 50 | } 51 | 52 | public struct QuestBoard has key, store { 53 | id: UID, 54 | quests: vector, 55 | players: Table 56 | } 57 | 58 | public struct Flag has key, store { 59 | id: UID, 60 | user: address, 61 | flag: bool 62 | } 63 | 64 | public struct Player has key, store { 65 | id: UID, 66 | user: address, 67 | coins: Coin, 68 | power: u8 69 | } 70 | 71 | // --------------------------------------------------- 72 | // MINT CASH 73 | // --------------------------------------------------- 74 | 75 | fun init(witness: OTTER, ctx: &mut TxContext) { 76 | let (mut treasury, metadata) = coin::create_currency( 77 | witness, 9, b"OSEC", b"Osec", b"Otter ca$h", option::some(url::new_unsafe_from_bytes(b"https://osec.io/")), ctx 78 | ); 79 | transfer::public_freeze_object(metadata); 80 | 81 | let pool_liquidity = coin::mint(&mut treasury, 50000, ctx); 82 | 83 | let vault = Vault { 84 | id: object::new(ctx), 85 | cash: pool_liquidity 86 | }; 87 | 88 | let supply = coin::treasury_into_supply(treasury); 89 | 90 | let osec_supply = OsecSuply { 91 | id: object::new(ctx), 92 | supply 93 | }; 94 | 95 | transfer::transfer(osec_supply, tx_context::sender(ctx)); 96 | 97 | transfer::share_object(QuestBoard { 98 | id: object::new(ctx), 99 | quests: vector::empty(), 100 | players: table::new(ctx) 101 | }); 102 | 103 | transfer::share_object(vault); 104 | } 105 | 106 | public fun mint(sup: &mut OsecSuply, amount: u64, ctx: &mut TxContext): Coin { 107 | let osecBalance = balance::increase_supply(&mut sup.supply, amount); 108 | coin::from_balance(osecBalance, ctx) 109 | } 110 | 111 | public entry fun mint_to(sup: &mut OsecSuply, amount: u64, to: address, ctx: &mut TxContext) { 112 | let osec = mint(sup, amount, ctx); 113 | transfer::public_transfer(osec, to); 114 | } 115 | 116 | public fun burn(sup: &mut OsecSuply, c: Coin): u64 { 117 | balance::decrease_supply(&mut sup.supply, coin::into_balance(c)) 118 | } 119 | 120 | // --------------------------------------------------- 121 | // REGISTER 122 | // --------------------------------------------------- 123 | 124 | public fun register(sup: &mut OsecSuply, board: &mut QuestBoard, player: address, ctx: &mut TxContext) { 125 | assert!(!table::contains(&board.players, player), ALREADY_REGISTERED); 126 | 127 | table::add(&mut board.players, player, false); 128 | 129 | transfer::transfer(Player { 130 | id: object::new(ctx), 131 | user: tx_context::sender(ctx), 132 | coins: mint(sup, 137, ctx), 133 | power: 10 134 | }, player); 135 | } 136 | 137 | // --------------------------------------------------- 138 | // SHOP 139 | // --------------------------------------------------- 140 | 141 | #[allow(lint(self_transfer))] 142 | public fun buy_flag(vault: &mut Vault, player: &mut Player, ctx: &mut TxContext): Flag { 143 | assert!(coin::value(&player.coins) >= 1337, WRONG_AMOUNT); 144 | 145 | let coins = coin::split(&mut player.coins, 1337, ctx); 146 | coin::join(&mut vault.cash, coins); 147 | 148 | Flag { 149 | id: object::new(ctx), 150 | user: tx_context::sender(ctx), 151 | flag: true 152 | } 153 | } 154 | 155 | public fun buy_sword(vault: &mut Vault, player: &mut Player, ctx: &mut TxContext) { 156 | assert!(coin::value(&player.coins) >= 137, WRONG_AMOUNT); 157 | 158 | let coins = coin::split(&mut player.coins, 137, ctx); 159 | coin::join(&mut vault.cash, coins); 160 | 161 | player.power = player.power + 100; 162 | } 163 | 164 | // --------------------------------------------------- 165 | // ADVENTURE TIME 166 | // --------------------------------------------------- 167 | 168 | #[allow(lint(public_random))] 169 | public fun find_a_monster(board: &mut QuestBoard, r: &Random, ctx: &mut TxContext) { 170 | assert!(vector::length(&board.quests) <= QUEST_LIMIT, TOO_MUCH_MONSTERS); 171 | 172 | let mut generator = random::new_generator(r, ctx); 173 | 174 | let quest = Monster { 175 | fight_status: NEW, 176 | reward: random::generate_u8_in_range(&mut generator, 13, 37), 177 | power: random::generate_u8_in_range(&mut generator, 13, 73) 178 | }; 179 | 180 | vector::push_back(&mut board.quests, quest); 181 | 182 | } 183 | 184 | public fun fight_monster(board: &mut QuestBoard, player: &mut Player, quest_id: u64) { 185 | let quest = vector::borrow_mut(&mut board.quests, quest_id); 186 | assert!(quest.fight_status == NEW, WRONG_STATE); 187 | assert!(player.power > quest.power, BETTER_BRING_A_KNIFE_TO_A_GUNFIGHT); 188 | 189 | player.power = 10; // sword breaks after fighting the monster :c 190 | 191 | quest.fight_status = WON; 192 | } 193 | 194 | public fun return_home(board: &mut QuestBoard, quest_id: u64) { 195 | let quest_to_finish = vector::borrow_mut(&mut board.quests, quest_id); 196 | assert!(quest_to_finish.fight_status == WON, WRONG_STATE); 197 | 198 | quest_to_finish.fight_status = FINISHED; 199 | } 200 | 201 | #[allow(lint(self_transfer))] 202 | public fun get_the_reward( 203 | vault: &mut Vault, 204 | board: &mut QuestBoard, 205 | player: &mut Player, 206 | quest_id: u64, 207 | ctx: &mut TxContext, 208 | ) { 209 | let quest_to_claim = vector::borrow_mut(&mut board.quests, quest_id); 210 | assert!(quest_to_claim.fight_status == FINISHED, WRONG_STATE); 211 | 212 | let monster = vector::pop_back(&mut board.quests); 213 | 214 | let Monster { 215 | fight_status: _, 216 | reward: reward, 217 | power: _ 218 | } = monster; 219 | 220 | let coins = coin::split(&mut vault.cash, (reward as u64), ctx); 221 | coin::join(&mut player.coins, coins); 222 | } 223 | 224 | // --------------------------------------------------- 225 | // PROVE SOLUTION 226 | // --------------------------------------------------- 227 | 228 | public fun prove(board: &mut QuestBoard, flag: Flag) { 229 | let Flag { 230 | id, 231 | user, 232 | flag 233 | } = flag; 234 | 235 | object::delete(id); 236 | 237 | assert!(table::contains(&board.players, user), NOT_REGISTERED); 238 | assert!(flag, NOT_SOLVED); 239 | *table::borrow_mut(&mut board.players, user) = true; 240 | } 241 | 242 | // --------------------------------------------------- 243 | // CHECK WINNER 244 | // --------------------------------------------------- 245 | 246 | public fun check_winner(board: &QuestBoard, player: address) { 247 | assert!(*table::borrow(&board.players, player) == true, NOT_SOLVED); 248 | } 249 | 250 | } 251 | -------------------------------------------------------------------------------- /writeup/justctf2024/db/sources/framework/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | sed -i \ 4 | -E 's/(host[[:space:]]+all[[:space:]]+all[[:space:]]+127.0.0.1\/32[[:space:]]+)scram-sha-256/\1trust/' \ 5 | /etc/postgresql/16/main/pg_hba.conf 6 | 7 | sed -i \ 8 | -E 's/(host[[:space:]]+all[[:space:]]+all[[:space:]]+::1\/128[[:space:]]+)scram-sha-256/\1trust/' \ 9 | /etc/postgresql/16/main/pg_hba.conf 10 | 11 | /etc/init.d/postgresql restart 12 | 13 | sudo -u postgres psql postgres -c "ALTER ROLE postgres WITH SUPERUSER LOGIN PASSWORD 'postgrespw';" 14 | sudo -u postgres psql postgres -c "CREATE DATABASE sui_indexer_v2;" -c "ALTER SYSTEM SET max_connections = 500;" 15 | 16 | cargo r --release 17 | -------------------------------------------------------------------------------- /writeup/justctf2024/db/sources/framework/rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.76" 3 | -------------------------------------------------------------------------------- /writeup/justctf2024/db/sources/run_client.sh: -------------------------------------------------------------------------------- 1 | set -eux 2 | 3 | cd framework-solve/solve && sui move build 4 | cd .. 5 | cargo r --release 6 | -------------------------------------------------------------------------------- /writeup/justctf2024/db/sources/run_server.sh: -------------------------------------------------------------------------------- 1 | set -eux 2 | 3 | cd framework/chall && sui move build 4 | cd .. 5 | cargo r --release 6 | -------------------------------------------------------------------------------- /writeup/justctf2024/tos/.dockerignore: -------------------------------------------------------------------------------- 1 | **/target 2 | **/build 3 | -------------------------------------------------------------------------------- /writeup/justctf2024/tos/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM embe221ed/otter_template:latest 2 | 3 | ADD ./sources/framework/chall /work/framework/chall 4 | ADD ./sources/framework/src/main.rs /work/framework/src/ 5 | 6 | # build the challenge contracts 7 | WORKDIR /work/framework/chall 8 | RUN sui move build 9 | 10 | WORKDIR /work/framework 11 | 12 | # build the framework 13 | RUN touch src/main.rs 14 | RUN cargo build --locked --release 15 | 16 | CMD ./run.sh 17 | -------------------------------------------------------------------------------- /writeup/justctf2024/tos/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | tos: 3 | environment: 4 | FLAG: justCTF{REDACTED} 5 | PORT: 31337 6 | build: 7 | context: ./ 8 | dockerfile: ./Dockerfile 9 | ports: 10 | - "31337:31337" 11 | restart: always 12 | -------------------------------------------------------------------------------- /writeup/justctf2024/tos/solve.move: -------------------------------------------------------------------------------- 1 | #https://ctftime.org/writeup/39191 2 | module solve::solve { 3 | 4 | // [*] Import dependencies 5 | use challenge::theotterscrolls; 6 | 7 | public fun solve( 8 | _spellbook: &mut theotterscrolls::Spellbook, 9 | _ctx: &mut TxContext 10 | ) { 11 | let spell_sequence = vector[1, 0, 3, 3, 3]; 12 | theotterscrolls::cast_spell(spell_sequence, _spellbook); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /writeup/justctf2024/tos/sources/framework-solve/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "solve-framework" 7 | version = "0.1.0" 8 | -------------------------------------------------------------------------------- /writeup/justctf2024/tos/sources/framework-solve/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "solve-framework" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | -------------------------------------------------------------------------------- /writeup/justctf2024/tos/sources/framework-solve/dependency/Move.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "challenge" 3 | version = "0.0.1" 4 | edition = "2024.beta" 5 | 6 | [dependencies] 7 | Sui = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "devnet-v1.27.0" } 8 | 9 | [addresses] 10 | admin = "0xfccc9a421bbb13c1a66a1aa98f0ad75029ede94857779c6915b44f94068b921e" 11 | challenge = "" 12 | -------------------------------------------------------------------------------- /writeup/justctf2024/tos/sources/framework-solve/dependency/sources/the_otter_scrolls.move: -------------------------------------------------------------------------------- 1 | module challenge::theotterscrolls { 2 | 3 | // --------------------------------------------------- 4 | // DEPENDENCIES 5 | // --------------------------------------------------- 6 | 7 | use sui::table::{Self, Table}; 8 | use std::string::{Self, String}; 9 | use std::debug; 10 | 11 | // --------------------------------------------------- 12 | // STRUCTS 13 | // --------------------------------------------------- 14 | 15 | public struct Spellbook has key { 16 | id: UID, 17 | casted: bool, 18 | spells: Table> 19 | } 20 | 21 | // --------------------------------------------------- 22 | // FUNCTIONS 23 | // --------------------------------------------------- 24 | 25 | //The spell consists of five magic words, which have to be read in the correct order! 26 | 27 | fun init(ctx: &mut TxContext) { 28 | 29 | let mut all_words = table::new(ctx); 30 | 31 | let fire = vector[ 32 | string::utf8(b"Blast"), 33 | string::utf8(b"Inferno"), 34 | string::utf8(b"Pyre"), 35 | string::utf8(b"Fenix"), 36 | string::utf8(b"Ember") 37 | ]; 38 | 39 | let wind = vector[ 40 | string::utf8(b"Zephyr"), 41 | string::utf8(b"Swirl"), 42 | string::utf8(b"Breeze"), 43 | string::utf8(b"Gust"), 44 | string::utf8(b"Sigil") 45 | ]; 46 | 47 | let water = vector[ 48 | string::utf8(b"Aquarius"), 49 | string::utf8(b"Mistwalker"), 50 | string::utf8(b"Waves"), 51 | string::utf8(b"Call"), 52 | string::utf8(b"Storm") 53 | ]; 54 | 55 | let earth = vector[ 56 | string::utf8(b"Tremor"), 57 | string::utf8(b"Stoneheart"), 58 | string::utf8(b"Grip"), 59 | string::utf8(b"Granite"), 60 | string::utf8(b"Mudslide") 61 | ]; 62 | 63 | let power = vector[ 64 | string::utf8(b"Alakazam"), 65 | string::utf8(b"Hocus"), 66 | string::utf8(b"Pocus"), 67 | string::utf8(b"Wazzup"), 68 | string::utf8(b"Wrath") 69 | ]; 70 | 71 | table::add(&mut all_words, 0, fire); 72 | table::add(&mut all_words, 1, wind); 73 | table::add(&mut all_words, 2, water); 74 | table::add(&mut all_words, 3, earth); 75 | table::add(&mut all_words, 4, power); 76 | 77 | let spellbook = Spellbook { 78 | id: object::new(ctx), 79 | casted: false, 80 | spells: all_words 81 | }; 82 | 83 | transfer::share_object(spellbook); 84 | } 85 | 86 | public fun cast_spell(spell_sequence: vector, book: &mut Spellbook) { 87 | 88 | let fire = table::remove(&mut book.spells, 0); 89 | let wind = table::remove(&mut book.spells, 1); 90 | let water = table::remove(&mut book.spells, 2); 91 | let earth = table::remove(&mut book.spells, 3); 92 | let power = table::remove(&mut book.spells, 4); 93 | 94 | let fire_word_id = *vector::borrow(&spell_sequence, 0); 95 | let wind_word_id = *vector::borrow(&spell_sequence, 1); 96 | let water_word_id = *vector::borrow(&spell_sequence, 2); 97 | let earth_word_id = *vector::borrow(&spell_sequence, 3); 98 | let power_word_id = *vector::borrow(&spell_sequence, 4); 99 | 100 | let fire_word = vector::borrow(&fire, fire_word_id); 101 | let wind_word = vector::borrow(&wind, wind_word_id); 102 | let water_word = vector::borrow(&water, water_word_id); 103 | let earth_word = vector::borrow(&earth, earth_word_id); 104 | let power_word = vector::borrow(&power, power_word_id); 105 | 106 | if (fire_word == string::utf8(b"Inferno")) { 107 | if (wind_word == string::utf8(b"Zephyr")) { 108 | if (water_word == string::utf8(b"Call")) { 109 | if (earth_word == string::utf8(b"Granite")) { 110 | if (power_word == string::utf8(b"Wazzup")) { 111 | book.casted = true; 112 | } 113 | } 114 | } 115 | } 116 | } 117 | 118 | } 119 | 120 | public fun check_if_spell_casted(book: &Spellbook): bool { 121 | let casted = book.casted; 122 | assert!(casted == true, 1337); 123 | casted 124 | } 125 | 126 | } 127 | 128 | -------------------------------------------------------------------------------- /writeup/justctf2024/tos/sources/framework-solve/solve/Move.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "solve" 3 | version = "0.0.1" 4 | edition = "2024.alpha" 5 | 6 | [dependencies] 7 | Sui = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "devnet-v1.27.0" } 8 | 9 | [dependencies.challenge] 10 | version = '1.0.0' 11 | local = '../dependency' 12 | 13 | [addresses] 14 | solve = "0x0" 15 | -------------------------------------------------------------------------------- /writeup/justctf2024/tos/sources/framework-solve/solve/sources/solve.move: -------------------------------------------------------------------------------- 1 | module solve::solve { 2 | 3 | // [*] Import dependencies 4 | use challenge::theotterscrolls; 5 | 6 | public fun solve( 7 | _spellbook: &mut theotterscrolls::Spellbook, 8 | _ctx: &mut TxContext 9 | ) { 10 | // Your code here... 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /writeup/justctf2024/tos/sources/framework-solve/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::net::TcpStream; 2 | use std::io::{Read, Write}; 3 | use std::str::from_utf8; 4 | use std::{error::Error, fs}; 5 | use std::env; 6 | 7 | fn main() -> Result<(), Box> { 8 | 9 | let host = env::var("HOST").unwrap_or_else(|_| "127.0.0.1".to_string()); // replace with remote ip 10 | let port = env::var("PORT").unwrap_or_else(|_| "31337".to_string()); 11 | 12 | match TcpStream::connect(format!("{}:{}", host, port)) { 13 | Ok(mut stream) => { 14 | println!(" - Connected!"); 15 | 16 | let mod_data : Vec = fs::read("./solve/build/solve/bytecode_modules/solve.mv").unwrap(); 17 | println!(" - Loaded solution!"); 18 | 19 | stream.write_all(&mod_data)?; 20 | stream.flush()?; 21 | println!(" - Sent solution!"); 22 | 23 | let mut return_data1 = [0 as u8; 200]; 24 | match stream.read(&mut return_data1) { 25 | Ok(_) => { 26 | println!(" - Connection Output: '{}'", from_utf8(&return_data1).unwrap()); // Get module address 27 | let mut return_data2 = [0 as u8; 200]; 28 | match stream.read(&mut return_data2) { 29 | Ok(_) => { 30 | println!(" - Connection Output: '{}'", from_utf8(&return_data2).unwrap()); // Get module address 31 | let mut flag = [0 as u8; 200]; 32 | match stream.read(&mut flag) { 33 | Ok(_) => { 34 | println!(" - Connection Output: '{}'", from_utf8(&flag).unwrap()); // Get flag 35 | 36 | }, 37 | Err(e) => { 38 | println!(" - Failed to receive data: {}", e); 39 | } 40 | } 41 | }, 42 | Err(e) => { 43 | println!(" - Failed to receive data: {}", e); 44 | } 45 | } 46 | }, 47 | Err(e) => { 48 | println!(" - Failed to connect: {}", e); 49 | } 50 | } 51 | }, 52 | Err(e) => { 53 | println!(" - Failed to connect: {}", e); 54 | } 55 | } 56 | println!(" - Terminated."); 57 | 58 | Ok(()) 59 | } 60 | -------------------------------------------------------------------------------- /writeup/justctf2024/tos/sources/framework/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "framework" 3 | version = "0.1.0" 4 | edition = "2021" 5 | exclude = ["chall/"] 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | quote = "1.0.26" 11 | threadpool = "1.8.1" 12 | proc-macro2 = "1.0.66" 13 | 14 | tokio = { version = "1", features = ["full"] } 15 | 16 | move-core-types = { git = "https://github.com/MystenLabs/sui", tag = "devnet-v1.27.0", package = "move-core-types" } 17 | move-bytecode-source-map = { git = "https://github.com/MystenLabs/sui", tag = "devnet-v1.27.0", package = "move-bytecode-source-map" } 18 | move-binary-format = { git = "https://github.com/MystenLabs/sui", tag = "devnet-v1.27.0", package = "move-binary-format" } 19 | move-symbol-pool = { git = "https://github.com/MystenLabs/sui", tag = "devnet-v1.27.0", package = "move-symbol-pool" } 20 | move-transactional-test-runner = { git = "https://github.com/MystenLabs/sui", tag = "devnet-v1.27.0", package = "move-transactional-test-runner" } 21 | 22 | sui-types = { git = "https://github.com/MystenLabs/sui", tag = "devnet-v1.27.0", package = "sui-types"} 23 | sui-transactional-test-runner = { git = "https://github.com/MystenLabs/sui", tag = "devnet-v1.27.0", package = "sui-transactional-test-runner"} 24 | 25 | sui-ctf-framework = { git = "https://github.com/otter-sec/sui-ctf-framework", branch = "justctf2024" } 26 | -------------------------------------------------------------------------------- /writeup/justctf2024/tos/sources/framework/chall/Move.lock: -------------------------------------------------------------------------------- 1 | # @generated by Move, please check-in and do not edit manually. 2 | 3 | [move] 4 | version = 2 5 | manifest_digest = "C1FECA3B112600A783F9F421CC2BF9CE00CE66320440DD4E76D8CD5B87B3AD2C" 6 | deps_digest = "F8BBB0CCB2491CA29A3DF03D6F92277A4F3574266507ACD77214D37ECA3F3082" 7 | dependencies = [ 8 | { name = "Sui" }, 9 | ] 10 | 11 | [[move.package]] 12 | name = "MoveStdlib" 13 | source = { git = "https://github.com/MystenLabs/sui.git", rev = "devnet-v1.27.0", subdir = "crates/sui-framework/packages/move-stdlib" } 14 | 15 | [[move.package]] 16 | name = "Sui" 17 | source = { git = "https://github.com/MystenLabs/sui.git", rev = "devnet-v1.27.0", subdir = "crates/sui-framework/packages/sui-framework" } 18 | 19 | dependencies = [ 20 | { name = "MoveStdlib" }, 21 | ] 22 | 23 | [move.toolchain-version] 24 | compiler-version = "1.27.0" 25 | edition = "2024.beta" 26 | flavor = "sui" 27 | -------------------------------------------------------------------------------- /writeup/justctf2024/tos/sources/framework/chall/Move.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "challenge" 3 | version = "0.0.1" 4 | edition = "2024.beta" 5 | 6 | [dependencies] 7 | Sui = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "devnet-v1.27.0" } 8 | 9 | [addresses] 10 | challenge = "0x0" 11 | admin = "0xfccc9a421bbb13c1a66a1aa98f0ad75029ede94857779c6915b44f94068b921e" 12 | -------------------------------------------------------------------------------- /writeup/justctf2024/tos/sources/framework/chall/sources/the_otter_scrolls.move: -------------------------------------------------------------------------------- 1 | module challenge::theotterscrolls { 2 | 3 | // --------------------------------------------------- 4 | // DEPENDENCIES 5 | // --------------------------------------------------- 6 | 7 | use sui::table::{Self, Table}; 8 | use std::string::{Self, String}; 9 | use std::debug; 10 | 11 | // --------------------------------------------------- 12 | // STRUCTS 13 | // --------------------------------------------------- 14 | 15 | public struct Spellbook has key { 16 | id: UID, 17 | casted: bool, 18 | spells: Table> 19 | } 20 | 21 | // --------------------------------------------------- 22 | // FUNCTIONS 23 | // --------------------------------------------------- 24 | 25 | //The spell consists of five magic words, which have to be read in the correct order! 26 | 27 | fun init(ctx: &mut TxContext) { 28 | 29 | let mut all_words = table::new(ctx); 30 | 31 | let fire = vector[ 32 | string::utf8(b"Blast"), 33 | string::utf8(b"Inferno"), 34 | string::utf8(b"Pyre"), 35 | string::utf8(b"Fenix"), 36 | string::utf8(b"Ember") 37 | ]; 38 | 39 | let wind = vector[ 40 | string::utf8(b"Zephyr"), 41 | string::utf8(b"Swirl"), 42 | string::utf8(b"Breeze"), 43 | string::utf8(b"Gust"), 44 | string::utf8(b"Sigil") 45 | ]; 46 | 47 | let water = vector[ 48 | string::utf8(b"Aquarius"), 49 | string::utf8(b"Mistwalker"), 50 | string::utf8(b"Waves"), 51 | string::utf8(b"Call"), 52 | string::utf8(b"Storm") 53 | ]; 54 | 55 | let earth = vector[ 56 | string::utf8(b"Tremor"), 57 | string::utf8(b"Stoneheart"), 58 | string::utf8(b"Grip"), 59 | string::utf8(b"Granite"), 60 | string::utf8(b"Mudslide") 61 | ]; 62 | 63 | let power = vector[ 64 | string::utf8(b"Alakazam"), 65 | string::utf8(b"Hocus"), 66 | string::utf8(b"Pocus"), 67 | string::utf8(b"Wazzup"), 68 | string::utf8(b"Wrath") 69 | ]; 70 | 71 | table::add(&mut all_words, 0, fire); 72 | table::add(&mut all_words, 1, wind); 73 | table::add(&mut all_words, 2, water); 74 | table::add(&mut all_words, 3, earth); 75 | table::add(&mut all_words, 4, power); 76 | 77 | let spellbook = Spellbook { 78 | id: object::new(ctx), 79 | casted: false, 80 | spells: all_words 81 | }; 82 | 83 | transfer::share_object(spellbook); 84 | } 85 | 86 | public fun cast_spell(spell_sequence: vector, book: &mut Spellbook) { 87 | 88 | let fire = table::remove(&mut book.spells, 0); 89 | let wind = table::remove(&mut book.spells, 1); 90 | let water = table::remove(&mut book.spells, 2); 91 | let earth = table::remove(&mut book.spells, 3); 92 | let power = table::remove(&mut book.spells, 4); 93 | 94 | let fire_word_id = *vector::borrow(&spell_sequence, 0); 95 | let wind_word_id = *vector::borrow(&spell_sequence, 1); 96 | let water_word_id = *vector::borrow(&spell_sequence, 2); 97 | let earth_word_id = *vector::borrow(&spell_sequence, 3); 98 | let power_word_id = *vector::borrow(&spell_sequence, 4); 99 | 100 | let fire_word = vector::borrow(&fire, fire_word_id); 101 | let wind_word = vector::borrow(&wind, wind_word_id); 102 | let water_word = vector::borrow(&water, water_word_id); 103 | let earth_word = vector::borrow(&earth, earth_word_id); 104 | let power_word = vector::borrow(&power, power_word_id); 105 | 106 | if (fire_word == string::utf8(b"Inferno")) { 107 | if (wind_word == string::utf8(b"Zephyr")) { 108 | if (water_word == string::utf8(b"Call")) { 109 | if (earth_word == string::utf8(b"Granite")) { 110 | if (power_word == string::utf8(b"Wazzup")) { 111 | book.casted = true; 112 | } 113 | } 114 | } 115 | } 116 | } 117 | 118 | } 119 | 120 | public fun check_if_spell_casted(book: &Spellbook): bool { 121 | let casted = book.casted; 122 | assert!(casted == true, 1337); 123 | casted 124 | } 125 | 126 | } 127 | 128 | -------------------------------------------------------------------------------- /writeup/justctf2024/tos/sources/framework/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | sed -i \ 4 | -E 's/(host[[:space:]]+all[[:space:]]+all[[:space:]]+127.0.0.1\/32[[:space:]]+)scram-sha-256/\1trust/' \ 5 | /etc/postgresql/16/main/pg_hba.conf 6 | 7 | sed -i \ 8 | -E 's/(host[[:space:]]+all[[:space:]]+all[[:space:]]+::1\/128[[:space:]]+)scram-sha-256/\1trust/' \ 9 | /etc/postgresql/16/main/pg_hba.conf 10 | 11 | /etc/init.d/postgresql restart 12 | 13 | sudo -u postgres psql postgres -c "ALTER ROLE postgres WITH SUPERUSER LOGIN PASSWORD 'postgrespw';" 14 | sudo -u postgres psql postgres -c "CREATE DATABASE sui_indexer_v2;" -c "ALTER SYSTEM SET max_connections = 500;" 15 | 16 | cargo r --release 17 | -------------------------------------------------------------------------------- /writeup/justctf2024/tos/sources/framework/rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.76" 3 | -------------------------------------------------------------------------------- /writeup/justctf2024/tos/sources/framework/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::error::Error; 3 | use std::fmt; 4 | use std::io::{Read, Write}; 5 | use std::mem::drop; 6 | use std::net::{TcpListener, TcpStream}; 7 | use std::path::Path; 8 | 9 | use tokio; 10 | 11 | use move_transactional_test_runner::framework::{MaybeNamedCompiledModule, MoveTestAdapter}; 12 | use move_bytecode_source_map::{source_map::SourceMap, utils::source_map_from_file}; 13 | use move_binary_format::file_format::CompiledModule; 14 | use move_symbol_pool::Symbol; 15 | use move_core_types::{ 16 | account_address::AccountAddress, 17 | language_storage::TypeTag, 18 | }; 19 | 20 | use sui_ctf_framework::NumericalAddress; 21 | use sui_transactional_test_runner::{args::SuiValue, test_adapter::FakeID}; 22 | 23 | async fn handle_client(mut stream: TcpStream) -> Result<(), Box> { 24 | 25 | // Initialize SuiTestAdapter 26 | let modules = vec!["theotterscrolls"]; 27 | let mut deployed_modules: Vec = Vec::new(); 28 | 29 | let named_addresses = vec![ 30 | ( 31 | "challenge".to_string(), 32 | NumericalAddress::parse_str( 33 | "0x0", 34 | )?, 35 | ), 36 | ( 37 | "solve".to_string(), 38 | NumericalAddress::parse_str( 39 | "0x0", 40 | )?, 41 | ), 42 | ( 43 | "admin".to_string(), 44 | NumericalAddress::parse_str( 45 | "0xfccc9a421bbb13c1a66a1aa98f0ad75029ede94857779c6915b44f94068b921e", 46 | )?, 47 | ), 48 | ]; 49 | 50 | let mut adapter = sui_ctf_framework::initialize( 51 | named_addresses, 52 | Some(vec!["challenger".to_string(), "solver".to_string()]), 53 | ).await; 54 | 55 | // Check Admin Account 56 | let object_output1 = sui_ctf_framework::view_object(&mut adapter, FakeID::Enumerated(0, 0)).await; 57 | println!("Object Output: {:#?}", object_output1); 58 | 59 | let mut mncp_modules : Vec = Vec::new(); 60 | 61 | for i in 0..modules.len() { 62 | 63 | let module = &modules[i]; 64 | 65 | let mod_path = format!("./chall/build/challenge/bytecode_modules/{}.mv", module); 66 | let src_path = format!("./chall/build/challenge/source_maps/{}.mvsm", module); 67 | let mod_bytes: Vec = std::fs::read(mod_path)?; 68 | 69 | let module: CompiledModule = match CompiledModule::deserialize_with_defaults(&mod_bytes) { 70 | Ok(data) => data, 71 | Err(e) => { 72 | return Err(Box::new(e)) 73 | } 74 | }; 75 | let named_addr_opt: Option = Some(Symbol::from("challenge")); 76 | let source_map: Option = match source_map_from_file(Path::new(&src_path)) { 77 | Ok(data) => Some(data), 78 | Err(e) => { 79 | let _ = adapter.cleanup_resources().await; 80 | println!("error: {:?}, src_path: {}", e, src_path); 81 | return Err("error when generating source map".into()) 82 | } 83 | }; 84 | 85 | let maybe_ncm = MaybeNamedCompiledModule { 86 | named_address: named_addr_opt, 87 | module, 88 | source_map, 89 | }; 90 | 91 | mncp_modules.push( maybe_ncm ); 92 | } 93 | 94 | // Publish Challenge Module 95 | let chall_dependencies: Vec = Vec::new(); 96 | let chall_addr = sui_ctf_framework::publish_compiled_module( 97 | &mut adapter, 98 | mncp_modules, 99 | chall_dependencies, 100 | Some(String::from("challenger")), 101 | ).await; 102 | deployed_modules.push(chall_addr); 103 | println!("[SERVER] Module published at: {:?}", chall_addr); 104 | 105 | let mut solution_data = [0 as u8; 2000]; 106 | let _solution_size = stream.read(&mut solution_data)?; 107 | 108 | // Send Challenge Address 109 | let mut output = String::new(); 110 | fmt::write( 111 | &mut output, 112 | format_args!( 113 | "[SERVER] Challenge modules published at: {}", 114 | chall_addr.to_string().as_str(), 115 | ), 116 | ) 117 | .unwrap(); 118 | stream.write(output.as_bytes()).unwrap(); 119 | 120 | // Publish Solution Module 121 | let mut sol_dependencies: Vec = Vec::new(); 122 | sol_dependencies.push(String::from("challenge")); 123 | 124 | let mut mncp_solution : Vec = Vec::new(); 125 | let module: CompiledModule = match CompiledModule::deserialize_with_defaults(&solution_data.to_vec()) { 126 | Ok(data) => data, 127 | Err(e) => { 128 | let _ = adapter.cleanup_resources().await; 129 | return Err(Box::new(e)) 130 | } 131 | }; 132 | let named_addr_opt: Option = Some(Symbol::from("solve")); 133 | let source_map : Option = None; 134 | 135 | let maybe_ncm = MaybeNamedCompiledModule { 136 | named_address: named_addr_opt, 137 | module, 138 | source_map, 139 | }; 140 | mncp_solution.push( maybe_ncm ); 141 | 142 | let sol_addr = sui_ctf_framework::publish_compiled_module( 143 | &mut adapter, 144 | mncp_solution, 145 | sol_dependencies, 146 | Some(String::from("solver")), 147 | ).await; 148 | println!("[SERVER] Solution published at: {:?}", sol_addr); 149 | 150 | // Send Solution Address 151 | output = String::new(); 152 | fmt::write( 153 | &mut output, 154 | format_args!( 155 | "[SERVER] Solution published at {}", 156 | sol_addr.to_string().as_str() 157 | ), 158 | ) 159 | .unwrap(); 160 | stream.write(output.as_bytes()).unwrap(); 161 | 162 | // Prepare Function Call Arguments 163 | let mut args_solve: Vec = Vec::new(); 164 | let spellbook = SuiValue::Object(FakeID::Enumerated(2, 0), None); 165 | args_solve.push(spellbook.clone()); 166 | 167 | let type_args_solve: Vec = Vec::new(); 168 | 169 | // Call solve Function 170 | let ret_val = match sui_ctf_framework::call_function( 171 | &mut adapter, 172 | sol_addr, 173 | "solve", 174 | "solve", 175 | args_solve, 176 | type_args_solve, 177 | Some("solver".to_string()), 178 | ).await { 179 | Ok(output) => output, 180 | Err(e) => { 181 | let _ = adapter.cleanup_resources().await; 182 | println!("[SERVER] error: {e}"); 183 | return Err("error during call to solve::solve".into()) 184 | } 185 | }; 186 | println!("[SERVER] Return value {:#?}", ret_val); 187 | println!(""); 188 | 189 | // Check Solution 190 | let mut args_check: Vec = Vec::new(); 191 | args_check.push(spellbook.clone()); 192 | let type_args_check: Vec = Vec::new(); 193 | 194 | let sol_ret = sui_ctf_framework::call_function( 195 | &mut adapter, 196 | chall_addr, 197 | "theotterscrolls", 198 | "check_if_spell_casted", 199 | args_check, 200 | type_args_check, 201 | Some("solver".to_string()), 202 | ).await; 203 | println!("[SERVER] Return value {:#?}", sol_ret); 204 | println!(""); 205 | 206 | // Validate Solution 207 | match sol_ret { 208 | Ok(_) => { 209 | println!("[SERVER] Correct Solution!"); 210 | println!(""); 211 | if let Ok(flag) = env::var("FLAG") { 212 | let message = format!("[SERVER] Congrats, flag: {}", flag); 213 | stream.write(message.as_bytes()).unwrap(); 214 | } else { 215 | stream.write("[SERVER] Flag not found, please contact admin".as_bytes()).unwrap(); 216 | } 217 | } 218 | Err(_) => { 219 | println!("[SERVER] Invalid Solution!"); 220 | println!(""); 221 | stream.write("[SERVER] Invalid Solution!".as_bytes()).unwrap(); 222 | } 223 | }; 224 | 225 | let _ = adapter.cleanup_resources().await; 226 | Ok(()) 227 | } 228 | 229 | #[tokio::main] 230 | async fn main() -> Result<(), Box> { 231 | // Create Socket - Port 31337 232 | let listener = TcpListener::bind("0.0.0.0:31337")?; 233 | println!("[SERVER] Starting server at port 31337!"); 234 | 235 | let local = tokio::task::LocalSet::new(); 236 | 237 | // Wait For Incoming Solution 238 | for stream in listener.incoming() { 239 | match stream { 240 | Ok(stream) => { 241 | println!("[SERVER] New connection: {}", stream.peer_addr()?); 242 | let result = local.run_until( async move { 243 | tokio::task::spawn_local( async { 244 | handle_client(stream).await 245 | }).await 246 | }).await; 247 | println!("[SERVER] Result: {:?}", result); 248 | } 249 | Err(e) => { 250 | println!("[SERVER] Error: {}", e); 251 | } 252 | } 253 | } 254 | 255 | // Close Socket Server 256 | drop(listener); 257 | Ok(()) 258 | } 259 | -------------------------------------------------------------------------------- /writeup/justctf2024/tos/sources/run_client.sh: -------------------------------------------------------------------------------- 1 | set -eux 2 | 3 | cd framework-solve/solve && sui move build 4 | cd .. 5 | cargo r --release 6 | -------------------------------------------------------------------------------- /writeup/justctf2024/tos/sources/run_server.sh: -------------------------------------------------------------------------------- 1 | set -eux 2 | 3 | cd framework/chall && sui move build 4 | cd .. 5 | cargo r --release 6 | -------------------------------------------------------------------------------- /writeup/justctf2024/woo/.dockerignore: -------------------------------------------------------------------------------- 1 | **/target 2 | **/build 3 | -------------------------------------------------------------------------------- /writeup/justctf2024/woo/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM embe221ed/otter_template:latest 2 | 3 | ADD ./sources/framework/chall /work/framework/chall 4 | ADD ./sources/framework/src/main.rs /work/framework/src/ 5 | 6 | # build the challenge contracts 7 | WORKDIR /work/framework/chall 8 | RUN sui move build 9 | 10 | WORKDIR /work/framework 11 | 12 | # build the framework 13 | RUN touch src/main.rs 14 | RUN cargo build --locked --release 15 | 16 | CMD ./run.sh 17 | -------------------------------------------------------------------------------- /writeup/justctf2024/woo/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | woo: 3 | environment: 4 | FLAG: justCTF{REDACTED} 5 | PORT: 31337 6 | build: 7 | context: ./ 8 | dockerfile: ./Dockerfile 9 | ports: 10 | - "31337:31337" 11 | restart: always 12 | -------------------------------------------------------------------------------- /writeup/justctf2024/woo/solve.move: -------------------------------------------------------------------------------- 1 | #https://ctftime.org/writeup/39192 2 | module solve::solve { 3 | 4 | // [*] Import dependencies 5 | use challenge::Otter::{Self, OTTER}; 6 | 7 | public fun solve( 8 | _board: &mut Otter::QuestBoard, 9 | _vault: &mut Otter::Vault, 10 | _player: &mut Otter::Player, 11 | _ctx: &mut TxContext 12 | ) { 13 | let mut ticket = Otter::enter_tavern(_player); 14 | Otter::buy_sword(_player, &mut ticket); 15 | Otter::checkout(ticket, _player, _ctx, _vault, _board); 16 | 17 | let mut i = 0; 18 | while (i < 25) { 19 | Otter::find_a_monster(_board, _player); 20 | i = i + 1; 21 | }; 22 | 23 | Otter::bring_it_on(_board, _player, 0); 24 | Otter::return_home(_board, _player); 25 | Otter::get_the_reward(_vault, _board, _player, _ctx); 26 | 27 | let mut i = 0; 28 | while (i < 24) { 29 | let mut ticket = Otter::enter_tavern(_player); 30 | Otter::buy_shield(_player, &mut ticket); 31 | Otter::get_the_reward(_vault, _board, _player, _ctx); 32 | Otter::checkout(ticket, _player, _ctx, _vault, _board); 33 | i = i + 1; 34 | }; 35 | 36 | let mut ticket = Otter::enter_tavern(_player); 37 | Otter::buy_flag(&mut ticket, _player); 38 | Otter::checkout(ticket, _player, _ctx, _vault, _board); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /writeup/justctf2024/woo/sources/framework-solve/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "solve-framework" 7 | version = "0.1.0" 8 | -------------------------------------------------------------------------------- /writeup/justctf2024/woo/sources/framework-solve/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "solve-framework" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | -------------------------------------------------------------------------------- /writeup/justctf2024/woo/sources/framework-solve/dependency/Move.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "challenge" 3 | version = "0.0.1" 4 | edition = "2024.beta" 5 | 6 | [dependencies] 7 | Sui = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "devnet-v1.27.0" } 8 | 9 | [addresses] 10 | admin = "0xfccc9a421bbb13c1a66a1aa98f0ad75029ede94857779c6915b44f94068b921e" 11 | challenge = "" 12 | -------------------------------------------------------------------------------- /writeup/justctf2024/woo/sources/framework-solve/solve/Move.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "solve" 3 | version = "0.0.1" 4 | edition = "2024.alpha" 5 | 6 | [dependencies] 7 | Sui = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "devnet-v1.27.0" } 8 | 9 | [dependencies.challenge] 10 | version = '1.0.0' 11 | local = '../dependency' 12 | 13 | [addresses] 14 | solve = "0x0" 15 | -------------------------------------------------------------------------------- /writeup/justctf2024/woo/sources/framework-solve/solve/sources/solve.move: -------------------------------------------------------------------------------- 1 | module solve::solve { 2 | 3 | // [*] Import dependencies 4 | use challenge::Otter::{Self, OTTER}; 5 | 6 | public fun solve( 7 | _board: &mut Otter::QuestBoard, 8 | _vault: &mut Otter::Vault, 9 | _player: &mut Otter::Player, 10 | _ctx: &mut TxContext 11 | ) { 12 | // Your code here... 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /writeup/justctf2024/woo/sources/framework-solve/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::net::TcpStream; 2 | use std::io::{Read, Write}; 3 | use std::str::from_utf8; 4 | use std::{error::Error, fs}; 5 | use std::env; 6 | 7 | fn main() -> Result<(), Box> { 8 | 9 | let host = env::var("HOST").unwrap_or_else(|_| "127.0.0.1".to_string()); // replace with remote ip 10 | let port = env::var("PORT").unwrap_or_else(|_| "31337".to_string()); 11 | 12 | match TcpStream::connect(format!("{}:{}", host, port)) { 13 | Ok(mut stream) => { 14 | println!(" - Connected!"); 15 | 16 | let mod_data : Vec = fs::read("./solve/build/solve/bytecode_modules/solve.mv").unwrap(); 17 | println!(" - Loaded solution!"); 18 | 19 | stream.write_all(&mod_data)?; 20 | stream.flush()?; 21 | println!(" - Sent solution!"); 22 | 23 | let mut return_data1 = [0 as u8; 200]; 24 | match stream.read(&mut return_data1) { 25 | Ok(_) => { 26 | println!(" - Connection Output: '{}'", from_utf8(&return_data1).unwrap()); // Get module address 27 | let mut return_data2 = [0 as u8; 200]; 28 | match stream.read(&mut return_data2) { 29 | Ok(_) => { 30 | println!(" - Connection Output: '{}'", from_utf8(&return_data2).unwrap()); // Get module address 31 | let mut flag = [0 as u8; 200]; 32 | match stream.read(&mut flag) { 33 | Ok(_) => { 34 | println!(" - Connection Output: '{}'", from_utf8(&flag).unwrap()); // Get flag 35 | 36 | }, 37 | Err(e) => { 38 | println!(" - Failed to receive data: {}", e); 39 | } 40 | } 41 | }, 42 | Err(e) => { 43 | println!(" - Failed to receive data: {}", e); 44 | } 45 | } 46 | }, 47 | Err(e) => { 48 | println!(" - Failed to connect: {}", e); 49 | } 50 | } 51 | }, 52 | Err(e) => { 53 | println!(" - Failed to connect: {}", e); 54 | } 55 | } 56 | println!(" - Terminated."); 57 | 58 | Ok(()) 59 | } 60 | -------------------------------------------------------------------------------- /writeup/justctf2024/woo/sources/framework/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "framework" 3 | version = "0.1.0" 4 | edition = "2021" 5 | exclude = ["chall/"] 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | quote = "1.0.26" 11 | threadpool = "1.8.1" 12 | proc-macro2 = "1.0.66" 13 | 14 | tokio = { version = "1", features = ["full"] } 15 | 16 | move-core-types = { git = "https://github.com/MystenLabs/sui", tag = "devnet-v1.27.0", package = "move-core-types" } 17 | move-bytecode-source-map = { git = "https://github.com/MystenLabs/sui", tag = "devnet-v1.27.0", package = "move-bytecode-source-map" } 18 | move-binary-format = { git = "https://github.com/MystenLabs/sui", tag = "devnet-v1.27.0", package = "move-binary-format" } 19 | move-symbol-pool = { git = "https://github.com/MystenLabs/sui", tag = "devnet-v1.27.0", package = "move-symbol-pool" } 20 | move-transactional-test-runner = { git = "https://github.com/MystenLabs/sui", tag = "devnet-v1.27.0", package = "move-transactional-test-runner" } 21 | 22 | sui-types = { git = "https://github.com/MystenLabs/sui", tag = "devnet-v1.27.0", package = "sui-types"} 23 | sui-transactional-test-runner = { git = "https://github.com/MystenLabs/sui", tag = "devnet-v1.27.0", package = "sui-transactional-test-runner"} 24 | 25 | sui-ctf-framework = { git = "https://github.com/otter-sec/sui-ctf-framework", branch = "justctf2024" } 26 | -------------------------------------------------------------------------------- /writeup/justctf2024/woo/sources/framework/chall/Move.lock: -------------------------------------------------------------------------------- 1 | # @generated by Move, please check-in and do not edit manually. 2 | 3 | [move] 4 | version = 2 5 | manifest_digest = "C1FECA3B112600A783F9F421CC2BF9CE00CE66320440DD4E76D8CD5B87B3AD2C" 6 | deps_digest = "F8BBB0CCB2491CA29A3DF03D6F92277A4F3574266507ACD77214D37ECA3F3082" 7 | dependencies = [ 8 | { name = "Sui" }, 9 | ] 10 | 11 | [[move.package]] 12 | name = "MoveStdlib" 13 | source = { git = "https://github.com/MystenLabs/sui.git", rev = "devnet-v1.27.0", subdir = "crates/sui-framework/packages/move-stdlib" } 14 | 15 | [[move.package]] 16 | name = "Sui" 17 | source = { git = "https://github.com/MystenLabs/sui.git", rev = "devnet-v1.27.0", subdir = "crates/sui-framework/packages/sui-framework" } 18 | 19 | dependencies = [ 20 | { name = "MoveStdlib" }, 21 | ] 22 | 23 | [move.toolchain-version] 24 | compiler-version = "1.27.0" 25 | edition = "2024.beta" 26 | flavor = "sui" 27 | -------------------------------------------------------------------------------- /writeup/justctf2024/woo/sources/framework/chall/Move.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "challenge" 3 | version = "0.0.1" 4 | edition = "2024.beta" 5 | 6 | [dependencies] 7 | Sui = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "devnet-v1.27.0" } 8 | 9 | [addresses] 10 | challenge = "0x0" 11 | admin = "0xfccc9a421bbb13c1a66a1aa98f0ad75029ede94857779c6915b44f94068b921e" 12 | -------------------------------------------------------------------------------- /writeup/justctf2024/woo/sources/framework/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | sed -i \ 4 | -E 's/(host[[:space:]]+all[[:space:]]+all[[:space:]]+127.0.0.1\/32[[:space:]]+)scram-sha-256/\1trust/' \ 5 | /etc/postgresql/16/main/pg_hba.conf 6 | 7 | sed -i \ 8 | -E 's/(host[[:space:]]+all[[:space:]]+all[[:space:]]+::1\/128[[:space:]]+)scram-sha-256/\1trust/' \ 9 | /etc/postgresql/16/main/pg_hba.conf 10 | 11 | /etc/init.d/postgresql restart 12 | 13 | sudo -u postgres psql postgres -c "ALTER ROLE postgres WITH SUPERUSER LOGIN PASSWORD 'postgrespw';" 14 | sudo -u postgres psql postgres -c "CREATE DATABASE sui_indexer_v2;" -c "ALTER SYSTEM SET max_connections = 500;" 15 | 16 | cargo r --release 17 | -------------------------------------------------------------------------------- /writeup/justctf2024/woo/sources/framework/rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.76" 3 | -------------------------------------------------------------------------------- /writeup/justctf2024/woo/sources/run_client.sh: -------------------------------------------------------------------------------- 1 | set -eux 2 | 3 | cd framework-solve/solve && sui move build 4 | cd .. 5 | cargo r --release 6 | -------------------------------------------------------------------------------- /writeup/justctf2024/woo/sources/run_server.sh: -------------------------------------------------------------------------------- 1 | set -eux 2 | 3 | cd framework/chall && sui move build 4 | cd .. 5 | cargo r --release 6 | --------------------------------------------------------------------------------