导航菜单
首页
排名 涨幅榜 跌幅榜 24h成交额 新币榜
快讯 机构 观点 人物 专题

3.2亿美元安全事件复盘:Liquid缓存键碰撞与Symbiosis负手续费漏洞

导语:过去一周(2026/09/07-2026/09/13)收录2起安全事件,合计损失约3.2亿美元。一起发生在比特币侧链Liquid Network,另一起是部署在BNB Smart Chain、Ethereum和Rootstock的比特币桥Symbiosis。Liquid事件占99%以上,Symbiosis估计损失约77万美元。

两起事件共同点在于:本该拦下伪造的校验都跑通并返回“通过”,但通过理由都不对。Liquid节点把一份自己从未检视过的范围证明判定为true,因为另一份证明的结论恰好归档在同一缓存键下;Symbiosis的签名者对铸造数额签下有效签名,而这个数额由比特币一侧decoder根据存款人可控字段算出。

一笔伪造余额值多少钱,取决于它最终如何变现。Liquid凭空造出4,000枚L-BTC,背后没有真实比特币,但绝大部分经双向锚定变成真BTC,约3.2亿美元流出;Symbiosis铸出的syBTC超过比特币总供应量两千倍,但必须卖进的资金池里只有11.26枚syBTC,流动性提供者与用户估计损失9.97 BTC。

3.2 亿美元:毁在一个缓存键上

表1:本周收录的安全事件概览

  • Liquid Network:2026/09/06,损失约3.2亿美元,漏洞为范围证明缓存键碰撞。
  • Symbiosis:2026/09/11,估计损失9.97 BTC(约77万美元),漏洞为链下身份解码与负手续费。

注:Liquid Network事件发生于9月6日,未被上周报告覆盖,此处纳入以保持完整性。

本周看点:Liquid Network

2026/09/06,比特币侧链Liquid Network被攻击,损失约3.2亿美元。Liquid所运行的节点软件Elements,其范围证明校验缓存发生碰撞:为某个输出记录下的valid结论,被返还给另一个完全不同的输出,后者的证明因此在从未被检视的情况下获得通过。攻击者借此凭空造出4,000枚没有任何比特币对应的L-BTC,再通过网络正常的peg-out提走。次日,3,400枚BTC被退还给联邦,约598.5枚BTC留在攻击者手中。

背景

Liquid Network是一条构建在Elements之上的比特币侧链。比特币在两条网络之间通过双向锚定往返:peg-in时用户把BTC锁定给联邦,换得等量L-BTC;peg-out时用户销毁L-BTC,联邦释放对应BTC。Liquid区块由联邦轮值职能节点提出,再由达到门限数量的联邦签名完成最终确认。

和比特币一样,Liquid把资金记作未花费交易输出。每个输出都带有一段锁定脚本scriptPubKey。以OP_RETURN开头的输出永远无法被花费,一次peg-out正是以这种输出形式表达。Liquid还默认隐藏金额,写入输出的是Pedersen承诺。承诺具有可加性,节点无需知晓数值即可确认输入与输出是否配平,但这也意味着必须依赖范围证明,证明被承诺金额落在[0, 2^64)之内。验证范围证明代价不低,同一输出会被验证多次,因此Elements会把已经通过的证明缓存起来,以该证明本身以及它当时所比对的数据为键。

漏洞分析

缺陷在于Elements如何缓存已验证范围证明。CachingRangeProofChecker::VerifyRangeProof()会为眼前输出推导缓存条目,一旦命中就立即返回成功,根本不去碰那份证明。

3.2 亿美元:毁在一个缓存键上

条目由ComputeEntryRangeProof()生成,它把四个字段一个接一个写进同一个SHA-256流,然后取出摘要:范围证明本身、对金额的Pedersen承诺、标识输出资产种类的资产生成元,以及scriptPubKey。四个字段都没有长度前缀或分隔符:范围证明和scriptPubKey是变长的,Pedersen承诺和资产生成元则始终是33字节定长点。于是摘要记下的只是拼接后的字节,字段边界无从体现。两组不同字段只要拼接出同一串字节,就会得到同一个条目,先被校验的那一组会留下结论,供另一组直接取用。哈希器用每个节点各自的随机salt初始化,但salt同样加在两条字节流最前面,改变摘要却无法区分两份输入。该缺陷在提交94000967中被修复。

3.2 亿美元:毁在一个缓存键上

攻击分析

以下分析基于交易271147...187ec5和f24a4b...0a183f,两笔都被运行着存在缺陷版本代码的节点所接受。

Step 1:攻击者发出一笔交易,其第一个输出是一个承载67字节数据的OP_RETURN。

3.2 亿美元:毁在一个缓存键上

它的scriptPubKey以6a 43开头,后跟一次67字节推送,然后是两个33字节值和结尾的一个6a。被推送的67字节并不是这笔交易需要的数据,而是另一个尚不存在输出的金额承诺和资产生成元。对这个输出的校验,把一条valid结论存进了由它自己四个字段所决定的条目下。

