
Paradigm CTF これは、ブロックチェーン業界におけるスマート コントラクト ハッカーのためのトップかつ最もよく知られたオンライン コンテストです。web3 のトップ投資会社である Paradigm によって主催されています。CTF トピックは、Sumczsun と招待されたゲスト著者によって作成された複数の課題で構成されています。各チャレンジの目標は、技術的な問題をハッキングまたは攻撃して解決することです。
コンテスト中、出場者は一連のソフトウェア パズルの課題を完了します。チャレンジ期間が終了する前に、参加者が正しく解決するか最高スコアを獲得する各チャレンジに対してポイントが付与されます。キング オブ ザ ヒル チャレンジのスコアは Elo スコア システムに基づいて決定されます。チャレンジを正しく解決することで各参加者が獲得できるポイント数は、チャレンジ期間が終了するまでわかりません。
Salus セキュリティ チームは合計 13 の課題を解決し、3645.60 点で 1,011 チーム中 9 位で終了しました。そして、Paradigm CTF 2024 のゲスト著者として招待されました。このブログ投稿では、コンテスト中に解決したすべての課題を取り上げます。

解決された課題
- Hello World 
- Black Sheep 
- 100% 
- Dai++ 
- DoDont 
- Grains of Sand 
- Suspicious Charity 
- Token Locker 
- Skill Based Game 
- Enterprise Blockchain 
- Dragon Tyrant 
- Hopping Into Place 
- Oven 
1. Hello World
このチャレンジの目標は、ターゲット アドレスに以前より少なくとも 13.37 多い ETH 残高があることを確認することです。
テスト コントラクト SolveTest と操作 Solve を実行するコントラクトの 2 つのコントラクトを作成しました。 SolveTest コントラクトは、初期環境をセットアップし、テスト攻撃を実行することによって、課題が解決されたことを検証します。Solve コントラクトは、killMySelf() 関数の自己破壊操作を通じてターゲット アドレスに資金を転送し、それによってターゲット アドレスの ETH 残高を増やすという目的を達成します。

2. Black Sheep
このチャレンジの目標は、BANK 契約からすべての ETH を引き出すことです。この脆弱性は WITHDRAW() 関数に存在し、CHECKSIG() 関数は戻り値を正しく処理できないため、場合によっては値をスタックにプッシュせずに直接実行を終了し、戻り値が CHECKVALUE( ).の結果。私たちの解決策は、WITHDRAW() 関数の脆弱性を悪用し、CHECKVALUE() が 0 を返すようにするソルバー コントラクトを作成して、WITHDRAW() 関数が正常に実行され、BANK コントラクトからすべての ETH を抽出することです。
脆弱性分析
まず CHECKVALUE() 関数と CHECKSIG() 関数を順番に実行し、実行結果に基づいてコントラクトのすべての ETH を msg.sender に送信する WITHDRAW() 関数を研究しました。で、CHECKSIG() 関数は関数の戻り値を正しく処理しません。この関数は、関数の実行を終了する前に、結果を戻り値としてスタックにプッシュする必要があります。ただし、場合によっては、関数がスタックに値をプッシュせずに直接実行を終了するため、戻り値がスタックの先頭の最初の要素 (CHECKVALUE() 関数の実行結果) として誤って読み取られてしまいます。 CHECKSIG() 関数の設計上の欠陥により、署名検証が失敗した場合でも、CHECKVALUE() 関数が 0 を返すようにすることで、WITHDRAW() 関数を成功させることができます。

CHECKSIG() 関数で、入力パラメータ (バイト 32、uint 8、バイト 32、バイト 32) を使用して WITHDRAW() 関数を呼び出し、アドレス 0x 1 を呼び出します。このコントラクトはプリコンパイルされたコントラクトであり、その機能はパラメーターに基づいて公開キー アドレスを回復することです。ここには2つのチェックがあります。 1 つ目は、署名が有効かどうかを確認することです。静的呼び出しが正常に実行された場合は、署名が有効であることを意味するため、入力パラメータの内容は重要ではありません。 2 番目のステップでは、公開キーの正当性がチェックされます。公開キーのアドレスが間違っている場合は、フォールバックせずに関数の最後に直接ジャンプします。この関数には戻り値があり、通常の実行では関数の実行を終了する前に結果を戻り値としてスタックにプッシュする必要があります。
ただし、実行が直接終了した場合、値はスタックにプッシュされません。これにより、戻り値がスタックの先頭の最初の要素 (CHECKVALUE() の結果) として誤って読み取られます。したがって、CHECKVALUE() 関数の実行結果が 0 を返す限り、WITHDRAW() 関数はスムーズに実行され、10 ETH を msg.sender に正常に送信できます。

CHECKVALUE() 関数の実行結果が 0、つまりスタックの最上位要素が 0 であることを望みます。呼び出し操作を失敗させるには、「0x 10 > callvalue」を満たすだけで済みます。

