Back to Blog

#10 パノプティック事件:XOR線形性による位置フィンガープリント方式の破綻

Code Auditing
February 13, 2026
8 min read

2025年8月25日、CantinaとSeal911の協力を得て、Panopticはホワイトハットによる救済作戦を実施し、約40万ドル相当のリスク資産を保護しました[1]。根本原因はs_positionsHashの構築における欠陥であり、プロトコルはポジションIDのKeccak256ハッシュをXORで集約して単一のフィンガープリントを作成していました。個々のKeccak256ハッシュは衝突耐性を維持しますが、XORの数学的線形性により、複合フィンガープリントは安全ではありませんでした。攻撃者は、XOR集約ハッシュが任意のターゲットフィンガープリントと一致する偽のポジションIDのセットを生成し、プロトコルのポジション検証を回避して、借入を返済せずに担保を引き出すことができました。

背景

Panopticは、イーサリアム上に構築された分散型永久オプション取引プロトコルであり、ユーザーがプットオプションとコールオプションを取引できるようにします。

withdraw()関数には、tokenIdのセットで構成される入力パラメータpositionListがあります。各tokenIdはポジションを表します。withdraw()関数は、s_positionsHashに基づいて各ポジションの負債状況をチェックし、ユーザーの担保を取得します。

ガス代を節約するため、プロトコルはユーザーのすべてのポジション(つまりtokenId)を保存しません。代わりに、ユーザーのすべてのtokenIdに基づいてフィンガープリントを計算し、s_positionsHashに記録します。各s_positionsHashの最も上位8ビットはnumLegsを表し、下位248ビットはuser position hashを表します。

渡された各tokenIdについて、プロトコルはそのハッシュを計算し、下位248ビットを取り、現在のs_positionsHashの下位248ビットとビット単位XOR演算を実行してuser position hashを更新します。

同時に、countLegstokenIdの数値範囲に基づいて調整されます。tokenId2642^{64}未満の場合は変更されず、範囲 (264,2112)(2^{64}, 2^{112})(2112,2168)(2^{112}, 2^{168})、および (2168,2208)(2^{168}, 2^{208})ではそれぞれ1、2、または3増加します。これは最終的にs_positionsHashの最上位8ビットに書き込まれてnumLegsが更新されます。

脆弱性分析

根本原因は、s_positionsHashを構築するためのコントラクトのアルゴリズムの欠陥、特にKeccak256ハッシュ結果を集約するためにXOR操作を使用したことです。単一のハッシュ関数は安全ですが、XOR操作の数学的線形性により、全体的なフィンガープリントアルゴリズム(つまり、ハッシュのXOR和)は安全ではありません[2]。

線形性とは、攻撃者がハッシュ関数自体を破る(つまり、ハッシュからtokenIdを逆元にする)必要がないことを意味します。代わりに、組み合わせ戦略を採用できます。多数のランダムなtokenIdを生成し、それらのKeccak256(tokenId)を計算し、これらのハッシュ値から特定のサブセットを選択して、これらのtokenIdハッシュのXOR和がターゲットフィンガープリントと正確に一致するようにします。

これにより、攻撃者はwithdraw()を呼び出す際に偽のtokenIdのセットを渡して、ヘルスチェックを通過し、s_positionsHashに対応するすべての担保を取得できます。

理論

ユーザーのポジションフィンガープリントs_positionsHashuser position hash TTnumLegs kkを持ち、ユーザーのtokenId{t1,t2,,tn}\{t_1, t_2, \dots, t_n\}であると仮定します。したがって:

i=1k[Hash(ti)(mod2248)]=T\bigoplus_{i=1}^{k} [\text{Hash}(t_i) \pmod{2^{248}}] = T

ここで、各248ビットハッシュ値は248次元ベクトルとして見なすことができます。

Hash(ti)(mod2248)=[b0,b1,,b247]T,ここで bi{0,1}\text{Hash}(t_i) \pmod{2^{248}} = [b_0, b_1, \dots, b_{247}]^T, ここで\ b_i \in \{0, 1\}

