العقود الذكية الهجمات و الثغرات

الآن بعد أن مررنا ببعض مسببات الهجمات في لغة Solidity ، دعنا ننتقل إلى المزيد من المخاطر والهجمات الخاصة بالعقود الذكية العامة. هذه هي أنماط هجوم أكثر عمومية والتي تتضمن الخروج من دائرة البرمجة إلى خط عمل العقود الذكية الأوسع و المسماة (Call Known Attacks). هناك أيضًا مخاوف تأتي من مستوى البروتوكول ، والتي ستتطلب التزاوج بين المعلومات التي تعلمناها  سابقاً  مع معرفة بالعقد الذكية التي اكتسبتها (network Known Attacks).

تم تجميع جميع موجهات الهجوم هذه (والمزيد) في تصنيف ضعف العقد الذكي  وحالات الاختبار (Smart Contract Weakness Classification)  أو SWC Registry, ، وسنشير إلى متجهات الهجوم من خلال رقم مؤشر SWC الخاص بهم:

هجمات الشبكة المعروفة:

  • Front-Running (SWC-114)
  • Timestamp Dependence (SWC-116)
  • Network Stuffing DoS

هجمات المكالمة المعروفة:

Frontrunning

أصبح Frontrunning مشكلة كبيرة في مجتمع Ethereum فيما يتعلق بالقيمة المستخرجة من عامل التعدين (MEV) ،. يستغل Frontrunning كيفية تضمين المعاملات في blockchain والاعتبارات المتعلقة بالعملية. المعاملات التي يتم بثها إلى الشبكة ولكن لم يتم تضمينها بعد في كتلة موجودة في مجموعة الذاكرة.

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

قد يكون هذا مشكلة بالنسبة لأشياء مثل الأسواق اللامركزية.

الحماية من هذا أمر صعب وستحتاج على الأرجح إلى ابتكار حلول محددة للعقد.

يمكن للأسواق اللامركزية تخفيف المخاوف من خلال تنفيذ المزادات على دفعات أو استخدام مخطط الالتزام المسبق ، حيث يتم تقديم التفاصيل بعد الالتزام بالمعاملة.

يتوفر المزيد من المعلومات حول اعتماد أمر المعاملة والعينات الملموسة في إدخال سجل SWC الخاص بها. نظرًا للتركيز المتزايد على MEV ، يمكننا أن نتوقع رؤية المزيد من النصائح الأمنية والبحث حولها.

Timestamp Dependence

غالبًا ما تحتاج العقود إلى الوصول إلى قيم الوقت لأداء أنواع معينة من الوظائف. يمكن أن تمنحك قيم مثل block.timestamp و block.number فكرة عن الوقت الحالي أو دلتا الوقت ، ومع ذلك فهي ليست آمنة للاستخدام في معظم الأغراض.

في حالة block.timestamp ، يحاول المطورون غالبًا استخدامه لتشغيل أحداث تعتمد على الوقت. نظرًا لأن Ethereum لا مركزية ، يمكن للعقد مزامنة الوقت فقط إلى حد ما. علاوة على ذلك ، يمكن لعمال المناجم الخبيثة تغيير الطابع الزمني للكتل الخاصة بهم ، خاصة إذا كان بإمكانهم الحصول على مزايا من خلال القيام بذلك. (مصدر)

Network Stuffing DoS

هذا ناقل للهجوم يعتمد على عمليات حساسة للوقت ، مثل المزاد أو المحفظة المؤمنة زمنياً ، أو العمليات التي تتطلب إدخال المستخدم قبل ارتكاب إجراء لا رجوع فيه. بشكل أساسي ، نحن بحاجة إلى الحذر من حقيقة أنه ، خاصة الآن ، يمكن أن تكون هناك لحظات من الازدحام المروري (وليس حتى ضارًا) حيث يكون من شبه المستحيل تحويل معاملات المرء إلى عقد ذكي. نأمل مع EIP-1559 أن تكون هذه اللحظات قصيرة ، لكنها قد تكون موجودة. على هذا النحو ، نحتاج إلى التأكد من أن العمليات الحساسة للوقت بها بعض الآليات الاحتياطية أو الآمنة للحماية من حشو الشبكة ، ضار أو غير ذلك.

Forcibly Sending Ether

الخطر الآخر هو استخدام المنطق الذي يعتمد على رصيد العقد.

يجب أن تدرك أنه من الممكن إرسال الأثير إلى عقد دون تشغيل وظيفته الاحتياطية.

استخدام وظيفة التدمير الذاتي في عقد آخر واستخدام العقد المستهدف حيث سيجبر المستلم على إرسال أموال العقد المدمرة إلى الهدف.