解決
私たちは Bank コントラクトからお金を引き出すために Solver コントラクトを作成しました。 Bank コントラクトの ETH は、WITHDRAW() 関数の呼び出し操作を通じて Solver コントラクトに送信されます。具体的なプロセスは次のとおりです。
- ソルバー コントラクトのsolve() 関数で、銀行コントラクトの WITHDRAW() 関数を呼び出して、引き出し操作を開始します。 
- WITHDRAW() 関数では、最初に CHECKVALUE() 関数が実行されます。callvalue は 5 wei (0x 10 未満) なので、over ラベルにジャンプします。 
- over タグでは、callvalue * 2 (つまり、10 wei) が呼び出し元 (つまり、Solver コントラクト) に送信されます。なぜなら、ソルバーコントラクトのフォールバック関数では、受信した Ether の量が 10 wei に等しい場合、トランザクションはロールバックされるため、over タグ内の呼び出し操作は失敗し、CHECKVALUE() 関数は 0 を返します。 
- WITHDRAW() 関数は実行を継続します。Bank コントラクトの残高全体を呼び出し元 (つまり、Solver コントラクト) に送信します。これは、セルフバランス呼び出し元のガス呼び出しのコード行によって実現されます。このうち、selfbalance は契約の残高、caller は発信者のアドレス、gas call は通話を開始する操作です。 
- この呼び出し操作が成功すると、Bank コントラクトの残高全体が Solver コントラクトに送信されます。この操作が失敗した場合、noauth ラベルに直接ジャンプし、revert 操作を実行してトランザクションをロールバックします。 

3. 100%
このチャレンジの目標は、SPLIT と _splitsById[ 0 ].wallet の両方の ETH 残高が 0 である必要があることです。この脆弱性は、abi.encodePacked 結果のハッシュを比較することによってパラメータを検証するだけのdistribute() 関数に存在しますが、アカウントとパーセンテージは動的に型指定されるため、割り当てプロセス中に調整することができます。私たちの解決策は、distribute() 関数の引数の検証が不十分であることを利用して、アカウントとパーセンテージの配列を操作することで、入金したよりも多くの ETH を抽出することです。
脆弱性分析
Split コントラクトのdistribute() 関数を使用すると、SplitWallet の作成時に指定したアカウントとパーセンテージに基づいて特定の資産を分配できます。割り当て後、ユーザーは残高に保存されている値に基づいてお金を引き出すことができます。ただし、distribute() 関数にはパラメーターの検証が不十分です。この関数は、abi.encodePacked 結果のハッシュを比較することによってパラメーターを検証するだけですが、アカウントとパーセンテージは動的に型指定されます。したがって、配分プロセス中に、勘定科目と割合をわずかに調整することができます。

SplitWallet{id: 0} を作成するときに、最初のインデックスのアカウントが誤って空白のままになってしまいました。

したがって、変更されたアカウントと割合を使用して SplitWallet{id: 0} からすべての ETH を抽出できますが、ハッシュは変更せずに、誰にも配布することはできません (配列要素は 32 バイトに埋め込まれていることに注意してください)。

同様に、abi.encodePacked によって引き起こされるハッシュ衝突を使用して、スプリットを排出するために入金されたよりも多くの ETH を引き出すことができます。
解決
私たちは主に、SPLIT と _splitsById[ 0 ].wallet の ETH 残高を空にするソルブ関数を作成しました。ソリューション全体の鍵は、アカウントとパーセントの配列を操作し、配布関数の動作を利用することで、ハッシュ検証メカニズムに違反することなく、より多くの ETH を抽出することです。具体的な考え方は以下のとおりです。
- アカウントとパーセンテージの配列を調整することで、ETH の割り当てを制御できます。ここでは、住所 (私たちの住所) を 1 つだけ持つ accounts 配列と、2 つの要素を持つパーセント配列を使用します。 
- Split.distribute 関数を使用して、SplitWallet から当社のアカウントに ETH を引き出します。このステップは、配布関数のパラメータを適切に調整して ETH を確実に受け取れるようにすることで実現されます。 
- 次に、Split インスタンスを作成し、受信者としてアドレスを設定します。 
- Split.deposit 関数を通じて一定量の ETH を入金し、再度 Split.distribute 関数を使用してさらに ETH を引き出します。 
- 最後に、split.withdraw 関数を呼び出して、Split コントラクトからすべての ETH を引き出し、チャレンジを完了します。 

4. Dai++
このチャレンジの目標は、ステーブルコインの総供給量が 10^12* 10^18 を超えるようにすることです。この脆弱性は、AccountManager コントラクトが ClonesWithImmutableArgs を使用して新しいアカウントを作成する場合、不変パラメータの長さ制限が無視され、パラメータの長さが 65535 バイトを超えるとデプロイされるコントラクトが破損するというものです。私たちの解決策は、長すぎるパラメーターを使用してアカウントを作成し、increaseDebt() 関数をファントム関数に変えることでした。これにより、ヘルスチェックがバイパスされ、負債を増やすことなく大量のステーブルコインが鋳造できるようになります。
脆弱性分析
SystemConfiguration コントラクトによって承認されたアカウントのみがステーブルコインを鋳造できます。 SystemConfiguration の所有者のみがシステム コントラクトを更新 (つまり、アカウントを承認) でき、AccountManager コントラクトが唯一の承認されたコントラクトです。
AccountManager コントラクトでは、有効なアカウントのみがステーブルコインを鋳造できます。同時に、口座の借金も増えてしまいます。

cancelDebt() 関数では、負債が増加した後にアカウントが健全でない場合、トランザクションは失敗します。ただし、プレイヤーには 10^12 ステーブルコインを鋳造してアカウントを健全に保つのに十分な ETH がありません。

AccountManager は ClonesWithImmutableArgs を使用して新しいアカウントを作成することに注意してください。アカウントと対話するとき、ガスコストを節約するために、不変パラメータが calldata から読み取られます。ただし、CloneWithImmutableArgs には「データ長の保存に 2 バイトが使用されるため、@dev データは 65535 バイトを超えることはできません」というコメントがあります。

