ஒரு ஒப்பந்தத்தை ரிவர்ஸ் இன்ஜினியரிங் செய்தல்
அறிமுகம்
பிளாக்செயினில் எந்த ரகசியங்களும் இல்லை, நடக்கும் அனைத்தும் நிலையானவை, சரிபார்க்கக்கூடியவை மற்றும் பொதுவில் கிடைக்கக்கூடியவை. பொதுவாக, ஒப்பந்தங்கள் அவற்றின் மூலக் குறியீட்டை Etherscan இல் வெளியிட்டு சரிபார்க்கப்பட்டிருக்க வேண்டும் (opens in a new tab). இருப்பினும், எப்பொழுதும் அப்படி இருப்பதில்லை (opens in a new tab). இந்தக் கட்டுரையில், மூலக் குறியீடு இல்லாத ஒரு ஒப்பந்தமான 0x2510c039cc3b061d79e564b38836da87e31b342f (opens in a new tab) ஐப் பார்ப்பதன் மூலம் ஒப்பந்தங்களை எவ்வாறு ரிவர்ஸ் இன்ஜினியரிங் செய்வது என்பதை நீங்கள் கற்றுக் கொள்வீர்கள்.
ரிவர்ஸ் கம்பைலர்கள் உள்ளன, ஆனால் அவை எப்போதும் பயன்படுத்தக்கூடிய முடிவுகளை (opens in a new tab) தருவதில்லை. இந்தக் கட்டுரையில், ஆப்கோடுகளிலிருந்து (opcodes) (opens in a new tab) ஒரு ஒப்பந்தத்தை எவ்வாறு கைமுறையாக ரிவர்ஸ் இன்ஜினியரிங் செய்து புரிந்துகொள்வது என்பதையும், ஒரு டீகம்பைலரின் முடிவுகளை எவ்வாறு விளக்குவது என்பதையும் நீங்கள் கற்றுக் கொள்வீர்கள்.
இந்தக் கட்டுரையைப் புரிந்துகொள்ள, நீங்கள் ஏற்கனவே EVM இன் அடிப்படைகளை அறிந்திருக்க வேண்டும், மேலும் EVM அசெம்பிளருடன் ஓரளவு பரிச்சயமானவராக இருக்க வேண்டும். இந்தத் தலைப்புகளைப் பற்றி நீங்கள் இங்கே படிக்கலாம் (opens in a new tab).
செயல்படுத்தக்கூடிய குறியீட்டைத் தயார் செய்தல்
ஒப்பந்தத்திற்கான Etherscan-க்குச் சென்று, Contract தாவலைக் கிளிக் செய்து, பின்னர் Switch to Opcodes View என்பதைத் தேர்ந்தெடுப்பதன் மூலம் நீங்கள் ஆப்-குறியீடுகளைப் (opcodes) பெறலாம். ஒரு வரிக்கு ஒரு ஆப்-குறியீடு என்ற பார்வையை நீங்கள் பெறுவீர்கள்.
இருப்பினும், தாவல்களைப் (jumps) புரிந்துகொள்ள, குறியீட்டில் ஒவ்வொரு ஆப்-குறியீடும் எங்கு அமைந்துள்ளது என்பதை நீங்கள் தெரிந்துகொள்ள வேண்டும். அதைச் செய்ய, ஒரு Google Spreadsheet-ஐத் திறந்து, C நெடுவரிசையில் ஆப்-குறியீடுகளை ஒட்டுவது ஒரு வழியாகும். ஏற்கனவே தயார் செய்யப்பட்ட இந்த விரிதாளின் நகலை உருவாக்குவதன் மூலம் பின்வரும் படிகளை நீங்கள் தவிர்க்கலாம் (opens in a new tab).
அடுத்த படியாக, சரியான குறியீட்டு இருப்பிடங்களைப் பெறுவதன் மூலம் நம்மால் தாவல்களைப் புரிந்துகொள்ள முடியும். B நெடுவரிசையில் ஆப்-குறியீட்டின் அளவையும், A நெடுவரிசையில் இருப்பிடத்தையும் (ஹெக்ஸாடெசிமலில்) வைப்போம். B1 கலத்தில் இந்தச் சார்பை (function) தட்டச்சு செய்து, குறியீட்டின் இறுதி வரை B நெடுவரிசையின் மீதமுள்ள கலங்களுக்கு அதை நகலெடுத்து ஒட்டவும். இதைச் செய்த பிறகு, நீங்கள் B நெடுவரிசையை மறைக்கலாம்.
1=1+IF(REGEXMATCH(C1,"PUSH"),REGEXEXTRACT(C1,"PUSH(\d+)"),0)முதலில் இந்தச் சார்பு ஆப்-குறியீட்டிற்காக ஒரு பைட்டைச் சேர்க்கிறது, பின்னர் PUSH உள்ளதா எனத் தேடுகிறது. புஷ் (Push) ஆப்-குறியீடுகள் சிறப்பானவை, ஏனெனில் தள்ளப்படும் மதிப்பிற்கு அவற்றுக்குக் கூடுதல் பைட்டுகள் தேவைப்படும். ஆப்-குறியீடு PUSH ஆக இருந்தால், பைட்டுகளின் எண்ணிக்கையைப் பிரித்தெடுத்து அதைச் சேர்ப்போம்.
முதல் ஆஃப்செட்டான (offset) பூஜ்ஜியத்தை 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 அனைத்தும் பூஜ்ஜியங்களாக இருக்கும்).
- கால்டேட்டா (calldata) அளவைப் படிக்கவும். பொதுவாக ஒரு Ethereum ஒப்பந்தத்திற்கான கால் டேட்டா ABI (பயன்பாட்டு பைனரி இடைமுகம்) (opens in a new tab) ஐப் பின்பற்றுகிறது, இதற்குச் செயல்பாட்டுத் தேர்விக்கு (function selector) குறைந்தபட்சம் நான்கு பைட்டுகள் தேவைப்படும். கால் டேட்டா அளவு நான்கிற்கும் குறைவாக இருந்தால், 0x5E க்குத் தாவவும்.
0x5E இல் உள்ள கையாளுநர் (ABI அல்லாத கால் டேட்டாவிற்கு)
| ஆஃப்செட் | ஆப்-கோடு |
|---|---|
| 5E | JUMPDEST |
| 5F | CALLDATASIZE |
| 60 | PUSH2 0x007c |
| 63 | JUMPI |
இந்தத் துணுக்கு JUMPDEST உடன் தொடங்குகிறது. JUMPDEST அல்லாத ஆப்-கோடிற்கு நீங்கள் தாவினால் EVM (Ethereum மெய்நிகர் இயந்திரம்) நிரல்கள் விதிவிலக்கை (exception) எறியும். பின்னர் அது CALLDATASIZE ஐப் பார்க்கிறது, அது "உண்மை" (அதாவது, பூஜ்ஜியம் அல்ல) எனில் 0x7C க்குத் தாவுகிறது. அதை நாம் கீழே பார்ப்போம்.
| ஆஃப்செட் | ஆப்-கோடு | ஸ்டாக் (ஆப்-கோடிற்குப் பிறகு) |
|---|---|---|
| 64 | CALLVALUE | அழைப்பால் வழங்கப்பட்ட . Solidity இல் 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 ஐப் பரிமாற்றும் பரிவர்த்தனைகள் Etherscan இல் Transfer முறையைக் கொண்டுள்ளன. உண்மையில், ஒப்பந்தம் பெற்ற முதல் பரிவர்த்தனை (opens in a new tab) ஒரு பரிமாற்றமாகும்.
அந்தப் பரிவர்த்தனையைப் பார்த்து, Click to see More என்பதைக் கிளிக் செய்தால், உள்ளீட்டுத் தரவு (input data) எனப்படும் கால் டேட்டா உண்மையில் காலியாக (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 |
தாவல் இலக்கில் (jump destination) இந்தக் குறியீட்டைத் தொடர்ந்து கண்காணிப்போம்.
| ஆஃப்செட் | ஆப்-கோடு | ஸ்டாக் |
|---|---|---|
| 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 இல் ஓவர்ஃப்ளோ கண்டறியப்பட்டால் ஒப்பந்தம் திரும்பப் பெறப்படுவதை (revert) நாம் காண்கிறோம், இது சாதாரண நடத்தை ஆகும்.
இத்தகைய ஓவர்ஃப்ளோ ஏற்படுவது மிகவும் சாத்தியமற்றது என்பதை நினைவில் கொள்க, ஏனெனில் இதற்கு கால் மதிப்பு மற்றும் 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 இல் உள்ள கையாளுபவர்
இந்த கையாளுபவர் என்ன செய்கிறார் என்பதை நான் வேண்டுமென்றே தலைப்பில் குறிப்பிடவில்லை. இந்த குறிப்பிட்ட ஒப்பந்தம் எவ்வாறு செயல்படுகிறது என்பதை உங்களுக்குக் கற்பிப்பது இதன் நோக்கமல்ல, மாறாக ஒப்பந்தங்களை எவ்வாறு ரிவர்ஸ் இன்ஜினியரிங் (reverse engineer) செய்வது என்பதைக் கற்பிப்பதே ஆகும். குறியீட்டைப் பின்பற்றுவதன் மூலம், நான் கற்றுக்கொண்ட அதே வழியில் இது என்ன செய்கிறது என்பதை நீங்கள் கற்றுக்கொள்வீர்கள்.
நாம் பல இடங்களிலிருந்து இங்கு வருகிறோம்:
- 1, 2, அல்லது 3 பைட்டுகள் கொண்ட அழைப்புத் தரவு (call data) இருந்தால் (ஆஃப்செட் 0x63 இலிருந்து)
- முறை கையொப்பம் (method signature) தெரியவில்லை என்றால் (ஆஃப்செட்டுகள் 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 |
இது மற்றொரு சேமிப்பக செல் (storage cell) ஆகும், இதை என்னால் எந்த பரிவர்த்தனைகளிலும் கண்டுபிடிக்க முடியவில்லை, எனவே இதன் அர்த்தம் என்ன என்பதை அறிவது கடினம். கீழே உள்ள குறியீடு இதைத் தெளிவாக்கும்.
| ஆஃப்செட் | ஆப்கோடு | ஸ்டாக் |
|---|---|---|
| 85 | PUSH20 0xffffffffffffffffffffffffffffffffffffffff | 0xff....ff Storage[3] 0x9D 0x00 |
| 9A | AND | Storage[3]-as-address 0x9D 0x00 |
இந்த ஆப்கோடுகள் Storage[3] இலிருந்து நாம் படிக்கும் மதிப்பை 160 பிட்டுகளாகக் குறைக்கின்றன, இது ஒரு Ethereum முகவரியின் நீளமாகும்.
| ஆஃப்செட் | ஆப்கோடு | ஸ்டாக் |
|---|---|---|
| 9B | SWAP1 | 0x9D Storage[3]-as-address 0x00 |
| 9C | JUMP | Storage[3]-as-address 0x00 |
நாம் அடுத்த ஆப்கோட்டிற்குச் செல்வதால், இந்த ஜம்ப் (jump) தேவையற்றது. இந்தக் குறியீடு இருக்க வேண்டிய அளவுக்கு எரிவாயு-திறன் (gas-efficient) கொண்டதாக இல்லை.
| ஆஃப்செட் | ஆப்கோடு | ஸ்டாக் |
|---|---|---|
| 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 இலிருந்து தொடங்கி, அனைத்து அழைப்புத் தரவையும் (call data) நினைவகத்திற்கு நகலெடுக்கவும்.
| ஆஃப்செட் | ஆப்கோடு | ஸ்டாக் |
|---|---|---|
| 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 |
இப்போது விஷயங்கள் மிகவும் தெளிவாக உள்ளன. இந்த ஒப்பந்தம் ஒரு ப்ராக்ஸியாக (proxy) (opens in a new tab) செயல்பட முடியும், உண்மையான வேலையைச் செய்ய Storage[3] இல் உள்ள முகவரியை அழைக்கிறது. DELEGATE_CALL ஒரு தனி ஒப்பந்தத்தை அழைக்கிறது, ஆனால் அதே சேமிப்பகத்தில் (storage) தங்கியுள்ளது. இதன் பொருள், நாம் ப்ராக்ஸியாகச் செயல்படும் அந்தப் பிரதிநிதித்துவப்படுத்தப்பட்ட ஒப்பந்தம், அதே சேமிப்பக இடத்தையே அணுகுகிறது. அழைப்பிற்கான அளவுருக்கள் (parameters) பின்வருமாறு:
- எரிவாயு (Gas): மீதமுள்ள அனைத்து எரிவாயுவும்
- அழைக்கப்பட்ட முகவரி (Called address): Storage[3]-as-address
- அழைப்புத் தரவு (Call data): 0x80 இலிருந்து தொடங்கும் CALLDATASIZE பைட்டுகள், இங்குதான் அசல் அழைப்புத் தரவை வைத்துள்ளோம்
- திரும்பப் பெறும் தரவு (Return data): ஏதுமில்லை (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 இலிருந்து தொடங்கும் நினைவக இடையகத்திற்கு (memory buffer) நகலெடுக்கிறோம்.
| ஆஃப்செட் | ஆப்கோடு | ஸ்டாக் |
|---|---|---|
| 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 க்கு வந்தால், நாம் அழைத்த ஒப்பந்தம் திரும்பப் பெறப்பட்டது (reverted) என்று அர்த்தம். அந்த ஒப்பந்தத்திற்கு நாம் ஒரு ப்ராக்ஸியாக மட்டுமே இருப்பதால், அதே தரவைத் திருப்பித் தரவும், நாமும் திரும்பப் பெறவும் (revert) விரும்புகிறோம்.
| ஆஃப்செட் | ஆப்கோடு | ஸ்டாக் |
|---|---|---|
| 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 |
எனவே நாம் முன்பு RETURN செய்வதற்குப் பயன்படுத்திய அதே இடையகத்துடன் REVERT செய்கிறோம்: 0x80 - 0x80+RETURNDATASIZE
ABI அழைப்புகள்
அழைப்பு தரவின் (call data) அளவு நான்கு பைட்டுகள் அல்லது அதற்கும் அதிகமாக இருந்தால், இது சரியான 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) இது வலதுபுறம் நகர்த்தப்படுவதைக் (shift right) காட்டுகிறது
| ஆஃப்செட் | ஆப்கோடு | ஸ்டாக் |
|---|---|---|
| 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 பைட்டுகள்)))) |
முறை கையொப்பம் (method signature) பொருத்தும் சோதனைகளை இதுபோன்று இரண்டாகப் பிரிப்பதன் மூலம் சராசரியாக பாதி சோதனைகள் சேமிக்கப்படுகின்றன. இதைத் தொடர்ந்து வரும் குறியீடும் 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 |
பொருத்தம் எதுவும் காணப்படவில்லை எனில், நாம் ப்ராக்ஸியாக (proxy) இருக்கும் ஒப்பந்தத்தில் பொருத்தம் இருக்கும் என்ற நம்பிக்கையில், குறியீடு 0x7C இல் உள்ள ப்ராக்ஸி கையாளுநருக்கு (proxy handler) தாவுகிறது.
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 | (((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) ஒரு நினைவக சுட்டியைப் பெறுகிறது, மேலும் 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 | 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 க்கான தாவல்கள் (jumps) ஸ்டாக்கின் உச்சியை (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 | Storage[0] 0xDA |
| 101 | DUP2 | 0xDA Storage[0] 0xDA |
| 102 | JUMP | Storage[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 பைட்டுகள் (ஒரு சொல்) கால் டேட்டாவை (call data) எடுத்துக்கொள்வது போல் தெரிகிறது.
| ஆஃப்செட் | ஆப்கோடு | ஸ்டாக் |
|---|---|---|
| 19D | DUP1 | 0x00 0x00 0x04 CALLDATASIZE 0x0153 0xDA |
| 19E | DUP2 | 0x00 0x00 0x00 0x04 CALLDATASIZE 0x0153 0xDA |
| 19F | REVERT |
அதற்கு கால் டேட்டா கிடைக்கவில்லை என்றால், எந்த ரிட்டர்ன் டேட்டாவும் (return data) இல்லாமல் பரிவர்த்தனை ரிவர்ட் (revert) செய்யப்படும்.
சார்புக்குத் தேவையான கால் டேட்டா கிடைத்தால் என்ன நடக்கும் என்று பார்ப்போம்.
| ஆஃப்செட் | ஆப்கோடு | ஸ்டாக் |
|---|---|---|
| 1A0 | JUMPDEST | 0x00 0x04 CALLDATASIZE 0x0153 0xDA |
| 1A1 | POP | 0x04 CALLDATASIZE 0x0153 0xDA |
| 1A2 | CALLDATALOAD | calldataload(4) CALLDATASIZE 0x0153 0xDA |
calldataload(4) என்பது மெத்தட் கையொப்பத்திற்குப் (method signature) பிறகு வரும் கால் டேட்டாவின் முதல் சொல்லாகும்
| ஆஃப்செட் | ஆப்கோடு | ஸ்டாக் |
|---|---|---|
| 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 | (((0x00-0x1F இன் SHA3))) calldataload(4) calldataload(4) 0xDA |
| 189 | ADD | (((0x00-0x1F இன் SHA3)))+calldataload(4) calldataload(4) 0xDA |
| 18A | SLOAD | Storage[(((0x00-0x1F இன் SHA3))) + calldataload(4)] calldataload(4) 0xDA |
எனவே ஸ்டோரேஜில் ஒரு லுக்அப் டேபிள் (lookup table) உள்ளது, இது 0x000...0004 இன் SHA3 இல் தொடங்குகிறது மற்றும் ஒவ்வொரு சரியான கால் டேட்டா மதிப்பிற்கும் (Storage[4]-க்குக் கீழே உள்ள மதிப்பு) ஒரு உள்ளீட்டைக் கொண்டுள்ளது.
| ஆஃப்செட் | ஆப்கோடு | ஸ்டாக் |
|---|---|---|
| 18B | SWAP1 | calldataload(4) Storage[(((0x00-0x1F இன் SHA3))) + calldataload(4)] 0xDA |
| 18C | POP | Storage[(((0x00-0x1F இன் SHA3))) + calldataload(4)] 0xDA |
| 18D | DUP2 | 0xDA Storage[(((0x00-0x1F இன் SHA3))) + calldataload(4)] 0xDA |
| 18E | JUMP | Storage[(((0x00-0x1F இன் SHA3))) + calldataload(4)] 0xDA |
ஆஃப்செட் 0xDA இல் உள்ள குறியீடு என்ன செய்கிறது என்பதை நாம் ஏற்கனவே அறிவோம், அது ஸ்டாக்கின் மேல் மதிப்பை அழைப்பாளருக்கு (caller) வழங்கும். எனவே இந்தச் சார்பு லுக்அப் டேபிளில் உள்ள மதிப்பை அழைப்பாளருக்கு வழங்கும்.
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-ல் உள்ள குறியீடு என்ன செய்கிறது என்பதை நாம் ஏற்கனவே அறிவோம், அது ஸ்டாக்கின் மேல் மதிப்பை அழைப்பாளருக்கு (caller) வழங்குகிறது. எனவே இந்தச் சார்பு Value*-ஐ வழங்குகிறது.
முறை சுருக்கம்
இந்த கட்டத்தில் ஒப்பந்தத்தை (contract) நீங்கள் புரிந்துகொண்டதாக உணர்கிறீர்களா? எனக்குப் புரியவில்லை. இதுவரை நம்மிடம் இந்த முறைகள் உள்ளன:
| முறை | பொருள் |
|---|---|
| Transfer | அழைப்பால் வழங்கப்பட்ட மதிப்பை ஏற்றுக்கொண்டு, அந்த அளவின்படி Value*-ஐ அதிகரிக்கவும் |
| splitter() | Storage[3]-ஐ வழங்கவும், இது ப்ராக்ஸி முகவரியாகும் |
| currentWindow() | Storage[1]-ஐ வழங்கவும் |
| merkleRoot() | Storage[0]-ஐ வழங்கவும் |
| 0x81e580d3 | அளவுரு (parameter) Storage[4]-ஐ விடக் குறைவாக இருந்தால், லுக்அப் அட்டவணையிலிருந்து (lookup table) மதிப்பை வழங்கவும் |
| 0x1f135823 | Storage[6]-ஐ வழங்கவும், அதாவது Value* |
ஆனால் மற்ற எந்தவொரு செயல்பாடும் Storage[3]-ல் உள்ள ஒப்பந்தத்தால் வழங்கப்படுகிறது என்பதை நாம் அறிவோம். அந்த ஒப்பந்தம் என்னவென்று நமக்குத் தெரிந்தால், அது நமக்கு ஒரு துப்பைக் கொடுக்கலாம். அதிர்ஷ்டவசமாக, இது பிளாக்செயின் மற்றும் கோட்பாட்டளவிலாவது அனைத்தும் அறியப்பட்டதே. Storage[3]-ஐ அமைக்கும் எந்த முறைகளையும் நாம் பார்க்கவில்லை, எனவே அது கன்ஸ்ட்ரக்டரால் (constructor) அமைக்கப்பட்டிருக்க வேண்டும்.
கட்டமைப்பாளர்
நாம் ஒரு ஒப்பந்தத்தைப் பார்க்கும்போது (opens in a new tab), அதை உருவாக்கிய பரிவர்த்தனையையும் நாம் காண முடியும்.
அந்தப் பரிவர்த்தனையைக் கிளிக் செய்து, பின்னர் State தாவலைக் கிளிக் செய்தால், அளவுருக்களின் ஆரம்ப மதிப்புகளை நாம் காண முடியும். குறிப்பாக, Storage[3] இல் 0x2f81e57ff4f4d83b40a9f719fd892d8e806e0761 (opens in a new tab) இருப்பதைக் காணலாம். அந்த ஒப்பந்தத்தில் விடுபட்ட செயல்பாடு இருக்க வேண்டும். நாம் ஆராயும் ஒப்பந்தத்திற்குப் பயன்படுத்திய அதே கருவிகளைப் பயன்படுத்தி அதைப் புரிந்துகொள்ளலாம்.
ப்ராக்ஸி ஒப்பந்தம்
மேலே உள்ள அசல் ஒப்பந்தத்திற்கு நாம் பயன்படுத்திய அதே நுட்பங்களைப் பயன்படுத்தி, பின்வரும் நிலைகளில் ஒப்பந்தம் திரும்பப் பெறப்படுவதை (reverts) நாம் காணலாம்:
- அழைப்புடன் ஏதேனும் ETH இணைக்கப்பட்டிருந்தால் (0x05-0x0F)
- அழைப்பு தரவின் (call data) அளவு நான்கிற்கும் குறைவாக இருந்தால் (0x10-0x19 மற்றும் 0xBE-0xC2)
மேலும் இது ஆதரிக்கும் முறைகள் (methods):
| முறை (Method) | முறை கையொப்பம் (Method signature) | தாவுவதற்கான ஆஃப்செட் (Offset to jump into) |
|---|---|---|
| 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 |
கீழே உள்ள நான்கு முறைகளை நாம் புறக்கணிக்கலாம், ஏனெனில் நாம் அவற்றை ஒருபோதும் அடைய மாட்டோம். அவற்றின் கையொப்பங்கள் நமது அசல் ஒப்பந்தமே அவற்றைக் கவனித்துக்கொள்ளும் வகையில் உள்ளன (மேலே உள்ள விவரங்களைக் காண நீங்கள் கையொப்பங்களைக் கிளிக் செய்யலாம்), எனவே அவை மேலெழுதப்பட்ட முறைகளாக (overridden methods) (opens in a new tab) இருக்க வேண்டும்.
மீதமுள்ள முறைகளில் ஒன்று claim(<params>), மற்றொன்று isClaimed(<params>), எனவே இது ஒரு airdrop ஒப்பந்தம் போல் தெரிகிறது. மீதமுள்ளவற்றை ஒவ்வொரு ஆப்-கோடாக (opcode) பார்ப்பதற்குப் பதிலாக, நாம் டிகம்பைலரை (decompiler) முயற்சி செய்யலாம் (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 ஆனது, செயல்பாட்டு கையொப்பத்தின் நான்கு பைட்டுகளுக்கு கூடுதலாக, அழைப்பு தரவில் (call data) இரண்டு அளவுருக்களுக்கு (parameters) போதுமான குறைந்தபட்சம் 64 பைட்டுகள் உள்ளதா எனச் சோதிக்கிறது. இல்லையெனில், வெளிப்படையாக ஏதோ தவறு உள்ளது.
if கூற்று _param1 பூஜ்ஜியமாக இல்லை என்பதையும், _param1 * _param2 எதிர்மறையாக இல்லை என்பதையும் சரிபார்ப்பதாகத் தெரிகிறது. இது அநேகமாக ரேப் அரவுண்ட் (wrap around) நிகழ்வுகளைத் தடுப்பதற்காக இருக்கலாம்.
இறுதியாக, இந்தச் செயல்பாடு அளவிடப்பட்ட மதிப்பை (scaled value) வழங்குகிறது.
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, 'cannot claim for a future window'இங்கே நாம் இரண்டு முக்கியமான விஷயங்களைக் காண்கிறோம்:
_param2என்பதுuint256ஆக அறிவிக்கப்பட்டிருந்தாலும், அது உண்மையில் ஒரு முகவரியாகும் (address)_param1என்பது கோரப்படும் சாளரம் (window), இதுcurrentWindowஅல்லது அதற்கு முந்தையதாக இருக்க வேண்டும்.
1 ...2 if stor5[_claimWindow][addr(_claimFor)]:3 revert with 0, 'Account already claimed the given window'எனவே இப்போது Storage[5] என்பது சாளரங்கள் மற்றும் முகவரிகளின் அணிவரிசை (array) என்பதையும், அந்த முகவரி அந்தச் சாளரத்திற்கான வெகுமதியைக் கோரியதா என்பதையும் நாம் அறிவோம்.
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() செயல்பாடு என்பதை நாம் அறிவோம், எனவே இந்தக் குறியீடு ஒரு மெர்க்கல் சான்றை (merkle proof) (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-இன் airdrop போல் தெரிகிறது.
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] என்பதும் நாம் அழைக்கும் ஒரு ஒப்பந்தம் என்பதை நமக்குச் சொல்கின்றன. நாம் கட்டமைப்பாளர் பரிவர்த்தனையைப் (constructor transaction) பார்த்தால் (opens in a new tab), இந்த ஒப்பந்தம் 0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2 (opens in a new tab) என்பதைக் காண்கிறோம், இது ஒரு Wrapped Ether ஒப்பந்தமாகும், இதன் மூலக் குறியீடு Etherscan-இல் பதிவேற்றப்பட்டுள்ளது (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)செயல்பாட்டின் முடிவில் ஒரு பதிவு உள்ளீடு (log entry) உருவாக்கப்படுவதைக் காண்கிறோம். உருவாக்கப்பட்ட பதிவு உள்ளீடுகளைப் பாருங்கள் (opens in a new tab) மற்றும் 0xdbd5... என்று தொடங்கும் தலைப்பில் வடிகட்டவும். அத்தகைய உள்ளீட்டை உருவாக்கிய பரிவர்த்தனைகளில் ஒன்றைக் கிளிக் செய்தால் (opens in a new tab), அது உண்மையில் ஒரு கோரிக்கை (claim) போல் இருப்பதைக் காண்கிறோம் - கணக்கு நாம் ரிவர்ஸ் இன்ஜினியரிங் செய்யும் ஒப்பந்தத்திற்கு ஒரு செய்தியை அனுப்பியது, அதற்குப் பதிலாக 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)முக்கிய வேறுபாடு என்னவென்றால், திரும்பப் பெறுவதற்கான சாளரமான முதல் அளவுரு (parameter) அங்கு இல்லை. அதற்குப் பதிலாக, கோரப்படக்கூடிய அனைத்து சாளரங்களின் மீதும் ஒரு லூப் (loop) உள்ளது.
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



![Storage[6] இல் ஏற்பட்ட மாற்றம்](/_next/image/?url=%2Fcontent%2Fdevelopers%2Ftutorials%2Freverse-engineering-a-contract%2Fstorage6.png&w=1920&q=75)



