Crashing RPC: Phân tích một loại lỗ hổng mới trong các nút RPC chuỗi khối an toàn cho bộ nhớ

Nhóm Skyfall của CertiK gần đây đã phát hiện ra nhiều lỗ hổng trong các nút RPC dựa trên Rust trong một số chuỗi khối bao gồm Aptos, StarCoin và Sui. Vì các nút RPC là các thành phần cơ sở hạ tầng quan trọng kết nối các dApp và chuỗi khối cơ bản, nên độ bền của chúng rất quan trọng đối với hoạt động liền mạch. Các nhà thiết kế chuỗi khối biết tầm quan trọng của các dịch vụ RPC ổn định, vì vậy họ áp dụng các ngôn ngữ an toàn cho bộ nhớ như Rust để tránh các lỗ hổng phổ biến có thể phá hủy các nút RPC.

Việc áp dụng ngôn ngữ an toàn cho bộ nhớ như Rust giúp các nút RPC tránh được nhiều cuộc tấn công dựa trên các lỗ hổng làm hỏng bộ nhớ. Tuy nhiên, thông qua một cuộc kiểm tra gần đây, chúng tôi nhận thấy rằng ngay cả các triển khai Rust an toàn cho bộ nhớ, nếu không được thiết kế và hiệu đính cẩn thận, cũng có thể dễ bị tấn công bởi một số mối đe dọa bảo mật có thể làm gián đoạn tính khả dụng của các dịch vụ RPC.

Trong bài viết này, chúng tôi sẽ giới thiệu phát hiện của chúng tôi về hàng loạt lỗ hổng thông qua các trường hợp thực tế.

Vai trò nút RPC chuỗi khối

Dịch vụ cuộc gọi thủ tục từ xa (RPC) của chuỗi khối là thành phần cơ sở hạ tầng cốt lõi của chuỗi khối Lớp 1. Nó cung cấp cho người dùng một giao diện người dùng API quan trọng và hoạt động như một cổng vào mạng chuỗi khối phía sau. Tuy nhiên, dịch vụ RPC blockchain khác với dịch vụ RPC truyền thống ở chỗ nó tạo điều kiện cho người dùng tương tác mà không cần xác thực. Tính khả dụng liên tục của dịch vụ là rất quan trọng và bất kỳ sự gián đoạn nào trong dịch vụ đều có thể ảnh hưởng nghiêm trọng đến tính khả dụng của chuỗi khối cơ bản.

Phối cảnh kiểm tra: máy chủ RPC truyền thống VS máy chủ RPC chuỗi khối

Việc kiểm tra các máy chủ RPC truyền thống chủ yếu tập trung vào xác minh đầu vào, ủy quyền/xác thực, giả mạo yêu cầu trên nhiều trang web/giả mạo yêu cầu phía máy chủ (CSRF/SSRF), các lỗ hổng tiêm nhiễm (chẳng hạn như tiêm nhiễm SQL, tiêm lệnh) và rò rỉ thông tin.

Tuy nhiên, tình hình lại khác đối với các máy chủ blockchain RPC. Miễn là giao dịch được ký, không cần xác thực ứng dụng khách yêu cầu ở lớp RPC. Là mặt trước của chuỗi khối, một trong những mục tiêu chính của dịch vụ RPC là đảm bảo tính khả dụng của nó. Nếu không thành công, người dùng không thể tương tác với chuỗi khối, ngăn họ truy vấn dữ liệu trên chuỗi, gửi giao dịch hoặc phát hành các chức năng hợp đồng.

Do đó, khía cạnh dễ bị tổn thương nhất của máy chủ RPC blockchain là "tính khả dụng". Nếu máy chủ ngừng hoạt động, người dùng sẽ mất khả năng tương tác với chuỗi khối. Điều nghiêm trọng hơn là một số cuộc tấn công sẽ lan rộng trên chuỗi, ảnh hưởng đến một số lượng lớn các nút và thậm chí dẫn đến tê liệt toàn bộ mạng.