したがって、TTは0と1で構成される248次元空間(F2248\mathbb{F}_2^{248})に存在します。この空間では、XOR操作はベクトル加算と同等です(付録I)。

攻撃者の目標は、下位248ビットのXOR和がTTと等しくなるような、nn個の248次元ベクトル{v1,v2,,vn}\{v_1, v_2, \dots, v_n\}をすべての利用可能なベクトルから見つけることです。したがって、攻撃者の目的を線形方程式のシステムとして定式化できます。

x1v1+x2v2++xnvn=Tx_1 v_1 + x_2 v_2 + \dots + x_n v_n = T

具体的には、TTを直接構築しようとする必要はありません。代わりに、nn個の線形独立なハッシュベクトル{v1,v2,,vn}\{v_1, v_2, \dots, v_n\}(ここでnnは次元248に等しい)を選択し、それらを列ベクトルとして使用してn×nn \times n行列AAを構築します。

A=[v1,v2,,vn]A = [v_1, v_2, \dots, v_n]

問題は、次のような係数ベクトルxxを見つけることに変換されます。

Ax=TA \cdot x = T

ここで、x=[x1,x2,,xn]Tx = [x_1, x_2, \dots, x_n]^Txi{0,1}x_i \in \{0, 1\}とします。

線形代数の理論によれば、行列AAがフルランクである限り、それは完全なnn次元空間をスパンします。これは、任意のターゲットTTに対して、方程式のシステムが一意の解を持つことを意味します。xxを解いた後、xi=1x_i=1となるベクトルviv_iのみを保持します。それらのXOR和はTTになります。

理解を容易にするために、ケーススタディでこの構築プロセスを実証します。ベクトルが3次元で、各次元が0または1のみで構成され、ターゲットハッシュ値が101、つまりT=[1,0,1]TT = [1, 0, 1]^Tであると仮定します。

次に、3つのハッシュ値をランダムに生成します。

  • v1=[1,1,0]Tv_1 = [1, 1, 0]^T
  • v2=[0,1,0]Tv_2 = [0, 1, 0]^T
  • v3=[0,1,1]Tv_3 = [0, 1, 1]^T

これらの3つのベクトルを列として使用して行列AAを構築し、Ax=TAx=Tという方程式を設定します。

[100111001][x1x2x3]=[101]\begin{bmatrix} 1 & 0 & 0 \\ 1 & 1 & 1 \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} x_1 \\ x_2 \\ x_3 \end{bmatrix} = \begin{bmatrix} 1 \\ 0 \\ 1 \end{bmatrix}

ガウス消去法で解くと、x=[1,0,1]Tx = [1, 0, 1]^Tが得られます。これは、v1v_1v3v_3x1=1,x3=1x_1=1, x_3=1に対応)を選択する必要があることを意味し、それらのXOR和はターゲットTTと正確に一致します。

nn個の線形独立なベクトルの選択

前述のように、Ax=TAx=Tを解くには、フルランク行列AAを構築する必要があります。非常に大きなベクトル空間(m=2248m = 2^{248})を扱っていることを考えると、中心的な質問は次のとおりです。この広大な空間からnn個の線形独立なベクトルを迅速に選択するにはどうすればよいですか?

最も簡単な方法を検討します。nn個のtokenIdをランダムに生成し、それらのハッシュ結果をベクトルとして選択します。

F2\mathbb{F}_2上では、nn個のランダムに選択されたnn次元ベクトルがフルランク行列を形成する確率PPは次のようになります。

P(n)=k=0n1(12k2n)P(n) = \prod_{k=0}^{n-1} \left(1 - \frac{2^k}{2^n}\right)

nnが大きい場合(この例ではn=248n=248)、この確率は定数に収束します(付録II)。

limnP(n)0.28879\lim_{n \to \infty} P(n) \approx 0.28879

