見出し画像

[Polygon] MATICを危険にさらすバグがコントラクトに潜んでいました。

概要

Polygon とは、Ethereum ベースのレイヤー 2 スケーリングネットワークのことです。Polygon には約 240 億ドル相当のネイティブトークン「MATIC」があります。今回の修正によって、MATIC を危険にさらす脆弱性が排除されました。

Polygon のブログによると、ネットワークの PoS ジェネシスコントラクトの脆弱性は、ブロックチェーンセキュリティとバグバウンティホスティングプラットフォーム Immunefi を介して12月3日と4日に 2 人のホワイトハットハッカーによって報告されました。

脆弱性

MATIC トークンは、Polygon エコシステムにおいて以下のようなことに使用できます。

  • Polygon Improvement Proposal(PIP)への投票

  • ステーキングによるセキュリティへの貢献

  • ガスコストの支払い

MATICトークン は、Polygon ネットワークのネイティブなガス支払い資産ですが、Polygon に展開されるコントラクトでもあります。 このコントラクトは MRC20 コントラクトです。MRC20 規格は、主に Ether では不可能な MATIC のガス抜き転送を可能にするために使用されています。Ether を送る場合、ウォレットが署名する必要のあるトランザクションを行うことになります。

ガス抜き MATIC 転送は、transferWithSig( ) によって可能です。トークンを所有するユーザは、オペレーター、金額、nonce、有効期限などのパラメータを束ねたものに署名します。この署名は、後にオペレーターが MRC20 コントラクトに渡すことで、トークン所有者に代わって送金を実行することができます。これは、オペレーターがガス代を支払うため、トークン所有者にとってはガス抜きとなります。

function transferWithSig(bytes calldata sig, uint256 amount, bytes32 data, uint256 expiration, address to) external returns (address from) {
    require(amount > 0);
    require(expiration == 0 || block.number <= expiration, "Signature is expired");

    bytes32 dataHash = getTokenTransferOrderHash(
      msg.sender,
      amount,
      data,
      expiration
    );
    require(disabledHashes[dataHash] == false, "Sig deactivated");
    disabledHashes[dataHash] = true;

    from = ecrecovery(dataHash, sig);
    _transferFrom(from, to, amount);
}

まず、署名用ハッシュ(dataHash)がまだ使用されていないことを確認する。これはリプレイ攻撃を防いでいます。ハッシュが検証されたら、ecrecovery( ) を使ってトークン所有者のアドレスを抽出し、それを _transferFrom( ) 関数に渡します。

Ethereum 上のスマートコントラクトは、ecrecover を通じてビルトインの ECDSA 署名検証アルゴリズムにアクセスすることができます。この組み込み関数により、ハッシュ化されたデータ上で署名の整合性を検証し、署名者の公開鍵を返すことができます。ecrecovery は標準の ecrecover の上にあるラッパー関数で、V、R、S を分ける必要なくパックされた署名を渡すことができます。

function ecrecovery(bytes32 hash, bytes memory sig)
        public
        pure
        returns (address result)
    {
        bytes32 r;
        bytes32 s;
        uint8 v;
        if (sig.length != 65) {
            return address(0x0);
        }
        assembly {
            r := mload(add(sig, 32))
            s := mload(add(sig, 64))
            v := and(mload(add(sig, 65)), 255)
        }
        // https://github.com/ethereum/go-ethereum/issues/2053
        if (v < 27) {
            v += 27;
        }
        if (v != 27 && v != 28) {
            return address(0x0);
        }
        // get address out of hash and signature
        result = ecrecover(hash, v, r, s);
        // ecrecover returns zero on error
        require(result != address(0x0), "Error in ecrecover");
    }

しかし、トークンのバグにより、攻撃者は MRC20 コントラクトから任意の数のトークンを生成することができたのです。つまり、当時の MATIC の価値である 9,276,584,332 のすべてが盗まれていた可能性があります。

主な問題は、_transferFrom が from に十分な残高があるかどうかをチェックせずに直接 _transfer 関数を呼び出してしまうことです。そして、ecrecovery がゼロのアドレスを返すかどうかのチェックがないおかげで、有効な署名なしで transferWithSig( ) を呼び出すことができました。

function _transferFrom(address from, address to, uint256 value)
        internal
        returns (bool)
    {
        uint256 input1 = this.balanceOf(from);
        uint256 input2 = this.balanceOf(to);
        _transfer(from, to, value);
        emit LogTransfer(
            token,
            from,
            to,
            value,
            input1,
            input2,
            this.balanceOf(from),
            this.balanceOf(to)
        );
        return true;
    }

この関数は、"from" と "to" のアドレスの残高を受け取り、それを _transfer() に渡しますが、これも同じ問題を抱えており、送信者が十分な残高を持っているかどうかをチェックするものではありません。

以下のように実行すると、ジェネシスコントラクトから任意のトークンを生成することができます。

  1. 65 以外の長さのバイト列を作成する。ecrecovery のチェックにより、パックされた署名が長さ 65 でない場合、ゼロのアドレスが返されます。つまり、攻撃を続行するために有効な署名は必要ありません。

  2. 関数に渡される金額は任意の金額ですが、MRC20 コントラクトの残高全額を使用することができます。

  3. to アドレスは攻撃者のアドレスになります。

  4. from が無効な署名から回収された後(無効な署名を渡したので、これはゼロのアドレス)、_transferFrom( ) が呼び出されます。

  5. from と to の残高が確認されていないため、コントラクトは _transfer( ) を呼び出します。

  6. _transfer( ) は、受取人が MRC20 コントラクト自身でない場合のみチェックし、MRC20 コントラクトから攻撃者に全額を転送します。

  7. すべての MATIC トークンの利益を受け取ります。

Polygon はtransferWithSig 関数を取り除きました。この修正は以下のリンクから確認できます。

参考文献

https://cointelegraph.com/news/polygon-upgrade-quietly-fixes-bug-that-put-24b-of-matic-at-risk


この記事が気に入ったらサポートをしてみませんか?