Tại sao chuỗi khối mới sẽ sử dụng RPC an toàn cho bộ nhớ

Một số blockchain Lớp 1 nổi tiếng, chẳng hạn như Aptos và Sui, sử dụng ngôn ngữ lập trình an toàn bộ nhớ Rust để triển khai các dịch vụ RPC của họ. Nhờ tính an toàn mạnh mẽ và kiểm tra thời gian biên dịch nghiêm ngặt, Rust làm cho các chương trình hầu như miễn nhiễm với các lỗ hổng làm hỏng bộ nhớ, chẳng hạn như tràn ngăn xếp, các lỗ hổng tham chiếu con trỏ null và tham chiếu lại sau khi miễn phí.

Để tăng cường bảo mật cho cơ sở mã, các nhà phát triển tuân thủ nghiêm ngặt các phương pháp hay nhất, chẳng hạn như không giới thiệu mã không an toàn. Sử dụng #![forbid(unsafe_code)] trong mã nguồn để đảm bảo rằng mã không an toàn bị chặn và lọc.

Ví dụ về các nhà phát triển chuỗi khối triển khai thực hành lập trình Rust

Để ngăn tràn số nguyên, các nhà phát triển thường sử dụng các hàm như đã kiểm tra_add, đã kiểm tra_sub, bão hòa_add, bão hòa_sub, v.v. thay vì cộng và trừ đơn giản (+, -). Giảm thiểu tình trạng cạn kiệt tài nguyên bằng cách đặt thời gian chờ thích hợp, giới hạn kích thước yêu cầu và giới hạn mục yêu cầu.

Các mối đe dọa RPC về an toàn bộ nhớ trong Chuỗi khối lớp 1

Mặc dù không dễ bị mất an toàn bộ nhớ theo nghĩa truyền thống, nhưng các nút RPC dễ bị kẻ tấn công thao túng đầu vào. Trong triển khai RPC an toàn cho bộ nhớ, có một số tình huống có thể dẫn đến từ chối dịch vụ. Ví dụ: khuếch đại bộ nhớ có thể làm cạn kiệt bộ nhớ của dịch vụ, trong khi các sự cố logic có thể tạo ra các vòng lặp vô hạn. Ngoài ra, các điều kiện tương tranh có thể gây ra mối đe dọa theo đó các hoạt động đồng thời có thể có một chuỗi sự kiện không mong muốn, khiến hệ thống ở trạng thái không xác định. Ngoài ra, các phụ thuộc được quản lý không đúng cách và thư viện của bên thứ ba có thể đưa các lỗ hổng chưa biết vào hệ thống.

Trong bài đăng này, mục đích của chúng tôi là thu hút sự chú ý đến những cách tức thời hơn mà các biện pháp bảo vệ thời gian chạy của Rust có thể được kích hoạt, khiến các dịch vụ tự hủy bỏ.

Sự hoảng loạn rỉ sét rõ ràng: Một cách để chấm dứt trực tiếp các dịch vụ RPC

Các nhà phát triển có thể giới thiệu mã hoảng loạn rõ ràng, cố ý hoặc vô ý. Các mã này chủ yếu được sử dụng để xử lý các điều kiện bất ngờ hoặc ngoại lệ. Một số ví dụ phổ biến bao gồm:

khẳng định!(): Sử dụng macro này khi một điều kiện phải được đáp ứng. Nếu điều kiện được khẳng định không thành công, chương trình sẽ hoảng loạn, cho biết rằng có một lỗi nghiêm trọng trong mã.

panic!(): Hàm này được gọi khi chương trình gặp lỗi từ đó không thể phục hồi và không thể tiếp tục.

không thể truy cập!(): Sử dụng macro này khi một đoạn mã không được thực thi. Nếu macro này được gọi, nó chỉ ra một lỗi logic nghiêm trọng.

chưa thực hiện!() và việc cần làm!(): Các macro này là phần giữ chỗ cho chức năng chưa thực hiện. Nếu đạt đến giá trị này, chương trình sẽ bị sập.