不変パラメータは作成したプロキシコントラクトのコード領域に格納されるため、デプロイ時にデータ長に基づいてコードサイズが計算されます。ただし、返すコードサイズも2バイトで格納されます。したがって、runSize が 65535 バイトを超える場合、壊れたコントラクトがデプロイされる可能性があります。この呼び出しを無視するには、increaseDebt() 関数をファントム関数として扱うことができます。

既存のパラメータの長さは 20 + 20 + 32 = 72 バイトで、エンコードされた RecoveryAddresses の長さは 32 バイトの倍数になります。

解決
私たちは Solve コントラクトを作成し、AccountManager コントラクトの脆弱性を悪用して、大量のステーブル コインを鋳造しました。
- まず、AccountManager の openAccount 関数を呼び出して、異常に長いパラメーターを含む新しいアカウントを作成します。これは、長さ 2044 の空のアドレス配列を渡すことによって実現されます。これにより、パラメータの長さが予想される 65535 バイト制限を超えたため、内部で作成されたプロキシ コントラクトが破損しました。 
- パラメーターの長さが正しいことを確認するために、計算式 72 + 2044 * 32 + 2 + 0x 43 - 11 = 65538 が使用されます。ここで、72は既存のパラメータの長さ、2044 * 32はrecoveryAddressesのエンコードされた長さ、2はデータ長を保存するバイト数、0x 43は作成フェーズのバイトコード長、11はランタイムコントラクト時のバイトコード長です。創造された 。計算結果 65538 が最大長 65535 を超えているため、導入時に壊れたコントラクトが作成される。 
- 新しく作成された破損したアカウントを使用して、mintStablecoins 関数を通じて大量のステーブルコインを鋳造します。アカウント契約の破損により、cancelDebt 関数 (アカウントの負債を増加させる) は実際には正しく実行されず、負債を追加せずにステーブルコインを鋳造できます。 

5. DoDont
このチャレンジの目標は、DVM (Proxy Voting Mechanism) プロジェクト内のすべての WETH を盗むことです。この脆弱性は DVM.sol の init 関数にあり、呼び出し制限がないため、誰でも BASE_TOKEN および QUOTE_TOKEN アドレスを変更できます。当社のソリューションは、フラッシュ ローン プロセス中にこれらのアドレスを当社が管理するトークン コントラクトに変更し、フラッシュ ローン メカニズムの残高チェックをバイパスすることでこの脆弱性を悪用します。
脆弱性分析
この DVM プロジェクトを簡単にレビューした結果、次のことに気づきました。DVM.sol の init 関数には呼び出しに関する制限がありません。これが問題の根本原因です。

いつでも init() 関数を呼び出して、BASE_TOKEN と QUOTE_TOKEN を変更できます。、これらはチャレンジのフラッシュ ローンのベース トークン アドレスです。フラッシュ ローンのこのような脆弱性を悪用するのは簡単です。フラッシュ ローン プロセス中に、BASE_TOKEN と QUOTE_TOKEN を、それらが管理するトークン コントラクト アドレスに変更します。これにより、フラッシュ ローン メカニズムの残高チェックをバイパスして、フラッシュ ローン期間中の残高を管理することができます。

解決
チャレンジ コントラクトと対話するために、Solve コントラクトを作成しました。攻撃を実行するエクスプロイト コントラクトを作成します。このコントラクトでは、最初に flashLoan 関数を使用して WETH 残高を取得し、次に DVMFlashLoanCall 関数を通じて init を呼び出して、BASE_TOKEN と QUOTE_TOKEN のアドレスを制御されたトークン コントラクトに変更します。このようにして、フラッシュ ローン メカニズムの残高チェックをバイパスし、最終的には DVM 内のすべての WETH を盗むことができます。

6.Grains of Sand
このチャレンジの目標は、トークン ストアの GoldReserve (XGR) 残高を少なくとも 11111 × 10^8 減らすことです。抜け穴は、GoldReserve トークンは転送時に手数料が請求されるが、トークン ストアは転送手数料のあるトークンをサポートしていないことです。私たちの解決策は、GoldReserve トークンの入出金を繰り返してトークンのストアを使い切ることです (どちらの操作にも送金手数料がかかります)。
脆弱性分析
この課題が存在するプライベート チェーンは、イーサリアム メインネットのブロック 18437825 からフォークされています。

GoldReserve (XGR) トークンは転送時に手数料がかかりますが、転送手数料のあるトークンはトークン ストアではサポートされていません。したがって、コインの入金と引き出しを繰り返すことで、ストアからコインを排出することができます。

次に、まず GoldReserve トークンを取得する必要があります。 trade() 関数を通じて、$XGR の署名を交換できます。

取引注文は部分的に約定される可能性があります。合格Dune、有効期限が切れていない GoldReserve トークンの注文を見つけることができます。幸いなことに、大量の売れ残ったコインを伴う注文が 2 件ありました。

解決
チャレンジ コントラクトと対話するために、Solve コントラクトを作成しました。まず、 trade() 関数を通じて取引して、GoldReserve トークンを取得します。次に、トークン ストアの入出金メカニズムを使用して、トークン ストアのトークン残高を減らす操作を繰り返します。このようにして、GoldReserve トークンをトークン ストアから正常に排出し、チャレンジの条件を満たします。