これは、各ランダムな試行が約**28.9%**の成功確率を持つことを意味します。平均して、攻撃者は線形独立なベクトルのセットを見つけるために約3.5回の試行しか必要としません。したがって、計算コストは非常に低く、攻撃者は条件を満たす行列AAを迅速に構築できます。

上位8ビットのcountLegsを制御するには、ターゲットcountLegsに基づいてランダムに生成されたtokenIdの範囲を調整するだけで済みます。

たとえば、偽造されたフィンガープリントのnumLegsを0にしたい場合、ランダムに生成されたnn個のtokenIdがすべて2642^{64}未満であることを保証するだけで十分です。この範囲のtokenIdのレッグインクリメントは0であるため、ガウス消去法による解xxがどのベクトルを選択したかにかかわらず、最終的な累積numLegsは必然的に0になります。

攻撃分析

ホワイトハットレスキュー担当者は、複数の救済トランザクションを開始しました。簡単にするために、次の議論は、それらのうちの1つのみに基づいています[3]。

コアロジックは、次の5つのステップで構成されています。

  1. Aaveからフラッシュローンを介して0.23e8 WBTCと28e18 WETHを借入します。
  2. 0.23e8 WBTCと28e18 WETHをpoWBTCコントラクトと0x1f8d_poWETHコントラクトに預け入れます。
  3. 通常のpositionIdListPanopticPoolコントラクトのmintOptions()を呼び出し、資金を借入してレバレッジポジションをオープンします。
  4. 偽造されたtokenIdを渡してwithdraw()を呼び出します。これらの偽造されたポジションはpositionSizeが0であるため、関数はtokenRequiredを0として返します。これは、すべてのポジションに必要な総担保が誤ってゼロとして計算されることを意味します。一方、これらのtokenIdによって生成されたs_positionsHashは、ステップ3で生成されたものとまったく同じであり、レスキュー担当者は借入を返済せずにすべての担保を取得できます。
  5. フラッシュローンを返済し、次のラウンドを実行します。

まとめ

このインシデントは、2つの複合的な欠陥を浮き彫りにし、これらが組み合わさって不正な担保引き出しを可能にしました。

  • XORはハッシュの安全な集約関数ではありません。 Keccak256は衝突耐性がありますが、XORの線形性により、複数のハッシュのXOR和は衝突耐性を継承しません。XORでターゲット値になる入力のセットを構築することは、F2\mathbb{F}_2上の線形方程式システムの解に還元され、計算上は非常に簡単です。ハッシュの合成には、連結と再ハッシュなど、衝突耐性を維持する操作が必要です。
  • ポジションIDの検証漏れ。 プロトコルは、渡されたpositionIdが有効なオプションポジションに対応するかどうかを検証しませんでした。2642^{64}未満の値はcountLegsが0でpositionSizeが0であるため、偽造されたポジションは借入を発生させません。これにより、攻撃者はターゲットフィンガープリントと一致させながら、担保要件ゼロでヘルスチェックを通過することができました。

参照

  1. https://x.com/Panoptic_xyz/status/1961187739866644524

  2. https://cseweb.ucsd.edu/~mihir/papers/inchash.pdf

  3. https://app.blocksec.com/explorer/tx/eth/0x67a45dfe5ff4b190058674d7c791bbdc48e889f319f937c24fa13a5f9093f088

付録

以下の2つの付録は、本文中のステートメント、すなわちXOR操作がベクトル加算(付録I)と同等であること、およびランダム行列がフルランクである確率(付録II)についての数学的な説明と証明を提供します。

付録I

有限体F2={0,1}\mathbb{F}_2 = \{0, 1\}では、加算は2を法とする加算として定義されます。

0+0=00+1=11+0=11+1=0(mod2)\begin{aligned} 0 + 0 &= 0 \\ 0 + 1 &= 1 \\ 1 + 0 &= 1 \\ 1 + 1 &= 0 \pmod 2 \end{aligned}

