Reinitialisierungsangriffe
Reinitialisierungsangriffe nutzen Programme aus, die nicht überprüfen, ob ein Konto bereits initialisiert wurde, und ermöglichen Angreifern, bestehende Daten zu überschreiben und die Kontrolle über wertvolle Konten zu übernehmen.
Während die Initialisierung legitim neue Konten für die Erstnutzung einrichtet, setzt die Reinitialisierung bestehende Konten böswillig auf vom Angreifer kontrollierte Zustände zurück.
Ohne ordnungsgemäße Initialisierungsvalidierung können Angreifer Initialisierungsfunktionen auf Konten aufrufen, die bereits in Verwendung sind, und so effektiv eine feindliche Übernahme des etablierten Programmzustands durchführen. Dies ist besonders verheerend in Protokollen wie Treuhandkonten, Tresoren oder jedem System, in dem der Kontobesitz die Kontrolle über wertvolle Vermögenswerte bestimmt.
Die Initialisierung setzt die Daten eines neuen Kontos zum ersten Mal. Es ist wichtig zu prüfen, ob ein Konto bereits initialisiert wurde, um das Überschreiben vorhandener Daten zu verhindern.
Anchor
Betrachten Sie diese anfällige Anweisung, die ein Programmkonto initialisiert:
#[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,
}Dieser Code hat einen fatalen Fehler: Er überprüft nie, ob das Konto bereits initialisiert wurde. Jedes Mal, wenn diese Anweisung aufgerufen wird, überschreibt sie bedingungslos die Kontodaten und setzt den Aufrufer als neuen Eigentümer, unabhängig vom vorherigen Zustand des Kontos.
Ein Angreifer kann dies ausnutzen, indem er:
Ein wertvolles initialisiertes Konto identifiziert (wie ein Treuhand-PDA, das Token-Konten kontrolliert)
unsafe_initialize_accountmit diesem bestehenden Konto aufruftDer neue "Eigentümer" wird, indem er die vorherigen Eigentümerdaten überschreibt
Seine neu gewonnene Eigentümerschaft nutzt, um alle von diesem Konto kontrollierten Vermögenswerte abzuziehen
Dieser Angriff ist besonders verheerend in Treuhandszenarien. Stellen Sie sich ein Treuhand-PDA vor, das Token-Konten besitzt, die Vermögenswerte im Wert von Tausenden von Dollar enthalten. Die ursprüngliche Treuhand-Initialisierung hat das Konto ordnungsgemäß mit legitimen Teilnehmern eingerichtet. Aber wenn ein Angreifer die Reinitialisierungsfunktion aufrufen kann, kann er die Treuhanddaten überschreiben, sich selbst als Eigentümer festlegen und die Kontrolle über alle treuhänderisch verwahrten Token erlangen.
Glücklicherweise macht es Anchor super einfach, diese Prüfung direkt in der Account-Struktur durchzuführen, indem man einfach die initEinschränkung bei der Initialisierung des Accounts wie folgt verwendet:
#[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,
}Oder man könnte einfach in der Anweisung überprüfen, ob der Account bereits initialisiert wurde, indem man die ctx.accounts.program_account.is_initializedPrüfung wie folgt verwendet:
pub fn update_ownership(ctx: Context<UpdateOwnership>) -> Result<()> {
if ctx.accounts.program_account.is_initialized {
return Err(ProgramError::AccountAlreadyInitialized.into());
}
Ok(())
}Pinocchio
In Pinocchio haben wir nicht die Möglichkeit, Sicherheitsprüfungen direkt in der Account-Struktur hinzuzufügen, daher sind wir gezwungen, dies in der Anweisungslogik zu tun.
Wir können dies tun, indem wir prüfen, ob der Account den korrekten Diskriminator hat:
let account_data = self.accounts.program_account.try_borrow_data()?;
if account_data[0] == DISCRIMINATOR {
return Err(ProgramError::AccountAlreadyInitialized.into());
}