Anchor هو الإطار التطويري الأكثر شيوعًا في نظام Solana البيئي. إنه يقلل من عتبة التطوير بشكل كبير من خلال ميزات مثل التحقق الإعلاني من الحسابات، والتسلسل التلقائي، والفحوصات الأمنية المضمنة. ومع ذلك، بينما يوفر الإطار الراحة، فإنه يُدخل خلف الكواليس بعض الأوامر الداخلية التي قد لا يكون المطورون على دراية بها في كل برنامج. يمكن للمهاجمين استغلال هذه "الأوامر المخفية" تحت ظروف معينة، مما يؤدي إلى خسائر مالية جسيمة.
ستكشف فرقة أمان Beosin عن نمط ثغرة حرجة: في الإصدارات القديمة من Anchor، عندما يستخدم المطورون AccountInfo للإعلان عن حسابات PDA المملوكة للبرنامج، يمكن للمهاجمين الاستيلاء على تحكم الحساب وتفريغ جميع SOL فيه من خلال تعليمات IDL التي تُحقنها Anchor تلقائيًا، وذلك بخطوتين فقط دون الحاجة إلى أي صلاحيات خاصة.
أولاً، تحليل تعليمات IDL والآليات ذات الصلة
1.1 أمر IDL
يقوم Anchor تلقائيًا بحقن مجموعة من تعليمات IDL (Interface Definition Language) في كل برنامج، ما لم يتم تمكين ميزة no-idl صراحةً أثناء البناء. تشمل هذه التعليمات:
IdlCreateAccount: إنشاء حساب IDL على السلسلة
IdlWrite: كتابة البيانات إلى حساب IDL / حساب التخزين المؤقت
IdlSetAuthority: تغيير سلطة حساب IDL
IdlCloseAccount: إغلاق حساب IDL وتحويل جميع lamports الخاصة به إلى الطرف المستلم المحدد
IdlResizeAccount: ضبط حجم حساب IDL
IdlCreateBuffer: إنشاء حساب مخزن IDL (IDL Buffer)
IdlSetBuffer: تغطية حساب IDL الرسمي ببيانات حساب الذاكرة المؤقتة
تم تصميم هذه الأوامر في الأصل لإدارة IDL على السلسلة، لكنها تمتلك قدرات خاصة على الحسابات التي يملكها البرنامج (قراءة البيانات وكتابتها، وتغيير المالك، وإغلاق الحساب ونقل lamports) — وهذا بالضبط هو جوهر الثغرة. لا يحتاج المهاجم إلى استدعاء أي أمر أعمال مكتوب من قبل المطور، بل يمكنه استدعاء هذه الأوامر المضمنة مباشرة.
1.2 حساب مخزن IDL
حساب مخزن IDL (IDL Buffer) هو حساب مؤقت أدخله Anchor لتحميل بيانات IDL الكبيرة على مراحل. نظرًا لأن IDL الكامل (المضغوط بتنسيق JSON) قد يتجاوز حد حجم المعاملة الواحدة، يسمح Anchor بإنشاء مخزن أولًا عبر IdlCreateBuffer، ثم كتابة البيانات على دفعات باستخدام عدة أوامر IdlWrite، وأخيرًا تقديمها دفعة واحدة عبر IdlSetBuffer.
النقطة الرئيسية هي هيكل البيانات لحساب IDL / حساب التخزين المؤقت: يبدأ برأس بتنسيق ثابت، يحتوي الرأس على حقل authority: Pubkey (يُشار إليه في نتائج الاختبار هذه باسم controller). تستخدم تعليمات IDL هذا الحقل لتحديد "من لديه الصلاحية للتعامل مع هذا الحساب".
لكن المشكلة تكمن في أن منطق معالجة IdlCreateBuffer في الإصدارات الأقدم من Anchor يُهَيِّئ أي حساب يُمرَّر ويملكه البرنامج كحساب مخزن، ويُعيّن السلطة مباشرةً إلى الموقع الذي وقّع المعاملة. بمعنى آخر، ما دام مالك أي حساب هو البرنامج نفسه (مثل صندوق PDA للبرنامج)، يمكن للمهاجم أن يُسجّل نفسه كمُتحكم فيه، ثم يستخدم IdlCloseAccount لنقل جميع SOL الموجودة في الحساب بشكل قانوني.
1.3 شروط تفعيل الثغرة
يحتاج تشغيل هذه الثغرة إلى استيفاء الشروط التالية في نفس الوقت:
- استخدام إصدار قديم من Anchor: تعليمات IDL غير الكافية في التحقق من الحسابات تسمح بمعالجة الحسابات التجارية كحسابات IDL عن طريق الخطأ.
- no-idl غير مُفعّل: حافظ البرنامج على نقطة الدخول المُحقنة افتراضيًا للـ IDL أثناء البناء، مما يُعرّض سطح الهجوم للخارج.
- الإعلان عن PDA الذي يملكه البرنامج باستخدام AccountInfo: يستخدم المطورون AccountInfo العاري لحمل حسابات الأموال (مثل PDA للخزنة)، مفتقدًا المميزات المضمنة في حسابات Anchor المُصنَّفة (مثل Account) مثل مُميّز discriminator / المالك، مما يجعل هذا الحساب غير قابل للتمييز عن حسابات IDL من منظور تعليمات IDL.
- الحساب مملوك للبرنامج ويهتم بـ lamports: owner == هذا البرنامج هو شرط لكي تتمكن تعليمات IDL من التعامل معه؛ فقط عند امتلاك SOL يكون له قيمة للتفريغ.
بعد استيفاء الشروط أعلاه، يكفي المهاجم بمعاملتين عاديتين: أولاً IdlCreateBuffer للاستيلاء على controller، ثم IdlCloseAccount لنقل جميع SOL، ليكمل الهجوم دون الحاجة إلى منح أي صلاحيات من قبل البرنامج المستهدف.
1.4 سلسلة الهجوم
بالإضافة إلى التنفيذ الداخلي لـ Anchor، يتم تفكيك كيفية قيام المهاجم باستخدام أمرين مدمجين فقط لخداع صندوق أموال عادي ليبدو كحساب IDL وتفريغه.
نظرة عامة على عملية الهجوم
الخطوة 1: IdlCreateBuffer(treasury, signer = attacker)
└─ treasury.controller ==> attacker (يصبح المُتحكم)
الخطوة 2: IdlCloseAccount(treasury, authority = attacker, dest = attacker)
└─ treasury.lamports ==> attacker (تفريغ الخزينة)
الخطوة الأولى: IdlCreateBuffer تستولي على الصلاحية
يتم تنفيذ Anchor داخليًا على النحو التالي:
#[derive(Accounts)]pub struct IdlCreateBuffer {
#[account(zero)] // ← Key: an account with its discriminator all 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; // set attack as authority Ok(())}
يعني #[account(zero)]: قبول حساب يحتوي على discriminator مكون بالكامل من الأصفار ومملوك من قبل البرنامج، كحساب IDL غير مُهَيَّأ لتهيئته. و vault يلبي هذين الشرطين بالضبط في نفس الوقت:
حالة الصندوق
الشروط
مملوك من قبل البرنامج
بعد init_if_needed، يُعيَّن المالك إلى هذا البرنامج
المميز تمامًا صفر
استخدام نوع AccountInfo، لا يكتب Anchor discriminator، البيانات كلها صفر
وبعد ذلك يُمرر المهاجم vault إلى IdlCreateBuffer:
- يكتب Anchor 8 بايتات من مُميّز IdlAccount قبل vault؛
- اكتب المفتاح العام للمهاجم في حقل authority.
هكذا، تم تحويل الخزنة إلى IdlAccount تُعدّ المهاجم سلطةً لها — بينما بقيت وحدات SOL داخلها دون تغيير.
الخطوة الثانية: IdlCloseAccount —— تفريغ الأموال
#[derive(Accounts)]pub struct IdlCloseAccount { #[account(mut, has_one = authority)] // ← check authority == signer pub account: Account, pub authority: Signer, #[account(mut)] pub destination: AccountInfo, // ← attack wallet}
تم التحقق من الصندوق بالكامل الآن:
- يتطابق discriminator مع IdlAccount
- حقل authority = المفتاح العام للمهاجم
- has_one = authority تم التحقق من الصلاحية
لذلك، تم تحويل جميع lamports (جميع SOL التي أودعها المستخدم) بشكل قانوني إلى حساب المهاجم، وأصبح الصندوق صفراً.
السبب الجذري:
استخدام AccountInfo في deposit.rs بدلاً من حساب Anchor المُحدَّد بنوعه هو الجذر القاتل لهذا الثغرة:
(1) يتم كتابة مُميّز 8 بايتات مخصص لهذا الهيكل عند تهيئة الحسابات من نوع معين (مثل Account)؛
(2) بمجرد كتابة الـ discriminator الخاص به، لا يمكن لـ Anchor أن يعامله كـ IdlAccount (عدم تطابق الـ discriminator)، وسيفشل الخطوة الأولى IdlCreateBuffer، مما يقطع سلسلة الهجوم.
ثانيًا: تحليل الحالة
تم إجراء هذا الاختبار على عقد جسر متعدد السلاسل تم بناؤه باستخدام Anchor 0.31.0. يحاكي الاختبار خزنة جسر حقيقية (Bridge Treasury)، حيث يكون مالكها هو برنامج الجسر نفسه، وتحتوي على 1.001281 SOL مودعة من قبل المستخدمين. يمتلك محفظة المهاجم في البداية 2 SOL ولا يمتلك أي صلاحيات.
مشروع
القيمة / الوصف
برنامج الجسر (Bridge Program)
CJutNlr8d3oxdb11ReOLaZd5jPsqUxgwt2mv5e7equtE
حساب الخزينة (Treasury)
DxGkzaMhP5Wy824GhoehErdD6MudfuEPGPwaV2s77FsM
مالك الخزنة
البرنامج المملوك للعقد (program-owned PDA)
الرصيد الأولي للخزينة
1.001281 SOL (إيداع أموال المستخدم)
المُحكم الأولي
0x0000...0000 (جميع الأصفار، غير مضبوط)
الرصيد الأولي للمحفظة المهاجمة
2.000000 SOL
الصلاحيات المطلوبة
لا شيء (لا حاجة لأي إذن)
عدد المعاملات
2 صفقة
الرصيد النهائي للخزينة
0.000000 SOL (تم تفريغه)
المهاجمون يحققون أرباحًا
+1.001281 SOL
لقطة شاشة لتشغيل PoC (tests/poc-idl-hijack.ts):
يمكن ملاحظة أن IdlCreateBuffer في الخطوة 1 يعيّن المهاجم كمُتحكم في الخزنة (الرصيد غير متغير)؛ بينما يقوم IdlCloseAccount في الخطوة 2 بنقل كامل مبلغ 1.001281 SOL من الخزنة إلى محفظة المهاجم، ليصبح رصيد الخزنة صفرًا.
اقتراحات الإصلاح/الحماية:
(1) تحديث إصدار Anchor: تم إصلاح هذه المشكلة في أحدث إصدار (ستقوم تعليمات IDL بتمييز حسابات IDL عن حسابات الأعمال بدقة)، وأبسط وأعمق وسيلة للدفاع هي تجنب استخدام إصدارات Anchor الأقدم.
(2) تمكين no-idl أثناء البناء: تعطيل صريح لحقن تعليمات IDL في برامج الإنتاج، مما يزيل نقطة الهجوم هذه من جذورها.
(3) استخدم حسابات مُصنَّفة بدلاً من AccountInfo العارية: استخدم حسابات مُحمَّلة بـ discriminator وتحقق من المالك، مثل Account / SystemAccount، لضمان عدم سوء تفسيرها من قبل تعليمات IDL.
(4) تقليل الحسابات القابلة للتصفية التي يملكها البرنامج: أضف قيودًا واضحة على المالك / البذور / المميّز لـ PDA المخزنة للأموال، وتحقق من مميّز الحساب في الأوامر الأساسية.
خاتمة
يتمثل هذا الثغرة جوهرًا في مزيج من "إخفاء الأوامر الإطارية + غياب التحقق من نوع الحساب". يجب على المطورين الإدراك أن أوامر IDL التي يتم إدخالها افتراضيًا من قبل Anchor تمثل سطح هجوم حقيقي، ويمكن منع مخاطر تفريغ الأموال غير المصرح بها بفعالية من خلال ترقية إصدار الإطار واستخدام قيود من نوع قوي على حسابات الأموال.
Beosin هي شركة رائدة في مجال أمن البلوكشين والامتثال التنظيمي، وتتخصّص في مراجعة أمان العقود الذكية قبل إطلاق المشروع، ومراقبة ومنع مخاطر الأمان أثناء تشغيل المشروع، واسترداد الأصول المسروقة، ومنع غسل الأموال (AML) على الأصول الافتراضية، والتحقيق والتتبع. وقد قدّمت Beosin منتجات وخدمات متكاملة للامتثال البلوكشيني لـ 200 مزوّد خدمة للأصول الافتراضية، و4500 مشروع Web3، بالإضافة إلى هيئات تنظيمية وإنفاذية في أكثر من 20 دولة ومنطقة حول العالم. نرحب بمراسلتنا من خلال مربع الرسائل في قناتنا.

