تخطي إلى المحتوى الرئيسي

كيفية استخدام إيكيدنا لاختبار العقود الذكية

Solidity
العقود الذكية
الأمان
الاختبار
الاختبار العشوائي
متقدم
تريل أوف بيتس
10 أبريل 2020
13 دقيقة للقراءة

التثبيت

يمكن تثبيت إيكيدنا من خلال Docker أو باستخدام الملف الثنائي المجمع مسبقًا.

إيكيدنا من خلال Docker

docker pull trailofbits/eth-security-toolbox
docker run -it -v "$PWD":/home/training trailofbits/eth-security-toolbox

يقوم الأمر الأخير بتشغيل eth-security-toolbox في Docker لديه حق الوصول إلى دليلك الحالي. يمكنك تغيير الملفات من مضيفك، وتشغيل الأدوات على الملفات من Docker

داخل Docker، قم بتشغيل:

solc-select 0.5.11
cd /home/training

الملف الثنائي

https://github.com/crytic/echidna/releases/tag/v1.4.0.0 (opens in a new tab)

مقدمة عن الاختبار العشوائي القائم على الخصائص (Property-based fuzzing)

إيكيدنا هي أداة اختبار عشوائي قائمة على الخصائص، وقد وصفناها في منشورات مدونتنا السابقة (1 (opens in a new tab)، 2 (opens in a new tab)، 3 (opens in a new tab)).

الاختبار العشوائي (Fuzzing)

يُعد الاختبار العشوائي (opens in a new tab) تقنية معروفة في مجتمع الأمان. وتتكون من إنشاء مدخلات عشوائية إلى حد ما للعثور على أخطاء في البرنامج. تُعرف أدوات الاختبار العشوائي للبرامج التقليدية (مثل AFL (opens in a new tab) أو LibFuzzer (opens in a new tab)) بأنها أدوات فعالة للعثور على الأخطاء.

بالإضافة إلى الإنشاء العشوائي البحت للمدخلات، هناك العديد من التقنيات والاستراتيجيات لإنشاء مدخلات جيدة، بما في ذلك:

  • الحصول على ملاحظات من كل عملية تنفيذ وتوجيه الإنشاء باستخدامها. على سبيل المثال، إذا أدى إدخال تم إنشاؤه حديثًا إلى اكتشاف مسار جديد، فقد يكون من المنطقي إنشاء مدخلات جديدة قريبة منه.
  • إنشاء الإدخال مع احترام قيد هيكلي. على سبيل المثال، إذا كان الإدخال الخاص بك يحتوي على رأس مع مجموع اختباري (checksum)، فسيكون من المنطقي السماح لأداة الاختبار العشوائي بإنشاء إدخال يتحقق من صحة المجموع الاختباري.
  • استخدام مدخلات معروفة لإنشاء مدخلات جديدة: إذا كان لديك وصول إلى مجموعة بيانات كبيرة من المدخلات الصالحة، فيمكن لأداة الاختبار العشوائي الخاصة بك إنشاء مدخلات جديدة منها، بدلاً من بدء إنشائها من الصفر. وتسمى هذه عادةً البذور (seeds).

الاختبار العشوائي القائم على الخصائص

تنتمي إيكيدنا إلى عائلة محددة من أدوات الاختبار العشوائي: الاختبار العشوائي القائم على الخصائص المستوحى بشكل كبير من QuickCheck (opens in a new tab). على عكس أدوات الاختبار العشوائي الكلاسيكية التي ستحاول العثور على الأعطال، ستحاول إيكيدنا كسر الثوابت (invariants) التي يحددها المستخدم.

في العقود الذكية، الثوابت هي دوال Solidity، والتي يمكن أن تمثل أي حالة غير صحيحة أو غير صالحة يمكن أن يصل إليها العقد، بما في ذلك:

  • التحكم غير الصحيح في الوصول: أصبح المهاجم مالك العقد.
  • آلة الحالة (state machine) غير الصحيحة: يمكن نقل الرموز المميزة أثناء إيقاف العقد مؤقتًا.
  • الحساب غير الصحيح: يمكن للمستخدم التسبب في تجاوز الحد الأدنى (underflow) لرصيده والحصول على رموز مميزة مجانية غير محدودة.

اختبار خاصية باستخدام إيكيدنا