unwrap(): Phương thức này dùng cho các loại Option hoặc Result, khi gặp biến Err hoặc None thì chương trình sẽ crash.

Lỗ hổng 1: Kích hoạt xác nhận trong Move Verifier!

Chuỗi khối Aptos sử dụng trình xác minh mã byte Move để thực hiện phân tích bảo mật tham chiếu thông qua diễn giải trừu tượng của mã byte. Hàm ute() là một phần của việc triển khai đặc điểm TransferFunctions và mô phỏng việc thực hiện các lệnh mã byte trong các khối cơ bản.

Nhiệm vụ của hàm ute_inner() là diễn giải lệnh bytecode hiện tại và cập nhật trạng thái tương ứng. Nếu chúng ta đã thực thi đến lệnh cuối cùng trong khối cơ bản, như được biểu thị bằng chỉ mục == last_index, hàm sẽ gọi khẳng định!(self.stack.is_empty()) để đảm bảo ngăn xếp trống. Mục đích đằng sau hành vi này là để đảm bảo rằng tất cả các hoạt động được cân bằng, điều đó cũng có nghĩa là mỗi lần đẩy đều có một cửa sổ bật lên tương ứng.

Trong luồng thực thi bình thường, ngăn xếp luôn được cân bằng trong quá trình diễn giải trừu tượng. Điều này được đảm bảo bởi Trình kiểm tra số dư ngăn xếp, xác minh mã byte trước khi diễn giải nó. Tuy nhiên, một khi chúng ta mở rộng quan điểm của mình sang lĩnh vực trình thông dịch trừu tượng, chúng ta sẽ thấy rằng giả định về số dư ngăn xếp không phải lúc nào cũng hợp lệ.

Bản vá cho lỗ hổng phân tích_function trong AbstractInterpreter

Về cốt lõi, một trình thông dịch trừu tượng mô phỏng mã byte ở cấp độ khối cơ bản. Trong quá trình triển khai ban đầu, việc gặp lỗi trong quá trình ute_block sẽ nhắc quá trình phân tích ghi lại lỗi và tiếp tục thực thi khối tiếp theo trong biểu đồ luồng điều khiển. Điều này có thể tạo ra tình huống trong đó một lỗi trong khối thực thi có thể khiến ngăn xếp trở nên mất cân bằng. Nếu việc thực thi tiếp tục trong trường hợp này, một kiểm tra khẳng định sẽ được thực hiện nếu ngăn xếp không trống, gây ra sự hoảng loạn.

Điều này tạo cơ hội cho những kẻ tấn công khai thác. Kẻ tấn công có thể gây ra lỗi bằng cách thiết kế một mã byte cụ thể trong ute_block(), sau đó ute() có thể thực thi xác nhận nếu ngăn xếp không trống, khiến cho kiểm tra xác nhận không thành công. Điều này sẽ khiến dịch vụ RPC thêm hoảng loạn và chấm dứt, ảnh hưởng đến tính khả dụng của dịch vụ.

Để ngăn chặn điều này, bản sửa lỗi được triển khai đảm bảo rằng toàn bộ quá trình phân tích bị dừng khi hàm ute_block gặp lỗi lần đầu, nhờ đó tránh nguy cơ xảy ra các sự cố tiếp theo có thể xảy ra khi tiếp tục phân tích do mất cân bằng ngăn xếp do lỗi. Sửa đổi này loại bỏ các điều kiện có thể gây hoảng loạn và giúp cải thiện độ mạnh mẽ và an toàn của trình thông dịch trừu tượng.

** Lỗ hổng 2: Kích hoạt sự hoảng loạn trong StarCoin! **

Chuỗi khối Starcoin có nhánh triển khai Move riêng. Trong repo Move này, có một hoảng loạn trong hàm tạo của loại Struct! Nếu StructDefinition được cung cấp có thông tin trường Gốc, thì hoảng loạn sẽ được kích hoạt rõ ràng! .

Sự hoảng loạn rõ ràng đối với các cấu trúc được khởi tạo trong các thói quen chuẩn hóa

