臭名昭著的漏洞摘要 #7:时间戳攻击、重入失败与金库劫持

该文是《The Notorious Bug Digest 7》精选汇编,深入分析了近期Web3领域的三起安全漏洞和事件。文章详细解读了CometBFT中的时间戳操纵漏洞、Gnoswap中因值语义导致的重入锁失效问题,以及Taraxa Bridge中不当授权检查造成的资产劫持。通过这些案例,旨在为Web3安全社区提供教育资源和经验分享。

欢迎来到《臭名昭著的漏洞摘要》第七期,这是一份精选的关于近期Web3漏洞和安全事件的洞察汇编。当我们的安全研究人员不忙于审计时,他们会投入时间及时了解安全领域的最新动态,分析审计报告,并剖析链上事件。我们相信这些知识对更广泛的安全社区来说是无价的,它为研究人员提供了提升技能的资源,并帮助新来者了解Web3安全世界。加入我们,一起探索这批漏洞!

漏洞分析:BFT时间计算中静默跳过如何导致时间戳操纵

在CometBFT中,区块时间并非简单地由提议者的本地时钟决定。相反,它被确定性地计算为前一个区块的LastCommit中验证者提供的时间戳的加权中位数。这种机制,被称为“BFT时间”,确保了恶意少数派无法改变区块链的时间。

当一个区块被提议时,其他节点通过检查LastCommit来验证它。这涉及两个不同的操作:

签名验证

确保超过2/3的投票权签署了该区块。

时间验证

重新计算加权中位数时间,以确保它与区块头中的时间匹配。

索引与地址

核心问题源于在执行这两个操作时,验证者身份识别方式的不一致。

签名验证(基于索引)

为了效率,签名验证依赖于验证者在验证者集中的索引。在易受攻击的代码中,验证逻辑通过索引获取验证者,但并未验证提交签名(CommitSig)中的ValidatorAddress字段是否与验证者匹配。签名涵盖了BlockID、Height、Round和Timestamp,但涵盖ValidatorAddress字段。

时间计算(基于地址)

相反,用于计算区块时间的MedianTime函数通过提交的ValidatorAddress来查找验证者。如果找不到给定地址的验证者,代码会静默跳过该签名,而不是返回错误。

攻击者,一个恶意验证者,可以提议一个包含有效签名的区块,并将ValidatorAddress替换为假的地址。签名验证(使用索引)通过,但时间计算(使用地址)未能找到验证者并静默跳过它。通过选择性地“静默”特定验证者,攻击者可以使加权中位数计算偏向他们选择的值。

为了修复这个问题,CometBFT v0.38.21在签名验证期间引入了严格的地址验证(确保索引与地址匹配),并更新了MedianTime函数,使其在找不到验证者时返回错误,而不是静默跳过。

这强调了数据验证一致性的重要性。当多个子系统(验证与计算)依赖于相同的数据结构时,它们必须使用相同的识别方法和验证严格性。此外,静默失败(例如在没有错误的情况下跳过缺失的验证者)通常会隐藏可被利用的逻辑错误。

请参阅以下文章以了解可能的利用途径概述:Tachyon:从时间扭曲攻击中挽救6亿美元

漏洞分析:值语义静默破坏Gnoswap中的重入锁

此漏洞是在OpenZeppelin对Gnoswap进行扩展审计期间发现的。

Gnoswap是一个建立在Gno(一种Go衍生智能合约语言)之上的去中心化交易所。与Uniswap V3类似,Gnoswap的Swap函数使用Slot0结构体内部的布尔型unlocked标志来防止重入。预期的模式是:读取Slot0,在任何外部回调之前设置unlocked = false,执行兑换,并通过defer恢复unlocked = true。任何重入调用都会看到该标志并触发panic

池通过一个getter公开Slot0,该getter按值返回结构体:

在Go(和Gno)中,按值返回结构体意味着调用者会收到一个独立的副本。Swap函数以以下方式使用此getter:

由于pool.Slot0()返回一个值,slot0Start是一个独立的副本。因此,调用slot0Start.SetUnlocked(false)只会修改这个局部变量,而池的实际slot0.unlocked字段仍保持true。重入防护从未生效。任何重入调用Swap都会读取pool.Slot0().Unlocked(),看到true,并毫无问题地通过检查。

由于防护被禁用,swapCallback成为了一个无防护的入口点。在回调返回后,safeSwapCallback会检查池的代币余额是否按预期增加,作为安全网。然而,由于重入是可能的,攻击者可以在回调中调用其他池或头寸函数(例如IncreaseLiquidity),将代币存入池中。这些存款会夸大余额,导致回调后检查通过,即使攻击者没有进行预期的兑换偿还,从而有效地从池中窃取价值。

在像Go和Gno这样具有值语义的语言中,依赖于突变(修改)的模式(例如重入防护)可能会静默失败。当getter按值返回结构体时,对返回副本的任何突变都会丢失,除非明确地写回。审计人员在审查Go或Gno智能合约时,应密切关注状态修改操作是作用于实际存储的状态还是作用于临时副本。

漏洞分析:FAsset系统中的代理金库通过陈旧委托链接被劫持

此漏洞是在OpenZeppelin对Flare FAssets进行审计期间发现的。

FAsset系统是Flare网络的核心组件,它能够将其他链(例如BTC、XRP)的资产以FAssets的形式在Flare上进行无信任发行。代理在代理金库中锁定原生抵押品(例如FLR或SGB),并以此赚取费用。

代理委托机制

