الهندسة العكسية لعقد
المقدمة
لا توجد أسرار على سلسلة الكتل، كل ما يحدث يكون متسقًا، وقابلاً للتحقق، ومتاحًا للجمهور. في الوضع المثالي، يجب أن تكون العقود قد نُشر كود المصدر الخاص بها وتم التحقق منه على Etherscan (opens in a new tab). ومع ذلك، ليس هذا هو الحال دائمًا (opens in a new tab). في هذه المقالة ستتعلم كيفية إجراء الهندسة العكسية للعقود من خلال النظر إلى عقد بدون كود المصدر، 0x2510c039cc3b061d79e564b38836da87e31b342f (opens in a new tab).
توجد مترجمات عكسية، لكنها لا تنتج دائمًا نتائج قابلة للاستخدام (opens in a new tab). في هذه المقالة ستتعلم كيفية إجراء الهندسة العكسية يدويًا وفهم عقد من خلال رموز التشغيل (opens in a new tab)، بالإضافة إلى كيفية تفسير نتائج المترجم العكسي.
لتتمكن من فهم هذه المقالة، يجب أن تكون على دراية بأساسيات EVM، وأن تكون ملمًا إلى حد ما بلغة التجميع الخاصة بـ EVM. يمكنك القراءة عن هذه المواضيع هنا (opens in a new tab).
إعداد الكود القابل للتنفيذ
يمكنك الحصول على رموز التشغيل بالانتقال إلى Etherscan للعقد، والنقر على علامة التبويب Contract ثم Switch to Opcodes View. ستحصل على عرض يحتوي على رمز تشغيل واحد في كل سطر.
ولكن لتتمكن من فهم القفزات، تحتاج إلى معرفة موقع كل رمز تشغيل في الكود. للقيام بذلك، إحدى الطرق هي فتح جدول بيانات Google ولصق رموز التشغيل في العمود C. يمكنك تخطي الخطوات التالية عن طريق إنشاء نسخة من جدول البيانات المُعد مسبقًا هذا (opens in a new tab).
الخطوة التالية هي الحصول على مواقع الكود الصحيحة حتى نتمكن من فهم القفزات. سنضع حجم رمز التشغيل في العمود B، والموقع (بالنظام السداسي عشري) في العمود A. اكتب هذه الدالة في الخلية B1 ثم انسخها والصقها لبقية العمود B، حتى نهاية الكود. بعد القيام بذلك، يمكنك إخفاء العمود B.
=1+IF(REGEXMATCH(C1,"PUSH"),REGEXEXTRACT(C1,"PUSH(\d+)"),0)
أولاً، تضيف هذه الدالة بايتًا واحدًا لرمز التشغيل نفسه، ثم تبحث عن PUSH. تعتبر رموز التشغيل PUSH خاصة لأنها تحتاج إلى بايتات إضافية للقيمة التي يتم دفعها. إذا كان رمز التشغيل هو PUSH، فإننا نستخرج عدد البايتات ونضيفه.
في A1 ضع الإزاحة الأولى، صفر. ثم، في A2، ضع هذه الدالة وانسخها والصقها مرة أخرى لبقية العمود A:
=dec2hex(hex2dec(A1)+B1)
نحتاج إلى هذه الدالة لتعطينا القيمة السداسية العشرية لأن القيم التي يتم دفعها قبل القفزات (JUMP و JUMPI) تُعطى لنا بالنظام السداسي عشري.
نقطة الدخول (0x00)
يتم تنفيذ العقود دائمًا من البايت الأول. هذا هو الجزء الأولي من الكود:
| الإزاحة | رمز التشغيل | المكدس (بعد رمز التشغيل) |
|---|---|---|
| 0 | PUSH1 0x80 | 0x80 |
| 2 | PUSH1 0x40 | 0x40, 0x80 |
| 4 | MSTORE | فارغ |
| 5 | PUSH1 0x04 | 0x04 |
| 7 | CALLDATASIZE | CALLDATASIZE 0x04 |
| 8 | LT | CALLDATASIZE<4 |
| 9 | PUSH2 0x005e | 0x5E CALLDATASIZE<4 |
| C | JUMPI | فارغ |
يقوم هذا الكود بشيئين:
- كتابة 0x80 كقيمة بحجم 32 byte في مواقع الذاكرة 0x40-0x5F (يتم تخزين 0x80 في 0x5F، وتكون المواقع 0x40-0x5E كلها أصفارًا).
- قراءة حجم بيانات الاستدعاء. عادةً ما تتبع بيانات الاستدعاء لعقد إيثريوم واجهة التطبيق الثنائية (ABI) (opens in a new tab)، والتي تتطلب كحد أدنى أربعة بايتات لمُحدد الدالة. إذا كان حجم بيانات الاستدعاء أقل من أربعة، انتقل إلى 0x5E.
المعالج عند 0x5E (لبيانات الاستدعاء غير التابعة لـ ABI)
| الإزاحة | رمز التشغيل |
|---|---|
| 5E | JUMPDEST |
| 5F | CALLDATASIZE |
| 60 | PUSH2 0x007c |
| 63 | JUMPI |
تبدأ هذه المقتطفة بـ JUMPDEST. تُلقي برامج آلة إيثريوم الافتراضية (EVM) استثناءً إذا انتقلت إلى رمز تشغيل ليس JUMPDEST. ثم تنظر إلى CALLDATASIZE، وإذا كانت "صحيحة" (أي ليست صفرًا) تنتقل إلى 0x7C. سنصل إلى ذلك أدناه.
| الإزاحة | رمز التشغيل | المكدس (بعد رمز التشغيل) |
|---|---|---|
| 64 | CALLVALUE | المقدمة بواسطة الاستدعاء. تُسمى msg.value في Solidity |
| 65 | PUSH1 0x06 | 6 CALLVALUE |
| 67 | PUSH1 0x00 | 0 6 CALLVALUE |
| 69 | DUP3 | CALLVALUE 0 6 CALLVALUE |
| 6A | DUP3 | 6 CALLVALUE 0 6 CALLVALUE |
| 6B | SLOAD | Storage[6] CALLVALUE 0 6 CALLVALUE |
لذا عندما لا توجد بيانات استدعاء، نقرأ قيمة Storage[6]. لا نعرف ما هي هذه القيمة بعد، ولكن يمكننا البحث عن المعاملات التي تلقاها العقد بدون بيانات استدعاء. المعاملات التي تقوم فقط بتحويل ETH بدون أي بيانات استدعاء (وبالتالي بدون طريقة) تظهر في Etherscan بالطريقة Transfer. في الواقع، أول معاملة تلقاها العقد على الإطلاق (opens in a new tab) هي تحويل.
إذا نظرنا في تلك المعاملة ونقرنا على Click to see More (انقر لرؤية المزيد)، نرى أن بيانات الاستدعاء، والتي تسمى بيانات الإدخال، فارغة بالفعل (0x). لاحظ أيضًا أن القيمة هي 1.559 ETH، وسيكون ذلك ذا صلة لاحقًا.
بعد ذلك، انقر فوق علامة التبويب State (الحالة) وقم بتوسيع العقد الذي نقوم بهندسته العكسية (0x2510...). يمكنك أن ترى أن Storage[6] قد تغيرت بالفعل أثناء المعاملة، وإذا قمت بتغيير Hex (النظام السداسي عشري) إلى Number (رقم)، فسترى أنها أصبحت 1,559,000,000,000,000,000، وهي القيمة المحولة بوحدة wei (أضفت الفواصل للتوضيح)، والتي تتوافق مع قيمة العقد التالية.
إذا نظرنا في تغييرات الحالة الناتجة عن معاملات Transfer أخرى من نفس الفترة (opens in a new tab)، نرى أن Storage[6] تتبعت قيمة العقد لفترة من الوقت. في الوقت الحالي سنسميها Value*. تذكرنا العلامة النجمية (*) بأننا لا نعرف ما يفعله هذا المتغير بعد، ولكن لا يمكن أن يكون مجرد تتبع لقيمة العقد لأنه لا توجد حاجة لاستخدام التخزين، وهو مكلف للغاية، عندما يمكنك الحصول على رصيد حساباتك باستخدام ADDRESS BALANCE. يدفع رمز التشغيل الأول عنوان العقد نفسه. ويقرأ الثاني العنوان الموجود في أعلى المكدس ويستبدله برصيد ذلك العنوان.
| الإزاحة | رمز التشغيل | المكدس |
|---|---|---|
| 6C | PUSH2 0x0075 | 0x75 Value* CALLVALUE 0 6 CALLVALUE |
| 6F | SWAP2 | CALLVALUE Value* 0x75 0 6 CALLVALUE |
| 70 | SWAP1 | Value* CALLVALUE 0x75 0 6 CALLVALUE |
| 71 | PUSH2 0x01a7 | 0x01A7 Value* CALLVALUE 0x75 0 6 CALLVALUE |
| 74 | JUMP |
سنستمر في تتبع هذا الكود عند وجهة الانتقال.
| الإزاحة | رمز التشغيل | المكدس |
|---|---|---|
| 1A7 | JUMPDEST | Value* CALLVALUE 0x75 0 6 CALLVALUE |
| 1A8 | PUSH1 0x00 | 0x00 Value* CALLVALUE 0x75 0 6 CALLVALUE |
| 1AA | DUP3 | CALLVALUE 0x00 Value* CALLVALUE 0x75 0 6 CALLVALUE |
| 1AB | NOT | 2^256-CALLVALUE-1 0x00 Value* CALLVALUE 0x75 0 6 CALLVALUE |
العملية NOT هي عملية على مستوى البت (bitwise)، لذا فهي تعكس قيمة كل بت في قيمة الاستدعاء.
| الإزاحة | رمز التشغيل | المكدس |
|---|---|---|
| 1AC | DUP3 | Value* 2^256-CALLVALUE-1 0x00 Value* CALLVALUE 0x75 0 6 CALLVALUE |
| 1AD | GT | Value*>2^256-CALLVALUE-1 0x00 Value* CALLVALUE 0x75 0 6 CALLVALUE |
| 1AE | ISZERO | Value*<=2^256-CALLVALUE-1 0x00 Value* CALLVALUE 0x75 0 6 CALLVALUE |
| 1AF | PUSH2 0x01df | 0x01DF Value*<=2^256-CALLVALUE-1 0x00 Value* CALLVALUE 0x75 0 6 CALLVALUE |
| 1B2 | JUMPI |
ننتقل إذا كانت Value* أصغر من 2^256-CALLVALUE-1 أو تساويها. يبدو هذا كمنطق لمنع تجاوز السعة (overflow). وبالفعل، نرى أنه بعد بضع عمليات غير منطقية (الكتابة في الذاكرة على وشك أن تُحذف، على سبيل المثال) عند الإزاحة 0x01DE، يتراجع العقد إذا تم اكتشاف تجاوز السعة، وهو سلوك طبيعي.
لاحظ أن حدوث مثل هذا التجاوز في السعة مستبعد للغاية، لأنه سيتطلب أن تكون قيمة الاستدعاء بالإضافة إلى Value* قابلة للمقارنة بـ 2^256 wei، أي حوالي 10^59 ETH. إجمالي المعروض من ETH، وقت الكتابة، أقل من مائتي مليون (opens in a new tab).
| الإزاحة | رمز التشغيل | المكدس |
|---|---|---|
| 1DF | JUMPDEST | 0x00 Value* CALLVALUE 0x75 0 6 CALLVALUE |
| 1E0 | POP | Value* CALLVALUE 0x75 0 6 CALLVALUE |
| 1E1 | ADD | Value*+CALLVALUE 0x75 0 6 CALLVALUE |
| 1E2 | SWAP1 | 0x75 Value*+CALLVALUE 0 6 CALLVALUE |
| 1E3 | JUMP |
إذا وصلنا إلى هنا، احصل على Value* + CALLVALUE وانتقل إلى الإزاحة 0x75.
| الإزاحة | رمز التشغيل | المكدس |
|---|---|---|
| 75 | JUMPDEST | Value*+CALLVALUE 0 6 CALLVALUE |
| 76 | SWAP1 | 0 Value*+CALLVALUE 6 CALLVALUE |
| 77 | SWAP2 | 6 Value*+CALLVALUE 0 CALLVALUE |
| 78 | SSTORE | 0 CALLVALUE |
إذا وصلنا إلى هنا (وهو ما يتطلب أن تكون بيانات الاستدعاء فارغة) فإننا نضيف إلى Value* قيمة الاستدعاء. هذا يتوافق مع ما نقوله عن ما تفعله معاملات Transfer.
| الإزاحة | رمز التشغيل |
|---|---|
| 79 | POP |
| 7A | POP |
| 7B | STOP |
أخيرًا، قم بمسح المكدس (وهو أمر غير ضروري) وأشر إلى النهاية الناجحة للمعاملة.
لتلخيص كل ذلك، إليك مخطط انسيابي للكود الأولي.
المعالج عند 0x7C
لم أضع عمدًا في العنوان ما يفعله هذا المعالج. الهدف ليس تعليمك كيف يعمل هذا العقد المحدد، بل كيفية الهندسة العكسية للعقود. ستتعلم ما يفعله بنفس الطريقة التي تعلمت بها، من خلال تتبع الكود.
نصل إلى هنا من عدة أماكن:
- إذا كانت هناك بيانات استدعاء بحجم 1 أو 2 أو 3 بايت (من الإزاحة 0x63)
- إذا كان توقيع الطريقة غير معروف (من الإزاحات 0x42 و 0x5D)
| الإزاحة | رمز التشغيل | المكدس |
|---|---|---|
| 7C | JUMPDEST | |
| 7D | PUSH1 0x00 | 0x00 |
| 7F | PUSH2 0x009d | 0x9D 0x00 |
| 82 | PUSH1 0x03 | 0x03 0x9D 0x00 |
| 84 | SLOAD | Storage[3] 0x9D 0x00 |
هذه خلية تخزين أخرى، لم أتمكن من العثور عليها في أي معاملات، لذا من الصعب معرفة ما تعنيه. الكود أدناه سيوضح ذلك.
| الإزاحة | رمز التشغيل | المكدس |
|---|---|---|
| 85 | PUSH20 0xffffffffffffffffffffffffffffffffffffffff | 0xff....ff Storage[3] 0x9D 0x00 |
| 9A | AND | Storage[3]-as-address 0x9D 0x00 |
تقوم رموز التشغيل هذه باقتطاع القيمة التي قرأناها من Storage[3] إلى 160 بت، وهو طول عنوان إيثريوم.
| الإزاحة | رمز التشغيل | المكدس |
|---|---|---|
| 9B | SWAP1 | 0x9D Storage[3]-as-address 0x00 |
| 9C | JUMP | Storage[3]-as-address 0x00 |
هذه القفزة زائدة عن الحاجة، لأننا سننتقل إلى رمز التشغيل التالي. هذا الكود ليس فعالاً من حيث استهلاك الغاز كما ينبغي أن يكون.
| الإزاحة | رمز التشغيل | المكدس |
|---|---|---|
| 9D | JUMPDEST | Storage[3]-as-address 0x00 |
| 9E | SWAP1 | 0x00 Storage[3]-as-address |
| 9F | POP | Storage[3]-as-address |
| A0 | PUSH1 0x40 | 0x40 Storage[3]-as-address |
| A2 | MLOAD | Mem[0x40] Storage[3]-as-address |
في بداية الكود تمامًا، قمنا بتعيين Mem[0x40] إلى 0x80. إذا بحثنا عن 0x40 لاحقًا، فسنرى أننا لم نقم بتغييره - لذا يمكننا افتراض أنه 0x80.
| الإزاحة | رمز التشغيل | المكدس |
|---|---|---|
| A3 | CALLDATASIZE | CALLDATASIZE 0x80 Storage[3]-as-address |
| A4 | PUSH1 0x00 | 0x00 CALLDATASIZE 0x80 Storage[3]-as-address |
| A6 | DUP3 | 0x80 0x00 CALLDATASIZE 0x80 Storage[3]-as-address |
| A7 | CALLDATACOPY | 0x80 Storage[3]-as-address |
انسخ جميع بيانات الاستدعاء إلى الذاكرة، بدءًا من 0x80.
| الإزاحة | رمز التشغيل | المكدس |
|---|---|---|
| A8 | PUSH1 0x00 | 0x00 0x80 Storage[3]-as-address |
| AA | DUP1 | 0x00 0x00 0x80 Storage[3]-as-address |
| AB | CALLDATASIZE | CALLDATASIZE 0x00 0x00 0x80 Storage[3]-as-address |
| AC | DUP4 | 0x80 CALLDATASIZE 0x00 0x00 0x80 Storage[3]-as-address |
| AD | DUP6 | Storage[3]-as-address 0x80 CALLDATASIZE 0x00 0x00 0x80 Storage[3]-as-address |
| AE | GAS | GAS Storage[3]-as-address 0x80 CALLDATASIZE 0x00 0x00 0x80 Storage[3]-as-address |
| AF | DELEGATE_CALL |
الآن أصبحت الأمور أكثر وضوحًا. يمكن أن يعمل هذا العقد كـ وكيل (opens in a new tab)، حيث يستدعي العنوان الموجود في Storage[3] للقيام بالعمل الحقيقي. يستدعي DELEGATE_CALL عقدًا منفصلاً، ولكنه يبقى في نفس مساحة التخزين. هذا يعني أن العقد المفوض، الذي نعمل كوكيل له، يصل إلى نفس مساحة التخزين. معلمات الاستدعاء هي:
- الغاز: كل الغاز المتبقي
- العنوان المستدعى: Storage[3]-as-address
- بيانات الاستدعاء: بايتات CALLDATASIZE بدءًا من 0x80، وهو المكان الذي وضعنا فيه بيانات الاستدعاء الأصلية
- بيانات الإرجاع: لا شيء (0x00 - 0x00) سنحصل على بيانات الإرجاع بوسائل أخرى (انظر أدناه)
| الإزاحة | رمز التشغيل | المكدس |
|---|---|---|
| B0 | RETURNDATASIZE | RETURNDATASIZE (((call success/failure))) 0x80 Storage[3]-as-address |
| B1 | DUP1 | RETURNDATASIZE RETURNDATASIZE (((call success/failure))) 0x80 Storage[3]-as-address |
| B2 | PUSH1 0x00 | 0x00 RETURNDATASIZE RETURNDATASIZE (((call success/failure))) 0x80 Storage[3]-as-address |
| B4 | DUP5 | 0x80 0x00 RETURNDATASIZE RETURNDATASIZE (((call success/failure))) 0x80 Storage[3]-as-address |
| B5 | RETURNDATACOPY | RETURNDATASIZE (((call success/failure))) 0x80 Storage[3]-as-address |
هنا نقوم بنسخ جميع بيانات الإرجاع إلى المخزن المؤقت للذاكرة بدءًا من 0x80.
| الإزاحة | رمز التشغيل | المكدس |
|---|---|---|
| B6 | DUP2 | (((call success/failure))) RETURNDATASIZE (((call success/failure))) 0x80 Storage[3]-as-address |
| B7 | DUP1 | (((call success/failure))) (((call success/failure))) RETURNDATASIZE (((call success/failure))) 0x80 Storage[3]-as-address |
| B8 | ISZERO | (((did the call fail))) (((call success/failure))) RETURNDATASIZE (((call success/failure))) 0x80 Storage[3]-as-address |
| B9 | PUSH2 0x00c0 | 0xC0 (((did the call fail))) (((call success/failure))) RETURNDATASIZE (((call success/failure))) 0x80 Storage[3]-as-address |
| BC | JUMPI | (((call success/failure))) RETURNDATASIZE (((call success/failure))) 0x80 Storage[3]-as-address |
| BD | DUP2 | RETURNDATASIZE (((call success/failure))) RETURNDATASIZE (((call success/failure))) 0x80 Storage[3]-as-address |
| BE | DUP5 | 0x80 RETURNDATASIZE (((call success/failure))) RETURNDATASIZE (((call success/failure))) 0x80 Storage[3]-as-address |
| BF | RETURN |
لذا بعد الاستدعاء، نقوم بنسخ بيانات الإرجاع إلى المخزن المؤقت 0x80 - 0x80+RETURNDATASIZE، وإذا كان الاستدعاء ناجحًا، فإننا نقوم بـ RETURN باستخدام هذا المخزن المؤقت بالضبط.
فشل DELEGATECALL
إذا وصلنا إلى هنا، إلى 0xC0، فهذا يعني أن العقد الذي استدعيناه قد تراجع. وبما أننا مجرد وكيل لهذا العقد، فإننا نريد إرجاع نفس البيانات والتراجع أيضًا.
| الإزاحة | رمز التشغيل | المكدس |
|---|---|---|
| C0 | JUMPDEST | (((call success/failure))) RETURNDATASIZE (((call success/failure))) 0x80 Storage[3]-as-address |
| C1 | DUP2 | RETURNDATASIZE (((call success/failure))) RETURNDATASIZE (((call success/failure))) 0x80 Storage[3]-as-address |
| C2 | DUP5 | 0x80 RETURNDATASIZE (((call success/failure))) RETURNDATASIZE (((call success/failure))) 0x80 Storage[3]-as-address |
| C3 | REVERT |
لذا نقوم بـ REVERT بنفس المخزن المؤقت الذي استخدمناه لـ RETURN سابقًا: 0x80 - 0x80+RETURNDATASIZE
استدعاءات ABI
إذا كان حجم بيانات الاستدعاء أربعة بايتات أو أكثر، فقد يكون هذا استدعاء ABI صالحًا.
| الإزاحة | رمز التشغيل | المكدس |
|---|---|---|
| D | PUSH1 0x00 | 0x00 |
| F | CALLDATALOAD | (((الكلمة الأولى (256 بت) من بيانات الاستدعاء))) |
| 10 | PUSH1 0xe0 | 0xE0 (((الكلمة الأولى (256 بت) من بيانات الاستدعاء))) |
| 12 | SHR | (((أول 32 بت (4 بايتات) من بيانات الاستدعاء))) |
يخبرنا Etherscan أن 1C هو رمز تشغيل غير معروف، لأنه تمت إضافته بعد أن برمج Etherscan هذه الميزة (opens in a new tab) ولم يقوموا بتحديثها. يوضح لنا جدول رموز التشغيل المُحدَّث (opens in a new tab) أن هذا يعني إزاحة لليمين.
| الإزاحة | رمز التشغيل | المكدس |
|---|---|---|
| 13 | DUP1 | (((أول 32 بت (4 بايتات) من بيانات الاستدعاء))) (((أول 32 بت (4 بايتات) من بيانات الاستدعاء))) |
| 14 | PUSH4 0x3cd8045e | 0x3CD8045E (((أول 32 بت (4 بايتات) من بيانات الاستدعاء))) (((أول 32 بت (4 بايتات) من بيانات الاستدعاء))) |
| 19 | GT | 0x3CD8045E>first-32-bits-of-the-call-data (((أول 32 بت (4 بايتات) من بيانات الاستدعاء))) |
| 1A | PUSH2 0x0043 | 0x43 0x3CD8045E>first-32-bits-of-the-call-data (((أول 32 بت (4 بايتات) من بيانات الاستدعاء))) |
| 1D | JUMPI | (((أول 32 بت (4 بايتات) من بيانات الاستدعاء))) |
من خلال تقسيم اختبارات مطابقة توقيع الطريقة إلى قسمين بهذا الشكل، يتم توفير نصف الاختبارات في المتوسط. يتبع الكود الذي يلي هذا مباشرة والكود الموجود في 0x43 نفس النمط: DUP1 أول 32 بت من بيانات الاستدعاء، PUSH4 (((method signature>، ثم تشغيل EQ للتحقق من التطابق، وبعد ذلك JUMPI إذا كان توقيع الطريقة متطابقًا. إليك تواقيع الطرق، وعناوينها، وتعريف الطريقة المقابل (opens in a new tab) إذا كان معروفًا:
| الطريقة | توقيع الطريقة | الإزاحة للانتقال إليها |
|---|---|---|
| splitter() (opens in a new tab) | 0x3cd8045e | 0x0103 |
| ??? | 0x81e580d3 | 0x0138 |
| currentWindow() (opens in a new tab) | 0xba0bafb4 | 0x0158 |
| ??? | 0x1f135823 | 0x00C4 |
| merkleRoot() (opens in a new tab) | 0x2eb4a7ab | 0x00ED |
إذا لم يتم العثور على تطابق، ينتقل الكود إلى معالج الوكيل عند 0x7C، على أمل أن يكون لدى العقد الذي نعمل كوكيل له تطابق.
splitter()
| الإزاحة | رمز التشغيل | المكدس |
|---|---|---|
| 103 | JUMPDEST | |
| 104 | CALLVALUE | CALLVALUE |
| 105 | DUP1 | CALLVALUE CALLVALUE |
| 106 | ISZERO | CALLVALUE==0 CALLVALUE |
| 107 | PUSH2 0x010f | 0x010F CALLVALUE==0 CALLVALUE |
| 10A | JUMPI | CALLVALUE |
| 10B | PUSH1 0x00 | 0x00 CALLVALUE |
| 10D | DUP1 | 0x00 0x00 CALLVALUE |
| 10E | REVERT |
أول شيء تفعله هذه الدالة هو التحقق من أن الاستدعاء لم يرسل أي ETH. هذه الدالة ليست payable (opens in a new tab). إذا أرسل لنا شخص ما ETH، فلا بد أن يكون ذلك خطأ ونريد التراجع (REVERT) لتجنب بقاء ETH في مكان لا يمكنهم استرداده منه.
| الإزاحة | رمز التشغيل | المكدس |
|---|---|---|
| 10F | JUMPDEST | |
| 110 | POP | |
| 111 | PUSH1 0x03 | 0x03 |
| 113 | SLOAD | (((Storage[3] المعروف أيضًا باسم العقد الذي نعتبر عقد وكيل له))) |
| 114 | PUSH1 0x40 | 0x40 (((Storage[3] المعروف أيضًا باسم العقد الذي نعتبر عقد وكيل له))) |
| 116 | MLOAD | 0x80 (((Storage[3] المعروف أيضًا باسم العقد الذي نعتبر عقد وكيل له))) |
| 117 | PUSH20 0xffffffffffffffffffffffffffffffffffffffff | 0xFF...FF 0x80 (((Storage[3] المعروف أيضًا باسم العقد الذي نعتبر عقد وكيل له))) |
| 12C | SWAP1 | 0x80 0xFF...FF (((Storage[3] المعروف أيضًا باسم العقد الذي نعتبر عقد وكيل له))) |
| 12D | SWAP2 | (((Storage[3] المعروف أيضًا باسم العقد الذي نعتبر عقد وكيل له))) 0xFF...FF 0x80 |
| 12E | AND | ProxyAddr 0x80 |
| 12F | DUP2 | 0x80 ProxyAddr 0x80 |
| 130 | MSTORE | 0x80 |
ويحتوي 0x80 الآن على عنوان الوكيل
| الإزاحة | رمز التشغيل | المكدس |
|---|---|---|
| 131 | PUSH1 0x20 | 0x20 0x80 |
| 133 | ADD | 0xA0 |
| 134 | PUSH2 0x00e4 | 0xE4 0xA0 |
| 137 | JUMP | 0xA0 |
كود E4
هذه هي المرة الأولى التي نرى فيها هذه الأسطر، لكنها مشتركة مع طرق أخرى (انظر أدناه). لذلك سنطلق على القيمة في المكدس اسم X، وتذكر فقط أنه في splitter() تكون قيمة X هذه هي 0xA0.
| الإزاحة | رمز التشغيل | المكدس |
|---|---|---|
| E4 | JUMPDEST | X |
| E5 | PUSH1 0x40 | 0x40 X |
| E7 | MLOAD | 0x80 X |
| E8 | DUP1 | 0x80 0x80 X |
| E9 | SWAP2 | X 0x80 0x80 |
| EA | SUB | X-0x80 0x80 |
| EB | SWAP1 | 0x80 X-0x80 |
| EC | RETURN |
إذن، يتلقى هذا الكود مؤشر ذاكرة في المكدس (X)، ويجعل العقد ينفذ RETURN مع مخزن مؤقت بحجم 0x80 - X.
في حالة splitter()، يُرجع هذا العنوان الذي نعتبر عقد وكيل له. يُرجع RETURN المخزن المؤقت في النطاق 0x80-0x9F، وهو المكان الذي كتبنا فيه هذه البيانات (الإزاحة 0x130 أعلاه).
currentWindow()
الكود في الإزاحات 0x158-0x163 مطابق لما رأيناه في 0x103-0x10E في splitter() (باستثناء وجهة JUMPI)، لذلك نعلم أن currentWindow() ليس أيضًا payable.
| الإزاحة | رمز التشغيل | المكدس |
|---|---|---|
| 164 | JUMPDEST | |
| 165 | POP | |
| 166 | PUSH2 0x00da | 0xDA |
| 169 | PUSH1 0x01 | 0x01 0xDA |
| 16B | SLOAD | Storage[1] 0xDA |
| 16C | DUP2 | 0xDA Storage[1] 0xDA |
| 16D | JUMP | Storage[1] 0xDA |
كود DA
هذا الكود مشترك أيضًا مع دوال أخرى. لذلك سنسمي القيمة في المكدس Y، ونتذكر فقط أنه في currentWindow() قيمة Y هذه هي Storage[1].
| الإزاحة | رمز التشغيل | المكدس |
|---|---|---|
| DA | JUMPDEST | Y 0xDA |
| DB | PUSH1 0x40 | 0x40 Y 0xDA |
| DD | MLOAD | 0x80 Y 0xDA |
| DE | SWAP1 | Y 0x80 0xDA |
| DF | DUP2 | 0x80 Y 0x80 0xDA |
| E0 | MSTORE | 0x80 0xDA |
اكتب Y في 0x80-0x9F.
| الإزاحة | رمز التشغيل | المكدس |
|---|---|---|
| E1 | PUSH1 0x20 | 0x20 0x80 0xDA |
| E3 | ADD | 0xA0 0xDA |
والباقي مشروح بالفعل أعلاه. لذا فإن القفزات إلى 0xDA تكتب أعلى المكدس (Y) في 0x80-0x9F، وتُرجع تلك القيمة. في حالة currentWindow()، فإنها تُرجع Storage[1].
merkleRoot()
الكود في الإزاحات 0xED-0xF8 مطابق لما رأيناه في 0x103-0x10E في splitter() (باستثناء وجهة JUMPI)، لذا نعلم أن merkleRoot() ليس أيضًا payable.
| الإزاحة | رمز التشغيل | المكدس |
|---|---|---|
| F9 | JUMPDEST | |
| FA | POP | |
| FB | PUSH2 0x00da | 0xDA |
| FE | PUSH1 0x00 | 0x00 0xDA |
| 100 | SLOAD | Storage[0] 0xDA |
| 101 | DUP2 | 0xDA Storage[0] 0xDA |
| 102 | JUMP | Storage[0] 0xDA |
ما يحدث بعد القفزة اكتشفناه بالفعل. لذا يُرجع merkleRoot() القيمة Storage[0].
0x81e580d3
الرمز في الإزاحات 0x138-0x143 مطابق لما رأيناه في 0x103-0x10E في splitter() (باستثناء الوجهة JUMPI)، لذلك نعلم أن هذه الدالة ليست payable أيضًا.
| الإزاحة | رمز التشغيل | المكدس |
|---|---|---|
| 144 | JUMPDEST | |
| 145 | POP | |
| 146 | PUSH2 0x00da | 0xDA |
| 149 | PUSH2 0x0153 | 0x0153 0xDA |
| 14C | CALLDATASIZE | CALLDATASIZE 0x0153 0xDA |
| 14D | PUSH1 0x04 | 0x04 CALLDATASIZE 0x0153 0xDA |
| 14F | PUSH2 0x018f | 0x018F 0x04 CALLDATASIZE 0x0153 0xDA |
| 152 | JUMP | 0x04 CALLDATASIZE 0x0153 0xDA |
| 18F | JUMPDEST | 0x04 CALLDATASIZE 0x0153 0xDA |
| 190 | PUSH1 0x00 | 0x00 0x04 CALLDATASIZE 0x0153 0xDA |
| 192 | PUSH1 0x20 | 0x20 0x00 0x04 CALLDATASIZE 0x0153 0xDA |
| 194 | DUP3 | 0x04 0x20 0x00 0x04 CALLDATASIZE 0x0153 0xDA |
| 195 | DUP5 | CALLDATASIZE 0x04 0x20 0x00 0x04 CALLDATASIZE 0x0153 0xDA |
| 196 | SUB | CALLDATASIZE-4 0x20 0x00 0x04 CALLDATASIZE 0x0153 0xDA |
| 197 | SLT | CALLDATASIZE-4<32 0x00 0x04 CALLDATASIZE 0x0153 0xDA |
| 198 | ISZERO | CALLDATASIZE-4>=32 0x00 0x04 CALLDATASIZE 0x0153 0xDA |
| 199 | PUSH2 0x01a0 | 0x01A0 CALLDATASIZE-4>=32 0x00 0x04 CALLDATASIZE 0x0153 0xDA |
| 19C | JUMPI | 0x00 0x04 CALLDATASIZE 0x0153 0xDA |
يبدو أن هذه الدالة تأخذ 32 بايت (كلمة واحدة) على الأقل من بيانات الاستدعاء.
| الإزاحة | رمز التشغيل | المكدس |
|---|---|---|
| 19D | DUP1 | 0x00 0x00 0x04 CALLDATASIZE 0x0153 0xDA |
| 19E | DUP2 | 0x00 0x00 0x00 0x04 CALLDATASIZE 0x0153 0xDA |
| 19F | REVERT |
إذا لم تحصل على بيانات الاستدعاء، تتراجع المعاملة دون أي بيانات إرجاع.
دعونا نرى ما يحدث إذا حصلت الدالة بالفعل على بيانات الاستدعاء التي تحتاجها.
| الإزاحة | رمز التشغيل | المكدس |
|---|---|---|
| 1A0 | JUMPDEST | 0x00 0x04 CALLDATASIZE 0x0153 0xDA |
| 1A1 | POP | 0x04 CALLDATASIZE 0x0153 0xDA |
| 1A2 | CALLDATALOAD | calldataload(4) CALLDATASIZE 0x0153 0xDA |
calldataload(4) هي الكلمة الأولى من بيانات الاستدعاء بعد توقيع الطريقة
| الإزاحة | رمز التشغيل | المكدس |
|---|---|---|
| 1A3 | SWAP2 | 0x0153 CALLDATASIZE calldataload(4) 0xDA |
| 1A4 | SWAP1 | CALLDATASIZE 0x0153 calldataload(4) 0xDA |
| 1A5 | POP | 0x0153 calldataload(4) 0xDA |
| 1A6 | JUMP | calldataload(4) 0xDA |
| 153 | JUMPDEST | calldataload(4) 0xDA |
| 154 | PUSH2 0x016e | 0x016E calldataload(4) 0xDA |
| 157 | JUMP | calldataload(4) 0xDA |
| 16E | JUMPDEST | calldataload(4) 0xDA |
| 16F | PUSH1 0x04 | 0x04 calldataload(4) 0xDA |
| 171 | DUP2 | calldataload(4) 0x04 calldataload(4) 0xDA |
| 172 | DUP2 | 0x04 calldataload(4) 0x04 calldataload(4) 0xDA |
| 173 | SLOAD | Storage[4] calldataload(4) 0x04 calldataload(4) 0xDA |
| 174 | DUP2 | calldataload(4) Storage[4] calldataload(4) 0x04 calldataload(4) 0xDA |
| 175 | LT | calldataload(4)<Storage[4] calldataload(4) 0x04 calldataload(4) 0xDA |
| 176 | PUSH2 0x017e | 0x017EC calldataload(4)<Storage[4] calldataload(4) 0x04 calldataload(4) 0xDA |
| 179 | JUMPI | calldataload(4) 0x04 calldataload(4) 0xDA |
إذا لم تكن الكلمة الأولى أقل من Storage[4]، تفشل الدالة. وتتراجع دون أي قيمة مُرجعة:
| الإزاحة | رمز التشغيل | المكدس |
|---|---|---|
| 17A | PUSH1 0x00 | 0x00 ... |
| 17C | DUP1 | 0x00 0x00 ... |
| 17D | REVERT |
إذا كانت calldataload(4) أقل من Storage[4]، نحصل على هذا الرمز:
| الإزاحة | رمز التشغيل | المكدس |
|---|---|---|
| 17E | JUMPDEST | calldataload(4) 0x04 calldataload(4) 0xDA |
| 17F | PUSH1 0x00 | 0x00 calldataload(4) 0x04 calldataload(4) 0xDA |
| 181 | SWAP2 | 0x04 calldataload(4) 0x00 calldataload(4) 0xDA |
| 182 | DUP3 | 0x00 0x04 calldataload(4) 0x00 calldataload(4) 0xDA |
| 183 | MSTORE | calldataload(4) 0x00 calldataload(4) 0xDA |
وتحتوي مواقع الذاكرة 0x00-0x1F الآن على البيانات 0x04 (جميع 0x00-0x1E أصفار، و 0x1F هي أربعة)
| الإزاحة | رمز التشغيل | المكدس |
|---|---|---|
| 184 | PUSH1 0x20 | 0x20 calldataload(4) 0x00 calldataload(4) 0xDA |
| 186 | SWAP1 | calldataload(4) 0x20 0x00 calldataload(4) 0xDA |
| 187 | SWAP2 | 0x00 0x20 calldataload(4) calldataload(4) 0xDA |
| 188 | SHA3 | (((SHA3 of 0x00-0x1F))) calldataload(4) calldataload(4) 0xDA |
| 189 | ADD | (((SHA3 of 0x00-0x1F)))+calldataload(4) calldataload(4) 0xDA |
| 18A | SLOAD | Storage[(((SHA3 of 0x00-0x1F))) + calldataload(4)] calldataload(4) 0xDA |
لذا يوجد جدول بحث في التخزين، والذي يبدأ عند SHA3 لـ 0x000...0004 ويحتوي على إدخال لكل قيمة بيانات استدعاء مشروعة (القيمة أقل من Storage[4]).
| الإزاحة | رمز التشغيل | المكدس |
|---|---|---|
| 18B | SWAP1 | calldataload(4) Storage[(((SHA3 of 0x00-0x1F))) + calldataload(4)] 0xDA |
| 18C | POP | Storage[(((SHA3 of 0x00-0x1F))) + calldataload(4)] 0xDA |
| 18D | DUP2 | 0xDA Storage[(((SHA3 of 0x00-0x1F))) + calldataload(4)] 0xDA |
| 18E | JUMP | Storage[(((SHA3 of 0x00-0x1F))) + calldataload(4)] 0xDA |
نحن نعلم بالفعل ما يفعله الرمز عند الإزاحة 0xDA، فهو يُرجع القيمة العليا للمكدس إلى المستدعي. لذا تُرجع هذه الدالة القيمة من جدول البحث إلى المستدعي.
0x1f135823
الكود في الإزاحات 0xC4-0xCF مطابق لما رأيناه في 0x103-0x10E في splitter() (باستثناء الوجهة JUMPI)، لذلك نعلم أن هذه الدالة ليست payable أيضًا.
| الإزاحة | رمز التشغيل | المكدس |
|---|---|---|
| D0 | JUMPDEST | |
| D1 | POP | |
| D2 | PUSH2 0x00da | 0xDA |
| D5 | PUSH1 0x06 | 0x06 0xDA |
| D7 | SLOAD | Value* 0xDA |
| D8 | DUP2 | 0xDA Value* 0xDA |
| D9 | JUMP | Value* 0xDA |
نحن نعلم بالفعل ما يفعله الكود عند الإزاحة 0xDA، فهو يُرجع القيمة العليا في المكدس إلى المستدعي. لذلك تُرجع هذه الدالة Value*.
ملخص الطرق
هل تشعر أنك تفهم العقد في هذه المرحلة؟ أنا لا أشعر بذلك. حتى الآن لدينا هذه الطرق:
| الطريقة | المعنى |
|---|---|
| تحويل | قبول القيمة المقدمة بواسطة الاستدعاء وزيادة Value* بهذا المقدار |
| splitter() | إرجاع Storage[3]، عنوان العقد الوكيل |
| currentWindow() | إرجاع Storage[1] |
| merkleRoot() | إرجاع Storage[0] |
| 0x81e580d3 | إرجاع القيمة من جدول بحث، بشرط أن تكون المعلمة أقل من Storage[4] |
| 0x1f135823 | إرجاع Storage[6]، والمعروف أيضًا باسم Value* |
لكننا نعلم أن أي وظيفة أخرى يتم توفيرها بواسطة العقد في Storage[3]. ربما إذا عرفنا ما هو هذا العقد، فسيعطينا ذلك دليلًا. لحسن الحظ، هذه هي سلسلة الكتل وكل شيء معروف، على الأقل نظريًا. لم نر أي طرق تقوم بتعيين Storage[3]، لذلك لابد أنه تم تعيينه بواسطة المُنشئ.
المُنشئ
عندما ننظر إلى عقد (opens in a new tab)، يمكننا أيضًا رؤية المعاملة التي أنشأته.
إذا نقرنا على تلك المعاملة، ثم على علامة التبويب الحالة، يمكننا رؤية القيم الأولية للمعلمات. على وجه التحديد، يمكننا أن نرى أن Storage[3] يحتوي على 0x2f81e57ff4f4d83b40a9f719fd892d8e806e0761 (opens in a new tab). يجب أن يحتوي ذلك العقد على الوظيفة المفقودة. يمكننا فهمه باستخدام نفس الأدوات التي استخدمناها للعقد الذي نحقق فيه.
العقد الوكيل
باستخدام نفس التقنيات التي استخدمناها للعقد الأصلي أعلاه، يمكننا أن نرى أن العقد يتراجع في الحالات التالية:
- إذا كان هناك أي ETH مرفق بالاستدعاء (0x05-0x0F)
- إذا كان حجم بيانات الاستدعاء أقل من أربعة (0x10-0x19 و 0xBE-0xC2)
وأن الطرق التي يدعمها هي:
| الطريقة | توقيع الطريقة | إزاحة القفز إليها |
|---|---|---|
| scaleAmountByPercentage(uint256,uint256) (opens in a new tab) | 0x8ffb5c97 | 0x0135 |
| isClaimed(uint256,address) (opens in a new tab) | 0xd2ef0795 | 0x0151 |
| claim(uint256,address,uint256,bytes32[]) (opens in a new tab) | 0x2e7ba6ef | 0x00F4 |
| incrementWindow() (opens in a new tab) | 0x338b1d31 | 0x0110 |
| ??? | 0x3f26479e | 0x0118 |
| ??? | 0x1e7df9d3 | 0x00C3 |
| currentWindow() (opens in a new tab) | 0xba0bafb4 | 0x0148 |
| merkleRoot() (opens in a new tab) | 0x2eb4a7ab | 0x0107 |
| ??? | 0x81e580d3 | 0x0122 |
| ??? | 0x1f135823 | 0x00D8 |
يمكننا تجاهل الطرق الأربع السفلية لأننا لن نصل إليها أبدًا. تواقيعها تجعل العقد الأصلي الخاص بنا يتولى أمرها بنفسه (يمكنك النقر على التواقيع لرؤية التفاصيل أعلاه)، لذا يجب أن تكون طرقًا تم تجاوزها (opens in a new tab).
إحدى الطرق المتبقية هي claim(<params>)، وأخرى هي isClaimed(<params>)، لذا يبدو أنه عقد إسقاط جوي. بدلاً من المرور عبر الباقي رمز تشغيل برمز تشغيل، يمكننا تجربة أداة فك التجميع (opens in a new tab)، والتي تنتج نتائج قابلة للاستخدام لثلاث دوال من هذا العقد. تُترك الهندسة العكسية للدوال الأخرى كتمرين للقارئ.
scaleAmountByPercentage
هذا ما تقدمه لنا أداة فك التجميع لهذه الدالة:
def unknown8ffb5c97(uint256 _param1, uint256 _param2) payable:
require calldata.size - 4 >=′ 64
if _param1 and _param2 > -1 / _param1:
revert with 0, 17
return (_param1 * _param2 / 100 * 10^6)
يختبر require الأول أن بيانات الاستدعاء تحتوي، بالإضافة إلى البايتات الأربعة لتوقيع الدالة، على 64 بايت على الأقل، وهو ما يكفي للمعلمتين. إذا لم يكن الأمر كذلك، فمن الواضح أن هناك خطأ ما.
يبدو أن عبارة if تتحقق من أن _param1 ليس صفرًا، وأن _param1 * _param2 ليس سالبًا. من المحتمل أن يكون هذا لمنع حالات الالتفاف (wrap around).
أخيرًا، تُرجع الدالة قيمة متناسبة.
claim
الكود الذي تنشئه أداة فك التجميع معقد، وليس كله ذا صلة بنا. سأتخطى بعضًا منه للتركيز على الأسطر التي أعتقد أنها توفر معلومات مفيدة
def unknown2e7ba6ef(uint256 _param1, uint256 _param2, uint256 _param3, array _param4) payable:
...
require _param2 == addr(_param2)
...
if currentWindow <= _param1:
revert with 0, 'cannot claim for a future window'
نرى هنا شيئين مهمين:
_param2، على الرغم من الإعلان عنه كـuint256، هو في الواقع عنوان_param1هي النافذة التي تتم المطالبة بها، والتي يجب أن تكونcurrentWindowأو أقدم.
...
if stor5[_claimWindow][addr(_claimFor)]:
revert with 0, 'Account already claimed the given window'
لذا نعلم الآن أن Storage[5] عبارة عن مصفوفة من النوافذ والعناوين، وما إذا كان العنوان قد طالب بالمكافأة لتلك النافذة.
...
idx = 0
s = 0
while idx < _param4.length:
...
if s + sha3(mem[(32 * _param4.length) + 328 len mem[(32 * _param4.length) + 296]]) > mem[(32 * idx) + 296]:
mem[mem[64] + 32] = mem[(32 * idx) + 296]
...
s = sha3(mem[_62 + 32 len mem[_62]])
continue
...
s = sha3(mem[_66 + 32 len mem[_66]])
continue
if unknown2eb4a7ab != s:
revert with 0, 'Invalid proof'
نعلم أن unknown2eb4a7ab هي في الواقع الدالة merkleRoot()، لذا يبدو أن هذا الكود يتحقق من إثبات ميركل (opens in a new tab). هذا يعني أن _param4 هو إثبات ميركل.
call addr(_param2) with:
value unknown81e580d3[_param1] * _param3 / 100 * 10^6 wei
gas 30000 wei
هذه هي الطريقة التي يقوم بها العقد بتحويل ETH الخاص به إلى عنوان آخر (عقد أو مملوك خارجيًا). يقوم باستدعائه بقيمة تمثل المبلغ المراد تحويله. لذا يبدو أن هذا إسقاط جوي لـ ETH.
if not return_data.size:
if not ext_call.success:
require ext_code.size(stor2)
call stor2.deposit() with:
value unknown81e580d3[_param1] * _param3 / 100 * 10^6 wei
يخبرنا السطران السفليان أن Storage[2] هو أيضًا عقد نقوم باستدعائه. إذا نظرنا إلى معاملة المُنشئ (opens in a new tab) نرى أن هذا العقد هو 0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2 (opens in a new tab)، وهو عقد إيثر مغلف (WETH) تم رفع كود المصدر الخاص به إلى Etherscan (opens in a new tab).
لذا يبدو أن العقد يحاول إرسال ETH إلى _param2. إذا تمكن من ذلك، فهذا رائع. وإذا لم يتمكن، فإنه يحاول إرسال WETH (opens in a new tab). إذا كان _param2 حسابًا مملوكًا خارجيًا (EOA) فيمكنه دائمًا تلقي ETH، لكن العقود يمكنها رفض تلقي ETH. ومع ذلك، فإن WETH هو ERC-20 ولا يمكن للعقود رفض قبوله.
...
log 0xdbd5389f: addr(_param2), unknown81e580d3[_param1] * _param3 / 100 * 10^6, bool(ext_call.success)
في نهاية الدالة نرى أنه يتم إنشاء إدخال سجل. انظر إلى إدخالات السجل التي تم إنشاؤها (opens in a new tab) وقم بالتصفية حسب الموضوع الذي يبدأ بـ 0xdbd5.... إذا نقرنا على إحدى المعاملات التي أنشأت مثل هذا الإدخال (opens in a new tab) نرى أنه يبدو بالفعل كمطالبة - أرسل الحساب رسالة إلى العقد الذي نقوم بهندسته عكسيًا، وفي المقابل حصل على ETH.
1e7df9d3
هذه الدالة مشابهة جدًا لـ claim أعلاه. كما أنها تتحقق من إثبات ميركل، وتحاول تحويل ETH إلى الأول، وتنتج نفس النوع من إدخال السجل.
def unknown1e7df9d3(uint256 _param1, uint256 _param2, array _param3) payable:
...
idx = 0
s = 0
while idx < _param3.length:
if idx >= mem[96]:
revert with 0, 50
_55 = mem[(32 * idx) + 128]
if s + sha3(mem[(32 * _param3.length) + 160 len mem[(32 * _param3.length) + 128]]) > mem[(32 * idx) + 128]:
...
s = sha3(mem[_58 + 32 len mem[_58]])
continue
mem[mem[64] + 32] = s + sha3(mem[(32 * _param3.length) + 160 len mem[(32 * _param3.length) + 128]])
...
if unknown2eb4a7ab != s:
revert with 0, 'Invalid proof'
...
call addr(_param1) with:
value s wei
gas 30000 wei
if not return_data.size:
if not ext_call.success:
require ext_code.size(stor2)
call stor2.deposit() with:
value s wei
gas gas_remaining wei
...
log 0xdbd5389f: addr(_param1), s, bool(ext_call.success)
الفرق الرئيسي هو أن المعلمة الأولى، وهي النافذة المراد سحبها، غير موجودة. بدلاً من ذلك، هناك حلقة تكرار عبر جميع النوافذ التي يمكن المطالبة بها.
idx = 0
s = 0
while idx < currentWindow:
...
if stor5[mem[0]]:
if idx == -1:
revert with 0, 17
idx = idx + 1
s = s
continue
...
stor5[idx][addr(_param1)] = 1
if idx >= unknown81e580d3.length:
revert with 0, 50
mem[0] = 4
if unknown81e580d3[idx] and _param2 > -1 / unknown81e580d3[idx]:
revert with 0, 17
if s > !(unknown81e580d3[idx] * _param2 / 100 * 10^6):
revert with 0, 17
if idx == -1:
revert with 0, 17
idx = idx + 1
s = s + (unknown81e580d3[idx] * _param2 / 100 * 10^6)
continue
لذا يبدو أنه متغير من claim يطالب بجميع النوافذ.
الخاتمة
بحلول الآن، يجب أن تكون قد عرفت كيفية فهم العقود التي لا يتوفر كودها المصدري، باستخدام إما رموز التشغيل أو (عندما تنجح) أداة فك التجميع. وكما هو واضح من طول هذا المقال، فإن الهندسة العكسية لعقد ليست أمراً بسيطاً، ولكن في نظام يكون فيه الأمان أمراً بالغ الأهمية، فإن القدرة على التحقق من أن العقود تعمل كما هو موعود تعد مهارة مهمة.
انظر هنا للمزيد من أعمالي (opens in a new tab).
آخر تحديث للصفحة: 3 أبريل 2026



![التغيير في Storage[6]](/_next/image/?url=%2Fcontent%2Fdevelopers%2Ftutorials%2Freverse-engineering-a-contract%2Fstorage6.png&w=1920&q=75)



