ایک کنٹریکٹ کی ریورس انجینئرنگ
تعارف
بلاک چین پر کوئی راز نہیں ہیں، جو کچھ بھی ہوتا ہے وہ مستقل، قابل تصدیق، اور عوامی طور پر دستیاب ہے۔ مثالی طور پر، کنٹریکٹس کا سورس کوڈ Etherscan پر شائع اور تصدیق شدہ ہونا چاہیےopens in a new tab۔ تاہم، ہمیشہ ایسا نہیں ہوتاopens in a new tab۔ اس مضمون میں آپ بغیر سورس کوڈ والے کنٹریکٹ، 0x2510c039cc3b061d79e564b38836da87e31b342fopens in a new tab کو دیکھ کر کنٹریکٹس کی ریورس انجینئرنگ کرنا سیکھیں گے۔
ریورس کمپائلرز موجود ہیں، لیکن وہ ہمیشہ قابل استعمال نتائجopens in a new tab نہیں دیتے۔ اس مضمون میں آپ opcodesopens in a new tab سے ایک کنٹریکٹ کو دستی طور پر ریورس انجینئر کرنا اور سمجھنا سیکھیں گے، اور ساتھ ہی ایک ڈی کمپائلر کے نتائج کی تشریح کرنا بھی سیکھیں گے۔
اس مضمون کو سمجھنے کے لیے آپ کو EVM کی بنیادی باتیں پہلے سے معلوم ہونی چاہئیں، اور EVM اسمبلر سے کم از کم کچھ حد تک واقف ہونا چاہیے۔ آپ ان موضوعات کے بارے میں یہاں پڑھ سکتے ہیںopens in a new tab۔
قابل عمل کوڈ تیار کریں
آپ کنٹریکٹ کے لیے Etherscan پر جا کر، کنٹریکٹ ٹیب پر کلک کرکے اور پھر Switch to Opcodes View پر کلک کرکے opcodes حاصل کرسکتے ہیں۔ آپ کو ایک ویو ملتا ہے جس میں ہر لائن پر ایک opcode ہوتا ہے۔
تاہم، جمپس کو سمجھنے کے لیے، آپ کو یہ جاننا ہوگا کہ کوڈ میں ہر opcode کہاں واقع ہے۔ ایسا کرنے کے لیے، ایک طریقہ یہ ہے کہ ایک Google Spreadsheet کھولیں اور کالم C میں opcodes پیسٹ کریں۔ آپ اس پہلے سے تیار شدہ اسپریڈشیٹ کی ایک کاپی بنا کر درج ذیل مراحل کو چھوڑ سکتے ہیںopens in a new tab۔
اگلا مرحلہ کوڈ کے صحیح مقامات حاصل کرنا ہے تاکہ ہم جمپس کو سمجھ سکیں۔ ہم کالم B میں opcode کا سائز، اور کالم A میں مقام (ہیکسا ڈیسیمل میں) رکھیں گے۔ سیل B1 میں یہ فنکشن ٹائپ کریں اور پھر اسے کوڈ کے آخر تک باقی کالم B کے لیے کاپی اور پیسٹ کریں۔ ایسا کرنے کے بعد آپ کالم B کو چھپا سکتے ہیں۔
1=1+IF(REGEXMATCH(C1,\"PUSH\"),REGEXEXTRACT(C1,\"PUSH(\\d+)\"),0)پہلے یہ فنکشن خود opcode کے لیے ایک بائٹ کا اضافہ کرتا ہے، اور پھر PUSH کو تلاش کرتا ہے۔ پش opcodes خاص ہوتے ہیں کیونکہ انہیں پش کی جانے والی ویلیو کے لیے اضافی بائٹس کی ضرورت ہوتی ہے۔ اگر opcode ایک PUSH ہے، تو ہم بائٹس کی تعداد نکالتے ہیں اور اسے جوڑ دیتے ہیں۔
A1 میں پہلا آفسیٹ، صفر ڈالیں۔ پھر، A2 میں، یہ فنکشن ڈالیں اور اسے دوبارہ کالم A کے باقی حصے کے لیے کاپی اور پیسٹ کریں:
1=dec2hex(hex2dec(A1)+B1)ہمیں اس فنکشن کی ضرورت ہے تاکہ یہ ہمیں ہیکسا ڈیسیمل ویلیو دے کیونکہ جمپس (JUMP اور JUMPI) سے پہلے پش کی جانے والی ویلیوز ہمیں ہیکسا ڈیسیمل میں دی جاتی ہیں۔
انٹری پوائنٹ (0x00)
کنٹریکٹس ہمیشہ پہلے بائٹ سے ایگزیکیوٹ ہوتے ہیں۔ یہ کوڈ کا ابتدائی حصہ ہے:
| آفسیٹ | Opcode | اسٹیک (opcode کے بعد) |
|---|---|---|
| 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 کی پیروی کرتا ہے، جس کے لیے فنکشن سلیکٹر کے لیے کم از کم چار بائٹس کی ضرورت ہوتی ہے۔ اگر کال ڈیٹا کا سائز چار سے کم ہے، تو 0x5E پر جمپ کریں۔
0x5E پر ہینڈلر (غیر-ABI کال ڈیٹا کے لیے)
| آفسیٹ | Opcode |
|---|---|
| 5E | JUMPDEST |
| 5F | CALLDATASIZE |
| 60 | PUSH2 0x007c |
| 63 | JUMPI |
یہ سنیپٹ JUMPDEST سے شروع ہوتا ہے۔ EVM (Ethereum ورچوئل مشین) پروگرام ایک استثناء (exception) دیتے ہیں اگر آپ کسی ایسے opcode پر جمپ کرتے ہیں جو JUMPDEST نہیں ہے۔ پھر یہ CALLDATASIZE کو دیکھتا ہے، اور اگر یہ "درست" ہے (یعنی، صفر نہیں ہے) تو 0x7C پر جمپ کرتا ہے۔ ہم اس پر نیچے بات کریں گے۔
| آفسیٹ | Opcode | اسٹیک (opcode کے بعد) |
|---|---|---|
| 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 پر کلک کریں، تو ہم دیکھتے ہیں کہ کال ڈیٹا، جسے ان پٹ ڈیٹا کہا جاتا ہے، واقعی خالی (0x) ہے۔ یہ بھی نوٹ کریں کہ ویلیو 1.559 ETH ہے، جو بعد میں متعلقہ ہوگا۔
اگلا، اسٹیٹ ٹیب پر کلک کریں اور جس کنٹریکٹ کی ہم ریورس انجینئرنگ کر رہے ہیں اسے پھیلائیں (0x2510...)۔ آپ دیکھ سکتے ہیں کہ ٹرانزیکشن کے دوران Storage[6] تبدیل ہوا، اور اگر آپ Hex کو نمبر میں تبدیل کرتے ہیں، تو آپ دیکھتے ہیں کہ یہ 1,559,000,000,000,000,000 ہو گیا، جو wei میں منتقل کی گئی ویلیو ہے (میں نے وضاحت کے لیے کوما شامل کیے ہیں)، اور یہ اگلے کنٹریکٹ کی ویلیو کے مطابق ہے۔
اگر ہم اسی مدت کی دیگر Transfer ٹرانزیکشنزopens in a new tab کی وجہ سے ہونے والی اسٹیٹ تبدیلیوں کو دیکھیں تو ہم دیکھتے ہیں کہ Storage[6] نے کچھ وقت کے لیے کنٹریکٹ کی ویلیو کو ٹریک کیا۔ ابھی کے لیے ہم اسے Value* کہیں گے۔ ستارے کا نشان (*) ہمیں یاد دلاتا ہے کہ ہم ابھی تک نہیں جانتے کہ یہ متغیر کیا کرتا ہے، لیکن یہ صرف کنٹریکٹ کی ویلیو کو ٹریک کرنے کے لیے نہیں ہو سکتا کیونکہ اسٹوریج، جو بہت مہنگا ہے، استعمال کرنے کی کوئی ضرورت نہیں ہے، جب آپ ADDRESS BALANCE استعمال کرکے اپنے اکاؤنٹس کا بیلنس حاصل کرسکتے ہیں۔ پہلا opcode کنٹریکٹ کا اپنا ایڈریس پش کرتا ہے۔ دوسرا والا اسٹیک کے اوپری حصے میں موجود ایڈریس کو پڑھتا ہے اور اسے اس ایڈریس کے بیلنس سے بدل دیتا ہے۔
| آفسیٹ | Opcode | اسٹیک |
|---|---|---|
| 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 |
ہم اس کوڈ کو جمپ ڈیسٹینیشن پر ٹریس کرنا جاری رکھیں گے۔
| آفسیٹ | Opcode | اسٹیک |
|---|---|---|
| 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 بٹ وائز ہے، لہذا یہ کال ویلیو میں ہر بٹ کی ویلیو کو الٹ دیتا ہے۔
| آفسیٹ | Opcode | اسٹیک |
|---|---|---|
| 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۔
| آفسیٹ | Opcode | اسٹیک |
|---|---|---|
| 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 پر جمپ کریں۔
| آفسیٹ | Opcode | اسٹیک |
|---|---|---|
| 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 ٹرانزیکشنز کرتی ہیں۔
| آفسیٹ | Opcode |
|---|---|
| 79 | POP |
| 7A | POP |
| 7B | STOP |
آخر میں، اسٹیک کو صاف کریں (جو ضروری نہیں ہے) اور ٹرانزیکشن کے کامیاب اختتام کا اشارہ دیں۔
اس سب کا خلاصہ کرنے کے لیے، یہاں ابتدائی کوڈ کے لیے ایک فلو چارٹ ہے۔
0x7C پر ہینڈلر
میں نے جان بوجھ کر ہیڈنگ میں یہ نہیں بتایا کہ یہ ہینڈلر کیا کرتا ہے۔ مقصد آپ کو یہ سکھانا نہیں ہے کہ یہ مخصوص کنٹریکٹ کیسے کام کرتا ہے، بلکہ یہ سکھانا ہے کہ کنٹریکٹس کو کیسے ریورس انجینئر کیا جائے۔ آپ اسی طرح سیکھیں گے جیسے میں نے کوڈ کی پیروی کرکے سیکھا تھا۔
ہم یہاں کئی جگہوں سے آتے ہیں:
- اگر 1، 2، یا 3 بائٹس کا کال ڈیٹا ہے (آفسیٹ 0x63 سے)
- اگر میتھڈ سگنیچر نامعلوم ہے (آفسیٹس 0x42 اور 0x5D سے)
| آفسیٹ | Opcode | اسٹیک |
|---|---|---|
| 7C | JUMPDEST | |
| 7D | PUSH1 0x00 | 0x00 |
| 7F | PUSH2 0x009d | 0x9D 0x00 |
| 82 | PUSH1 0x03 | 0x03 0x9D 0x00 |
| 84 | SLOAD | Storage[3] 0x9D 0x00 |
یہ ایک اور اسٹوریج سیل ہے، جسے میں کسی بھی ٹرانزیکشن میں نہیں ڈھونڈ سکا لہذا یہ جاننا مشکل ہے کہ اس کا کیا مطلب ہے۔ نیچے دیا گیا کوڈ اسے مزید واضح کر دے گا۔
| آفسیٹ | Opcode | اسٹیک |
|---|---|---|
| 85 | PUSH20 0xffffffffffffffffffffffffffffffffffffffff | 0xff....ff Storage[3] 0x9D 0x00 |
| 9A | AND | Storage[3]-بطور-ایڈریس 0x9D 0x00 |
یہ opcodes اس ویلیو کو جو ہم Storage[3] سے پڑھتے ہیں، اسے 160 بٹس تک چھوٹا کر دیتے ہیں، جو کہ ایک Ethereum ایڈریس کی لمبائی ہے۔
| آفسیٹ | Opcode | اسٹیک |
|---|---|---|
| 9B | SWAP1 | 0x9D Storage[3]-بطور-ایڈریس 0x00 |
| 9C | JUMP | Storage[3]-بطور-ایڈریس 0x00 |
یہ جمپ فالتو ہے، کیونکہ ہم اگلے opcode پر جا رہے ہیں۔ یہ کوڈ اتنا گیس-ایفیشنٹ نہیں ہے جتنا ہو سکتا تھا۔
| آفسیٹ | Opcode | اسٹیک |
|---|---|---|
| 9D | JUMPDEST | Storage[3]-بطور-ایڈریس 0x00 |
| 9E | SWAP1 | 0x00 Storage[3]-بطور-ایڈریس |
| 9F | POP | Storage[3]-بطور-ایڈریس |
| A0 | PUSH1 0x40 | 0x40 Storage[3]-بطور-ایڈریس |
| A2 | MLOAD | Mem[0x40] Storage[3]-بطور-ایڈریس |
کوڈ کے بالکل شروع میں ہم نے Mem[0x40] کو 0x80 پر سیٹ کیا تھا۔ اگر ہم بعد میں 0x40 کو تلاش کریں تو ہم دیکھتے ہیں کہ ہم اسے تبدیل نہیں کرتے ہیں - لہذا ہم فرض کر سکتے ہیں کہ یہ 0x80 ہے۔
| آفسیٹ | Opcode | اسٹیک |
|---|---|---|
| A3 | CALLDATASIZE | CALLDATASIZE 0x80 Storage[3]-بطور-ایڈریس |
| A4 | PUSH1 0x00 | 0x00 CALLDATASIZE 0x80 Storage[3]-بطور-ایڈریس |
| A6 | DUP3 | 0x80 0x00 CALLDATASIZE 0x80 Storage[3]-بطور-ایڈریس |
| A7 | CALLDATACOPY | 0x80 Storage[3]-بطور-ایڈریس |
تمام کال ڈیٹا کو میموری میں کاپی کریں، 0x80 سے شروع کرتے ہوئے۔
| آفسیٹ | Opcode | اسٹیک |
|---|---|---|
| A8 | PUSH1 0x00 | 0x00 0x80 Storage[3]-بطور-ایڈریس |
| AA | DUP1 | 0x00 0x00 0x80 Storage[3]-بطور-ایڈریس |
| AB | CALLDATASIZE | CALLDATASIZE 0x00 0x00 0x80 Storage[3]-بطور-ایڈریس |
| AC | DUP4 | 0x80 CALLDATASIZE 0x00 0x00 0x80 Storage[3]-بطور-ایڈریس |
| AD | DUP6 | Storage[3]-بطور-ایڈریس 0x80 CALLDATASIZE 0x00 0x00 0x80 Storage[3]-بطور-ایڈریس |
| AE | GAS | GAS Storage[3]-بطور-ایڈریس 0x80 CALLDATASIZE 0x00 0x00 0x80 Storage[3]-بطور-ایڈریس |
| AF | DELEGATE_CALL |
اب چیزیں بہت واضح ہو گئی ہیں۔ یہ کنٹریکٹ ایک پراکسیopens in a new tab کے طور پر کام کر سکتا ہے، جو اصل کام کرنے کے لیے Storage[3] میں موجود ایڈریس کو کال کرتا ہے۔ DELEGATE_CALL ایک الگ کنٹریکٹ کو کال کرتا ہے، لیکن اسی اسٹوریج میں رہتا ہے۔ اس کا مطلب یہ ہے کہ ڈیلیگیٹڈ کنٹریکٹ، جس کے لیے ہم ایک پراکسی ہیں، اسی اسٹوریج اسپیس تک رسائی حاصل کرتا ہے۔ کال کے پیرامیٹرز یہ ہیں:
- گیس: باقی تمام گیس
- کال کیا گیا ایڈریس: Storage[3]-بطور-ایڈریس
- کال ڈیٹا: CALLDATASIZE بائٹس 0x80 سے شروع ہوتی ہیں، جہاں ہم نے اصل کال ڈیٹا ڈالا تھا
- ریٹرن ڈیٹا: کوئی نہیں (0x00 - 0x00) ہم ریٹرن ڈیٹا دوسرے طریقوں سے حاصل کریں گے (نیچے دیکھیں)
| آفسیٹ | Opcode | اسٹیک |
|---|---|---|
| B0 | RETURNDATASIZE | RETURNDATASIZE (((کال کی کامیابی/ناکامی))) 0x80 Storage[3]-بطور-ایڈریس |
| B1 | DUP1 | RETURNDATASIZE RETURNDATASIZE (((کال کی کامیابی/ناکامی))) 0x80 Storage[3]-بطور-ایڈریس |
| B2 | PUSH1 0x00 | 0x00 RETURNDATASIZE RETURNDATASIZE (((کال کی کامیابی/ناکامی))) 0x80 Storage[3]-بطور-ایڈریس |
| B4 | DUP5 | 0x80 0x00 RETURNDATASIZE RETURNDATASIZE (((کال کی کامیابی/ناکامی))) 0x80 Storage[3]-بطور-ایڈریس |
| B5 | RETURNDATACOPY | RETURNDATASIZE (((کال کی کامیابی/ناکامی))) 0x80 Storage[3]-بطور-ایڈریس |
یہاں ہم تمام ریٹرن ڈیٹا کو 0x80 سے شروع ہونے والے میموری بفر میں کاپی کرتے ہیں۔
| آفسیٹ | Opcode | اسٹیک |
|---|---|---|
| B6 | DUP2 | (((کال کی کامیابی/ناکامی))) RETURNDATASIZE (((کال کی کامیابی/ناکامی))) 0x80 Storage[3]-بطور-ایڈریس |
| B7 | DUP1 | (((کال کی کامیابی/ناکامی))) (((کال کی کامیابی/ناکامی))) RETURNDATASIZE (((کال کی کامیابی/ناکامی))) 0x80 Storage[3]-بطور-ایڈریس |
| B8 | ISZERO | (((کیا کال ناکام ہوئی))) (((کال کی کامیابی/ناکامی))) RETURNDATASIZE (((کال کی کامیابی/ناکامی))) 0x80 Storage[3]-بطور-ایڈریس |
| B9 | PUSH2 0x00c0 | 0xC0 (((کیا کال ناکام ہوئی))) (((کال کی کامیابی/ناکامی))) RETURNDATASIZE (((کال کی کامیابی/ناکامی))) 0x80 Storage[3]-بطور-ایڈریس |
| BC | JUMPI | (((کال کی کامیابی/ناکامی))) RETURNDATASIZE (((کال کی کامیابی/ناکامی))) 0x80 Storage[3]-بطور-ایڈریس |
| BD | DUP2 | RETURNDATASIZE (((کال کی کامیابی/ناکامی))) RETURNDATASIZE (((کال کی کامیابی/ناکامی))) 0x80 Storage[3]-بطور-ایڈریس |
| BE | DUP5 | 0x80 RETURNDATASIZE (((کال کی کامیابی/ناکامی))) RETURNDATASIZE (((کال کی کامیابی/ناکامی))) 0x80 Storage[3]-بطور-ایڈریس |
| BF | RETURN |
لہذا کال کے بعد ہم ریٹرن ڈیٹا کو بفر 0x80 - 0x80+RETURNDATASIZE میں کاپی کرتے ہیں، اور اگر کال کامیاب ہوتی ہے تو ہم بالکل اسی بفر کے ساتھ RETURN کرتے ہیں۔
DELEGATECALL ناکام
اگر ہم یہاں، 0xC0 پر پہنچتے ہیں، تو اس کا مطلب ہے کہ ہم نے جس کنٹریکٹ کو کال کیا تھا وہ ریورٹ ہو گیا ہے۔ چونکہ ہم اس کنٹریکٹ کے لیے صرف ایک پراکسی ہیں، ہم وہی ڈیٹا واپس کرنا چاہتے ہیں اور ریورٹ بھی کرنا چاہتے ہیں۔
| آفسیٹ | Opcode | اسٹیک |
|---|---|---|
| C0 | JUMPDEST | (((کال کی کامیابی/ناکامی))) RETURNDATASIZE (((کال کی کامیابی/ناکامی))) 0x80 Storage[3]-بطور-ایڈریس |
| C1 | DUP2 | RETURNDATASIZE (((کال کی کامیابی/ناکامی))) RETURNDATASIZE (((کال کی کامیابی/ناکامی))) 0x80 Storage[3]-بطور-ایڈریس |
| C2 | DUP5 | 0x80 RETURNDATASIZE (((کال کی کامیابی/ناکامی))) RETURNDATASIZE (((کال کی کامیابی/ناکامی))) 0x80 Storage[3]-بطور-ایڈریس |
| C3 | REVERT |
لہذا ہم اسی بفر کے ساتھ REVERT کرتے ہیں جو ہم نے پہلے RETURN کے لیے استعمال کیا تھا: 0x80 - 0x80+RETURNDATASIZE
ABI کالز
اگر کال ڈیٹا کا سائز چار بائٹس یا اس سے زیادہ ہے تو یہ ایک درست ABI کال ہو سکتی ہے۔
| آفسیٹ | Opcode | اسٹیک |
|---|---|---|
| D | PUSH1 0x00 | 0x00 |
| F | CALLDATALOAD | (((کال ڈیٹا کا پہلا لفظ (256 بٹس)))) |
| 10 | PUSH1 0xe0 | 0xE0 (((کال ڈیٹا کا پہلا لفظ (256 بٹس)))) |
| 12 | SHR | (((کال ڈیٹا کے پہلے 32 بٹس (4 بائٹس)))) |
Etherscan ہمیں بتاتا ہے کہ 1C ایک نامعلوم opcode ہے، کیونکہ اسے Etherscan کے اس فیچر کو لکھنے کے بعد شامل کیا گیا تھاopens in a new tab اور انہوں نے اسے اپ ڈیٹ نہیں کیا ہے۔ ایک اپ ٹو ڈیٹ opcode ٹیبلopens in a new tab ہمیں دکھاتا ہے کہ یہ شفٹ رائٹ ہے۔
| آفسیٹ | Opcode | اسٹیک |
|---|---|---|
| 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()
| آفسیٹ | Opcode | اسٹیک |
|---|---|---|
| 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 نہیں بھیجا ہے۔ یہ فنکشن payableopens in a new tab نہیں ہے۔ اگر کسی نے ہمیں ETH بھیجا ہے تو یہ ایک غلطی ہونی چاہیے اور ہم REVERT کرنا چاہتے ہیں تاکہ اس ETH کو ایسی جگہ رکھنے سے بچا جا سکے جہاں وہ اسے واپس نہ لے سکیں۔
| آفسیٹ | Opcode | اسٹیک |
|---|---|---|
| 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 | پراکسی ایڈریس 0x80 |
| 12F | DUP2 | 0x80 پراکسی ایڈریس 0x80 |
| 130 | MSTORE | 0x80 |
اور 0x80 میں اب پراکسی ایڈریس موجود ہے
| آفسیٹ | Opcode | اسٹیک |
|---|---|---|
| 131 | PUSH1 0x20 | 0x20 0x80 |
| 133 | ADD | 0xA0 |
| 134 | PUSH2 0x00e4 | 0xE4 0xA0 |
| 137 | JUMP | 0xA0 |
E4 کوڈ
یہ پہلی بار ہے جب ہم یہ لائنیں دیکھ رہے ہیں، لیکن یہ دیگر میتھڈز کے ساتھ بھی شیئر کی گئی ہیں (نیچے دیکھیں)۔ لہذا ہم اسٹیک میں موجود ویلیو کو X کہیں گے، اور بس یاد رکھیں گے کہ splitter() میں اس X کی ویلیو 0xA0 ہے۔
| آفسیٹ | Opcode | اسٹیک |
|---|---|---|
| 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 میں موجود کوڈ اس سے مماثل ہے جو ہم نے splitter() میں 0x103-0x10E میں دیکھا تھا (JUMPI کی منزل کے علاوہ)، لہذا ہم جانتے ہیں کہ currentWindow() بھی payable نہیں ہے۔
| آفسیٹ | Opcode | اسٹیک |
|---|---|---|
| 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] ہے۔
| آفسیٹ | Opcode | اسٹیک |
|---|---|---|
| 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 پر لکھیں۔
| آفسیٹ | Opcode | اسٹیک |
|---|---|---|
| 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 نہیں ہے۔
| آفسیٹ | Opcode | اسٹیک |
|---|---|---|
| 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 نہیں ہے۔
| آفسیٹ | Opcode | اسٹیک |
|---|---|---|
| 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 بائٹس (ایک لفظ) کال ڈیٹا لیتا ہے۔
| آفسیٹ | Opcode | اسٹیک |
|---|---|---|
| 19D | DUP1 | 0x00 0x00 0x04 CALLDATASIZE 0x0153 0xDA |
| 19E | DUP2 | 0x00 0x00 0x00 0x04 CALLDATASIZE 0x0153 0xDA |
| 19F | REVERT |
اگر اسے کال ڈیٹا نہیں ملتا ہے تو ٹرانزیکشن بغیر کسی ریٹرن ڈیٹا کے ریورٹ ہو جاتی ہے۔
آئیے دیکھتے ہیں کہ اگر فنکشن کو واقعی وہ کال ڈیٹا ملتا ہے جس کی اسے ضرورت ہے۔
| آفسیٹ | Opcode | اسٹیک |
|---|---|---|
| 1A0 | JUMPDEST | 0x00 0x04 CALLDATASIZE 0x0153 0xDA |
| 1A1 | POP | 0x04 CALLDATASIZE 0x0153 0xDA |
| 1A2 | CALLDATALOAD | calldataload(4) CALLDATASIZE 0x0153 0xDA |
calldataload(4) میتھڈ سگنیچر کے بعد کال ڈیٹا کا پہلا لفظ ہے
| آفسیٹ | Opcode | اسٹیک |
|---|---|---|
| 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] سے کم نہیں ہے تو فنکشن ناکام ہو جاتا ہے۔ یہ بغیر کسی واپس کی گئی ویلیو کے ریورٹ ہو جاتا ہے:
| آفسیٹ | Opcode | اسٹیک |
|---|---|---|
| 17A | PUSH1 0x00 | 0x00 ... |
| 17C | DUP1 | 0x00 0x00 ... |
| 17D | REVERT |
اگر calldataload(4) کی ویلیو Storage[4] سے کم ہے، تو ہمیں یہ کوڈ ملتا ہے:
| آفسیٹ | Opcode | اسٹیک |
|---|---|---|
| 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 چار ہے)
| آفسیٹ | Opcode | اسٹیک |
|---|---|---|
| 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 |
لہذا اسٹوریج میں ایک لک اپ ٹیبل ہے، جو 0x000...0004 کے SHA3 سے شروع ہوتا ہے اور اس میں ہر جائز کال ڈیٹا ویلیو (Storage[4] سے کم ویلیو) کے لیے ایک انٹری ہوتی ہے۔
| آفسیٹ | Opcode | اسٹیک |
|---|---|---|
| 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 پر موجود کوڈ کیا کرتا ہے، یہ اسٹیک ٹاپ ویلیو کو کالر کو واپس کرتا ہے۔ لہذا یہ فنکشن لک اپ ٹیبل سے ویلیو کو کالر کو واپس کرتا ہے۔
0x1f135823
آفسیٹس 0xC4-0xCF میں موجود کوڈ اس سے مماثل ہے جو ہم نے splitter() میں 0x103-0x10E میں دیکھا تھا (JUMPI کی منزل کے علاوہ)، لہذا ہم جانتے ہیں کہ یہ فنکشن بھی payable نہیں ہے۔
| آفسیٹ | Opcode | اسٹیک |
|---|---|---|
| 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] میں 0x2f81e57ff4f4d83b40a9f719fd892d8e806e0761opens 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>) ہے، لہذا یہ ایک ایئر ڈراپ کنٹریکٹ کی طرح لگتا ہے۔ باقی کو opcode-به-opcode دیکھنے کے بجائے، ہم ڈی کمپائلر کو آزما سکتے ہیں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, '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() ہے، لہذا یہ کوڈ ایسا لگتا ہے جیسے یہ ایک merkle proofopens in a new tab کی تصدیق کر رہا ہے۔ اس کا مطلب یہ ہے کہ _param4 ایک merkle proof ہے۔
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 تو ہم دیکھتے ہیں کہ یہ کنٹریکٹ 0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2opens in a new tab ہے، جو ایک Wrapped Ether کنٹریکٹ ہے جس کا سورس کوڈ Etherscan پر اپ لوڈ کیا گیا ہےopens in a new tab۔
تو ایسا لگتا ہے کہ کنٹریکٹس _param2 کو ETH بھیجنے کی کوشش کرتے ہیں۔ اگر یہ کر سکتا ہے تو بہت اچھا۔ اگر نہیں، تو یہ WETHopens 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 سے بہت ملتا جلتا ہے۔ یہ ایک merkle proof بھی چیک کرتا ہے، پہلے کو 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 کا ویرینٹ لگتا ہے جو تمام ونڈوز کا دعوی کرتا ہے۔
نتیجہ
اب تک آپ کو یہ جان لینا چاہیے کہ ان کنٹریکٹس کو کیسے سمجھا جائے جن کا سورس کوڈ دستیاب نہیں ہے، یا تو opcodes کا استعمال کرکے یا (جب یہ کام کرتا ہے) ڈی کمپائلر کا استعمال کرکے۔ جیسا کہ اس مضمون کی طوالت سے ظاہر ہے، ایک کنٹریکٹ کی ریورس انجینئرنگ کوئی معمولی کام نہیں ہے، لیکن ایک ایسے نظام میں جہاں سیکیورٹی ضروری ہے، یہ ایک اہم ہنر ہے کہ کنٹریکٹس کے وعدے کے مطابق کام کرنے کی تصدیق کی جا سکے۔
میرے مزید کام کے لیے یہاں دیکھیں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)



