ایک کنٹریکٹ کی ریورس انجینئرنگ
تعارف
بلاک چین پر کوئی راز نہیں ہوتے، جو کچھ بھی ہوتا ہے وہ مستقل، قابل تصدیق اور عوامی طور پر دستیاب ہوتا ہے۔ مثالی طور پر، کنٹریکٹس کا سورس کوڈ Etherscan پر شائع اور تصدیق شدہ ہونا چاہیے (opens in a new tab)۔ تاہم، ہمیشہ ایسا نہیں ہوتا (opens in a new tab)۔ اس مضمون میں آپ سیکھیں گے کہ سورس کوڈ کے بغیر کسی کنٹریکٹ کو دیکھ کر کنٹریکٹس کی ریورس انجینئرنگ کیسے کی جاتی ہے، 0x2510c039cc3b061d79e564b38836da87e31b342f (opens in a new tab)۔
ریورس کمپائلرز موجود ہیں، لیکن وہ ہمیشہ قابل استعمال نتائج (opens in a new tab) پیدا نہیں کرتے۔ اس مضمون میں آپ سیکھیں گے کہ دستی طور پر ریورس انجینئرنگ کیسے کی جائے اور آپ کوڈز (opens in a new tab) سے کنٹریکٹ کو کیسے سمجھا جائے، نیز ڈی کمپائلر کے نتائج کی تشریح کیسے کی جائے۔
اس مضمون کو سمجھنے کے قابل ہونے کے لیے آپ کو پہلے سے ہی EVM کی بنیادی باتوں کا علم ہونا چاہیے، اور کم از کم EVM اسمبلر سے کچھ حد تک واقف ہونا چاہیے۔ آپ ان موضوعات کے بارے میں یہاں پڑھ سکتے ہیں (opens in a new tab)۔
قابل عمل کوڈ تیار کریں
آپ کنٹریکٹ کے لیے Etherscan پر جا کر، کنٹریکٹ ٹیب پر کلک کر کے اور پھر Switch to Opcodes View پر کلک کر کے آپ کوڈز حاصل کر سکتے ہیں۔ آپ کو ایک ایسا ویو ملے گا جس میں ہر لائن پر ایک آپ کوڈ ہوگا۔
تاہم، jumps کو سمجھنے کے لیے، آپ کو یہ جاننے کی ضرورت ہے کہ کوڈ میں ہر آپ کوڈ کہاں واقع ہے۔ ایسا کرنے کا ایک طریقہ یہ ہے کہ ایک Google Spreadsheet کھولیں اور آپ کوڈز کو کالم C میں پیسٹ کریں۔ آپ اس پہلے سے تیار شدہ اسپریڈشیٹ کی کاپی بنا کر درج ذیل مراحل کو چھوڑ سکتے ہیں (opens in a new tab)۔
اگلا قدم کوڈ کے درست مقامات حاصل کرنا ہے تاکہ ہم jumps کو سمجھ سکیں۔ ہم آپ کوڈ کا سائز کالم B میں، اور مقام (ہیکسا ڈیسیمل میں) کالم A میں رکھیں گے۔ سیل B1 میں یہ فنکشن ٹائپ کریں اور پھر اسے کالم B کے باقی حصوں کے لیے کاپی اور پیسٹ کریں، جب تک کہ کوڈ ختم نہ ہو جائے۔ ایسا کرنے کے بعد آپ کالم B کو چھپا سکتے ہیں۔
=1+IF(REGEXMATCH(C1,"PUSH"),REGEXEXTRACT(C1,"PUSH(\d+)"),0)
سب سے پہلے یہ فنکشن خود آپ کوڈ کے لیے ایک بائٹ کا اضافہ کرتا ہے، اور پھر PUSH کو تلاش کرتا ہے۔ Push آپ کوڈز خاص ہوتے ہیں کیونکہ انہیں پش کی جانے والی ویلیو کے لیے اضافی بائٹس کی ضرورت ہوتی ہے۔ اگر آپ کوڈ PUSH ہے، تو ہم بائٹس کی تعداد نکالتے ہیں اور اسے شامل کرتے ہیں۔
A1 میں پہلا آف سیٹ، صفر رکھیں۔ پھر، A2 میں، یہ فنکشن رکھیں اور اسے دوبارہ کالم A کے باقی حصوں کے لیے کاپی اور پیسٹ کریں:
=dec2hex(hex2dec(A1)+B1)
ہمیں اس فنکشن کی ضرورت ہے تاکہ یہ ہمیں ہیکسا ڈیسیمل ویلیو دے سکے کیونکہ jumps سے پہلے پش کی جانے والی ویلیوز (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 سب صفر ہیں)۔
- کال ڈیٹا کا سائز پڑھیں۔ عام طور پر ایتھریم کنٹریکٹ کا کال ڈیٹا اے بی آئی (ایپلیکیشن بائنری انٹرفیس) (opens in a new tab) کی پیروی کرتا ہے، جس کے لیے فنکشن سلیکٹر کے لیے کم از کم چار بائٹس درکار ہوتے ہیں۔ اگر کال ڈیٹا کا سائز چار سے کم ہے، تو 0x5E پر جمپ کریں۔
0x5E پر ہینڈلر (نان-اے بی آئی کال ڈیٹا کے لیے)
| آفسیٹ | آپ کوڈ |
|---|---|
| 5E | JUMPDEST |
| 5F | CALLDATASIZE |
| 60 | PUSH2 0x007c |
| 63 | JUMPI |
یہ اسنپٹ JUMPDEST کے ساتھ شروع ہوتا ہے۔ ای وی ایم (ایتھریم ورچوئل مشین) پروگرامز ایک ایکسیپشن تھرو کرتے ہیں اگر آپ کسی ایسے آپ کوڈ پر جمپ کرتے ہیں جو JUMPDEST نہیں ہے۔ پھر یہ 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 پر کلک کریں، تو ہم دیکھتے ہیں کہ کال ڈیٹا، جسے ان پٹ ڈیٹا کہا جاتا ہے، واقعی خالی ہے (0x)۔ یہ بھی نوٹ کریں کہ ویلیو 1.559 ETH ہے، جو بعد میں متعلقہ ہوگی۔
اس کے بعد، State ٹیب پر کلک کریں اور اس کنٹریکٹ کو پھیلائیں جسے ہم ریورس انجینئر کر رہے ہیں (0x2510...)۔ آپ دیکھ سکتے ہیں کہ ٹرانزیکشن کے دوران Storage[6] تبدیل ہوا تھا، اور اگر آپ Hex کو Number میں تبدیل کرتے ہیں، تو آپ دیکھیں گے کہ یہ 1,559,000,000,000,000,000 ہو گیا، جو wei میں منتقل کی گئی ویلیو ہے (میں نے وضاحت کے لیے کوما شامل کیے ہیں)، جو اگلی کنٹریکٹ ویلیو کے مساوی ہے۔
اگر ہم اسی عرصے کی دیگر Transfer ٹرانزیکشنز (opens in a new tab) کی وجہ سے ہونے والی حالت کی تبدیلیوں کو دیکھیں تو ہمیں معلوم ہوتا ہے کہ Storage[6] نے کچھ عرصے تک کنٹریکٹ کی ویلیو کو ٹریک کیا۔ فی الحال ہم اسے Value* کہیں گے۔ ایسٹرسک (*) ہمیں یاد دلاتا ہے کہ ہم ابھی تک نہیں جانتے کہ یہ ویری ایبل کیا کرتا ہے، لیکن یہ صرف کنٹریکٹ کی ویلیو کو ٹریک کرنے کے لیے نہیں ہو سکتا کیونکہ اسٹوریج استعمال کرنے کی کوئی ضرورت نہیں ہے، جو کہ بہت مہنگا ہے، جب آپ ADDRESS BALANCE کا استعمال کرتے ہوئے اپنے اکاؤنٹ کا بیلنس حاصل کر سکتے ہیں۔ پہلا آپ کوڈ کنٹریکٹ کا اپنا پتہ پش کرتا ہے۔ دوسرا اسٹیک کے اوپری حصے پر موجود پتہ پڑھتا ہے اور اسے اس پتے کے بیلنس سے بدل دیتا ہے۔
| آفسیٹ | آپ کوڈ | اسٹیک |
|---|---|---|
| 6C | PUSH2 0x0075 | 0x75 Value* CALLVALUE 0 6 CALLVALUE |
| 6F | SWAP2 | CALLVALUE Value* 0x75 0 6 CALLVALUE |
| 70 | SWAP1 | Value* CALLVALUE 0x75 0 6 CALLVALUE |
| 71 | PUSH2 0x01a7 | 0x01A7 Value* CALLVALUE 0x75 0 6 CALLVALUE |
| 74 | JUMP |
ہم جمپ ڈیسٹینیشن پر اس کوڈ کو ٹریس کرنا جاری رکھیں گے۔
| آفسیٹ | آپ کوڈ | اسٹیک |
|---|---|---|
| 1A7 | JUMPDEST | Value* CALLVALUE 0x75 0 6 CALLVALUE |
| 1A8 | PUSH1 0x00 | 0x00 Value* CALLVALUE 0x75 0 6 CALLVALUE |
| 1AA | DUP3 | CALLVALUE 0x00 Value* CALLVALUE 0x75 0 6 CALLVALUE |
| 1AB | NOT | 2^256-CALLVALUE-1 0x00 Value* CALLVALUE 0x75 0 6 CALLVALUE |
NOT بٹ وائز ہے، لہذا یہ کال ویلیو میں ہر بٹ کی ویلیو کو ریورس کر دیتا ہے۔
| آفسیٹ | آپ کوڈ | اسٹیک |
|---|---|---|
| 1AC | DUP3 | Value* 2^256-CALLVALUE-1 0x00 Value* CALLVALUE 0x75 0 6 CALLVALUE |
| 1AD | GT | Value*>2^256-CALLVALUE-1 0x00 Value* CALLVALUE 0x75 0 6 CALLVALUE |
| 1AE | ISZERO | Value*<=2^256-CALLVALUE-1 0x00 Value* CALLVALUE 0x75 0 6 CALLVALUE |
| 1AF | PUSH2 0x01df | 0x01DF Value*<=2^256-CALLVALUE-1 0x00 Value* CALLVALUE 0x75 0 6 CALLVALUE |
| 1B2 | JUMPI |
ہم جمپ کرتے ہیں اگر Value* 2^256-CALLVALUE-1 سے چھوٹا یا اس کے برابر ہے۔ یہ اوور فلو کو روکنے کے لیے لاجک کی طرح لگتا ہے۔ اور واقعی، ہم دیکھتے ہیں کہ چند بے معنی آپریشنز کے بعد (مثال کے طور پر، میموری میں لکھنا ڈیلیٹ ہونے والا ہے) آفسیٹ 0x01DE پر کنٹریکٹ ریورٹ ہو جاتا ہے اگر اوور فلو کا پتہ چلتا ہے، جو کہ ایک عام رویہ ہے۔
نوٹ کریں کہ اس طرح کا اوور فلو انتہائی ناممکن ہے، کیونکہ اس کے لیے کال ویلیو پلس Value* کو 2^256 wei کے برابر ہونا درکار ہوگا، جو تقریباً 10^59 ETH بنتا ہے۔ لکھتے وقت، کل ETH سپلائی دو سو ملین سے کم ہے (opens in a new tab)۔
| آفسیٹ | آپ کوڈ | اسٹیک |
|---|---|---|
| 1DF | JUMPDEST | 0x00 Value* CALLVALUE 0x75 0 6 CALLVALUE |
| 1E0 | POP | Value* CALLVALUE 0x75 0 6 CALLVALUE |
| 1E1 | ADD | Value*+CALLVALUE 0x75 0 6 CALLVALUE |
| 1E2 | SWAP1 | 0x75 Value*+CALLVALUE 0 6 CALLVALUE |
| 1E3 | JUMP |
اگر ہم یہاں پہنچ گئے ہیں، تو Value* + CALLVALUE حاصل کریں اور آفسیٹ 0x75 پر جمپ کریں۔
| آفسیٹ | آپ کوڈ | اسٹیک |
|---|---|---|
| 75 | JUMPDEST | Value*+CALLVALUE 0 6 CALLVALUE |
| 76 | SWAP1 | 0 Value*+CALLVALUE 6 CALLVALUE |
| 77 | SWAP2 | 6 Value*+CALLVALUE 0 CALLVALUE |
| 78 | SSTORE | 0 CALLVALUE |
اگر ہم یہاں پہنچتے ہیں (جس کے لیے کال ڈیٹا کا خالی ہونا ضروری ہے) تو ہم Value* میں کال ویلیو شامل کرتے ہیں۔ یہ اس بات سے مطابقت رکھتا ہے جو ہم کہتے ہیں کہ Transfer ٹرانزیکشنز کرتی ہیں۔
| آفسیٹ | آپ کوڈ |
|---|---|
| 79 | POP |
| 7A | POP |
| 7B | STOP |
آخر میں، اسٹیک کو صاف کریں (جو ضروری نہیں ہے) اور ٹرانزیکشن کے کامیاب اختتام کا سگنل دیں۔
مختصراً، یہاں ابتدائی کوڈ کے لیے ایک فلو چارٹ ہے۔
0x7C پر ہینڈلر
میں نے جان بوجھ کر ہیڈنگ میں یہ نہیں لکھا کہ یہ ہینڈلر کیا کرتا ہے۔ مقصد آپ کو یہ سکھانا نہیں ہے کہ یہ مخصوص کنٹریکٹ کیسے کام کرتا ہے، بلکہ یہ سکھانا ہے کہ کنٹریکٹس کو ریورس انجینئر کیسے کیا جاتا ہے۔ آپ بھی اسی طرح سیکھیں گے جیسے میں نے سیکھا، یعنی کوڈ کی پیروی کر کے۔
ہم یہاں کئی جگہوں سے آتے ہیں:
- اگر 1, 2, یا 3 بائٹس کا کال ڈیٹا موجود ہو (آفسیٹ 0x63 سے)
- اگر میتھڈ کے دستخط نامعلوم ہوں (آفسیٹس 0x42 اور 0x5D سے)
| آفسیٹ | آپ کوڈ | اسٹیک |
|---|---|---|
| 7C | JUMPDEST | |
| 7D | PUSH1 0x00 | 0x00 |
| 7F | PUSH2 0x009d | 0x9D 0x00 |
| 82 | PUSH1 0x03 | 0x03 0x9D 0x00 |
| 84 | SLOAD | Storage[3] 0x9D 0x00 |
یہ ایک اور اسٹوریج سیل ہے، جو مجھے کسی بھی ٹرانزیکشنز میں نہیں مل سکا اس لیے یہ جاننا مشکل ہے کہ اس کا کیا مطلب ہے۔ نیچے دیا گیا کوڈ اسے مزید واضح کر دے گا۔
| آفسیٹ | آپ کوڈ | اسٹیک |
|---|---|---|
| 85 | PUSH20 0xffffffffffffffffffffffffffffffffffffffff | 0xff....ff Storage[3] 0x9D 0x00 |
| 9A | AND | Storage[3]-as-address 0x9D 0x00 |
یہ آپ کوڈز اس ویلیو کو جو ہم Storage[3] سے پڑھتے ہیں، 160 بٹس تک مختصر کر دیتے ہیں، جو کہ ایک ایتھریم کے پتے کی لمبائی ہے۔
| آفسیٹ | آپ کوڈ | اسٹیک |
|---|---|---|
| 9B | SWAP1 | 0x9D Storage[3]-as-address 0x00 |
| 9C | JUMP | Storage[3]-as-address 0x00 |
یہ جمپ غیر ضروری ہے، کیونکہ ہم اگلے آپ کوڈ پر جا رہے ہیں۔ یہ کوڈ گیس کے لحاظ سے اتنا موثر نہیں ہے جتنا اسے ہونا چاہیے تھا۔
| آفسیٹ | آپ کوڈ | اسٹیک |
|---|---|---|
| 9D | JUMPDEST | Storage[3]-as-address 0x00 |
| 9E | SWAP1 | 0x00 Storage[3]-as-address |
| 9F | POP | Storage[3]-as-address |
| A0 | PUSH1 0x40 | 0x40 Storage[3]-as-address |
| A2 | MLOAD | Mem[0x40] Storage[3]-as-address |
کوڈ کے بالکل شروع میں ہم نے Mem[0x40] کو 0x80 پر سیٹ کیا تھا۔ اگر ہم بعد میں 0x40 کو تلاش کریں، تو ہم دیکھتے ہیں کہ ہم اسے تبدیل نہیں کرتے - لہذا ہم فرض کر سکتے ہیں کہ یہ 0x80 ہے۔
| آفسیٹ | آپ کوڈ | اسٹیک |
|---|---|---|
| A3 | CALLDATASIZE | CALLDATASIZE 0x80 Storage[3]-as-address |
| A4 | PUSH1 0x00 | 0x00 CALLDATASIZE 0x80 Storage[3]-as-address |
| A6 | DUP3 | 0x80 0x00 CALLDATASIZE 0x80 Storage[3]-as-address |
| A7 | CALLDATACOPY | 0x80 Storage[3]-as-address |
تمام کال ڈیٹا کو میموری میں کاپی کریں، جس کی شروعات 0x80 سے ہوتی ہے۔
| آفسیٹ | آپ کوڈ | اسٹیک |
|---|---|---|
| A8 | PUSH1 0x00 | 0x00 0x80 Storage[3]-as-address |
| AA | DUP1 | 0x00 0x00 0x80 Storage[3]-as-address |
| AB | CALLDATASIZE | CALLDATASIZE 0x00 0x00 0x80 Storage[3]-as-address |
| AC | DUP4 | 0x80 CALLDATASIZE 0x00 0x00 0x80 Storage[3]-as-address |
| AD | DUP6 | Storage[3]-as-address 0x80 CALLDATASIZE 0x00 0x00 0x80 Storage[3]-as-address |
| AE | GAS | GAS Storage[3]-as-address 0x80 CALLDATASIZE 0x00 0x00 0x80 Storage[3]-as-address |
| AF | DELEGATE_CALL |
اب چیزیں بہت زیادہ واضح ہیں۔ یہ کنٹریکٹ ایک پراکسی (opens in a new tab) کے طور پر کام کر سکتا ہے، جو اصل کام کرنے کے لیے Storage[3] میں موجود پتے کو کال کرتا ہے۔ DELEGATE_CALL ایک الگ کنٹریکٹ کو کال کرتا ہے، لیکن اسی اسٹوریج میں رہتا ہے۔ اس کا مطلب یہ ہے کہ ڈیلیگیٹڈ کنٹریکٹ، جس کے لیے ہم ایک پراکسی ہیں، اسی اسٹوریج اسپیس تک رسائی حاصل کرتا ہے۔ کال کے لیے پیرامیٹرز یہ ہیں:
- گیس: تمام باقی ماندہ گیس
- کال کیا گیا پتہ: Storage[3]-as-address
- کال ڈیٹا: CALLDATASIZE بائٹس جو 0x80 سے شروع ہوتی ہیں، جہاں ہم نے اصل کال ڈیٹا رکھا تھا
- ریٹرن ڈیٹا: کوئی نہیں (0x00 - 0x00) ہم ریٹرن ڈیٹا دیگر ذرائع سے حاصل کریں گے (نیچے دیکھیں)
| آفسیٹ | آپ کوڈ | اسٹیک |
|---|---|---|
| B0 | RETURNDATASIZE | RETURNDATASIZE (((call success/failure))) 0x80 Storage[3]-as-address |
| B1 | DUP1 | RETURNDATASIZE RETURNDATASIZE (((call success/failure))) 0x80 Storage[3]-as-address |
| B2 | PUSH1 0x00 | 0x00 RETURNDATASIZE RETURNDATASIZE (((call success/failure))) 0x80 Storage[3]-as-address |
| B4 | DUP5 | 0x80 0x00 RETURNDATASIZE RETURNDATASIZE (((call success/failure))) 0x80 Storage[3]-as-address |
| B5 | RETURNDATACOPY | RETURNDATASIZE (((call success/failure))) 0x80 Storage[3]-as-address |
یہاں ہم تمام ریٹرن ڈیٹا کو میموری بفر میں کاپی کرتے ہیں جس کی شروعات 0x80 سے ہوتی ہے۔
| آفسیٹ | آپ کوڈ | اسٹیک |
|---|---|---|
| B6 | DUP2 | (((call success/failure))) RETURNDATASIZE (((call success/failure))) 0x80 Storage[3]-as-address |
| B7 | DUP1 | (((call success/failure))) (((call success/failure))) RETURNDATASIZE (((call success/failure))) 0x80 Storage[3]-as-address |
| B8 | ISZERO | (((did the call fail))) (((call success/failure))) RETURNDATASIZE (((call success/failure))) 0x80 Storage[3]-as-address |
| B9 | PUSH2 0x00c0 | 0xC0 (((did the call fail))) (((call success/failure))) RETURNDATASIZE (((call success/failure))) 0x80 Storage[3]-as-address |
| BC | JUMPI | (((call success/failure))) RETURNDATASIZE (((call success/failure))) 0x80 Storage[3]-as-address |
| BD | DUP2 | RETURNDATASIZE (((call success/failure))) RETURNDATASIZE (((call success/failure))) 0x80 Storage[3]-as-address |
| BE | DUP5 | 0x80 RETURNDATASIZE (((call success/failure))) RETURNDATASIZE (((call success/failure))) 0x80 Storage[3]-as-address |
| BF | RETURN |
لہذا کال کے بعد ہم ریٹرن ڈیٹا کو بفر 0x80 - 0x80+RETURNDATASIZE میں کاپی کرتے ہیں، اور اگر کال کامیاب ہو جاتی ہے تو ہم بالکل اسی بفر کے ساتھ RETURN کرتے ہیں۔
DELEGATECALL ناکام ہو گیا
اگر ہم یہاں، 0xC0 پر پہنچتے ہیں، تو اس کا مطلب ہے کہ جس کنٹریکٹ کو ہم نے کال کیا تھا وہ ریورٹ ہو گیا۔ چونکہ ہم اس کنٹریکٹ کے لیے صرف ایک پراکسی ہیں، ہم وہی ڈیٹا واپس کرنا چاہتے ہیں اور ساتھ ہی ریورٹ بھی کرنا چاہتے ہیں۔
| آفسیٹ | آپ کوڈ | اسٹیک |
|---|---|---|
| C0 | JUMPDEST | (((call success/failure))) RETURNDATASIZE (((call success/failure))) 0x80 Storage[3]-as-address |
| C1 | DUP2 | RETURNDATASIZE (((call success/failure))) RETURNDATASIZE (((call success/failure))) 0x80 Storage[3]-as-address |
| C2 | DUP5 | 0x80 RETURNDATASIZE (((call success/failure))) RETURNDATASIZE (((call success/failure))) 0x80 Storage[3]-as-address |
| C3 | REVERT |
لہذا ہم اسی بفر کے ساتھ REVERT کرتے ہیں جسے ہم نے پہلے RETURN کے لیے استعمال کیا تھا: 0x80 - 0x80+RETURNDATASIZE
ABI کالز
اگر کال ڈیٹا کا سائز چار بائٹس یا اس سے زیادہ ہے تو یہ ایک درست ABI کال ہو سکتی ہے۔
| آفسیٹ | آپ کوڈ | اسٹیک |
|---|---|---|
| D | PUSH1 0x00 | 0x00 |
| F | CALLDATALOAD | (((کال ڈیٹا کا پہلا لفظ (256 bits)))) |
| 10 | PUSH1 0xe0 | 0xE0 (((کال ڈیٹا کا پہلا لفظ (256 bits)))) |
| 12 | SHR | (((کال ڈیٹا کے پہلے 32 bits (4 bytes)))) |
Etherscan ہمیں بتاتا ہے کہ 1C ایک نامعلوم آپ کوڈ ہے، کیونکہ اسے Etherscan کے اس فیچر کو بنانے کے بعد شامل کیا گیا تھا (opens in a new tab) اور انہوں نے اسے اپ ڈیٹ نہیں کیا ہے۔ ایک اپ ٹو ڈیٹ آپ کوڈ ٹیبل (opens in a new tab) ہمیں دکھاتا ہے کہ یہ شفٹ رائٹ (shift right) ہے۔
| آفسیٹ | آپ کوڈ | اسٹیک |
|---|---|---|
| 13 | DUP1 | (((کال ڈیٹا کے پہلے 32 bits (4 bytes)))) (((کال ڈیٹا کے پہلے 32 bits (4 bytes)))) |
| 14 | PUSH4 0x3cd8045e | 0x3CD8045E (((کال ڈیٹا کے پہلے 32 bits (4 bytes)))) (((کال ڈیٹا کے پہلے 32 bits (4 bytes)))) |
| 19 | GT | 0x3CD8045E>first-32-bits-of-the-call-data (((کال ڈیٹا کے پہلے 32 bits (4 bytes)))) |
| 1A | PUSH2 0x0043 | 0x43 0x3CD8045E>first-32-bits-of-the-call-data (((کال ڈیٹا کے پہلے 32 bits (4 bytes)))) |
| 1D | JUMPI | (((کال ڈیٹا کے پہلے 32 bits (4 bytes)))) |
میتھڈ کے دستخط کے میچنگ ٹیسٹس کو اس طرح دو حصوں میں تقسیم کرنے سے اوسطاً آدھے ٹیسٹس کی بچت ہوتی ہے۔ وہ کوڈ جو اس کے فوراً بعد آتا ہے اور 0x43 میں موجود کوڈ اسی پیٹرن کی پیروی کرتے ہیں: کال ڈیٹا کے پہلے 32 bits کو DUP1 کریں، PUSH4 (((method signature> کریں، برابری چیک کرنے کے لیے EQ چلائیں، اور پھر اگر میتھڈ کے دستخط میچ کر جائیں تو JUMPI کریں۔ یہاں میتھڈ کے دستخط، ان کے پتے، اور اگر معلوم ہو تو متعلقہ میتھڈ کی تعریف (opens in a new tab) دی گئی ہے:
| میتھڈ | میتھڈ کے دستخط | جمپ کرنے کے لیے آفسیٹ |
|---|---|---|
| splitter() (opens in a new tab) | 0x3cd8045e | 0x0103 |
| ??? | 0x81e580d3 | 0x0138 |
| currentWindow() (opens in a new tab) | 0xba0bafb4 | 0x0158 |
| ??? | 0x1f135823 | 0x00C4 |
| merkleRoot() (opens in a new tab) | 0x2eb4a7ab | 0x00ED |
اگر کوئی میچ نہیں ملتا، تو کوڈ 0x7C پر موجود پراکسی ہینڈلر پر جمپ کر جاتا ہے، اس امید کے ساتھ کہ جس کنٹریکٹ کے لیے ہم پراکسی ہیں اس میں کوئی میچ موجود ہو۔
splitter()
| آفسیٹ | آپ کوڈ | اسٹیک |
|---|---|---|
| 103 | JUMPDEST | |
| 104 | CALLVALUE | CALLVALUE |
| 105 | DUP1 | CALLVALUE CALLVALUE |
| 106 | ISZERO | CALLVALUE==0 CALLVALUE |
| 107 | PUSH2 0x010f | 0x010F CALLVALUE==0 CALLVALUE |
| 10A | JUMPI | CALLVALUE |
| 10B | PUSH1 0x00 | 0x00 CALLVALUE |
| 10D | DUP1 | 0x00 0x00 CALLVALUE |
| 10E | REVERT |
یہ فنکشن سب سے پہلے یہ چیک کرتا ہے کہ کال نے کوئی ETH نہیں بھیجا ہے۔ یہ فنکشن payable (opens in a new tab) نہیں ہے۔ اگر کسی نے ہمیں ETH بھیجا ہے تو یہ یقیناً ایک غلطی ہوگی اور ہم REVERT کرنا چاہتے ہیں تاکہ وہ ETH ایسی جگہ نہ پھنس جائے جہاں سے وہ اسے واپس نہ لے سکیں۔
| آفسیٹ | آپ کوڈ | اسٹیک |
|---|---|---|
| 10F | JUMPDEST | |
| 110 | POP | |
| 111 | PUSH1 0x03 | 0x03 |
| 113 | SLOAD | (((Storage[3] یعنی وہ کنٹریکٹ جس کے لیے ہم پراکسی ہیں))) |
| 114 | PUSH1 0x40 | 0x40 (((Storage[3] یعنی وہ کنٹریکٹ جس کے لیے ہم پراکسی ہیں))) |
| 116 | MLOAD | 0x80 (((Storage[3] یعنی وہ کنٹریکٹ جس کے لیے ہم پراکسی ہیں))) |
| 117 | PUSH20 0xffffffffffffffffffffffffffffffffffffffff | 0xFF...FF 0x80 (((Storage[3] یعنی وہ کنٹریکٹ جس کے لیے ہم پراکسی ہیں))) |
| 12C | SWAP1 | 0x80 0xFF...FF (((Storage[3] یعنی وہ کنٹریکٹ جس کے لیے ہم پراکسی ہیں))) |
| 12D | SWAP2 | (((Storage[3] یعنی وہ کنٹریکٹ جس کے لیے ہم پراکسی ہیں))) 0xFF...FF 0x80 |
| 12E | AND | ProxyAddr 0x80 |
| 12F | DUP2 | 0x80 ProxyAddr 0x80 |
| 130 | MSTORE | 0x80 |
اور اب 0x80 میں پراکسی کا پتہ موجود ہے۔
| آفسیٹ | آپ کوڈ | اسٹیک |
|---|---|---|
| 131 | PUSH1 0x20 | 0x20 0x80 |
| 133 | ADD | 0xA0 |
| 134 | PUSH2 0x00e4 | 0xE4 0xA0 |
| 137 | JUMP | 0xA0 |
E4 کوڈ
یہ پہلی بار ہے جب ہم ان لائنوں کو دیکھ رہے ہیں، لیکن یہ دیگر میتھڈز کے ساتھ شیئر کی گئی ہیں (نیچے دیکھیں)۔ لہذا ہم اسٹیک میں موجود ویلیو کو X کہیں گے، اور بس یہ یاد رکھیں کہ splitter() میں اس X کی ویلیو 0xA0 ہے۔
| آفسیٹ | آپ کوڈ | اسٹیک |
|---|---|---|
| E4 | JUMPDEST | X |
| E5 | PUSH1 0x40 | 0x40 X |
| E7 | MLOAD | 0x80 X |
| E8 | DUP1 | 0x80 0x80 X |
| E9 | SWAP2 | X 0x80 0x80 |
| EA | SUB | X-0x80 0x80 |
| EB | SWAP1 | 0x80 X-0x80 |
| EC | RETURN |
لہذا یہ کوڈ اسٹیک (X) میں ایک میموری پوائنٹر وصول کرتا ہے، اور کنٹریکٹ کو ایک بفر کے ساتھ RETURN کرنے کا سبب بنتا ہے جو 0x80 - X ہے۔
splitter() کے معاملے میں، یہ وہ پتہ واپس کرتا ہے جس کے لیے ہم پراکسی ہیں۔ RETURN بفر کو 0x80-0x9F میں واپس کرتا ہے، جو وہ جگہ ہے جہاں ہم نے یہ ڈیٹا لکھا تھا (اوپر آفسیٹ 0x130)۔
currentWindow()
0x158-0x163 آفسیٹس میں موجود کوڈ بالکل ویسا ہی ہے جیسا ہم نے 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 پر جمپس اسٹیک کے اوپری حصے (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 بائٹس (ایک ورڈ) کا کال ڈیٹا لیتا ہے۔
| آف سیٹ | آپ کوڈ | اسٹیک |
|---|---|---|
| 19D | DUP1 | 0x00 0x00 0x04 CALLDATASIZE 0x0153 0xDA |
| 19E | DUP2 | 0x00 0x00 0x00 0x04 CALLDATASIZE 0x0153 0xDA |
| 19F | REVERT |
اگر اسے کال ڈیٹا نہیں ملتا ہے تو ٹرانزیکشن کسی بھی ریٹرن ڈیٹا کے بغیر ریورٹ ہو جاتی ہے۔
آئیے دیکھتے ہیں کہ اگر فنکشن کو اپنی ضرورت کا کال ڈیٹا مل جائے تو کیا ہوتا ہے۔
| آف سیٹ | آپ کوڈ | اسٹیک |
|---|---|---|
| 1A0 | JUMPDEST | 0x00 0x04 CALLDATASIZE 0x0153 0xDA |
| 1A1 | POP | 0x04 CALLDATASIZE 0x0153 0xDA |
| 1A2 | CALLDATALOAD | calldataload(4) CALLDATASIZE 0x0153 0xDA |
calldataload(4) میتھڈ کے دستخط کے بعد کال ڈیٹا کا پہلا ورڈ ہے۔
| آف سیٹ | آپ کوڈ | اسٹیک |
|---|---|---|
| 1A3 | SWAP2 | 0x0153 CALLDATASIZE calldataload(4) 0xDA |
| 1A4 | SWAP1 | CALLDATASIZE 0x0153 calldataload(4) 0xDA |
| 1A5 | POP | 0x0153 calldataload(4) 0xDA |
| 1A6 | JUMP | calldataload(4) 0xDA |
| 153 | JUMPDEST | calldataload(4) 0xDA |
| 154 | PUSH2 0x016e | 0x016E calldataload(4) 0xDA |
| 157 | JUMP | calldataload(4) 0xDA |
| 16E | JUMPDEST | calldataload(4) 0xDA |
| 16F | PUSH1 0x04 | 0x04 calldataload(4) 0xDA |
| 171 | DUP2 | calldataload(4) 0x04 calldataload(4) 0xDA |
| 172 | DUP2 | 0x04 calldataload(4) 0x04 calldataload(4) 0xDA |
| 173 | SLOAD | Storage[4] calldataload(4) 0x04 calldataload(4) 0xDA |
| 174 | DUP2 | calldataload(4) Storage[4] calldataload(4) 0x04 calldataload(4) 0xDA |
| 175 | LT | calldataload(4)<Storage[4] calldataload(4) 0x04 calldataload(4) 0xDA |
| 176 | PUSH2 0x017e | 0x017EC calldataload(4)<Storage[4] calldataload(4) 0x04 calldataload(4) 0xDA |
| 179 | JUMPI | calldataload(4) 0x04 calldataload(4) 0xDA |
اگر پہلا ورڈ Storage[4] سے کم نہیں ہے، تو فنکشن ناکام ہو جاتا ہے۔ یہ کسی بھی ریٹرن ویلیو کے بغیر ریورٹ ہو جاتا ہے:
| آف سیٹ | آپ کوڈ | اسٹیک |
|---|---|---|
| 17A | PUSH1 0x00 | 0x00 ... |
| 17C | DUP1 | 0x00 0x00 ... |
| 17D | REVERT |
اگر calldataload(4) کی قدر Storage[4] سے کم ہے، تو ہمیں یہ کوڈ ملتا ہے:
| آف سیٹ | آپ کوڈ | اسٹیک |
|---|---|---|
| 17E | JUMPDEST | calldataload(4) 0x04 calldataload(4) 0xDA |
| 17F | PUSH1 0x00 | 0x00 calldataload(4) 0x04 calldataload(4) 0xDA |
| 181 | SWAP2 | 0x04 calldataload(4) 0x00 calldataload(4) 0xDA |
| 182 | DUP3 | 0x00 0x04 calldataload(4) 0x00 calldataload(4) 0xDA |
| 183 | MSTORE | calldataload(4) 0x00 calldataload(4) 0xDA |
اور میموری لوکیشنز 0x00-0x1F میں اب ڈیٹا 0x04 موجود ہے (0x00-0x1E تمام صفر ہیں، 0x1F چار ہے)
| آف سیٹ | آپ کوڈ | اسٹیک |
|---|---|---|
| 184 | PUSH1 0x20 | 0x20 calldataload(4) 0x00 calldataload(4) 0xDA |
| 186 | SWAP1 | calldataload(4) 0x20 0x00 calldataload(4) 0xDA |
| 187 | SWAP2 | 0x00 0x20 calldataload(4) calldataload(4) 0xDA |
| 188 | SHA3 | (((SHA3 of 0x00-0x1F))) calldataload(4) calldataload(4) 0xDA |
| 189 | ADD | (((SHA3 of 0x00-0x1F)))+calldataload(4) calldataload(4) 0xDA |
| 18A | SLOAD | Storage[(((SHA3 of 0x00-0x1F))) + calldataload(4)] calldataload(4) 0xDA |
لہذا اسٹوریج میں ایک لوک اپ ٹیبل ہے، جو 0x000...0004 کے SHA3 سے شروع ہوتا ہے اور اس میں ہر جائز کال ڈیٹا ویلیو (Storage[4] سے کم ویلیو) کے لیے ایک اندراج موجود ہے۔
| آف سیٹ | آپ کوڈ | اسٹیک |
|---|---|---|
| 18B | SWAP1 | calldataload(4) Storage[(((SHA3 of 0x00-0x1F))) + calldataload(4)] 0xDA |
| 18C | POP | Storage[(((SHA3 of 0x00-0x1F))) + calldataload(4)] 0xDA |
| 18D | DUP2 | 0xDA Storage[(((SHA3 of 0x00-0x1F))) + calldataload(4)] 0xDA |
| 18E | JUMP | Storage[(((SHA3 of 0x00-0x1F))) + calldataload(4)] 0xDA |
ہم پہلے ہی جانتے ہیں کہ آف سیٹ 0xDA پر موجود کوڈ کیا کرتا ہے، یہ کالر کو اسٹیک کی ٹاپ ویلیو واپس کرتا ہے۔ لہذا یہ فنکشن لوک اپ ٹیبل سے کالر کو ویلیو واپس کرتا ہے۔
0x1f135823
0xC4-0xCF آفسیٹس میں موجود کوڈ بالکل ویسا ہی ہے جیسا ہم نے 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* واپس کرتا ہے۔
میتھڈ کا خلاصہ
کیا آپ کو لگتا ہے کہ آپ اس مقام پر کنٹریکٹ کو سمجھ گئے ہیں؟ مجھے تو نہیں لگتا۔ اب تک ہمارے پاس یہ میتھڈز ہیں:
| میتھڈ | مطلب |
|---|---|
| Transfer | کال کے ذریعے فراہم کردہ ویلیو کو قبول کریں اور 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)
اور یہ جن طریقوں (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 |
ہم نیچے والے چار طریقوں کو نظر انداز کر سکتے ہیں کیونکہ ہم کبھی ان تک نہیں پہنچ پائیں گے۔ ان کے دستخط کچھ اس طرح کے ہیں کہ ہمارا اصل کنٹریکٹ خود ہی ان کو سنبھال لیتا ہے (آپ تفصیلات دیکھنے کے لیے اوپر دیے گئے دستخطوں پر کلک کر سکتے ہیں)، اس لیے یہ لازمی طور پر اوور رائیڈ کیے گئے طریقے (methods that are overridden) (opens in a new tab) ہوں گے۔
باقی ماندہ طریقوں میں سے ایک claim(<params>) ہے، اور دوسرا isClaimed(<params>) ہے، اس لیے یہ ایک ایئر ڈراپ کنٹریکٹ لگتا ہے۔ باقی ماندہ کو ایک ایک آپ کوڈ کر کے دیکھنے کے بجائے، ہم ڈی کمپائلر آزما سکتے ہیں (opens in a new tab)، جو اس کنٹریکٹ کے تین فنکشنز کے لیے قابل استعمال نتائج پیدا کرتا ہے۔ باقی فنکشنز کی ریورس انجینئرنگ قاری کے لیے ایک مشق کے طور پر چھوڑی گئی ہے۔
scaleAmountByPercentage
اس فنکشن کے لیے ڈی کمپائلر ہمیں یہ دیتا ہے:
def unknown8ffb5c97(uint256 _param1, uint256 _param2) payable:
require calldata.size - 4 >=′ 64
if _param1 and _param2 > -1 / _param1:
revert with 0, 17
return (_param1 * _param2 / 100 * 10^6)
پہلا require یہ جانچتا ہے کہ کال ڈیٹا میں، فنکشن کے دستخط کے چار بائٹس کے علاوہ، کم از کم 64 بائٹس موجود ہیں، جو دو پیرامیٹرز کے لیے کافی ہیں۔ اگر ایسا نہیں ہے تو ظاہر ہے کہ کچھ گڑبڑ ہے۔
if اسٹیٹمنٹ بظاہر یہ چیک کرتی ہے کہ _param1 صفر نہیں ہے، اور یہ کہ _param1 * _param2 منفی نہیں ہے۔ یہ غالباً ریپ اراؤنڈ (wrap around) کے معاملات کو روکنے کے لیے ہے۔
آخر میں، فنکشن ایک اسکیلڈ ویلیو (scaled value) واپس کرتا ہے۔
claim
ڈی کمپائلر جو کوڈ بناتا ہے وہ پیچیدہ ہے، اور اس کا سارا حصہ ہمارے لیے متعلقہ نہیں ہے۔ میں اس میں سے کچھ کو چھوڑنے جا رہا ہوں تاکہ ان لائنوں پر توجہ مرکوز کر سکوں جو میرے خیال میں مفید معلومات فراہم کرتی ہیں
def unknown2e7ba6ef(uint256 _param1, uint256 _param2, uint256 _param3, array _param4) payable:
...
require _param2 == addr(_param2)
...
if currentWindow <= _param1:
revert with 0, 'cannot claim for a future window'
ہم یہاں دو اہم چیزیں دیکھتے ہیں:
_param2، اگرچہ اسےuint256کے طور پر ڈکلیئر کیا گیا ہے، دراصل ایک پتہ ہے۔_param1وہ ونڈو ہے جس کا دعویٰ کیا جا رہا ہے، جسےcurrentWindowیا اس سے پہلے کا ہونا چاہیے۔
...
if stor5[_claimWindow][addr(_claimFor)]:
revert with 0, 'Account already claimed the given window'
تو اب ہم جانتے ہیں کہ Storage[5] ونڈوز اور پتوں کی ایک ایرے (array) ہے، اور یہ کہ آیا اس پتے نے اس ونڈو کے لیے انعام کا دعویٰ کیا ہے۔
...
idx = 0
s = 0
while idx < _param4.length:
...
if s + sha3(mem[(32 * _param4.length) + 328 len mem[(32 * _param4.length) + 296]]) > mem[(32 * idx) + 296]:
mem[mem[64] + 32] = mem[(32 * idx) + 296]
...
s = sha3(mem[_62 + 32 len mem[_62]])
continue
...
s = sha3(mem[_66 + 32 len mem[_66]])
continue
if unknown2eb4a7ab != s:
revert with 0, 'Invalid proof'
ہم جانتے ہیں کہ unknown2eb4a7ab دراصل فنکشن merkleRoot() ہے، اس لیے یہ کوڈ ایسا لگتا ہے کہ یہ ایک مرکل ثبوت (opens in a new tab) کی تصدیق کر رہا ہے۔ اس کا مطلب ہے کہ _param4 ایک مرکل ثبوت ہے۔
call addr(_param2) with:
value unknown81e580d3[_param1] * _param3 / 100 * 10^6 wei
gas 30000 wei
اس طرح ایک کنٹریکٹ اپنا ETH کسی دوسرے پتے (کنٹریکٹ یا بیرونی ملکیت والے) کو منتقل کرتا ہے۔ یہ اسے ایک ویلیو کے ساتھ کال کرتا ہے جو کہ منتقل کی جانے والی رقم ہوتی ہے۔ تو ایسا لگتا ہے کہ یہ ETH کا ایک ایئر ڈراپ ہے۔
if not return_data.size:
if not ext_call.success:
require ext_code.size(stor2)
call stor2.deposit() with:
value unknown81e580d3[_param1] * _param3 / 100 * 10^6 wei
نیچے کی دو لائنیں ہمیں بتاتی ہیں کہ Storage[2] بھی ایک کنٹریکٹ ہے جسے ہم کال کرتے ہیں۔ اگر ہم کنسٹرکٹر ٹرانزیکشن کو دیکھیں (opens in a new tab) تو ہم دیکھتے ہیں کہ یہ کنٹریکٹ 0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2 (opens in a new tab) ہے، جو کہ ایک ریپڈ ایتھر (ڈبلیو ایتھ) کنٹریکٹ ہے جس کا سورس کوڈ Etherscan پر اپ لوڈ کیا گیا ہے (opens in a new tab)۔
تو ایسا لگتا ہے کہ کنٹریکٹ _param2 کو ETH بھیجنے کی کوشش کرتا ہے۔ اگر یہ ایسا کر سکتا ہے، تو بہت اچھا۔ اگر نہیں، تو یہ WETH (opens in a new tab) بھیجنے کی کوشش کرتا ہے۔ اگر _param2 ایک بیرونی ملکیت والا اکاؤنٹ (EOA) ہے تو یہ ہمیشہ ETH وصول کر سکتا ہے، لیکن کنٹریکٹس ETH وصول کرنے سے انکار کر سکتے ہیں۔ تاہم، WETH ایک ERC-20 ہے اور کنٹریکٹس اسے قبول کرنے سے انکار نہیں کر سکتے۔
...
log 0xdbd5389f: addr(_param2), unknown81e580d3[_param1] * _param3 / 100 * 10^6, bool(ext_call.success)
فنکشن کے آخر میں ہم دیکھتے ہیں کہ ایک لاگ انٹری تیار کی جا رہی ہے۔ تیار کردہ لاگ انٹریز کو دیکھیں (opens in a new tab) اور اس ٹاپک پر فلٹر کریں جو 0xdbd5... سے شروع ہوتا ہے۔ اگر ہم ایسی انٹری تیار کرنے والی ٹرانزیکشنز میں سے کسی ایک پر کلک کریں (opens in a new tab) تو ہم دیکھتے ہیں کہ واقعی یہ ایک دعویٰ لگتا ہے - اکاؤنٹ نے اس کنٹریکٹ کو ایک پیغام بھیجا جس کی ہم ریورس انجینئرنگ کر رہے ہیں، اور بدلے میں ETH حاصل کیا۔
1e7df9d3
یہ فنکشن اوپر دیے گئے claim سے بہت ملتا جلتا ہے۔ یہ بھی ایک مرکل ثبوت کی جانچ کرتا ہے، پہلے کو ETH منتقل کرنے کی کوشش کرتا ہے، اور اسی قسم کی لاگ انٹری تیار کرتا ہے۔
def unknown1e7df9d3(uint256 _param1, uint256 _param2, array _param3) payable:
...
idx = 0
s = 0
while idx < _param3.length:
if idx >= mem[96]:
revert with 0, 50
_55 = mem[(32 * idx) + 128]
if s + sha3(mem[(32 * _param3.length) + 160 len mem[(32 * _param3.length) + 128]]) > mem[(32 * idx) + 128]:
...
s = sha3(mem[_58 + 32 len mem[_58]])
continue
mem[mem[64] + 32] = s + sha3(mem[(32 * _param3.length) + 160 len mem[(32 * _param3.length) + 128]])
...
if unknown2eb4a7ab != s:
revert with 0, 'Invalid proof'
...
call addr(_param1) with:
value s wei
gas 30000 wei
if not return_data.size:
if not ext_call.success:
require ext_code.size(stor2)
call stor2.deposit() with:
value s wei
gas gas_remaining wei
...
log 0xdbd5389f: addr(_param1), s, bool(ext_call.success)
بنیادی فرق یہ ہے کہ پہلا پیرامیٹر، یعنی نکالنے کے لیے ونڈو، وہاں موجود نہیں ہے۔ اس کے بجائے، ان تمام ونڈوز پر ایک لوپ (loop) ہے جن کا دعویٰ کیا جا سکتا ہے۔
idx = 0
s = 0
while idx < currentWindow:
...
if stor5[mem[0]]:
if idx == -1:
revert with 0, 17
idx = idx + 1
s = s
continue
...
stor5[idx][addr(_param1)] = 1
if idx >= unknown81e580d3.length:
revert with 0, 50
mem[0] = 4
if unknown81e580d3[idx] and _param2 > -1 / unknown81e580d3[idx]:
revert with 0, 17
if s > !(unknown81e580d3[idx] * _param2 / 100 * 10^6):
revert with 0, 17
if idx == -1:
revert with 0, 17
idx = idx + 1
s = s + (unknown81e580d3[idx] * _param2 / 100 * 10^6)
continue
تو یہ claim کا ایک ایسا ویریئنٹ (variant) لگتا ہے جو تمام ونڈوز کا دعویٰ کرتا ہے۔
نتیجہ
اب تک آپ کو یہ جان لینا چاہیے کہ ان کنٹریکٹس کو کیسے سمجھا جائے جن کا سورس کوڈ دستیاب نہیں ہے، اس کے لیے یا تو آپ کوڈز کا استعمال کیا جاتا ہے یا (جب یہ کام کرے) ڈی کمپائلر کا۔ جیسا کہ اس مضمون کی طوالت سے ظاہر ہے، کسی کنٹریکٹ کی ریورس انجینئرنگ کوئی معمولی کام نہیں ہے، لیکن ایک ایسے سسٹم میں جہاں سیکیورٹی انتہائی ضروری ہو، یہ تصدیق کرنے کے قابل ہونا کہ کنٹریکٹس وعدے کے مطابق کام کرتے ہیں، ایک اہم مہارت ہے۔
میرے مزید کام کے لیے یہاں دیکھیں (opens in a new tab)۔
صفحہ کی آخری اپ ڈیٹ: ۳ اپریل، ۲۰۲۶



![Storage[6] میں تبدیلی](/_next/image/?url=%2Fcontent%2Fdevelopers%2Ftutorials%2Freverse-engineering-a-contract%2Fstorage6.png&w=1920&q=75)