7.Suspicious Charity
この課題の目標は、Python スクリプトで価格キャッシュを操作して、トークンの価格と流動性の計算に影響を与えることです。この課題の脆弱性は、名前に基づいてプール内のトークン アドレスをキャッシュする Python スクリプトに起因しており、これらの名前が string(uint 8) を使用して構築されている場合、0x 80 を超える値は Python で同じになり、不正なキャッシュが発生します。私たちの解決策は 2 つの取引ペアを作成することです: 1 つはキャッシュ内の tokenPrice を更新するために使用される高価格かつ低流動性の取引ペアであり、もう 1 つは低価格で高流動性の取引ペアであり、 「同じ名前のプール」の tokenAmount。この方法により、Python スクリプトの計算ミスを利用してトークンの価格と流動性を操作することに成功し、最終的に DVM 内のすべての WETH を盗むという目標を達成しました。
脆弱性分析
この問題は、Python スクリプトが string(uint 8) を使用して構築された名前に基づいてトークン アドレスをプールにキャッシュすることに起因します。私たちは気づいたのですが、値が 0x80 を超えると、Python スクリプト内で値が同一になり、不正なキャッシュが発生する可能性があります。 Python スクリプトの get_pair_prices 関数では、これにより価格計算が正しく行われませんでした。

私たちはまず 78 の役に立たない取引ペアを作成し、次に 2 つの操作された取引ペアを作成して攻撃を開始しました。

高価格と低い流動性を特徴とする最初の取引ペアは、キャッシュ内の tokenPrice を更新します。続いて、低価格で流動性の高い 2 番目の取引ペアが「同名プール」内の tokenAmount を更新します。デーモンが実行を続けると、蓄積される寄付額はかなり高い値に達します。

解決
チャレンジを完了するにはエクスプロイト コントラクトを作成します。この契約は最初にいくつかの役に立たないトークン取引ペアを作成し、次に高価格低流動性取引ペアと低価格高流動性取引ペアを作成します。このようにして、次のことができます。Python スクリプトで価格キャッシュを操作すると、特定の条件下でトークンの価格と流動性の計算にエラーが発生します。チャレンジ完了後、累計値を指定アドレスに転送します。

8.Token Locker
このチャレンジの目的は、UNCX_ProofOfReservesV2_UniV3 コントラクトの脆弱性を悪用して、コントラクト内の NFT を盗むことです。この脆弱性は、lock() 関数を使用するとユーザーがコントラクト内の流動性をロックできるものの、この関数が受け取る LockParams 構造体の nftPositionManager パラメータが悪意のあるコントラクトに置き換えられる可能性があることです。これにより、カスタム NFT ロケーション マネージャーを通じて NFT のロケーションと流動性を制御できるようになります。私たちの解決策は、UNCX_ProofOfReservesV2_UniV3 コントラクトのロック機能を操作し、CustomNftPositionManager コントラクトを使用して NFT のポジションと流動性を操作する TokenLockerExploit コントラクトを作成することです。このようにして、NFT 契約内の資産を転送および制御し、最終的には契約内の資金を正常に空にすることができます。
脆弱性分析
この問題は、コントラクト UNCX_ProofOfReservesV2_UniV3 に起因します。これは、実際には、イーサリアム メインネット上の 0x7f5C649856F900d15C83741f45AE46f5C6858234 コントラクトのフォークです。コードを簡単に確認した後、ユーザーが操作できる外部関数、特に lock() 関数を詳しく調べる必要があります。
UNCX_ProofOfReservesV2_UniV3 コントラクトでは、lock() 関数を使用してユーザーが流動性をコントラクト内にロックすることで保護できるようにします。この機能には 2 つのオプションがあります。ユーザーは NFT を全範囲に変換して関連料金を請求し、その料金は要求者に返還されます。もう 1 つは、既存のポジションを活用することです。

この関数は、入力パラメータとして構造体 LockParams、具体的には nftPositionManager を受け取ります。

INonfungiblePositionManager nftPositionManager が利用できるということは、コントラクトを入力できることを意味します。これにより、コントラクトを排出する必要がある外部呼び出しから UNCX_ProofOfReservesV2_UniV3 が返されます。
lock() 関数の実行中に、_convertPositionToFullRange() 関数が呼び出される場合があります。以下に弱点を強調します。

次のようにパラメータを渡すだけです。
- mintParams.token 0 // nftPositionManager は実際の Uniswap ポジション マネージャーのアドレスを返します 
- address(_nftPositionManager) // nftPositionManager のアドレスをカスタマイズします 
- mintParams.amount 1 Desired // 排出したい NFT ID を渡す必要があります。 
ERC 721 と ERC 20 には同じ transfer() 関数があるため、_convertPositionToFullRange() 関数の次の式は次のようになります。自身のNFTを悪意のあるnftPositionManagerに転送する:

解決
NFT を盗むために TokenLockerExploit コントラクトを作成しました。このコントラクトは、UNCX_ProofOfReservesV2_UniV3 コントラクトの lock() 関数を操作し、CustomNftPositionManager コントラクトを通じて NFT のポジションと流動性を操作することにより、契約資金を空にすることを実現します。

9. Skill Based Game
このチャレンジの目標は、ブラックジャック ゲームに連続して勝つことで、0xA65D59708838581520511d98fB8b5d1F76A96cad イーサリアム メインネット上のすべての資金を使い果たすことです。このチャレンジの脆弱性は、ブラックジャック ゲーム コントラクトのディーリング関数 (Deck.deal()) がブロック属性 (block.number や block.timestamp など) に依存してランダム性をシミュレートしており、結果が予測可能になる可能性があることです。私たちの解決策は、カード配付プロセスをシミュレートする攻撃者コントラクトを作成し、予測された結果に基づいて実際に賭けるかどうかを決定することです。
脆弱性分析
このチャレンジを完了するには、どのゲームをプレイするかについて十分な情報に基づいた決定を下せるように、どのカードが引かれるかを事前に知っておく必要があります。次に、カードの処理が契約によってどのように規定されているかを詳しく見てみましょう。プレイヤーは deal() 関数を呼び出す必要があり、最後に checkGameResult() をトリガーする必要があります。

