Fuzzamoto: Giới thiệu

Bài viết này được dịch máy
Xem bản gốc

Tác giả: NIKLAS GÖGGE

Nguồn: https://brink.dev/blog/2026/01/07/fuzzamoto-introduction/

Khoảng một năm trước, tôi bắt đầu phát triển " Fuzzamoto ", một công cụ kiểm thử mờ (fuzz testing) được triển khai như một nút Bitcoin hoàn chỉnh. Trong sê-ri trên blog này, tôi sẽ chia sẻ kinh nghiệm, những khó khăn và những câu hỏi còn bỏ ngỏ mà tôi gặp phải trong quá trình thực hiện. Bài đăng đầu tiên sẽ giới thiệu động lực đằng sau việc tạo ra dự án Fuzzamoto và cung cấp tổng quan về thiết kế và kiến ​​trúc của nó. Trong các chương tiếp theo, tôi sẽ đi sâu vào tính xác định, việc tạo trường hợp kiểm thử và kiểm thử đột biến, các lỗi được phát hiện, phản hồi về các bài kiểm thử mờ vượt quá phạm vi bao phủ của chúng, và nhiều hơn nữa.

động lực

Cho đến nay, Bitcoin Core là phiên bản được sử dụng rộng rãi nhất của giao thức Bitcoin trên mạng, vì vậy các lỗi của nó có thể gây ra hậu quả thảm khốc. Đây cũng là lý do tại sao Bitcoin Core có một văn hóa phát triển rất thận trọng, điều này cần thiết ở một mức độ nào đó và là một lợi thế (rõ ràng là lý do chính khiến nó không có nhiều lỗi nghiêm trọng), nhưng nó cũng có thể dẫn đến sự thất vọng, kiệt sức và làm chậm tiến độ của các dự án lớn.

Ngay cả khi bạn chỉ muốn cải thiện việc kiểm thử, tái cấu trúc mã hiện có để dễ kiểm thử hơn, hoặc thêm các mục kiểm thử, bạn cũng có thể đối diện sự phản đối đáng kể. Nguồn lực cho dự án này cực kỳ hạn chế, và các yêu cầu kéo (PR) phức tạp không hấp dẫn người đánh giá vì việc tăng tốc 20% thời gian đồng bộ hóa khởi tạo, các tính năng giao thức P2P và các thay đổi nhỏ khác đòi hỏi ít thời gian và công sức hơn để xem xét. Tệ hơn nữa, nếu bạn phải tái cấu trúc mã để thêm nhiều bài kiểm thử hơn, chính quá trình tái cấu trúc đó có thể gây ra lỗi. Điều này trở thành một vấn đề "con gà và quả trứng" - việc kiểm thử làm giảm rủi ro khi tái cấu trúc, nhưng việc thêm bài kiểm thử lại đòi hỏi phải tái cấu trúc.

Những thao tác tái cấu trúc này thường tách rời mô-đun, chia mã thành mô-đun riêng biệt hoặc thêm các giao diện để cho phép giả lập (mocking), điều này thường cần thiết cho việc kiểm thử mờ (fuzzing) trong quá trình xử lý/liên tục. Ví dụ, nếu thành phần cần kiểm thử phụ thuộc vào I/O ổ đĩa, việc kiểm thử mờ sẽ chậm và có khả năng mở rộng, vì vậy bạn sẽ muốn giả lập nó (I/O ổ đĩa) để tăng tốc độ.

Ở giai đoạn này, việc cải thiện đáng kể quá trình kiểm thử Bitcoin Core (phát hiện các lỗi nghiêm trọng hiện có hoặc ngăn chặn lỗi leo thang) vẫn rất khả thi. Ví dụ, mã P2P để xác thực các khối, giao dịch và khối dày đặc chưa được kiểm thử mờ, cũng như các đường dẫn mã liên kết hơn liên quan đến việc lắp ráp lại Chuỗi và các quy trình tương tự khác. Để kiểm thử hiệu quả mã này bằng các biện pháp kiểm soát trong quá trình truyền thống, cần phải thay đổi mã quan trọng (đồng thuận, P2P, v.v.), mã chưa được kiểm thử ở mức độ cần thiết.

Từ kinh nghiệm cá nhân, tôi có thể khẳng định rằng việc nhận được sự ủng hộ cho những lần tái cấu trúc này rất khó khăn, và tôi đã mất hứng thú duy trì các yêu cầu kéo (PR) trong nhiều tháng hoặc thậm chí nhiều năm; lợi ích của những lần tái cấu trúc này thường bị lu mờ bởi tỷ lệ thay đổi tính năng cao hơn và những thay đổi nhỏ.