Step 2:攻击者随后提交增发交易。这笔交易的OP_RETURN输出,其scriptPubKey只有单字节6a,而它的金额承诺正是前一笔交易那次推送里嵌着的33字节值。它的范围证明字段,则由前一笔交易的范围证明、承诺和资产生成元拼成,末尾再接上6a 43两个字节。

3.2 亿美元:毁在一个缓存键上

于是这个输出的四个字段拼接出的字节流与前一笔交易完全相同,只是切分位置换了地方:Primer为P0 | C0 | X | S0,其中S0=6a 43 6a;Inflation为P1 | C1 | X | S1,其中P1=P0 || C0 || X || 6a 43,S1=6a。两者都拼接为P0 || C0 || X || 6a 43 || C1 || X || 6a。缓存返还了此前那条结论,P1从未被验证过,它根本不是一份范围证明。C1承诺的是一个数额很小的负值。这笔交易的另一个输出是发往攻击者地址ex1q7kg...qa2w的普通P2WPKH输出,带着一个数额很大的正向承诺和一份货真价实的范围证明,两者正好相互抵消,交易因此配平。它留下了4,000枚没有任何比特币对应的未花费L-BTC。

Step 3:攻击者执行peg-out。两笔交易通过普通OP_RETURN peg-out输出销毁了3,996.01834922和2.65138358枚L-BTC,合计3,998.66973280枚。负责授权peg-out的职能节点用的还是存在缺陷的代码,校验通过后释放比特币:3,995.99999857枚BTC和2.49749857枚BTC抵达攻击者在比特币网络上的地址,共计3,998.49749714枚。

网络暂停并分裂成两条链,事件处置后恢复运行时拒绝了那笔无效peg-out。如今区块浏览器上primer交易显示为已确认,而inflation交易没有,尽管它放出的比特币早已从联邦钱包流出。次日16:09:25 UTC,攻击者把3,400枚BTC退还到联邦锚定钱包,剩余约598.5枚BTC仍未归还。

结论

攻击者没有攻破任何密码学,也没有拿到任何私钥。缺陷在于范围证明缓存如何标识“这个我已经查过了”:它的键把四个字段——其中两个是变长的——拼进同一条哈希流,而没有任何东西标明每个字段到哪里为止,于是另一组字段可以把同样的字节重新切分一遍,落到同一个条目上。一条存好的valid结论就这样被返还给了一份任何节点都不曾验证过的证明;一旦范围证明不再约束背后的金额,隐藏金额账本也就失去了唯一能保证这些数值非负的东西。一个精心构造的输出变成了4,000枚L-BTC,双向锚定又把它们变成了主链上真正的比特币。任何由一个以上变长输入推导出来的标识符,都必须把输入之间的边界一并纳入:要么给每个字段加长度前缀,要么把各字段哈希进彼此分开的定长槽位。做不到这一点,推导就不是单射的,两份不同输入便可能被当成同一份。这条要求在以缓存替代校验的场合尤其关键,因为一次命中就是一个“不再执行校验”的决定。

本周其它事件:Symbiosis

2026/09/11,跨链桥Symbiosis的比特币通道在BNB Smart Chain、Ethereum和Rootstock三条部署上被攻击,流动性提供者与用户的估计损失为9.97 BTC(约77万美元,按9月11日约77,000美元价格计)。负责决定要为一笔比特币存款铸造多少合成比特币的链下代码,从交易中存款人可以控制的一部分数据里读取存款人身份,让攻击者得以冒充管理员,把最低手续费压到负数;随后它从存款额里减去这笔手续费时不检查符号,于是减法变成了加法。

背景

Symbiosis是一个跨链流动性协议,它在链间转移价值靠锁定加铸造。比特币经由这条通道抵达时以syBTC形式出现,这是一个8位小数、精度与比特币自身最小单位相匹配的代币,发行在BNB Smart Chain、Ethereum和Rootstock上,并在资金池中与BTCB、cbBTC、WBTC和RBTC配对。

3.2 亿美元:毁在一个缓存键上

比特币没有智能合约,也就没有Portal合约来锁定存款。取而代之的是portal——一个扮演同样角色的指定比特币地址。一笔存款就是一笔付给它的普通比特币交易,目标链所需指令作为数据附着在这笔交易上。属于该桥的链下代码读取这些数据,从中得知是谁在存款、存了多少、合成代币要发去哪里,并负责征收portal手续费。比特币一侧发生的一切,目标链都无从得知,因此由Relayers Network把请求送到对岸。请求抵达链上入口BridgeV2,再由它转发给Synthesis。授权依托于一个MPC密钥:协议的签名者各持一份私钥分片,共同对请求产出单一签名。接收中继请求的入口只有一个修饰器把关。

