Polygon 桥利用内存损坏实现任意消息伪造

  • hexens
  • 发布于 2026-03-24 15:26
  • 阅读 2

本文深入分析了 Polygon Plasma 桥中一个价值 8 亿美元的严重安全漏洞。该漏洞由 MPT 证明库的提前终止缺陷与 RLP 阅读器库的内存越界漏洞共同构成,攻击者可利用 Solidity 内存残留机制伪造任意提现事件。目前该漏洞已修复。

关键影响:

  • 无需任何先决条件或特定要求。
  • 仅需单笔包含恶意证明的交易即可发起攻击。
  • 报告时约有 8 亿美元的 POL 面临风险。

本文披露了 Polygon Plasma 桥中的一个漏洞。该漏洞已于 2024 年 7 月修复,修复补丁也已推送到受影响的库中。

Polygon Plasma 桥利用每个区块生成的交易收据(Transaction Receipts)的默克尔帕特里夏树(Merkle Patricia Trie, MPT),允许用户证明 Polygon 上触发了某个事件。对于 POL 而言,这通常是 Withdraw 事件,该事件仅在用户销毁 POL 时触发。

第一个漏洞存在于 MPT 证明库中,该库允许在扩展节点(Extension Node)上提前停止,导致扩展节点内的 32 字节哈希值被解释为 RLP 编码的交易收据。

通常情况下,哈希值无法正确解析为交易收据,但 RLP 解析库中的第二个漏洞可被利用,使得某些哈希值的解析过程超出边界,进入未分配的内存区域。

通过构造特定哈希值,研究发现其复杂性较低,通过搜索 Polygon 链的历史记录,可以找到多个可用于触发 RLP 漏洞的哈希。

由于对 MPT 验证器的外部调用,未分配的内存可以填充可控数据。Solidity 会将调用数据(calldata)填充到内存中,但随后会减小空闲内存指针以“取消分配”该内存。

交易收据的 RLP 解析会跳转到这块内存中。通过计算精确的偏移量,可以在证明之后完全控制收据内容。这意味着可以证明 Polygon 链上发生了任何任意事件,包括提取 Polygon 桥中全部资金的 Withdraw 事件。

Polygon 跨链桥概览

Polygon 网络采用了多种跨链桥,如 PoS 桥、Plasma 桥、FxPortal 和 zkEVM 桥。前三者的工作方式类似,都使用交易收据证明,但在使用场景上略有不同。本次漏洞仅适用于 Plasma 桥(0x401F6c983eA34274ec46f84D70b31C151321188b),该桥负责锁定以太坊与 Polygon PoS 之间的所有 POL。

Plasma 桥架构

Polygon Plasma 桥最初设计为包含退出交易、状态证明等功能的完整 Plasma 桥。但由于 Plasma 的复杂性和高成本,它后来被简化为与 PoS 桥几乎相同的模型:证明交易收据。目前实际运行的代码是“仅限销毁(Burn Only)”的谓词逻辑。

上图展示了 Plasma 桥的简化版本:

  • DepositManager:负责 POL 充值和托管。
  • WithdrawManager:负责验证证明并管理退出队列。目前退出队列的等待时间仅为 1 个区块。
  • Predicate 合约:针对不同代币类型(ERC20, ERC721, ERC1155)有不同的谓词合约。在 Plasma 桥中,仅使用 ERC20PredicateBurnOnly 处理 ERC20 类型的 POL。

事件证明机制

用户必须提供证明,证实 Withdraw 事件已在 Polygon PoS 链上由代币合约触发。该事件包含接收者和释放的代币数量。

Polygon PoS 是以太坊 Geth 的分叉,因此使用与以太坊相同的数据结构。交易过程中发生的事件存储在交易收据中,而交易收据存储在 MPT 中。用户需要根据收据根哈希(Receipt Root Hash)进行证明。

在以太坊主网上,验证者通过 RootChain 合约提交检查点(Checkpoints)。一旦提交,检查点哈希及其证明的数据就成为了 Polygon PoS 历史的一部分。

function startExitWithBurntTokens(bytes calldata data) external {
    ExitPayloadReader.ExitPayload memory payload = data.toExitPayload();
    ExitPayloadReader.Receipt memory receipt = payload.getReceipt();
    uint256 logIndex = payload.getReceiptLogIndex();
    require(logIndex < MAX_LOGS, "Supporting a max of 10 logs");
    uint256 age = withdrawManager.verifyInclusion(
        data,
        0, /* 偏移量 */
        false /* 验证交易包含情况 */
    );
    ExitPayloadReader.Log memory log = receipt.getLog();

    address childToken = log.getEmitter();
    ExitPayloadReader.LogTopics memory topics = log.getTopics();

    require(
        bytes32(topics.getField(0).toUint()) == WITHDRAW_EVENT_SIG,
        "Not a withdraw event signature"
    );
    require(
        msg.sender == address(topics.getField(2).toUint()), // 来自
        "Withdrawer and burn exit tx do not match"
    );
    address rootToken = address(topics.getField(1).toUint());
    uint256 exitAmount = BytesLib.toUint(log.getData(), 0); // 金额或代币 ID
    withdrawManager.addExitToQueue(
        msg.sender,
        childToken,
        rootToken,
        exitAmount,
        bytes32(0x0),
        true, /* 是否为常规退出 */
        age << 1
    );
}

在 Github 上查看

ERC20PredicateBurnOnly 的退出函数中,输入数据首先被解析为 ExitPayload。随后提取收据、日志和主题。谓词合约通过 WithdrawManagerverifyInclusion 函数委托验证 MPT 证明。

