Атаки повторної ініціалізації
Атаки повторної ініціалізації використовують програми, які не перевіряють, чи вже було ініціалізовано обліковий запис, дозволяючи зловмисникам перезаписувати наявні дані та перехоплювати контроль над цінними обліковими записами.
У той час як ініціалізація законно налаштовує нові облікові записи для першого використання, повторна ініціалізація зловмисно скидає наявні облікові записи до станів, контрольованих зловмисником.
Без належної перевірки ініціалізації зловмисники можуть викликати функції ініціалізації для облікових записів, які вже використовуються, фактично здійснюючи вороже захоплення встановленого стану програми. Це особливо руйнівно в протоколах, таких як депозитарії, сховища або будь-яка система, де право власності на обліковий запис визначає контроль над цінними активами.
Ініціалізація встановлює дані нового облікового запису вперше. Важливо перевіряти, чи вже був ініціалізований обліковий запис, щоб запобігти перезапису наявних даних.
Anchor
Розгляньмо цю вразливу інструкцію, яка ініціалізує обліковий запис програми:
#[program]
pub mod unsafe_initialize_account{
use super::*;
//..
pub fn unsafe_initialize_account(ctx: Context<InitializeAccount>) -> Result<()> {
let mut writer: Vec<u8> = vec![];
ProgramAccount {
owner: ctx.accounts.owner.key()
}.try_serialize(&mut writer)?;
let mut data = ctx.accounts.program_account.try_borrow_mut_data()?;
sol_memcpy(&mut data, &writer, writer.len());
Ok(())
}
//..
}
#[derive(Accounts)]
pub struct InitializeAccount<'info> {
pub owner: Signer<'info>,
#[account(mut)]
/// CHECK: This account will not be checked by Anchor
pub program_account: UncheckedAccount<'info>,
}
#[account]
pub struct ProgramAccount {
owner: Pubkey,
}
Цей код має фатальний недолік: він ніколи не перевіряє, чи вже був ініціалізований обліковий запис. Щоразу, коли викликається ця інструкція, вона безумовно перезаписує дані облікового запису та встановлює викликача як нового власника, незалежно від попереднього стану облікового запису.
Зловмисник може використати це, виконавши такі дії:
- Ідентифікувати цінний ініціалізований обліковий запис (наприклад, PDA депозитарію, що контролює токен-рахунки)
- Викликати
unsafe_initialize_account
з цим наявним обліковим записом - Стати новим "власником", перезаписавши дані попереднього власника
- Використати своє нове право власності для виведення будь-яких активів, контрольованих цим обліковим записом
Ця атака особливо руйнівна в сценаріях депозитарію. Уявіть PDA депозитарію, який володіє токен-рахунками, що містять активи вартістю тисячі доларів. Початкова ініціалізація депозитарію правильно налаштувала обліковий запис із законними учасниками. Але якщо зловмисник може викликати функцію повторної ініціалізації, він може перезаписати дані депозитарію, встановити себе як власника та отримати контроль над усіма токенами в депозитарії.
На щастя, Anchor
робить цю перевірку надзвичайно простою безпосередньо в структурі облікового запису, просто використовуючи обмеження init
під час ініціалізації облікового запису, як показано нижче:
#[derive(Accounts)]
pub struct InitializeAccount<'info> {
pub owner: Signer<'info>,
#[account(
init,
payer = owner,
space = 8 + ProgramAccount::INIT_SPACE
)]
pub program_account: Account<'info, ProgramAccount>,
}
#[account]
#[derive(InitSpace)]
pub struct ProgramAccount {
owner: Pubkey,
}
Або ви можете просто перевірити, чи обліковий запис уже ініціалізовано в інструкції, використовуючи перевірку ctx.accounts.program_account.is_initialized
таким чином:
pub fn update_ownership(ctx: Context<UpdateOwnership>) -> Result<()> {
if ctx.accounts.program_account.is_initialized {
return Err(ProgramError::AccountAlreadyInitialized.into());
}
Ok(())
}
Pinocchio
У Pinocchio, оскільки ми не маємо можливості додавати перевірки безпеки безпосередньо всередині структури облікового запису, ми змушені робити це в логіці інструкцій.
Ми можемо зробити це, перевіривши, чи має обліковий запис правильний дискримінатор:
let account_data = self.accounts.program_account.try_borrow_data()?;
if account_data[0] == DISCRIMINATOR {
return Err(ProgramError::AccountAlreadyInitialized.into());
}