Đối diện bối cảnh này, Bitcoin Core rõ ràng sẽ được hưởng lợi từ việc thử nghiệm không gây thêm gánh nặng không cần thiết cho quy trình xem xét nghiêm ngặt hoặc yêu cầu bất kỳ thay đổi nào đối với chính Bitcoin Core. Việc thử nghiệm như vậy có thể lấp đầy những lỗ hổng thử nghiệm còn lại và giảm thiểu rủi ro trong tương lai. Viễn cảnh mong đợi cuối cùng là một công cụ thử nghiệm bên ngoài nhận các tệp nhị phân sẵn sàng cho hoàn cảnh làm đầu vào và lỗi làm đầu ra.

Các bài kiểm thử chức năng của Bitcoin Core từng là điều gần nhất với ý tưởng này vì chúng tạo ra nút hoàn chỉnh và kiểm thử chúng thông qua các giao diện bên ngoài của nút(RPC, P2P, IPC, v.v.). Việc viết các bài kiểm thử chức năng không yêu cầu tái cấu trúc; nhiều nhất, nó chỉ yêu cầu thêm phương pháp RPC mới, cho phép phân tích các phần trạng thái của phần mềm. Mặc dù các bài kiểm thử này đạt được độ phủ mã cao và phát hiện ra lượng lớn lỗi, nhưng chúng không phải là các bài kiểm thử dựa trên thuộc tính và không tự động phát hiện ra các trường hợp ngoại lệ. Ví dụ, một bản vá một dòng cho một bài kiểm thử chức năng đã không tìm thấy CVE-2024-35202 ; nó thực sự được phát hiện thông qua việc tái cấu trúc và viết các bài kiểm thử fuzz mới (ngẫu nhiên, các bài kiểm thử và việc tái cấu trúc này chưa bao giờ được hợp nhất vào Bitcoin Core). Nếu các bài kiểm thử chức năng đó là các bài kiểm thử dựa trên thuộc tính, có lẽ chúng đã có thể tìm ra vấn đề.

Sau khi nhận ra điều này, tôi tự hỏi: Liệu chúng ta có thể có "kiểm thử mờ chức năng" không? Nó chia sẻ các khái niệm kiểm thử tương tự như kiểm thử chức năng, nhưng thay vì sử dụng các kịch bản kiểm thử được mã hóa cứng và kết quả được xác định trước, nó sử dụng kiểm thử mờ để kiểm tra các thuộc tính ở cấp độ hệ thống. Đây là triết lý đằng sau Fuzzamoto: mô phỏng toàn hệ thống, dựa trên kiểm thử mờ.

thiết kế

Nói một cách trừu tượng, việc kiểm thử bằng Fuzzamoto bao gồm một tiến trình nút đầy đủ (ví dụ: bitcoind, btcd, v.v.) làm mục tiêu kiểm thử, một công cụ kiểm thử (kiểm soát việc thực thi kiểm thử dựa trên đầu vào mờ) và một công cụ kiểm thử (được sử dụng để tạo ra các đầu vào cung cấp cho công cụ để thực thi).

Kiến trúc Fuzzamoto

Kiểm tra mờ ảnh chụp nhanh

Một thách thức rõ ràng cần được giải quyết là việc triển khai đơn giản thiết kế này có thể dẫn đến việc nút mục tiêu tích lũy trạng thái sau khi kiểm thử mờ được thực thi (do đó ảnh hưởng đến các bài kiểm thử tiếp theo), dẫn đến tính không xác định. Tôi sẽ khám phá tính xác định và những thách thức của nó sâu hơn trong các bài đăng trên blog sau này trong sê-ri này, nhưng tóm lại: để làm cho các bài kiểm thử mờ hiệu quả, chúng ta muốn việc thực thi các trường hợp kiểm thử phải mang tính xác định, nghĩa là, với cùng một đầu vào, các hành động kiểm thử phải giống nhau.

Fuzzamato giải quyết vấn đề trạng thái này bằng cách sử dụng phương pháp fuzzing ảnh chụp nhanh hoàn toàn có hệ thống. Nguyên tắc là chạy nút mục tiêu và các công cụ kiểm thử trong một máy ảo đặc biệt; máy ảo này có khả năng tạo ra ảnh chụp nhanh tất cả các trạng thái của nó (bộ nhớ, trạng thái CPU, thiết bị, v.v.) và nhanh chóng thiết lập lại về trạng thái đó. Hiện tại, Fuzzamato sử dụng Nyx làm máy ảo nền tảng, nhưng về mặt lý thuyết, bất kỳ máy ảo nền tảng nào có khả năng tương tự đều có thể hoạt động.

