اہم مواد پر جائیں

ایک کنٹریکٹ کی ریورس انجینئرنگ

evm
opcodes
ترقی
Ori Pomerantz
30 دسمبر، 2021
35 منٹ کی پڑھائی

تعارف

بلاک چین پر کوئی راز نہیں ہیں، جو کچھ بھی ہوتا ہے وہ مستقل، قابل تصدیق، اور عوامی طور پر دستیاب ہے۔ مثالی طور پر، کنٹریکٹس کا سورس کوڈ 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 ہوتا ہے۔

Etherscan سے 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 کے بعد)
0PUSH1 0x800x80
2PUSH1 0x400x40, 0x80
4MSTOREخالی
5PUSH1 0x040x04
7CALLDATASIZECALLDATASIZE 0x04
8LTCALLDATASIZE<4
9PUSH2 0x005e0x5E CALLDATASIZE<4
CJUMPIخالی

یہ کوڈ دو کام کرتا ہے:

  1. 0x80 کو 32 بائٹ ویلیو کے طور پر میموری کے مقامات 0x40-0x5F پر لکھیں (0x80 کو 0x5F میں اسٹور کیا جاتا ہے، اور 0x40-0x5E سب صفر ہیں)۔
  2. calldata کا سائز پڑھیں۔ عام طور پر ایک Ethereum کنٹریکٹ کے لیے کال ڈیٹا ABI (ایپلیکیشن بائنری انٹرفیس)opens in a new tab کی پیروی کرتا ہے، جس کے لیے فنکشن سلیکٹر کے لیے کم از کم چار بائٹس کی ضرورت ہوتی ہے۔ اگر کال ڈیٹا کا سائز چار سے کم ہے، تو 0x5E پر جمپ کریں۔

اس حصے کے لیے فلو چارٹ

0x5E پر ہینڈلر (غیر-ABI کال ڈیٹا کے لیے)

آفسیٹOpcode
5EJUMPDEST
5FCALLDATASIZE
60PUSH2 0x007c
63JUMPI

یہ سنیپٹ JUMPDEST سے شروع ہوتا ہے۔ EVM (Ethereum ورچوئل مشین) پروگرام ایک استثناء (exception) دیتے ہیں اگر آپ کسی ایسے opcode پر جمپ کرتے ہیں جو JUMPDEST نہیں ہے۔ پھر یہ CALLDATASIZE کو دیکھتا ہے، اور اگر یہ "درست" ہے (یعنی، صفر نہیں ہے) تو 0x7C پر جمپ کرتا ہے۔ ہم اس پر نیچے بات کریں گے۔

