كيفية استخدام سليذر للعثور على أخطاء العقود الذكية
كيفية استخدام سليذر
الهدف من هذا البرنامج التعليمي هو إظهار كيفية استخدام سليذر للعثور تلقائيًا على الأخطاء في العقود الذكية.
- التثبيت
- استخدام سطر الأوامر
- مقدمة في التحليل الثابت: مقدمة موجزة عن التحليل الثابت
- API: وصف API الخاص بلغة Python
التثبيت
يتطلب سليذر إصدار Python >= 3.6. يمكن تثبيته من خلال pip أو باستخدام Docker.
تثبيت سليذر من خلال pip:
pip3 install --user slither-analyzer
تثبيت سليذر من خلال Docker:
docker pull trailofbits/eth-security-toolbox
docker run -it -v "$PWD":/home/trufflecon trailofbits/eth-security-toolbox
يقوم الأمر الأخير بتشغيل eth-security-toolbox في Docker لديه حق الوصول إلى دليلك الحالي. يمكنك تغيير الملفات من مضيفك، وتشغيل الأدوات على الملفات من Docker
داخل Docker، قم بتشغيل:
solc-select 0.5.11
cd /home/trufflecon/
تشغيل برنامج نصي
لتشغيل برنامج نصي بلغة Python باستخدام Python 3:
python3 script.py
سطر الأوامر
سطر الأوامر مقابل البرامج النصية المعرفة من قبل المستخدم. يأتي سليذر مع مجموعة من أجهزة الكشف المحددة مسبقًا التي تعثر على العديد من الأخطاء الشائعة. سيؤدي استدعاء سليذر من سطر الأوامر إلى تشغيل جميع أجهزة الكشف، ولا حاجة إلى معرفة تفصيلية بالتحليل الثابت:
slither project_paths
بالإضافة إلى أجهزة الكشف، يتمتع سليذر بقدرات مراجعة التعليمات البرمجية من خلال الطابعات (printers) (opens in a new tab) والأدوات (opens in a new tab) الخاصة به.
استخدم crytic.io (opens in a new tab) للوصول إلى أجهزة الكشف الخاصة والتكامل مع GitHub.
التحليل الثابت
تم وصف قدرات وتصميم إطار عمل التحليل الثابت لسليذر في منشورات المدونة (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 عبارة عن شجرة منظمة حيث تحتوي كل ورقة عادةً على متغير أو ثابت، وتكون العقد الداخلية عبارة عن معاملات أو عمليات تدفق التحكم. ضع في اعتبارك التعليمات البرمجية التالية:
function safeAdd(uint a, uint b) pure internal returns(uint){
if(a + b <= a){
revert();
}
return a + b;
}
يظهر AST المقابل في:
يستخدم سليذر AST المُصدَّر بواسطة solc.
على الرغم من سهولة بنائه، إلا أن AST عبارة عن هيكل متداخل. في بعض الأحيان، لا يكون هذا هو الأسهل للتحليل. على سبيل المثال، لتحديد العمليات التي يستخدمها التعبير a + b <= a، يجب عليك أولاً تحليل <= ثم +. النهج الشائع هو استخدام ما يسمى بنمط الزائر (visitor pattern)، والذي يتنقل عبر الشجرة بشكل متكرر. يحتوي سليذر على زائر عام في ExpressionVisitor (opens in a new tab).
تستخدم التعليمات البرمجية التالية ExpressionVisitor لاكتشاف ما إذا كان التعبير يحتوي على عملية جمع:
from slither.visitors.expression.expression import ExpressionVisitor
from slither.core.expressions.binary_operation import BinaryOperationType
class HasAddition(ExpressionVisitor):
def result(self):
return self._result
def _post_binary_operation(self, expression):
if expression.type == BinaryOperationType.ADDITION:
self._result = True
visitor = HasAddition(expression) # التعبير هو التعبير المراد اختباره
print(f'The expression {expression} has a addition: {visitor.result()}')
رسم تدفق التحكم (CFG)
ثاني أكثر تمثيل للتعليمات البرمجية شيوعًا هو رسم تدفق التحكم (CFG). كما يوحي اسمه، فهو تمثيل قائم على الرسم البياني يعرض جميع مسارات التنفيذ. تحتوي كل عقدة على تعليمة واحدة أو عدة تعليمات. تمثل الحواف في الرسم البياني عمليات تدفق التحكم (if/then/else، loop، إلخ). إن CFG للمثال السابق هو:
إن CFG هو التمثيل الذي تُبنى عليه معظم التحليلات.
توجد العديد من تمثيلات التعليمات البرمجية الأخرى. لكل تمثيل مزايا وعيوب وفقًا للتحليل الذي تريد إجراؤه.
التحليل
أبسط أنواع التحليلات التي يمكنك إجراؤها باستخدام سليذر هي التحليلات النحوية.
التحليل النحوي
يمكن لسليذر التنقل عبر المكونات المختلفة للتعليمات البرمجية وتمثيلها للعثور على التناقضات والعيوب باستخدام نهج يشبه مطابقة الأنماط.
على سبيل المثال، تبحث أجهزة الكشف التالية عن المشكلات المتعلقة ببناء الجملة:
-
تظليل متغير الحالة (State variable shadowing) (opens in a new tab): يكرر عبر جميع متغيرات الحالة ويتحقق مما إذا كان أي منها يظلل متغيرًا من عقد موروث (state.py#L51-L62 (opens in a new tab))
-
واجهة ERC-20 غير صحيحة (opens in a new tab): يبحث عن تواقيع دوال ERC-20 غير الصحيحة (incorrect_erc20_interface.py#L34-L55 (opens in a new tab))
التحليل الدلالي
على عكس التحليل النحوي، سيتعمق التحليل الدلالي ويحلل "معنى" التعليمات البرمجية. تتضمن هذه العائلة بعض الأنواع الواسعة من التحليلات. إنها تؤدي إلى نتائج أكثر قوة وفائدة، ولكنها أيضًا أكثر تعقيدًا في الكتابة.
تُستخدم التحليلات الدلالية لاكتشافات الثغرات الأمنية الأكثر تقدمًا.
تحليل تبعية البيانات
يُقال إن المتغير variable_a يعتمد على بيانات variable_b إذا كان هناك مسار تتأثر فيه قيمة variable_a بـ variable_b.
في التعليمات البرمجية التالية، يعتمد variable_a على variable_b:
// ...
variable_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 ويتبع الحواف، فمن المحتمل أن ترى عقدًا تمت زيارتها بالفعل. على سبيل المثال، إذا تم تقديم حلقة كما هو موضح أدناه:
for(uint i; i < range; ++){
variable_a += 1
}
سيحتاج تحليلك إلى معرفة متى يتوقف. هناك استراتيجيتان رئيسيتان هنا: (1) التكرار على كل عقدة لعدد محدود من المرات، (2) حساب ما يسمى النقطة الثابتة (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) هو لغة يُقصد بها أن تكون أكثر قابلية للتحليل الثابت من اللغة الأصلية. يترجم سليذر لغة Solidity إلى IR الخاص به: SlithIR (opens in a new tab).
فهم SlithIR ليس ضروريًا إذا كنت تريد فقط كتابة فحوصات أساسية. ومع ذلك، سيكون مفيدًا إذا كنت تخطط لكتابة تحليلات دلالية متقدمة. ستساعدك طابعات SlithIR (opens in a new tab) وSSA (opens in a new tab) على فهم كيفية ترجمة التعليمات البرمجية.
أساسيات API
يحتوي سليذر على API يتيح لك استكشاف السمات الأساسية للعقد ودواله.
لتحميل قاعدة تعليمات برمجية:
from slither import Slither
slither = Slither('/path/to/project')
استكشاف العقود والدوال
يحتوي كائن 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): إرجاع مُعدِّل (Modifier) من توقيعهget_state_variable_from_name (str): إرجاع متغير حالة (StateVariable) من اسمه
يحتوي كائن Function أو Modifier على:
name (str): اسم الدالةcontract (contract): العقد الذي تم الإعلان عن الدالة فيهnodes (list(Node)): قائمة العقد التي يتكون منها CFG للدالة/المُعدِّلentry_point (Node): نقطة الدخول إلى CFGvariables_read (list(Variable)): قائمة المتغيرات المقروءةvariables_written (list(Variable)): قائمة المتغيرات المكتوبةstate_variables_read (list(StateVariable)): قائمة متغيرات الحالة المقروءة (مجموعة فرعية من المتغيرات المقروءة)state_variables_written (list(StateVariable)): قائمة متغيرات الحالة المكتوبة (مجموعة فرعية من المتغيرات المكتوبة)
آخر تحديث للصفحة: 15 أبريل 2026