観察すると、これは排他的論理和(XOR、記号\oplus)論理演算と同一であることがわかります。

nn次元ベクトル空間F2n\mathbb{F}_2^n(この場合n=248n=248)では、2つのベクトルu=[u1,,un]Tu = [u_1, \dots, u_n]^Tv=[v1,,vn]Tv = [v_1, \dots, v_n]^Tの加算は、成分ごとの2を法とする加算として定義されます。

u+v=[u1+v1(mod2)un+vn(mod2)]=[u1v1unvn]=uvu + v = \begin{bmatrix} u_1 + v_1 \pmod 2 \\ \vdots \\ u_n + v_n \pmod 2 \end{bmatrix} = \begin{bmatrix} u_1 \oplus v_1 \\ \vdots \\ u_n \oplus v_n \end{bmatrix} = u \oplus v

したがって、F2n\mathbb{F}_2^{n}ベクトル空間では、ベクトル加算はベクトル成分のビット単位XOR操作と同等です。

付録II

行列をフルランクにするには、これらのnn個のベクトルは線形独立でなければなりません。

最初のベクトルv1v_1は、ゼロベクトル以外のF2n\mathbb{F}_2^nの任意のベクトルとすることができ、2n12^n - 1の選択肢があります。2番目のベクトルv2v_2は、{v1}\{v_1\}によってスパンされる部分空間(212^1個のベクトルを含む)に存在してはならず、2n22^n - 2の選択肢が残ります。この論理に従うと、kk番目のベクトルvkv_kは、前のk1k-1個のベクトルによってスパンされる部分空間に存在してはならず、結果として2n2k12^n - 2^{k-1}の可能な選択肢があります。

このような行列の総数(つまり、GL(n,F2)GL(n, \mathbb{F}_2)の位数)は次のとおりです。

N=k=0n1(2n2k)=(2n1)(2n2)(2n4)(2n2n1)N = \prod_{k=0}^{n-1} (2^n - 2^k) = (2^n - 1)(2^n - 2)(2^n - 4)\cdots(2^n - 2^{n-1})

そして、すべての可能なn×nn \times n行列の総数は(2n)n=2n2(2^n)^n = 2^{n^2}です。

したがって、確率PPは次のようになります。

P=k=0n1(2n2k)2n2=k=0n1(2n2k2n)=k=0n1(12k2n)P = \frac{\prod_{k=0}^{n-1} (2^n - 2^k)}{2^{n^2}} = \prod_{k=0}^{n-1} \left( \frac{2^n - 2^k}{2^n} \right) = \prod_{k=0}^{n-1} (1 - \frac{2^k}{2^n})

j=nkj = n - kと置くと、この式を次のように書き換えることができます。

P=j=1n(112j)P = \prod_{j=1}^{n} \left(1 - \frac{1}{2^j}\right)

nn \to \inftyとすると、この積は定数に収束します。

P0.288788P \approx 0.288788

これは、大きなnnに対して、ランダム行列がフルランクである確率は約**28.9%**であることを意味します。


BlockSecについて

BlockSecは、フルスタックのブロックチェーンセキュリティおよび暗号コンプライアンスプロバイダーです。私たちは、顧客がコード監査(スマートコントラクト、ブロックチェーン、ウォレットを含む)、リアルタイムで攻撃を傍受、インシデントを分析、不正な資金を追跡し、プロトコルとプラットフォームのライフサイクル全体でAML/CFT義務を遵守できるようにする製品とサービスを構築しています。

BlockSecは、著名なカンファレンスで複数のブロックチェーンセキュリティ論文を発表し、DeFiアプリケーションのいくつかのゼロデイ攻撃を報告し、2000万ドル以上の救済のために複数のハッキングを阻止し、数十億ドルの暗号通貨を保護してきました。

Best Security Auditor for Web3

Validate design, code, and business logic before launch. Aligned with the highest industry security standards.

BlockSec Audit