سنرى كيفية اختبار عقد ذكي باستخدام إيكيدنا. الهدف هو العقد الذكي التالي token.sol (opens in a new tab):

سنفترض أن هذا الرمز المميز يجب أن يحتوي على الخصائص التالية:

  • يمكن لأي شخص أن يمتلك كحد أقصى 1000 رمز مميز
  • لا يمكن نقل الرمز المميز (إنه ليس رمزًا مميزًا من نوع ERC-20)

كتابة خاصية

خصائص إيكيدنا هي دوال Solidity. يجب أن تكون الخاصية:

  • ليس لها أي وسيطات (arguments)
  • تُرجع true إذا كانت ناجحة
  • يبدأ اسمها بـ echidna

ستقوم إيكيدنا بما يلي:

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

تتحقق الخاصية التالية من أن المتصل ليس لديه أكثر من 1000 رمز مميز:

function echidna_balance_under_1000() public view returns(bool){
      return balances[msg.sender] <= 1000;
}

استخدم الوراثة (inheritance) لفصل عقدك عن خصائصك:

contract TestToken is Token{
      function echidna_balance_under_1000() public view returns(bool){
            return balances[msg.sender] <= 1000;
      }
  }

ينفذ token.sol (opens in a new tab) الخاصية ويرث من الرمز المميز.

تهيئة عقد

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

هناك بعض العناوين المحددة في إيكيدنا:

  • 0x00a329c0648769A73afAc7F9381E08FB43dBEA72 والذي يستدعي المُنشئ.
  • 0x10000، و 0x20000، و 0x00a329C0648769a73afAC7F9381e08fb43DBEA70 والتي تستدعي الدوال الأخرى بشكل عشوائي.

لا نحتاج إلى أي تهيئة معينة في مثالنا الحالي، ونتيجة لذلك فإن المُنشئ الخاص بنا فارغ.

تشغيل إيكيدنا

يتم تشغيل إيكيدنا باستخدام:

echidna-test contract.sol

إذا كان contract.sol يحتوي على عقود متعددة، فيمكنك تحديد الهدف:

echidna-test contract.sol --contract MyContract

ملخص: اختبار خاصية

يلخص ما يلي تشغيل إيكيدنا على مثالنا:

contract TestToken is Token{
    constructor() public {}
        function echidna_balance_under_1000() public view returns(bool){
          return balances[msg.sender] <= 1000;
        }
  }

وجدت إيكيدنا أنه يتم انتهاك الخاصية إذا تم استدعاء backdoor.

تصفية الدوال التي سيتم استدعاؤها أثناء حملة الاختبار العشوائي

سنرى كيفية تصفية الدوال التي سيتم اختبارها عشوائيًا. الهدف هو العقد الذكي التالي:

يجبر هذا المثال الصغير إيكيدنا على العثور على تسلسل معين من المعاملات لتغيير متغير حالة. هذا صعب بالنسبة لأداة الاختبار العشوائي (يُوصى باستخدام أداة تنفيذ رمزي مثل مانتيكور (opens in a new tab)). يمكننا تشغيل إيكيدنا للتحقق من ذلك:

echidna-test multi.sol
...
echidna_state4: passed! 🎉
Seed: -3684648582249875403

تصفية الدوال

تواجه إيكيدنا مشكلة في العثور على التسلسل الصحيح لاختبار هذا العقد لأن دالتي إعادة التعيين (reset1 و reset2) ستعينان جميع متغيرات الحالة إلى false. ومع ذلك، يمكننا استخدام ميزة خاصة في إيكيدنا إما لإدراج دالة إعادة التعيين في القائمة السوداء أو لإدراج دوال f، و g، و h، و i فقط في القائمة البيضاء.

لإدراج الدوال في القائمة السوداء، يمكننا استخدام ملف التكوين هذا:

filterBlacklist: true
filterFunctions: ["reset1", "reset2"]

نهج آخر لتصفية الدوال هو سرد الدوال المدرجة في القائمة البيضاء. للقيام بذلك، يمكننا استخدام ملف التكوين هذا:

filterBlacklist: false
filterFunctions: ["f", "g", "h", "i"]
  • filterBlacklist هي true افتراضيًا.
  • سيتم إجراء التصفية بالاسم فقط (بدون معلمات). إذا كان لديك f() و f(uint256)، فإن عامل التصفية "f" سيطابق كلتا الدالتين.

