المؤلف: شياوباي

المحرر: ليز

نظرة عامة على الخلفية

في نظام Ethereum البيئي، توفر آلية التوليد الحتمية لعناوين العقود الراحة للمطورين، ولكنها تقدم أيضًا أسطح هجوم جديدة. في هذا العدد، سنقوم بتحليل استراتيجيات الهجوم والدفاع لنشر عقود مختلفة على نفس العنوان في أوقات مختلفة باستخدام أكواد التشغيل CREATE وCREATE2. للاطلاع على المقالات التمهيدية السابقة حول تدقيق أمان العقود الذكية، راجع المجموعة.

المتطلبات الأساسية

دعونا أولاً نفهم القاعدتين لتوليد عناوين Ethereum:

1. إنشاء

CREATE هو الكود الأصلي في Ethereum Virtual Machine (EVM) لنشر العقود الذكية بشكل ديناميكي. منذ إنشاء Ethereum Genesis، اعتمدت جميع عمليات نشر العقود على هذه الآلية. الميزة الأساسية هي أن إنشاء العنوان يعتمد على nonce لحساب المستخدم، وبالتالي فإن العنوان غير حتمي (لا يمكن التنبؤ به بدقة قبل النشر).

يتحدد عنوان العقد الذي ينشئه CREATE بواسطة عنوان المُنشئ وnonce الخاصة به:

2. CREATE2

CREATE2 هو opcode جديد لإنشاء العقود قدمته هارد فورك Byzantium؟ في Constantinople (فبراير 2019) ويعرف رسميًا بـ EIP-1014. بخلاف CREATE التقليدي، يمكن للمشاركين خارج السلسلة (off-chain) حساب عنوان العقد مسبقًا قبل نشر العقد on-chain، ما يجعل التفاعل خارج السلسلة (مثل قنوات الحالة) وبنى العقود المعقدة ممكنة。

يتم تحديد عنوان العقد المُنشأ بواسطة CREATE2 بواسطة المعلمات الأربع التالية:

حتى هنا، أتمنى أن تكون قد فهمت طريقتي إنشاء عناوين العقود في إيثيريوم. قد يتساءل القارئ الدقيق: ماذا لو كان عنوان العقد المحسوب موجودًا بالفعل؟ لا داعي للقلق؛ ففي إيثيريوم، سواءً تم إنشاء العنوان عبر CREATE أو CREATE2، طالما أن العنوان موجود بالفعل على السلسلة (سواء كان حسابًا خارجيًا EOA أو حسابًا عقديًا)، سيرفض EVM طلب إنشاء عقد. فيما يلي حالتا تعارض العناوين:

1. العنوان الهدف هو حساب خارجي (EOA)

  • القاعدة: إذا كان العنوان الهدف هو EOA موجود بالفعل (مثل عنوان محفظة المستخدم)، فسيقوم EVM برفض طلب نشر العقد。

  • النتيجة: يفشل تنفيذ المعاملة، ويتم استهلاك الغاز، ولن يتم إنشاء العقد، كما لن يتم الكتابة فوق أي بيانات موجودة في ذلك العنوان。

2. العنوان الهدف هو حساب عقدي

  • القاعدة: إذا كان العنوان الهدف هو حساب عقدي تم نشره بالفعل، فـ EVM يرفض أيضًا طلب النشر。

  • النتيجة: يفشل تنفيذ المعاملة، ويتم استهلاك الغاز، وتبقى شفرة العقد الأصلي وبيانات التخزين دون تغيير。

بالطبع، توجد استثناءات: إذا كان العنوان الهدف عقدًا وقد تم تدميره ذاتيًا عبر selfdestruct، فحينها يمكن إعادة نشر عقد جديد على هذا العنوان。

حتى الآن اكتمل شرح مختلف خصائص عمليتي CREATE وCREATE2. بعد ذلك، لنتعرف على كيفية استغلال خصائص هاتين العمليتين لتنفيذ مجموعة من الخطوات المتتابعة (ضربة مركبة) من أجل تنفيذ هجوم على عقد ما。

مثال على الثغرة

تحليل الثغرة

ينفذ عقد DAO آلية حوكمة أساسية: يقوم الـ Owner عبر دالة approve بمراجعة وتسجيل عنوان عقد الـ Proposal داخل مصفوفة Proposals. ويمكن لأي مستخدم لاحقًا استدعاء دالة execute لتنفيذ الـ Proposal الذي تمّت مراجعته. يبدو التحكم الصلاحيّاتي محكمًا (فقط الـ Owner يمكنه اعتماد الـ Proposal) مع فحوص التنفيذ (يجب أن يكون الـ Proposal معتمدًا ولم يتم تنفيذه بعد)، لكن توجد ثغرة منطقية خفية: قد يشير عنوان الـ Proposal المُعتمد أثناء التنفيذ إلى كود عقد مختلف تمامًا. يمكن تنفيذ الهجوم عبر الخطوات التالية بثلاثة مستويات:

1. نشر عقد عادي للحصول على التفويض

يقوم المهاجم أولاً بنشر عقد A يحتوي على دالة executeProposal() بريئة، ثم تتم إضافة عنوانه إلى قائمة الـ proposals عبر موافقة الـ Owner。

2. تدمير العقد الأصلي ذاتيًا لاحتكار العنوان

يقوم العقد A بتنفيذ عملية التدمير الذاتي (selfdestruct) لمسح الكود؛ ثم يستخدم المهاجم opcode CREATE2 لنشر عقد خبيث B على نفس العنوان (يتضمن منطقًا خطيرًا لـ executeProposal())。

