https://medium.com/num-cyber-labs/the-story-of-a-high-vulnerability-in-move-reference-safety-verify-module-2340f3d8c642
Lời nói đầu 0x0
Một thời gian trước, chúng tôi đã tìm thấy một quan trọnglỗ hổng trong Aptos Movevm. Sau một thời gian nghiên cứu sâu, chúng tôi đã phát hiện ra một lỗ hổng nghiêm trọng khác cũng là lỗi tràn số nguyên nhưng lần này cực kỳ thú vị.
Chúng tôi biết rằng ngôn ngữ Move xác minh đơn vị mã trước khi thực thi mã byte. Trong đơn vị mã xác minh, nó được chia thành 4 bước. Lỗi này xảy ra tại Reference_safety. Chúng tôi sẽ đi vào chi tiết hơn về nó dưới đây.
Mô-đun này xác định các hàm truyền để xác minh tính an toàn tham chiếu của phần thân thủ tục. Việc kiểm tra bao gồm (nhưng không giới hạn) việc xác minh rằng không có tham chiếu lơ lửng nào, quyền truy cập vào các tham chiếu có thể thay đổi là an toàn và quyền truy cập vào các tham chiếu lưu trữ toàn cầu là an toàn.
Đây là điểm vào xác minh, nó sẽ gọi hàm phân tích.
Trong Analyze_function, hàm sẽ được xác minh với từng khối cơ bản, vậy khối cơ bản là gì?
bên trongxây dựng trình biên dịch , một khối cơ bản là một chuỗi mã đường thẳng không có nhánh nào ngoại trừ đầu vào và không có nhánh nào ngoại trừ đầu ra.
Trong ngôn ngữ Move, làm thế nào để chúng ta xác định một khối cơ bản?
Trong ngôn ngữ Move, khối cơ bản được xác định bằng cách duyệt mã byte, tìm tất cả các hướng dẫn nhánh và hướng dẫn vòng lặp. Bạn có thể xem mã lõi bên dưới:
Dưới đây là một ví dụ về khối mã của ngôn ngữ Move. Chúng ta có thể thấy có 3 khối cơ bản. Các nhánh được xác định bởi lệnh: BrTrue, Branch, Ret.
An toàn tham chiếu 0x1 khi di chuyển
Move hỗ trợ hai loại tham chiếu:bất biến — được xác định bằng & (ví dụ: &T) vàcó thể thay đổi — &mut (ví dụ: &mut T). Bạn có thể sử dụng các tham chiếu không thể thay đổi (&) để đọc dữ liệu từ các cấu trúc và sử dụng có thể thay đổi (&mut) để sửa đổi chúng. Bằng cách sử dụng loại tham chiếu thích hợp, bạn giúp duy trì bảo mật và bạn nên biết liệu phương thức này có thay đổi giá trị hay chỉ đọc.
Dưới đây là một ví dụ từ hướng dẫn Move chính thức:
Trong ví dụ trên, chúng ta có thể thấy mut_ref_t là tham chiếu có thể thay đổi của giá trị t.
Vì vậy, mô-đun an toàn tham chiếu Move cố gắng xác nhận xem tham chiếu có hợp lệ hay không bằng cách lấy chức năng làm đơn vị, quét các khối cơ bản trong chức năng và đánh giá xem tất cả các hoạt động tham chiếu có hợp pháp hay không thông qua xác minh hướng dẫn mã byte.
Hình dưới đây cho thấy quy trình trong đó nó xác minh tính an toàn của tham chiếu.
Trạng thái ở đây là AbstractState chứa biểu đồ mượn và các địa phương mà cả hai đều được sử dụng để bảo mật các tham chiếu.
vay_graph là một biểu đồ biểu thị mối quan hệ giữa các tham chiếu địa phương.
Chúng ta có thể thấy từ hình trên, có mộttrạng thái trước bao gồm biểu đồ cục bộ và mượn (L ,BG) và sau đó thực hiện khối cơ bản sẽ tạo ra mộtbài trạng thái với (L’,BG’), sau đó sẽ hợp nhất trạng thái trước và sau để cập nhật trạng thái khối và truyền điều kiện sau của khối này sang các khối kế tiếp. Điều này giống nhưbiển nút tối ưu hóa trong động cơ phản lực V8.
Đoạn mã dưới đây là vòng lặp chính tương ứng với hình trên. Đầu tiên, thực thi mã khối (Nó sẽ trả về một Lỗi phân tích nếu thực hiện lệnh không thành công) và sau đó thử hợp nhấttrạng thái trước Vàbài trạng thái bằng cách xác định liệutham gia_result có bị thay đổi hay không. Nếu nó bị thay đổi và khối hiện tại chứa một điểm cạnh (có nghĩa là có một vòng lặp), nó sẽ quay trở lại điểm đầu của vòng lặp, trong vòng tiếp theo sẽ vẫn thực hiện khối này cho đến khibài trạng thái bằngtrạng thái trước hoặc hủy bỏ bởi một số lỗi.
Làm cách nào để chúng tôi đánh giá liệu tham giaResult đã thay đổi hay không thay đổi?
Thông qua đoạn mã trên, chúng ta có thể biết liệu kết quả nối có thay đổi hay không bằng cách đánh giá xem mối quan hệ giữa các địa phương và mối quan hệ mượn có thay đổi hay không. Hàm join_ ở đây được sử dụng để cập nhật trạng thái biểu đồ cục bộ và mượn.
Với mã tham gia bên dưới, dòng 6 là để khởi tạo Bản đồ địa phương mới, dòng 9 dùng để lặp lại tất cả chỉ mục trong địa phương nếu tất cả giá trị là Không, trước và sau khi thực hiện khối để bạn không chèn vào bản đồ mới bản đồ địa phương. Nếu trước trạng thái có giá trị, trạng thái sau là Không thì chúng ta cần giải phóng id brow_graph, nghĩa là loại bỏ mối quan hệ vay mượn của giá trị. Ngược lại cũng vậy. Đặc biệt, khi cả hai giá trị đều tồn tại và giống nhau, hãy chèn chúng vào bản đồ mới giống như dòng 30–33 và sau đó hợp nhất đồ thị mượn tại dòng 38.
Từ phía trên, chúng ta có thể thấy self.iter_locals() là số của người dân địa phương. Và lưu ý rằng cục bộ này không chỉ bao gồm các cục bộ thực của hàm mà còn cả các tham số.
Lỗ hổng 0x2
Ở đây chúng tôi đã vượt qua tất cả các mã liên quan đến lỗ hổng, bạn đã tìm thấy nó chưa?
Nếu bạn không thể tìm thấy lỗ hổng, điều đó không thành vấn đề. Tôi sẽ trình bày chi tiết quá trình kích hoạt lỗ hổng bên dưới.
Đầu tiên trong đoạn mã bị lỗi nếu tham số độ dài thêm độ dài cục bộ lớn hơn 256. Có vẻ như không có vấn đề gì phải không?
Nhưng chức năng này sẽ trả về Iterator với loại mục u8.
Vì vậy, trong hàm join_() là giá trị kết hợp của function_view.parameters().len() và function_view.locals().len() lớn hơn 256.
Trong mã,cho địa phương trong self.iter_locals() , biến cục bộ có kiểu u8. Sau 256 lần lặp sẽ gây tràn. Sau khi tràn, giá trị của local là 8.
Trên thực tế, Move có quy trình xác minh số cục bộ, nhưng tiếc là chỉ xác minh số cục bộ trong mô-đun giới hạn kiểm tra, không bao gồm độ dài của tham số.
Có vẻ như các nhà phát triển biết ở đây cần phải kiểm tra các tham số + giá trị địa phương. Tuy nhiên, mã xác minh số lượng cục bộ trong mô-đun giới hạn kiểm tra, nó không bao gồm độ dài của tham số.
0x3 Di chuyển tràn sang DoS
Chúng tôi biết có một vòng lặp chính để quét khối mã và sau khi gọi hàm exec_block và tham gia trạng thái. Nếu mã di chuyển tồn tại, một vòng lặp sẽ nhảy đến khối bắt đầu thực hiện lại.
Vì vậy, nếu chúng ta tạo một khối mã vòng lặp và khai thác tràn để thay đổi trạng thái của khối, nó sẽ tạo ra bản đồ cục bộ mới trong đối tượng AbstractState Khác với trước đó, sau đó thực thi lại khối bằng hàm exec_block hàm, chúng ta biết chức năng này phân tích mã bytecode và cấp quyền truy cập cho các cục bộ, vì vậy nếu phần bù giá trị tham chiếu không tồn tại trong bản đồ cục bộ của AbstractState mới, nó sẽ dẫn đến DoS.
Sau khi kiểm tra mã, tôi thấy rằng bằng cách sử dụng opcode MoveLoc/CopyLoc/FreeRef, chúng tôi có thể đạt được mục tiêu này.
Ở đây chúng ta hãy xem hàm copy_loc là mã được gọi bởi hàm exec_block trong đường dẫn tệp:
di chuyển/ngôn ngữ/move-bytecode-verifier/src/reference_safety/abstract_state.rs
Trong dòng 287, mã này cố gắng lấy giá trị cục bộ bằng tham số Local Index và nếu Local Index không tồn tại, điều đó sẽ dẫn đến hoảng loạn, Tưởng tượng, khi nút thực thi mã xấu này, sẽ khiến toàn bộ nút bị sập.
0x4PoC
Đây là PoC, trong đó bạn có thể sao chép trong git commit:add615b64390ea36e377e2a575f8cb91c9466844
Đây là nhật ký sự cố:
chủ đề 'regression_tests::reference_analysis::PoC' hoảng sợ trước 'được gọi là `Option::unwrap()` trên `Không` value’, language/move-bytecode-verifier/src/reference_safety/abstract_state.rs:287:39
lưu ý: chạy với `RUST_BACKTRACE=1` biến môi trường để hiển thị một backtrace
Các bước kích hoạt DoS:
chúng ta có thể thấy khối mã là một nhánh không có điều kiện và mỗi khi nó thực hiện nhánh lệnh cuối cùng (0), nó sẽ quay trở lại lệnh đầu tiên nên nó sẽ gọi hàm exec_block và tham gia nhiều lần.
1. Lần đầu Tại đây chúng ta thiết lập thông số là SignatureIndex(0), các địa phương tới SignatureIndex(0) sẽ dẫn num_locals 132*2=264. Vì vậy, sau khi gọi
Dẫn đầu độ dài cục bộ mới là 264–256=8
2. Lần 2 khi thực hiện hàm exec_block và thực hiện lệnh đầu tiên copy_local(57) thì 57 là offset của các local cần đẩy vào stack nhưng lần này chỉ có local với độ dài 8, offset 57 không tồn tại , vì vậy điều này sẽ khiến hàm get(57).unwrap() không trả về kết quả nào và gây hoảng loạn.
Tóm tắt 0x5
Đây là toàn bộ câu chuyện về lỗ hổng này. Từ đó, chúng ta học được rằng:
Thứ nhất, lỗ hổng này cho thấy không có mã nào là an toàn tuyệt đối. Ngôn ngữ Move thực hiện xác minh tĩnh tốt trước khi mã được thực thi, nhưng cũng giống như lỗ hổng này, kiểm tra ranh giới trước đó có thể bị bỏ qua hoàn toàn thông qua lỗ hổng tràn.
Thứ hai, kiểm tra mã là rất quan trọng, vì đôi khi các lập trình viên có thể cẩu thả. Với tư cách là những người đứng đầu về bảo mật ngôn ngữ Move, chúng tôi sẽ tiếp tục tìm hiểu sâu về các vấn đề bảo mật của Move.
Điểm thứ ba là đối với ngôn ngữ Move, chúng tôi khuyên các nhà thiết kế ngôn ngữ nên thêm nhiều mã kiểm tra để ngăn ngừa các trường hợp không mong muốn xảy ra.
Hiện tại, ngôn ngữ Move chủ yếu thực hiện một loạt kiểm tra bảo mật trong giai đoạn xác minh, nhưng tôi nghĩ điều này sẽ không đủ. Khi quá trình xác minh bị bỏ qua, sẽ không có quá nhiều sự củng cố bảo mật trong giai đoạn thời gian chạy, điều này sẽ dẫn đến các mối nguy hiểm tiếp tục được đào sâu, gây ra các vấn đề nghiêm trọng hơn. Cuối cùng, chúng tôi đã phát hiện ra một lỗ hổng khác trong ngôn ngữ Move và chúng tôi sẽ sớm tiết lộ cho bạn.