Điều này cho phép chúng ta tránh việc tạo và hủy trạng thái lần(điều này rất tốn kém). Chúng ta có thể thiết lập trực tiếp trạng thái cần thiết ngay từ đầu, sau đó chụp ảnh trạng thái, bắt đầu kiểm thử lỗi và nhanh chóng khôi phục máy ảo về trạng thái ban đầu sau mỗi lần thực thi. Cụ thể, đối với việc kiểm thử lỗi Bitcoin nút đầy đủ của Bitcoin, điều này cho phép chúng ta (ví dụ) khai thác trước một blockchain và cung cấp một đầu ra Coinbase hoàn chỉnh (và có thể kiểm thử ngay lập tức).

Trong bài viết tiếp theo của sê-ri bài blog này, chúng ta sẽ giải thích chi tiết kỹ thuật của Nyx: cách thức hoạt động, cách Fuzzamoto sử dụng nó và cách thức chỉ dẫn độ phủ hoạt động trong chế độ này.

Bối cảnh

Trong Fuzzamoto, các công cụ fuzzing có thể được gọi là "scenes" (cảnh), chịu trách nhiệm thiết lập trạng thái chụp nhanh, kiểm soát việc thực thi các đầu vào fuzzing và báo cáo kết quả cho công cụ kiểm thử. Mỗi scene cần thực hiện hai chức năng:

  • Việc tạo ra kịch bản và thiết lập trạng thái chụp nhanh bao gồm việc tạo ra tiến trình nút đầy đủ mục tiêu và đưa nút về trạng thái cần thiết để chạy thử nghiệm fuzz.
  • Việc thực thi một trường hợp kiểm thử có nghĩa là thực thi trường hợp kiểm thử đó trong trạng thái đã được tạo ra trước đó.

Việc kiểm thử máy chủ HTTP, di chuyển ví, giao diện RPC và các luồng giao thức P2P cụ thể (như chuyển tiếp khối nhỏ gọn) đều có các kịch bản riêng. Tất cả đều nhận các byte thô làm đầu vào và sử dụng Arbitrary để phân tích các byte này thành dữ liệu đầu vào kiểm thử có cấu trúc, sau đó được thực thi trên mục tiêu. Vì đầu vào là một mảng byte chung, chúng ta có thể sử dụng AFL++ để kiểm thử các kịch bản này, vì nó hỗ trợ kiểm thử ảnh chụp nhanh bằng Nyx.

Trong quá trình phát triển các kịch bản riêng biệt để kiểm tra các luồng giao thức P2P khác nhau (chuyển tiếp giao dịch, chuyển tiếp khối dày đặc, lắp ráp lại Chuỗi, v.v.), tôi chợt nhận ra rằng việc chồng chéo giữa chúng có những lợi ích nhất định. Ví dụ, để kiểm tra chuyển tiếp khối dày đặc hoặc lắp ráp lại Chuỗi, việc giả định rằng việc gửi đồng thời các giao dịch có hình dạng và loại khác nhau đến nút kiểm thử (như trong kịch bản kiểm thử chuyển tiếp giao dịch) sẽ gây ra lỗi là điều hoàn toàn hợp lý. Do đó, ý tưởng chuyển sang phát triển một kịch bản rất rộng để nút toàn bộ giao diện P2P của nút, bao gồm mọi thứ. Fuzzamoto tích hợp một công cụ fuzzing tùy chỉnh dựa trên LibAFL dành riêng cho kịch bản fuzzing này, mà chúng ta sẽ thảo luận chi tiết hơn trong các chương sau của sê-ri này.

Thành công ban đầu

Một trong những kịch bản đầu tiên tôi viết nhắm vào các RPC của Bitcoin Core, cụ thể là kết hợp nhiều kết quả liên quan và không liên quan từ các RPC theo những cách thú vị. Kịch bản này gọi các RPC theo thứ tự được chọn bởi công cụ kiểm thử mờ, sau đó phân tích một phần đầu vào mờ thành đầu vào của RPC, hoặc chọn từ một tập hợp các giá trị được trả về bởi các RPC trước đó. Ví dụ, nếu bài kiểm tra gọi generatetoaddress , sau đó nó có thể truyền mã băm khối được trả về bởi RPC này làm đầu vào cho các RPC khác, hoặc loại bỏ một giá trị băm khỏi đầu vào mờ.

