كيفية استخدام سليذر للعثور على الأخطاء في العقود الذكية
كيفية استخدام سليذر
الهدف من هذا الدرس التعليمي هو توضيح كيفية استخدام سليذر للعثور تلقائيًا على الأخطاء في العقود الذكية.
- التثبيت
- استخدام سطر الأوامر
- مقدمة في التحليل الثابت: مقدمة موجزة في التحليل الثابت
- واجهة برمجة التطبيقات: وصف واجهة برمجة تطبيقات بايثون
التثبيت
يتطلب سليذر إصدار بايثون >= 3.6. يمكن تثبيته من خلال pip أو باستخدام docker.
سليذر من خلال pip:
pip3 install --user slither-analyzerسليذر من خلال docker:
docker pull trailofbits/eth-security-toolboxdocker run -it -v "$PWD":/home/trufflecon trailofbits/eth-security-toolboxالأمر الأخير يشغل eth-security-toolbox في حاوية docker لديها صلاحية الوصول إلى دليلك الحالي. يمكنك تغيير الملفات من مضيفك، وتشغيل الأدوات على الملفات من حاوية docker
داخل حاوية docker، قم بتشغيل:
solc-select 0.5.11cd /home/trufflecon/تشغيل نص برمجي
لتشغيل نص python برمجي باستخدام python 3:
python3 script.pyسطر الأوامر
سطر الأوامر مقابل البرامج النصية المعرفة من قبل المستخدم. يأتي سليذر مع مجموعة من أجهزة الكشف المحددة مسبقًا والتي تعثر على العديد من الأخطاء الشائعة. سيؤدي استدعاء سليذر من سطر الأوامر إلى تشغيل جميع أجهزة الكشف، دون الحاجة إلى معرفة تفصيلية بالتحليل الثابت:
slither project_pathsبالإضافة إلى أجهزة الكشف، يمتلك سليذر إمكانيات مراجعة النص البرمجي من خلال الطابعات (opens in a new tab) والأدوات (opens in a new tab) الخاصة به.
استخدم crytic.io (opens in a new tab) للوصول إلى أجهزة الكشف الخاصة وتكامل غيت هاب.
التحليل الثابت
تم وصف إمكانيات وتصميم إطار عمل التحليل الثابت سليذر في منشورات المدونة (1 (opens in a new tab)، 2 (opens in a new tab)) وورقة أكاديمية (opens in a new tab).
يوجد التحليل الثابت في أنواع مختلفة. من المرجح أنك تدرك أن المترجمات مثل clang (opens in a new tab) وgcc (opens in a new tab) تعتمد على تقنيات البحث هذه، ولكنها تدعم أيضًا (Infer (opens in a new tab)، CodeClimate (opens in a new tab)، FindBugs (opens in a new tab) والأدوات القائمة على الأساليب الرسمية مثل Frama-C (opens in a new tab) وPolyspace (opens in a new tab).
لن نقوم بمراجعة شاملة لتقنيات التحليل الثابت والباحثين هنا. بدلاً من ذلك، سنركز على ما هو مطلوب لفهم كيفية عمل سليذر حتى تتمكن من استخدامه بشكل أكثر فاعلية للعثور على الأخطاء وفهم النص البرمجي.
تمثيل النص البرمجي
على النقيض من التحليل الديناميكي، الذي يستنتج مسار تنفيذ واحد، يستنتج التحليل الثابت جميع المسارات في وقت واحد. للقيام بذلك، فإنه يعتمد على تمثيل مختلف للنص البرمجي. الأكثر شيوعًا هما شجرة بناء الجملة المجردة (AST) ورسم بياني لتدفق التحكم (CFG).
أشجار بناء الجملة المجردة (AST)
تُستخدم أشجار بناء الجملة المجردة (AST) في كل مرة يقوم فيها المحول البرمجي بتحليل النص البرمجي. ربما تكون البنية الأساسية التي يمكن على أساسها إجراء التحليل الثابت.
باختصار، شجرة بناء الجملة المجردة (AST) هي شجرة منظمة حيث تحتوي كل ورقة عادةً على متغير أو ثابت، والعقد الداخلية هي معاملات أو عمليات تدفق تحكم. خذ بعين الاعتبار النص البرمجي التالي:
1function safeAdd(uint a, uint b) pure internal returns(uint){2 if(a + b <= a){3 revert();4 }5 return a + b;6}يتم عرض شجرة بناء الجملة المجردة (AST) المقابلة في:
يستخدم سليذر شجرة بناء الجملة المجردة (AST) التي يتم تصديرها بواسطة solc.
على الرغم من بساطة بنائها، فإن شجرة بناء الجملة المجردة (AST) هي بنية متداخلة. في بعض الأحيان، لا يكون هذا هو التحليل الأكثر وضوحًا. على سبيل المثال، لتحديد العمليات المستخدمة في التعبير a + b <= a، يجب عليك أولاً تحليل <= ثم +. النهج الشائع هو استخدام ما يسمى بنمط الزائر (visitor pattern)، والذي يتنقل عبر الشجرة بشكل متكرر. يحتوي سليذر على زائر عام في ExpressionVisitor (opens in a new tab).
يستخدم النص البرمجي التالي ExpressionVisitor للكشف عما إذا كان التعبير يحتوي على إضافة:
1from slither.visitors.expression.expression import ExpressionVisitor2from slither.core.expressions.binary_operation import BinaryOperationType34class HasAddition(ExpressionVisitor):56 def result(self):7 return self._result89 def _post_binary_operation(self, expression):10 if expression.type == BinaryOperationType.ADDITION:11 self._result = True1213visitor = HasAddition(expression) # التعبير هو التعبير المراد اختباره14print(f'التعبير {expression} لديه إضافة: {visitor.result()}')إظهار الكلالرسم البياني لتدفق التحكم (CFG)
التمثيل الثاني الأكثر شيوعًا للنص البرمجي هو الرسم البياني لتدفق التحكم (CFG). كما يوحي اسمه، فهو تمثيل قائم على الرسم البياني يكشف جميع مسارات التنفيذ. تحتوي كل عقدة على تعليمة واحدة أو عدة تعليمات. تمثل الحواف في الرسم البياني عمليات تدفق التحكم (if/then/else، والحلقة، وما إلى ذلك). الرسم البياني لتدفق التحكم (CFG) لمثالنا السابق هو:
الرسم البياني لتدفق التحكم (CFG) هو التمثيل الذي تُبنى عليه معظم التحليلات.
توجد العديد من تمثيلات النص البرمجي الأخرى. لكل تمثيل مزايا وعيوب وفقًا للتحليل الذي تريد إجراءه.
التحليل
أبسط أنواع التحليلات التي يمكنك إجراؤها باستخدام سليذر هي التحليلات النحوية.
تحليل بناء الجملة
يمكن لـ سليذر التنقل عبر المكونات المختلفة للنص البرمجي وتمثيلها للعثور على التناقضات والعيوب باستخدام نهج يشبه مطابقة الأنماط.
على سبيل المثال، تبحث أجهزة الكشف التالية عن المشكلات المتعلقة ببناء الجملة:
-
إخفاء متغير الحالة (opens in a new tab): يتكرر على جميع متغيرات الحالة ويتحقق مما إذا كان أي منها يخفي متغيرًا من عقد موروث (state.py#L51-L62 (opens in a new tab))
-
واجهة ERC20 غير الصحيحة (opens in a new tab): ابحث عن تواقيع دالة ERC20 غير الصحيحة (incorrect_erc20_interface.py#L34-L55 (opens in a new tab))
التحليل الدلالي
على النقيض من تحليل بناء الجملة، فإن التحليل الدلالي سوف يتعمق ويحلل "معنى" النص البرمجي. تشمل هذه العائلة بعض الأنواع الواسعة من التحليلات. إنها تؤدي إلى نتائج أكثر قوة وفائدة، ولكنها أيضًا أكثر تعقيدًا في الكتابة.
تُستخدم التحليلات الدلالية في الكشف عن الثغرات الأمنية الأكثر تقدمًا.
تحليل تبعية البيانات
يقال إن المتغير variable_a يعتمد على بيانات variable_b إذا كان هناك مسار تتأثر فيه قيمة variable_a بـ variable_b.
في النص البرمجي التالي، يعتمد variable_a على variable_b:
1// ...2variable_a = variable_b + 1;يأتي سليذر مزودًا بإمكانيات تبعية البيانات (opens in a new tab) المضمنة، وذلك بفضل التمثيل الوسيط الخاص به (والذي تمت مناقشته في قسم لاحق).
يمكن العثور على مثال لاستخدام تبعية البيانات في كاشف المساواة الصارمة الخطير (opens in a new tab). هنا سيبحث سليذر عن مقارنة المساواة الصارمة بقيمة خطيرة (incorrect_strict_equality.py#L86-L87 (opens in a new tab))، وسيبلغ المستخدم أنه يجب عليه استخدام >= أو <= بدلاً من ==، لمنع المهاجم من محاصرة العقد. من بين أمور أخرى، سيعتبر الكاشف القيمة المرجعة لاستدعاء balanceOf(address) خطيرة (incorrect_strict_equality.py#L63-L64 (opens in a new tab))، وسيستخدم محرك تبعية البيانات لتتبع استخدامه.
حساب النقطة الثابتة
إذا كان تحليلك يتنقل عبر الرسم البياني لتدفق التحكم (CFG) ويتبع الحواف، فمن المحتمل أن ترى العقد التي تمت زيارتها بالفعل. على سبيل المثال، إذا تم تقديم حلقة كما هو موضح أدناه:
1for(uint i; i < range; ++){2 variable_a += 13}سيحتاج تحليلك إلى معرفة متى يتوقف. هناك استراتيجيتان رئيسيتان هنا: (1) التكرار على كل عقدة عددًا محدودًا من المرات، (2) حساب ما يسمى بـ نقطة ثابتة (fixpoint). النقطة الثابتة (fixpoint) تعني بشكل أساسي أن تحليل هذه العقدة لا يوفر أي معلومات ذات معنى.
يمكن العثور على مثال لاستخدام النقطة الثابتة في كاشفات إعادة الدخول: يستكشف سليذر العقد، ويبحث عن الاستدعاءات الخارجية والكتابة والقراءة إلى التخزين. بمجرد وصولها إلى نقطة ثابتة (reentrancy.py#L125-L131 (opens in a new tab))، فإنها توقف الاستكشاف، وتحلل النتائج لمعرفة ما إذا كان هناك إعادة دخول، من خلال أنماط إعادة دخول مختلفة (reentrancy_benign.py (opens in a new tab)، reentrancy_read_before_write.py (opens in a new tab)، reentrancy_eth.py (opens in a new tab)).
تتطلب كتابة التحليلات باستخدام حساب النقطة الثابتة الفعال فهمًا جيدًا لكيفية نشر التحليل لمعلوماته.
التمثيل الوسيط
التمثيل الوسيط (IR) هو لغة يُقصد بها أن تكون أكثر قابلية للتحليل الثابت من اللغة الأصلية. يترجم سليذر سوليديتي إلى التمثيل الوسيط الخاص به: SlithIR (opens in a new tab).
فهم SlithIR ليس ضروريًا إذا كنت تريد فقط كتابة فحوصات أساسية. ومع ذلك، سيكون مفيداً إذا كنت تخطط لكتابة تحليلات دلالية متقدمة. ستساعدك طابعات SlithIR (opens in a new tab) وSSA (opens in a new tab) على فهم كيفية ترجمة النص البرمجي.
أساسيات واجهة برمجة التطبيقات
يحتوي سليذر على واجهة برمجة تطبيقات تتيح لك استكشاف السمات الأساسية للعقد ووظائفه.
لتحميل قاعدة بيانات برمجية:
1from slither import Slither2slither = Slither('/path/to/project')3استكشاف العقود والوظائف
يحتوي كائن Slither على:
contracts (list(Contract): قائمة العقودcontracts_derived (list(Contract): قائمة العقود التي لم يتم توريثها من عقد آخر (مجموعة فرعية من العقود)get_contract_from_name (str): إرجاع عقد من اسمه
يحتوي كائن Contract على:
name (str): اسم العقدfunctions (list(Function)): قائمة الوظائفmodifiers (list(Modifier)): قائمة المُعدِّلاتall_functions_called (list(Function/Modifier)): قائمة بجميع الوظائف الداخلية التي يمكن الوصول إليها بواسطة العقدinheritance (list(Contract)): قائمة العقود الموروثةget_function_from_signature (str): إرجاع وظيفة من توقيعهاget_modifier_from_signature (str): إرجاع مُعدِّل من توقيعهget_state_variable_from_name (str): إرجاع متغير حالة من اسمه
يحتوي كائن Function أو Modifier على:
name (str): اسم الوظيفةcontract (contract): العقد الذي تم الإعلان عن الوظيفة فيهnodes (list(Node)): قائمة العقد التي يتكون منها الرسم البياني لتدفق التحكم للوظيفة/المُعدِّلentry_point (Node): نقطة الدخول للرسم البياني لتدفق التحكمvariables_read (list(Variable)): قائمة المتغيرات المقروءةvariables_written (list(Variable)): قائمة المتغيرات المكتوبةstate_variables_read (list(StateVariable)): قائمة متغيرات الحالة المقروءة (مجموعة فرعية من variables`read)state_variables_written (list(StateVariable)): قائمة متغيرات الحالة المكتوبة (مجموعة فرعية من variables`written)
آخر تحديث للصفحة: 3 فبراير 2025

