الهندسة العكسية للعقد
مقدمة
لا توجد أسرار على سلسلة الكتل (blockchain)، فكل ما يحدث متسق وقابل للتحقق ومتاح للعامة. من الناحية المثالية، يجب نشر شفرة المصدر الخاصة بالعقود والتحقق منها على إيثرسكان (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).
إعداد الشفرة القابلة للتنفيذ
يمكنك الحصول على أكواد التشغيل بالانتقال إلى إيثرسكان للعقد، والنقر على علامة التبويب العقد (Contract) ثم التبديل إلى عرض أكواد التشغيل (Switch to Opcodes View). ستحصل على عرض يحتوي على كود تشغيل واحد في كل سطر.
لكي تتمكن من فهم القفزات، مع ذلك، تحتاج إلى معرفة مكان وجود كل كود تشغيل في الشفرة. للقيام بذلك، تتمثل إحدى الطرق في فتح جدول بيانات Google ولصق أكواد التشغيل في العمود C. يمكنك تخطي الخطوات التالية عن طريق عمل نسخة من جدول البيانات هذا المُعد مسبقًا (opens in a new tab).
الخطوة التالية هي الحصول على مواقع الشفرة الصحيحة حتى نتمكن من فهم القفزات. سنضع حجم كود التشغيل في العمود B، والموقع (بالنظام الست عشري) في العمود A. اكتب هذه الدالة في الخلية B1 ثم انسخها والصقها لبقية العمود B، حتى نهاية الشفرة. بعد القيام بذلك يمكنك إخفاء العمود B.
1=1+IF(REGEXMATCH(C1,"PUSH"),REGEXEXTRACT(C1,"PUSH(\d+)"),0)أولاً، تضيف هذه الدالة بايتًا واحدًا لكود التشغيل نفسه، ثم تبحث عن PUSH. تعتبر أكواد تشغيل الدفع (Push) خاصة لأنها تحتاج إلى بايتات إضافية للقيمة التي يتم دفعها. إذا كان كود التشغيل هو PUSH، فإننا نستخرج عدد البايتات ونضيفه.
في A1 ضع الإزاحة الأولى، صفر. ثم، في A2، ضع هذه الدالة ومرة أخرى انسخها والصقها لبقية العمود A:
1=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 بايت إلى مواقع الذاكرة 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 في سوليديتي |
| 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 فقط بدون أي بيانات استدعاء (وبالتالي لا توجد طريقة) يكون لها في إيثرسكان الطريقة Transfer. في الواقع، أول معاملة تلقاها العقد (opens in a new tab) هي عملية تحويل.
إذا نظرنا في تلك المعاملة ونقرنا على انقر لرؤية المزيد، نرى أن بيانات الاستدعاء، التي تسمى بيانات الإدخال، فارغة بالفعل (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 تعمل على مستوى البت، لذلك فهي تعكس قيمة كل بت في قيمة الاستدعاء.
| الإزاحة | كود التشغيل | كومة |
|---|---|---|
| 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 أو تساويها. يبدو هذا منطقًا لمنع التجاوز. وبالفعل، نرى أنه بعد بضع عمليات لا معنى لها (على سبيل المثال، الكتابة في الذاكرة على وشك الحذف)، عند الإزاحة 0x01DE، يتم إرجاع العقد إذا تم اكتشاف التجاوز، وهو سلوك طبيعي.
لاحظ أن مثل هذا التجاوز غير محتمل للغاية، لأنه يتطلب أن تكون قيمة الاستدعاء بالإضافة إلى Value* قابلة للمقارنة بـ 2^256 وي، أي حوالي 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 (((نجاح/فشل الاستدعاء))) 0x80 Storage[3]-as-address |
| B1 | DUP1 | RETURNDATASIZE RETURNDATASIZE (((نجاح/فشل الاستدعاء))) 0x80 Storage[3]-as-address |
| B2 | PUSH1 0x00 | 0x00 RETURNDATASIZE RETURNDATASIZE (((نجاح/فشل الاستدعاء))) 0x80 Storage[3]-as-address |
| B4 | DUP5 | 0x80 0x00 RETURNDATASIZE RETURNDATASIZE (((نجاح/فشل الاستدعاء))) 0x80 Storage[3]-as-address |
| B5 | RETURNDATACOPY | RETURNDATASIZE (((نجاح/فشل الاستدعاء))) 0x80 Storage[3]-as-address |
هنا ننسخ جميع بيانات الإرجاع إلى المخزن المؤقت للذاكرة بدءًا من 0x80.
| الإزاحة | كود التشغيل | كومة |
|---|---|---|
| B6 | DUP2 | (((نجاح/فشل الاستدعاء))) RETURNDATASIZE (((نجاح/فشل الاستدعاء))) 0x80 Storage[3]-as-address |
| B7 | DUP1 | (((نجاح/فشل الاستدعاء))) (((نجاح/فشل الاستدعاء))) RETURNDATASIZE (((نجاح/فشل الاستدعاء))) 0x80 Storage[3]-as-address |
| B8 | ISZERO | (((هل فشل الاستدعاء))) (((نجاح/فشل الاستدعاء))) RETURNDATASIZE (((نجاح/فشل الاستدعاء))) 0x80 Storage[3]-as-address |
| B9 | PUSH2 0x00c0 | 0xC0 (((هل فشل الاستدعاء))) (((نجاح/فشل الاستدعاء))) RETURNDATASIZE (((نجاح/فشل الاستدعاء))) 0x80 Storage[3]-as-address |
| BC | JUMPI | (((نجاح/فشل الاستدعاء))) RETURNDATASIZE (((نجاح/فشل الاستدعاء))) 0x80 Storage[3]-as-address |
| BD | DUP2 | RETURNDATASIZE (((نجاح/فشل الاستدعاء))) RETURNDATASIZE (((نجاح/فشل الاستدعاء))) 0x80 Storage[3]-as-address |
| BE | DUP5 | 0x80 RETURNDATASIZE (((نجاح/فشل الاستدعاء))) RETURNDATASIZE (((نجاح/فشل الاستدعاء))) 0x80 Storage[3]-as-address |
| BF | RETURN |
لذا بعد الاستدعاء، ننسخ بيانات الإرجاع إلى المخزن المؤقت 0x80 - 0x80+RETURNDATASIZE، وإذا نجح الاستدعاء، فإننا نُعيد (RETURN) هذا المخزن المؤقت بالضبط.
فشل DELEGATECALL
إذا وصلنا إلى هنا، إلى 0xC0، فهذا يعني أن العقد الذي استدعيناه قد تم إرجاعه. بما أننا مجرد وكيل لهذا العقد، فإننا نريد إرجاع نفس البيانات وإجراء الإرجاع أيضًا.
| الإزاحة | كود التشغيل | كومة |
|---|---|---|
| C0 | JUMPDEST | (((نجاح/فشل الاستدعاء))) RETURNDATASIZE (((نجاح/فشل الاستدعاء))) 0x80 Storage[3]-as-address |
| C1 | DUP2 | RETURNDATASIZE (((نجاح/فشل الاستدعاء))) RETURNDATASIZE (((نجاح/فشل الاستدعاء))) 0x80 Storage[3]-as-address |
| C2 | DUP5 | 0x80 RETURNDATASIZE (((نجاح/فشل الاستدعاء))) RETURNDATASIZE (((نجاح/فشل الاستدعاء))) 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 بايتات) من بيانات الاستدعاء))) |
يخبرنا إيثرسكان أن 1C هو كود تشغيل غير معروف، لأنه تمت إضافته بعد أن كتب إيثرسكان هذه الميزة (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>أول-32-بت-من-بيانات-الاستدعاء (((أول 32 بت (4 بايتات) من بيانات الاستدعاء))) |
| 1A | PUSH2 0x0043 | 0x43 0x3CD8045E>أول-32-بت-من-بيانات-الاستدعاء (((أول 32 بت (4 بايتات) من بيانات الاستدعاء))) |
| 1D | JUMPI | (((أول 32 بت (4 بايتات) من بيانات الاستدعاء))) |
يؤدي تقسيم اختبارات مطابقة توقيع الطريقة إلى قسمين مثل هذا إلى توفير نصف الاختبارات في المتوسط. تتبع الشفرة التي تلي ذلك مباشرة والشفرة الموجودة في 0x43 نفس النمط: DUP1 أول 32 بت من بيانات الاستدعاء، PUSH4 (((توقيع الطريقة))، وتشغيل 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 أيضًا.
| الإزاحة | كود التشغيل | كومة |
|---|---|---|
| 99.63+40*1.1018 = 143.702 | 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 |
| _mintFee | 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) يمكننا أيضًا رؤية المعاملة التي أنشأته.
إذا نقرنا على تلك المعاملة، ثم على علامة التبويب الحالة (State)، يمكننا رؤية القيم الأولية للمعلمات. على وجه التحديد، يمكننا أن نرى أن 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
هذا ما يمنحنا إياه أداة فك التجميع لهذه الدالة:
1def unknown8ffb5c97(uint256 _param1, uint256 _param2) payable:2 require calldata.size - 4 >=′ 643 if _param1 and _param2 > -1 / _param1:4 revert with 0, 175 return (_param1 * _param2 / 100 * 10^6)يختبر require الأول أن بيانات الاستدعاء تحتوي، بالإضافة إلى الأربعة بايتات الخاصة بتوقيع الدالة، على 64 بايتًا على الأقل، وهو ما يكفي للمعلمتين. إذا لم يكن الأمر كذلك، فمن الواضح أن هناك شيئًا خاطئًا.
يبدو أن عبارة if تتحقق من أن _param1 ليس صفرًا، وأن _param1 * _param2 ليس سالبًا. من المحتمل أن يكون لمنع حالات الالتفاف.
أخيرًا، تعيد الدالة قيمة متدرجة.
claim
الشفرة التي ينشئها أداة فك التجميع معقدة، وليست كلها ذات صلة بنا. سأتخطى بعضها للتركيز على الأسطر التي أعتقد أنها توفر معلومات مفيدة
1def unknown2e7ba6ef(uint256 _param1, uint256 _param2, uint256 _param3, array _param4) payable:2 ...3 require _param2 == addr(_param2)4 ...5 if currentWindow <= _param1:6 revert with 0, 'لا يمكن المطالبة بنافذة مستقبلية'نرى هنا شيئين مهمين:
_param2، على الرغم من الإعلان عنها كـuint256، هي في الواقع عنوان_param1هي النافذة التي تتم المطالبة بها، والتي يجب أن تكونcurrentWindowأو أقدم.
1 ...2 if stor5[_claimWindow][addr(_claimFor)]:3 revert with 0, 'الحساب طالب بالفعل بالنافذة المحددة'لذا نعلم الآن أن Storage[5] عبارة عن مصفوفة من النوافذ والعناوين، وما إذا كان العنوان قد طالب بالمكافأة لتلك النافذة.
1 ...2 idx = 03 s = 04 while idx < _param4.length:5 ...6 if s + sha3(mem[(32 * _param4.length) + 328 len mem[(32 * _param4.length) + 296]]) > mem[(32 * idx) + 296]:7 mem[mem[64] + 32] = mem[(32 * idx) + 296]8 ...9 s = sha3(mem[_62 + 32 len mem[_62]])10 continue11 ...12 s = sha3(mem[_66 + 32 len mem[_66]])13 continue14 if unknown2eb4a7ab != s:15 revert with 0, 'Invalid proof'إظهار الكلنعلم أن unknown2eb4a7ab هي في الواقع الدالة merkleRoot()، لذلك تبدو هذه الشفرة وكأنها تتحقق من إثبات ميركل (opens in a new tab). هذا يعني أن _param4 هو إثبات ميركل.
1 call addr(_param2) with:2 value unknown81e580d3[_param1] * _param3 / 100 * 10^6 wei3 gas 30000 weiهذه هي الطريقة التي ينقل بها العقد ETH الخاص به إلى عنوان آخر (عقد أو مملوك خارجيًا). يستدعيها بقيمة هي المبلغ المراد تحويله. لذا يبدو أن هذا إسقاط جوي من ETH.
1 if not return_data.size:2 if not ext_call.success:3 require ext_code.size(stor2)4 call stor2.deposit() with:5 value unknown81e580d3[_param1] * _param3 / 100 * 10^6 weiيخبرنا السطران السفليان أن Storage[2] هو أيضًا عقد نستدعيه. إذا نظرنا إلى معاملة المنشئ (opens in a new tab)، نرى أن هذا العقد هو 0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2 (opens in a new tab)، وهو عقد إيثر مغلف تم تحميل شفرة المصدر الخاصة به إلى إيثرسكان (opens in a new tab).
لذا يبدو أن العقود تحاول إرسال ETH إلى _param2. إذا كان بإمكانه فعل ذلك، فهذا رائع. إذا لم يكن كذلك، فإنه يحاول إرسال WETH (opens in a new tab). إذا كان _param2 حسابًا مملوكًا خارجيًا (EOA)، فيمكنه دائمًا تلقي ETH، ولكن يمكن للعقود رفض تلقي ETH. ومع ذلك، فإن WETH هو ERC-20 ولا يمكن للعقود رفض قبول ذلك.
1 ...2 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 إلى الأول، وتنتج نفس النوع من إدخال السجل.
1def unknown1e7df9d3(uint256 _param1, uint256 _param2, array _param3) payable:2 ...3 idx = 04 s = 05 while idx < _param3.length:6 if idx >= mem[96]:7 revert with 0, 508 _55 = mem[(32 * idx) + 128]9 if s + sha3(mem[(32 * _param3.length) + 160 len mem[(32 * _param3.length) + 128]]) > mem[(32 * idx) + 128]:10 ...11 s = sha3(mem[_58 + 32 len mem[_58]])12 continue13 mem[mem[64] + 32] = s + sha3(mem[(32 * _param3.length) + 160 len mem[(32 * _param3.length) + 128]])14 ...15 if unknown2eb4a7ab != s:16 revert with 0, 'Invalid proof'17 ...18 call addr(_param1) with:19 value s wei20 gas 30000 wei21 if not return_data.size:22 if not ext_call.success:23 require ext_code.size(stor2)24 call stor2.deposit() with:25 value s wei26 gas gas_remaining wei27 ...28 log 0xdbd5389f: addr(_param1), s, bool(ext_call.success)إظهار الكلالفرق الرئيسي هو أن المعلمة الأولى، وهي نافذة السحب، غير موجودة. بدلاً من ذلك، توجد حلقة على جميع النوافذ التي يمكن المطالبة بها.
1 idx = 02 s = 03 while idx < currentWindow:4 ...5 if stor5[mem[0]]:6 if idx == -1:7 revert with 0, 178 idx = idx + 19 s = s10 continue11 ...12 stor5[idx][addr(_param1)] = 113 if idx >= unknown81e580d3.length:14 revert with 0, 5015 mem[0] = 416 if unknown81e580d3[idx] and _param2 > -1 / unknown81e580d3[idx]:17 revert with 0, 1718 if s > !(unknown81e580d3[idx] * _param2 / 100 * 10^6):19 revert with 0, 1720 if idx == -1:21 revert with 0, 1722 idx = idx + 123 s = s + (unknown81e580d3[idx] * _param2 / 100 * 10^6)24 continueإظهار الكللذا يبدو أنه متغير مطالبة يطالب بجميع النوافذ.
الخلاصة
يجب أن تعرف الآن كيفية فهم العقود التي لا تتوفر شفرة المصدر الخاصة بها، إما باستخدام أكواد التشغيل أو (عندما تعمل) أداة فك التجميع. كما يتضح من طول هذه المقالة، فإن الهندسة العكسية للعقد ليست تافهة، ولكن في نظام يكون فيه الأمان ضروريًا، من المهم أن تكون لديك القدرة على التحقق من أن العقود تعمل كما هو موعود.
انظر هنا لمزيد من أعمالي (opens in a new tab).
آخر تحديث للصفحة: 22 أغسطس 2025



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



