حمله بازگشتی در سالیدیتی

حمله بازگشتی در سالیدیتی زمانی اتفاق می افتد که یک قرارداد هوشمند در میانه اجرای یک تابع، کنترل جریان اجرای برنامه را به یک قرارداد دیگر واگذار کند. این واگذاری معمولاً از طریق فراخوانی تابع یا ارسال اتر به قرارداد دیگر انجام می شود.

اگر در طول اجرای یک تابع، هیچ فراخوانی به قرارداد دیگر یا ارسال اتر صورت نگیرد، کنترل برنامه همچنان در اختیار قرارداد فعلی باقی می ماند و حمله بازگشتی نمی تواند رخ دهد.

به مثال زیر توجه کنید:

نکته مهم اینجاست که همیشه به سادگی نمی توان متوجه شد که در حال فراخوانی یک قرارداد دیگر هستید. برای مثال، کد زیر در ظاهر امن به نظر می رسد اما اگر داخل یک قرارداد ERC1155 استفاده شود، در برابر حمله بازگشتی آسیب پذیر خواهد بود:

چرا این فراخوانی ساده mint می تواند ناامن باشد؟ برای پاسخ به این سوال، بیایید نگاهی به پیاده سازی تابع _mint در استاندارد ERC1155 از کتابخانه OpenZeppelin بیندازیم(اینجا):

کد سالیدیتی ERC1155

تابع _mint در انتهای اجرای خود، تابعی به نام _doSafeTransferAcceptanceCheck را فراخوانی می کند. حالا بیایید این تابع را مرحله به مرحله بررسی کنیم تا ببینیم چرا می تواند در معرض حمله بازگشتی (Reentrancy) قرار بگیرد.

کد سالیدیتی IERC1155Receiver

در پیاده سازی ERC1155، تابع _doSafeTransferAcceptanceCheck در نهایت سعی می کند تابعی به نام onERC1155Received را روی قرارداد گیرنده فراخوانی کند. این یعنی ما در این نقطه کنترل اجرا را به یک قرارداد خارجی واگذار کرده ایم—و دقیقاً همین واگذاری می تواند راه را برای حمله بازگشتی باز کند.

ابزاری به نام Slither می تواند به صورت خودکار فراخوانی های تابع های خارجی را شناسایی کند. بنابراین، توصیه می شود برای تحلیل امنیتی قراردادها از این ابزار استفاده کنید.

ممکن است این موضوع کمی گیج کننده به نظر برسد، اما اجازه بدهید نکته ای را روشن کنیم: قطعه کدی که تقریباً مشابه مثال قبل است

اگر از استاندارد ERC20 مشتق شده باشد، در برابر حمله بازگشتی آسیب پذیر نیست. دلیل آن در رفتار داخلی تابع transferFrom در استاندارد ERC20 نهفته است. در پیاده سازی این تابع در سالیدیتی، هیچ فراخوانی به توابع خارجی انجام نمی شود و بنابراین کنترلی به قرارداد دیگر واگذار نمی گردد. این تفاوت ساختاری، امنیت بیشتری در برابر حملات بازگشتی فراهم می کند.
پیاده‌سازی تابع انتقال (transfer) در ERC20

ERC721

safeTransferFrom
_safeMint

نکته گیج‌کننده این است که واژه “safe” در توابع safeTransferFrom و _safeMint به این معناست که بررسی می‌شود آیا آدرس گیرنده یک قرارداد هوشمند است یا خیر. اگر گیرنده یک قرارداد باشد، تابع onERC721Received در آن قرارداد فراخوانی می‌شود.

اما توابع transferFrom و _mint چنین بررسی‌ انجام نمی‌دهند، بنابراین هنگام استفاده از آن‌ها نگرانی‌ بابت حمله بازگشتی (Reentrancy) وجود ندارد.

البته این موضوع به این معنا نیست که نباید از safeTransferFrom یا _safeMint استفاده کنید؛ بلکه اگر از این توابع استفاده می‌کنید، باید از الگوی check-effects یا محافظ‌های ضد بازگشت (reentrancy guards) برای جلوگیری از حمله بازگشتی استفاده کنید.

