ثغرة Reentrancy: كيفية التعرف عليها والاستفادة منها والوقاية منها

في عالم العقود الذكية، يُعتبر reentrancy أحد أخطر الثغرات الأمنية. ستساعدك هذه المقالة على فهم ما هو هجوم reentrancy، بالإضافة إلى طرق فعالة لمواجهته. من التقنيات الأساسية إلى الحلول المتقدمة، سنستكشف سبل حماية مشروعك بالكامل.

كيف يعمل reentrancy: آلية الهجوم الأساسية

لفهم reentrancy، أولاً نحتاج إلى فهم المفهوم الأساسي: يمكن لعقد ذكي أن يستدعي عقدًا آخر، وفي تلك الحالة، يمكن للعقد الثاني أن يستدعي العقد الأول مرة أخرى أثناء تنفيذه.

تخيل أن لديك عقدين: ContractA يحتوي على 10 إيثريوم وContractB أرسل إليه 1 إيثريوم. عندما يستدعي ContractB وظيفة السحب، يتم التحقق من الرصيد. إذا كان كافياً، يتم إرسال الإيثريوم مرة أخرى إلى ContractB. هنا، إذا لم تكن هناك إجراءات حماية مناسبة، فإن هذه اللحظة تمثل نقطة ضعف يمكن للمهاجم استغلالها.

في هجوم reentrancy النموذجي، يحتاج المهاجم إلى دالتين: attack() لبدء الهجوم، وfallback() لتنفيذ الاستدعاء العكسي. تعتبر دالة fallback خاصة في Solidity — لا اسم لها، ولا تتلقى معلمات، وتُستدعى تلقائيًا عند إرسال إيثريوم إلى العقد بدون بيانات.

خطوات تنفيذ هجوم reentrancy

تابع عملية الهجوم خطوة بخطوة. يبدأ المهاجم باستدعاء دالة attack() من عقده. داخل هذه الدالة، يستدعي دالة withdraw() من ContractA.

عند استلام ContractA لهذا الاستدعاء، يتحقق مما إذا كان ContractB لديه رصيد أكبر من 0. إذا كان هناك 1 إيثريوم، يتجاوز الاختبار. عندها، يرسل ContractA إيثريوم إلى ContractB، مما يُفعّل دالة fallback الخاصة به. في هذه اللحظة، يبقى ContractA برصيد 9 إيثريوم، لكن الأهم من ذلك، أن رصيد ContractB في سجل ContractA لم يُحدّث إلى 0 بعد.

هذه مشكلة: دالة fallback تستدعي withdraw() من ContractA مرة أخرى. ContractA يتحقق من رصيد ContractB — لا يزال 1 إيثريوم! لماذا؟ لأن السطر balance[msg.sender] = 0 لم يُنفذ بعد، لأنه يقع بعد إرسال الإيثريوم.

تتكرر هذه العملية: استدعاء withdraw → التحقق من الرصيد (لا يزال > 0) → إرسال الإيثريوم → تفعيل fallback → استدعاء withdraw مرة أخرى… حتى يتم سحب كل إيثريوم من ContractA.

تحليل الكود: عندما يتحول reentrancy إلى واقع

عقد EtherStore هو مثال نموذجي على عقد سهل الاختراق. يحتوي على دالة deposit() لتخزين الرصيد، ودالة withdrawAll() للسحب. المشكلة تكمن في طريقة تنفيذ withdrawAll(): يتحقق من الشرط، يرسل الإيثريوم، ثم يُحدّث الرصيد.

عقد Attack يستغل هذا الثغرة. في منشئه، يمرر عنوان EtherStore، ليتمكن من استدعاء وظائفه. دالة fallback في عقد Attack تُستدعى كلما أرسل Ether إلى EtherStore، وتستمر في استدعاء withdrawAll() طالما هناك إيثريوم. تبدأ عملية الهجوم بإرسال 1 إيثريوم أولاً إلى EtherStore لتجاوز الاختبار الأولي.

النتيجة: يتم سحب كامل أموال EtherStore في عملية واحدة.

ثلاث استراتيجيات لحماية العقود من reentrancy

للحماية، هناك ثلاث مستويات من التدابير، من الأساسي إلى الشامل.

نموذج noReentrant: الحل الأساسي للحماية

أسهل طريقة هي استخدام معدل noReentrant(). المعدل هو نوع خاص من الدوال في Solidity يسمح لك بتعديل سلوك دوال أخرى دون إعادة كتابتها بالكامل.