核心漏洞分析

MerklePatriciaProof 库漏洞

第一个漏洞位于 MerklePatriciaProof 验证库中。

在 MPT 树中证明数据需要提供节点和路径。MPT 节点有特定的前缀:0 和 1 代表扩展节点,2 和 3 代表叶节点。正常情况下,用户只能通过遍历到叶节点来证明数据。

然而,该库允许在扩展节点上提前停止。如果提供的证明路径较短,遍历会在扩展节点处终止:

                if (pathPtr + traversed == path.length) {
                    //叶节点
                    if (
                        keccak256(RLPReader.toBytes(currentNodeList[1])) ==
                        keccak256(value)
                    ) {
                        return true;
                    } else {
                        return false;
                    }
                }

在 Github 上查看

在扩展节点中,currentNodeList[1] 是下一个分支节点的哈希。通过提前停止,攻击者可以将该哈希值作为 value 输入,从而使证明通过。虽然哈希值是随机的 32 字节,通常无法解析为完整的交易收据,但这为配合第二个漏洞提供了可能。

RLPReader 库漏洞

第二个漏洞存在于 RLPReader 库中,具体位于将 RLPItem 解析为列表的函数中。

    function toList(RLPItem memory item) internal pure returns (RLPItem[] memory) {
        require(isList(item));

        uint256 items = numItems(item);
        RLPItem[] memory result = new RLPItem[](items);

        uint256 memPtr = item.memPtr + _payloadOffset(item.memPtr);
        uint256 dataLen;
        for (uint256 i = 0; i < items; i++) {
            dataLen = _itemLength(memPtr);
            result[i] = RLPItem(dataLen, memPtr);
            memPtr = memPtr + dataLen;
        }

        require(memPtr - item.memPtr == item.len);

        return result;
    }

在 Github 上查看

该函数直接信任由 itemLength 读取的数据长度。由于没有校验子项长度是否超过了父项的总长度,列表的最后一个元素可以溢出到父 RLPItem 的边界之外。

漏洞利用过程

哈希解析构造

为了利用该漏洞,我们需要找到一个特殊的扩展节点哈希,使其满足以下条件:

  1. 能被解析为收据(RLP 列表)。
  2. 第 4 个元素(日志列表)触发越界读取。
  3. 日志列表中的某个元素引导解析器跳转到 ExitPayload 之外的内存区域。

通过分析 ExitPayloadReader 的逻辑,攻击者可以精心构造偏移量,通过设置 receipt.logIndex = 1,跳过大元素并从“未分配但已填充”的内存中解析实际的 Log 结构。

寻找目标哈希

研究人员编写了脚本在 Polygon 历史记录中搜索符合条件的哈希。在 1600 万到 1700 万区块范围内,很快就在区块 17074251 中找到了一个目标哈希:8cf8a384e97b4bf8c814e0be6e1c3573d267ffdf9b8ea8546ba5b5b9e5f2a205

该哈希的解析步骤如下:

  • 8c:小于 0xc0,被跳过。
  • f8 a3:长度为 0xa3 的长列表。
  • 84 e97b4bf8:4 字节字符串(收据第 1 元素)。
  • c8 14e0be6e1c3573d2:8 字节列表(收据第 2 元素)。
  • 67:单字节(收据第 3 元素)。
  • ff df9b8ea8546ba5b5:超长列表,将被解析为日志。
  • b9 e5f2:长度为 58866 字节的长字符串,用于实现内存跳转。

污染未分配内存

由于 ERC20PredicateBurnOnly 在解析日志之前会先调用 WithdrawManager 进行外部调用,Solidity 会使用 CALL 指令。这会将整个 data 字节参数加载到内存中。调用结束后,Solidity 虽然会移动空闲内存指针,但不会清除这些数据。

攻击者只需在 calldata 中填充正确的字节和偏移量,当 RLP 解析器发生跳转时,就会正好落在攻击者控制的恶意 Log 数据上。

构造最终 Payload

利用 Python 脚本重新计算 RLP 编码长度,并构造包含以下内容的恶意 Payload:

  • 目标区块的检查点证明和收据根。
  • 缩短的 MPT 证明路径(在扩展节点停止)。
  • 目标扩展节点哈希。
  • 约 57049 个零字节的缓冲区,用于对齐内存跳转。
  • 注入的恶意 Log 数据(包含伪造的接收者地址和巨大的 POL 金额)。
  • 结束标记 0xbfffffffffffffffff,防止解析器因处理超长列表而耗尽 Gas。

影响分析

通过调用谓词合约的退出函数并提交该恶意证明,攻击者可以伪造任何事件。这足以在单笔交易中提取跨链桥内的全部 POL 余额,造成毁灭性损失。

修复与披露

该漏洞已于 2024 年 7 月报告给 Polygon 安全团队。

修复措施:

  1. RLPReader 库:在 toList 函数中增加了对子项长度的总和校验。查看提交
  2. MerklePatriciaProof 库:增加了对扩展节点和叶节点的前缀校验。查看提交

进一步研究

由于 RLPReader 是一个外部库,研究人员使用静态分析工具 Glider 对以太坊及其他链上的合约进行了扫描。虽然发现多个项目使用了该库,但大多数项目的调用顺序或逻辑并不满足漏洞触发的严苛条件(例如,必须在两次解析之间存在能污染内存的操作)。目前已知受影响的项目均已得到妥善处理。

  • 原文链接: hexens.io/research/polyg...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
hexens
hexens
江湖只有他的大名,没有他的介绍。