Phát triển hợp đồng thông minh phải đọc: 10 vấn đề bảo mật Solidity này không thể bỏ qua
Lưu ý của biên tập viên: Bài viết này đến từCộng đồng Đăng Liên, được in lại bởi Odaily với sự cho phép.
Lưu ý của biên tập viên: Bài viết này đến từ
Cộng đồng Đăng Liên
Cộng đồng Đăng Liên
, được in lại bởi Odaily với sự cho phép.
Vào năm 2018, chúng tôi (CheckMarx) đã thực hiện một nghiên cứu sơ bộ về tình trạng bảo mật hợp đồng thông minh, tập trung vào các hợp đồng thông minh được viết bằng Solidity[1]. Vào thời điểm đó, chúng tôi đã tổng hợp 10 vấn đề bảo mật hợp đồng thông minh hàng đầu dựa trên mã nguồn hợp đồng có sẵn công khai. Hai năm sau, đã đến lúc cập nhật nghiên cứu và đánh giá mức độ bảo mật hợp đồng thông minh đã tiến triển như thế nào.
Các vấn đề đáng quan tâm khác
Mặc dù thật tuyệt khi có một xếp hạng về vấn đề bảo mật, nhưng nó có xu hướng có những chi tiết thú vị vì một số chi tiết không hoàn toàn phù hợp với danh sách xếp hạng. Trước khi tìm hiểu sâu hơn về 10 vấn đề hàng đầu, cần giải thích một số điểm nổi bật của nghiên cứu ban đầu đáng được quan tâm:
Năm 2018, hai vấn đề hàng đầu là từ chối hợp đồng bên ngoài của dịch vụ và reentrancy. Nhưng bây giờ những vấn đề này đã giảm bớt (nhưng vẫn còn gây lo ngại). Bạn có thể tìm hiểu thêm về Reentrancy từ blog nghiên cứu của chúng tôi: Quan điểm bảo mật về hợp đồng thông minh [2].
Lưu ý của người dịch: Trên thực tế, do sự kết hợp của các ứng dụng DeFi (chẳng hạn như các khoản vay flash), đã có nhiều cuộc tấn công vào lại nghiêm trọng.
Giờ đây, Solidity v0.6.x đã được phát hành [3] và nó mang lại nhiều thay đổi đột phá [4], tuy nhiên 50% hợp đồng thông minh được quét thậm chí chưa sẵn sàng cho trình biên dịch Solidity v0.5.0. 30% hợp đồng thông minh khác sử dụng cú pháp lỗi thời (ví dụ: sử dụng sha3, ném, hằng số, v.v.) và 83% hợp đồng có vấn đề về đặc điểm kỹ thuật (pragma) trong phiên bản trình biên dịch được chỉ định.
Lưu ý của người dịch: Solidity 0.6 rõ ràng hơn về mặt ngữ nghĩa (ví dụ: phiên bản 0.6 được nâng cấp về mặt kế thừa [5]), giúp trình biên dịch phát hiện vấn đề kịp thời và làm cho mã an toàn hơn.
Mặc dù các vấn đề về khả năng hiển thị[6] không xuất hiện trong top 10 năm 2018 cũng như trong top 10 năm nay, nhưng mức tăng 48% về các vấn đề về khả năng hiển thị là rất đáng chú ý.
Bảng dưới đây so sánh các thay đổi giữa danh sách 10 Câu hỏi thường gặp hàng đầu năm 2018 và 2020. Những vấn đề này được sắp xếp theo mức độ nghiêm trọng và phổ biến:
1. Các cuộc gọi bên ngoài không được kiểm soát
if(!addr.send(1)) {
revert()
}
Các cuộc gọi bên ngoài không được kiểm soát là vấn đề phổ biến thứ ba trong danh sách 10 vấn đề bảo mật hàng đầu của Solidity năm 2018. Với hai vấn đề đầu tiên hiện đã được giải quyết, các cuộc gọi bên ngoài không được kiểm soát là vấn đề phổ biến nhất trong danh sách cập nhật năm 2020.
Phương thức gọi cơ bản của Solidity, (chẳng hạn như address.call()) sẽ không đưa ra ngoại lệ. Thay vào đó, khi gặp lỗi, sai được trả về.
Và nếu bạn sử dụng một hợp đồng để gọi ExternalContract.doS Something(), nếu doS Something () đưa ra một ngoại lệ, thì ngoại lệ đó sẽ tiếp tục "bong bóng" và lan truyền.
Các trường hợp không thành công nên được xử lý rõ ràng bằng cách kiểm tra giá trị trả về, chuyển ether sau đây bằng cách sử dụng addr.send() là một ví dụ điển hình, điều này cũng hợp lệ cho các lệnh gọi bên ngoài khác.
for(uint256 i=0; i< elements.length; i++) {
// do something
}
2. Chu kỳ chi phí cao
High Cost Loops chuyển từ vị trí thứ tư lên vị trí thứ hai trong danh sách Solidity Security. Số lượng hợp đồng thông minh bị ảnh hưởng bởi vấn đề này đã tăng gần 30%.
Mọi người đều biết rằng các tính toán trên Ethereum yêu cầu thanh toán. Do đó, việc giảm tính toán cần thiết để hoàn thành một hoạt động không chỉ là vấn đề tối ưu hóa (hiệu quả) mà còn là vấn đề chi phí.
Vòng lặp là một thao tác tốn kém, đây là một ví dụ điển hình: một mảng chứa càng nhiều phần tử thì càng cần nhiều lần lặp để hoàn thành vòng lặp. Cuối cùng, vòng lặp vô hạn sẽ sử dụng hết lượng gas hiện có.
Nếu kẻ tấn công có thể tác động đến độ dài của mảng phần tử, đoạn mã trên sẽ gây ra sự từ chối dịch vụ (việc thực thi không thể thoát ra khỏi vòng lặp). Trong các hợp đồng thông minh được quét, 8% hợp đồng được phát hiện có vấn đề thao túng độ dài mảng.
3. Chủ sở hữu áp đảo
function calculateBonus(uint amount) returns (uint) {
return amount/DELIMITER*BONUS;
}
Đây là một vấn đề mới nổi trong mười vấn đề bảo mật hàng đầu của Soldiity, ảnh hưởng đến khoảng 16% hợp đồng, một số hợp đồng có liên quan chặt chẽ với chủ sở hữu (Chủ sở hữu) và một số chức năng chỉ có thể được gọi theo địa chỉ của chủ sở hữu, như trong ví dụ sau:
Chỉ chủ sở hữu hợp đồng mới có thể gọi các hàm doS Something() và doS SomethingElse(): hàm trước sử dụng trình trang trí onlyOwner, trong khi hàm sau triển khai nó một cách rõ ràng. Điều này gây rủi ro nghiêm trọng: nếu khóa riêng của chủ sở hữu bị xâm phạm, kẻ tấn công có thể kiểm soát hợp đồng.
4. Bài toán độ chính xác số học
function transferTo(address dest, uint amount) {
require(tx.origin == owner) {
dest.transfer(amount);
}
}
Các kiểu dữ liệu của Solidity hơi phức tạp do sử dụng máy ảo 256-bit (EVM[7]). Solidity không cung cấp số học dấu phẩy động và các loại dữ liệu có ít hơn 32 byte sẽ được đóng gói vào cùng một khe 32 byte. Với suy nghĩ này, bạn nên mong đợi các vấn đề về độ chính xác của chương trình sau:
Như minh họa trong ví dụ trên, phép chia được thực hiện trước phép nhân có thể có sai số làm tròn rất lớn.
5. Phụ thuộc vào tx.origin
for (uint i = border; i >= 0; i--) {
ans += i;
}
Các hợp đồng thông minh không nên dựa vào tx.origin để xác thực, vì một hợp đồng độc hại có thể thực hiện một cuộc tấn công trung gian và rút hết tiền. Bạn nên sử dụng msg.sender để thay thế:<、>Có thể tìm thấy mô tả chi tiết về cuộc tấn công Tx Origin trong tài liệu của Solidity [8]. Nói một cách đơn giản, tx.origin luôn là tài khoản của người khởi xướng ban đầu trong chuỗi lệnh gọi hợp đồng, trong khi msg.sender đại diện cho người gọi trực tiếp. Nếu hợp đồng cuối cùng trong chuỗi dựa vào tx.origin để xác thực, thì việc gọi hợp đồng ở giữa chuỗi sẽ có thể rút tiền từ hợp đồng được gọi, vì xác thực không kiểm tra ai (msg.sender) thực sự đã thực hiện cuộc gọi.
6. Tràn (Overflow/Underflow)
Máy ảo 256 bit của Solidity có vấn đề tràn và tràn (Ghi chú của người dịch: Do kết quả vượt quá phạm vi giá trị nên gọi là tràn), ở đây [9] có phân tích cụ thể. Các nhà phát triển nên hết sức cẩn thận khi sử dụng kiểu dữ liệu uint trong điều kiện vòng lặp for, vì nó có thể dẫn đến một vòng lặp vô hạn:
Trong ví dụ trên, khi giá trị của i là 0, giá trị tiếp theo là 2^256 -1, điều này làm cho điều kiện luôn đúng. Các nhà phát triển nên cố gắng sử dụng
, != và == để so sánh.
for (var i = 0; i < elements.length; i++) {
// to something
}
7. Loại trừ không an toàn
Vấn đề này đã tăng hai bậc trong danh sách 10 vấn đề bảo mật hàng đầu của Solidity và hiện ảnh hưởng đến hơn 17% hợp đồng thông minh so với trước đây.
Solidity hỗ trợ suy luận kiểu, nhưng nó có một số hành vi kỳ lạ. Ví dụ, chữ 0 được suy ra là kiểu byte thay vì số nguyên được mong đợi thông thường.
Trong ví dụ bên dưới, loại i được suy ra là uint8, vì khi đó có thể lưu trữ giá trị của i dưới dạng uint8 là đủ. Nhưng nếu mảng phần tử chứa hơn 256 phần tử, đoạn mã sau sẽ bị tràn:
Bạn nên khai báo các loại dữ liệu một cách rõ ràng để tránh hành vi và/hoặc lỗi không mong muốn.
if(!addr.send(1)) {
revert()
}
Lưu ý của người dịch: Biến định nghĩa var đã bị loại bỏ trong Solidity 0.6 (không có loại trừ sau Solidity 0.6), nếu bạn sử dụng trình biên dịch mới thì sẽ không có vấn đề gì.
8. Chuyển sai
Vấn đề đã giảm từ thứ sáu xuống thứ tám trong danh sách 10 vấn đề bảo mật hàng đầu của Solidity và hiện ảnh hưởng đến chưa đến 1% hợp đồng thông minh.
for (uint i = 0; i < users.lenghth; i++) {
users[i].transfer(amount);
}
Có một số cách để chuyển ether giữa các hợp đồng. Mặc dù khuyến nghị chính thức là sử dụng hàm addr.transfer(x), chúng tôi vẫn tìm thấy các hợp đồng thông minh vẫn sử dụng hàm send():
Lưu ý rằng addr.transfer(x) tự động đưa ra một ngoại lệ nếu quá trình chuyển không thành công, một lần nữa giảm thiểu sự cố của cuộc gọi bên ngoài không được kiểm tra đầu tiên
9. Chuyển giao trong chu kỳ
if (timeHasCome == block.timestamp) {
winner.transfer(amount);
}
Khi thực hiện chuyển ether trong thân vòng lặp, nếu một trong các lần chuyển không thành công (ví dụ: không thể nhận hợp đồng), thì toàn bộ giao dịch sẽ bị khôi phục.<、>Trong ví dụ này, kẻ tấn công có thể khai thác hành vi này để tiến hành tấn công từ chối dịch vụ, ngăn người dùng khác nhận ether.
Năm 2018, vấn đề phụ thuộc dấu thời gian xếp thứ năm và điều quan trọng cần nhớ là các hợp đồng thông minh chạy trên nhiều nút vào các thời điểm khác nhau. Máy ảo Ethereum (EVM) không cung cấp thời gian đồng hồ và biến hiện tại (bí danh cho block.timestamp ) thường được sử dụng để lấy dấu thời gian thực sự là một biến môi trường mà người khai thác có thể thao tác.
tóm tắt
Vì công cụ khai thác có thể thao tác với các biến môi trường hiện tại nên nó chỉ có thể được sử dụng trong bất đẳng thức >,
= và <= sử dụng giá trị của nó.
tóm tắt
Liên kết nguồn:securityboulevard.com


