एक कॉन्ट्रैक्ट की रिवर्स इंजीनियरिंग
परिचय
ब्लॉकचेन पर कोई रहस्य नहीं हैं, जो कुछ भी होता है वह सुसंगत, सत्यापन योग्य और सार्वजनिक रूप से उपलब्ध होता है। आदर्श रूप से, कॉन्ट्रैक्ट्स का सोर्स कोड ईथरस्कैन पर प्रकाशित और सत्यापित होना चाहिए (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)।
निष्पादन योग्य कोड तैयार करें
आप कॉन्ट्रैक्ट के लिए ईथरस्कैन पर जाकर, कॉन्ट्रैक्ट टैब पर क्लिक करके और फिर ऑपकोड व्यू पर स्विच करें पर क्लिक करके ऑपकोड प्राप्त कर सकते हैं। आपको एक ऐसा व्यू मिलता है जिसमें प्रति लाइन एक ऑपकोड होता है।
हालांकि, जंप को समझने के लिए, आपको यह जानना होगा कि प्रत्येक ऑपकोड कोड में कहाँ स्थित है। ऐसा करने के लिए, एक तरीका यह है कि एक 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 है, तो हम बाइट्स की संख्या निकालते हैं और उसे जोड़ते हैं।
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 | खाली |
यह कोड दो चीजें करता है:
- मेमोरी लोकेशन 0x40-0x5F में 0x80 को 32 बाइट मान के रूप में लिखें (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 को देखता है, और यदि यह "true" है (यानी, शून्य नहीं) तो 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 | भंडारण[6] CALLVALUE 0 6 CALLVALUE |
तो जब कोई कॉल डेटा नहीं होता है तो हम Storage[6] का मान पढ़ते हैं। हमें अभी तक यह नहीं पता कि यह मान क्या है, लेकिन हम उन लेन-देन की तलाश कर सकते हैं जो कॉन्ट्रैक्ट को बिना किसी कॉल डेटा के प्राप्त हुए हैं। वे लेनदेन जो बिना किसी कॉल डेटा (और इसलिए कोई विधि नहीं) के केवल ETH ट्रांसफर करते हैं, ईथरस्कैन में Transfer विधि होती है। वास्तव में, कॉन्ट्रैक्ट को प्राप्त पहला लेनदेन (opens in a new tab) एक ट्रांसफर है।
यदि हम उस लेनदेन में देखते हैं और अधिक देखने के लिए क्लिक करें पर क्लिक करते हैं, तो हम देखते हैं कि कॉल डेटा, जिसे इनपुट डेटा कहा जाता है, वास्तव में खाली (0x) है। यह भी ध्यान दें कि मान 1.559 ETH है, जो बाद में प्रासंगिक होगा।
इसके बाद, स्टेट टैब पर क्लिक करें और उस कॉन्ट्रैक्ट का विस्तार करें जिसकी हम रिवर्स इंजीनियरिंग कर रहे हैं (0x2510...)। आप देख सकते हैं कि लेनदेन के दौरान Storage[6] बदल गया, और यदि आप Hex को नंबर में बदलते हैं, तो आप देखते हैं कि यह 1,559,000,000,000,000,000 हो गया, wei में स्थानांतरित मान (मैंने स्पष्टता के लिए कॉमा जोड़ा है), जो अगले कॉन्ट्रैक्ट मान के अनुरूप है।
यदि हम उसी अवधि के अन्य ट्रांसफर लेन-देन (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 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* में कॉल वैल्यू जोड़ते हैं। यह उसके अनुरूप है जो हम कहते हैं कि ट्रांसफर लेनदेन करते हैं।
| ऑफ़सेट | ऑपकोड |
|---|---|
| 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 | भंडारण[3] 0x9D 0x00 |
यह एक और भंडारण सेल है, जिसे मैं किसी भी लेनदेन में नहीं खोज सका, इसलिए यह जानना कठिन है कि इसका क्या मतलब है। नीचे दिया गया कोड इसे और स्पष्ट कर देगा।
| ऑफ़सेट | ऑपकोड | स्टैक |
|---|---|---|
| 85 | PUSH20 0xffffffffffffffffffffffffffffffffffffffff | 0xff....ff भंडारण[3] 0x9D 0x00 |
| 9A | AND | भंडारण[3]-के-रूप-में-पता 0x9D 0x00 |
ये ऑपकोड हमारे द्वारा Storage[3] से पढ़े गए मान को 160 बिट्स तक छोटा कर देते हैं, जो एक एथेरियम पते की लंबाई है।
| ऑफ़सेट | ऑपकोड | स्टैक |
|---|---|---|
| 9B | SWAP1 | 0x9D भंडारण[3]-के-रूप-में-पता 0x00 |
| 9C | JUMP | भंडारण[3]-के-रूप-में-पता 0x00 |
यह जंप अनावश्यक है, क्योंकि हम अगले ऑपकोड पर जा रहे हैं। यह कोड उतना गैस-कुशल नहीं है जितना हो सकता था।
| ऑफ़सेट | ऑपकोड | स्टैक |
|---|---|---|
| 9D | JUMPDEST | भंडारण[3]-के-रूप-में-पता 0x00 |
| 9E | SWAP1 | 0x00 भंडारण[3]-के-रूप-में-पता |
| 9F | POP | भंडारण[3]-के-रूप-में-पता |
| A0 | PUSH1 0x40 | 0x40 भंडारण[3]-के-रूप-में-पता |
| A2 | MLOAD | Mem[0x40] भंडारण[3]-के-रूप-में-पता |
कोड की शुरुआत में ही हमने Mem[0x40] को 0x80 पर सेट किया है। यदि हम बाद में 0x40 की तलाश करते हैं, तो हम देखते हैं कि हम इसे नहीं बदलते हैं - इसलिए हम मान सकते हैं कि यह 0x80 है।
| ऑफ़सेट | ऑपकोड | स्टैक |
|---|---|---|
| A3 | CALLDATASIZE | CALLDATASIZE 0x80 भंडारण[3]-के-रूप-में-पता |
| A4 | PUSH1 0x00 | 0x00 CALLDATASIZE 0x80 भंडारण[3]-के-रूप-में-पता |
| A6 | DUP3 | 0x80 0x00 CALLDATASIZE 0x80 भंडारण[3]-के-रूप-में-पता |
| A7 | CALLDATACOPY | 0x80 भंडारण[3]-के-रूप-में-पता |
सभी कॉल डेटा को मेमोरी में कॉपी करें, 0x80 से शुरू करते हुए।
| ऑफ़सेट | ऑपकोड | स्टैक |
|---|---|---|
| A8 | PUSH1 0x00 | 0x00 0x80 भंडारण[3]-के-रूप-में-पता |
| AA | DUP1 | 0x00 0x00 0x80 भंडारण[3]-के-रूप-में-पता |
| AB | CALLDATASIZE | CALLDATASIZE 0x00 0x00 0x80 भंडारण[3]-के-रूप-में-पता |
| AC | DUP4 | 0x80 CALLDATASIZE 0x00 0x00 0x80 भंडारण[3]-के-रूप-में-पता |
| AD | DUP6 | भंडारण[3]-के-रूप-में-पता 0x80 CALLDATASIZE 0x00 0x00 0x80 भंडारण[3]-के-रूप-में-पता |
| AE | GAS | GAS भंडारण[3]-के-रूप-में-पता 0x80 CALLDATASIZE 0x00 0x00 0x80 भंडारण[3]-के-रूप-में-पता |
| AF | DELEGATE_CALL |
अब चीजें बहुत स्पष्ट हैं। यह कॉन्ट्रैक्ट प्रॉक्सी (opens in a new tab) के रूप में कार्य कर सकता है, जो वास्तविक कार्य करने के लिए Storage[3] में पते को कॉल करता है। DELEGATE_CALL एक अलग कॉन्ट्रैक्ट को कॉल करता है, लेकिन उसी भंडारण में रहता है। इसका मतलब है कि प्रत्यायोजित कॉन्ट्रैक्ट, जिसके लिए हम एक प्रॉक्सी हैं, उसी भंडारण स्थान तक पहुँचता है। कॉल के लिए पैरामीटर हैं:
- गैस: बची हुई सभी गैस
- कॉल किया गया पता: भंडारण[3]-के-रूप-में-पता
- कॉल डेटा: 0x80 से शुरू होने वाले CALLDATASIZE बाइट्स, जहाँ हमने मूल कॉल डेटा रखा था
- रिटर्न डेटा: कोई नहीं (0x00 - 0x00) हम रिटर्न डेटा अन्य तरीकों से प्राप्त करेंगे (नीचे देखें)
| ऑफ़सेट | ऑपकोड | स्टैक |
|---|---|---|
| B0 | RETURNDATASIZE | RETURNDATASIZE (((कॉल सफलता/विफलता))) 0x80 भंडारण[3]-के-रूप-में-पता |
| B1 | DUP1 | RETURNDATASIZE RETURNDATASIZE (((कॉल सफलता/विफलता))) 0x80 भंडारण[3]-के-रूप-में-पता |
| B2 | PUSH1 0x00 | 0x00 RETURNDATASIZE RETURNDATASIZE (((कॉल सफलता/विफलता))) 0x80 भंडारण[3]-के-रूप-में-पता |
| B4 | DUP5 | 0x80 0x00 RETURNDATASIZE RETURNDATASIZE (((कॉल सफलता/विफलता))) 0x80 भंडारण[3]-के-रूप-में-पता |
| B5 | RETURNDATACOPY | RETURNDATASIZE (((कॉल सफलता/विफलता))) 0x80 भंडारण[3]-के-रूप-में-पता |
यहां हम सभी रिटर्न डेटा को मेमोरी बफर में कॉपी करते हैं जो 0x80 से शुरू होता है।
| ऑफ़सेट | ऑपकोड | स्टैक |
|---|---|---|
| B6 | DUP2 | (((कॉल सफलता/विफलता))) RETURNDATASIZE (((कॉल सफलता/विफलता))) 0x80 भंडारण[3]-के-रूप-में-पता |
| B7 | DUP1 | (((कॉल सफलता/विफलता))) (((कॉल सफलता/विफलता))) RETURNDATASIZE (((कॉल सफलता/विफलता))) 0x80 भंडारण[3]-के-रूप-में-पता |
| B8 | ISZERO | (((क्या कॉल विफल हुआ))) (((कॉल सफलता/विफलता))) RETURNDATASIZE (((कॉल सफलता/विफलता))) 0x80 भंडारण[3]-के-रूप-में-पता |
| B9 | PUSH2 0x00c0 | 0xC0 (((क्या कॉल विफल हुआ))) (((कॉल सफलता/विफलता))) RETURNDATASIZE (((कॉल सफलता/विफलता))) 0x80 भंडारण[3]-के-रूप-में-पता |
| BC | JUMPI | (((कॉल सफलता/विफलता))) RETURNDATASIZE (((कॉल सफलता/विफलता))) 0x80 भंडारण[3]-के-रूप-में-पता |
| BD | DUP2 | RETURNDATASIZE (((कॉल सफलता/विफलता))) RETURNDATASIZE (((कॉल सफलता/विफलता))) 0x80 भंडारण[3]-के-रूप-में-पता |
| BE | DUP5 | 0x80 RETURNDATASIZE (((कॉल सफलता/विफलता))) RETURNDATASIZE (((कॉल सफलता/विफलता))) 0x80 भंडारण[3]-के-रूप-में-पता |
| BF | RETURN |
तो कॉल के बाद हम रिटर्न डेटा को बफर 0x80 - 0x80+RETURNDATASIZE पर कॉपी करते हैं, और यदि कॉल सफल होता है तो हम ठीक उसी बफर के साथ RETURN करते हैं।
DELEGATECALL विफल
अगर हम यहां, 0xC0 पर पहुंचते हैं, तो इसका मतलब है कि हमने जिस कॉन्ट्रैक्ट को कॉल किया था वह रिवर्ट हो गया। चूंकि हम केवल उस कॉन्ट्रैक्ट के लिए एक प्रॉक्सी हैं, हम उसी डेटा को वापस करना चाहते हैं और रिवर्ट भी करना चाहते हैं।
| ऑफ़सेट | ऑपकोड | स्टैक |
|---|---|---|
| C0 | JUMPDEST | (((कॉल सफलता/विफलता))) RETURNDATASIZE (((कॉल सफलता/विफलता))) 0x80 भंडारण[3]-के-रूप-में-पता |
| C1 | DUP2 | RETURNDATASIZE (((कॉल सफलता/विफलता))) RETURNDATASIZE (((कॉल सफलता/विफलता))) 0x80 भंडारण[3]-के-रूप-में-पता |
| C2 | DUP5 | 0x80 RETURNDATASIZE (((कॉल सफलता/विफलता))) RETURNDATASIZE (((कॉल सफलता/विफलता))) 0x80 भंडारण[3]-के-रूप-में-पता |
| 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 में कोड एक ही पैटर्न का पालन करता है: कॉल डेटा के पहले 32 बिट्स को DUP1 करें, 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 भेजा है तो यह एक गलती होनी चाहिए और हम उस ETH को वापस पाने से बचने के लिए REVERT करना चाहते हैं।
| ऑफ़सेट | ऑपकोड | स्टैक |
|---|---|---|
| 10F | JUMPDEST | |
| 110 | POP | |
| 111 | PUSH1 0x03 | 0x03 |
| 113 | SLOAD | (((भंडारण[3] उर्फ कॉन्ट्रैक्ट जिसके लिए हम एक प्रॉक्सी हैं))) |
| 114 | PUSH1 0x40 | 0x40 (((भंडारण[3] उर्फ कॉन्ट्रैक्ट जिसके लिए हम एक प्रॉक्सी हैं))) |
| 116 | MLOAD | 0x80 (((भंडारण[3] उर्फ कॉन्ट्रैक्ट जिसके लिए हम एक प्रॉक्सी हैं))) |
| 117 | PUSH20 0xffffffffffffffffffffffffffffffffffffffff | 0xFF...FF 0x80 (((भंडारण[3] उर्फ कॉन्ट्रैक्ट जिसके लिए हम एक प्रॉक्सी हैं))) |
| 12C | SWAP1 | 0x80 0xFF...FF (((भंडारण[3] उर्फ कॉन्ट्रैक्ट जिसके लिए हम एक प्रॉक्सी हैं))) |
| 12D | SWAP2 | (((भंडारण[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) प्राप्त करता है, और कॉन्ट्रैक्ट को 0x80 - X के बफर के साथ RETURN करने का कारण बनता है।
splitter() के मामले में, यह उस पते को लौटाता है जिसके लिए हम एक प्रॉक्सी हैं। RETURN बफर को 0x80-0x9F में लौटाता है, जहां हमने यह डेटा लिखा था (ऊपर ऑफ़सेट 0x130)।
currentWindow()
ऑफसेट 0x158-0x163 में कोड splitter() में 0x103-0x10E में हमने जो देखा था, उसके समान है (JUMPI गंतव्य के अलावा), इसलिए हम जानते हैं कि currentWindow() भी payable नहीं है।
| ऑफ़सेट | ऑपकोड | स्टैक |
|---|---|---|
| 164 | JUMPDEST | |
| 165 | POP | |
| 166 | PUSH2 0x00da | 0xDA |
| 169 | PUSH1 0x01 | 0x01 0xDA |
| 16B | SLOAD | भंडारण[1] 0xDA |
| 16C | DUP2 | 0xDA भंडारण[1] 0xDA |
| 16D | JUMP | भंडारण[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 में कोड splitter() में 0x103-0x10E में हमने जो देखा था, उसके समान है (JUMPI गंतव्य के अलावा), इसलिए हम जानते हैं कि merkleRoot() भी payable नहीं है।
| ऑफ़सेट | ऑपकोड | स्टैक |
|---|---|---|
| F9 | JUMPDEST | |
| FA | POP | |
| FB | PUSH2 0x00da | 0xDA |
| FE | PUSH1 0x00 | 0x00 0xDA |
| 100 | SLOAD | भंडारण[0] 0xDA |
| 101 | DUP2 | 0xDA भंडारण[0] 0xDA |
| 102 | JUMP | भंडारण[0] 0xDA |
जंप के बाद क्या होता है यह हम पहले ही पता लगा चुके हैं। तो merkleRoot() Storage[0] लौटाता है।
0x81e580d3
ऑफसेट 0x138-0x143 में कोड splitter() में 0x103-0x10E में हमने जो देखा था, उसके समान है (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 | भंडारण[4] calldataload(4) 0x04 calldataload(4) 0xDA |
| 174 | DUP2 | calldataload(4) भंडारण[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 | (((0x00-0x1F का SHA3))) calldataload(4) calldataload(4) 0xDA |
| 189 | ADD | (((0x00-0x1F का SHA3)))+calldataload(4) calldataload(4) 0xDA |
| 18A | SLOAD | भंडारण[(((0x00-0x1F का SHA3))) + calldataload(4)] calldataload(4) 0xDA |
तो भंडारण में एक लुकअप तालिका है, जो 0x000...0004 के SHA3 पर शुरू होती है और प्रत्येक वैध कॉल डेटा मान (Storage[4] से नीचे का मान) के लिए एक प्रविष्टि है।
| ऑफ़सेट | ऑपकोड | स्टैक |
|---|---|---|
| 18B | SWAP1 | calldataload(4) भंडारण[(((0x00-0x1F का SHA3))) + calldataload(4)] 0xDA |
| 18C | POP | भंडारण[(((0x00-0x1F का SHA3))) + calldataload(4)] 0xDA |
| 18D | DUP2 | 0xDA भंडारण[(((0x00-0x1F का SHA3))) + calldataload(4)] 0xDA |
| 18E | JUMP | भंडारण[(((0x00-0x1F का SHA3))) + calldataload(4)] 0xDA |
हम पहले से ही जानते हैं कि ऑफसेट 0xDA पर कोड क्या करता है, यह कॉलर को स्टैक टॉप मान लौटाता है। तो यह फ़ंक्शन कॉलर को लुकअप तालिका से मान लौटाता है।
0x1f135823
ऑफसेट 0xC4-0xCF में कोड splitter() में 0x103-0x10E में हमने जो देखा था, उसके समान है (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
यह वह है जो डीकंपाइलर हमें इस फ़ंक्शन के लिए देता है:
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 ऋणात्मक नहीं है। यह शायद रैप अराउंड के मामलों को रोकने के लिए है।
अंत में, फ़ंक्शन एक स्केल्ड मान लौटाता है।
दावा
डीकंपाइलर द्वारा बनाया गया कोड जटिल है, और इसका सब कुछ हमारे लिए प्रासंगिक नहीं है। मैं उपयोगी जानकारी प्रदान करने वाली पंक्तियों पर ध्यान केंद्रित करने के लिए इसमें से कुछ को छोड़ने जा रहा हूं
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, 'cannot claim for a future window'हम यहां दो महत्वपूर्ण बातें देखते हैं:
_param2, जबकि इसेuint256के रूप में घोषित किया गया है, वास्तव में एक पता है_param1दावा की जा रही विंडो है, जिसेcurrentWindowया उससे पहले होना चाहिए।
1 ...2 if stor5[_claimWindow][addr(_claimFor)]:3 revert with 0, 'Account already claimed the given window'तो अब हम जानते हैं कि 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)।
तो ऐसा लगता है कि कॉन्ट्रैक्ट _param2 को ETH भेजने का प्रयास करता है। अगर यह कर सकता है, तो बढ़िया। यदि नहीं, तो यह 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तो यह एक claim संस्करण जैसा लगता है जो सभी विंडो का दावा करता है।
निष्कर्ष
अब तक आपको पता होना चाहिए कि उन कॉन्ट्रैक्ट्स को कैसे समझा जाए जिनका सोर्स कोड उपलब्ध नहीं है, या तो ऑपकोड का उपयोग करके या (जब यह काम करता है) डीकंपाइलर का उपयोग करके। जैसा कि इस लेख की लंबाई से स्पष्ट है, किसी कॉन्ट्रैक्ट की रिवर्स इंजीनियरिंग करना कोई मामूली बात नहीं है, लेकिन एक ऐसी प्रणाली में जहाँ सुरक्षा आवश्यक है, यह एक महत्वपूर्ण कौशल है कि यह सत्यापित किया जा सके कि कॉन्ट्रैक्ट वादे के अनुसार काम करते हैं।
मेरे और काम के लिए यहाँ देखें (opens in a new tab)।
पेज का अंतिम अपडेट: 3 मार्च 2026



![भंडारण[6] में परिवर्तन](/_next/image/?url=%2Fcontent%2Fdevelopers%2Ftutorials%2Freverse-engineering-a-contract%2Fstorage6.png&w=1920&q=75)



