Type Cosplay
Type-Cosplay-Angriffe nutzen Programme aus, die Kontotypen nicht überprüfen, wodurch Angreifer Konten mit identischen Datenstrukturen, aber unterschiedlichen Verwendungszwecken einschleusen können. Da Solana alle Kontodaten als Rohbytes speichert, kann ein Programm, das Kontotypen nicht überprüft, dazu gebracht werden, ein VaultConfig als ein AdminSettings zu behandeln, was potenziell katastrophale Folgen haben kann.
Die Sicherheitslücke entsteht durch strukturelle Mehrdeutigkeit. Wenn mehrere Kontotypen dasselbe Datenlayout teilen (wie beide ein owner: PubkeyFeld haben), reichen Eigentümerprüfungen und Datenvalidierung allein nicht aus, um zwischen ihnen zu unterscheiden. Ein Angreifer, der einen Kontotyp kontrolliert, kann sich als Eigentümer eines völlig anderen Kontotyps ausgeben und so die Autorisierungslogik umgehen, die für bestimmte Kontozwecke konzipiert wurde.
Ohne Diskriminatoren (eindeutige Kennungen, die Kontotypen unterscheiden) wird dein Programm anfällig für ausgeklügelte Identitätsvortäuschungsangriffe, bei denen böswillige Akteure die Lücke zwischen struktureller Ähnlichkeit und logischer Absicht ausnutzen können.
Anchor
Betrachte diese anfällige Anweisung, die Administratoroperationen basierend auf dem Kontoeigentum durchführt:
#[program]
pub mod insecure_check{
use super::*;
//..
pub fn instruction(ctx: Context<Instruction>) -> Result<()> {
let program_account_one = ctx.accounts.program_account_one.to_account_info();
if program_account_one.owner != ctx.program_id {
return Err(ProgramError::IllegalOwner.into());
}
if ctx.accounts.program_account_one.owner != ctx.accounts.admin.key() {
return Err(ProgramError::InvalidAccountData.into());
}
//..do something
Ok(())
}
//..
}
#[derive(Accounts)]
pub struct Instruction<'info> {
pub admin: Signer<'info>,
#[account(mut)]
/// CHECK: This account will not be checked by Anchor
pub program_account_one: UncheckedAccount<'info>,
#[account(mut)]
/// CHECK: This account will not be checked by Anchor
pub program_account_two: UncheckedAccount<'info>,
}
#[derive(AnchorSerialize, AnchorDeserialize, InitSpace)]
pub struct ProgramAccountOne {
owner: Pubkey,
}
#[derive(AnchorSerialize, AnchorDeserialize, InitSpace)]
pub struct ProgramAccountTwo {
owner: Pubkey,
}Dieser Code sieht sicher aus: Er überprüft den Programmeigentümer und validiert die Administratorautorität. Aber es gibt einen fatalen Fehler: Er überprüft nie, ob program_account_one tatsächlich ein ProgramAccountOne ist und nicht ein anderer Kontotyp mit derselben Datenstruktur.
Ein Angreifer kann dies ausnutzen, indem er:
Ein
ProgramAccountTwoKonto erstellt oder kontrolliertSich selbst als Eigentümer in den Daten dieses Kontos einsetzt
Sein
ProgramAccountTwoalsprogram_account_oneParameter übergibtDa beide Kontotypen identische
owner: PubkeyStrukturen haben, ist die Deserialisierung erfolgreichDer Angreifer wird zum "Admin" für Operationen, die nur für
ProgramAccountOneEigentümer vorgesehen sind
Solana verwendet Diskriminatoren, um dieses Problem zu lösen:
Anchor's 8-Byte-Diskriminator (Standard): Abgeleitet vom Kontonamen, wird automatisch zu Konten hinzugefügt, die mit #[account] markiert sind. (aus anchor
0.31.0ist es möglich, "benutzerdefinierte" Diskriminatoren zu implementieren)Längenbasierte Diskriminierung: Wird vom Token-Programm verwendet, um zwischen Token- und Mint-Konten zu unterscheiden (obwohl Token2022 jetzt explizite Diskriminatoren verwendet)
Die einfachste Lösung ist die Verwendung von Anchor's eingebauter Typvalidierung:
#[derive(Accounts)]
pub struct Instruction<'info> {
pub admin: Signer<'info>,
#[account(mut)]
/// CHECK: This account will not be checked by Anchor
pub program_account_one: Account<'info, ProgramAccountOne>,
#[account(mut)]
/// CHECK: This account will not be checked by Anchor
pub program_account_two: Account<'info, ProgramAccountTwo>,
}
#[account]
pub struct ProgramAccountOne {
owner: Pubkey,
}
#[account]
pub struct ProgramAccountTwo {
owner: Pubkey,
}Oder für benutzerdefinierte Validierung können Sie explizite Diskriminator-Prüfungen hinzufügen:
pub fn instruction(ctx: Context<Instruction>) -> Result<()> {
let program_account_one = ctx.accounts.program_account_one.to_account_info();
if program_account_one.owner != ctx.program_id {
return Err(ProgramError::IllegalOwner.into());
}
if ctx.accounts.program_account_one.owner != ctx.accounts.admin.key() {
return Err(ProgramError::InvalidAccountData.into());
}
let data = program_account_one.data.borrow();
// Assume ProgramAccountOne has a discriminator of 8 bytes
let discriminator = &data[..8];
if discriminator != ProgramAccountOne::DISCRIMINATOR {
return Err(ProgramError::InvalidAccountData.into());
}
//..do something
Ok(())
}Pinocchio
In Pinocchio implementieren Sie die Diskriminator-Prüfung manuell:
let account_data = self.accounts.program_account.try_borrow_data()?;
if account_data[0] != DISCRIMINATOR {
return Err(ProgramError::AccountAlreadyInitialized.into());
}