در ادامه مثالی ساده از یک تابع mint را می‌بینید که در آن مهاجم می‌تواند همه NFTها را برای خود ضرب (mint) کند:

ERC1155

safeTransferFrom
_mint
safeBatchTransferFrom
_mintBatch

موضوع حتی پیچیده‌تر از قبل می‌شود: در استاندارد ERC1155، تابع _mint برخلاف _mint در ERC721 رفتار می‌کند و بیشتر شبیه به _safeMint در ERC721 عمل می‌کند.

به عبارت دیگر، در ERC1155 هیچ تابعی واقعاً “امن” (safe) نیست؛ چون همه توابع، بدون استثنا، با قرارداد گیرنده تعامل برقرار می‌کنند.

این موضوع به‌خودی‌خود اشتباه طراحی محسوب نمی‌شود، اما الزاماً به این معناست که شما باید همیشه از الگوی “بررسی–تأثیر–تعامل” (check-effects-interactions) یا محافظ بازگشتی (reentrancy guard) استفاده کنید؛ چیزی که در هر صورت باید در تمام قراردادهای امن رعایت شود.

در ادامه مثالی از یک کد آسیب‌پذیر ERC1155 را می‌بینید:

ERC223، ERC677، ERC777 و ERC1363

در اینجا نمی‌توانیم همه نسخه‌های پیشنهادی از استاندارد ERC20 را پوشش دهیم.
این واقعیت که در استاندارد اصلی ERC20، توابع transfer و transferFrom باعث حمله بازگشتی (reentrancy) نمی‌شوند، یک مزیت امنیتی محسوب می‌شود.
اما همین ویژگی باعث مشکلاتی در تجربه کاربری (UX) می‌شود؛ چرا که قرارداد هوشمند گیرنده نمی‌تواند تشخیص دهد که توکنی برایش ارسال شده است.

استانداردهایی مثل ERC223، ERC677، ERC777 و ERC1363 برای رفع همین مشکل معرفی شده‌اند؛ این استانداردها هنگام دریافت توکن، به قرارداد گیرنده اطلاع می‌دهند.

با این حال، باید هنگام تعامل با توکن های ناشناخته و غیرقابل اعتماد، محتاط بود. چرا که ممکن است این توکن ها در ظاهر شبیه ERC20 باشند، اما در واقع یکی از این نسخه‌های توسعه یافته باشند و بتوانند فراخوانی خارجی انجام دهند و حمله بازگشتی را فعال کنند.

در ERC777، فراخوانی خارجی به قرارداد گیرنده، پس از انتقال توکن ها و از طریق تابع _callTokensReceived انجام می‌شود. پیاده‌سازی این بخش را می‌توانید در این خط از مخزن OpenZeppelin مشاهده کنید: مشاهده کد در گیت‌هاب (خط ۴۹۹)

در اینجا، اگر گیرنده یک قرارداد باشد و پیاده‌ساز tokensReceived باشد، این تابع مستقیماً روی قرارداد گیرنده اجرا می‌شود—که همین باعث امکان بروز حمله بازگشتی در صورت نبود محافظ می‌شود.

در استاندارد ERC1363، برای بهبود تجربه کاربری، انتقال عادی (transfer) دقیقاً مانند ERC20 رفتار می‌کند، بنابراین در حالت عادی ریسک حمله بازگشتی ندارد.
اما اگر بخواهیم قرارداد گیرنده را از دریافت توکن مطلع کنیم، باید از تابع transferAndCall استفاده کنیم که همانند safeTransferFrom در استانداردهای دیگر عمل می‌کند.

حملات بازگشتی در ERC777 در دنیای واقعی اتفاق افتاده‌اند و می‌توانند منجر به خسارات فاجعه‌بار شوند. بنابراین، هنگام طراحی اپلیکیشنی که با توکن های ERC20 مختلف سروکار دارد، نباید به‌طور پیش‌فرض فرض کنید که transfer و transferFrom همیشه ایمن و غیرقابل بازگشت هستند.