3. تشغيل التنفيذ لاغتصاب السيطرة

عندما يستدعي المستخدم execute، يقوم العقد عبر delegatecall بتنفيذ كود العقد الخبيث B الذي تم نشره. وبما أن delegatecall يحتفظ بسياق العقد الحالي، يمكن للمهاجم عبر هذا الإجراء العبث بحالة عقد DAO (مثل تعديل Owner) أو تحويل الأصول。

لنبدأ بالربط مع عقد الهجوم لفهم خطوات الهجوم بالتفصيل。

عقد الهجوم

يتم شرح سير عملية الهجوم كما يلي:

1. قام Alice بنشر عقد DAO。

2. يقوم Evil بنشر عقد DeployerDeployer، ويكون العنوان DD。

3. يقوم Evil باستدعاء DD.deploy()، باستخدام CREATE2 لنشر Deployer على العنوان D (salt ثابت)。

4. استدعِ D.deployProposal()، لإنشاء عقد الـ Proposal بعنوان P؛ في هذه اللحظة يكون nonce الخاص بـ D هو 0。

5. يقوم Alice بالموافقة على العنوان P。

6. يقوم المهاجم باستدعاء D.kill() لتدمير D؛ عندها يتم مسح حساب العنوان D، بما في ذلك إعادة ضبط nonce إلى 0。

7. يقوم المهاجم مرة أخرى باستدعاء DD.deploy() لإعادة نشر Deployer على العنوان D (نفس معاملات CREATE2)。

8. استدعِ D.deployAttack()؛ نظرًا لأن nonce لدى D تمت إعادة ضبطه إلى 0 وأن عنوان D لم يتغير، فإن عنوان عقد Attack الذي تم إنشاؤه سيكون مطابقًا تمامًا لعنوان عقد الـ Proposal السابق P。

9. في هذه اللحظة، يوجد في مصفوفة proposals لدى DAO proposal يشير إلى العنوان P، لكن الآن تم إعادة نشر العنوان P ليصبح عقد Attack. لذلك، عند استدعاء execute() سيتم تنفيذ executeProposal() في Attack، وبالنتيجة يتم عبر delegatecall تعديل Owner الخاص بـ DAO إلى msg.sender، أي إلى عقد Attack。

يمكن تلخيص مبدأ الهجوم بأن المهاجم يستغل ميزة CREATE2 لإعادة نشر عقد على نفس العنوان. أولاً ينشر عقد Proposal شرعيًا لكي يعتمد DAO هذا العنوان. ثم يقوم بتدمير عقد Deployer، وإعادة نشر Deployer على نفس العنوان، وإعادة ضبط nonce الخاص بهذا العنوان؛ وبذلك ينجح في نشر عقد خبيث على العنوان نفسه الذي كان يحمل الـ Proposal سابقًا. وبما أن DAO يخزن الآن عنوان الـ proposal وهو يشير إلى العقد الخبيث، عند تنفيذ الـ proposal يتم تنفيذ الشفرة الخبيثة داخل سياق عقد DAO، فينجح في تغيير Owner إلى المهاجم.

اقتراحات الإصلاح

كمطور:

  • عند اعتماد الـ proposal، يجب ألا يتم تسجيل العنوان فقط، بل أيضًا تجزئة كود هذا العنوان؛ ثم عند التنفيذ يجب التحقق من أن تجزئة الكود لم تتغير مقارنة بالتجزئة وقت الاعتماد。

  • تجنب استخدام delegatecall لاستدعاء عقود خارجية، ما لم توجد إجراءات أمان كافية。

  • ضع في الحسبان إمكانية إعادة النشر على نفس العنوان بعد تدمير العقد ذاتيًا. وعند إجراء استدعاء خارجي، يلزم التحقق مما إذا كان العقد الخارجي موثوقًا؛ وإذا كان من الضروري استدعاء عقد خارجي غير مألوف، يُنصح بالتحقق مما إذا كان العقد يتضمن آلية تمنع التدمير الذاتي。

كمراجع (مدقق):

  • تحقق مما إذا كانت دوال التدمير الذاتي (selfdestruct) موجودة في العقود المُستدعاة خارجيًا؛ إذا كانت موجودة، فانتبه إلى مخاطر الهجوم التي تسمح بنشر كود خبيث على نفس العنوان بعد تدمير العقد خارجيًا。

  • تحقق مما إذا كان نشر العقود يستخدم CREATE2، وتأكد أن قيمة الـ salt المستخدمة لتوليد عنوان العقد عشوائية بدرجة كافية، لمنع استغلال التنبؤ المسبق بعنوان العقد من قبل المهاجمين لاختطافه بنشر عقدهم قبل ذلك。

  • تحقق مما إذا كان DAO يتحقق من تطابق كود العنوان الهدف عند تنفيذ الـ proposals؛ مثل مقارنة تجزئة الكود الحالية مع التجزئة وقت الاعتماد。

  • حلل بحذر سيناريوهات استخدام delegatecall، وتأكد أن العنوان الهدف موثوق، وهل توجد مخاطر استدعاء أي عنوان تعسفي (أي عنوان غير موثوق)。

  • راجع إدارة دورة حياة الـ proposals، مثل وجود آلية تمنع تعديل الـ proposal أو استبداله؛ مثل قفل حالة العنوان الهدف بعد اعتماده。