取引プロセスは Deck.deal() 関数内で処理されます。ランダム性を生成するこの方法は、ブロックのプロパティと特定の変数に依存します。,以下のコード スニペットで示されているように。この実装では、結果の予測を可能にする脆弱性が導入されます。

カードを配るプロセスには、ブロックハッシュ、プレイヤーのアドレス、配られるカードの数、block.timestamp のハッシュの計算が含まれます。これはランダム性を模倣するよく知られた方法で、単に必要なブロックを待ち、新しいデータに基づいてゲームの結果を再計算し、ゲームの結果が要件と一致する場合はプレイする必要があります。
解決
攻撃を実行するために、Deck ライブラリを使用して攻撃者コントラクトを作成しました。契約では、まずカードの配付プロセスをシミュレーションし、次に予測された結果に基づいて実際に賭けるかどうかを決定します。

この時点で必要なのは、BLACKJACK コントラクトの資金がなくなるまで、値として 5 ether を使用して、このコントラクトの play() 関数を繰り返し実行することだけです。これを実現するためのスクリプトは次のとおりです。

10. Enterprise Blockchain
このチャレンジの目標は、L1 チェーン上の l1 ブリッジから少なくとも 10 個の FlagToken を抽出することです。この課題の脆弱性は、特定の ADMIN プリコンパイル済みコントラクト呼び出しを処理するときに L2 ノードがクラッシュし、L2 ノードが再起動して前の状態からロードされる可能性があることです。私たちの解決策は、この脆弱性を悪用し、最初にリモート メッセージを L2 から L1 に送信して FlagToken を L1 に転送し、その後 L2 ノードのクラッシュと再起動をトリガーすることです。このようにして、L2ノードの状態が転送前の状態に戻っても、L1への資金転送は成功しており、L2上の資金は減っていないので、チャレンジの目的は達成できた。
脆弱性分析
ここには2つのチェーンがあります。
(1) チャレンジ コントラクトが L1 に展開されます。初期状態では、l1 ブリッジには 100 個の FlagToken (10 進数 18 桁) があります。

ユーザーはブリッジを使用してチェーン間で資金を転送できます。リレーは両方のチェーンで SendRemoteMessage イベントをリッスンし、メッセージをターゲット チェーンに転送します。


SendRemoteMessage イベントを発行するには、sendRemoteMessage() 関数を呼び出し、他のチェーンで実行されるトランザクションをカスタマイズできます。

L2 RPC も提供されており、プレイヤーはある程度のイーサを所有しているため、L2 から L1 にリモート メッセージを送信し、l1 ブリッジからユーザーにトークンを転送できます。

しかし、the sendRemoteMessage() この関数は公的使用を目的としておらず、ethOut() / ERC 20 Out() を介してチェーン間で資金を転送することのみが期待されています。
(2) SimpleMultiSigGov コントラクトが L2 チェーン上に展開されます、アドレス 0x 31337 にあります。これは、プリコンパイルされたコントラクト ADMIN と対話するために使用できます。

ADMIN のプリコンパイルされたコントラクトには fn_dump_state() 関数があり、その関数内の操作により未定義の動作が発生する可能性があります。まず、x.len() は 0x 10 より大きくなければなりません。そうしないと、i == x.len() のときにインデックスが範囲外であるためにプログラムがパニックになります。 states は、x 86-64 上の 16 バイトであるスライス [u 8 ] への生のポインターです。 states.offset のカウント単位はスライスです。 i の最大値は 0x 10 であるため、割り当てる必要がある最小メモリは 0x 100 ではなく 0x 110 (16 * (0x 10 + 1)) になります。したがって、x.len() が 0x 10 より大きい場合、プログラムは未割り当てメモリ states.offset(0x 10) に書き込みます。

fn_dump_state() を呼び出すとき、x.len() > 0x 10 の場合、L2 ノードがクラッシュします。アンビルサービスは間もなく利用可能になります再起動そして以前にダンプされた状態から積載状況。
ステータス ダンプの間隔は 5 秒ですが、リピーターが SendRemoteMessage イベントをキャッチしている限り、メッセージは転送されます。新しいクロスチェーン転送トランザクションがブロックに含まれているが、最新の状態がダンプされていないときに L2 ノードがクラッシュした場合、メッセージは L1 に転送され、L2 の状態は、トランザクションが実行される前の状態にのみ復元できます。転送が発生しました。この場合、ユーザーは L2 に資金を費やすことなく、L1 に資金を転送できます。

0x 31337 の SimpleMultiSigGov のみが ADMIN と対話できますが、トランザクションを実行するための有効な署名を取得できませんでした。さらに、状態カバレッジ セットを使用して、0x 31337 のコードを一時的に上書きし、呼び出しをシミュレートできます。
ADMIN の admin_func_run() 関数がエントリ ポイントです。 fn_dump_state() 関数を呼び出すには、最初の 2 バイトが 0x 0204 である必要があります。

解決
pwn やその他のツールを使用すると、一連の操作を実行して L2 ノードのクラッシュをトリガーし、L2 ノードが再起動して以前の状態からロードするときにクロスチェーン転送を実行できます。このようにして、実際に L2 に資金を費やすことなく、資金を L1 に移動できます。このプロセスには、正確なタイミング制御と L2 ノード ステータスの操作が必要です。