الفكرة بسيطة: عند حماية دالة بواسطة noReentrant()، يتم قفل العقد أثناء التنفيذ. أي استدعاء يحاول إعادة استدعاء هذه الدالة سيفشل لأن متغير الحالة الخاص بالقفل يمنعه. فقط بعد انتهاء الدالة وفتح القفل، يمكن استدعاؤها مرة أخرى.

هذه الطريقة فعالة لحماية وظيفة واحدة، لكنها لا تتعامل مع حالات أكثر تعقيدًا.

نموذج Check-Effect-Interaction: منع reentrancy متعدد الوظائف

الأسلوب الثاني أقوى: تطبيق نمط Check-Effect-Interaction. بدلاً من حماية وظيفة واحدة، يغير هذا النمط طريقة كتابة المنطق.

المبدأ هو: التحقق من الشرط أولاً (Check)، تحديث الحالة مباشرة بعد ذلك (Effect)، ثم التفاعل مع العقود الخارجية (Interaction). هذا يمنع المهاجم من الاستغلال، لأنه عند استدعائه مرة أخرى، يكون الرصيد قد تم تحديثه إلى 0.

بدلاً من تحديث balance[msg.sender] = 0 بعد إرسال الإيثريوم، تنقلها إلى قبل ذلك مباشرة. عندها، حتى لو استدعى fallback() مرة أخرى، فإن التحقق سيفشل لأن الرصيد أصبح 0.

هذه الطريقة تحمي العقد من reentrancy بشكل دائم، حتى مع وجود وظائف سحب متعددة.

GlobalReentrancyGuard: حماية شاملة للمشروع

بالنسبة للمشاريع المعقدة التي تتضمن العديد من العقود المتفاعلة، نحتاج إلى حل شامل أكثر: GlobalReentrancyGuard.

بدلاً من قفل كل وظيفة على حدة، يُنشئ عقد مستقل يخزن متغير حالة القفل العام، وتُشير إليه جميع العقود الأخرى في المشروع.

تخيل السيناريو: المهاجم يستدعي وظيفة في عقد ScheduledTransfer. بعد اجتياز الفحوصات، يرسل إيثريوم إلى عقد AttackTransfer. يتم تفعيل دالة fallback في AttackTransfer وتحاول استدعاء ScheduledTransfer مرة أخرى. لكن، بسبب أن GlobalReentrancyGuard قفل الحالة العامة، يتم منع هذا الاستدعاء على الفور.

هذه الطريقة مفيدة جدًا للمشاريع الكبيرة التي تتضمن العديد من العقود، حيث يمكن أن يحدث reentrancy بين عقود مختلفة.

اختيار التقنية المناسبة لمشروعك

اختيار الاستراتيجية يعتمد على تعقيد مشروعك. إذا كان عقدك يتضمن وظائف تفاعل قليلة، فإن noReentrant() كافية. إذا كانت هناك العديد من وظائف السحب، فإن نمط Check-Effect-Interaction هو الخيار الأمثل. للمشاريع الكبيرة التي تتضمن العديد من العقود، يوفر GlobalReentrancyGuard حماية شاملة.

مهما كانت الطريقة التي تختارها، المهم أن تفهم كيف يعمل reentrancy، حتى تتمكن من التعرف عليه ومواجهته بشكل استباقي.

للحصول على تحديثات يومية حول أمان العقود الذكية، مراجعة الكود المصدري، وأحدث الاتجاهات في مجال Web3، تابع الموارد المتخصصة في أمان Solidity.

شاهد النسخة الأصلية
قد تحتوي هذه الصفحة على محتوى من جهات خارجية، يتم تقديمه لأغراض إعلامية فقط (وليس كإقرارات/ضمانات)، ولا ينبغي اعتباره موافقة على آرائه من قبل Gate، ولا بمثابة نصيحة مالية أو مهنية. انظر إلى إخلاء المسؤولية للحصول على التفاصيل.
  • أعجبني
  • تعليق
  • إعادة النشر
  • مشاركة
تعليق
إضافة تعليق
إضافة تعليق
لا توجد تعليقات
  • Gate Fun الساخن

    عرض المزيد
  • القيمة السوقية:$0.1عدد الحائزين:1
    0.00%
  • القيمة السوقية:$2.46Kعدد الحائزين:1
    0.00%
  • القيمة السوقية:$2.45Kعدد الحائزين:1
    0.00%
  • القيمة السوقية:$0.1عدد الحائزين:1
    0.00%
  • القيمة السوقية:$2.45Kعدد الحائزين:1
    0.00%
  • تثبيت