Bối cảnh

Vào ngày 18 tháng 6 năm 2026, hợp đồng RollupProcessor của giao thức riêng tư Layer2 Ethereum Aztec Connect đã bị tấn công. Kẻ tấn công đã lợi dụng cơ chế Escape Hatch (cánh cửa thoát hiểm) khi mà lớp Solidity thiếu quy định độc lập về quyền sở hữu và giới hạn rút tiền, tạo nên một lỗ hổng trong niềm tin. Họ đã nộp chứng minh escape hatch được TurboVerifier chấp nhận, trực tiếp rút 1,158 ETH (khoảng 2,06 triệu đô la) từ số dư hợp đồng, và thông qua cùng một cơ chế đã đánh cắp 150,000 DAI và 0.47 renBTC, tổng thiệt hại khoảng 2,22 triệu đô la.

Tổng quan về cuộc tấn công

Giao dịch tấn công:

Nguyên nhân gốc rễ của lỗ hổng

Thiếu ranh giới tin cậy của escapeHatch()

Hàm public entry của RollupProcessor, escapeHatch(), chỉ kiểm tra rằng trạng thái escape hatch có đang bật hay không, rồi trực tiếp đi vào processRollupProof(), không thực hiện bất kỳ kiểm tra quyền hạn nào đối với người gọi:

Ngược lại, đường xử lý rollup thông thường processRollup() sẽ kiểm tra ủy quyền của rollupProviders[provider] và xác minh chữ ký provider signature. Nhưng trong cuộc tấn công này, kẻ tấn công gọi escapeHatch(), truyền signatures = 0x, viewingKeys = 0x, hoàn toàn bỏ qua quy trình ủy quyền của provider.

verifyProofAndUpdateState() tin mù vào Verifier

Sau khi vào processRollupProof(), hợp đồng gọi TurboVerifier.verify(proofData, 0):

Bản thân TurboVerifier hoạt động đúng—nó thực hiện xác minh toán học của proof Plonk. Vấn đề thực sự nằm ở việc RollupProcessor đồng nhất việc xác minh toán học thành tính hợp lệ nghiệp vụ, không thực hiện thêm một lớp kiểm tra độc lập ở tầng Solidity.

processDepositsAndWithdrawals() thực hiện rút tiền không điều kiện

Logic thực thi cốt lõi sau khi Verifier trả về thành công:

Hàm chuyển tiền cuối cùng:

Mạch zkSNARK thiếu cổng ràng buộc đẳng thức (nguyên nhân gốc rễ)

Trong mạch proof join-split của Aztec, old_data_root được chia thành hai nhánh độc lập sử dụng:

  • Nhân chứng nội bộ A: truyền vào mạch join-split con, dùng để kiểm tra tính thành viên Merkle của ghi chú riêng

  • Đầu vào công khai B: được lộ ra như một đầu vào công khai cho lớp Solidity, để đối chiếu validateMerkleRoots() với dataRoot trên chuỗi

Trong mạch thiếu ràng buộc A == B, dẫn đến cả hai có thể được gán giá trị độc lập:

  • A có thể là root Merkle giả mạo do kẻ tấn công tạo (cây chứa các ghi chú có số tiền bất kỳ)

  • B có thể là dataRoot thật trên chuỗi (để kiểm tra của Solidity vượt qua)

Do hệ thống ràng buộc cho phép A ≠ B, toàn bộ proof zkSNARK vẫn hợp lệ.

Quy trình tấn công

Giai đoạn chuẩn bị: giả mạo cây Merkle và proof

Kẻ tấn công xây dựng tại chỗ một cây data Merkle giả mạo, chèn vào đó một ghi chú riêng:

  • Giá trị: 1158 ETH (hợp đồng lúc đó nắm giữ khoảng 1158.7598 ETH)

  • Chủ sở hữu: kẻ tấn công nắm giữ khóa riêng tương ứng

Kẻ tấn công tạo proof zkSNARK:

  • Nhân chứng nội bộ A (old_data_root) = root của cây giả mạo

  • Đầu vào công khai B (old_data_root) = dataRoot thực tế trên chuỗi = 0x184bea7d9493cd9a5efb6b679d04066a8c92a34ac8ec150e9635133c6010977b

Giai đoạn thứ nhất: tạo giao dịch tấn công

Kẻ tấn công EOA 0x6952d9246e9aFE8B887B2877225163436F78E97F trực tiếp thực hiện lệnh gọi cấp cao nhất tới RollupProcessor (0x737901bea3eeb88459df9ef1be8ff3ae1b42a2ba):

  • Chữ ký hàm: escapeHatch(bytes,bytes,bytes)

  • msg.value = 0

  • signatures = 0x

  • viewingKeys = 0x

proofData do kẻ tấn công tạo ra bao gồm các public inputs quan trọng:

Giai đoạn thứ hai: vượt qua kiểm tra quyền hạn

