Appearance
閃光ローンで狙え! 埋蔵金「WETH」を回収せよ! – Naive Receiver チャレンジ解説
もしあなたが、ブロックチェーンの世界に眠る財宝「WETH」を、大胆かつスマートに回収する方法を探しているなら、この「Naive Receiver」チャレンジはあなたにぴったりです。これは、Damn Vulnerable DeFi v4 の CTF(Capture The Flag)問題の一つで、巧妙に隠された脆弱性を突いて、プールとユーザーコントラクトに眠る全てのWETHを、指定された回収アカウントへ移動させるという、スリリングなミッションです。
チャレンジの舞台裏:
このチャレンジの核心は、NaiveReceiverPool というフラッシュローンを提供するコントラクトにあります。このプールは、1000 WETH を元手に、1 WETH の固定手数料でフラッシュローンを提供しています。さらに、BasicForwarder というコントラクトとの連携により、メタトランザクション(ガス代を別の主体に肩代わりしてもらう仕組み)にも対応しています。
一方、ユーザー(あなた)は、10 WETH を保有するサンプルコントラクト FlashLoanReceiver をデプロイしています。このコントラクトは、フラッシュローンを実行できる能力を持っています。
危険信号:全ての資金がリスクに!
この状況は、一見すると正常に見えますが、実は潜んだ脆弱性があります。チャレンジの目的は、この脆弱性を突いて、プールとユーザーコントラクトに眠る全てのWETHを、指定された「回収アカウント」へ無事に送り届けることです。
攻撃の鍵:BasicForwarder と FlashLoanReceiver の連携
このチャレンジを解く鍵は、BasicForwarder がどのように NaiveReceiverPool のフラッシュローン機能と連携しているかを理解することにあります。
NaiveReceiverPoolのflashLoan関数: この関数は、指定されたreceiver(フラッシュローンを受け取るコントラクト)に WETH を転送し、そのコントラクトのonFlashLoan関数を呼び出します。重要なのは、onFlashLoan関数が成功した後に、receiverからローン額と手数料を徴収する処理です。FlashLoanReceiverのonFlashLoan関数: この関数は、フラッシュローンを受け取った際に実行されるコールバック関数です。注目すべきは、この関数内部で_executeActionDuringFlashLoan()が呼び出されている点です。しかし、この_executeActionDuringFlashLoan()関数は空っぽです! つまり、フラッシュローンを受け取った際に、本来行うべき「ローンの返済」や「何らかのアクション」が、このコントラクトでは実装されていません。BasicForwarderとメタトランザクション:BasicForwarderは、外部から署名されたトランザクションを受け付け、指定されたコントラクトの関数を実行する機能を持っています。これにより、ユーザーは自らのコントラクトから直接トランザクションを発行するのではなく、BasicForwarderを介して、あたかも他のアカウント(この場合はdeployer)からトランザクションが発行されたかのように見せかけることができます。
脆弱性の発見と攻略法:
この構造から、以下のような脆弱性が浮かび上がります。
FlashLoanReceiverのonFlashLoan関数は、フラッシュローンを受け取った後、ローンの返済処理を実際には行っていません。NaiveReceiverPoolは、onFlashLoanのコールバックが成功したとみなすと、FlashLoanReceiverから WETH を引き出そうとします。- しかし、
FlashLoanReceiverが実際にはNaiveReceiverPoolに WETH を返還しないため、NaiveReceiverPoolはFlashLoanReceiverから WETH を徴収できず、結果としてNaiveReceiverPoolから WETH が流出します。 - さらに、
BasicForwarderを利用することで、この WETH の流出操作を、あたかもdeployerが指示したかのように偽装できます。
具体的な攻撃手順(CTF解答コードより抜粋):
フラッシュローンを繰り返し実行:
pool.multicall(calls);の部分では、NaiveReceiverPoolに対して、FlashLoanReceiverをreceiverとして指定し、flashLoan関数を複数回呼び出します。これにより、FlashLoanReceiverは繰り返し WETH を受け取りますが、返済処理を行わないため、プールから大量の WETH がFlashLoanReceiverに転送されます。全資産の引き出しと署名: 次に、
NaiveReceiverPoolのwithdraw関数を呼び出すためのデータ (withdrawData) を作成します。この際、引き出す量はプールとFlashLoanReceiverに存在する全ての WETH を合算した額にします。 そして、BasicForwarderのRequest構造体を構築し、withdrawDataをdataフィールドに含めます。fromはdeployerに設定し、targetはpoolに、deadlineは現実的な未来の時間に設定します。vm.sign(deployerPk, digest);により、deployerの秘密鍵でこのリクエストに署名します。メタトランザクションによる実行: 最後に、
forwarder.execute{value: 0}(req, sig);を実行します。これにより、BasicForwarderは署名を検証し、deployerが指示したかのようにpool.withdraw関数を実行します。この時、FlashLoanReceiverから徴収されなかった WETH が、最終的に指定されたrecoveryアドレスに移動します。
まとめ:
「Naive Receiver」チャレンジは、フラッシュローンの仕組み、メタトランザクションの応用、そしてコントラクト間の連携における見落としがちな脆弱性を巧みに突いた問題です。このチャレンジをクリアすることで、あなたはブロックチェーンの複雑な相互作用を理解し、より安全なスマートコントラクト開発に活かせる貴重な洞察を得られるでしょう。
このスリリングな冒険に、ぜひ挑戦してみてください!