تشغيل إيكيدنا

لتشغيل إيكيدنا باستخدام ملف تكوين blacklist.yaml:

echidna-test multi.sol --config blacklist.yaml
...
echidna_state4: failed!💥
  Call sequence:
    f(12)
    g(8)
    h(42)
    i()

ستجد إيكيدنا تسلسل المعاملات لتزييف الخاصية على الفور تقريبًا.

ملخص: تصفية الدوال

يمكن لإيكيدنا إما إدراج الدوال في القائمة السوداء أو القائمة البيضاء لاستدعائها أثناء حملة الاختبار العشوائي باستخدام:

filterBlacklist: true
filterFunctions: ["f1", "f2", "f3"]
echidna-test contract.sol --config config.yaml
...

تبدأ إيكيدنا حملة اختبار عشوائي إما بإدراج f1، و f2، و f3 في القائمة السوداء أو باستدعاء هذه الدوال فقط، وفقًا لقيمة المتغير المنطقي filterBlacklist.

كيفية اختبار تأكيد (assert) Solidity باستخدام إيكيدنا

في هذا البرنامج التعليمي القصير، سنوضح كيفية استخدام إيكيدنا لاختبار التحقق من التأكيد في العقود. لنفترض أن لدينا عقدًا مثل هذا:

كتابة تأكيد

نريد التأكد من أن tmp أقل من أو يساوي counter بعد إرجاع الفرق بينهما. يمكننا كتابة خاصية إيكيدنا، لكننا سنحتاج إلى تخزين قيمة tmp في مكان ما. بدلاً من ذلك، يمكننا استخدام تأكيد مثل هذا:

تشغيل إيكيدنا

لتمكين اختبار فشل التأكيد، قم بإنشاء ملف تكوين إيكيدنا (opens in a new tab) config.yaml:

checkAsserts: true

عندما نقوم بتشغيل هذا العقد في إيكيدنا، نحصل على النتائج المتوقعة:

كما ترى، تبلغ إيكيدنا عن بعض حالات فشل التأكيد في دالة inc. من الممكن إضافة أكثر من تأكيد واحد لكل دالة، لكن إيكيدنا لا يمكنها معرفة أي تأكيد قد فشل.

متى وكيف تستخدم التأكيدات

يمكن استخدام التأكيدات كبدائل للخصائص الصريحة، خاصة إذا كانت الشروط المراد التحقق منها مرتبطة بشكل مباشر بالاستخدام الصحيح لعملية ما f. ستؤدي إضافة التأكيدات بعد بعض التعليمات البرمجية إلى فرض حدوث التحقق فور تنفيذها:

function f(..) public {
    // بعض التعليمات البرمجية المعقدة
    ...
    assert (condition);
    ...
}

على العكس من ذلك، سيؤدي استخدام خاصية إيكيدنا صريحة إلى تنفيذ المعاملات بشكل عشوائي ولا توجد طريقة سهلة لفرض وقت التحقق منها بالضبط. لا يزال من الممكن القيام بهذا الحل البديل:

function echidna_assert_after_f() public returns (bool) {
    f(..);
    return(condition);
}

ومع ذلك، هناك بعض المشكلات:

  • يفشل إذا تم الإعلان عن f كـ internal أو external.
  • من غير الواضح ما هي الوسيطات التي يجب استخدامها لاستدعاء f.
  • إذا تراجعت (reverts) f، فستفشل الخاصية.

بشكل عام، نوصي باتباع توصية جون ريجر (John Regehr) (opens in a new tab) حول كيفية استخدام التأكيدات:

  • لا تفرض أي أثر جانبي أثناء التحقق من التأكيد. على سبيل المثال: assert(ChangeStateAndReturn() == 1)
  • لا تؤكد العبارات الواضحة. على سبيل المثال assert(var >= 0) حيث يتم الإعلان عن var كـ uint.

أخيرًا، يرجى عدم استخدام require بدلاً من assert، لأن إيكيدنا لن تتمكن من اكتشافه (لكن العقد سيتراجع على أي حال).

ملخص: التحقق من التأكيد

يلخص ما يلي تشغيل إيكيدنا على مثالنا:

وجدت إيكيدنا أن التأكيد في inc يمكن أن يفشل إذا تم استدعاء هذه الدالة عدة مرات باستخدام وسيطات كبيرة.