3.2 亿美元:毁在一个缓存键上

receiveRequestV2Signed把调用转发给_processRequest,仅由onlySignedByMPC修饰器把关。该修饰器对请求做哈希,并要求随附签名对MPC地址有效。

3.2 亿美元:毁在一个缓存键上

这份合约确立的是:MPC密钥签了这个请求。请求里携带的数额,是在比特币一侧算出来的。

漏洞分析

缺陷不在目标链合约里,而在那个读取比特币存款、把它们转成跨链请求的链下服务中。它的源码不公开,因此以下内容依据项目方post-mortem,以及一份2024年的第三方审计报告。存款路径上的两个缺陷必须叠加才能成事,post-mortem明确指出,单独任何一个都不足以让攻击得手。

第一个缺陷在于存款人身份如何被识别。附着在存款上的指令会被解码,以还原出是谁在存款,而这个decoder把身份取自交易数据中由花费该输入的人所控制的部分。于是存款人可以把自己冒充成协议认得的任何角色,包括portal管理员,也就是设定portal能接受的最低手续费的那个角色。

第二个缺陷在于手续费如何被扣除。2024年审计报告指出decodeWrap()里那一行代码,它通过从存款输出金额中减去手续费来计算铸造数额。该审计发现types.Satoshi是一个有符号整型,而携带PortalFee的info结构体不可信且由用户控制,因此结果可以被压成负数;序列化给目标链时又按无符号数解释,足以铸出任意数量合成比特币。审计当时将这一问题记为已修复。2026年post-mortem描述了同类失效:这笔手续费在没有检查符号的情况下被减去,一笔负手续费因此放大了存款额,而不是缩小它。存款路径上似乎没有任何环节,把铸造数额与实际收到比特币数量绑定起来。

攻击分析

十二笔伪造铸造在BNB Smart Chain、Ethereum和Rootstock上于约四分钟内接连得手。以下分析基于其中一笔,即BNB Smart Chain上的交易0x9a2bc0...21b9b959。

3.2 亿美元:毁在一个缓存键上

Step 1:攻击者冒充portal管理员,把portal能接受的最低手续费压到负数,这样一笔声明了负手续费的存款就会被处理,而不是被拒绝。

Step 2:攻击者存入了330 satoshi,体现为0x000000000000014a。抵达BNB Smart Chain的请求携带的却是0x400000000000014a——同一个值,只是2^62那一位被置为1,即4,611,686,018,427,388,234个最小单位。两者之差恰好是2^62,说明从这笔存款中减去的手续费为-4,611,686,018,427,387,904 satoshi:330 - (-4,611,686,018,427,387,904) = 4,611,686,018,427,388,234。请求就以这个数额被签名和转发。

Step 3:receiveRequestV2Signed()校验了请求上的MPC签名并将其向下传递,最终到达metaMintSyntheticTokenBTC()。该函数按源链ID解析出真实代币的合成表示syBTC,并以签名中的完整数额调用synthesize()。全部铸出的代币都被转给了攻击者地址0x025122...5d3Ba2:46,116,860,184.27388234 syBTC。

Step 4:攻击者把合成代币卖进持有它的那些资金池。一次经由Uniswap v4的syBTC/WBTC资金池兑换,向池中结算了18,446,744,072,845,450,682个最小单位的syBTC,约为上面那笔交易铸出量的四倍,并取走了4.38897292 WBTC。再经后续路由,所得约为137 ether。

3.2 亿美元:毁在一个缓存键上

项目方暂停了比特币通道,数小时内约15.2 BTC的portal资金被转入储备地址。项目方给出了返还金额20%的赏金,有效期至9月13日。协议的其它通道未受影响。

结论

没有私钥被盗,也没有合约被骗着去执行它本不该执行的代码。目标链校验的那个签名是真的;它所授权的是一个链下代码已经出错两次才产出的数字:读取比特币存款的那段代码从存款人自己填写的字段里取走了存款人身份,于是存款人可以冒充掌管portal手续费的那个角色;随后它从存款额里减去这笔手续费时不检查符号,一笔负手续费因此放大了存款额,而不是缩小它。一个从不可信输入里读出来的身份,算不上身份:一个特权角色只能靠存款人无法选择的东西来确立,比如真正为被花费的输入签过名的那个地址,拿去与协议维护的一份名单做比对,而不是存款人可以随意填写的字段。一个要被减去的数值需要两端都设定边界:手续费应当被要求落在零和存款额之间,而铸造数额永远不应被允许超过源链实际收到的数量。这两道边界中的任何一道,单独就能拦下这次攻击。

结语:两起事件均非密码学被攻破,而是标识、边界与符号检查缺失。缓存键碰撞让未验证证明获得通过;链下身份与负手续费让签名授权了错误数额。对加密协议而言,校验通过不等于校验正确,缓存命中尤其不能替代重新验证。