همیشه منطق امنیتی خود را بر اساس رفتار واقعی توکن‌ها، نه فقط نام استاندارد آن‌ها طراحی کنید.

ارسال اتر (Ether) در سالیدیتی

وقتی شما از دستور address.call("") برای ارسال اتر استفاده می‌کنید، کنترل اجرای برنامه را به قرارداد گیرنده واگذار می‌کنید. همین واگذاری می‌تواند زمینه ساز حمله بازگشتی (reentrancy) شود.

به مثال کلاسیک زیر توجه کنید:

مهاجم می‌تواند از طریق قرارداد زیر، بانک را هدف قرار دهد:
چون مقدار balances[msg.sender] بعد از ارسال اتر به صفر می‌رسد، مهاجم می‌تواند بارها ۱ اتر برداشت کند (و اتر سایر کاربران را سرقت کند)، تا زمانی که موجودی حساب بانک به کمتر از ۱ اتر برسد.

چگونه transfer و send جلوی حمله بازگشتی را می‌گیرند، و چرا نباید از آن‌ها استفاده کنید؟

در حاشیه بحث امنیت، توابع transfer() و send() در ظاهر در برابر حمله بازگشتی ایمن هستند، با اینکه همچنان می‌توانند تابع‌های fallback یا receive را در قرارداد مقصد فعال کنند.
دلیل این ایمنی این است که این دو تابع، فقط ۲۳۰۰ واحد گس (gas) به قرارداد گیرنده ارسال می‌کنند، که برای اجرای یک حمله بازگشتی کافی نیست.

اما با وجود این مزیت امنیتی، استفاده از این توابع به عنوان یک الگوی طراحی، عموماً توصیه نمی‌شود.
فرض کنید یک قرارداد هوشمند دارید که باید وام دریافتی از یک قرارداد دیگر را بازپرداخت کند. اگر این بازپرداخت را با transfer یا send انجام دهید، قرارداد وام‌دهنده به دلیل کمبود گس نمی‌تواند متوجه شود که وام بازپرداخت شده است.

در واقع، پس از رخداد هک معروف DAO در سال ۲۰۱۶ که تقریباً برای کل اکوسیستم اتریوم فاجعه‌بار بود، طراحان سالیدیتی تصمیم گرفتند توابعی مثل transfer و send را ایجاد کنند تا از وقوع مجدد چنین حملاتی جلوگیری کنند.

نکته فنی مهم اینجاست:
وقتی از transfer یا send استفاده می‌کنید، فقط ۲۳۰۰ گس ارسال می‌شود. طبق قواعد EVM، زمانی که قرارداد گیرنده کمتر از ۲۳۰۰ گس دریافت کند، اجازه ندارد تغییری در متغیرهای ذخیره‌شده ایجاد کند (یعنی نمی‌تواند حالت دائمی تغییر دهد).
به همین دلیل، قرارداد مهاجم حتی اگر تابع fallback داشته باشد، نمی‌تواند وضعیت داخلی قرارداد قربانی را تغییر دهد.

مشکل اینجاست که بسیاری از قراردادها عمداً نیاز دارند هنگام دریافت اتر واکنش نشان دهند.
برای مثال، تصور کنید یک پلتفرم وام غیرمتمرکز دارید که باید هنگام دریافت اتر از وام‌گیرنده، وضعیت وام را به عنوان بازپرداخت‌شده علامت‌گذاری کند. اما اگر وام‌گیرنده از transfer برای بازپرداخت استفاده کند، قرارداد وام‌دهنده به دلیل محدودیت گس، نمی‌تواند هیچ تغییری در داده‌هایش اعمال کند.

