Sec3的高级安全研究员Q7在2023 MetaTrust CTF的CFT-Sui赛道中赢得了第一名,文章深入解析了四个Sui Move挑战,包括其原理、实现与解决方案,适合想要提升智能合约安全技能的读者。
上周,Sec3高级安全研究员 Q7 参加了 2023 MetaTrust Web3 Security CTF 的 CTF-Sui 赛道。在与近600支队伍的竞争中,Q7凭借两次首次击杀和两次第二次击杀获得了第一名。该活动由 Mysten Labs 和其他组织者共同举办,设有四个独特的 Sui Move 挑战。向组织者致敬,感谢他们创造如此引人入胜且富有教育意义的体验!
在这篇博客文章中,我们将深入探讨这些挑战,提供解决方案和见解。所以,让我们开始吧!
与你的道格拉斯·亚当斯打个招呼。
顾名思义,挑战 1 是一个理智检查,旨在让玩家熟悉 Sui 和 CTF 框架。
/* https://github.com/MetaTrustLabs/ctf/blob/master/hellowWorld/framework/src/main.rs#L100-L114 */
// 检查解决方案
let mut args2: Vec<SuiValue> = Vec::new();
let arg_ob2 = SuiValue::Object(FakeID::Enumerated(1, 0));
args2.push(arg_ob2);
let ret_val = sui_ctf_framework::call_function(
&mut adapter,
chall_addr,
chall,
"is_owner",
args2,
Some("challenger".to_string()),
);
println!("[SERVER] 返回值 {:#?}", ret_val);
println!("");
从上面的框架代码中,我们可以看到此挑战通过调用 `is_owner` 函数来检查玩家是否成功解决挑战,通过检查 `status` 并评估返回值。
/* https://github.com/MetaTrustLabs/ctf/blob/master/hellowWorld/framework/chall/sources/hello_world.move#L26-L38 */
// [*] 公共函数
public entry fun answer_to_life(status: &mut Status, answer : vector<u8>) {
// 生命的答案是什么?
let actual = x"2f0039e93a27221fcf657fb877a1d4f60307106113e885096cb44a461cd0afbf";
let answer_hash: vector<u8> = hash::blake2b256(&answer);
assert!(actual == answer_hash, ERR_INVALID_CODE);
status.solved = true;
}
public entry fun is_owner(status: &mut Status) {
assert!(status.solved == true, 0);
}
从合约源码中可以明显看出,将 `status` 中的 `solved` 字段设置为 `true` 需要调用 `answer_to_life` 函数。该函数要求用户提供 `status` 和一个名为 answer 的 `u8` 向量。然后,它使用 `blake2b256` 哈希答案并将其与预定义哈希进行比较。如果匹配, `solved` 字段将被设置为 true。
该哈希显然是不可逆的,但根据注释中的提示,“生命的答案是什么?” 我们可以推测答案是 42,正如在《银河系漫游指南》中所揭示的。
module solution::hello_world_solution {
use challenge::hello_world;
public entry fun solve(status: &mut hello_world::Status) {
let answer : vector<u8> = vector[52,50]; // "42" 的ascii
challenge::hello_world::answer_to_life(status, answer);
}
}
保持你朋友的接近,敌人更近。
/* https://github.com/MetaTrustLabs/ctf/blob/master/friendlyFire/framework/chall/sources/friendly_fire.move#L27-L40 */
// [*] 公共函数
public(friend) fun get_flag(status: &mut Status) {
status.solved = true;
}
public entry fun is_owner(status: &mut Status) {
assert!(status.solved == true, 0);
}
public entry fun prestige(status: &mut Status, ctxSender: String, _ctx: &mut TxContext) {
// let digest: &vector<u8> = tx_context::digest(_ctx);
assert!(ctxSender == std::string::utf8(b"0x31337690420"), ERR_INVALID_CODE) ;
get_flag(status);
}
这个挑战与前一个相似,因为它也要求我们将 `status` 的 `solved` 字段设置为 true。然而,在这个挑战中,可以修改 `status` 的函数是 `get_flag`。由于 `friend` 机制的限制,我们只能通过 `prestige` 函数调用 `get_flag`。为此,用户需要将 `ctxSender` 的值设置为 `0x31337690420`。
由于合约对 `ctxSender` 的用户输入没有进行任何检查(这似乎相当奇怪),因此只需提供请求的值即可。
module solution::friendly_fire_solution {
use sui::tx_context::TxContext;
use challenge::friendly_fire;
public entry fun solve(status: &mut friendly_fire::Status, ctx: &mut TxContext) {
challenge::friendly_fire::prestige(status, std::string::utf8(b"0x31337690420"), ctx);
}
}
顾客刚刚从秘密菜单上点了一单,但没有员工知道如何做秘密汉堡。你能帮忙吗?
这个挑战提出了一个有趣的难题,合约实现了 `place_order` 和 `deliver_order` 函数,以模拟餐厅的点餐和上菜过程。在这里,一个汉堡可以由五种成分组成:蛋黄酱、生菜、鸡肉肉排、奶酪和面包。
/* https://github.com/MetaTrustLabs/ctf/blob/master/McChicken/framework/chall/sources/mc_chicken.move#L19-L23 */
struct Mayo has store, copy, drop { calories : u16 }
struct Lettuce has store, copy, drop { calories : u16 }
struct ChickenSchnitzel has store, copy, drop { calories : u16 }
struct Cheese has store, copy, drop { calories : u16 }
struct Bun has store, copy, drop { calories : u16 }
/* https://github.com/MetaTrustLabs/ctf/blob/master/McChicken/framework/chall/sources/mc_chicken.move#L69-L87 */
public fun get_mayo ( _chef: &mut ChefCapability ) : Mayo {
Mayo { calories: 679 }
}
public fun get_lettuce ( _chef: &mut ChefCapability ) : Lettuce {
Lettuce { calories: 14 }
}
public fun get_chicken_schnitzel ( _chef: &mut ChefCapability ) : ChickenSchnitzel {
ChickenSchnitzel { calories: 297 }
}
public fun get_cheese ( _chef: &mut ChefCapability ) : Cheese {
Cheese { calories: 420 }
}
public fun get_bun ( _chef: &mut ChefCapability ) : Bun {
Bun { calories: 120 }
}
有趣的是,当用户为汉堡下订单时,他们提供一个表示汉堡的序列化字节序列。作为负责上餐的厨师,我们需要反序列化顾客的订单,以获取他们所需的汉堡的配方。
在 BCS 编码的背景下,有一个有趣的事实需要注意:结构的序列化结果本质上是其所有字段序列化结果的组合。在我们当前的情境中,序列化一个汉堡意味着将组成部分的多个 u16 卡路里值连接在一起。
/* https://github.com/MetaTrustLabs/ctf/blob/master/McChicken/framework/src/main.rs#L84-L112 */
// 下订单1
let mut order_args : Vec<SuiValue> = Vec::new();
let order_args_1 = SuiValue::Object(FakeID::Enumerated(3, 0), None);
let recepit1 = Vec::from(
[MoveValue::U8(0x78),\
MoveValue::U8(0x00),\
MoveValue::U8(0xa7),\
MoveValue::U8(0x02),\
MoveValue::U8(0x0e),\
MoveValue::U8(0x00),\
MoveValue::U8(0x29),\
MoveValue::U8(0x01),\
MoveValue::U8(0xa4),\
MoveValue::U8(0x01),\
MoveValue::U8(0x78),\
MoveValue::U8(0x00)]);
order_args.push(order_args_1);
order_args.push(SuiValue::MoveValue(MoveValue::Vector(recepit1)));
let ret_val = sui_ctf_framework::call_function(
&mut adapter,
chall_addr,
"mc_chicken",
"place_order",
order_args,
Some("customer".to_string())
).await;
println!("[SERVER] 返回值 {:#?}", ret_val);
println!("");
以订单1为例,其序列化结果的长度为12。因此,我们可以尝试将其反序列化为六个 u16 数字:
In [1]: import struct
In [2]: struct.unpack("<6H", b"\x78\x00\xa7\x02\x0e\x00\x29\x01\xa4\x01\x78\x00")
Out[2]: (120, 679, 14, 297, 420, 120)
根据合约源码中的成分卡路里设定,我们可以推断出第一个汉堡的配方如下:面包、蛋黄酱、生菜、鸡肉肉排、奶酪、面包。类似地,我们可以推导出第二个的配方。通过使用 `ChefCapability` 权限“创造”汉堡的各个成分,我们可以使用两个包装结构来表示这两个汉堡,然后进行上菜。
module solution::mc_chicken_solution {
// [*] 导入依赖
use sui::tx_context::TxContext;
use challenge::mc_chicken;
struct Order1Burger has store, drop {
bun: mc_chicken::Bun,
mayo: mc_chicken::Mayo,
lettuce: mc_chicken::Lettuce,
chicken_schnitzel: mc_chicken::ChickenSchnitzel,
cheese: mc_chicken::Cheese,
bun2: mc_chicken::Bun,
}
struct Order2Burger has store, drop {
bun: mc_chicken::Bun,
cheese: mc_chicken::Cheese,
cheese2: mc_chicken::Cheese,
chicken_schnitzel: mc_chicken::ChickenSchnitzel,
cheese3: mc_chicken::Cheese,
chicken_schnitzel2: mc_chicken::ChickenSchnitzel,
cheese4: mc_chicken::Cheese,
chicken_schnitzel3: mc_chicken::ChickenSchnitzel,
cheese5: mc_chicken::Cheese,
cheese6: mc_chicken::Cheese,
bun2: mc_chicken::Bun,
}
// [*] 公共函数
public fun solve(chef: &mut mc_chicken::ChefCapability, order1: &mut mc_chicken::Order,
order2: &mut mc_chicken::Order, ctx: &mut TxContext) {
let burger1 = Order1Burger {
bun: mc_chicken::get_bun(chef),
mayo: mc_chicken::get_mayo(chef),
lettuce: mc_chicken::get_lettuce(chef),
chicken_schnitzel: mc_chicken::get_chicken_schnitzel(chef),
cheese: mc_chicken::get_cheese(chef),
bun2: mc_chicken::get_bun(chef),
};
mc_chicken::deliver_order(chef, order1, burger1, ctx);
let burger2 = Order2Burger {
bun: mc_chicken::get_bun(chef),
cheese: mc_chicken::get_cheese(chef),
cheese2: mc_chicken::get_cheese(chef),
chicken_schnitzel: mc_chicken::get_chicken_schnitzel(chef),
cheese3: mc_chicken::get_cheese(chef),
chicken_schnitzel2: mc_chicken::get_chicken_schnitzel(chef),
cheese4: mc_chicken::get_cheese(chef),
chicken_schnitzel3: mc_chicken::get_chicken_schnitzel(chef),
cheese5: mc_chicken::get_cheese(chef),
cheese6: mc_chicken::get_cheese(chef),
bun2: mc_chicken::get_bun(chef),
};
mc_chicken::deliver_order(chef, order2, burger2, ctx);
}
}
一切都与运气有关……他们说……
在这个挑战中,作者创建了一个掷Coin游戏,要求用户连续猜对12次。值得注意的是,掷Coin的随机性并不是通过 VRF(可验证随机函数)提供,而是使用自定义定义的 LCG(线性同余生成器)生成随机数。
/* https://github.com/MetaTrustLabs/ctf/blob/master/coinFlip/framework/chall/sources/coin_flip.move#L38-L50 */
public entry fun create_game( stake: Coin<SUI>, randomness: u64, fee: u8, ctx: &mut TxContext ) {
let game = Game {
id: object::new(ctx),
stake: stake,
combo: 0,
fee: fee,
player: RANDOM_ADDRESS,
author: tx_context::sender(ctx),
randomness: new_generator(randomness),
solved: false,
};
transfer::public_share_object(game);
}
/* https://github.com/MetaTrustLabs/ctf/blob/master/coinFlip/framework/chall/sources/coin_flip.move#L90-L97 */
fun new_generator(seed: u64): Random {
Random { seed }
}
fun generate_rand(r: &mut Random): u64 {
r.seed = ((((9223372036854775783u128 * ((r.seed as u128)) + 999983) >> 1) & 0x0000000000000000ffffffffffffffff) as u64);
r.seed
}
如果我们仔细检查框架中的游戏创建代码,我们可以观察到 LCG 的种子其实只是一个 u8。因此,我们可以通过强行破解这个种子来预测掷Coin的结果,猜中种子的概率为 1/256。
/* https://github.com/MetaTrustLabs/ctf/blob/master/coinFlip/framework/src/main.rs#L74-L82 */
let mut create_args : Vec<SuiValue> = Vec::new();
let mut rng = rand::thread_rng();
let random_byte: u8 = rng.gen();
println!("随机种子: {}", random_byte);
let create_args_1 = SuiValue::Object(FakeID::Enumerated(3, 0), None);
let create_args_2 = SuiValue::MoveValue(MoveValue::U64(random_byte as u64));
let create_args_3 = SuiValue::MoveValue(MoveValue::U8(10));
有没有更优雅的方法来获取种子而不必进行强力破解?答案是肯定的。虽然 Move 语言在正常情况下限制我们访问 Foo 模块中定义的 struct 的字段,但存在聪明的变通方法,即使用 BCS 编码。
/* https://github.com/MetaTrustLabs/ctf/blob/master/coinFlip/framework/chall/sources/coin_flip.move#L22-L35 */
struct Random has drop, store, copy {
seed: u64
}
struct Game has key, store {
id: UID,
stake: Coin<SUI>,
combo: u8,
fee: u8,
player: address,
author: address,
randomness: Random,
solved : bool,
}
回想一下我们之前提到的,BCS 编码仅序列化并连接结构中所有的底层类型。在 `Game` 的情况下,序列化后,如果我们读取倒数第九个字节(跳过1个字节的 `solved` 和7个高位字节的 `seed`),我们将获得 `game.randomness.seed` 的最低有效字节。一旦我们有了这个种子,我们可以使用与挑战中相同的 LCG 生成随机数并预测结果,从而达到 100% 的猜对率。
module solution::coin_flip_solution {
// [*] 导入依赖
use sui::tx_context::TxContext;
use challenge::coin_flip;
use sui::coin::{Self, Coin};
use sui::sui::SUI;
use std::bcs;
use std::vector;
struct Random has drop, store, copy {
seed: u64
}
// [*] 公共函数
public entry fun solve(game: &mut coin_flip::Game, balance: Coin<SUI>, ctx: &mut TxContext) {
let bytes: vector<u8> = bcs::to_bytes(game);
let secret = *vector::borrow(&bytes, vector::length(&bytes) - 9);
let r = new_generator((secret as u64));
let round = 0;
let fee = coin::split(&mut balance, 10, ctx);
coin_flip::start_game(game, fee, ctx);
while (round < 11) {
let guess = generate_rand(&mut r) % 2;
round = round + 1;
coin_flip::play_game(game, (guess as u8), coin::split(&mut balance, 10, ctx), ctx);
};
let guess = generate_rand(&mut r) % 2;
coin_flip::play_game(game, (guess as u8), balance, ctx);
}
fun new_generator(seed: u64): Random {
Random { seed }
}
fun generate_rand(r: &mut Random): u64 {
r.seed = ((((9223372036854775783u128 * ((r.seed as u128)) + 999983) >> 1) & 0x0000000000000000ffffffffffffffff) as u64);
r.seed
}
}
2023 MetaTrust Web3 Security CTF 提供了一套很棒的挑战,考验了我们在 Sui Move 中的技能。
从基本的“Hello World”理智检查到复杂的“Coin Flip”的机制,每个挑战都提供了独特的学习体验。我们希望这个解读能够成为希望提升智能合约安全技能的人的宝贵资源。
- 原文链接: sec3.dev/blog/sec3-wins-...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!