Hook
Một dòng code. Chỉ một dòng require thiếu dấu kiểm tra số dư trong hàm verifySignature — và đó là điểm kết thúc của 320 triệu USD. Tháng 2 năm 2022, cầu nối Wormhole trên Solana bị khai thác triệt để. Kẻ tấn công đúc được 120.000 wETH (Wormhole-wrapped ETH) mà không cần nạp collateral. Lỗ hổng không nằm ở mật mã elliptic curve hay cơ chế đồng thuận, mà ở một điều kiện logic đơn giản: chữ ký được xác thực nhưng người ký không được kiểm tra đã thực sự khóa tài sản hay chưa. Đó là bài học cũ nhưng luôn mới: bảo mật trong cross-chain không phải là bài toán mã hóa, mà là bài toán trạng thái. Và đây là lần đầu tiên tôi, sau 5 năm audit blockchain, thấy một lỗ hổng cross-chain lại đơn giản đến mức làm xấu hổ cả một hệ sinh thái.
Context
Wormhole là một cầu nối cross-chain (bridge) cho phép chuyển tài sản giữa Solana và các blockchain khác (Ethereum, BSC, Polygon…). Cơ chế hoạt động cơ bản: người dùng gửi tài sản vào hợp đồng thông minh trên chain nguồn; một nhóm validator (gọi là Guardian) quan sát sự kiện gửi và ký một chứng nhận (VAA - Verifiable Action Approval); sau đó VAA được gửi đến chain đích, nơi hợp đồng kiểm tra chữ ký của các Guardian và mint token tương ứng. Tại thời điểm bị tấn công, Wormhole có 19 Guardian, yêu cầu ít nhất 13 chữ ký hợp lệ để một VAA được chấp nhận. Hợp đồng thông minh Solana của Wormhole được viết bằng Rust và sử dụng thư viện secp256k1 để xác thực chữ ký ECDSA. Tuy nhiên, hàm xác thực chữ ký chỉ kiểm tra tính hợp lệ của chữ ký với một khóa công khai, nhưng không kiểm tra xem khóa công khai đó có thuộc về Guardian đã được phép hay không. Trên thực tế, hợp đồng có lưu trữ danh sách Guardian hiện tại, nhưng trong quá trình xử lý VAA, nó quên mất việc so sánh khóa ký với danh sách đó. Đó là lỗi logic: việc xác thực chữ ký được tách rời khỏi việc xác thực danh tính người ký.
Core (60-70% nội dung)
Tôi sẽ đi vào mã nguồn. Hợp đồng Wormhole Solana có một hàm verify_signatures trong module signature_set. Hàm này nhận đầu vào là một tập hợp các chữ ký (dạng bytes) và một message (hash của VAA). Nó gọi thư viện solana_program::secp256k1_recover để recover khóa công khai từ mỗi chữ ký, sau đó so sánh recovered key với một danh sách các public key được lưu trong tài khoản guardian_set. Đây là đoạn pseudocode: