Mỗi lần kiểm toán, tôi đều đặt câu hỏi: liệu có ai đó đang cố tình chèn lỗi vào code không? Không phải lỗi vô tình, mà là backdoor có chủ đích. Tuần trước, một tin đồn lan truyền trong cộng đồng DeFi: một nhà phát triển cốt lõi của giao thức ZK-Rollup tên 'Arbitron' bị cáo buộc đã khai thác lỗ hổng trong hợp đồng cầu nối để rút trộm 2 triệu USDC. Tôi không tin vào tin đồn, tôi tin vào bằng chứng. Tôi đã tự tay audit code của Arbitron từ tháng 6 năm ngoái, và phát hiện ra một điều ngược đời: lỗ hổng đó không chỉ là sơ suất, mà là một cánh cửa được thiết kế tinh vi để chỉ có người nắm private key mới mở được. Bài viết này sẽ phân tích sự kiện dưới góc nhìn kỹ thuật và địa chính trị blockchain, để thấy rõ rằng những vụ tấn công nội bộ đang trở thành vũ khí trong cuộc chiến giữa các hệ sinh thái.
Bối cảnh giao thức và sự kiện
Arbitron là một giao thức Layer 2 sử dụng công nghệ zero-knowledge proof để tăng tốc giao dịch trên Ethereum. Nó có cầu nối riêng để chuyển tài sản giữa L1 và L2. Vào ngày 15 tháng 3, một người dùng báo cáo rằng số dư USDC của họ trên Arbitron bỗng nhiên giảm 500.000 đơn vị. Điều tra sơ bộ cho thấy các giao dịch rút tiền được thực hiện từ một địa chỉ có liên quan đến nhà phát triển chính, người đã rời dự án từ tháng 2. Nhà phát triển này phủ nhận, nhưng cộng đồng đã chia rẽ. Tôi đã có quyền truy cập vào mã nguồn của cầu nối trước khi nó được triển khai, và tôi nhận thấy một điều kỳ lạ: hàm withdraw() có một tham số ẩn cho phép bỏ qua kiểm tra chữ ký nếu người gọi cung cấp một giá trị hash đặc biệt. Giá trị hash đó được tạo ra từ private key của nhà phát triển. Đây không phải lỗi vô tình, đó là backdoor.
Phân tích kỹ thuật chuyên sâu
Tôi sẽ đi vào chi tiết. Hợp đồng cầu nối được viết bằng Solidity, phiên bản 0.8.17. Hàm withdraw có cấu trúc như sau:
function withdraw(bytes memory _data, bytes32 _secret) public {
require(verifySignature(_data, _secret) || _secret == keccak256(abi.encodePacked(ownerPrivateKey)), "Invalid secret");
} ```
Dòng thứ hai là điểm mấu chốt. verifySignature là hàm kiểm tra chữ ký hợp lệ từ người dùng. Nhưng nếu _secret bằng với giá trị băm của ownerPrivateKey, thì điều kiện || sẽ bypass kiểm tra chữ ký. ownerPrivateKey là một biến private trong hợp đồng, được set một lần khi triển khai. Nhưng ai cũng biết rằng private key của nhà phát triển không thể lưu trên chain. Vậy làm sao để tính keccak256(abi.encodePacked(ownerPrivateKey))? Nó là hằng số, được hardcode trong bytecode. Tôi đã decompile bytecode và tìm thấy giá trị đó: 0xabc123.... Bất kỳ ai biết giá trị này đều có thể gọi withdraw mà không cần chữ ký. Nhà phát triển có thể đã cố tình đặt nó ở đó để dễ dàng rút tiền sau này.
Điểm mù bảo mật: Góc nhìn phản trực giác
Điều ngược đời là: cộng đồng thường tin rằng các giao thức mã nguồn mở là an toàn vì ai cũng có thể audit. Nhưng ở đây, backdoor được giấu trong một điều kiện logic tưởng chừng vô hại. Hầu hết auditor chỉ kiểm tra verifySignature mà không để ý đến nhánh ||. Đây là điểm mù: các lỗ hổng bảo mật thường nằm ở những chỗ mà người ta cho là 'hiển nhiên'. Tôi không tin vào việc chỉ dựa vào audit mã nguồn, tôi tin vào việc kiểm tra bytecode và so sánh với source code. Sự khác biệt giữa source và bytecode mới là nơi ẩn náu thực sự.
Dự báo lỗ hổng và bài học
Sự kiện này cho thấy một xu hướng đáng lo ngại: các nhà phát triển cốt lõi ngày càng tinh vi hơn trong việc chèn backdoor. Họ không chỉ tạo lỗi, họ tạo ra những cánh cửa bí mật chỉ dành cho riêng mình. Điều này đặt ra câu hỏi: liệu chúng ta có thể tin tưởng vào bất kỳ giao thức nào mà không có cơ chế kiểm toán độc lập cấp bytecode? Câu trả lời là không. Tôi dự đoán rằng trong năm tới, sẽ có ít nhất 3 vụ tấn công tương tự bị phát hiện, và chúng sẽ làm rung chuyển niềm tin vào Layer 2. Hãy nhớ: code là luật, nhưng kẻ viết luật có thể đã giấu một điều khoản riêng.