Beosin tiết lộ lỗ hổng lệnh IDL nghiêm trọng trong Framework Solana Anchor phiên bản cũ

iconMetaEra
Chia sẻ
AI summary iconTóm tắt
Đội ngũ bảo mật của Beosin đã phát hiện lỗ hổng nghiêm trọng trong hướng dẫn IDL của các framework Solana Anchor phiên bản cũ. Lỗ hổng này cho phép kẻ tấn công chiếm quyền kiểm soát các tài khoản PDA thuộc về chương trình bằng cách khai thác các khai báo AccountInfo, cho phép đánh cắp tiền theo hai bước mà không cần quyền người dùng. Sự cố này nhấn mạnh nhu cầu về một khung tuân thủ mạnh mẽ hơn trong phát triển hợp đồng thông minh. Với MiCA (Quy định Thị trường Tài sản Tiền mã hóa của EU) sắp tới, các lỗ hổng như vậy làm nổi bật tầm quan trọng của việc kiểm toán bảo mật theo thời gian thực và tuân thủ các tiêu chuẩn quy định.

Anchor là khung phát triển phổ biến nhất trong hệ sinh thái Solana. Nhờ các tính năng như xác thực tài khoản theo cách khai báo, tự động chuỗi hóa và kiểm tra bảo mật tích hợp, nó đã làm giảm đáng kể rào cản phát triển. Tuy nhiên, trong khi mang lại sự tiện lợi, khung này cũng âm thầm chèn vào mỗi chương trình một số lệnh nội bộ mà nhà phát triển có thể không biết đến. Những lệnh “ẩn” này có thể bị kẻ tấn công khai thác trong các điều kiện cụ thể, dẫn đến tổn thất vốn nghiêm trọng.

Đội ngũ an ninh Beosin sẽ tiết lộ một mẫu lỗ hổng quan trọng: trong các phiên bản Anchor cũ, khi nhà phát triển sử dụng AccountInfo để khai báo tài khoản PDA do chương trình sở hữu, kẻ tấn công có thể chỉ cần hai bước thao tác để chiếm quyền kiểm soát tài khoản và trống hết tất cả SOL thông qua lệnh IDL được Anchor tự động chèn, toàn bộ quá trình không yêu cầu bất kỳ đặc quyền nào.

I. Phân tích lệnh IDL và các cơ chế liên quan

1.1 Lệnh IDL

Anchor sẽ tự động chèn một bộ lệnh quản lý IDL (Interface Definition Language) vào mỗi chương trình, trừ khi tính năng no-idl được kích hoạt rõ ràng trong quá trình xây dựng. Các lệnh này bao gồm:

IdlCreateAccount:Tạo một tài khoản IDL trên chuỗi

IdlWrite: Ghi dữ liệu vào tài khoản IDL / tài khoản bộ đệm

IdlSetAuthority: Thay đổi authority của tài khoản IDL

IdlCloseAccount:关闭 IDL tài khoản và chuyển toàn bộ lamports đến người nhận được chỉ định

IdlResizeAccount: Điều chỉnh kích thước tài khoản IDL

IdlCreateBuffer: Tạo tài khoản bộ đệm IDL (IDL Buffer)

IdlSetBuffer:Sử dụng dữ liệu từ tài khoản bộ đệm để ghi đè tài khoản IDL chính thức

Các lệnh này được thiết kế ban đầu để quản lý IDL trên chuỗi, nhưng chúng có khả năng thực hiện các thao tác đặc biệt đối với các tài khoản do chương trình sở hữu (đọc ghi dữ liệu, thay đổi người kiểm soát, đóng và chuyển lamports) — đây chính là lỗ hổng cốt lõi. Kẻ tấn công không cần gọi bất kỳ lệnh nghiệp vụ nào do nhà phát triển viết, mà chỉ cần gọi trực tiếp các lệnh tích hợp này.

1.2 Tài khoản đệm IDL