ممکن است عجیب به نظر برسد که سالیدیتی توابعی دارد که نباید از آن‌ها استفاده کرد، اما این موضوع بخشی از تکامل درک ما از بهترین شیوه‌های برنامه نویسی در بلاکچین است.
در ابتدا به نظر می‌رسید که محدود کردن گس برای جلوگیری از حمله بازگشتی، ایده خوبی است.
اما تجربه نشان داد که نمی‌توان گس آینده را پیش‌بینی کرد و حتی مقدار گس مورد نیاز برای عملیات EVM ممکن است در نسخه‌های آینده تغییر کند. به همین دلیل، کدنویسی با گس ثابت (hardcoded gas) اکنون به عنوان یک الگوی نادرست شناخته می‌شود.

بازگشت‌پذیری بین توابع – بازگشت‌پذیری الزاماً نباید به همان تابع اولیه برگردد

در حمله‌های بازگشتی، بسیاری تصور می‌کنند که مهاجم باید حتماً به همان تابعی بازگردد که ابتدا آن را فراخوانی کرده است. اما در واقعیت، اگر قرارداد قربانی در زمان نامناسبی کنترل اجرا را به یک قرارداد خارجی واگذار کند، مهاجم می‌تواند به تابعی دیگر از همان قرارداد وارد شود که باز هم در برابر بازگشت‌پذیری آسیب‌پذیر است.

این الگو به نام بازگشت‌پذیری متقابل یا بازگشت‌پذیری بین توابع (Cross-function Reentrancy) شناخته می‌شود. برخی مهندسان از اصطلاح Trampoline یا بازگشت متقابل (Mutual Recursion) نیز استفاده می‌کنند.

در ادامه، مثالی از یک قرارداد آسیب‌پذیر به این نوع حمله را بررسی می‌کنیم:

در کد بالا، کاربران می‌توانند توکن A را با B تعویض کنند (و بالعکس) و در ازای آن، توکن‌های حاکمیتی دریافت کنند. با این حال، قرارداد سعی می‌کند با محدود کردن کاربران به انجام سواپ در هر ۲۴ ساعت، از ضرب شدن بیش از حد سریع توکن‌های حاکمیتی جلوگیری کند.

توکن‌های ERC777، همان‌طور که قبلاً اشاره شد، قابلیت بازگشت‌پذیری دارند. اما اجرای یک حمله بازگشتی ساده روی یکی از توابع کارآمد نخواهد بود، چون مهاجم در نهایت موجودی توکن A یا B خود را از دست می‌دهد.

با این حال، اگر مهاجم بارها توکن A را با B تعویض کند، می‌تواند تمام توکن‌های حاکمیتی را برای خود ضرب کند.

در این مثال، ما توکن حاکمیتی را از نوع ERC20 قرار داده‌ایم، بنابراین مهاجم نمی‌تواند به همان تابع دوباره وارد شود. اما زمانی که transferFrom(address(this), msg.sender) اجرا می‌شود، مهاجم پیش از آنکه مقدار lastSwap به‌روزرسانی شود، کنترل اجرا را در اختیار می‌گیرد.

بازگشت‌پذیری فقط‌خواندنی که با نام بازگشت‌پذیری بین قراردادی نیز شناخته می‌شود

بازگشت‌پذیری فقط‌خواندنی در سال ۲۰۲۲ وارد ذهن توسعه‌دهندگان شد، زمانی که یکی از سخنرانی‌های کنفرانس Devcon اتریوم، آسیب‌پذیری‌ را در پروژه Curve Finance شرح داد.

در واقع، بازگشت‌پذیری فقط‌خواندنی همان آسیب‌پذیری بازگشت‌پذیری بین قراردادی است، اما با نامی جدید.

این نوع حمله زمانی رخ می‌دهد که قرارداد Foo به وضعیت (state) قرارداد Bar متکی باشد، و Bar در میانه تراکنش نتواند اطلاعات درستی از وضعیت خود ارائه دهد. در چنین شرایطی، مهاجم می‌تواند Foo را فریب دهد.

مثال حمله در پروژه Curve:

در حمله‌ای که به Curve نسبت داده می‌شود، خود Curve مستقیماً مورد سوءاستفاده قرار نگرفت، بلکه قراردادهایی که به وضعیت Curve وابسته بودند قربانی شدند. روند حمله به صورت زیر بود:

  1. مهاجم مقداری اتر و توکن‌های ERC20 را به Curve واریز می‌کند. Curve در ازای آن توکن نقدینگی (liquidity token) صادر می‌کند.

  2. مهاجم با سوزاندن توکن‌های نقدینگی، درخواست برداشت می‌دهد.

  3. Curve ابتدا اتر را به مهاجم بازمی‌گرداند و سپس قرار است توکن‌های ERC20 را بفرستد.

  4. به‌محض ارسال اتر، مهاجم مجدداً کنترل اجرای برنامه را به دست می‌گیرد و در همین لحظه، در یک قرارداد دیگر معامله‌ای انجام می‌دهد.

  5. این قرارداد دوم برای محاسبه قیمت، از Curve اطلاعات قیمت بین توکن نقدینگی، اتر و توکن ERC20 را می‌خواهد. اما چون توکن‌های نقدینگی سوزانده شده‌اند و اتر بازگشته، ولی ERC20ها هنوز منتقل نشده‌اند، محاسبه قیمت‌ها در این لحظه دچار خطا می‌شود.

  6. تراکنش ادامه پیدا می‌کند و Curve در انتها ERC20ها را نیز ارسال می‌کند. حالا قیمت‌ها به حالت عادی بازمی‌گردند—اما فایده‌ای ندارد، چون معامله‌ی ناعادلانه قبلاً انجام شده است.

این نوع حمله شباهت زیادی به حمله وام سریع (Flash Loan) دارد و در بسیاری از موارد، برای موفقیت به یک فلش‌لون نیاز دارد.

راهکارهای دفاعی در برابر بازگشت‌پذیری فقط‌خواندنی

برای مقابله با این نوع حمله دو راهکار کلیدی وجود دارد:

  1. استفاده از قفل بازگشتی (reentrancy lock) برای توابع view یا public کردن آن
    تابع نمایشی (view) که قیمت را گزارش می‌دهد، در لحظه برداشت جزئی نقدینگی در حالت نادرستی از وضعیت قرار دارد. پس صرافی می‌تواند اجازه استفاده از این تابع را در حین برداشت نقدینگی مسدود کند.

  2. اجازه بررسی عمومی وضعیت قفل بازگشتی
    اگر قفل بازگشتی به‌صورت عمومی قابل بررسی باشد، هر اپلیکیشنی که به داده‌های آن تابع نمایشی متکی است، می‌تواند ابتدا بررسی کند که آیا برداشت نقدینگی در حال انجام است یا خیر.
    در مثال Curve، اگر اتر ارسال شده اما توکن‌های ERC20 هنوز منتقل نشده‌اند، قفل بازگشتی همچنان فعال خواهد بود—چون تابع برداشت کامل اجرا نشده است.

نکته پایانی

این آسیب‌پذیری فقط در صورتی رخ می‌دهد که ترتیب ارسال دارایی‌ها بتواند اجرای توابع خارجی را فعال کند.
در مثال Curve، ارسال اتر پیش از ERC20 چنین شرایطی را فراهم کرد. اما همین سناریو ممکن است با استفاده از توکن‌هایی مثل ERC777 نیز تکرار شود، چرا که این توکن‌ها نیز می‌توانند باعث واگذاری کنترل به قرارداد گیرنده شوند.

5/5 - (1 امتیاز)

راستی! برای دریافت مطالب جدید در کانال تلگرام یا پیج اینستاگرام سورس باران عضو شوید.

پکیج آموزش سی شارپ | مختص ورود به بازار کار + آموزش ساخت بازی Quiz of King
  • انتشار: ۷ خرداد ۱۴۰۴

دسته بندی موضوعات

آخرین محصولات فروشگاه

مشاهده همه

نظرات

بازخوردهای خود را برای ما ارسال کنید