金色財經
核心要點:
本報告收錄2起安全事件,合計損失約$320M,一起發生在比特幣側鏈Liquid Network,另一起是比特幣橋Symbiosis(部署在BNB Smart Chain、Ethereum和Rootstock)。Liquid Network這起事件占了總損失的99%以上,Symbiosis造成的估計損失為$770K。
兩起事件中,那道本該攔下偽造的校驗都跑通了、也都返回了「通過」,只是通過的理由都不對。Liquid節點把一份自己從未檢視過的範圍證明判定為true,因為另一份證明的結論恰好歸檔在了同一個緩存鍵下;Symbiosis的簽名者在一個鑄造數額上籤下了有效簽名,而這個數額,是他們自己在比特幣一側的decoder根據存款人可控字段算出來的。
一筆偽造出來的餘額值多少錢,取決於它最終怎麼變現,而不是它的面值。Liquid憑空造出的4,000枚L-BTC背後沒有任何真實比特幣,但絕大部分經由雙向錨定變成了真正的比特幣,約$320M因此流出;Symbiosis鑄出的syBTC數量超過比特幣總供應量的兩千倍,但它必須賣進的那些資金池裡只有11.26枚syBTC,流動性提供者與用戶的估計損失為9.97 BTC。
過去一周(2026/09/07 - 2026/09/13)內,以下2起安全事件被收錄,總損失約$320M。
表 1:本周收錄的安全事件概覽
* Liquid Network事件發生於9月6日,未被上周報告覆蓋,此處納入以保持完整性。
本事件被選為本周看點,是因為它那套隱蔽的緩存鍵碰撞機制,以及它造成的巨額損失。攻擊者構造出一筆交易,讓它的校驗數據把前一筆交易的字節重新切分了一遍,於是一份從未被檢查過的證明,拿到了緩存里那條現成的校驗結論。
2026/09/06,比特幣側鏈Liquid Network被攻擊,損失約$320M [1]。Liquid所運行的節點軟體Elements,其範圍證明校驗緩存發生了一次碰撞:為某個輸出記錄下的valid結論,被返還給了另一個完全不同的輸出,後者的證明因此在從未被檢視的情況下獲得通過。攻擊者藉此憑空造出4,000枚沒有任何比特幣與之對應的L-BTC,再通過網路正常的peg-out把它們提走。次日,3,400枚BTC被退還給聯邦,約598.5枚BTC留在攻擊者手中 [2]。
Liquid Network是一條構建在Elements之上的比特幣側鏈,Elements是一個從比特幣代碼庫派生出來的開源區塊鏈平台。它的存在是為了讓比特幣的轉移比主鏈更快、更便宜、更私密。比特幣在兩條網路之間通過雙向錨定往返:peg-in時,用戶把BTC鎖定給網路的聯邦(一組共同託管錨定資金的受信任機構),換得等量的L-BTC;peg-out時,用戶銷毀L-BTC,聯邦釋放出對應的BTC。Liquid的區塊不靠挖礦產生,而是由聯邦的一組輪值職能節點(functionary)提出,再由達到門限數量的聯邦簽名完成最終確認。
和比特幣一樣,Liquid把資金記作未花費的交易輸出,而不是帳戶餘額。一筆交易要指明已有的未花費輸出、將其解鎖,並創建新的輸出,且它創建的價值必須等於它消耗的價值。每個輸出都帶有一段鎖定腳本,即scriptPubKey。以OP_RETURN開頭的輸出永遠無法被花費,它的用途就是承載數據,而一次peg-out正是以這種輸出的形式表達的。
Liquid還默認隱藏金額。它寫入輸出的不是金額本身,而是對該金額的Pedersen承諾(金額承諾)。承諾具有可加性,因此節點無需知曉其中任何一個數值,就能確認一筆交易的輸入與輸出是否配平。這個性質是雙向的:承諾同樣可以藏起一個行為上為負的數值,那會讓一筆交易的輸出超過輸入卻仍然配平。所以每個隱藏了金額的輸出還要附帶一份範圍證明,用來證明被承諾的金額落在[0, 2^64)之內。驗證一份範圍證明代價不低,而同一個輸出會被驗證不止一次——交易進入內存池時一次,交易隨區塊抵達時又一次——因此Elements會把已經通過的證明緩存起來,以該證明本身、以及它當時所比對的數據為鍵。
缺陷在於Elements(每個Liquid參與者都運行的節點軟體)是如何緩存已驗證範圍證明的。
CachingRangeProofChecker::VerifyRangeProof()會為眼前這個輸出推導出一個緩存條目,一旦命中就立即返回成功,根本不去碰那份證明:
圖1:Elements範圍證明校驗代碼在緩存命中時直接返回true,此時證明本身尚未被檢視
條目由ComputeEntryRangeProof()生成,它把四個字段一個接一個寫進同一個SHA-256流,然後取出摘要:
圖2:ComputeEntryRangeProof把四次Write串接進同一個帶salt的SHA-256哈希器,字段之間既無長度前綴也無分隔符
這四個字段分別是:範圍證明本身、對金額的Pedersen承諾、標識該輸出持有何種資產的資產生成元,以及scriptPubKey,納入它是為了讓一份通過的證明綁定到某一段特定的鎖定腳本上。
四個字段都沒有帶着長度前綴或分隔符寫入:範圍證明和scriptPubKey是變長的,Pedersen承諾和資產生成元則始終是33字節的定長點。於是摘要記下的只是拼接後的那串字節,至於一個字段在哪裡結束、下一個從哪裡開始,無從體現。兩組不同的字段只要拼接出同一串字節,就會得到同一個條目,而先被校驗的那一組會留下一條結論,供另一組直接取用。哈希器確實用每個節點各自的隨機salt做了初始化,但這個salt同樣被加在兩條字節流的最前面,它改變了摘要,卻並不能把兩份輸入區分開。這個缺陷在提交94000967中被修復 [3]。
以下分析基於交易271147...187ec5和f24a4b...0a183f,兩筆都被運行着存在缺陷版本代碼的節點所接受。
Step 1:攻擊者發出一筆交易,其第一個輸出是一個承載了67字節數據的OP_RETURN。
圖3:Liquid交易輸出,顯示一段帶67字節推送的OP_RETURN腳本,並標註出scriptPubKey與金額承諾
它的scriptPubKey以6a 43開頭——OP_RETURN操作碼,後跟一次67字節的推送,然後是兩個33字節的值和結尾的一個6a。被推送的這67字節並不是這筆交易需要的數據。它們是另一個尚不存在的輸出的金額承諾和資產生成元。對這個輸出的校驗,把一條valid結論存進了由它自己那四個字段所決定的條目下。
Step 2:攻擊者隨後提交了增發交易。這筆交易的OP_RETURN輸出,其scriptPubKey只有單字節6a,而它的金額承諾,正是前一筆交易那次推送里嵌着的那個33字節的值。它的範圍證明字段,則由前一筆交易的範圍證明、承諾和資產生成元拼成,末尾再接上6a 43兩個字節。
圖4:增發交易的輸出,第一個被標註為發往攻擊者地址的4,000枚有效L-BTC,第二個是腳本僅有單字節的OP_RETURN
於是這個輸出的四個字段,拼接出的字節流與前一筆交易完全相同,只是切分的位置換了地方:
Primer: P0 | C0 | X | S0, where S0 = 6a 43 6a
Inflation: P1 | C1 | X | S1, where P1 = P0 || C0 || X || 6a 43 and S1 = 6a
Both concatenate to: P0 || C0 || X || 6a 43 || C1 || X || 6a
緩存返還了此前那條結論,P1從未被驗證過,它根本不是一份範圍證明。C1承諾的是一個數額很小的負值。這筆交易的另一個輸出是一個發往攻擊者地址ex1q7kg...qa2w的普通P2WPKH輸出,帶着一個數額很大的正向承諾和一份貨真價實的範圍證明,兩者正好相互抵消,交易因此配平。它留下了4,000枚沒有任何比特幣與之對應的未花費L-BTC。
Step 3: 攻擊者執行peg-out。兩筆交易通過普通的OP_RETURNpeg-out輸出銷毀了3,996.01834922和2.65138358枚L-BTC,合計3,998.66973280枚。負責授權peg-out的職能節點用的還是那份存在缺陷的代碼,校驗通過後釋放了比特幣:3,995.99999857枚BTC和2.49749857枚BTC抵達攻擊者在比特幣網路上的地址,共計3,998.49749714枚。
網路暫停並分裂成兩條鏈,事件在這期間得到處置 [4],恢復運行時拒絕了那筆無效的peg-out [2]:如今區塊瀏覽器上primer交易顯示為已確認,而inflation交易沒有,儘管它放出的那些比特幣早已從聯邦的錢包里流出。次日16:09:25 UTC,攻擊者把3,400.00000000枚BTC退還到聯邦的錨定錢包,剩餘約598.5枚BTC仍未歸還 [2],雙方通過嵌入比特幣交易的消息繼續磋商 [4]。
攻擊者沒有攻破任何密碼學,也沒有拿到任何私鑰。缺陷在於範圍證明緩存是如何標識「這個我已經查過了」的:它的鍵把四個字段——其中兩個是變長的——拼進同一條哈希流,而沒有任何東西標明每個字段到哪裡為止,於是另一組字段可以把同樣的字節重新切分一遍,落到同一個條目上。一條存好的valid結論就這樣被返還給了一份任何節點都不曾驗證過的證明;而一旦範圍證明不再約束它背後的金額,一條隱藏金額的賬本也就失去了唯一能保證這些數值非負的東西。一個精心構造的輸出變成了4,000枚L-BTC,雙向錨定又把它們變成了主鏈上真正的比特幣。
任何由一個以上變長輸入推導出來的標識符,都必須把輸入之間的邊界一併納入:要麼給每個字段加上長度前綴,要麼把各字段哈希進彼此分開的定長槽位。做不到這一點,推導就不是單射的,兩份不同的輸入便可能被當成同一份。這條要求在以緩存替代校驗的場合尤其關鍵,因為在那裡,一次命中就是一個「不再執行校驗」的決定。
2026/09/11,跨鏈橋Symbiosis的比特幣通道在BNB Smart Chain、Ethereum和Rootstock三條部署上被攻擊,流動性提供者與用戶的估計損失為9.97 BTC(約$770K,按9月11日約$77K的價格計)[5]。負責決定要為一筆比特幣存款鑄造多少合成比特幣的那段鏈下代碼,從交易中存款人可以控制的一部分數據里讀取存款人身份,這讓攻擊者得以冒充管理員,把最低手續費壓到負數;隨後它從存款額里減去這筆手續費時不檢查符號,於是減法變成了加法。
Symbiosis是一個跨鏈流動性協議,它在鏈間轉移價值靠的是鎖定加鑄造,而不是把資產本身搬過去:在有智能合約的鏈上,用戶的代幣由源鏈上的Portal合約鎖定,宿主鏈上的Synthesis合約隨即鑄造出等量的合成代幣,即sToken;銷毀sToken則釋放出原始資產 [6]。比特幣經由這條通道抵達時以syBTC的形式出現,這是一個8位小數、精度與比特幣自身最小單位相匹配的代幣,發行在BNB Smart Chain、Ethereum和Rootstock上,並在資金池中與BTCB、cbBTC、WBTC和RBTC配對 [5]。
圖5:Symbiosis官方文檔中的鑄造流程與銷毀流程示意圖
比特幣沒有這些:沒有智能合約,也就沒有Portal合約來鎖定存款。取而代之的是portal——一個扮演同樣角色的指定比特幣地址。一筆存款就是一筆付給它的普通比特幣交易,目標鏈所需的指令作為數據附着在這筆交易上。屬於該橋的鏈下代碼讀取這些數據,從中得知是誰在存款、存了多少、合成代幣要發去哪裡,並負責徵收portal的手續費——這筆手續費在鑄造數額被確定之前先從存款額里扣除——其最低額度是一個由管理員設定的參數。
比特幣一側發生的一切,目標鏈都無從得知,因此由Relayers Network把由此產生的請求送到對岸。請求以編碼調用的形式抵達該橋的鏈上入口BridgeV2,再由它轉發給Synthesis。授權依託於一個MPC密鑰:協議的簽名者各持一份私鑰分片,共同對請求產出單一簽名。接收中繼請求的入口只有一個修飾器把關,在分發調用之前不做任何別的事:
圖6:receiveRequestV2Signed把調用轉發給_processRequest,僅由標紅的onlySignedByMPC修飾器把關
該修飾器對請求做哈希,並要求隨附的簽名對MPC地址有效:
圖7:onlySignedByMPC修飾器要求SignatureChecker.isValidSignatureNow針對請求哈希與MPC地址校驗通過
這份合約確立的是:MPC密鑰簽了這個請求。請求里攜帶的數額,是在比特幣一側算出來的。
缺陷不在目標鏈的合約里,而在那個讀取比特幣存款、把它們轉成跨鏈請求的鏈下服務中。它的源碼不公開,因此以下內容依據的是項目方的post-mortem [5],以及一份2024年的第三方審計報告,其中有一段代碼片段看起來與此相關 [7]。
存款路徑上的兩個缺陷必須疊加才能成事,post-mortem明確指出,單獨任何一個都不足以讓攻擊得手。
第一個缺陷在於存款人身份是如何被識別的。附着在存款上的指令會被解碼,以還原出是誰在存款,而這個decoder把身份取自交易數據中由花費該輸入的人所控制的那一部分。於是存款人可以把自己冒充成協議認得的任何角色,包括portal的管理員,也就是設定portal能接受的最低手續費的那個角色。
第二個缺陷在於這筆手續費是如何被扣除的。2024年的審計報告指出了decodeWrap()里那一行代碼,它通過從存款輸出的金額中減去手續費來計算鑄造數額:
Value: types.Satoshi(tx.TxOut[idx].Value) - info.PortalFee,
該審計的發現是:types.Satoshi是一個有符號整型,而攜帶PortalFee的info結構體不可信且由用戶控制,因此結果可以被壓成負數;序列化給目標鏈時又按無符號數解釋,足以鑄出任意數量的合成比特幣。審計當時將這一問題記為已修復 [7]。2026年的post-mortem描述了同類失效:這筆手續費在沒有檢查符號的情況下被減去 [5],一筆負的手續費因此放大了存款額,而不是縮小它。存款路徑上似乎沒有任何環節,把鑄造數額與實際收到的比特幣數量綁定起來。
十二筆偽造鑄造在BNB Smart Chain、Ethereum和Rootstock上於約四分鐘內接連得手 [5]。以下分析基於其中一筆,即BNB Smart Chain上的交易0x9a2bc0...21b9b959。
圖8:偽造鑄造的調用鏈,標紅的數額字段從簽過名的請求原封不動地一路傳到synthesize,直至最後的syBTC轉賬
Step 1:攻擊者冒充portal的管理員,把portal能接受的最低手續費壓到負數,這樣一筆聲明了負手續費的存款就會被處理,而不是被拒絕 [5]。
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。
圖9:Uniswap v4調用鏈,把syBTC結算進池中,並取走4.38897292 WBTC至攻擊者地址
再經後續路由,所得約為137 ether。
項目方暫停了比特幣通道,數小時內約15.2 BTC的portal資金被轉入儲備地址 [5]。項目方給出了返還金額20%的賞金,有效期至9月13日 [8]。協議的其它通道未受影響。
沒有私鑰被盜,也沒有合約被騙着去執行它本不該執行的代碼。目標鏈校驗的那個簽名是真的;它所授權的,是一個鏈下代碼已經出錯兩次才產出的數字:讀取比特幣存款的那段代碼從存款人自己填寫的字段里取走了存款人身份,於是存款人可以冒充掌管portal手續費的那個角色;隨後它從存款額里減去這筆手續費時不檢查符號,一筆負的手續費因此放大了存款額,而不是縮小它。
一個從不可信輸入里讀出來的身份,算不上身份:一個特權角色只能靠存款人無法選擇的東西來確立——比如真正為被花費的輸入簽過名的那個地址,拿去與協議維護的一份名單做比對,而不是存款人可以隨意填寫的字段。一個要被減去的數值需要兩端都設定邊界:手續費應當被要求落在零和存款額之間,而鑄造數額永遠不應被允許超過源鏈實際收到的數量。這兩道邊界中的任何一道,單獨就能攔下這次攻擊。
來源:金色財經
發佈者對本文章的內容承擔全部責任
在投資加密貨幣前,請務必深入研究,理解相關風險,並謹慎評估自己的風險承受能力。不要因為短期高回報的誘惑而忽視潛在的重大損失。
暢行幣圈交易全攻略,專家駐群實戰交流
▌立即加入鉅亨買幣實戰交流 LINE 社群(點此入群)
不管是新手發問,還是老手交流,只要你想參與加密貨幣現貨交易、合約跟單、合約網格、量化交易、理財產品的投資,都歡迎入群討論學習!
上一篇
下一篇