Nguy cơ tiềm ẩn này tồn tại trong quá trình phân phối lại các mô-đun. Nếu mô-đun đã xuất bản đã tồn tại trong kho lưu trữ dữ liệu, thì việc chuẩn hóa mô-đun là bắt buộc đối với cả mô-đun hiện có và mô-đun đầu vào do kẻ tấn công kiểm soát. Trong quá trình này, chức năng "chuẩn hóa::Mô-đun::mới" xây dựng cấu trúc mô-đun từ các mô-đun đầu vào do kẻ tấn công kiểm soát, gây ra "sự hoảng loạn!".

Điều kiện tiên quyết cho quy trình chuẩn hóa

Sự hoảng loạn này có thể được kích hoạt bằng cách gửi một tải trọng được chế tạo đặc biệt từ máy khách. Do đó, các tác nhân độc hại có thể làm gián đoạn tính khả dụng của các dịch vụ RPC.

Bản vá hoảng loạn khởi tạo cấu trúc

Bản vá Starcoin giới thiệu một hành vi mới để xử lý trường hợp Bản địa. Bây giờ, thay vì hoảng loạn, nó trả về một ec trống. Điều này làm giảm khả năng người dùng gửi dữ liệu gây hoảng loạn.

Sự hoảng loạn rỉ sét tiềm ẩn: Một cách dễ dàng bị bỏ qua để chấm dứt các dịch vụ RPC

Các hoảng loạn rõ ràng có thể dễ dàng xác định trong mã nguồn, trong khi các hoảng loạn tiềm ẩn có nhiều khả năng bị các nhà phát triển bỏ qua. Sự hoảng loạn tiềm ẩn thường xảy ra khi sử dụng API được cung cấp bởi thư viện tiêu chuẩn hoặc bên thứ ba. Các nhà phát triển cần đọc và hiểu kỹ tài liệu API, nếu không các chương trình Rust của họ có thể dừng đột ngột.

Sự hoảng loạn tiềm ẩn trong BTreeMap

Hãy lấy BTreeMap từ Rust STD làm ví dụ. BTreeMap là một cấu trúc dữ liệu thường được sử dụng để tổ chức các cặp khóa-giá trị trong cây nhị phân được sắp xếp. BTreeMap cung cấp hai phương thức để truy xuất giá trị theo khóa: get(&self, key: &Q) và index(&self, key: &Q).

Phương thức get(&self, key: &Q) truy xuất giá trị bằng cách sử dụng khóa và trả về một Tùy chọn. Tùy chọn có thể là Some(&V), nếu khóa tồn tại, hãy trả về tham chiếu của giá trị, nếu không tìm thấy khóa trong BTreeMap, hãy trả về Không.

Mặt khác, index(&self, key: &Q) trực tiếp trả về một tham chiếu đến giá trị tương ứng với khóa. Tuy nhiên, nó có một rủi ro lớn: nó sẽ gây ra sự hoảng loạn ngầm nếu khóa không tồn tại trong BTreeMap. Nếu không được xử lý đúng cách, chương trình có thể gặp sự cố bất ngờ, khiến nó trở thành một lỗ hổng tiềm ẩn.

Trên thực tế, phương thức index(&self, key: &Q) là triển khai cơ bản của đặc điểm std::ops::Index. Đặc điểm này là một hoạt động chỉ mục trong một bối cảnh không thay đổi (tức là vùng chứa [index] ) cung cấp đường cú pháp thuận tiện. Các nhà phát triển có thể trực tiếp sử dụng btree_map [key] , gọi phương thức index(&self, key: &Q). Tuy nhiên, họ có thể bỏ qua thực tế rằng việc sử dụng này có thể gây hoảng loạn nếu không tìm thấy khóa, do đó gây ra mối đe dọa tiềm ẩn đối với sự ổn định của chương trình.

Lỗ hổng 3: Kích hoạt sự hoảng loạn tiềm ẩn trong Sui RPC