جمع وتعديل مجموعة بيانات (corpus) إيكيدنا

سنرى كيفية جمع واستخدام مجموعة بيانات من المعاملات مع إيكيدنا. الهدف هو العقد الذكي التالي magic.sol (opens in a new tab):

يجبر هذا المثال الصغير إيكيدنا على العثور على قيم معينة لتغيير متغير حالة. هذا صعب بالنسبة لأداة الاختبار العشوائي (يُوصى باستخدام أداة تنفيذ رمزي مثل مانتيكور (opens in a new tab)). يمكننا تشغيل إيكيدنا للتحقق من ذلك:

echidna-test magic.sol
...

echidna_magic_values: passed! 🎉

Seed: 2221503356319272685

ومع ذلك، لا يزال بإمكاننا استخدام إيكيدنا لجمع مجموعة البيانات عند تشغيل حملة الاختبار العشوائي هذه.

جمع مجموعة بيانات

لتمكين جمع مجموعة البيانات، قم بإنشاء دليل لمجموعة البيانات:

mkdir corpus-magic

وملف تكوين إيكيدنا (opens in a new tab) config.yaml:

coverage: true
corpusDir: "corpus-magic"

الآن يمكننا تشغيل أداتنا والتحقق من مجموعة البيانات التي تم جمعها:

echidna-test magic.sol --config config.yaml

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

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

بذر (Seeding) مجموعة بيانات

تحتاج إيكيدنا إلى بعض المساعدة من أجل التعامل مع دالة magic. سنقوم بنسخ وتعديل الإدخال لاستخدام معلمات مناسبة له:

cp corpus/2712688662897926208.txt corpus/new.txt

سنقوم بتعديل new.txt لاستدعاء magic(42,129,333,0). الآن، يمكننا إعادة تشغيل إيكيدنا:

هذه المرة، وجدت أنه يتم انتهاك الخاصية على الفور.

العثور على المعاملات ذات الاستهلاك العالي للغاز

سنرى كيفية العثور على المعاملات ذات الاستهلاك العالي للغاز باستخدام إيكيدنا. الهدف هو العقد الذكي التالي:

هنا يمكن أن يكون لـ expensive استهلاك كبير للغاز.

حاليًا، تحتاج إيكيدنا دائمًا إلى خاصية لاختبارها: هنا تُرجع echidna_test دائمًا true. يمكننا تشغيل إيكيدنا للتحقق من ذلك:

echidna-test gas.sol
...
echidna_test: نجح! 🎉

البذرة: 2320549945714142710

قياس استهلاك الغاز

لتمكين استهلاك الغاز مع إيكيدنا، قم بإنشاء ملف تكوين config.yaml:

estimateGas: true

في هذا المثال، سنقوم أيضًا بتقليل حجم تسلسل المعاملات لجعل النتائج أسهل في الفهم:

seqLen: 2
estimateGas: true

تشغيل إيكيدنا

بمجرد إنشاء ملف التكوين، يمكننا تشغيل إيكيدنا على النحو التالي:

تصفية الاستدعاءات التي تقلل الغاز

يوضح البرنامج التعليمي حول تصفية الدوال التي سيتم استدعاؤها أثناء حملة الاختبار العشوائي أعلاه كيفية إزالة بعض الدوال من اختبارك.
يمكن أن يكون هذا بالغ الأهمية للحصول على تقدير دقيق للغاز. ضع في اعتبارك المثال التالي:

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

ذلك لأن التكلفة تعتمد على حجم addrs وتميل الاستدعاءات العشوائية إلى ترك المصفوفة فارغة تقريبًا. ومع ذلك، فإن إدراج pop و clear في القائمة السوداء يعطينا نتائج أفضل بكثير:

filterBlacklist: true
filterFunctions: ["pop", "clear"]
echidna-test pushpop.sol --config config.yaml
...
استخدم push بحد أقصى 40839 غاز
...
استخدم check بحد أقصى 1484472 غاز

ملخص: العثور على المعاملات ذات الاستهلاك العالي للغاز

يمكن لإيكيدنا العثور على المعاملات ذات الاستهلاك العالي للغاز باستخدام خيار التكوين estimateGas:

estimateGas: true
echidna-test contract.sol --config config.yaml
...

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