Tài khoản đệm IDL (IDL Buffer) là tài khoản tạm thời được Anchor giới thiệu để tải lên dữ liệu IDL lớn theo từng phần. Vì IDL đầy đủ (đã nén dưới dạng JSON) có thể vượt quá giới hạn kích thước của một giao dịch đơn lẻ, Anchor cho phép tạo một bộ đệm thông qua IdlCreateBuffer, sau đó ghi từng phần bằng nhiều lệnh IdlWrite, và cuối cùng gửi toàn bộ dữ liệu một lần thông qua IdlSetBuffer.

Điểm then chốt nằm ở cấu trúc dữ liệu của tài khoản IDL / tài khoản bộ đệm: nó bắt đầu bằng một phần đầu có bố cục cố định, trong đó chứa trường authority: Pubkey (được gọi là controller trong đầu ra kiểm tra của bài viết này). Lệnh IDL sử dụng trường này để xác định “ai có quyền thao tác tài khoản này”.

Vấn đề nằm ở chỗ: logic xử lý IdlCreateBuffer trong phiên bản Anchor cũ sẽ khởi tạo bất kỳ tài khoản nào được truyền vào, mà chương trình sở hữu, như một tài khoản bộ đệm và thiết lập authority trực tiếp là người ký giao dịch. Nói cách khác, chỉ cần một tài khoản có owner là chương trình này (ví dụ: PDA kho của chương trình), kẻ tấn công có thể tự ghi mình làm controller của nó, sau đó sử dụng IdlCloseAccount để hợp pháp chuyển toàn bộ SOL trong tài khoản ra ngoài.

1.3 Điều kiện kích hoạt lỗ hổng

Để kích hoạt lỗ hổng này, cần đồng thời đáp ứng các điều kiện sau:

  • Sử dụng Anchor phiên bản cũ: Lệnh IDL không kiểm tra tài khoản đầy đủ, cho phép nhầm tài khoản nghiệp vụ với tài khoản IDL.
  • no-idl chưa được kích hoạt: chương trình giữ lại lối vào lệnh IDL được chèn mặc định khi xây dựng, làm lộ diện bề mặt tấn công.
  • Khai báo PDA mà chương trình sở hữu bằng AccountInfo: Các nhà phát triển sử dụng AccountInfo thô để chứa tài khoản vốn (ví dụ: PDA kho), nhưng thiếu các bộ phân biệt / chủ sở hữu do tài khoản được định kiểu bởi Anchor (như Account) cung cấp, khiến tài khoản này không thể phân biệt với tài khoản IDL theo lệnh IDL.
  • Tài khoản thuộc về chương trình và sở hữu lamports: owner == chương trình này là điều kiện tiên quyết để lệnh IDL có thể thao tác nó; chỉ khi sở hữu SOL thì mới có giá trị để làm rỗng.

Sau khi đáp ứng các điều kiện trên, kẻ tấn công chỉ cần hai giao dịch thông thường: trước tiên dùng IdlCreateBuffer để chiếm quyền kiểm soát, sau đó dùng IdlCloseAccount để chuyển đi toàn bộ SOL, hoàn thành cuộc tấn công mà không cần chương trình mục tiêu cấp bất kỳ quyền nào.

1.4 Chuỗi tấn công

Dưới đây, kết hợp với thực hiện nội bộ của Anchor, chúng ta sẽ từng bước phân tích cách kẻ tấn công chỉ sử dụng hai lệnh tích hợp để “giả mạo” một vault vốn thông thường thành tài khoản IDL và xóa sạch nó.

Tổng quan quy trình tấn công

Bước 1: IdlCreateBuffer(treasury, signer = attacker)    
└─ treasury.controller ==> attacker (trở thành người kiểm soát)  
Bước 2: IdlCloseAccount(treasury, authority = attacker, dest = attacker)  
  └─ treasury.lamports ==> attacker (hút hết số dư trong kho bạc)

Bước đầu tiên: IdlCreateBuffer chiếm quyền authority

Anchor thực hiện nội bộ gần như sau:

#[derive(Accounts)]pub struct IdlCreateBuffer {  
#[account(zero)]          // ← Key: một tài khoản với discriminator của nó là tất cả 0  pub buffer: Account,  
pub authority: Signer,}    
pub fn idl_create_buffer(ctx: Context) -> Result 
{ 
 let idl = &mut ctx.accounts.buffer;  idl.authority = *ctx.accounts.authority.key; // thiết lập attack làm authority  Ok(())}

Ý nghĩa của #[account(zero)] là: chấp nhận một tài khoản có discriminator toàn bộ bằng không và do chương trình sở hữu, để khởi tạo như một tài khoản IDL chưa được khởi tạo. Và vault đúng là đồng thời đáp ứng cả hai điều kiện này:

Trạng thái của vault

Điều kiện

được sở hữu bởi chương trình

Sau init_if_needed, owner được đặt thành chương trình này

discriminator toàn bộ bằng không

Sử dụng loại AccountInfo, Anchor không ghi discriminator, dữ liệu đều bằng không

Sau đó, kẻ tấn công truyền vault vào IdlCreateBuffer:

  • Anchor ghi discriminator của IdlAccount vào 8 byte đầu tiên trước khi đưa vào vault;
  • Ghi khóa công khai của kẻ tấn công vào trường authority.

至此,vault đã bị “giả mạo” thành một IdlAccount có authority là kẻ tấn công—mà bên trong nó, số SOL vẫn nguyên vẹn.

Bước thứ hai: IdlCloseAccount —— Làm trống số dư

#[derive(Accounts)] pub struct IdlCloseAccount {  #[account(mut, has_one = authority)]  // ← kiểm tra authority == signer  pub account: Account,  pub authority: Signer,  #[account(mut)]  pub destination: AccountInfo,  // ← ví tấn công}

Vault lúc này đã được xác minh hoàn toàn:

  • discriminator khớp với IdlAccount
  • Trường authority = khóa công khai của kẻ tấn công
  • has_one = xác thực quyền hạn thành công

Do đó, tất cả lamports trong vault (toàn bộ SOL mà người dùng gửi vào) đã được chuyển hợp pháp vào tài khoản của kẻ tấn công, khiến vault về 0.

Nguyên nhân gốc rễ:

Việc sử dụng AccountInfo trong deposit.rs thay vì Anchor Account có kiểu là nguyên nhân chết người của lỗ hổng này:

(1) Các tài khoản có kiểu (ví dụ: Account) sẽ ghi vào 8 byte discriminator đặc trưng cho cấu trúc này khi được khởi tạo;

(2) Sau khi đã ghi discriminator của riêng mình, Anchor sẽ không còn có thể coi nó là IdlAccount (discriminator không khớp), do đó bước đầu tiên IdlCreateBuffer sẽ thất bại và chuỗi tấn công bị phá vỡ.

Hai, Phân tích trường hợp

Các bài kiểm tra này được xây dựng dựa trên hợp đồng PoC cầu liên chuỗi với Anchor 0.31.0. Bài kiểm tra mô phỏng một kho cầu (Bridge Treasury) thực tế, trong đó chủ sở hữu là chính chương trình cầu, chứa 1.001281 SOL do người dùng gửi vào. Ví của kẻ tấn công ban đầu sở hữu 2 SOL và không có bất kỳ đặc quyền nào.

Dự án

Giá trị / Mô tả

Chương trình cầu nối (Bridge Program)

CJutNlr8d3oxdb11ReOLaZd5jPsqUxgwt2mv5e7equtE

Tài khoản Kho bạc

DxGkzaMhP5Wy824GhoehErdD6MudfuEPGPwaV2s77FsM

Chủ sở hữu KuCoin

Chương trình sở hữu PDA

Số dư ban đầu của ví

1.001281 SOL (người dùng nạp tiền)

Bộ điều khiển ban đầu

0x0000...0000 (tất cả đều là số không, chưa được thiết lập)

Số dư ban đầu của ví kẻ tấn công

2.000000 SOL

Các đặc quyền cần thiết

KHÔNG CẦN GIẤY PHÉP

Số lượng giao dịch

2 giao dịch

Số dư cuối cùng của KuCoin

0.000000 SOL (đã được thanh lý)

Attacker profits

+1.001281 SOL

Ảnh chụp màn hình chạy PoC (tests/poc-idl-hijack.ts):

https://wdcdn.qpic.cn/MTMxMDI3MDE1MTgxMDU0NzA_812435_a37-yrU_dY02i1_I_1785900257?w=1080&h=1287

Có thể thấy, IdlCreateBuffer ở Bước 1 đã đặt người tấn công làm controller của kho (số dư không đổi); IdlCloseAccount ở Bước 2 đã chuyển toàn bộ 1.001281 SOL từ kho vào ví của người tấn công, khiến số dư kho về 0.

Gợi ý sửa chữa/bảo vệ:

(1) Cập nhật phiên bản Anchor: Phiên bản mới nhất đã sửa lỗi này (lệnh IDL sẽ phân biệt rõ ràng giữa tài khoản IDL và tài khoản nghiệp vụ), việc tránh sử dụng phiên bản Anchor cũ là biện pháp phòng thủ trực tiếp và cơ bản nhất.

(2) Kích hoạt no-idl khi xây dựng: Tắt rõ ràng việc tiêm lệnh IDL cho chương trình môi trường sản xuất, loại bỏ bề mặt tấn công này từ nguồn.

(3) Sử dụng tài khoản được định kiểu thay vì AccountInfo thô: Sử dụng tài khoản tiền tệ được đóng gói với Account / SystemAccount và các kiểm tra discriminator cùng owner để ngăn các lệnh IDL nhận nhầm.

(4) Tối thiểu hóa các tài khoản có thể bị xóa sạch thuộc về chương trình: Thêm các ràng buộc rõ ràng về owner / seeds / discriminator cho PDA lưu trữ vốn và kiểm tra discriminator của tài khoản trong các lệnh quan trọng.

Kết luận

Lỗ hổng này bản chất là sự kết hợp giữa “lệnh ẩn trong khung + thiếu phân biệt loại tài khoản”. Các nhà phát triển cần nhận thức rằng các lệnh IDL được Anchor chèn mặc định là bề mặt tấn công thực tế; việc nâng cấp phiên bản khung và áp dụng ràng buộc kiểu mạnh cho tài khoản vốn sẽ hiệu quả ngăn chặn rủi ro rút tiền không được ủy quyền này.

Beosin là một công ty công nghệ hàng đầu về an toàn blockchain và tuân thủ quy định, chuyên về kiểm toán an toàn hợp đồng thông minh trước khi dự án ra mắt, giám sát và ngăn chặn rủi ro an toàn trong quá trình vận hành dự án, truy tìm tài sản bị đánh cắp, chống rửa tiền (AML) cho tài sản ảo và điều tra truy vết. Beosin đã cung cấp các sản phẩm tuân thủ blockchain và dịch vụ an toàn “một cửa” cho hơn 200 nhà cung cấp tài sản ảo, cùng hơn 4.500 dự án Web3 tại hơn 20 quốc gia và vùng lãnh thổ trên toàn cầu. Vui lòng nhấn vào khung tin nhắn trên公众号 để liên hệ với chúng tôi.

Tuyên bố miễn trừ trách nhiệm: Thông tin trên trang này có thể được lấy từ bên thứ ba và không nhất thiết phản ánh quan điểm hoặc ý kiến của KuCoin. Nội dung này chỉ được cung cấp cho mục đích thông tin chung, không có bất kỳ đại diện hay bảo đảm nào dưới bất kỳ hình thức nào và cũng không được hiểu là lời khuyên tài chính hay đầu tư. KuCoin sẽ không chịu trách nhiệm về bất kỳ sai sót hoặc thiếu sót nào hoặc về bất kỳ kết quả nào phát sinh từ việc sử dụng thông tin này. Việc đầu tư vào tài sản kỹ thuật số có thể tiềm ẩn nhiều rủi ro. Vui lòng đánh giá cẩn thận rủi ro của sản phẩm và khả năng chấp nhận rủi ro của bạn dựa trên hoàn cảnh tài chính của chính bạn. Để biết thêm thông tin, vui lòng tham khảo Điều khoản sử dụngTiết lộ rủi ro của chúng tôi.