11. Dragon Tyrant
このチャレンジはドラゴンとのゲームです。ドラゴンを倒すと勝利し、チャレンジが完了します。この課題には 2 つの重要な脆弱性があります。
(1) 予測可能な乱数:ゲームの乱数発生過程は予測可能であり、この乱数が攻撃・防御の判断を決定し、ゲームの勝敗に影響を与える。ゲームの乱数ジェネレーターは、特定のブロックチェーン トランザクション (resolveRandomness) を監視することで事前に取得できる予測可能なシードに依存しています。私たちの解決策は、まずトランザクション プール リスナーを通じて十分なシード情報を監視および収集し、次にこの情報を使用して次のシードを予測することです。
(2) 論理的な抜け穴:プレイヤーが伝説の剣と盾を装備した場合にのみ、攻撃値と防御力を最大化できます。ゲーム コントラクトでは、プレイヤーが機器を購入するために自分のストア コントラクト アドレスを渡すことができます。このカスタム ストアが正当であるかどうかを検証するメカニズムは、アドレスではなくストア コントラクトのコードハッシュの比較に基づいています。これは、プレーヤーが公式ストアと同じコードハッシュでコンストラクターが異なるコントラクトを作成できる場合、通常の購入プロセスと価格制限をバイパスできることを意味します。私たちのソリューションは、伝説の剣と盾を購入するためのカスタム ストア契約を作成し、高額な購入コストを回避することでこの脆弱性を悪用します。
ゲームの背景
このチャレンジの背景はミニゲームです。ゲームで超怪力・体格(高い攻撃力・防御力)と60の健康ポイントを持つドラゴンがいます。そしてあなたは、主人公はランダムに生成された弱点属性と1つの健康ポイントを持ち、このドラゴンを倒す必要があります。ドラゴンが戦闘中、あなたとドラゴンは両方とも ERC 721 トークンです失敗してその後破壊される時間(解決策のチェック)、挑戦は成功に等しい。
戦わなければならない、戦わなければならない最大 256 のミニラウンドがあります。各ターンで、あなたとドラゴンは次のことができます。攻撃か防御かを選択してください。ダメージ計算要約すると次のようになります。

各小さなラウンドの後、両当事者の健康ポイントは対応するダメージだけ減少します。パーティの体力が0になるとパーティは破壊され、ゲームが終了します。双方の体力ポイントが0になった場合、攻撃側、つまりプレイヤーは破壊されます。
ドラゴンとプレイヤーの攻撃/防御属性は、それぞれの属性と装備に基づいて計算されます。各陣営は武器と盾を 1 つずつ装備できます。ショップでは非常に強力な剣を含むいくつかの装備が販売されています。ドラゴンには何も装備されておらず、プレイヤーは最初から装備しています。1000 ETH。
ゲーム内には乱数発生器が使用される場所が 2 か所あります。 1 つプレイヤーの属性を決定する、もう一方はドラゴンの攻撃/防御の決定を決定する。ECC ベースの乱数生成器の使用、シードオフチェーンで提供。
攻撃/防御の決定
ドラゴンを倒すには、唯一の体力を維持したままドラゴンの体力を 0 にする必要があります。攻撃/防御のマトリックスを見ると、これは攻撃/攻撃のシナリオを発生させることができないことを意味します。攻撃/攻撃ターン中、両方のプレイヤーの体力が0になり、プレイヤーは失敗します。これは、双方の攻撃属性が相手の体力よりもはるかに高いためです。防御-防御ラウンドはNOPに似ているため、攻撃/防御ラウンドと防御/攻撃ラウンドのみに頼ることができます。

双方の攻撃/防御の決定は次のとおりです。事前に提出してくださいはい、攻撃/攻撃ラウンドを回避するには、事前にドラゴンの選択を知る必要があります。これには予測が必要です乱数の生成そして、予測する必要がありますランダムシード。幸いなことに、PythonライブラリPython のランダム モジュールの出力は、ジェネレーターからの約 20 k ビットの出力を観察すると予測できます。
では、アクセスできない Python ランダム モジュールの 20 k ビット出力をこのライブラリに提供するにはどうすればよいでしょうか?できることが分かりました任意の数のプレイヤーをキャストできます、各鋳造トランザクションはオフチェーン シード プロバイダーをトリガーしてランダム シードを送信する。保留中のトランザクション プール内のこれらのトランザクションを監視することで、シードをキャプチャできます。実際、78 個の鋳造された種子を捕捉した後、ランダムな種子を予測できることがわかりました。:

ECC 乱数発生器は決定論的であるため、ドラゴンの攻撃/防御の決定を予測できます。私たちはいつもドラゴンとは逆のことをします。ドラゴンが攻撃してきた場合、私たちは防御します。ドラゴンが守るなら、私たちは攻撃します。
攻撃/防御属性
装備がなければ、地味な属性では戦闘に失敗してしまいます。ドラゴンの攻撃プロパティは type(uint 40).max、防御プロパティは type(uint 40).max - 1 です。何も装備せずに、私たちが攻撃し、ドラゴンが防御した場合、私たちはドラゴンにダメージを与えません。ドラゴンが攻撃し、私たちが防御すると、すぐに失敗します。
当然のことながら、私たちが注目したのは、伝説の剣。この剣を使用すると、攻撃属性が type(uint 40).max に達し、私たちが攻撃し、ドラゴンが防御したときにドラゴンに 1 HP のダメージを与えることができます。これを60回繰り返すとドラゴンは死んでしまいます。希望がある。
この剣は 100 万 ETH もするのに、1000 ETH しか持っていないのに、どうやって買うことができるでしょうか?判明したのは、この剣を装備せよ、ゲームでは、ストア契約を自分で渡すことができます。店舗契約は工場契約の所有者によって事前に承認されています、ゲームは楽しく続きます。詳しく調べてみると、このチェックは店舗の契約アドレスを確認することではなく、ストアコントラクトのコードハッシュを比較する完了します。これは、同じコードハッシュを使用してストア コントラクトを渡す限り、続行できることを意味します。 extcodehash にはコンストラクターが含まれていないため、同じコードで異なるコンストラクターを使用して独自のアイテム ショップを作成し、それを使用してプレイヤーに剣を装備することができます。
この方法は機能します。次のコンストラクターで偽のストアを使用すると、伝説の剣と新しい伝説の盾を入手できます。

この2つのレジェンド装備で、type(uint 40).maxの攻撃特性とtype(uint 40).maxの防御特性を実装します。ドラゴンが攻撃するとき、私たちはHPを失いません、そして、私たちが攻撃するとき、私たちはドラゴンに1 HPのダメージを与えます。
解決
解決策の段階的なプロセスは次のとおりです。
- プレイヤートークンを私たちのウォレットにミントします。 
- 偽のショップを展開し、それを使用してプレイヤーに伝説の装備を 2 つ装備します。 
- チャレンジの要求に応じて、攻撃者のコントラクトを展開します。この契約は、プレイヤーのトークンを引き継ぎ、戦闘を開始し、プレイヤーに攻撃/防御の決定を提供します。 
- プレイヤーのトークンを攻撃者のコントラクトに転送します。 
- 保留中のトランザクション プール リスナーを開始して、resolveRandomness トランザクションを監視します。シードを捕捉し、十分な情報を収集した後、次のシードを予測します。 
- 追加のプレイヤー トークン 78 個を鋳造します。 
- この時点で、プール リスナーは次のシードを予測するのに十分な情報を収集しているはずです。 
- 予測されたシードは乱数発生器に供給され、ドラゴンの攻撃/防御の決定が決定されます。 
- ドラゴンの決定文字列をビットごとに反転して、プレイヤーの決定を導き出します。質問されると、攻撃者のコントラクトによってプレーヤーの決定が提供されます。 
- 攻撃者コントラクトが攻撃を開始し、その結果ドラゴンは失敗します。 
12. Hopping Into Place
このチャレンジの目標は、クロスチェーンブリッジ契約からすべての資金を引き出すことです。この脆弱性は _AdditionalDebit() 関数に存在し、この関数が債権者の負債を計算する際、challengePeriod が 0 に設定されている場合、負債は追加されません。私たちの解決策は、challengePeriod を 0 に設定して numTimeSlots も 0 にすることでこの脆弱性を悪用し、それによって負債の増加を防ぐことです。次に、bondTransferRoot() 関数を使用して任意の量のトークンを引き出します。この場合、getDebitAndAdditionalDebit() 関数は本来の機能を失い、結果的に負債は増加しません。このようにして、クロスチェーンブリッジの資金を流出させることに成功しました。
脆弱性分析
このチャレンジでは、私たちのアイデンティティはガバナーであるため、クロスチェーン ブリッジの一部の構成を変更できます。
問題の根本は _AdditionalDebit() 関数にあることに気づきました。負債は if ステートメントに追加されます。これは、numTimeSlots が 0 に等しい場合、ステートメントは実行されないことを意味します。債権者の責任は増加しません。この計画が不合理であることは明らかであり、いかなる状況であっても債務の増加を回避すべきではありません。
これを利用して、challengePeriod を 0 に設定することで、numTimeSlots が 0 という条件を達成できます。

このようにして、getDebitAndAdditionalDebit 関数は追加機能を失います。何をしても借金は増えません。

これは、関数の実行後、クレジットが増加した負債より大きくなければならないことを要求する requirePositiveBalance 修飾子にも影響します。ただし、余分な機能が失われるため、負債は変わりません。これはつまりこの修飾子によって変更された関数を使用して、クロスチェーン ブリッジをドレインできます。

最後に、bondTransferRoot のロジックを見てみましょう。この関数は、totalAmount を呼び出し元の負債に設定し、抽出のために totalAmount を transferRoots に追加します。したがって、この関数を使用して任意の数のトークンを引き出すことができます。

解決
私たちは、クロスチェーンブリッジから資金を引き出すためにチャレンジ期間を0に設定することで、さらなる負債の追加を防ぐためにいくつかの重要な契約を書きました。各コントラクトは特定の機能を実装します。
- エクスプロイト コントラクト: これは攻撃の主要なコントラクトであり、攻撃プロセス全体の実行を担当します。最初にチャレンジ契約Challengeに関連付けられ、次に一連の操作を通じてクロスチェーンブリッジIBridgeを操作し、最終的に資金を引き出すという目的を達成します。 
- MockMessageWapper コントラクト: このコントラクトは、クロスチェーン メッセージ配信のプロセスをシミュレートします。実際のアプリケーションでは、効果的な操作は実行されませんが、プレースホルダーとして機能し、エクスプロイト コントラクトがクロスチェーン インタラクション プロセスをシミュレートできるようにします。 
- ソルブ コントラクト: このコントラクトは CTFolver から継承され、Capture The Flag (CTF) チャレンジのチャレンジ コントラクト Challenge と対話するために使用されます。これは主に、エクスプロイト コントラクトのエクスプロイト メソッドを呼び出して攻撃を実行し、攻撃が成功した後にチャレンジが解決されたかどうかを確認する役割を果たします。 
- IBridgeインターフェース:クロスチェーンブリッジのコントラクトメソッドを定義するインターフェースです。これには、保証人の追加、チャレンジ期限の設定、転送ルートの拘束、資金の引き出しなど、エクスプロイトコントラクトで使用されるクロスチェーンブリッジの操作方法が含まれます。 
- IChallenge インターフェイス: このインターフェイスは、チャレンジ コントラクト チャレンジ内のメソッドを定義し、エクスプロイト コントラクトがチャレンジ内のクロスチェーン ブリッジ アドレスにアクセスできるようにします。 


