https://medium.com/@numencyberlabs/analysis-of-the-first-critical-0-day-vulnerability-of-aptos-move-vm-8c1fd6c2b98e
1. Lời nói đầu
Ngôn ngữ lập trình Move đang ngày càng phổ biến gần đây do những lợi thế mạnh mẽ mà nó có so với ngôn ngữ Solidity của Ethereum. Move được sử dụng trong nhiều dự án nổi tiếng, chẳng hạn như Aptos và Sui. Gần đây,Bảo mật Numen Web3 sản phẩm phát hiện lỗ hổng bảo mật đã phát hiện ra lỗ hổng bảo mật cấp độ nghiêm trọng trong Máy ảo (VM) của chuỗi công khai Aptos. Những gì chúng tôi phát hiện ra là một lỗ hổng trong ngôn ngữ có thể khiến các nút Aptos gặp sự cố và gây ra tình trạng từ chối dịch vụ. Trong bài viết này, chúng tôi hy vọng bạn sẽ hiểu rõ hơn về ngôn ngữ Move và tính bảo mật của nó thông qua giải thích về lỗ hổng này. Với tư cách là người đi đầu trong nghiên cứu bảo mật ngôn ngữ Move, chúng tôi sẽ tiếp tục đóng góp không ngừng cho an ninh sinh thái của nó.
2. Các khái niệm quan trọng của ngôn ngữ Move
Mô-đun và Tập lệnh
Move có hai loại chương trình khác nhau: Mô-đun và Tập lệnh. Các mô-đun là các thư viện xác định các loại cấu trúc và các chức năng hoạt động trên các loại đó. Các loại cấu trúc xác định mẫu lưu trữ toàn cầu của Di chuyển và các chức năng mô-đun xác định các quy tắc để cập nhật bộ lưu trữ. Bản thân các mô-đun cũng được lưu trữ trong bộ lưu trữ chung. Tập lệnh là điểm vào để thực thi, tương tự như chức năng chính trong ngôn ngữ truyền thống. Các tập lệnh thường gọi các chức năng của các mô-đun đã xuất bản để cập nhật bộ nhớ chung. Tập lệnh là các đoạn mã tạm thời không được xuất bản trong bộ lưu trữ toàn cầu. Tệp nguồn Move (hoặc đơn vị biên dịch) có thể chứa nhiều mô-đun và tập lệnh. Tuy nhiên, xuất bản các mô-đun hoặc thực thi tập lệnh sử dụng các hoạt động máy ảo (VM) riêng biệt.
Đối với những người quen thuộc với hệ điều hành, mô-đun Move tương tự như mô-đun thư viện động được tải khi chạy tệp thực thi của hệ thống và tập lệnh tương tự như chương trình chính. Người dùng có thể viết tập lệnh của riêng họ để truy cập bộ nhớ chung, bao gồm cả mã gọi mô-đun.
Lưu trữ toàn cầu
Mục đích của chương trình Move là đọc và ghi vào bộ nhớ chung dưới dạng cây. Chương trình không thể truy cập hệ thống tệp, mạng hoặc bất kỳ dữ liệu nào bên ngoài cây này.
Trong một mã giả, bộ nhớ chung trông như thế này:
Về mặt cấu trúc, kho lưu trữ toàn cầu là một khu rừng, bao gồm các cây bắt nguồn từ địa chỉ của một tài khoản. Mỗi địa chỉ có thể lưu trữ dữ liệu tài nguyên và mã mô-đun. Như mã giả ở trên cho thấy, mỗi địa chỉ có thể lưu trữ nhiều nhất một giá trị tài nguyên của một loại đã cho và nhiều nhất một mô-đun của một tên đã cho.
Nguyên tắc máy ảo MOVE
Máy ảo movevm và evm giống nhau, ở chỗ nó cần biên dịch mã nguồn thành mã byte, sau đó được thực thi trong máy ảo. Biểu đồ sau đây cho thấy quá trình.
1. bytecode được nạp thông qua hàm exec_script
2. Thực thi chức năng load_script, chức năng này chủ yếu được sử dụng để giải tuần tự hóa mã byte và xác minh xem mã byte có hợp pháp hay không, nếu xác minh không thành công, nó sẽ trả về lỗi
3. Sau khi xác minh thành công, mã bytecode thực sẽ được thực thi
4. Thực thi mã byte, truy cập hoặc sửa đổi trạng thái lưu trữ toàn cầu, bao gồm tài nguyên, mô-đun
Lưu ý: Còn nhiều tính năng khác liên quan đến Move nhưng chúng tôi sẽ không giới thiệu hết ở đây mà sẽ tiếp tục phân tích các tính năng của ngôn ngữ move từ góc độ bảo mật.
3. Mô tả lỗ hổng
Lỗ hổng này chủ yếu liên quan đến mô-đun xác minh. Trước khi nói về lỗ hổng cụ thể, chức năng của mô-đun xác minh và StackUsageVerifier::verify sẽ được giới thiệu.
Mô-đun xác minh
Chúng tôi biết rằng trước khi thực thi mã bytecode thực sự, sẽ có xác minh mã byte và xác minh có thể được chia thành một số quy trình phụ tương ứng.
Họ đang:
Giới hạnChecker , chủ yếu được sử dụng để kiểm tra tính bảo mật ranh giới của mô-đun và tập lệnh. Điều này bao gồm kiểm tra ranh giới của chữ ký, hằng số, v.v.
Trình kiểm tra trùng lặp , một mô-đun triển khai trình kiểm tra để xác minh xem mỗi vectơ trong CompiledModule có chứa các giá trị khác nhau hay không
Người kiểm tra chữ ký , kiểm tra xem cấu trúc trường có đúng không khi chữ ký được sử dụng cho tham số chức năng, biến cục bộ và thành viên cấu trúc
Hướng dẫnTính nhất quán , xác minh tính nhất quán của hướng dẫn
hằng số được sử dụng để xác minh rằng các hằng số thuộc loại ban đầu và dữ liệu của các hằng số được sắp xếp theo thứ tự chính xác theo loại của chúng
CodeUnitVerifier , để xác minh tính chính xác của mã thân hàm, thông qua stack_usage_verifier.rs và abstract_interpreter.rs tương ứng
script_signature , để xác minh rằng tập lệnh hoặc hàm nhập là chữ ký hợp lệ
Lỗ hổng xảy ra trong quá trình xác minh
CodeUnitVerifier::verify_script(config, script)? ;
chức năng. Bạn có thể thấy rằng có nhiều quy trình con xác minh ở đây.
Đây là tổng kiểm tra an toàn ngăn xếp, tổng kiểm tra an toàn kiểu, tổng kiểm tra an toàn biến cục bộ và tổng kiểm tra an toàn tham chiếu. Lỗ hổng phát sinh trong quá trình xác minh bảo mật ngăn xếp.
Xác minh bảo mật ngăn xếp (StackUsageVerifier::verify)
Mô-đun này được sử dụng để xác minh rằng các khối cơ bản trong chuỗi lệnh mã byte của một chức năng được sử dụng một cách cân bằng. Mỗi khối cơ bản, ngoại trừ những khối kết thúc bằng opcode Ret (return to caller), phải đảm bảo rằng nó rời khỏi khối với cùng chiều cao ngăn xếp như lúc đầu. Ngoài ra, đối với bất kỳ khối cơ bản nào, chiều cao ngăn xếp không được thấp hơn chiều cao ngăn xếp ở đầu khối.
Lặp lại tất cả các khối để xác minh rằng các điều kiện trên được đáp ứng:
Vòng lặp lặp đi lặp lại để xác minh tính hợp pháp của tất cả các khối cơ bản.
Chi tiết lỗ hổng
Như đã giới thiệu trước đó, vì movevm là một máy ảo ngăn xếp, nên khi xác minh tính hợp pháp của các lệnh, rõ ràng trước hết chúng ta cần đảm bảo rằng mã byte của lệnh là chính xác và thứ hai, chúng ta cần đảm bảo rằng bộ nhớ ngăn xếp là hợp lệ sau lệnh gọi khối, nghĩa là ngăn xếp được cân bằng sau thao tác ngăn xếp.verify_block
chức năng được sử dụng để thực hiện mục đích thứ hai.
Như chúng ta có thể thấy từverify_block
mã, nó sẽ lặp qua tất cả các lệnh trong khối mã khối và sau đó xác minh xem tác dụng của khối lệnh trên ngăn xếp có hợp pháp hay không bằng cách cộng hoặc trừnum_pops
,num_pushes
. Thứ nhất, thông quastack_size_increment < num_pops
để xác định xem không gian ngăn xếp có hợp pháp hay không. Nếu nhưnum_pops
lớn hơnstack_size_increment
, điều đó có nghĩa là số lần bật mã byte lớn hơn kích thước của chính ngăn xếp và lỗi được trả về và tổng kiểm tra mã byte không thành công. Sau đó, thông quastack_size_increment -= num_pops; stack_size_increment += num_pushes;
, hai lệnh này sửa đổi tác động lên chiều cao ngăn xếp sau mỗi lệnh được thực thi. Và cuối cùng, khi vòng lặp kết thúc,stack_size_increment
cần bằng 0, tức là Sau khi giữ nguyên các thao tác trong khối này, ngăn xếp cần được cân bằng.
Có vẻ như không có gì sai ở đây, nhưng do trong quá trình thực thi 16 dòng mã, nó không xác định được liệu có tràn số nguyên hay không, dẫn đến lỗ hổng tràn số nguyên có thể được kiểm soát gián tiếp bằng cách xây dựng num_pushes, stack_size_increment lớn . Vì vậy, làm thế nào để chúng tôi xây dựng một số lượng đẩy lớn như vậy?
Có vẻ như không có vấn đề gì, nhưng vì dòng mã thứ 16 được thực thi ở đây nên không thể đánh giá liệu có tràn số nguyên hay không. Kết quả là, cácstack_size_increment
có thể được kiểm soát gián tiếp bằng cách xây dựng một quá khổnum_pushes
, dẫn đến lỗ hổng tràn số nguyên.
Ở đây trước tiên chúng ta cần giới thiệu định dạng tệp bytecode di chuyển.
Di chuyển định dạng tệp mã byte
Giống như các tệp Windows PE hoặc tệp ELF linux, các tệp mã byte di chuyển kết thúc bằng .mv và bản thân các tệp có một định dạng nhất định.
Đầu tiên là ma thuật, giá trị là A11CEB0B, tiếp theo là thông tin phiên bản và số lượng bảng, sau đó là tiêu đề bảng, có thể có nhiều bảng. Loại bảng là loại bảng, tổng cộng có các loại 0x10 (như thể hiện ở bên phải của hình), để biết thêm chi tiết, bạn có thể muốn xem tài liệu ngôn ngữ di chuyển, Tiếp theo là phần bù của bảng và độ dài của cái bàn. Sau đó là nội dung bảng và cuối cùng là Dữ liệu cụ thể, có hai loại, đối với mô-đun, đó là Dữ liệu cụ thể của mô-đun, đối với loại tập lệnh, đó là Dữ liệu cụ thể của tập lệnh.
Định dạng tệp độc hại được xây dựng
Ở đây chúng tôi đang tương tác với Aptos trong tập lệnh, vì vậy chúng tôi xây dựng định dạng tệp được hiển thị bên dưới để gây tràn stack_size_increment:
Trước tiên, hãy giải thích định dạng của tệp bytecode này:
+0x00–0x03: là từ ma thuật 0xA11CEB0B
+0x04–0x7: là phiên bản định dạng tệp,phiên bản của nó là 4
+0x8–0x8: là số lượng bảng, giá trị là 1
+0x9–0x9: là loại bảng, loại của nó là CHỮ KÝ
+0xa-0xa: là offset của bảng, giá trị là 0
+0xb-0xb: là độ dài của bảng, giá trị là 0x10
+0xc-0x18: là dữ liệu của SIGNATURES Token
Bắt đầu từ 0x22, nó là phần mã của mã chức năng chính của tập lệnh.
Thông qua công cụ move-disassembler, chúng ta có thể thấy mã lệnh tháo gỡ như sau:
Trong đó, các mã tương ứng với ba lệnh 0, 1 và 2 lần lượt là dữ liệu trong hộp màu đỏ, hộp màu xanh lá cây và hộp màu vàng.
LdU64 không có mối quan hệ nào với lỗ hổng bảo mật. Chúng tôi sẽ không đi vào quá nhiều chi tiết ở đây, nhưng bạn có thể kiểm tra mã nếu bạn quan tâm. Ở đây chúng tôi tập trung giải thích hướng dẫn VecUnpack. Chức năng của VecUnpack là đẩy tất cả dữ liệu vào ngăn xếp khi gặp đối tượng vectơ trong mã.
Trong tệp được tạo này, chúng tôi tạo VecUnpack hai lần,Số vectơ của nó lần lượt là 3315214543476364830,18394158839224997406.
Khi chức nănghướng_dẫn
được thực thi, dòng mã thứ hai bên dưới thực sự được thực thi:
Sau khi thực hiện cáchướng_dẫn
hàm, nó trả về (1,3315214543476364830) lần đầu tiên. Tại thời điểm này, stack_size_increment là 0, num_pops là 1 và num_pushes là 3315214543476364830. Giá trị trả về thứ hai là (1,18394158839224997406). Khi thực hiện lạistack_size_increment += num_pushes ;
stack_size_increment đã là 0x2e020210021e161d (3315214543476364829).
num_pushes là 0xff452e02021e161e (18394158839224997406), khi cả hai được thêm vào, giá trị này lớn hơn giá trị tối đa của u64, dẫn đến dữ liệu bị cắt bớt và giá trị của stack_size_increment trở thành 0x12d473012043c2c3b, gây tràn số nguyên khiến nút Aptos bị sập, do đó làm cho nút ngừng chạy. Do các tính năng bảo mật của ngôn ngữ rỉ sét, nó sẽ không gây thêm tác động bảo mật mã như C/C++.
4. Tác động của tính dễ bị tổn thương
Do lỗ hổng này xảy ra ở module thực thi Move nên đối với các node trên chain, nếu mã bytecode được thực thi sẽ gây ra tấn công DoS. Trong trường hợp nghiêm trọng, mạng Aptos có thể bị dừng hoàn toàn, điều này sẽ gây ra thiệt hại khôn lường và ảnh hưởng nghiêm trọng đến sự ổn định của nút.
5. Bản sửa lỗi chính thức
Khi phát hiện ra lỗ hổng này, chúng tôi đã báo cáo với nhóm Aptos chính thức và họ đã nhanh chóng khắc phục lỗ hổng. Bạn có thể tham khảo hình bên dưới để biết ảnh chụp màn hình sửa lỗi.
Liên kết mã có liên quan ở bên dưới: