作者:騰訊湛瀘實驗室
來源:微博@騰訊湛瀘實驗室
今年9月18號,比特幣主流客戶端Bitcoin Core發表文章對其代碼中存在的嚴重安全漏洞CVE-2018-17114進行了全面披露。該漏洞由匿名人士于9月17日提交,可導致特定版本的Bitcoin Core面臨拒絕服務攻擊(DoS,威脅版本: 0.14.x - 0.16.2)乃至雙花攻擊(Double Spend,威脅版本: 0.15.x - 0.16.2)。
Bitcoin Core項目組對于該漏洞進行了及時的修補,在向其他分支項目組(如Bitcoin ABC)進行了漏洞通告并提醒用戶進行版本升級后,公布了上段所提到的漏洞披露文章。該文章中對漏洞的成因、危害、影響版本及修復過程時間線進行了簡單介紹,但未對漏洞進行詳盡分析。
本文基于該漏洞披露文章及Bitcoin Core項目組在Github上的漏洞修復和測試代碼,著重分析該漏洞的修復方法、觸發方法、漏洞成因及其所帶來的危害。文中涉及測試腳本及PDF版本可于https://github.com/hikame/CVE-2018-17144_POC下載。
1. 漏洞修復
在Bitcoin Core的master代碼分支上,commit b8f8019對這一漏洞進行了修復,如圖 1所示。
這段代碼位于src/validation.cpp中的CheckBlock()函數,該函數在節點接收到新的區塊時被調用。第3125行調用的CheckTransaction()函數及其第三個參數的意義可以參照其代碼實現進行分析。

CheckTransaction()函數對于傳入的交易消息(CTransaction& tx)進行檢測,其中包括了檢測一筆交易是否發生雙花。檢測方案非常簡單,將這比交易中使用的所有Coin(即代碼中的txin.prevout,代表比特幣交易中的UTXO,本文后續均采用Coin一詞進行表述,以便與代碼持一致)記入std::set中,如果發現某項記錄被重復記錄了兩次,就會返回處理失敗的信息(state.DoS),這一消息最終會通過P2P通道,反饋給該區塊的發送者。基于代碼段中的備注部分,可以看出,這段檢測代碼在被CheckBlock()函數的調用過程中被認為是冗余和費時的,并通過將函數的第三個參數設置為False的方式,使其跳過。
CheckBlock()執行選擇跳過雙花檢查,是由于其后續會對于整個區塊中的交易進行更為復雜而全面的檢查。然而,這些檢查代碼未能像預期的那樣對某些異常情況進行檢測和處置,導致了漏洞的存在。
2. 漏洞PoC
Bitcoin Core的Github上提供了實現DoS攻擊的測試腳本;但要想進行雙花攻擊的測試,需要自己編寫攻擊腳本。
2.1. DoS攻擊PoC
Bitcoin的master代碼分支上,commit b8f8019(即前文提到的漏洞修復commit)的子commit 9b4a36e給出了該漏洞的驗證代碼,如圖 2所示。
這段使用Python編寫的測試代碼,位于test/functional/p2p_invalid_block.py測試腳本中。該腳本構建了一個測試網絡,測試代碼可以通過RPC接口、P2P接口等方式連接到目標節點,并發送測試數據,如惡意構造的區塊數據、交易信息等。圖 2中新添加的測試代碼的功能是:在block2_orig區塊中找到了第二項交易(vtx[2]),并將其交易輸入中的第一個Coin(vtx[2].vin[0])重復加入到了輸入序列中,從而構造一個使用vtx[2].vin[0]進行雙花的交易消息。如92行所示,向已被修復漏洞的node端發送block2_orig區塊時,會收到node反饋的拒絕接收消息,其消息內容即為“bad-txns-duplicate”。
如果利用該測試的代碼針對未修復漏洞的節點進行測試,則產生的效果如圖 3所示。由于測試腳本惡意構造的區塊數據引發了目標節點的崩潰,導致了Python腳本與node進程之間的P2P連接斷開,使其拋出了ConnectionResetError。
2.2. 雙花攻擊PoC
官方的PoC給出了DoS攻擊的示意。然而,這段PoC在僅有一個node的測試網絡中運行,并且所有交易數據的解鎖腳本均被設定為“任何人均可花費”。由于其特殊性,對于驗證雙花攻擊欠缺一定的說服力。因此,本文基于Bitcoin Core的測試框架,自行編寫了一套漏洞驗證腳本。
測試過程中的三個角色如圖 4所示。N0代表攻擊者,利用Python程序所編寫的惡意P2P服務,構造惡意區塊數據;N1代表諸多正常節點中的一個,是N0的鄰居節點,兩者通過P2P接口進行消息傳遞。測試腳本關鍵代碼如下。

3. 漏洞細節分析
本文從直接導致DoS的PoC開始進行調試,這可以幫助我們快速定位問題代碼的位置。利用GDB進行調試,發現發生崩潰時的代碼調用棧如下(線程名:msghand)。

崩潰現場代碼如圖 5,根據函數及變量名稱可以大致猜想,在進行Coin的更新過程中,會首先檢查每筆交易的是否已被花費,如是,則assert失敗,導致DoS(Bitcoin Core官方發布的客戶端程序開啟assert)。那么為什么又會存在雙花攻擊的效果呢?這里需要對inputs.SpendCoin()的實現做進一步的分析。
3.1. CCoinsViewCache::SpendCoin()分析
圖 5中,inputs變量的類型為CCoinsViewCache類,每個該類的對象均與一個區塊對應,并且在其名為base的域中存儲了指向其前驅區塊的CCoinsViewCache對象的指針。該類中另一個關鍵的內部變量為cacheCoins,存儲了當前區塊的處理過程中新產生的或從前驅區塊中查詢到的Coin信息,它是一個std::map結構,key值為Coin對象的索引信息(所屬交易的Hash、UTXO在該交易輸出序列中的序號),value值則為Coin的具體信息(貨幣數額、解鎖腳本等)。
CCoinsViewCache::SpendCoin()函數實現如圖 6所示。該函數作用為檢查outpoint所代表的某個交易的輸出是否被花費過。下面將對于這三點展開詳細分析。
圖 6 CCoinsViewCache::SpendCoin()代碼
3.1.1. CCoinsViewCache::FetchCoin()功能與實現
該函數用于查詢outpoint對應的交易的具體信息。圖 7中是該函數的實現代碼:
嘗試從當前CCoinsViewCache對象的cacheCoins中查詢Coin信息,如存在則返回(41-43行);
-
嘗試從當前CCoinsViewCache對象的cacheCoins中查詢Coin信息,如存在則返回(41-43行);
-
如1) 中未能找到,則從base所代表的前驅區塊中進行交易信息的查詢,查詢方式是調用GetCoin()函數,該函數會進一步調用FetchCoin()函數,也就是在base->cacheCoins中查找Coin信息,當Coin信息被順利查到,且其未被花費時,返回True(45-46行);
-
如2)從前驅區塊中順利找到Coin信息,則將其加入當前區塊的cacheCoins中,以備后續使用(47-52行)。
3.1.2. cacheCoins的內容維護
對于一個區塊所維護的cacheCoins,向其添加新的Coins的可能途徑有兩種:
-
第一種即圖 8第47行所顯示的,CCoinsViewCache::FetchCoin()執行過程中,從其前驅區塊中查詢到了相應Coin信息;
-
第二種發生在區塊的交易信息中產生了新的Coin時,其對應的函數為AddCoin(),源碼如圖 8所示,對于一個普通的Coin(非產生于Coinbase交易),會將其記錄到cacheCoins中,并于83行設置相應Coin Flag標志。
3.1.3. Coin Flag的意義與取值
CCoinsViewCache類SpendCoin()、FetchCoin()、AddCoin()函數中均有關于Coin的Flag操作。Coin Flag存在兩個狀態標志位Fresh和Dirty,Bitcoin Core中對于這兩個狀態標志為的定義及注釋如圖 9,可以看出:
-
Dirty標志位表示當前緩存的Coin信息與base所指向CCoinsViewCache對象所記錄的Coin信息不同;
-
Fresh標志位表示這個Coin的信息在base所指向的CCoinsViewCache對象中沒有記錄。
基于其描述,AddCoin()的代碼中(圖 8中76-83行),對于一個區塊中的普通交易所產生的新的Coin,其Fresh標志置1;FetchCoin()的代碼中,對于來自前驅區塊的Coin,其Flag在當前CCoinsViewCache對象中進行緩存時的flag被置0,即既非Fresh也非Dirty的初始狀態(圖 7中第47行)。
3.2. 漏洞觸發原理分析
在3.1中完成了對于相關代碼的細節分析后,我們可以對于代碼發生異常時的執行狀態開展進一步的分析了。
3.2.1. DoS攻擊原理分析
攻擊過程關鍵代碼示意如下,攻擊代碼第4行將block2.vtx[2].vin[0]重復加入了block2.vtx[2].vin中,是實現雙花的關鍵操作。block2.vtx[2]實際上是tx2,其構建代碼如第2行所示:可以看出tx2以tx1的輸出中序列號為0的Coin作為輸入。而tx1、tx2在第三行被加入同一區塊block2中。