آفسیٹOpcodeاسٹیک (opcode کے بعد)
64CALLVALUEکال کے ذریعے فراہم کردہ ۔ Solidity میں msg.value کہلاتا ہے۔
65PUSH1 0x066 CALLVALUE
67PUSH1 0x000 6 CALLVALUE
69DUP3CALLVALUE 0 6 CALLVALUE
6ADUP36 CALLVALUE 0 6 CALLVALUE
6BSLOADStorage[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 میں منتقل کی گئی ویلیو ہے (میں نے وضاحت کے لیے کوما شامل کیے ہیں)، اور یہ اگلے کنٹریکٹ کی ویلیو کے مطابق ہے۔

Storage[6] میں تبدیلی

اگر ہم اسی مدت کی دیگر Transfer ٹرانزیکشنزopens in a new tab کی وجہ سے ہونے والی اسٹیٹ تبدیلیوں کو دیکھیں تو ہم دیکھتے ہیں کہ Storage[6] نے کچھ وقت کے لیے کنٹریکٹ کی ویلیو کو ٹریک کیا۔ ابھی کے لیے ہم اسے Value* کہیں گے۔ ستارے کا نشان (*) ہمیں یاد دلاتا ہے کہ ہم ابھی تک نہیں جانتے کہ یہ متغیر کیا کرتا ہے، لیکن یہ صرف کنٹریکٹ کی ویلیو کو ٹریک کرنے کے لیے نہیں ہو سکتا کیونکہ اسٹوریج، جو بہت مہنگا ہے، استعمال کرنے کی کوئی ضرورت نہیں ہے، جب آپ ADDRESS BALANCE استعمال کرکے اپنے اکاؤنٹس کا بیلنس حاصل کرسکتے ہیں۔ پہلا opcode کنٹریکٹ کا اپنا ایڈریس پش کرتا ہے۔ دوسرا والا اسٹیک کے اوپری حصے میں موجود ایڈریس کو پڑھتا ہے اور اسے اس ایڈریس کے بیلنس سے بدل دیتا ہے۔

آفسیٹOpcodeاسٹیک
6CPUSH2 0x00750x75 Value* CALLVALUE 0 6 CALLVALUE
6FSWAP2CALLVALUE Value* 0x75 0 6 CALLVALUE
70SWAP1Value* CALLVALUE 0x75 0 6 CALLVALUE
71PUSH2 0x01a70x01A7 Value* CALLVALUE 0x75 0 6 CALLVALUE
74JUMP

ہم اس کوڈ کو جمپ ڈیسٹینیشن پر ٹریس کرنا جاری رکھیں گے۔

آفسیٹOpcodeاسٹیک
1A7JUMPDESTValue* CALLVALUE 0x75 0 6 CALLVALUE
1A8PUSH1 0x000x00 Value* CALLVALUE 0x75 0 6 CALLVALUE
1AADUP3CALLVALUE 0x00 Value* CALLVALUE 0x75 0 6 CALLVALUE
1ABNOT2^256-CALLVALUE-1 0x00 Value* CALLVALUE 0x75 0 6 CALLVALUE

NOT بٹ وائز ہے، لہذا یہ کال ویلیو میں ہر بٹ کی ویلیو کو الٹ دیتا ہے۔

آفسیٹOpcodeاسٹیک
1ACDUP3Value* 2^256-CALLVALUE-1 0x00 Value* CALLVALUE 0x75 0 6 CALLVALUE
1ADGTValue*>2^256-CALLVALUE-1 0x00 Value* CALLVALUE 0x75 0 6 CALLVALUE
1AEISZEROValue*<=2^256-CALLVALUE-1 0x00 Value* CALLVALUE 0x75 0 6 CALLVALUE
1AFPUSH2 0x01df0x01DF Value*<=2^256-CALLVALUE-1 0x00 Value* CALLVALUE 0x75 0 6 CALLVALUE
1B2JUMPI

ہم جمپ کرتے ہیں اگر Value*، 2^256-CALLVALUE-1 سے چھوٹا یا اس کے برابر ہو۔ یہ اوور فلو کو روکنے کی منطق کی طرح لگتا ہے۔ اور درحقیقت، ہم دیکھتے ہیں کہ کچھ بے معنی آپریشنز کے بعد (میموری میں لکھنا حذف ہونے والا ہے، مثال کے طور پر) آفسیٹ 0x01DE پر کنٹریکٹ ریورٹ ہوجاتا ہے اگر اوور فلو کا پتہ چلتا ہے، جو کہ عام رویہ ہے۔

نوٹ کریں کہ اس طرح کا اوور فلو انتہائی غیر متوقع ہے، کیونکہ اس کے لیے کال ویلیو اور Value* کا موازنہ 2^256 wei، تقریباً 10^59 ETH سے کرنا ہوگا۔ لکھنے کے وقت، کل ETH سپلائی دو سو ملین سے کم ہےopens in a new tab۔

آفسیٹOpcodeاسٹیک
1DFJUMPDEST0x00 Value* CALLVALUE 0x75 0 6 CALLVALUE
1E0POPValue* CALLVALUE 0x75 0 6 CALLVALUE
1E1ADDValue*+CALLVALUE 0x75 0 6 CALLVALUE
1E2SWAP10x75 Value*+CALLVALUE 0 6 CALLVALUE
1E3JUMP

اگر ہم یہاں پہنچ گئے تو، Value* + CALLVALUE حاصل کریں اور آفسیٹ 0x75 پر جمپ کریں۔

آفسیٹOpcodeاسٹیک
75JUMPDESTValue*+CALLVALUE 0 6 CALLVALUE
76SWAP10 Value*+CALLVALUE 6 CALLVALUE
77SWAP26 Value*+CALLVALUE 0 CALLVALUE
78SSTORE0 CALLVALUE

اگر ہم یہاں پہنچتے ہیں (جس کے لیے کال ڈیٹا کا خالی ہونا ضروری ہے) تو ہم Value* میں کال ویلیو کا اضافہ کرتے ہیں۔ یہ اس کے مطابق ہے جو ہم کہتے ہیں کہ Transfer ٹرانزیکشنز کرتی ہیں۔

آفسیٹOpcode
79POP
7APOP
7BSTOP

آخر میں، اسٹیک کو صاف کریں (جو ضروری نہیں ہے) اور ٹرانزیکشن کے کامیاب اختتام کا اشارہ دیں۔

اس سب کا خلاصہ کرنے کے لیے، یہاں ابتدائی کوڈ کے لیے ایک فلو چارٹ ہے۔

انٹری پوائنٹ فلو چارٹ

0x7C پر ہینڈلر

میں نے جان بوجھ کر ہیڈنگ میں یہ نہیں بتایا کہ یہ ہینڈلر کیا کرتا ہے۔ مقصد آپ کو یہ سکھانا نہیں ہے کہ یہ مخصوص کنٹریکٹ کیسے کام کرتا ہے، بلکہ یہ سکھانا ہے کہ کنٹریکٹس کو کیسے ریورس انجینئر کیا جائے۔ آپ اسی طرح سیکھیں گے جیسے میں نے کوڈ کی پیروی کرکے سیکھا تھا۔

ہم یہاں کئی جگہوں سے آتے ہیں:

  • اگر 1، 2، یا 3 بائٹس کا کال ڈیٹا ہے (آفسیٹ 0x63 سے)
  • اگر میتھڈ سگنیچر نامعلوم ہے (آفسیٹس 0x42 اور 0x5D سے)
آفسیٹOpcodeاسٹیک
7CJUMPDEST
7DPUSH1 0x000x00
7FPUSH2 0x009d0x9D 0x00
82PUSH1 0x030x03 0x9D 0x00
84SLOADStorage[3] 0x9D 0x00

یہ ایک اور اسٹوریج سیل ہے، جسے میں کسی بھی ٹرانزیکشن میں نہیں ڈھونڈ سکا لہذا یہ جاننا مشکل ہے کہ اس کا کیا مطلب ہے۔ نیچے دیا گیا کوڈ اسے مزید واضح کر دے گا۔

آفسیٹOpcodeاسٹیک
85PUSH20 0xffffffffffffffffffffffffffffffffffffffff0xff....ff Storage[3] 0x9D 0x00
9AANDStorage[3]-بطور-ایڈریس 0x9D 0x00

یہ opcodes اس ویلیو کو جو ہم Storage[3] سے پڑھتے ہیں، اسے 160 بٹس تک چھوٹا کر دیتے ہیں، جو کہ ایک Ethereum ایڈریس کی لمبائی ہے۔

آفسیٹOpcodeاسٹیک
9BSWAP10x9D Storage[3]-بطور-ایڈریس 0x00
9CJUMPStorage[3]-بطور-ایڈریس 0x00

یہ جمپ فالتو ہے، کیونکہ ہم اگلے opcode پر جا رہے ہیں۔ یہ کوڈ اتنا گیس-ایفیشنٹ نہیں ہے جتنا ہو سکتا تھا۔

آفسیٹOpcodeاسٹیک
9DJUMPDESTStorage[3]-بطور-ایڈریس 0x00
9ESWAP10x00 Storage[3]-بطور-ایڈریس
9FPOPStorage[3]-بطور-ایڈریس
A0PUSH1 0x400x40 Storage[3]-بطور-ایڈریس
A2MLOADMem[0x40] Storage[3]-بطور-ایڈریس

کوڈ کے بالکل شروع میں ہم نے Mem[0x40] کو 0x80 پر سیٹ کیا تھا۔ اگر ہم بعد میں 0x40 کو تلاش کریں تو ہم دیکھتے ہیں کہ ہم اسے تبدیل نہیں کرتے ہیں - لہذا ہم فرض کر سکتے ہیں کہ یہ 0x80 ہے۔

آفسیٹOpcodeاسٹیک
A3CALLDATASIZECALLDATASIZE 0x80 Storage[3]-بطور-ایڈریس
A4PUSH1 0x000x00 CALLDATASIZE 0x80 Storage[3]-بطور-ایڈریس
A6DUP30x80 0x00 CALLDATASIZE 0x80 Storage[3]-بطور-ایڈریس
A7CALLDATACOPY0x80 Storage[3]-بطور-ایڈریس

تمام کال ڈیٹا کو میموری میں کاپی کریں، 0x80 سے شروع کرتے ہوئے۔

آفسیٹOpcodeاسٹیک
A8PUSH1 0x000x00 0x80 Storage[3]-بطور-ایڈریس
AADUP10x00 0x00 0x80 Storage[3]-بطور-ایڈریس
ABCALLDATASIZECALLDATASIZE 0x00 0x00 0x80 Storage[3]-بطور-ایڈریس
ACDUP40x80 CALLDATASIZE 0x00 0x00 0x80 Storage[3]-بطور-ایڈریس
ADDUP6Storage[3]-بطور-ایڈریس 0x80 CALLDATASIZE 0x00 0x00 0x80 Storage[3]-بطور-ایڈریس
AEGASGAS Storage[3]-بطور-ایڈریس 0x80 CALLDATASIZE 0x00 0x00 0x80 Storage[3]-بطور-ایڈریس
AFDELEGATE_CALL

اب چیزیں بہت واضح ہو گئی ہیں۔ یہ کنٹریکٹ ایک پراکسیopens in a new tab کے طور پر کام کر سکتا ہے، جو اصل کام کرنے کے لیے Storage[3] میں موجود ایڈریس کو کال کرتا ہے۔ DELEGATE_CALL ایک الگ کنٹریکٹ کو کال کرتا ہے، لیکن اسی اسٹوریج میں رہتا ہے۔ اس کا مطلب یہ ہے کہ ڈیلیگیٹڈ کنٹریکٹ، جس کے لیے ہم ایک پراکسی ہیں، اسی اسٹوریج اسپیس تک رسائی حاصل کرتا ہے۔ کال کے پیرامیٹرز یہ ہیں:

  • گیس: باقی تمام گیس
  • کال کیا گیا ایڈریس: Storage[3]-بطور-ایڈریس
  • کال ڈیٹا: CALLDATASIZE بائٹس 0x80 سے شروع ہوتی ہیں، جہاں ہم نے اصل کال ڈیٹا ڈالا تھا
  • ریٹرن ڈیٹا: کوئی نہیں (0x00 - 0x00) ہم ریٹرن ڈیٹا دوسرے طریقوں سے حاصل کریں گے (نیچے دیکھیں)
آفسیٹOpcodeاسٹیک
B0RETURNDATASIZERETURNDATASIZE (((کال کی کامیابی/ناکامی))) 0x80 Storage[3]-بطور-ایڈریس
B1DUP1RETURNDATASIZE RETURNDATASIZE (((کال کی کامیابی/ناکامی))) 0x80 Storage[3]-بطور-ایڈریس
B2PUSH1 0x000x00 RETURNDATASIZE RETURNDATASIZE (((کال کی کامیابی/ناکامی))) 0x80 Storage[3]-بطور-ایڈریس
B4DUP50x80 0x00 RETURNDATASIZE RETURNDATASIZE (((کال کی کامیابی/ناکامی))) 0x80 Storage[3]-بطور-ایڈریس
B5RETURNDATACOPYRETURNDATASIZE (((کال کی کامیابی/ناکامی))) 0x80 Storage[3]-بطور-ایڈریس

یہاں ہم تمام ریٹرن ڈیٹا کو 0x80 سے شروع ہونے والے میموری بفر میں کاپی کرتے ہیں۔

آفسیٹOpcodeاسٹیک
B6DUP2(((کال کی کامیابی/ناکامی))) RETURNDATASIZE (((کال کی کامیابی/ناکامی))) 0x80 Storage[3]-بطور-ایڈریس
B7DUP1(((کال کی کامیابی/ناکامی))) (((کال کی کامیابی/ناکامی))) RETURNDATASIZE (((کال کی کامیابی/ناکامی))) 0x80 Storage[3]-بطور-ایڈریس
B8ISZERO(((کیا کال ناکام ہوئی))) (((کال کی کامیابی/ناکامی))) RETURNDATASIZE (((کال کی کامیابی/ناکامی))) 0x80 Storage[3]-بطور-ایڈریس
B9PUSH2 0x00c00xC0 (((کیا کال ناکام ہوئی))) (((کال کی کامیابی/ناکامی))) RETURNDATASIZE (((کال کی کامیابی/ناکامی))) 0x80 Storage[3]-بطور-ایڈریس
BCJUMPI(((کال کی کامیابی/ناکامی))) RETURNDATASIZE (((کال کی کامیابی/ناکامی))) 0x80 Storage[3]-بطور-ایڈریس
BDDUP2RETURNDATASIZE (((کال کی کامیابی/ناکامی))) RETURNDATASIZE (((کال کی کامیابی/ناکامی))) 0x80 Storage[3]-بطور-ایڈریس
BEDUP50x80 RETURNDATASIZE (((کال کی کامیابی/ناکامی))) RETURNDATASIZE (((کال کی کامیابی/ناکامی))) 0x80 Storage[3]-بطور-ایڈریس
BFRETURN

لہذا کال کے بعد ہم ریٹرن ڈیٹا کو بفر 0x80 - 0x80+RETURNDATASIZE میں کاپی کرتے ہیں، اور اگر کال کامیاب ہوتی ہے تو ہم بالکل اسی بفر کے ساتھ RETURN کرتے ہیں۔

DELEGATECALL ناکام

اگر ہم یہاں، 0xC0 پر پہنچتے ہیں، تو اس کا مطلب ہے کہ ہم نے جس کنٹریکٹ کو کال کیا تھا وہ ریورٹ ہو گیا ہے۔ چونکہ ہم اس کنٹریکٹ کے لیے صرف ایک پراکسی ہیں، ہم وہی ڈیٹا واپس کرنا چاہتے ہیں اور ریورٹ بھی کرنا چاہتے ہیں۔

آفسیٹOpcodeاسٹیک
C0JUMPDEST(((کال کی کامیابی/ناکامی))) RETURNDATASIZE (((کال کی کامیابی/ناکامی))) 0x80 Storage[3]-بطور-ایڈریس
C1DUP2RETURNDATASIZE (((کال کی کامیابی/ناکامی))) RETURNDATASIZE (((کال کی کامیابی/ناکامی))) 0x80 Storage[3]-بطور-ایڈریس
C2DUP50x80 RETURNDATASIZE (((کال کی کامیابی/ناکامی))) RETURNDATASIZE (((کال کی کامیابی/ناکامی))) 0x80 Storage[3]-بطور-ایڈریس
C3REVERT

لہذا ہم اسی بفر کے ساتھ REVERT کرتے ہیں جو ہم نے پہلے RETURN کے لیے استعمال کیا تھا: 0x80 - 0x80+RETURNDATASIZE

پراکسی پر کال کرنے کا فلو چارٹ

ABI کالز

اگر کال ڈیٹا کا سائز چار بائٹس یا اس سے زیادہ ہے تو یہ ایک درست ABI کال ہو سکتی ہے۔

آفسیٹOpcodeاسٹیک
DPUSH1 0x000x00
FCALLDATALOAD(((کال ڈیٹا کا پہلا لفظ (256 بٹس))))
10PUSH1 0xe00xE0 (((کال ڈیٹا کا پہلا لفظ (256 بٹس))))
12SHR(((کال ڈیٹا کے پہلے 32 بٹس (4 بائٹس))))

Etherscan ہمیں بتاتا ہے کہ 1C ایک نامعلوم opcode ہے، کیونکہ اسے Etherscan کے اس فیچر کو لکھنے کے بعد شامل کیا گیا تھاopens in a new tab اور انہوں نے اسے اپ ڈیٹ نہیں کیا ہے۔ ایک اپ ٹو ڈیٹ opcode ٹیبلopens in a new tab ہمیں دکھاتا ہے کہ یہ شفٹ رائٹ ہے۔

آفسیٹOpcodeاسٹیک
13DUP1(((کال ڈیٹا کے پہلے 32 بٹس (4 بائٹس))) (((کال ڈیٹا کے پہلے 32 بٹس (4 بائٹس))))
14PUSH4 0x3cd8045e0x3CD8045E (((کال ڈیٹا کے پہلے 32 بٹس (4 بائٹس))) (((کال ڈیٹا کے پہلے 32 بٹس (4 بائٹس))))
19GT0x3CD8045E>کال-ڈیٹا-کے-پہلے-32-بٹس (((کال ڈیٹا کے پہلے 32 بٹس (4 بائٹس))))
1APUSH2 0x00430x43 0x3CD8045E>کال-ڈیٹا-کے-پہلے-32-بٹس (((کال ڈیٹا کے پہلے 32 بٹس (4 بائٹس))))
1DJUMPI(((کال ڈیٹا کے پہلے 32 بٹس (4 بائٹس))))

اس طرح میتھڈ سگنیچر میچنگ ٹیسٹس کو دو حصوں میں تقسیم کرنے سے اوسطاً آدھے ٹیسٹ بچ جاتے ہیں۔ اس کے فوراً بعد آنے والا کوڈ اور 0x43 میں موجود کوڈ ایک ہی پیٹرن کی پیروی کرتا ہے: کال ڈیٹا کے پہلے 32 بٹس کو DUP1 کریں، PUSH4 (((میتھڈ سگنیچر))، برابری کی جانچ کے لیے EQ چلائیں، اور پھر اگر میتھڈ سگنیچر میچ کرتا ہے تو JUMPI کریں۔ یہاں میتھڈ سگنیچرز، ان کے ایڈریسز، اور اگر معلوم ہو تو متعلقہ میتھڈ ڈیفینیشنopens in a new tab ہیں:

میتھڈمیتھڈ سگنیچرجمپ کرنے کے لیے آفسیٹ
splitter()opens in a new tab0x3cd8045e0x0103
؟؟؟0x81e580d30x0138
currentWindow()opens in a new tab0xba0bafb40x0158
؟؟؟0x1f1358230x00C4
merkleRoot()opens in a new tab0x2eb4a7ab0x00ED

اگر کوئی میچ نہیں ملتا ہے، تو کوڈ پراکسی ہینڈلر پر 0x7C پر جمپ کرتا ہے، اس امید پر کہ جس کنٹریکٹ کے لیے ہم پراکسی ہیں اس کا کوئی میچ ہوگا۔

ABI کالز فلو چارٹ

splitter()

آفسیٹOpcodeاسٹیک
103JUMPDEST
104CALLVALUECALLVALUE
105DUP1CALLVALUE CALLVALUE
106ISZEROCALLVALUE==0 CALLVALUE
107PUSH2 0x010f0x010F CALLVALUE==0 CALLVALUE
10AJUMPICALLVALUE
10BPUSH1 0x000x00 CALLVALUE
10DDUP10x00 0x00 CALLVALUE
10EREVERT

یہ فنکشن سب سے پہلے یہ چیک کرتا ہے کہ کال نے کوئی ETH نہیں بھیجا ہے۔ یہ فنکشن payableopens in a new tab نہیں ہے۔ اگر کسی نے ہمیں ETH بھیجا ہے تو یہ ایک غلطی ہونی چاہیے اور ہم REVERT کرنا چاہتے ہیں تاکہ اس ETH کو ایسی جگہ رکھنے سے بچا جا سکے جہاں وہ اسے واپس نہ لے سکیں۔

آفسیٹOpcodeاسٹیک
10FJUMPDEST
110POP
111PUSH1 0x030x03
113SLOAD(((Storage[3] یعنی وہ کنٹریکٹ جس کے لیے ہم پراکسی ہیں)))
114PUSH1 0x400x40 (((Storage[3] یعنی وہ کنٹریکٹ جس کے لیے ہم پراکسی ہیں)))
116MLOAD0x80 (((Storage[3] یعنی وہ کنٹریکٹ جس کے لیے ہم پراکسی ہیں)))
117PUSH20 0xffffffffffffffffffffffffffffffffffffffff0xFF...FF 0x80 (((Storage[3] یعنی وہ کنٹریکٹ جس کے لیے ہم پراکسی ہیں)))
12CSWAP10x80 0xFF...FF (((Storage[3] یعنی وہ کنٹریکٹ جس کے لیے ہم پراکسی ہیں)))
12DSWAP2(((Storage[3] یعنی وہ کنٹریکٹ جس کے لیے ہم پراکسی ہیں))) 0xFF...FF 0x80
12EANDپراکسی ایڈریس 0x80
12FDUP20x80 پراکسی ایڈریس 0x80
130MSTORE0x80

اور 0x80 میں اب پراکسی ایڈریس موجود ہے

آفسیٹOpcodeاسٹیک
131PUSH1 0x200x20 0x80
133ADD0xA0
134PUSH2 0x00e40xE4 0xA0
137JUMP0xA0

E4 کوڈ

یہ پہلی بار ہے جب ہم یہ لائنیں دیکھ رہے ہیں، لیکن یہ دیگر میتھڈز کے ساتھ بھی شیئر کی گئی ہیں (نیچے دیکھیں)۔ لہذا ہم اسٹیک میں موجود ویلیو کو X کہیں گے، اور بس یاد رکھیں گے کہ splitter() میں اس X کی ویلیو 0xA0 ہے۔

آفسیٹOpcodeاسٹیک
E4JUMPDESTX
E5PUSH1 0x400x40 X
E7MLOAD0x80 X
E8DUP10x80 0x80 X
E9SWAP2X 0x80 0x80
EASUBX-0x80 0x80
EBSWAP10x80 X-0x80
ECRETURN

لہذا یہ کوڈ اسٹیک میں ایک میموری پوائنٹر (X) وصول کرتا ہے، اور کنٹریکٹ کو ایک ایسے بفر کے ساتھ RETURN کرنے کا سبب بنتا ہے جو 0x80 - X ہے۔

splitter() کے معاملے میں، یہ وہ ایڈریس واپس کرتا ہے جس کے لیے ہم پراکسی ہیں۔ RETURN بفر کو 0x80-0x9F میں واپس کرتا ہے، جو وہ جگہ ہے جہاں ہم نے یہ ڈیٹا لکھا تھا (اوپر آفسیٹ 0x130)۔

currentWindow()

آفسیٹس 0x158-0x163 میں موجود کوڈ اس سے مماثل ہے جو ہم نے splitter() میں 0x103-0x10E میں دیکھا تھا (JUMPI کی منزل کے علاوہ)، لہذا ہم جانتے ہیں کہ currentWindow() بھی payable نہیں ہے۔

آفسیٹOpcodeاسٹیک
164JUMPDEST
165POP
166PUSH2 0x00da0xDA
169PUSH1 0x010x01 0xDA
16BSLOADStorage[1] 0xDA
16CDUP20xDA Storage[1] 0xDA
16DJUMPStorage[1] 0xDA

DA کوڈ

یہ کوڈ دیگر میتھڈز کے ساتھ بھی شیئر کیا گیا ہے۔ لہذا ہم اسٹیک میں موجود ویلیو کو Y کہیں گے، اور بس یاد رکھیں گے کہ currentWindow() میں اس Y کی ویلیو Storage[1] ہے۔

آفسیٹOpcodeاسٹیک
DAJUMPDESTY 0xDA
DBPUSH1 0x400x40 Y 0xDA
DDMLOAD0x80 Y 0xDA
DESWAP1Y 0x80 0xDA
DFDUP20x80 Y 0x80 0xDA
E0MSTORE0x80 0xDA

Y کو 0x80-0x9F پر لکھیں۔

آفسیٹOpcodeاسٹیک
E1PUSH1 0x200x20 0x80 0xDA
E3ADD0xA0 0xDA

اور باقی کی وضاحت اوپر پہلے ہی کی جا چکی ہے۔ لہذا 0xDA پر جمپ اسٹیک ٹاپ (Y) کو 0x80-0x9F پر لکھتے ہیں، اور اس ویلیو کو واپس کرتے ہیں۔ currentWindow() کے معاملے میں، یہ Storage[1] واپس کرتا ہے۔

merkleRoot()

آفسیٹس 0xED-0xF8 میں موجود کوڈ اس سے مماثل ہے جو ہم نے splitter() میں 0x103-0x10E میں دیکھا تھا (JUMPI کی منزل کے علاوہ)، لہذا ہم جانتے ہیں کہ merkleRoot() بھی payable نہیں ہے۔

آفسیٹOpcodeاسٹیک
F9JUMPDEST
FAPOP
FBPUSH2 0x00da0xDA
FEPUSH1 0x000x00 0xDA
100SLOADStorage[0] 0xDA
101DUP20xDA Storage[0] 0xDA
102JUMPStorage[0] 0xDA

جمپ کے بعد کیا ہوتا ہے یہ ہم پہلے ہی معلوم کر چکے ہیں۔ لہذا merkleRoot()، Storage[0] واپس کرتا ہے۔

0x81e580d3

آفسیٹس 0x138-0x143 میں موجود کوڈ اس سے مماثل ہے جو ہم نے splitter() میں 0x103-0x10E میں دیکھا تھا (JUMPI کی منزل کے علاوہ)، لہذا ہم جانتے ہیں کہ یہ فنکشن بھی payable نہیں ہے۔

آفسیٹOpcodeاسٹیک
144JUMPDEST
145POP
146PUSH2 0x00da0xDA
149PUSH2 0x01530x0153 0xDA
14CCALLDATASIZECALLDATASIZE 0x0153 0xDA
14DPUSH1 0x040x04 CALLDATASIZE 0x0153 0xDA
14FPUSH2 0x018f0x018F 0x04 CALLDATASIZE 0x0153 0xDA
152JUMP0x04 CALLDATASIZE 0x0153 0xDA
18FJUMPDEST0x04 CALLDATASIZE 0x0153 0xDA
190PUSH1 0x000x00 0x04 CALLDATASIZE 0x0153 0xDA
192PUSH1 0x200x20 0x00 0x04 CALLDATASIZE 0x0153 0xDA
194DUP30x04 0x20 0x00 0x04 CALLDATASIZE 0x0153 0xDA
195DUP5CALLDATASIZE 0x04 0x20 0x00 0x04 CALLDATASIZE 0x0153 0xDA
196SUBCALLDATASIZE-4 0x20 0x00 0x04 CALLDATASIZE 0x0153 0xDA
197SLTCALLDATASIZE-4<32 0x00 0x04 CALLDATASIZE 0x0153 0xDA
198ISZEROCALLDATASIZE-4>=32 0x00 0x04 CALLDATASIZE 0x0153 0xDA
199PUSH2 0x01a00x01A0 CALLDATASIZE-4>=32 0x00 0x04 CALLDATASIZE 0x0153 0xDA
19CJUMPI0x00 0x04 CALLDATASIZE 0x0153 0xDA

ایسا لگتا ہے کہ یہ فنکشن کم از کم 32 بائٹس (ایک لفظ) کال ڈیٹا لیتا ہے۔

آفسیٹOpcodeاسٹیک
19DDUP10x00 0x00 0x04 CALLDATASIZE 0x0153 0xDA
19EDUP20x00 0x00 0x00 0x04 CALLDATASIZE 0x0153 0xDA
19FREVERT

اگر اسے کال ڈیٹا نہیں ملتا ہے تو ٹرانزیکشن بغیر کسی ریٹرن ڈیٹا کے ریورٹ ہو جاتی ہے۔

آئیے دیکھتے ہیں کہ اگر فنکشن کو واقعی وہ کال ڈیٹا ملتا ہے جس کی اسے ضرورت ہے۔

آفسیٹOpcodeاسٹیک
1A0JUMPDEST0x00 0x04 CALLDATASIZE 0x0153 0xDA
1A1POP0x04 CALLDATASIZE 0x0153 0xDA
1A2CALLDATALOADcalldataload(4) CALLDATASIZE 0x0153 0xDA

calldataload(4) میتھڈ سگنیچر کے بعد کال ڈیٹا کا پہلا لفظ ہے

آفسیٹOpcodeاسٹیک
1A3SWAP20x0153 CALLDATASIZE calldataload(4) 0xDA
1A4SWAP1CALLDATASIZE 0x0153 calldataload(4) 0xDA
1A5POP0x0153 calldataload(4) 0xDA
1A6JUMPcalldataload(4) 0xDA
153JUMPDESTcalldataload(4) 0xDA
154PUSH2 0x016e0x016E calldataload(4) 0xDA
157JUMPcalldataload(4) 0xDA
16EJUMPDESTcalldataload(4) 0xDA
16FPUSH1 0x040x04 calldataload(4) 0xDA
171DUP2calldataload(4) 0x04 calldataload(4) 0xDA
172DUP20x04 calldataload(4) 0x04 calldataload(4) 0xDA
173SLOADStorage[4] calldataload(4) 0x04 calldataload(4) 0xDA
174DUP2calldataload(4) Storage[4] calldataload(4) 0x04 calldataload(4) 0xDA
175LTcalldataload(4)<Storage[4] calldataload(4) 0x04 calldataload(4) 0xDA
176PUSH2 0x017e0x017EC calldataload(4)<Storage[4] calldataload(4) 0x04 calldataload(4) 0xDA
179JUMPIcalldataload(4) 0x04 calldataload(4) 0xDA

اگر پہلا لفظ Storage[4] سے کم نہیں ہے تو فنکشن ناکام ہو جاتا ہے۔ یہ بغیر کسی واپس کی گئی ویلیو کے ریورٹ ہو جاتا ہے:

آفسیٹOpcodeاسٹیک
17APUSH1 0x000x00 ...
17CDUP10x00 0x00 ...
17DREVERT

اگر calldataload(4) کی ویلیو Storage[4] سے کم ہے، تو ہمیں یہ کوڈ ملتا ہے:

آفسیٹOpcodeاسٹیک
17EJUMPDESTcalldataload(4) 0x04 calldataload(4) 0xDA
17FPUSH1 0x000x00 calldataload(4) 0x04 calldataload(4) 0xDA
181SWAP20x04 calldataload(4) 0x00 calldataload(4) 0xDA
182DUP30x00 0x04 calldataload(4) 0x00 calldataload(4) 0xDA
183MSTOREcalldataload(4) 0x00 calldataload(4) 0xDA

اور میموری کے مقامات 0x00-0x1F میں اب ڈیٹا 0x04 ہے (0x00-0x1E سب صفر ہیں، 0x1F چار ہے)

آفسیٹOpcodeاسٹیک
184PUSH1 0x200x20 calldataload(4) 0x00 calldataload(4) 0xDA
186SWAP1calldataload(4) 0x20 0x00 calldataload(4) 0xDA
187SWAP20x00 0x20 calldataload(4) calldataload(4) 0xDA
188SHA3(((0x00-0x1F کا SHA3))) calldataload(4) calldataload(4) 0xDA
189ADD(((0x00-0x1F کا SHA3)))+calldataload(4) calldataload(4) 0xDA
18ASLOADStorage[(((0x00-0x1F کا SHA3))) + calldataload(4)] calldataload(4) 0xDA

لہذا اسٹوریج میں ایک لک اپ ٹیبل ہے، جو 0x000...0004 کے SHA3 سے شروع ہوتا ہے اور اس میں ہر جائز کال ڈیٹا ویلیو (Storage[4] سے کم ویلیو) کے لیے ایک انٹری ہوتی ہے۔

آفسیٹOpcodeاسٹیک
18BSWAP1calldataload(4) Storage[(((0x00-0x1F کا SHA3))) + calldataload(4)] 0xDA
18CPOPStorage[(((0x00-0x1F کا SHA3))) + calldataload(4)] 0xDA
18DDUP20xDA Storage[(((0x00-0x1F کا SHA3))) + calldataload(4)] 0xDA
18EJUMPStorage[(((0x00-0x1F کا SHA3))) + calldataload(4)] 0xDA

ہم پہلے ہی جانتے ہیں کہ آفسیٹ 0xDA پر موجود کوڈ کیا کرتا ہے، یہ اسٹیک ٹاپ ویلیو کو کالر کو واپس کرتا ہے۔ لہذا یہ فنکشن لک اپ ٹیبل سے ویلیو کو کالر کو واپس کرتا ہے۔

0x1f135823

آفسیٹس 0xC4-0xCF میں موجود کوڈ اس سے مماثل ہے جو ہم نے splitter() میں 0x103-0x10E میں دیکھا تھا (JUMPI کی منزل کے علاوہ)، لہذا ہم جانتے ہیں کہ یہ فنکشن بھی payable نہیں ہے۔

آفسیٹOpcodeاسٹیک
D0JUMPDEST
D1POP
D2PUSH2 0x00da0xDA
D5PUSH1 0x060x06 0xDA
D7SLOADValue* 0xDA
D8DUP20xDA Value* 0xDA
D9JUMPValue* 0xDA

ہم پہلے ہی جانتے ہیں کہ آفسیٹ 0xDA پر موجود کوڈ کیا کرتا ہے، یہ اسٹیک ٹاپ ویلیو کو کالر کو واپس کرتا ہے۔ لہذا یہ فنکشن Value* واپس کرتا ہے۔

میتھڈ کا خلاصہ

کیا آپ کو لگتا ہے کہ آپ اس وقت کنٹریکٹ کو سمجھ گئے ہیں؟ میں تو نہیں۔ اب تک ہمارے پاس یہ میتھڈز ہیں:

میتھڈمطلب
ٹرانسفرکال کے ذریعے فراہم کردہ ویلیو کو قبول کریں اور Value* کو اس رقم سے بڑھائیں
splitter()Storage[3]، یعنی پراکسی ایڈریس واپس کریں
currentWindow()Storage[1] واپس کریں
merkleRoot()Storage[0] واپس کریں
0x81e580d3لک اپ ٹیبل سے ویلیو واپس کریں، بشرطیکہ پیرامیٹر Storage[4] سے کم ہو
0x1f135823Storage[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 tab0x8ffb5c970x0135
isClaimed(uint256,address)opens in a new tab0xd2ef07950x0151
claim(uint256,address,uint256,bytes32[])opens in a new tab0x2e7ba6ef0x00F4
incrementWindow()opens in a new tab0x338b1d310x0110
؟؟؟0x3f26479e0x0118
؟؟؟0x1e7df9d30x00C3
currentWindow()opens in a new tab0xba0bafb40x0148
merkleRoot()opens in a new tab0x2eb4a7ab0x0107
؟؟؟0x81e580d30x0122
؟؟؟0x1f1358230x00D8

ہم نچلے چار میتھڈز کو نظر انداز کر سکتے ہیں کیونکہ ہم ان تک کبھی نہیں پہنچ پائیں گے۔ ان کے سگنیچرز ایسے ہیں کہ ہمارا اصل کنٹریکٹ خود ہی ان کا خیال رکھتا ہے (تفصیلات کے لیے آپ سگنیچرز پر کلک کر سکتے ہیں)، لہذا یہ لازماً ایسے میتھڈز ہیں جنہیں اوور رائیڈ کیا گیا ہے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 >=64
3 if _param1 and _param2 > -1 / _param1:
4 revert with 0, 17
5 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 = 0
3 s = 0
4 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 continue
11 ...
12 s = sha3(mem[_66 + 32 len mem[_66]])
13 continue
14 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 wei
3 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 = 0
4 s = 0
5 while idx < _param3.length:
6 if idx >= mem[96]:
7 revert with 0, 50
8 _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 continue
13 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 wei
20 gas 30000 wei
21 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 wei
26 gas gas_remaining wei
27 ...
28 log 0xdbd5389f: addr(_param1), s, bool(ext_call.success)
سب دکھائیں

بنیادی فرق یہ ہے کہ پہلا پیرامیٹر، یعنی جس ونڈو سے ودڈرا کرنا ہے، وہ وہاں نہیں ہے۔ اس کے بجائے، ان تمام ونڈوز پر ایک لوپ ہے جن کا دعوی کیا جا سکتا ہے۔

1 idx = 0
2 s = 0
3 while idx < currentWindow:
4 ...
5 if stor5[mem[0]]:
6 if idx == -1:
7 revert with 0, 17
8 idx = idx + 1
9 s = s
10 continue
11 ...
12 stor5[idx][addr(_param1)] = 1
13 if idx >= unknown81e580d3.length:
14 revert with 0, 50
15 mem[0] = 4
16 if unknown81e580d3[idx] and _param2 > -1 / unknown81e580d3[idx]:
17 revert with 0, 17
18 if s > !(unknown81e580d3[idx] * _param2 / 100 * 10^6):
19 revert with 0, 17
20 if idx == -1:
21 revert with 0, 17
22 idx = idx + 1
23 s = s + (unknown81e580d3[idx] * _param2 / 100 * 10^6)
24 continue
سب دکھائیں

لہذا یہ ایک claim کا ویرینٹ لگتا ہے جو تمام ونڈوز کا دعوی کرتا ہے۔

نتیجہ

اب تک آپ کو یہ جان لینا چاہیے کہ ان کنٹریکٹس کو کیسے سمجھا جائے جن کا سورس کوڈ دستیاب نہیں ہے، یا تو opcodes کا استعمال کرکے یا (جب یہ کام کرتا ہے) ڈی کمپائلر کا استعمال کرکے۔ جیسا کہ اس مضمون کی طوالت سے ظاہر ہے، ایک کنٹریکٹ کی ریورس انجینئرنگ کوئی معمولی کام نہیں ہے، لیکن ایک ایسے نظام میں جہاں سیکیورٹی ضروری ہے، یہ ایک اہم ہنر ہے کہ کنٹریکٹس کے وعدے کے مطابق کام کرنے کی تصدیق کی جا سکے۔

میرے مزید کام کے لیے یہاں دیکھیںopens in a new tab۔

صفحہ کی آخری تازہ کاری: 22 اگست، 2025

کیا یہ ٹیوٹوریل کارآمد تھا؟