

Những bài viết liên quan:
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áp
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áp
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
tiêu đề cấp đầu tiên
1. Tổng quan về lỗ hổng tràn số nguyên
hình ảnh
Trong hầu hết các ngôn ngữ lập trình, giá trị của một số nguyên thường được lưu trữ trong bộ nhớ có độ dài cố định. Số nguyên có thể được chia thành hai loại, không dấu và có dấu. Sự khác biệt giữa chúng là liệu bit cao nhất có được sử dụng làm bit dấu hay không, được sử dụng để biểu thị dấu của một số nguyên. Ví dụ: không gian bộ nhớ 32 bit có thể lưu trữ số nguyên không dấu (uint32) trong khoảng từ 0 đến 4.294.967.295 hoặc số nguyên có dấu (int32) trong khoảng −2.147.483.648 đến 2.147.483.647.
Nhưng điều gì sẽ xảy ra khi chúng ta thực hiện phép tính 4,294,967,295 + 1 trong phạm vi của uint32 và cố gắng lưu kết quả lớn hơn giá trị lớn nhất của kiểu số nguyên đó?
0xFFFFFFFF
+ 0x00000001
------------
= 0x00000000
Mặc dù kết quả của việc thực thi này phụ thuộc vào ngôn ngữ lập trình và trình biên dịch cụ thể, nhưng trong hầu hết các trường hợp, kết quả của phép tính sẽ hiển thị "tràn" và trả về 0. Đồng thời, hầu hết các ngôn ngữ lập trình và trình biên dịch đều không kiểm tra loại lỗi này mà chỉ thực hiện thao tác modulo đơn giản, thậm chí còn có các hành vi không xác định khác.
Sự tồn tại của tràn số nguyên thường làm cho chương trình tạo ra kết quả không mong muốn khi chạy. Khi viết các hợp đồng thông minh chuỗi khối, đặc biệt là trong lĩnh vực tài chính phi tập trung, các tình huống sử dụng phép tính số nguyên là rất phổ biến, vì vậy cần đặc biệt chú ý đến khả năng xảy ra lỗ hổng tràn số nguyên.
0x00000000
- 0x00000001
------------
= 0xFFFFFFFF
Giả sử một tổ chức tài chính sử dụng số nguyên 32 bit không dấu để biểu thị giá cổ phiếu. Tuy nhiên, khi dùng kiểu số nguyên này để biểu diễn một số lớn hơn giá trị lớn nhất mà kiểu đó có thể biểu diễn, máy tính sẽ đặt thêm 1 hoặc nhiều bit ngoài phạm vi bộ nhớ 32 bit (tức là tràn), và cuối cùng là số sẽ được biểu diễn dưới dạng Các giá trị khác với bit tràn bị cắt bớt, $429,496,7296 được đọc là 0 nếu có thể. Tại thời điểm này, nếu ai đó tiếp tục giao dịch bằng giá trị đó, giá cổ phiếu sẽ bằng 0, điều này sẽ gây ra đủ loại nhầm lẫn. Do đó, vấn đề lỗ hổng tràn số nguyên đáng được chúng ta quan tâm.
Làm thế nào để tránh tràn số nguyên khi viết hợp đồng thông minh bằng ngôn ngữ Rust sẽ là trọng tâm của cuộc thảo luận tiếp theo trong bài viết này.
2. Định nghĩa tràn số nguyên
https://etherscan.io/tx/0xad89ff16fd1ebe3a0a7cf4ed282302c06626c1af33221ebe0d3a470aba4a660f
Nếu giá trị vượt quá phạm vi mà loại biến có thể đại diện, sẽ xảy ra tràn. Tràn có thể chủ yếu được chia thành hai trường hợp, cụ thể là tràn số nguyên (overflow) và tràn (underflow).
1. function batchTransfer(address[] _receivers, uint256 _value) public whenNotPausedreturns (bool) {
2. uint cnt = _receivers.length;
3. uint256 amount = uint256(cnt) * _value;
4. require(cnt > 0 && cnt <= 20);
5. require(_value > 0 && balances[msg.sender] >= amount);
6.
7. balances[msg.sender] = balances[msg.sender].sub(amount);
8. for (uint i = 0; i < cnt; i++) {
9. balances[_receivers[i]] = balances[_receivers[i]].add(_value);
10. Transfer(msg.sender, _receivers[i], _value);
11. }
12. return true;
13. }
2.1 Tràn số nguyên
Nghĩa là, tương tự như những gì được mô tả trong phần tổng quan về các lỗ hổng tràn số nguyên ở trên, chẳng hạn, phạm vi của các số nguyên không dấu có thể được biểu thị bởi uint32 trong Solidity là: 0 đến 2^32 - 1, 2^32 - 1 được biểu thị như 0xFFFFFFFF ở dạng thập lục phân, 2^32 - 1 cộng 1 sẽ gây tràn.
2.2 Dòng số nguyên
Phạm vi biểu diễn của số nguyên không dấu uin32 cũng có giới hạn dưới, nghĩa là giá trị nhỏ nhất là 0. Trừ 1 từ 0 sẽ dẫn đến dòng dưới của số nguyên uint32:
3. Trường hợp tràn số nguyên
Nhóm BeautyChain đã thông báo vào ngày 22 tháng 4 năm 2018 rằng mã thông báo BEC dao động bất thường vào ngày 22 tháng 4. Kẻ tấn công đã thu được thành công 10^58 BEC bằng cách khai thác lỗ hổng do tràn số nguyên.
[profile.release]
overflow-checks = true
panic = "abort"
Trong trường hợp tấn công hợp đồng này, kẻ tấn công đã thực hiện chức năng "batchTransfer" với lỗ hổng tràn số nguyên để thực hiện giao dịch
Sau đây là cách thực hiện cụ thể chức năng này:
Chức năng này dùng để chuyển tiền đến nhiều địa chỉ (người nhận), và số tiền chuyển của mỗi địa chỉ là giá trị.
Dòng thứ ba của đoạn mã trên uint256mount = uint256(cnt) * _value dùng để tính toán toàn bộ số tiền cần chuyển, tuy nhiên dòng mã này có khả năng bị tràn số nguyên. Khi giá trị = 0x800000000000000000000000000000000000000000000000000000 và độ dài của bộ thu là 2, thì một số nguyên tràn sẽ xảy ra trong quá trình nhân dòng mã thứ ba, làm cho số tiền = 0. Vì số tiền = 0 nhỏ hơn số dư của người dùng[msg.sender], nên việc kiểm tra xem số dư của người gọi hợp đồng người dùng msg.sender trong dòng 5 có lớn hơn số tiền được chuyển hay không sẽ dễ dàng được thông qua. Bằng cách này, kẻ tấn công có thể thực hiện các hoạt động chuyển tiền tiếp theo để kiếm lợi nhuận.
4. Công nghệ chống tràn số nguyên
Phần này sẽ giới thiệu cách sử dụng một số phương thức thông dụng kết hợp với tính năng của ngôn ngữ Rust để tránh tràn số nguyên.
Với ngôn ngữ Rust: Khi chúng ta biên dịch file đích của phiên bản phát hành, nếu không được cấu hình thì mặc định Rust sẽ không kiểm tra lỗi tràn số nguyên. Khi một số nguyên bị tràn, chẳng hạn như trong trường hợp số nguyên không dấu 8 bit (uint8), cách tiếp cận thông thường của Rust là biến giá trị 256 thành 0, 257 thành 1, v.v. Tại thời điểm này, Rust sẽ không kích hoạt Panic, nhưng giá trị của biến có thể không phải là giá trị mà chúng tôi mong đợi. Do đó, chúng ta cần định cấu hình một chút các tùy chọn biên dịch của chương trình Rust để chương trình cũng có thể kiểm tra lỗi tràn số nguyên trong chế độ Phát hành và kích hoạt Panic, để tránh các trường hợp ngoại lệ của chương trình do tràn số nguyên."0.9.1"Định cấu hình Cargo.toml để kiểm tra lỗi tràn số nguyên trong chế độ phát hành.
[dependencies]
Sử dụng cấu hình này, chúng ta có thể thiết lập chiến lược xử lý tràn số nguyên trong chương trình.
uint = { version = "0.9.1", default-features = false }
4.1 Sử dụng Rust Crate uint để hỗ trợ số nguyên lớn hơn (phiên bản mới nhất hiện tại là 0.9.1)
use uint::construct_uint;
So với loại số nguyên lớn nhất mà Solidity có thể hỗ trợ là u256, loại số nguyên lớn nhất mà thư viện chuẩn hiện tại của Rust có thể cung cấp chỉ là u128. Để hỗ trợ tốt hơn các hoạt động số nguyên lớn hơn trong hợp đồng thông minh Rust của chúng tôi, chúng tôi có thể sử dụng thùng Rust uint để giúp mở rộng quy mô.
construct_uint! {
pub struct U1024(16);
}
construct_uint! {
pub struct U512(8);
}
construct_uint! {
pub struct U256(4);
}
4.1.1 Giới thiệu về Rust uint crate
Sử dụng hộp Rust uint để cung cấp các loại số nguyên lớn không dấu và hỗ trợ tích hợp sẵn cho các API rất giống với các loại số nguyên ban đầu của Rust, đồng thời tính đến hiệu suất và khả năng sử dụng đa nền tảng.
// (2^1024)-1 = 179769313486231590772930519078902473361797697894230657273430081157732675805500963132708477322407536021120113879871393357658789768814416622492847430639474124377767893424865485276302219601246094119453082952085005768838150682342462881473913110540827237163350510684586298239947245938479716304835356329624224137215
let p =U1024::from_dec_str("179769313486231590772930519078902473361797697894230657273430081157732675805500963132708477322407536021120113879871393357658789768814416622492847430639474124377767893424865485276302219601246094119453082952085005768838150682342462881473913110540827237163350510684586298239947245938479716304835356329624224137215").expect("p to be a good number in the example");
4.1.2 Cách sử dụng Rust uint crate
#[test]
fn test_uint(){
let p = U1024::from_dec_str("179769313486231590772930519078902473361797697894230657273430081157732675805500963132708477322407536021120113879871393357658789768814416622492847430639474124377767893424865485276302219601246094119453082952085005768838150682342462881473913110540827237163350510684586298239947245938479716304835356329624224137215").expect("p to be a good number in the example");
assert_eq!(p,U1024::max_value());
}
Trước tiên, thêm phần phụ thuộc vào uint crate trong Cargo.toml của dự án Rust và chỉ định số phiên bản là mới nhất
running 1 test
test tests::test_uint ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 3 filtered out; finished in 0.00s
Phiên bản.
# Các phụ thuộc khác, chẳng hạn như tiêu chuẩn gần sdk, gần hợp đồng, v.v.
#[test]
fn test_overflow(){
Sau đó, chúng ta có thể nhập và sử dụng thùng trong chương trình Rust
let amounts: u128 = 340282366920938463463374607431768211455;
Câu lệnh sau đây có thể được sử dụng để xây dựng kiểu số nguyên không dấu mà bạn muốn:
let amount_u256 = U256::from(amounts) * U256::from(amounts);
println!("{:?}",amount_u256);
4.2 Sử dụng hàm chuyển đổi kiểu uint để phát hiện tràn số nguyên
let amount_u256 = U256::from(amounts) + 1;
println!("{:?}",amount_u256);
Trước tiên, chúng ta có thể sử dụng phương thức sau để xác định biến p và sử dụng phương thức from_dec_str được xác định bởi uint crate cho U1024 để gán giá trị cho biến p.
let amount_u128 = amount_u256.as_u128();
println!("{:?}",amount_u128);
}
Bài kiểm tra đơn vị 1: Nó được sử dụng để kiểm tra xem uint có thể hỗ trợ giá trị tối đa mà U1024 có thể biểu thị hay không.
running 1 test
115792089237316195423570985008687907852589419931798687112530834793049593217025
340282366920938463463374607431768211456
thread 'tests::test_overflow' panicked at 'Integer overflow when casting to u128', src/lib.rs:16:1
Kiểm tra đơn vị một kết quả:
Có thể thấy biến p:U1024 lưu chính xác giá trị lớn nhất mà U1024 có thể biểu diễn.
Bài kiểm tra đơn vị 2: Kiểm tra tràn số nguyên
// Giá trị lớn nhất mà u128 có thể biểu diễn, tức là 2^128 -1<_>// U256 thường có thể biểu thị kết quả hoạt động của (2^128 -1)*(2^128 -1) mà không bị tràn.
// ở đây (2^128 -1) + 1 = 2^128
#[test]
fn test_underflow(){
let amounts= U256::from(0);
let amount_u256 = amounts.checked_sub(U256::from(1));
println!("{:?}",amount_u256);
}
// Sẽ tràn phạm vi từ 0 đến 2^128 -1 có thể được biểu thị bằng số nguyên không dấu u128, do đó Panic sẽ được kích hoạt.
running 1 test
None
test tests::test_underflow ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 4 filtered out; finished in 0.00s
Kết quả của các bài kiểm tra đơn vị như sau:
#[test]
fn test_underflow(){
let amounts= U256::from(0);
- let amount_u256 = amounts.checked_sub(U256::from(1));
+ let amount_u256 =amounts.checked_sub(U256::from(1)).expect("ERR_SUB_INSUFFICIENT");
println!("{:?}",amount_u256);
}
Theo tính năng chuyển đổi loại hàm .as_u128() do uint crate cung cấp, khi lượng_u256 được chuyển đổi thành u128 theo loại, thì Painc sẽ được kích hoạt vì nó vượt quá phạm vi mà số nguyên không dấu u128 có thể biểu thị. Có thể thấy rằng lúc này Rust có thể phát hiện tràn số nguyên.
running 1 test
thread 'tests::test_underflow' panicked at 'ERR_SUB_INSUFFICIENT', src/lib.rs:126:62
4.3 Kiểm tra tràn và tràn số nguyên bằng toán học an toàn
Ngôn ngữ Rust cũng cung cấp các hành vi hoạt động khác nhau đối với tràn số nguyên có thể xảy ra trong các hoạt động số nguyên. Nếu cần kiểm soát hành vi tràn số nguyên một cách tinh vi hơn, bạn có thể gọi các hàm chuỗi Wrapping_*, saturating_*,Checked_* và Overflowing_* trong thư viện chuẩn, phần này sẽ tập trung vào các hàmcheck_* Bạn đọc có thể tra cứu theo các từ khóa trên để tìm hiểu thêm Cách kiểm soát tràn số nguyên.
Loại được check_* trả về là Tùy chọn