Quy trình phát hành mô-đun Sui cho phép người dùng gửi tải trọng mô-đun qua RPC. Trình xử lý RPC sử dụng chức năng SuiCommand::Publish để phân tích trực tiếp mô-đun đã nhận trước khi chuyển tiếp yêu cầu tới mạng xác minh phụ trợ để xác minh mã byte.

Trong quá trình tháo gỡ này, phần mã_đơn vị của mô-đun đã gửi được sử dụng để xây dựng VMControlFlowGraph. Quá trình xây dựng bao gồm tạo các khối cơ bản, được lưu trữ trong BTreeMap có tên là "'khối'". Quá trình này bao gồm việc tạo và thao tác trên Bản đồ, trong đó một cơn hoảng loạn tiềm ẩn được kích hoạt trong một số điều kiện nhất định.

Đây là một mã đơn giản hóa:

Tiềm ẩn sự hoảng loạn khi tạo VMControlFlowGraph

Trong mã đó, một VMControlFlowGraph mới được tạo bằng cách xem qua mã và tạo một khối cơ bản mới cho mỗi đơn vị mã. Các khối cơ bản được lưu trữ trong khối có tên BTreeMap.

Bản đồ khối được lập chỉ mục bằng cách sử dụng khối [& khối] trong một vòng lặp lặp qua ngăn xếp, đã được khởi tạo bằng ENTRY_BLOCK_ID. Giả định ở đây là có ít nhất một ENTRY_BLOCK_ID trong bản đồ khối.

Tuy nhiên, giả định này không phải lúc nào cũng đúng. Ví dụ: nếu mã đã cam kết trống, "bản đồ khối" sẽ vẫn trống sau quá trình "tạo khối cơ bản". Khi mã sau đó cố gắng duyệt qua bản đồ khối bằng cách sử dụng for succ in &blocks[&block].successors , một sự hoảng loạn tiềm ẩn có thể tăng lên nếu không tìm thấy khóa. Điều này là do biểu thức blocks[&block] về cơ bản là một lệnh gọi tới phương thức index(), như đã đề cập trước đó, sẽ gây hoảng loạn nếu khóa không tồn tại trong BTreeMap.

Kẻ tấn công có quyền truy cập từ xa có thể khai thác lỗ hổng trong chức năng này bằng cách gửi tải trọng mô-đun không đúng định dạng với trường mã_đơn vị trống. Yêu cầu RPC đơn giản này làm hỏng toàn bộ quy trình JSON-RPC. Nếu kẻ tấn công tiếp tục gửi các tải trọng không đúng định dạng như vậy với nỗ lực tối thiểu, thì điều đó sẽ dẫn đến sự gián đoạn dịch vụ kéo dài. Trong mạng blockchain, điều này có nghĩa là mạng có thể không xác nhận được các giao dịch mới, dẫn đến tình trạng từ chối dịch vụ (DoS). Chức năng mạng và niềm tin của người dùng vào hệ thống sẽ bị ảnh hưởng nghiêm trọng.

Cách khắc phục của Sui: loại bỏ tính năng tháo rời khỏi quy trình sự cố RPC

Cần lưu ý rằng CodeUnitVerifier trong Move Bytecode Verifier chịu trách nhiệm đảm bảo rằng phần code_unit không bao giờ trống. Tuy nhiên, thứ tự của các hoạt động khiến trình xử lý RPC gặp phải các lỗ hổng tiềm ẩn. Điều này là do quá trình xác thực diễn ra trên nút Trình xác thực, đây là giai đoạn sau khi RPC xử lý các mô-đun đầu vào.

Để giải quyết vấn đề này, Sui đã giải quyết lỗ hổng bằng cách loại bỏ chức năng tháo gỡ trong quy trình RPC phát hành của mô-đun. Đây là một cách hiệu quả để ngăn các dịch vụ RPC xử lý mã byte chưa được xác thực, nguy hiểm tiềm ẩn.