Tình huống này đã thành công trong việc phát hiện một lỗi trong cấu trúc dữ liệu chỉ mục khối. Lỗi này chỉ xảy ra khi sử dụng đồng thời các lệnh RPC invalidateblockreconsiderblock (cả hai chỉ có thể được sử dụng ở chế độ kiểm thử).

 bitcoind: validation.cpp:5392: void ChainstateManager::CheckBlockIndex(): Assertion '(pindex->nStatus & BLOCK_FAILED_MASK) == 0' failed.

Mặc dù lỗi này không phải là vấn đề bảo mật nghiêm trọng (nó chỉ có thể được kích hoạt thông qua RPC giới hạn ở chế độ kiểm thử), nhưng nó làm nổi bật sức mạnh của một phương pháp hoàn toàn có hệ thống: nó ngay lập tức cho thấy đó là một lỗi, chứ không phải là lỗi báo động giả (tức là tiến trình nền bị sập, điều này không bao giờ nên xảy ra), và kiểm thử mờ bằng ảnh chụp nhanh cho phép thiết lập lại trạng thái một cách hiệu quả (điều mà nếu không có ảnh chụp nhanh thì chỉ có thể thực hiện được thông qua việc tái cấu trúc).

Lỗi tương tự cũng được phát hiện bởi những người đóng góp khác, những người đã chỉnh sửa lại mã và viết các bài kiểm tra lỗi (fuzz test) cho mã lập chỉ mục khối. Yêu cầu kéo (PR) bổ sung công việc này vào mã nguồn đã được mở suốt một năm trước khi được hợp nhất!

Vì các bài kiểm tra Fuzzamoto hoạt động ở cùng cấp độ với các bài kiểm tra chức năng, chúng ta có thể chuyển đổi bất kỳ trường hợp kiểm tra Fuzzamoto nào thành một bài kiểm tra chức năng Bitcoin Core (điều này có thể thực hiện được miễn là lỗi có thể tái hiện một cách chắc chắn). Ví dụ, bài kiểm tra chức năng sau đây tái hiện lỗi chỉ mục khối:

 from test_framework.test_framework import BitcoinTestFrameworkclass CheckBlockIndexBug(BitcoinTestFramework): def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 1 def run_test(self): self.generatetoaddress(self.nodes[0], 1, "2N9hLwkSqr1cPQAPxbrGVUjxyjD11G2e1he"); hashes = self.generatetoaddress(self.nodes[0], 1, "2N9hLwkSqr1cPQAPxbrGVUjxyjD11G2e1he"); self.generatetoaddress(self.nodes[0], 1, "2N2CmnxjBbPTHrawgG2FkTuBLcJtEzA86sF"); res = self.nodes[0].gettxoutsetinfo() self.generatetoaddress(self.nodes[0], 3, "2N9hLwkSqr1cPQAPxbrGVUjxyjD11G2e1he"); self.log.info(self.nodes[0].invalidateblock(res["bestblock"])) self.generatetoaddress(self.nodes[0], 3, "2N9hLwkSqr1cPQAPxbrGVUjxyjD11G2e1he"); self.nodes[0].reconsiderblock(hashes[0]) self.nodes[0].invalidateblock(hashes[0]) self.log.info(self.nodes[0].reconsiderblock(res["bestblock"]))if __name__ == '__main__': CheckBlockIndexBug(__file__).main()

Bằng cách này, các nhà phát triển cần sửa lỗi này không cần phải tự biên dịch Fuzzamoto; họ chỉ cần sử dụng các công cụ quen thuộc để gỡ lỗi.

Để xem danh sách các vấn đề đã được xác định và công khai cho đến nay, vui lòng xem phần "khoe khoang" trong tệp readme của dự án này.

Bài viết tiếp theo trong sê-ri này sẽ đi sâu vào những khía cạnh liên quan đến thuyết bất định.

Nguồn
Tuyên bố từ chối trách nhiệm: Nội dung trên chỉ là ý kiến của tác giả, không đại diện cho bất kỳ lập trường nào của Followin, không nhằm mục đích và sẽ không được hiểu hay hiểu là lời khuyên đầu tư từ Followin.
Thích
Thêm vào Yêu thích
Bình luận