Trong một bài viết trước, chúng tôi đã thảo luận về việc xác minh chính thức nâng cao các hệ thống Bằng chứng không có kiến thức (ZKP): cách xác minh lệnh ZK. Bằng cách chính thức xác minh từng lệnh zkWasm, chúng tôi hoàn toàn có thể đảm bảo tính bảo mật kỹ thuật và tính chính xác của toàn bộ mạch zkWasm. Trong bài viết này, chúng tôi sẽ tập trung vào quan điểm phát hiện lỗ hổng, phân tích các lỗ hổng cụ thể được xác định trong quá trình kiểm toán và xác minh và các bài học kinh nghiệm từ chúng. Để có một cuộc thảo luận chung về xác minh chính thức nâng cao của các blockchain ZKP, vui lòng tham khảo bài viết về xác minh chính thức nâng cao của các blockchain ZKP.
Trước khi thảo luận về các lỗ hổng ZK, trước tiên chúng ta hãy hiểu cách CertiK thực hiện xác minh chính thức ZK. Đối với các hệ thống phức tạp như Máy ảo ZK (zkVM), bước đầu tiên trong xác minh chính thức (FV) là xác định rõ ràng những gì cần được xác minh và các thuộc tính của nó. Điều này đòi hỏi phải xem xét toàn diện về thiết kế, triển khai mã và thiết lập thử nghiệm của hệ thống ZK. Quá trình này trùng lặp với kiểm toán thường xuyên nhưng khác ở chỗ, sau khi xem xét, các mục tiêu và tài sản xác minh phải được thiết lập. Tại CertiK, chúng tôi gọi đây là xác minh theo định hướng kiểm toán. Công việc kiểm toán và xác minh thường được tích hợp. Đối với zkWasm, chúng tôi đã tiến hành cả kiểm toán và xác minh chính thức đồng thời.
Tính năng cốt lõi của các hệ thống Zero-Knowledge Proof (ZKP) là chúng cho phép chuyển một bằng chứng được mã hóa ngắn gọn về các tính toán được thực hiện ngoại tuyến hoặc riêng tư (chẳng hạn như giao dịch blockchain) sang trình xác minh ZKP. Người xác minh kiểm tra và xác nhận bằng chứng để đảm bảo rằng việc tính toán xảy ra như đã tuyên bố. Trong bối cảnh này, lỗ hổng ZK sẽ cho phép tin tặc gửi bằng chứng ZK giả mạo cho các giao dịch sai và được trình xác minh ZKP chấp nhận.
Đối với tục ngữ zkVM, quy trình chứng minh ZK liên quan đến việc chạy một chương trình, tạo bản ghi thực thi cho mỗi bước và chuyển đổi các bản ghi thực thi này thành một tập hợp các bảng số (một quá trình được gọi là "số học"). Các số này phải thỏa mãn một tập hợp các ràng buộc ("mạch"), bao gồm các mối quan hệ giữa các ô bảng cụ thể, hằng số cố định, ràng buộc tra cứu cơ sở dữ liệu giữa các bảng và phương trình đa thức (hoặc "cổng") mà mỗi cặp hàng bảng liền kề phải thỏa mãn. Xác minh trên chuỗi có thể xác nhận sự tồn tại của một bảng đáp ứng tất cả các ràng buộc mà không tiết lộ các con số cụ thể trong bảng.
Bảng số học tính bằng zkWasm
Độ chính xác của từng ràng buộc là rất quan trọng. Bất kỳ lỗi nào trong một ràng buộc, cho dù nó quá yếu hay thiếu, đều có thể cho phép tin tặc gửi bằng chứng gây hiểu lầm. Các bảng này có thể đại diện cho việc thực hiện hợp lệ hợp đồng thông minh nhưng thực tế thì không. So với các máy ảo truyền thống, độ mờ đục của các giao dịch zkVM khuếch đại các lỗ hổng này. Trong các chuỗi không phải ZK, các chi tiết tính toán giao dịch được ghi lại công khai trên blockchain; tuy nhiên, zkVM không lưu trữ các chi tiết này trên chuỗi. Sự thiếu minh bạch này gây khó khăn cho việc xác định chi tiết cụ thể của một cuộc tấn công hoặc ngay cả khi một cuộc tấn công đã xảy ra.
Mạch ZK thực thi các quy tắc thực thi của các lệnh zkVM cực kỳ phức tạp. Đối với zkWasm, việc triển khai mạch ZK của nó liên quan đến hơn 6.000 dòng mã Rust và hàng trăm ràng buộc. Sự phức tạp này thường có nghĩa là có thể có nhiều lỗ hổng đang chờ được phát hiện.
Kiến trúc mạch zkWasm
Thật vậy, thông qua việc kiểm toán và xác minh chính thức zkWasm, chúng tôi đã phát hiện ra một số lỗ hổng như vậy. Dưới đây, chúng tôi sẽ thảo luận về hai ví dụ đại diện và kiểm tra sự khác biệt giữa chúng.
Lỗ hổng đầu tiên liên quan đến lệnh Load8 trong zkWasm. Trong zkWasm, việc đọc bộ nhớ heap được thực hiện bằng cách sử dụng một tập hợp các lệnh LoadN, trong đó N là kích thước của dữ liệu sẽ được tải. Ví dụ: Load64 nên đọc dữ liệu 64 bit từ địa chỉ bộ nhớ zkWasm. Load8 nên đọc dữ liệu 8 bit (tức là một byte) từ bộ nhớ và đệm nó bằng số không để tạo giá trị 64 bit. Bên trong, zkWasm đại diện cho bộ nhớ dưới dạng một mảng gồm các byte 64 bit, vì vậy nó cần "chọn" một phần của mảng bộ nhớ. Điều này được thực hiện bằng cách sử dụng bốn biến trung gian (u16_cells), cùng nhau tạo thành giá trị tải 64 bit hoàn chỉnh.
Các ràng buộc đối với các lệnh LoadN này được định nghĩa như sau:
Ràng buộc này được chia thành ba trường hợp: Load32, Load16 và Load8. Load64 không có ràng buộc vì các đơn vị bộ nhớ chính xác là 64 bit. Đối với trường hợp Load32, mã đảm bảo rằng 4 byte cao (32 bit) trong đơn vị bộ nhớ phải bằng không.
Đối với trường hợp Load16, mã đảm bảo rằng 6 byte (48 bit) cao trong đơn vị bộ nhớ phải bằng không.
Đối với trường hợp Load8, nó sẽ yêu cầu 7 byte (56 bit) cao trong đơn vị bộ nhớ bằng không. Thật không may, đây không phải là trường hợp trong mã.
Như bạn có thể thấy, chỉ có 9 đến 16 bit cao bị giới hạn ở mức không. 48 bit cao khác có thể là bất kỳ giá trị nào và vẫn truyền dưới dạng "đọc từ bộ nhớ".
Khai thác lỗ hổng này, tin tặc có thể giả mạo bằng chứng ZK của chuỗi thực thi hợp pháp, khiến lệnh Load8 tải các byte không mong muốn này, dẫn đến hỏng dữ liệu. Hơn nữa, thông qua việc sắp xếp cẩn thận mã và dữ liệu xung quanh, nó có thể kích hoạt việc thực hiện và chuyển sai, dẫn đến đánh cắp dữ liệu và tài sản. Các giao dịch giả mạo này có thể vượt qua kiểm tra của trình kiểm tra zkWasm và được blockchain công nhận không chính xác là giao dịch hợp pháp.
Việc khắc phục lỗ hổng này thực sự khá đơn giản.
Lỗ hổng này đại diện cho một lớp lỗ hổng ZK được gọi là "lỗ hổng mã" vì chúng bắt nguồn từ việc triển khai mã và có thể dễ dàng sửa chữa thông qua các sửa đổi mã cục bộ nhỏ. Như bạn có thể đồng ý, những lỗ hổng này cũng tương đối dễ dàng hơn để mọi người xác định.
Chúng ta hãy xem xét một lỗ hổng khác, lần này liên quan đến việc gọi và trả về zkWasm. Gọi và trả về là các lệnh VM cơ bản cho phép một ngữ cảnh đang chạy (ví dụ: hàm) gọi một ngữ cảnh khác và tiếp tục thực hiện ngữ cảnh gọi sau khi người nhận hoàn tất việc thực thi. Mỗi lời kêu gọi mong đợi một sự trở lại sau này. Dữ liệu động được theo dõi bởi zkWasm trong suốt vòng đời của phép gọi và trả về được gọi là "khung cuộc gọi". Vì zkWasm thực hiện các lệnh tuần tự, tất cả các khung cuộc gọi có thể được sắp xếp dựa trên sự xuất hiện của chúng trong thời gian chạy. Dưới đây là một ví dụ về mã gọi / trả về chạy trên zkWasm.
Người dùng có thể gọi hàm buy_token() để mua token (có thể bằng cách thanh toán hoặc chuyển các vật phẩm có giá trị khác). Một trong những bước cốt lõi của nó là tăng số dư tài khoản token lên 1 bằng cách gọi hàm add_token(). Vì bản thân ZK prover không hỗ trợ cấu trúc dữ liệu khung cuộc gọi, nên Bảng thực thi (E-Table) và Bảng nhảy (J-Table) là cần thiết để ghi lại và theo dõi lịch sử đầy đủ của các khung cuộc gọi này.
Hình trên minh họa quá trình buy_token() gọi add_token() và trả về từ add_token() đến buy_token(). Có thể thấy rằng số dư tài khoản token tăng thêm 1. Trong Bảng Thực thi, mỗi bước thực thi chiếm một hàng, bao gồm số khung cuộc gọi hiện tại đang được thực thi, tên hàm ngữ cảnh hiện tại (chỉ nhằm mục đích minh họa), số lệnh đang chạy hiện tại trong hàm và lệnh hiện tại được lưu trữ trong bảng (chỉ nhằm mục đích minh họa). Trong Jump Table, mỗi khung cuộc gọi chiếm một hàng, lưu trữ số khung người gọi, tên ngữ cảnh hàm người gọi (chỉ nhằm mục đích minh họa) và vị trí lệnh tiếp theo của khung người gọi (để khung có thể trả về). Trong cả hai bảng, có một cột "jops" theo dõi xem lệnh hiện tại có phải là lệnh gọi / trả về hay không (trong Bảng Thực thi) và tổng số lệnh gọi / trả lại cho khung đó (trong Bảng Nhảy).
Như mong đợi, mỗi cuộc gọi nên có một kết quả tương ứng và mỗi khung chỉ nên có một cuộc gọi và một lần trả lời. Như thể hiện trong hình trên, giá trị "jops" cho khung đầu tiên trong Jump Table là 2, tương ứng với hàng đầu tiên và thứ ba trong Bảng thực thi, trong đó giá trị "jops" là 1. Mọi thứ có vẻ bình thường vào lúc này.
Tuy nhiên, có một vấn đề ở đây: trong khi một cuộc gọi và một lần trả về sẽ tăng số lượng "jops" cho khung lên 2, hai cuộc gọi hoặc hai lần trả về cũng sẽ dẫn đến số lượng 2. Có hai cuộc gọi hoặc hai lần trả về trên mỗi khung hình có vẻ vô lý, nhưng điều quan trọng cần nhớ là đây chính xác là những gì một hacker sẽ cố gắng làm bằng cách phá vỡ kỳ vọng.
Bạn có thể cảm thấy phấn khích bây giờ, nhưng chúng tôi đã thực sự tìm thấy vấn đề?
Nó chỉ ra rằng hai cuộc gọi không phải là một vấn đề bởi vì các ràng buộc của Execution Table và Jump Table ngăn hai cuộc gọi được mã hóa vào cùng một hàng của một khung vì mỗi cuộc gọi tạo ra một số khung mới, tức là số khung cuộc gọi hiện tại cộng với 1.
Tuy nhiên, tình huống không may mắn cho hai lần trả lại: vì không có khung mới nào được tạo khi trả về, một hacker thực sự có thể lấy Bảng thực thi và Bảng nhảy của một chuỗi thực thi hợp pháp và tiêm lợi nhuận giả mạo (và các khung tương ứng). Ví dụ: ví dụ về Execution Table và Jump Table trước đó về việc gọi buy_token() add_token() có thể bị hacker giả mạo theo kịch bản sau:
Tin tặc đã tiêm hai trả về giả mạo giữa lệnh gọi ban đầu và trả về trong Bảng thực thi và thêm một hàng khung giả mạo mới trong Bảng nhảy (trả về ban đầu và các bước thực thi lệnh tiếp theo trong Bảng thực thi cần được tăng thêm 4). Vì số lượng "jops" cho mỗi hàng trong Jump Table là 2, các ràng buộc được thỏa mãn và trình kiểm tra bằng chứng zkWasm sẽ chấp nhận "bằng chứng" của chuỗi thực thi giả mạo này. Như đã thấy từ hình, số dư tài khoản mã thông báo tăng gấp 3 lần thay vì 1. Do đó, hacker có thể nhận được 3 mã thông báo với giá 1.
Có nhiều cách khác nhau để giải quyết vấn đề này. Một cách tiếp cận rõ ràng là theo dõi riêng biệt các cuộc gọi và trả lại và đảm bảo rằng mỗi khung có chính xác một cuộc gọi và một lần trả lại.
Bạn có thể nhận thấy rằng cho đến nay chúng tôi đã không hiển thị một dòng mã duy nhất cho lỗ hổng này. Lý do chính là không có dòng mã có vấn đề; Việc triển khai mã hoàn toàn phù hợp với bảng và thiết kế ràng buộc. Vấn đề nằm trong chính thiết kế, và bản sửa lỗi cũng vậy.
Bạn có thể nghĩ rằng lỗ hổng này là rõ ràng, nhưng trong thực tế, nó không phải là. Điều này là do có một khoảng cách giữa "hai cuộc gọi hoặc hai lần trả về cũng sẽ dẫn đến số lượng 'jops' là 2" và "hai lần trả về thực sự có thể." Loại thứ hai đòi hỏi một phân tích chi tiết và toàn diện về các ràng buộc khác nhau trong Bảng thực thi và Bảng nhảy, gây khó khăn cho việc thực hiện lý luận không chính thức hoàn chỉnh.
"Lỗ hổng tiêm dữ liệu Load8" và "Lỗ hổng trả về giả mạo" đều có khả năng cho phép tin tặc thao túng bằng chứng ZK, tạo giao dịch sai, đánh lừa người kiểm tra bằng chứng và tham gia trộm cắp hoặc chiếm quyền điều khiển. Tuy nhiên, bản chất của chúng và cách chúng được phát hiện khá khác nhau.
"Lỗ hổng tiêm dữ liệu Load8" được phát hiện trong quá trình kiểm tra zkWasm. Đây không phải là nhiệm vụ dễ dàng, vì chúng tôi phải xem xét hàng trăm ràng buộc trong hơn 6.000 dòng mã Rust và hàng chục lệnh zkWasm. Mặc dù vậy, lỗ hổng tương đối dễ phát hiện và xác nhận. Vì lỗ hổng này đã được khắc phục trước khi bắt đầu xác minh chính thức, nên nó không gặp phải trong quá trình xác minh. Nếu lỗ hổng này không được phát hiện trong quá trình kiểm tra, chúng tôi có thể mong đợi nó được tìm thấy trong quá trình xác minh hướng dẫn Load8.
"Lỗ hổng trả lại giả mạo" đã được phát hiện trong quá trình xác minh chính thức sau khi kiểm toán. Một lý do khiến chúng tôi không phát hiện ra nó trong quá trình kiểm toán là zkWasm có một cơ chế tương tự như "jops" được gọi là "mops", theo dõi các hướng dẫn truy cập bộ nhớ tương ứng với dữ liệu lịch sử cho từng đơn vị bộ nhớ trong thời gian chạy zkWasm. Các ràng buộc về số lượng cây lau nhà thực sự chính xác vì nó chỉ theo dõi một loại lệnh bộ nhớ, ghi bộ nhớ và dữ liệu lịch sử của mỗi đơn vị bộ nhớ là bất biến và chỉ được ghi một lần (số lượng cây lau nhà là 1). Nhưng ngay cả khi chúng tôi nhận thấy lỗ hổng tiềm ẩn này trong quá trình kiểm toán, chúng tôi vẫn không thể dễ dàng xác nhận hoặc loại trừ mọi tình huống có thể xảy ra nếu không có lý do chính thức nghiêm ngặt về tất cả các ràng buộc có liên quan, vì thực sự không có dòng mã nào không chính xác.
Tóm lại, hai lỗ hổng này lần lượt thuộc về các loại "lỗ hổng mã" và "lỗ hổng thiết kế". Các lỗ hổng mã tương đối đơn giản, dễ phát hiện hơn (mã bị lỗi) và dễ lý luận và xác nhận hơn. Các lỗ hổng thiết kế có thể rất tinh vi, khó phát hiện hơn (không có mã "lỗi") và khó lý luận và xác nhận hơn.
Dựa trên kinh nghiệm của chúng tôi kiểm toán và xác minh chính thức zkVM và các chuỗi ZK và không phải ZK khác, đây là một số gợi ý về cách bảo vệ tốt nhất các hệ thống ZK:
Như đã đề cập trước đó, cả mã và thiết kế của ZK đều có thể chứa lỗ hổng. Cả hai loại lỗ hổng đều có khả năng ảnh hưởng đến tính toàn vẹn của hệ thống ZK, vì vậy chúng phải được giải quyết trước khi hệ thống được đưa vào hoạt động. Một vấn đề với các hệ thống ZK, so với các hệ thống không phải ZK, là bất kỳ cuộc tấn công nào cũng khó phát hiện và phân tích hơn vì chi tiết tính toán của chúng không có sẵn công khai hoặc được lưu giữ trên chuỗi. Kết quả là, mọi người có thể biết rằng một cuộc tấn công của hacker đã xảy ra, nhưng họ có thể không biết các chi tiết kỹ thuật về cách nó xảy ra. Điều này làm cho chi phí của bất kỳ lỗ hổng ZK nào rất cao. Do đó, giá trị của việc đảm bảo an ninh cho các hệ thống ZK trước cũng rất cao.
Hai lỗ hổng mà chúng ta thảo luận ở đây đã được phát hiện thông qua kiểm toán và xác minh chính thức. Một số người có thể cho rằng xác minh chính thức một mình loại bỏ nhu cầu kiểm toán vì tất cả các lỗ hổng sẽ được phát hiện thông qua xác minh chính thức. Tuy nhiên, khuyến nghị của chúng tôi là thực hiện cả hai. Như đã giải thích ở đầu bài viết này, công việc xác minh chính thức chất lượng cao bắt đầu bằng việc xem xét kỹ lưỡng, kiểm tra và lý luận không chính thức về mã và thiết kế, trùng lặp với kiểm toán. Ngoài ra, việc tìm kiếm và giải quyết các lỗ hổng đơn giản hơn trong quá trình kiểm toán có thể đơn giản hóa và hợp lý hóa quy trình xác minh chính thức.
Nếu bạn đang tiến hành cả kiểm toán và xác minh chính thức cho hệ thống ZK, cách tiếp cận tối ưu là thực hiện cả hai cùng một lúc. Điều này cho phép kiểm toán viên và kỹ sư xác minh chính thức cộng tác hiệu quả, có khả năng phát hiện ra nhiều lỗ hổng hơn vì đầu vào kiểm toán chất lượng cao được yêu cầu để xác minh chính thức.
Nếu dự án ZK của bạn đã trải qua kiểm toán (kudos) hoặc nhiều cuộc kiểm toán (danh tiếng lớn), đề xuất của chúng tôi là thực hiện xác minh chính thức trên mạch dựa trên kết quả kiểm toán trước đó. Kinh nghiệm của chúng tôi với kiểm toán và xác minh chính thức trong các dự án như zkVM và các dự án khác, cả ZK và không phải ZK, đã nhiều lần chỉ ra rằng xác minh chính thức thường nắm bắt các lỗ hổng bị bỏ lỡ trong quá trình kiểm toán. Với bản chất của ZKP, trong khi các hệ thống ZK sẽ cung cấp khả năng mở rộng và bảo mật blockchain tốt hơn so với các giải pháp không phải ZK, tính quan trọng của tính bảo mật và tính đúng đắn của chúng cao hơn nhiều so với các hệ thống không phải ZK truyền thống. Do đó, giá trị của xác minh chính thức chất lượng cao cho các hệ thống ZK vượt xa các hệ thống không phải ZK.
Các ứng dụng ZK thường bao gồm hai thành phần: mạch và hợp đồng thông minh. Đối với các ứng dụng dựa trên zkVM, có các mạch zkVM phổ quát và các ứng dụng hợp đồng thông minh. Đối với các ứng dụng không dựa trên zkVM, có các mạch ZK dành riêng cho ứng dụng cùng với các hợp đồng thông minh tương ứng được triển khai trên chuỗi L1 hoặc ở đầu kia của cầu.
Các nỗ lực kiểm toán và xác minh chính thức của chúng tôi cho zkWasm chỉ liên quan đến mạch zkWasm. Tuy nhiên, từ góc độ bảo mật tổng thể cho các ứng dụng ZK, điều quan trọng là phải kiểm toán và xác minh chính thức các hợp đồng thông minh của họ. Rốt cuộc, sẽ thật đáng tiếc nếu, sau khi đầu tư nỗ lực đáng kể vào việc đảm bảo an ninh mạch, sự lỏng lẻo trong việc giám sát hợp đồng thông minh dẫn đến thỏa hiệp ứng dụng.
Hai loại hợp đồng thông minh đáng được chú ý đặc biệt. Loại đầu tiên trực tiếp xử lý các bằng chứng ZK. Mặc dù chúng có thể không có quy mô lớn, nhưng nguy cơ của chúng đặc biệt cao. Loại thứ hai bao gồm các hợp đồng thông minh quy mô trung bình đến lớn chạy trên zkVM. Chúng tôi biết rằng các hợp đồng này đôi khi có thể rất phức tạp và những hợp đồng có giá trị nhất phải trải qua kiểm toán và xác minh, đặc biệt là vì chi tiết thực hiện của chúng không hiển thị trên chuỗi. May mắn thay, sau nhiều năm phát triển, xác minh chính thức cho các hợp đồng thông minh hiện đang thực tế và chuẩn bị cho các mục tiêu có giá trị cao phù hợp.
Hãy tóm tắt tác động của xác minh chính thức (FV) đối với các hệ thống ZK và các thành phần của chúng với các điểm sau.
Bài viết này được sao chép từ [panewslab], bản quyền thuộc về tác giả gốc [CertiK], nếu bạn có bất kỳ phản đối nào về việc tái bản, vui lòng liên hệ với đội ngũ Gate Learn và nhóm sẽ xử lý trong thời gian sớm nhất theo các thủ tục liên quan.
Tuyên bố từ chối trách nhiệm: Các quan điểm và ý kiến được thể hiện trong bài viết này chỉ đại diện cho quan điểm cá nhân của tác giả và không cấu thành bất kỳ lời khuyên đầu tư nào.
Các phiên bản ngôn ngữ khác của bài viết được dịch bởi nhóm Gate Learn và không được đề cập trong Gate.io, bài viết đã dịch không được sao chép, phân phối hoặc đạo văn.
Trong một bài viết trước, chúng tôi đã thảo luận về việc xác minh chính thức nâng cao các hệ thống Bằng chứng không có kiến thức (ZKP): cách xác minh lệnh ZK. Bằng cách chính thức xác minh từng lệnh zkWasm, chúng tôi hoàn toàn có thể đảm bảo tính bảo mật kỹ thuật và tính chính xác của toàn bộ mạch zkWasm. Trong bài viết này, chúng tôi sẽ tập trung vào quan điểm phát hiện lỗ hổng, phân tích các lỗ hổng cụ thể được xác định trong quá trình kiểm toán và xác minh và các bài học kinh nghiệm từ chúng. Để có một cuộc thảo luận chung về xác minh chính thức nâng cao của các blockchain ZKP, vui lòng tham khảo bài viết về xác minh chính thức nâng cao của các blockchain ZKP.
Trước khi thảo luận về các lỗ hổng ZK, trước tiên chúng ta hãy hiểu cách CertiK thực hiện xác minh chính thức ZK. Đối với các hệ thống phức tạp như Máy ảo ZK (zkVM), bước đầu tiên trong xác minh chính thức (FV) là xác định rõ ràng những gì cần được xác minh và các thuộc tính của nó. Điều này đòi hỏi phải xem xét toàn diện về thiết kế, triển khai mã và thiết lập thử nghiệm của hệ thống ZK. Quá trình này trùng lặp với kiểm toán thường xuyên nhưng khác ở chỗ, sau khi xem xét, các mục tiêu và tài sản xác minh phải được thiết lập. Tại CertiK, chúng tôi gọi đây là xác minh theo định hướng kiểm toán. Công việc kiểm toán và xác minh thường được tích hợp. Đối với zkWasm, chúng tôi đã tiến hành cả kiểm toán và xác minh chính thức đồng thời.
Tính năng cốt lõi của các hệ thống Zero-Knowledge Proof (ZKP) là chúng cho phép chuyển một bằng chứng được mã hóa ngắn gọn về các tính toán được thực hiện ngoại tuyến hoặc riêng tư (chẳng hạn như giao dịch blockchain) sang trình xác minh ZKP. Người xác minh kiểm tra và xác nhận bằng chứng để đảm bảo rằng việc tính toán xảy ra như đã tuyên bố. Trong bối cảnh này, lỗ hổng ZK sẽ cho phép tin tặc gửi bằng chứng ZK giả mạo cho các giao dịch sai và được trình xác minh ZKP chấp nhận.
Đối với tục ngữ zkVM, quy trình chứng minh ZK liên quan đến việc chạy một chương trình, tạo bản ghi thực thi cho mỗi bước và chuyển đổi các bản ghi thực thi này thành một tập hợp các bảng số (một quá trình được gọi là "số học"). Các số này phải thỏa mãn một tập hợp các ràng buộc ("mạch"), bao gồm các mối quan hệ giữa các ô bảng cụ thể, hằng số cố định, ràng buộc tra cứu cơ sở dữ liệu giữa các bảng và phương trình đa thức (hoặc "cổng") mà mỗi cặp hàng bảng liền kề phải thỏa mãn. Xác minh trên chuỗi có thể xác nhận sự tồn tại của một bảng đáp ứng tất cả các ràng buộc mà không tiết lộ các con số cụ thể trong bảng.
Bảng số học tính bằng zkWasm
Độ chính xác của từng ràng buộc là rất quan trọng. Bất kỳ lỗi nào trong một ràng buộc, cho dù nó quá yếu hay thiếu, đều có thể cho phép tin tặc gửi bằng chứng gây hiểu lầm. Các bảng này có thể đại diện cho việc thực hiện hợp lệ hợp đồng thông minh nhưng thực tế thì không. So với các máy ảo truyền thống, độ mờ đục của các giao dịch zkVM khuếch đại các lỗ hổng này. Trong các chuỗi không phải ZK, các chi tiết tính toán giao dịch được ghi lại công khai trên blockchain; tuy nhiên, zkVM không lưu trữ các chi tiết này trên chuỗi. Sự thiếu minh bạch này gây khó khăn cho việc xác định chi tiết cụ thể của một cuộc tấn công hoặc ngay cả khi một cuộc tấn công đã xảy ra.
Mạch ZK thực thi các quy tắc thực thi của các lệnh zkVM cực kỳ phức tạp. Đối với zkWasm, việc triển khai mạch ZK của nó liên quan đến hơn 6.000 dòng mã Rust và hàng trăm ràng buộc. Sự phức tạp này thường có nghĩa là có thể có nhiều lỗ hổng đang chờ được phát hiện.
Kiến trúc mạch zkWasm
Thật vậy, thông qua việc kiểm toán và xác minh chính thức zkWasm, chúng tôi đã phát hiện ra một số lỗ hổng như vậy. Dưới đây, chúng tôi sẽ thảo luận về hai ví dụ đại diện và kiểm tra sự khác biệt giữa chúng.
Lỗ hổng đầu tiên liên quan đến lệnh Load8 trong zkWasm. Trong zkWasm, việc đọc bộ nhớ heap được thực hiện bằng cách sử dụng một tập hợp các lệnh LoadN, trong đó N là kích thước của dữ liệu sẽ được tải. Ví dụ: Load64 nên đọc dữ liệu 64 bit từ địa chỉ bộ nhớ zkWasm. Load8 nên đọc dữ liệu 8 bit (tức là một byte) từ bộ nhớ và đệm nó bằng số không để tạo giá trị 64 bit. Bên trong, zkWasm đại diện cho bộ nhớ dưới dạng một mảng gồm các byte 64 bit, vì vậy nó cần "chọn" một phần của mảng bộ nhớ. Điều này được thực hiện bằng cách sử dụng bốn biến trung gian (u16_cells), cùng nhau tạo thành giá trị tải 64 bit hoàn chỉnh.
Các ràng buộc đối với các lệnh LoadN này được định nghĩa như sau:
Ràng buộc này được chia thành ba trường hợp: Load32, Load16 và Load8. Load64 không có ràng buộc vì các đơn vị bộ nhớ chính xác là 64 bit. Đối với trường hợp Load32, mã đảm bảo rằng 4 byte cao (32 bit) trong đơn vị bộ nhớ phải bằng không.
Đối với trường hợp Load16, mã đảm bảo rằng 6 byte (48 bit) cao trong đơn vị bộ nhớ phải bằng không.
Đối với trường hợp Load8, nó sẽ yêu cầu 7 byte (56 bit) cao trong đơn vị bộ nhớ bằng không. Thật không may, đây không phải là trường hợp trong mã.
Như bạn có thể thấy, chỉ có 9 đến 16 bit cao bị giới hạn ở mức không. 48 bit cao khác có thể là bất kỳ giá trị nào và vẫn truyền dưới dạng "đọc từ bộ nhớ".
Khai thác lỗ hổng này, tin tặc có thể giả mạo bằng chứng ZK của chuỗi thực thi hợp pháp, khiến lệnh Load8 tải các byte không mong muốn này, dẫn đến hỏng dữ liệu. Hơn nữa, thông qua việc sắp xếp cẩn thận mã và dữ liệu xung quanh, nó có thể kích hoạt việc thực hiện và chuyển sai, dẫn đến đánh cắp dữ liệu và tài sản. Các giao dịch giả mạo này có thể vượt qua kiểm tra của trình kiểm tra zkWasm và được blockchain công nhận không chính xác là giao dịch hợp pháp.
Việc khắc phục lỗ hổng này thực sự khá đơn giản.
Lỗ hổng này đại diện cho một lớp lỗ hổng ZK được gọi là "lỗ hổng mã" vì chúng bắt nguồn từ việc triển khai mã và có thể dễ dàng sửa chữa thông qua các sửa đổi mã cục bộ nhỏ. Như bạn có thể đồng ý, những lỗ hổng này cũng tương đối dễ dàng hơn để mọi người xác định.
Chúng ta hãy xem xét một lỗ hổng khác, lần này liên quan đến việc gọi và trả về zkWasm. Gọi và trả về là các lệnh VM cơ bản cho phép một ngữ cảnh đang chạy (ví dụ: hàm) gọi một ngữ cảnh khác và tiếp tục thực hiện ngữ cảnh gọi sau khi người nhận hoàn tất việc thực thi. Mỗi lời kêu gọi mong đợi một sự trở lại sau này. Dữ liệu động được theo dõi bởi zkWasm trong suốt vòng đời của phép gọi và trả về được gọi là "khung cuộc gọi". Vì zkWasm thực hiện các lệnh tuần tự, tất cả các khung cuộc gọi có thể được sắp xếp dựa trên sự xuất hiện của chúng trong thời gian chạy. Dưới đây là một ví dụ về mã gọi / trả về chạy trên zkWasm.
Người dùng có thể gọi hàm buy_token() để mua token (có thể bằng cách thanh toán hoặc chuyển các vật phẩm có giá trị khác). Một trong những bước cốt lõi của nó là tăng số dư tài khoản token lên 1 bằng cách gọi hàm add_token(). Vì bản thân ZK prover không hỗ trợ cấu trúc dữ liệu khung cuộc gọi, nên Bảng thực thi (E-Table) và Bảng nhảy (J-Table) là cần thiết để ghi lại và theo dõi lịch sử đầy đủ của các khung cuộc gọi này.
Hình trên minh họa quá trình buy_token() gọi add_token() và trả về từ add_token() đến buy_token(). Có thể thấy rằng số dư tài khoản token tăng thêm 1. Trong Bảng Thực thi, mỗi bước thực thi chiếm một hàng, bao gồm số khung cuộc gọi hiện tại đang được thực thi, tên hàm ngữ cảnh hiện tại (chỉ nhằm mục đích minh họa), số lệnh đang chạy hiện tại trong hàm và lệnh hiện tại được lưu trữ trong bảng (chỉ nhằm mục đích minh họa). Trong Jump Table, mỗi khung cuộc gọi chiếm một hàng, lưu trữ số khung người gọi, tên ngữ cảnh hàm người gọi (chỉ nhằm mục đích minh họa) và vị trí lệnh tiếp theo của khung người gọi (để khung có thể trả về). Trong cả hai bảng, có một cột "jops" theo dõi xem lệnh hiện tại có phải là lệnh gọi / trả về hay không (trong Bảng Thực thi) và tổng số lệnh gọi / trả lại cho khung đó (trong Bảng Nhảy).
Như mong đợi, mỗi cuộc gọi nên có một kết quả tương ứng và mỗi khung chỉ nên có một cuộc gọi và một lần trả lời. Như thể hiện trong hình trên, giá trị "jops" cho khung đầu tiên trong Jump Table là 2, tương ứng với hàng đầu tiên và thứ ba trong Bảng thực thi, trong đó giá trị "jops" là 1. Mọi thứ có vẻ bình thường vào lúc này.
Tuy nhiên, có một vấn đề ở đây: trong khi một cuộc gọi và một lần trả về sẽ tăng số lượng "jops" cho khung lên 2, hai cuộc gọi hoặc hai lần trả về cũng sẽ dẫn đến số lượng 2. Có hai cuộc gọi hoặc hai lần trả về trên mỗi khung hình có vẻ vô lý, nhưng điều quan trọng cần nhớ là đây chính xác là những gì một hacker sẽ cố gắng làm bằng cách phá vỡ kỳ vọng.
Bạn có thể cảm thấy phấn khích bây giờ, nhưng chúng tôi đã thực sự tìm thấy vấn đề?
Nó chỉ ra rằng hai cuộc gọi không phải là một vấn đề bởi vì các ràng buộc của Execution Table và Jump Table ngăn hai cuộc gọi được mã hóa vào cùng một hàng của một khung vì mỗi cuộc gọi tạo ra một số khung mới, tức là số khung cuộc gọi hiện tại cộng với 1.
Tuy nhiên, tình huống không may mắn cho hai lần trả lại: vì không có khung mới nào được tạo khi trả về, một hacker thực sự có thể lấy Bảng thực thi và Bảng nhảy của một chuỗi thực thi hợp pháp và tiêm lợi nhuận giả mạo (và các khung tương ứng). Ví dụ: ví dụ về Execution Table và Jump Table trước đó về việc gọi buy_token() add_token() có thể bị hacker giả mạo theo kịch bản sau:
Tin tặc đã tiêm hai trả về giả mạo giữa lệnh gọi ban đầu và trả về trong Bảng thực thi và thêm một hàng khung giả mạo mới trong Bảng nhảy (trả về ban đầu và các bước thực thi lệnh tiếp theo trong Bảng thực thi cần được tăng thêm 4). Vì số lượng "jops" cho mỗi hàng trong Jump Table là 2, các ràng buộc được thỏa mãn và trình kiểm tra bằng chứng zkWasm sẽ chấp nhận "bằng chứng" của chuỗi thực thi giả mạo này. Như đã thấy từ hình, số dư tài khoản mã thông báo tăng gấp 3 lần thay vì 1. Do đó, hacker có thể nhận được 3 mã thông báo với giá 1.
Có nhiều cách khác nhau để giải quyết vấn đề này. Một cách tiếp cận rõ ràng là theo dõi riêng biệt các cuộc gọi và trả lại và đảm bảo rằng mỗi khung có chính xác một cuộc gọi và một lần trả lại.
Bạn có thể nhận thấy rằng cho đến nay chúng tôi đã không hiển thị một dòng mã duy nhất cho lỗ hổng này. Lý do chính là không có dòng mã có vấn đề; Việc triển khai mã hoàn toàn phù hợp với bảng và thiết kế ràng buộc. Vấn đề nằm trong chính thiết kế, và bản sửa lỗi cũng vậy.
Bạn có thể nghĩ rằng lỗ hổng này là rõ ràng, nhưng trong thực tế, nó không phải là. Điều này là do có một khoảng cách giữa "hai cuộc gọi hoặc hai lần trả về cũng sẽ dẫn đến số lượng 'jops' là 2" và "hai lần trả về thực sự có thể." Loại thứ hai đòi hỏi một phân tích chi tiết và toàn diện về các ràng buộc khác nhau trong Bảng thực thi và Bảng nhảy, gây khó khăn cho việc thực hiện lý luận không chính thức hoàn chỉnh.
"Lỗ hổng tiêm dữ liệu Load8" và "Lỗ hổng trả về giả mạo" đều có khả năng cho phép tin tặc thao túng bằng chứng ZK, tạo giao dịch sai, đánh lừa người kiểm tra bằng chứng và tham gia trộm cắp hoặc chiếm quyền điều khiển. Tuy nhiên, bản chất của chúng và cách chúng được phát hiện khá khác nhau.
"Lỗ hổng tiêm dữ liệu Load8" được phát hiện trong quá trình kiểm tra zkWasm. Đây không phải là nhiệm vụ dễ dàng, vì chúng tôi phải xem xét hàng trăm ràng buộc trong hơn 6.000 dòng mã Rust và hàng chục lệnh zkWasm. Mặc dù vậy, lỗ hổng tương đối dễ phát hiện và xác nhận. Vì lỗ hổng này đã được khắc phục trước khi bắt đầu xác minh chính thức, nên nó không gặp phải trong quá trình xác minh. Nếu lỗ hổng này không được phát hiện trong quá trình kiểm tra, chúng tôi có thể mong đợi nó được tìm thấy trong quá trình xác minh hướng dẫn Load8.
"Lỗ hổng trả lại giả mạo" đã được phát hiện trong quá trình xác minh chính thức sau khi kiểm toán. Một lý do khiến chúng tôi không phát hiện ra nó trong quá trình kiểm toán là zkWasm có một cơ chế tương tự như "jops" được gọi là "mops", theo dõi các hướng dẫn truy cập bộ nhớ tương ứng với dữ liệu lịch sử cho từng đơn vị bộ nhớ trong thời gian chạy zkWasm. Các ràng buộc về số lượng cây lau nhà thực sự chính xác vì nó chỉ theo dõi một loại lệnh bộ nhớ, ghi bộ nhớ và dữ liệu lịch sử của mỗi đơn vị bộ nhớ là bất biến và chỉ được ghi một lần (số lượng cây lau nhà là 1). Nhưng ngay cả khi chúng tôi nhận thấy lỗ hổng tiềm ẩn này trong quá trình kiểm toán, chúng tôi vẫn không thể dễ dàng xác nhận hoặc loại trừ mọi tình huống có thể xảy ra nếu không có lý do chính thức nghiêm ngặt về tất cả các ràng buộc có liên quan, vì thực sự không có dòng mã nào không chính xác.
Tóm lại, hai lỗ hổng này lần lượt thuộc về các loại "lỗ hổng mã" và "lỗ hổng thiết kế". Các lỗ hổng mã tương đối đơn giản, dễ phát hiện hơn (mã bị lỗi) và dễ lý luận và xác nhận hơn. Các lỗ hổng thiết kế có thể rất tinh vi, khó phát hiện hơn (không có mã "lỗi") và khó lý luận và xác nhận hơn.
Dựa trên kinh nghiệm của chúng tôi kiểm toán và xác minh chính thức zkVM và các chuỗi ZK và không phải ZK khác, đây là một số gợi ý về cách bảo vệ tốt nhất các hệ thống ZK:
Như đã đề cập trước đó, cả mã và thiết kế của ZK đều có thể chứa lỗ hổng. Cả hai loại lỗ hổng đều có khả năng ảnh hưởng đến tính toàn vẹn của hệ thống ZK, vì vậy chúng phải được giải quyết trước khi hệ thống được đưa vào hoạt động. Một vấn đề với các hệ thống ZK, so với các hệ thống không phải ZK, là bất kỳ cuộc tấn công nào cũng khó phát hiện và phân tích hơn vì chi tiết tính toán của chúng không có sẵn công khai hoặc được lưu giữ trên chuỗi. Kết quả là, mọi người có thể biết rằng một cuộc tấn công của hacker đã xảy ra, nhưng họ có thể không biết các chi tiết kỹ thuật về cách nó xảy ra. Điều này làm cho chi phí của bất kỳ lỗ hổng ZK nào rất cao. Do đó, giá trị của việc đảm bảo an ninh cho các hệ thống ZK trước cũng rất cao.
Hai lỗ hổng mà chúng ta thảo luận ở đây đã được phát hiện thông qua kiểm toán và xác minh chính thức. Một số người có thể cho rằng xác minh chính thức một mình loại bỏ nhu cầu kiểm toán vì tất cả các lỗ hổng sẽ được phát hiện thông qua xác minh chính thức. Tuy nhiên, khuyến nghị của chúng tôi là thực hiện cả hai. Như đã giải thích ở đầu bài viết này, công việc xác minh chính thức chất lượng cao bắt đầu bằng việc xem xét kỹ lưỡng, kiểm tra và lý luận không chính thức về mã và thiết kế, trùng lặp với kiểm toán. Ngoài ra, việc tìm kiếm và giải quyết các lỗ hổng đơn giản hơn trong quá trình kiểm toán có thể đơn giản hóa và hợp lý hóa quy trình xác minh chính thức.
Nếu bạn đang tiến hành cả kiểm toán và xác minh chính thức cho hệ thống ZK, cách tiếp cận tối ưu là thực hiện cả hai cùng một lúc. Điều này cho phép kiểm toán viên và kỹ sư xác minh chính thức cộng tác hiệu quả, có khả năng phát hiện ra nhiều lỗ hổng hơn vì đầu vào kiểm toán chất lượng cao được yêu cầu để xác minh chính thức.
Nếu dự án ZK của bạn đã trải qua kiểm toán (kudos) hoặc nhiều cuộc kiểm toán (danh tiếng lớn), đề xuất của chúng tôi là thực hiện xác minh chính thức trên mạch dựa trên kết quả kiểm toán trước đó. Kinh nghiệm của chúng tôi với kiểm toán và xác minh chính thức trong các dự án như zkVM và các dự án khác, cả ZK và không phải ZK, đã nhiều lần chỉ ra rằng xác minh chính thức thường nắm bắt các lỗ hổng bị bỏ lỡ trong quá trình kiểm toán. Với bản chất của ZKP, trong khi các hệ thống ZK sẽ cung cấp khả năng mở rộng và bảo mật blockchain tốt hơn so với các giải pháp không phải ZK, tính quan trọng của tính bảo mật và tính đúng đắn của chúng cao hơn nhiều so với các hệ thống không phải ZK truyền thống. Do đó, giá trị của xác minh chính thức chất lượng cao cho các hệ thống ZK vượt xa các hệ thống không phải ZK.
Các ứng dụng ZK thường bao gồm hai thành phần: mạch và hợp đồng thông minh. Đối với các ứng dụng dựa trên zkVM, có các mạch zkVM phổ quát và các ứng dụng hợp đồng thông minh. Đối với các ứng dụng không dựa trên zkVM, có các mạch ZK dành riêng cho ứng dụng cùng với các hợp đồng thông minh tương ứng được triển khai trên chuỗi L1 hoặc ở đầu kia của cầu.
Các nỗ lực kiểm toán và xác minh chính thức của chúng tôi cho zkWasm chỉ liên quan đến mạch zkWasm. Tuy nhiên, từ góc độ bảo mật tổng thể cho các ứng dụng ZK, điều quan trọng là phải kiểm toán và xác minh chính thức các hợp đồng thông minh của họ. Rốt cuộc, sẽ thật đáng tiếc nếu, sau khi đầu tư nỗ lực đáng kể vào việc đảm bảo an ninh mạch, sự lỏng lẻo trong việc giám sát hợp đồng thông minh dẫn đến thỏa hiệp ứng dụng.
Hai loại hợp đồng thông minh đáng được chú ý đặc biệt. Loại đầu tiên trực tiếp xử lý các bằng chứng ZK. Mặc dù chúng có thể không có quy mô lớn, nhưng nguy cơ của chúng đặc biệt cao. Loại thứ hai bao gồm các hợp đồng thông minh quy mô trung bình đến lớn chạy trên zkVM. Chúng tôi biết rằng các hợp đồng này đôi khi có thể rất phức tạp và những hợp đồng có giá trị nhất phải trải qua kiểm toán và xác minh, đặc biệt là vì chi tiết thực hiện của chúng không hiển thị trên chuỗi. May mắn thay, sau nhiều năm phát triển, xác minh chính thức cho các hợp đồng thông minh hiện đang thực tế và chuẩn bị cho các mục tiêu có giá trị cao phù hợp.
Hãy tóm tắt tác động của xác minh chính thức (FV) đối với các hệ thống ZK và các thành phần của chúng với các điểm sau.
Bài viết này được sao chép từ [panewslab], bản quyền thuộc về tác giả gốc [CertiK], nếu bạn có bất kỳ phản đối nào về việc tái bản, vui lòng liên hệ với đội ngũ Gate Learn và nhóm sẽ xử lý trong thời gian sớm nhất theo các thủ tục liên quan.
Tuyên bố từ chối trách nhiệm: Các quan điểm và ý kiến được thể hiện trong bài viết này chỉ đại diện cho quan điểm cá nhân của tác giả và không cấu thành bất kỳ lời khuyên đầu tư nào.
Các phiên bản ngôn ngữ khác của bài viết được dịch bởi nhóm Gate Learn và không được đề cập trong Gate.io, bài viết đã dịch không được sao chép, phân phối hoặc đạo văn.