نقاط الضعف في عقود Etherium الذكية. أمثلة التعليمات البرمجية

مع هذا المنشور ، أبدأ سلسلة من المقالات حول أمان عقود Ethereum الذكية. أعتقد أن هذا الموضوع وثيق الصلة للغاية ، نظرًا لأن عدد المطورين ينمو مثل الانهيار الجليدي ، وليس هناك من ينقذ من "أشعل النار". وداعا - ترجمات ...

1. فحص عقود Live Ethereum بحثًا عن خطأ إرسال لم يتم التحقق منه


أصلي - مسح عقود Ethereum الحية بحثًا عن "Unchecked-Send ..."


المؤلف: Zikai Alex Wen و Andrew Miller

من المعروف أن برمجة العقد الذكي لـ Ethereum عرضة للخطأ [1] . لقد رأينا ذلك مؤخرًا
تحتوي العقود الذكية المتطورة مثل King of the Ether و DAO-1.0 على ثغرات أمنية بسبب أخطاء البرمجة.

منذ مارس 2015 ، تم تحذير مبرمجي العقود الذكية من مخاطر برمجة محددة قد تنشأ عندما ترسل العقود رسائل إلى بعضها البعض [6] .

توصي العديد من أدلة البرمجة بكيفية تجنب الأخطاء الشائعة (في الأوراق البيضاء Ethereum [3] وفي دليل مستقل من UMD [2] ). على الرغم من أن هذه المخاطر مفهومة بما يكفي لتفاديها ، فإن عواقب مثل هذا الخطأ فظيعة: يمكن حظر المال أو فقدانه أو سرقته.

ما مدى شيوع الأخطاء الناتجة عن هذه المخاطر؟ هل هناك المزيد من عقود blockchain Ethereum الحية ولكن الحية؟ في هذه المقالة ، نجيب على هذا السؤال من خلال تحليل العقود على بلوكشين Live Ethereum باستخدام أداة التحليل الجديدة التي طورناها.


ما هو خطأ الإرسال غير المحدد؟


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


/*** Listing 1 ***/ if (gameHasEnded && !( prizePaidOut ) ) { winner.send(1000); //    prizePaidOut = True; } 

المشكلة هنا هي أن طريقة الإرسال قد تفشل. إذا لم يفلح ذلك ، فلن يتلقى الفائز المال ، ولكن سيتم تعيين المتغير prizePaidOut إلى True.

هناك حالتان مختلفتان قد تفشل فيه وظيفة الفائز . سنقوم بتحليل الفرق بينهما فيما بعد. الحالة الأولى هي أن عنوان الفائز هو عقد (وليس حساب مستخدم) ، ورمز هذا العقد يثير استثناء (على سبيل المثال ، إذا كان يستخدم الكثير من "الغاز"). إذا كان الأمر كذلك ، فربما في هذه الحالة "خطأ الفائز". الحالة الثانية أقل وضوحا. يحتوي الجهاز الظاهري Ethereum على مورد محدود يسمى " callstack " (عمق مكدس الاستدعاء) ، ويمكن استخدام هذا المورد بواسطة رمز عقد آخر تم تنفيذه سابقًا في معاملة. إذا تم استخدام Callstack بالفعل في الوقت الذي يتم فيه تنفيذ الأمر send ، فسوف يفشل الأمر ، بغض النظر عن كيفية تحديد الفائز . سيتم تدمير جائزة الفائز من دون أي خطأ من جانبه!



كيف يمكن تجنب هذا الخطأ؟

تحتوي وثائق Ethereum على تحذير موجز حول هذا الخطر المحتمل [3] : "هناك بعض الخطر عند استخدام الإرسال - يفشل الإرسال إذا كان عمق مكدس المكالمة 1024 (يمكن أن يحدث ذلك دائمًا من قبل المتصل) ، ويفشل أيضًا إذا كان المستلم ينتهي "الغاز". لذلك ، لضمان البث الآمن ، تحقق دائمًا من القيمة المرجعة للإرسال أو الأفضل: استخدم نموذجًا يسحب فيه المستلم الأموال. "

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


  /*** Listing 2 ***/ if (gameHasEnded && !( prizePaidOut ) ) { if (winner.send(1000)) prizePaidOut = True; else throw; } 

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


 /*** Listing 3 ***/ if (gameHasEnded && !( prizePaidOut ) ) { if (winner.send(1000) && loser.send(10)) prizePaidOut = True; else throw; } 

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

لذلك ، حتى أفضل الممارسات (الموصى بها في دليل مبرمج Ethereum و Serpent ، على الرغم من أنه ينطبق بشكل متساوٍ على Solidity) ، هو التحقق من مورد callstack . يمكننا تعريف callStackIsEmpty () الماكرو ، والذي سيُرجع خطأ إذا وفقط callstack فقط.


 /*** Listing 4 ***/ if (gameHasEnded && !( prizePaidOut ) ) { if (callStackIsEmpty()) throw; winner.send(1000) loser.send(10) prizePaidOut = True; } 

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


 /*** Listing 5 ***/ if (gameHasEnded && !( prizePaidOut ) ) { accounts[winner] += 1000 accounts[loser] += 10 prizePaidOut = True; } ... function withdraw(amount) { if (accounts[msg.sender] >= amount) { msg.sender.send(amount); accounts[msg.sender] -= amount; } } 

العديد من العقود الذكية المتطورة ضعيفة. إن ملك اليانصيب العرش هو الحالة الأكثر شهرة لهذا الخطأ [4] . لم يلاحظ هذا الخطأ حتى لم يتمكن مبلغ 200 إيثر (بقيمة تزيد عن 2000 دولار أمريكي بسعر اليوم) من الحصول على الفائز المشروع في اليانصيب. يشبه الرمز المطابق في King of the Ether الرمز الموجود في القائمة 2. لحسن الحظ ، في هذه الحالة ، كان مطور العقد قادرًا على استخدام الوظيفة غير ذات الصلة في العقد كـ "تجاوز يدوي" للإفراج عن الأموال العالقة. يمكن للمسؤول الأقل دقة استخدام نفس الوظيفة لسرقة البث!


استمرار مسح عقود Ethereum الحية بحثًا عن خطأ إرسال غير محدد. الجزء 2

Source: https://habr.com/ru/post/ar430602/


All Articles