escapeHatch() chỉ kiểm tra rằng getEscapeHatchStatus() trả về true, sau đó trực tiếp gọi processRollupProof(). Đường dẫn này không thực hiện kiểm tra ủy quyền thông thường trong processRollup() đối với rollupProviders[provider], cũng không xác minh chữ ký provider. Không có bất kỳ ràng buộc nào giữa danh tính người gọi và bên nhận khoản rút tiền.

Giai đoạn thứ ba: Verifier xác minh toán học

RollupProcessor thực hiện STATICCALL tới TurboVerifier (0x48cb7ba00d087541dc8e2b3738f80fdd1fee8ce8):

Vì rollup_size = 0, TurboVerifier chọn EscapeHatchVk (vk.num_inputs = 26) thông qua VerificationKeys.getKeyById(0) để xác minh toán học proof Plonk. Verifier chỉ kiểm tra sự nhất quán toán học giữa proof và public inputs, không đọc trạng thái của RollupProcessor, không kiểm tra việc quyền sở hữu số dư.

Trong giao dịch này, lệnh gọi của verifier trả về thành công, cho thấy proof escape hatch mà kẻ tấn công gửi là đúng về mặt toán học.

Giai đoạn thứ tư: thực hiện rút tiền

Sau khi Verifier trả về thành công, RollupProcessor cập nhật trạng thái rollup:

Sau đó vào processDepositsAndWithdrawals(); ở chế độ escape hatch, khi rollupSize == 0 vẫn xử lý theo 1 inner tx. Sau khi phân tích proofData, phát hiện:

Gọi trực tiếp withdraw(1158 ETH, địa chỉ kẻ tấn công, 0), chuyển tiền ra thông qua receiverAddress.call{value: 1158 ETH}("")

Giai đoạn thứ năm: tấn công theo cùng một kiểu đối với DAI và renBTC

Kẻ tấn công sử dụng hoàn toàn cùng một đường dẫn lỗ hổng trong cùng thời điểm, lần lượt xây dựng proof escape hatch cho tài sản ERC20 (assetId ≠ 0) để đánh cắp 150,000 DAI và 0.47 renBTC từ RollupProcessor. Trong hàm withdraw(), nhánh assetId ≠ 0 thực thi chuyển tiền bằng transfer() của ERC20 để hoàn tất việc chuyển.

Lợi ích ròng từ ba vụ tấn công:

Theo dõi dòng tiền

Phân tích kẻ tấn công EOA 0x6952d9246e9aFE8B887B2877225163436F78E97F thông qua hệ thống theo dõi chống rửa tiền MistTrack của SlowMist:

Nguồn tiền: Gas ban đầu dùng để tạo địa chỉ của kẻ tấn công đến từ 0x963737c550e70ffe4d59464542a28604edb2ef9a (địa chỉ thực thể unionchain.ai). Địa chỉ này hoạt động lần đầu vào lúc 02:21:11 UTC ngày 18/06/2026, đúng vào thời điểm bắt đầu hành động tấn công, và là một địa chỉ dùng một lần được tạo riêng cho cuộc tấn công này.

Nơi tiền đã đi: phân bổ hiện tại của tiền bị đánh cắp như sau:

  • Token 802 ETH vẫn còn trong địa chỉ chính của kẻ tấn công 0x6952d9246e9aFE8B887B2877225163436F78E97F; 150,000 DAI và 0.47 renBTC cũng chưa được chuyển đi

  • 300 ETH đã được chuyển đến 0x15930a0fef3421f48c6553b5691682cc1b22edb3 (MistTrack gắn nhãn là địa chỉ độc hại liên quan Aztec Exploiter)

  • Khoảng 56 ETH đã được chuyển đến một địa chỉ độc hại khác (cũng được gắn nhãn là liên quan Aztec Exploiter)

  • Điểm rủi ro AML của 0x33d6a0d9bc210e823e043d604179cd844eb467df là 100/100 (Severe), cũng liên quan tới sự kiện Aztec Exploiter

Tóm tắt

Bài học cốt lõi từ cuộc tấn công này là: "toán học đúng" của mạch zkSNARK ≠ "an toàn nghiệp vụ". Bộ xác minh chỉ có thể chứng minh rằng "bạn biết một nhân chứng thỏa mãn ràng buộc", và nếu bản thân ràng buộc là sai (hoặc bị thiếu), hệ thống proof sẽ trở thành một cỗ máy tạo giả mạo hợp lệ. Nhóm an ninh SlowMist khuyến nghị dự án thực hiện kiểm toán chuyên sâu cho mọi đường dẫn ràng buộc của zero-knowledge proof để đảm bảo an toàn cho mạch.

Bài viết này được nhóm tình báo mối đe dọa SlowMist viết dựa trên hệ thống tình báo MistEye, nền tảng theo dõi MistTrack và phân tích do SlowMist Agent AI điều khiển; nếu có bất kỳ vấn đề gì, hãy liên hệ để phản hồi.