من الممكن أيضًا إجراء حساب مسبق لعنوان العقود وإرسال الأثير إلى العنوان قبل نشر العقد ، انظر (CREATE2).

سيكون رصيد العقد أكبر من 0 عندما يتم نشره أخيرًا.

Block Gas Limit DoS

يوجد حد لمقدار الحساب الذي يمكن تضمينه في كتلة Ethereum واحدة ، تبلغ قيمتها حاليًا 10000000 غاز. هذا يعني أنه إذا وصل عقدك الذكي إلى حالة تتطلب فيها المعاملة أكثر من 10000000 غاز لتنفيذها ، فلن يتم تنفيذ هذه المعاملة بنجاح (SWC-128). سيصل دائمًا إلى حد غاز الكتلة قبل الانتهاء.

وبالمثل ، إذا كان الغاز المطلوب للمعاملة أقل من 8،000،000 ، ولكن قريبًا منه ، فقد تواجه صعوبة أكبر في تضمين معاملتك في كتلة بواسطة عامل منجم. من الأرجح أنه إذا أرسلت معاملة إلى الشبكة ببداية غاز قريبة من 8،000،000 ، فلن يختار المُعدِّن المعاملة لتضمينها في كتلة. يمكنك العثور على مزيد من المعلومات حول ترتيب معاملة التعدين الافتراضية غير المدرجة في العملاء الأكثر شعبية هنا.

يصبح هذا الموقف ممكنًا إذا كان عقدك يتكرر على تكرار لـ مصفوفة غير محددة الحجم. إذا أصبحت المصفوفة كبيرة جدًا ، فقد لا يتم تنفيذها أبدًا. تتوفر عينة ملموسة من المصفوفات الديناميكي يحتمل أن يؤدي إلى DoS في إدخال سجل SWC هنا.

Reentrancy

مخطط هجوم العودة

تعتبر هجمات  Reentrancy (SWC-107)) معروفة جدًا بفضل اختراق DAO سيئ السمعة الذي حدث على شبكة Ethereum. في هجوم إعادة الدخول ، يرسل عقد ضعيف الحماية الأثير إلى عنوان غير معروف يحتوي على وظيفة fallback . بعد ذلك ، يستدعي الكود الضار بشكل متكرر إحدى الوظائف الموجودة في العقد الضعيف قبل إنهاء المكالمة الأولى.

// Vulnerable contract
function withdraw(uint _amount) public {
    require(balances[msg.sender] >= _amount, "Not enough balance!");
    msg.sender.call.value(_amount)("");
    balances[msg.sender] -= _amount;
}

// Malicious contract
function () payable external {
    if(address(vulnerableContract).balance > 1 ether) {
        vulnerableContract.withdraw(1 ether);
    }
}
    

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

mapping (address => uint) private userBalances;     // Better!

function withdrawBalance() public {
    uint amountToWithdraw = userBalances[msg.sender];
    userBalances[msg.sender] = 0;
    require(msg.sender.call.value(amountToWithdraw)()); // The user's balance is already 0, so future invocations won't withdraw anything
}

أو استخدام نمط تصميم السحب (withdrawal design pattern) وفصل منطق محاسبة العقد ومنطق التحويل.

شيء آخر يجب أن تكون على دراية به هو إعادة الدخول المحتملة للوظيفة المتقاطعة. قد يكون هذا مشكلة إذا كان عقدك يحتوي على وظائف متعددة تعمل على تعديل نفس الحالة.

    // INSECURE
    mapping (address => uint) private userBalances;
    
    function transfer(address to, uint amount) {
        if (userBalances[msg.sender] >= amount) {
           userBalances[to] += amount;
           userBalances[msg.sender] -= amount;
        }
    }
    
    function withdrawBalance() public {
        uint amountToWithdraw = userBalances[msg.sender];
        require(msg.sender.call.value(amountToWithdraw)()); // At this point, the caller's code is executed, and can call transfer()
        userBalances[msg.sender] = 0;
    }
  

في هذه الحالة ، يمكن للمهاجم استدعاء transfer() عندما يتم تنفيذ الكود الخاصة به على المكالمة الخارجية في pullBalance. نظرًا لأن رصيدهم لم يتم ضبطه على 0 بعد ، فإنهم قادرون على تحويل التوكنات على الرغم من أنهم قد تلقوا بالفعل عملية السحب. تم استخدام هذه الثغرة هذه أيضًا في هجوم DAO.

هناك عدة طرق للتخفيف من هذه المشاكل.