13. Oven
このチャレンジの目標は、隠された FLAG 値を回復することです。この課題の中心となるのは fiat_shamir() 関数です。この関数は、カスタム ハッシュ関数custom_hash() を使用して乱数を生成し、この数値を使用して計算に参加します。主要な脆弱性は fiat_shamir() 関数、特に式 r=(v - c * FLAG) mod (p-1) にあり、これには既知の r、c、p 値と未知の FLAG 値が関係します。解決策は、問題を格子問題に変換し、格子基底削減アルゴリズム (LLL アルゴリズム) を使用して FLAG 値を見つけることです。
脆弱性分析
コード関数: ユーザーは FLAG のランダムな署名を取得できます。ランダムな署名を生成するロジックは fiat_shamir() 関数にあります。カスタム ハッシュ関数custom_hash はハッシュ値の生成に使用され、4 つの異なるハッシュ アルゴリズムを呼び出すため、現時点ではそのランダム性を解読することはできません。

さらに、fiat_shamir 変換は暗号化において非常に重要なツールであり、その中心となるのは、ハッシュ アルゴリズムを使用して乱数を生成し、暗号化プロトコルにランダム性を追加することです。 FS 変換の典型的なアプリケーションは、ゼロ知識証明システムに非対話性を導入し、snark や stark などのプロトコルを構築することです。
ソースコードからはt、r、p、g、yなどの情報が得られますが、実はcはcustom_hash()関数を使って計算することができます。したがって、脆弱性は、r=(v - c * FLAG) mod (p-1) に焦点を当て、FLAG に署名する機能部分である fiat_shamir() 関数に集中しています。この式について、現在取得できる情報は、r、c、および p がすべて既知の値であり、FLAG ビットの数が決定されているということです。assert FLAG.bit_length()<384 。 これは、1996 年に Dan Boneh によって提案された HNP 問題 (可変係数による) に関連している可能性があり、標準的な格子アルゴリズムを使用して攻撃できます。ラティスベースの攻撃の暗号解析の詳細については、以下を参照してください。関連論文。
問題は、コード内の r=(v - c * FLAG) mod (p-1) です。 r、c、および p はすべて既知の値であるため、次のようになります。
- まず、上記の方程式を数学的に変換します: r-v+c*FLAG= 0 mod (p-1)。ここで、v と FLAG のみが未知数です。 
- 次に、格子を構築します。  ここで、K は FLAG の上限であり、すべての空白スペースは 0 です。 ここで、K は FLAG の上限であり、すべての空白スペースは 0 です。
- Babai の CVP 解アルゴリズムによれば、jM=jk が true となる解ベクトル j=[l1, l2, l3, FLAG, 1] が存在する必要があります。 
- jk は格子内の短いベクトルであるため、LLL アルゴリズムを使用して多項式時間でこの短いベクトルを見つけることができることに注意してください。 short ベクトルの各要素は 64 ビットで表現できるため、上限 K= 2^64 が決定されることに注意してください。 
ヒント: ここでは、データ量の問題に関するメモを示します。 FLAG を回復するために必要なデータセットの数はどうやってわかるのでしょうか?これには、ガウス ヒューリスティックを使用して最短ベクトル長を推定する必要があり、必要なターゲット ベクトル ノルムはこの長さよりも小さくなります。ただし、これは CTF コンテストのコンテキストであるため、通常は最初に 3 ~ 4、または 5 セットのデータを使用できます。そうでない場合は、上記の方法を使用して正確に計算できます。ここ、バックアップ用に 5 セットのデータを収集しましたが、実際に FLAG を解くために使用されたデータは 3 セットのみでした。
解決
私たちのコードは sage-python 環境で実行する必要があります。主なアイデアは次のとおりです。
- ラティスを構築する: まず、既知の p、c、r 値と未知の FLAG 値を含む特定のラティスを構築します。このグリッドは上記の方程式から変換されます。 
- LLL アルゴリズムの使用: LLL アルゴリズムを適用して、格子内の短いベクトルを見つけます。 LLL アルゴリズムは、元の問題の解法に数学的に関連する格子の基底ベクトルを多項式時間で見つけることができる効率的なアルゴリズムです。 
- FLAG の回復: 短いベクトルが見つかると、そこから FLAG の値を抽出できます。短いベクトルの要素は 64 ビットで表現できるため、FLAG のサイズに上限が設けられます。 

競技から練習まで
Salute チームは、Paradigm CTF 2023 コンテストで貴重な経験を積みました。Salus Security が提供する強化されたスマート コントラクト監査サービスの重要な部分。一流のスマートコントラクト監査サービスが必要な場合は、お気軽にお問い合わせ。私たちは、お客様のニーズに合わせて包括的かつ効率的なサポートを提供することに尽力します。