AgentOwnerRegistry合约支持两层权限模型,以实现操作安全:

管理地址:

由治理列入白名单的金库所有者,拥有完全权限,包括抵押品提款。

工作地址:

一个被委托的地址,拥有有限权限(例如,添加抵押品),用于日常操作。

两个映射实现了这一机制:workToMgmtAddress(工作地址 → 管理地址)和mgmtToWorkAddress(管理地址 → 工作地址)。一个列入白名单的代理通过setWorkAddress函数链接一个工作地址。重要的是,该函数阻止已列入白名单的地址被设置为工作地址。否则,在金库创建时使用的_getManagementAddress会将工作地址解析为其管理者,并错误归属金库所有权。规则是:一个列入白名单的代理必须始终被视为顶级所有者,而不是下属。

set-work-address-1

该漏洞是一个生命周期疏忽:setWorkAddress仅在委托时强制执行“不允许列入白名单的工作地址”规则。当一个地址的状态随后发生变化时,协议不会重新验证或清除现有链接。如果一个已经注册为工作地址的地址后来被治理列入白名单,旧的委托就会保留。这个陈旧的workToMgmtAddress条目随后可以被滥用以劫持新代理的金库。

失败链始于whitelistAndDescribeAgent。这个治理函数添加新代理,但检查候选地址是否已出现在workToMgmtAddress中。白名单过程继续进行,不清除或使任何现有工作地址链接无效,因此预先存在的委托仍然活跃。

white-list-functions

当受害者(现在已列入白名单)调用createAgentVault时,合约使用_getManagementAddress来解析所有权。该辅助函数信任workToMgmtAddress。由于受害者之前被绑定为工作地址,他们被解析为攻击者的管理地址。因此,金库在攻击者的控制下创建并通过白名单检查。

getManagementAddress-function

create-agent-vault

利用此漏洞的攻击将按以下方式进行:

  1. 攻击者A(已列入白名单)通过setWorkAddress(B)将受害者B(未列入白名单)绑定为A的工作地址。这成功了,因为B尚未列入白名单。A还可以通过监控内存池并在白名单交易执行前发送setWorkAddress(B)来抢先运行治理即将进行的B的白名单操作,确保在A的调用时B未列入白名单,同时锁定工作地址与管理地址的链接。
  2. 治理随后使用whitelistAndDescribeAgent将B列入白名单,该函数调用内部的_addAddressToWhitelist,但验证或清除现有的workToMgmtAddress条目。遗留映射持续存在。
  3. 当B创建代理金库时,所有权解析在_getManagementAddress中将B → A,并且createAgentVault中的白名单要求由A满足。此时,金库的管理地址是A,B是A的工作地址,允许B添加抵押品,同时使A能够窃取抵押品。

经验教训是,访问控制检查必须具备生命周期感知能力。仅在单个时间点(例如,在委托时)验证状态是不够的,因为实体随后可能会改变角色(例如,从工作地址变为列入白名单的代理)。因此,安全性必须考虑状态转换,并重新验证或使相关状态失效。

修复方案是引入工作地址委托的明确选择加入:被提名的工作地址必须接受链接,然后它才能生效。这消除了在未经同意的情况下绑定地址的能力,并关闭了劫持途径,同时保持了白名单和委托的可用性。

事件分析:不当授权检查导致Taraxa桥被恶意接管

Taraxa桥智能合约(用于在以太坊和Taraxa链之间进行资产转移)于2026年2月22日遭到利用,导致约1.3万美元的ETH、USDT和TARA代币损失。该漏洞是由于不当授权检查造成的,它依赖于同一交易中较早修改的状态,使攻击者能够颠覆基于验证者的法定人数阈值。

桥架构依赖于TaraClient合约在以太坊上最终确定Taraxa区块。此过程涉及验证者签名验证,确保累积验证者权重达到预定义的weightThreshold,然后通过finalizeBlocks函数将批准的桥根存储在finalizedBridgeRoots映射中。Bridge合约随后通过applyState函数使用这些已最终确定的根,以在以太坊上执行相应的状态变更,例如释放桥接资产。

漏洞源于TaraClient验证验证者法定人数是否达到所需的weightThreshold之前,通过processValidatorChanges应用了验证者更新。具体来说,合约使用与尝试区块最终确定在同一交易中提供的calldata来更新验证者的voteCounttotalWeights。这些新修改的权重随后立即用于授权检查。

taraXa-finalizeBlocks

通过精心制作恶意的验证者更新,攻击者人为地夸大了其控制下的验证者的权重。由于法定人数检查是针对这种修改后的状态执行的,攻击者能够在不拥有合法验证者共识的情况下满足weightThreshold。这使得攻击者能够最终确定一个恶意的桥根,并将其存储在finalizedBridgeRoots中。

taraXa-processValidatorChanges

随着欺诈性的桥根现在被视为有效,攻击者调用Bridge.applyState,该函数信任了该根并执行了编码的状态转换。结果,桥释放了其托管资产,将ETH、USDT和TARA代币直接转移给攻击者。

此事件强调了一个关键的智能合约设计缺陷:授权和共识检查绝不能依赖于在同一调用上下文中较早修改的状态。验证者集更新和法定人数验证必须严格分离或分阶段跨交易进行,以防止攻击者在执行过程中操纵信任假设。

免责声明

需要强调的是,此内容的目的并非批评或指责受影响的项目。相反,目标是提供对漏洞的客观分析,作为Web3社区学习和未来更好地保护自身的教育材料。

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

0 条评论

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