الأول هو نمط تصميم Check-Effect-Interaction الذي وصفناه سابقًا. من الجيد عمومًا التعامل مع تدفق دالة مثل:

  • تحقق من حالة الاختبار. على سبيل المثال require
  • تأثير متغير حالة التحديث (مثل تحديث الرصيد)
  • التفاعلات مع العقد الخارجي (مثل إرسال الأثير باستخدام call.value)

باختصار ، أنت تتعامل مع التغييرات في حالة عقدك الداخلي قبل استدعاء العقود الخارجية.

يمكن أن يؤدي الحل الأكثر تعقيدًا إلى الاستبعاد المتبادل (mutual exclusion)  أو المزامنة (mutex). هذا يسمح لك بقفل حالة والسماح فقط بالتغييرات من قبل مالك القفل. يمكنك مشاهدة مثال على كائن المزامنة في Solidity هنا.

يمكنك التعمق أكثر في الهجمات المعروفة مثل هذه هنا.

Integer Under / Overflow

ملاحظة: مع تضمين SafeMath أصلاً مع Solidity 0.8.x ، فإن احتمال كتابة integer under / overflow  أمر غير محتمل. ومع ذلك ، لابد من أخذ الحيطة.

يمكن للأرقام الصحيحة أن تتدفق أو تفيض في EVM (SWC-101). يحدث هذا عندما تتجاوز القيمة الحسابية الحد الأدنى أو الأقصى لحجم النوع.

القيمة القصوى لعدد صحيح بدون إشارة هي 2 ^ 256 – 1 ، أي ما يقرب من 1.15 مرة 10 ^ 77. إذا تجاوز عدد صحيح ، فستعود القيمة إلى 0. على سبيل المثال ، متغير يسمى درجة من النوع uint8 يخزن قيمة 255 التي تزداد بمقدار 1 ستخزن الآن القيمة 0.

قد تقلق أو لا تقلق بشأن تجاوز عدد صحيح اعتمادًا على عقدك الذكي.

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

Underflow هو موقف مشابه ، ولكن عندما تقل قيمة uint عن الحد الأدنى لقيمتها ، فسيتم تعيينها على قيمتها القصوى.

كن حذرًا مع أنواع البيانات الأصغر مثل uint8 و uint16 وما إلى ذلك … يمكن أن تصل بسهولة أكبر إلى قيمتها القصوى

يمكن العثور على مزيد من المعلومات والأمثلة الملموسة في إدخال سجل SWC الخاص بها

Unexpected Revert DoS

يتمثل هذا الهجوم أساسًا في جعل عقد ضعيف غير قابل للتشغيل عن طريق فرض حالة فشل أو انتظار ، أو قفل تنفيذ العقد مؤقتًا أو بشكل دائم ((SWC-113).

  // INSECURE
  contract Auction {
      address currentLeader;
      uint highestBid;
  
      function bid() payable {
          require(msg.value > highestBid);
  
          require(currentLeader.send(highestBid)); // Refund the old leader, if it fails then revert
  
          currentLeader = msg.sender;
          highestBid = msg.value;
      }
  }

في المثال المقدم ، يمكن أن يكون highestBidder  عقدًا آخر ، ويؤدي تحويل الأموال إلى العقد إلى تشغيل الوظيفة fallback للعقد. إذا كان fallback العقد يتراجع دائمًا ، تصبح وظيفة العطاء لعقد Auction غير قابلة للاستخدام – ستعود دائمًا. تتطلب وظيفة العطاء أن تنجح عملية النقل للتنفيذ الكامل.

العقد على العنوان المقدم يرمي استثناء ، يتوقف التنفيذ ويمرر الاستثناء إلى عقد الاستدعاء ويمنع المزيد من التنفيذ.

يمكن تجنب هذه المشكلة باستخدام نمط الانسحاب (withdrawal pattern)  والدفع-السحب (push-pull.). يمكنك أيضًا استخدام عقد متعدد التوقيع أو وقت انتهاء الصلاحية أو أسلوب آخر كخطة طوارئ لحالات القفل المحتملة.

tx.origin Authentication

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

  // Vulnerable contract
  function withdraw(address payable _recipient) public {
      require(tx.origin == owner);
      _recipient.transfer(address(this).balance);
  }
  
  // Malicious contract
  function() external payable {
      vulnerableContract.withdraw(attackerAddress);
  }

مثال على ذلك هو اختراق Poly Network ، على الرغم من عدم وجوده على شبكة Ethereum.

هناك دائمًا المزيد من متجهات الهجوم ، تأكد من البحث في سجل SWC ، أو متابعة Diligence على Twitter ، أو أي مصدر تحتاجه للتأكد من مواكبة الأمان على Ethereum.

مصادر إضافية 

إضافة تعليق