Ngoài ra, điều đáng chú ý là các phương thức RPC khác liên quan đến tra cứu đối tượng cũng chứa các khả năng tháo gỡ, nhưng chúng không dễ bị tổn thương khi sử dụng các ô mã trống. Điều này là do họ luôn truy vấn và tháo rời các mô-đun đã xuất bản hiện có. Các mô-đun đã xuất bản phải được xác minh, do đó, giả định về các ô mã không trống luôn được áp dụng khi xây dựng VMControlFlowGraph.

Đề xuất cho nhà phát triển

Sau khi hiểu các mối đe dọa đối với sự ổn định của các dịch vụ RPC trong chuỗi khối từ sự hoảng loạn rõ ràng và tiềm ẩn, các nhà phát triển phải nắm vững các chiến lược để ngăn chặn hoặc giảm thiểu những rủi ro này. Những chiến lược này có thể làm giảm khả năng ngừng dịch vụ ngoài kế hoạch và tăng khả năng phục hồi của hệ thống. Do đó, nhóm chuyên gia của CertiK đưa ra các đề xuất sau đây và liệt kê chúng là các phương pháp hay nhất để lập trình Rust.

Rust Panic Trừu tượng: Bất cứ khi nào có thể, hãy cân nhắc sử dụng chức năng catch_unwind của Rust để bắt hoảng loạn và chuyển chúng thành thông báo lỗi. Điều này giúp toàn bộ chương trình không bị lỗi và cho phép các nhà phát triển xử lý lỗi một cách có kiểm soát.

Thận trọng khi sử dụng API: Tình trạng hoảng loạn tiềm ẩn thường xảy ra do sử dụng sai API do thư viện tiêu chuẩn hoặc bên thứ ba cung cấp. Do đó, điều quan trọng là phải hiểu đầy đủ về API và học cách xử lý các lỗi tiềm ẩn một cách thích hợp. Các nhà phát triển phải luôn giả định rằng các API có thể bị lỗi và chuẩn bị cho những tình huống như vậy.

Xử lý lỗi thích hợp: sử dụng các loại Kết quả và Tùy chọn để xử lý lỗi thay vì hoảng loạn. Cái trước cung cấp một cách xử lý lỗi và trường hợp đặc biệt được kiểm soát nhiều hơn.

Thêm tài liệu và nhận xét: Đảm bảo mã của bạn được ghi chép đầy đủ và thêm nhận xét vào các phần quan trọng (bao gồm cả những phần có thể xảy ra hoảng loạn). Điều này sẽ giúp các nhà phát triển khác hiểu được những rủi ro tiềm ẩn và giải quyết chúng một cách hiệu quả.

Tóm tắt

Các nút RPC dựa trên rỉ sét đóng một vai trò quan trọng trong các hệ thống chuỗi khối như Aptos, StarCoin và Sui. Vì chúng được sử dụng để kết nối DApps và chuỗi khối cơ bản nên độ tin cậy của chúng rất quan trọng đối với hoạt động trơn tru của hệ thống chuỗi khối. Mặc dù các hệ thống này sử dụng ngôn ngữ Rust an toàn cho bộ nhớ nhưng vẫn có nguy cơ thiết kế kém. Nhóm nghiên cứu của CertiK đã khám phá những rủi ro này bằng các ví dụ thực tế chứng minh sự cần thiết của thiết kế cẩn thận và tỉ mỉ trong lập trình an toàn cho bộ nhớ.

Xem bản gốc
This page may contain third-party content, which is provided for information purposes only (not representations/warranties) and should not be considered as an endorsement of its views by Gate, nor as financial or professional advice. See Disclaimer for details.
  • Phần thưởng
  • Bình luận
  • Chia sẻ
Bình luận
0/400
Không có bình luận
  • Ghim
Giao dịch tiền điện tử mọi lúc mọi nơi
qrCode
Quét để tải xuống ứng dụng Gate
Cộng đồng
Tiếng Việt
  • 简体中文
  • English
  • Tiếng Việt
  • 繁體中文
  • Español
  • Русский
  • Français (Afrique)
  • Português (Portugal)
  • Bahasa Indonesia
  • 日本語
  • بالعربية
  • Українська
  • Português (Brasil)