被攻擊節點在接收到block2后的處理過程如下:
-
交易tx1處理。經一系列驗證分析后,該交易被認為是一筆有效交易,為了記錄其輸出,將調用圖 8中的AddCoins()函數,該函數會在當前CCoinsViewCache對象的cacheCoins中添加一個新的Coin,并將其Flag設置為Fresh | Dirty;
-
交易輸入tx2.vin[0]處理。圖 7 CCoinsViewCache::FetchCoin()代碼被調用以查找對應Coin信息,1) 中的操作已將Coin信息加入當前CCoinsViewCache對象的cacheCoins。因此第43行將直接返回;而圖 6 CCoinsViewCache::SpendCoin()代碼會因為該Coin有Fresh標簽,執行到第106行,并將其從cacheCoins中刪除;
-
交易輸入tx2.vin[1]處理。圖 7 CCoinsViewCache::FetchCoin()代碼將再次被調用,但是,由于2)中已將相應Coin信息刪除,而base->GetCoin()又無法查知該Coin,將導致46行代碼返回cacheCoins.end(),進而使SpendCoin()返回False,最終觸發assert失敗。
3.2.2. 雙花攻擊原理分析
攻擊過程關鍵代碼如下。第1行中,block1的挖礦獎勵的接收者被設定為node0的地址。第二行構建的交易消息tx2即以該交易輸出的Coin為輸入,并且重復使用了兩次,而且tx2輸出的Coin數量是挖礦獎勵的兩倍,是典型的雙花行為。

被攻擊節點在接收到block2的數據后的處理過程為:
-
處理第一個交易輸入block1.vtx[0]。由于該交易位于前驅節點,需要調用base->GetCoin()以獲取相應Coin信息,該信息的flag被默認置0,在圖 6 CCoinsViewCache::SpendCoin()代碼的執行過程中,將執行108-109行代碼,置Dirty位,并將其余額清除,以標記已被花費;
-
處理第二個交易輸入block1.vtx[0]。由于1)中已經添加了相應的Coin信息,在圖 7 CCoinsViewCache::FetchCoin()代碼中的43行可以直接返回該信息,但是在SpendCoin()及后續代碼中的執行過程中,沒有對該Coin是否已被花費進行有效驗證,導致雙花行為沒能檢測出來。
4. 危害分析
基于官方的漏洞通告,Bitcoin Core的0.14.X-0.16.2版本均面臨DoS攻擊的威脅,而且其中的0.15.X-0.16.2版本還面臨雙花攻擊的威脅。本文基于Bitnodes網站的數據對相應版本的節點數目做了如下表統計(總數為9970個節點,數據統計于2018-11-09)。
需要注意的事,要想利用此漏洞實現攻擊,其限定條件為:
-
異常交易數據必須打包到區塊中才能觸發漏洞。如果攻擊者試圖利用P2P接口向受害者節點直接發送異常交易數據,會觸發CheckTransaction()函數中的雙花檢查,無法觸發漏洞。
-
攻擊者必須自行挖掘出一個最新的比特幣區塊。包含惡意交易信息的最新區塊必須是有效的,否則,無法通過在交易處理之前的區塊頭檢查。
基于上述分析,在攻擊者擁有較大算力以進行區塊挖掘的前提下,兩種攻擊手段所能帶來的危害有:
-
DoS攻擊,大約可危害37%的主網節點;
-
雙花攻擊,需要超全網51%的算力認同惡意構造的區塊,并進行后續區塊的挖掘。基于表 1統計可知面臨此類攻擊威脅的節點數約占32%,但由于無法統計這些節點的算力占比,所以無法確認雙花攻擊的危害程度。
5. 總結
本文分析的CVE-2018-17144是近年來較為少見的、存在于比特幣主流客戶端中的安全漏洞。此漏洞所帶來的啟示有:一方面,Bitcoin Core項目組的漏洞修復和處置方案有效遏制了此次漏洞帶來的安全威脅,值得其他區塊鏈項目組借鑒;另一方面,區塊鏈節點客戶端的安全是整個區塊鏈系統安全的基石,對其開展更加深入和全面的研究是十分有必要的。
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/742/
暫無評論