Missing Sysvar Account Check
The sysvar (system account) account is often used while validating access control for restricted functions by confirming that the inputted sysvar account by the user matches up with the expected sysvar account. Without this check in place, any user is capable of passing in their own spoofed sysvar account and in turn bypassing any further authentication associated with it, causing potentially disastrous effects.
Exploit Scenario
secp256k1 is an elliptic curve used by a number of blockchains for signatures. Validating signatures is crucial as by bypassing signature checks, an attacker can gain access to restricted functions that could lead to drainage of funds.
Here, load_current_index
and load_instruction_at
are functions that don't verify that the inputted sysvar account is authorized, therefore allowing serialized maliciously fabricated data to successfully spoof as an authorized secp256k1 signature.
Example Contract
#![allow(unused)] fn main() { pub fn verify_signatures(account_info: &AccountInfo) -> ProgramResult { let index = solana_program::sysvar::instructions::load_current_index( &account_info.try_borrow_mut_data()?, ); let secp_instruction = sysvar::instructions::load_instruction_at( (index - 1) as usize, &account_info.try_borrow_mut_data()?, ); if secp_instruction.program_id != secp256k1_program::id() { return Err(InvalidSecpInstruction.into()); } ... } }
Refer to Mitigation to understand what's wrong with these functions and how sysvar account checks were added.
Mitigation
- Solana libraries should be running on version 1.8.1 and up
- Use
load_instruction_at_checked
andload_current_index_checked
Utilizing the latest Solana version and referencing checked functions, especially on sensitive parts of a contract is crucial even if potential attack vectors have been fixed post-audit. Leaving the system exposed to any point of failure compromises the entire system's integrity, especially while the contracts are being constantly updated.
Here is the code showing the sysvar account checks added between unchecked and checked functions:
Example: Wormhole Exploit (February 2022)
Funds lost: ~326,000,000 USD
Note: The following analysis is condensed down to be present this attack vector as clearly as possible, and certain details might’ve been left out for the sake of simplification
The Wormhole hack serves to be one of the most memorable exploits in terms of impact DeFi has ever seen.
This exploit also happens to incorporate a missing sysvar account check that allowed the attacker to:
- Spoof Guardian signatures as valid
- Use them to create a Validator Action Approval (VAA)
- Mint 120,000 ETH via calling complete_wrapped function
(These actions are all chronologically dependent on one another based on permissions and conditions - this analysis will only dive into “Step 1”)
The SignatureSet was able to be faked because the verify_signatures
function failed to appropriately verify the sysvar account passed in:
#![allow(unused)] fn main() { let secp_ix = solana_program::sysvar::instructions::load_instruction_at( secp_ix_index as usize, &accs.instruction_acc.try_borrow_mut_data()?, ) }
load_instruction_at
doesn't verify that the inputted data came from the authorized sysvar account.
The fix for this was to upgrade the Solana version and get rid of these unsafe deprecated functions (see Mitigation). Wormhole had caught this issue but didn't update their deployed contracts in time before the exploiter had already managed to drain funds.