上周,bitcoin core 0.16.3 版本客戶端的突然發布,以及開發者敦促大家盡快升級一事,令比特幣世界的人們感到了驚訝。表面上的原因,在于0.14-0.16.2版本客戶端中存在一個拒絕服務 (DoS) 向量需要被修補。到后來,我們才發現,在0.15-0.16.2版本core客戶端中的另一個漏洞,可能會引起比特幣的超發問題。
在這篇文章中,作者試圖說明:到底發生了什么?潛在的危險是什么?以及如果有人利用這個漏洞,還將會發生什么?
雙重支付的兩種方式
在我們接觸實際的漏洞之前,我們需要解釋一些東西。我們首先需要定義一下雙重支付,因為這個漏洞就可以用于雙重支付。
所謂雙重支付的情況,就比如說愛麗絲(Alice)向鮑勃(Bob)支付了一筆幣,然后她又把相同的幣再一次支付給了查利(Charlie),愛麗絲基本上試圖進行兩次支付,其中的一筆她知道會被拒回。當然,當我們考慮支付時,愛麗絲的某些賬戶通過寫這兩次支付被透支了。這很接近比特幣的工作原理,但并不是十分準確。
比特幣并不是基于帳戶模型的,而是基于未花費交易輸出(UTXO)。一筆交易的輸出基本包含了一個地址以及數量。一旦輸出被使用了,它就無法再次被花費。試想一下一個UTXO(作為一筆發送給你的幣),它可以是任意數量的,比如說0.413 BTC。
比特幣的雙重支付意味著一筆幣(UTXO)被花費了兩次。通常,這意味著愛麗絲將她的0.413 BTC發送給了鮑勃,然后她又把同一筆比特幣又發送給了查利。
比特幣的解決方法是,其中一筆交易會納入一個區塊,由此來決定實際誰得到了報酬。如果兩筆交易不知何故都傳遞到了多個區塊,那么后面發生的區塊,就會被軟件給拒絕掉。如果兩筆交易都在同一個區塊當中,那么這個區塊也會遭到軟件的拒絕。
基本上,比特幣軟件會檢測到雙重支付行為,如果有雙重支付行為的發生,則應該拒絕掉相應的區塊。
然而,在兩筆不同的交易中發送同一個UTXO,并不是唯一的雙花方法。實際還存在著同一UTXO在同一交易進行雙重支付的病態情況。在這種情況下,愛麗絲向鮑勃發送同一筆幣兩次。所以,愛麗絲實際支付的是0.413 BTC,但鮑勃收到的卻是0.826 BTC。這顯然不是一個有效的交易,因為只有一筆價值0.413 BTC的UTXO 是被發送的。這就相當于,愛麗絲用同一10美元向鮑勃發送了兩次,而鮑勃收到的則是20美元。
定義漏洞
因此,總結一下我們所定義的兩種類型的雙重支付嘗試:
使用兩筆或更多的交易,來花費相同的UTXO;
使用一筆交易花費同一UTXO多次;
結果表明,Bitcoin Core 軟件正確地處理了第一個問題,而第二個問題,正是我們要關心的。任何人都可以像這樣構造出一筆雙花交易,但要讓節點接受這種交易,又是另一回事了。
目前有兩種方法可以讓交易被納入一個區塊當中: A. 支付足夠的費用,將交易廣播到網絡上,那么礦工會負責把交易納入區塊當中;
B. 作為一名礦工,把交易納入一個區塊;
(A) 除了創建交易,并將其廣播到網絡上的節點之外,你不需要做太多的工作。 (B) 需要你找到足夠的工作量證明。這也是這次漏洞的關鍵。
(A) 不是一個可能的攻擊向量,因為這些交易會立即被標記為無效的,網絡上的節點會拒絕它們。沒有礦工們的合作,這種交易就無法進入礦工們的記憶庫,因為它們不會得到傳播。
(B)是漏洞顯現的唯一情況。換句話說,想要利用這個漏洞,你就需要工作量證明,或者說足夠的礦機設備和電力。
為了明確起見,雙花交易有4種情況需要處理:
1A?— 多筆 mempool交易花費了同一UTXO ;
1B?— 多筆區塊交易花費了同一UTXO ;
2A?— 單筆mempool交易花費了同一UTXO多次;
2B?— 單筆區塊交易花費了同一UTXO多次;
該漏洞有兩種表現形式。在0.14.x版本客戶端中,存在著一個拒絕服務(DoS)的漏洞,而在0.15.x - 0.16.2版本的客戶端,則存在一個超發漏洞。接下來,我們會分別分析它們。
拒絕服務攻擊
故事始于2009年的Bitcoin 0.1版本客戶端,這一版本的代碼通過拒絕案例1B和案例2B(檢查區塊沒有雙重支付)來強制達成共識。
你可以看到“檢查沖突”的注釋,其代碼負責檢查每個輸入沒有被花費。“將輸出標記為已使用”注釋下面的代碼,標記了UTXO的使用。如果任何UTXO的花費超過一次,則會導致錯誤。
在2011年,PR 443被合并到了比特幣代碼庫。這一改變是為了處理通過mempool (上面的情況2A)傳輸單筆交易雙重支付的情況。這個合并請求注釋的目的非常明確:
“而且,沒有具有重復輸入的交易會被納入區塊當中。..。..幾個星期前,有人嘗試過了,但這些交易并沒有被納入區塊。我假設某個地方存在了一個檢查關,它會阻止這些重復交易進入區塊,雖然我沒有對這個問題進行任何挖掘。這實際上是為了防止這種明顯無效的交易得到中繼?!?/p>
實際的代碼更改,或多或少與上面的ConnectInputs 中的“檢查沖突”注釋下的代碼執行了相同的操作,但位于的是不同的位置。代碼更改是在CheckTransaction 中運行的,其負責了所有上述的4種情況(1A, 1B, 2A, 2B)。因此,我們在區塊雙重支付共識代碼中有了一些冗余,正如案例1B和2B都被檢查了兩次,其中一次檢查是在CheckTransaction,另一次則發生在ConnectInputs。
到了2013年,PR 2224被納入了比特幣軟件。這一改變的目的是區分共識錯誤(例如雙重支付)和系統錯誤(例如磁盤空間耗盡)之間的差別,正如PR注釋中所表明的那樣:
“它引入了CValidationState,它會存儲關于區塊的元數據,或者正在執行的交易驗證數據。它被用于區分驗證錯誤(例如,未能滿足網絡規則)和運行時錯誤(比如磁盤空間的不足),從前這些可能會產生混淆,因磁盤空間用完會導致區塊被標記為無效。此外,CValidationState還承擔了跟蹤 DoS級別的角色(因此它不需要存儲于交易或區塊當中。..)”
實際的相關代碼更改如下:
在那個時候,ConnectInputs已經被模塊化成多個方法,并且這個函數成為了檢查雙重支付的函數。這里的關鍵改變是,曾經的error被改為了 assert
assert在C++中是做什么的?它會完全中止程序。程序員為什么要在這里停止程序?這就是Pull請求的目的所在。下面就是那個時候的代碼片段:
它會像以前一樣處理案例1B和2B。函數名則從ConnectInputs更改為ConnectBlock,但檢查案例1B和2B的冗余性仍然在PR 443中保留。正如我們已經看到的,UpdateCoins做了第二次雙重支付檢查。其中CheckBlock通過調用CheckTransaction進行了第一次雙重支付檢查:
由于這是第二次檢查相同的內容,所以要讓UpdateCoins的雙花檢查失敗的唯一方法,就是存在某種UTXO數據庫或交易存儲損壞。事實上,這似乎是改成assert的原因。因為CheckBlock通過CheckTransaction在UpdateCoins之前已經進行了檢查,我們已知道某筆交易并不是雙花交易。因此,PR 2224正確地推測到,UpdateCoins中的這個狀態必然是一個系統錯誤,而不是一個共識錯誤。在這種情況下,為了防止進一步的數據損壞,正確的做法就是停止程序。
到了2017年,PR 9049作為Bitcoin 0.14的一部分被引入比特幣網絡。隨著隔離見證(Segwit)的納入,它是加快區塊驗證時間的諸多更改的其中之一,其代碼更改實際是非常少的:
你可以看到布爾函數 fCheckDuplicateInputs被添加了進去,用于加快區塊檢查。我們將在下面看到,這是一個被認為是冗余的檢查。不幸的是, UpdateCoins中的代碼在PR 2224中被更改為系統損壞檢查,而不是共識檢查。到了0.14.0版本客戶端,其代碼進行了更多的模塊化更改,而assert也發生了一些改變:
曾經是作為一個冗余檢查,現在卻成了負責區塊單筆交易雙重支付檢查(案例2B),并負責停止程序。從技術上來說,它仍然是強制執行共識規則。只是在中止程序問題上,它表現地非常糟糕。
PR 9049是如何獲得通過的? Greg Maxwell 給了我IRC上的聊天記錄。
長話短說,開發者們在討論PR 9049時,傾向于認為區塊級單筆交易雙重支付(案例2B)會在PR 443處遭到檢查,而沒有考慮PR 2224。這使得開發者們并沒有密切關注PR 9049;
總而言之:
1、在2011年引入用于防止雙重支付交易中繼(案例2A)的 PR 443,實際產生了一個副作用,即對區塊的雙重支付共識規則檢查創造了冗余校驗(案例1B和 2B)。
2、PR 2224是在2013年引入的,作為一種副作用,將(1)中用于區塊驗證的代碼,從冗余升級到了共識層;
3、PR 9049是在2017年被引入的,并且它跳過了(1)中用于單個區塊單筆交易雙重支付(案例1B)檢查的代碼。開發人員錯誤地認為代碼是多余的,因為他們沒有考慮到(2)。事實上,這種改變跳過了共識的關鍵部分。
公平地講,這些事的匯合導致了這次漏洞。
DoS漏洞的嚴重性
這意味著 0.14.x 版本的Core軟件可能會因為一個奇怪的區塊而崩潰。而要讓軟件崩潰,攻擊者需要做的事是:
1.創建一筆花費兩次同一UTXO的交易;
2.通過足夠的工作量證明,將(1)中的交易納入一個比特幣區塊;
3.將這個區塊廣播到0.14.x版本軟件的節點;
(1) 和 (3) 的成本并不高,而步驟(2)的最小成本為12.5 BTC。
如果你認為從博弈論的角度來看,分裂網絡并不是那么好,那么利用這個漏洞的動機就相當低了。充其量,作為攻擊者,你花費了12.5 BTC將部分全節點給搞崩潰。由于不可能從分裂網絡中獲利,攻擊者無法輕易地補償自己的攻擊成本。
如果這是唯一的漏洞,那么攻擊者可能給很多人帶來一些不便,但這不會是持續的,因為這些被攻擊的節點可以簡單地重啟,并連接到其它誠實節點。一旦有一個較長的鏈,那么惡意區塊攻擊就會完全失去它的威脅。除非攻擊者以每區塊12.5 BTC的代價繼續創建區塊,并將其傳播給 0.14.x版本軟件的節點,否則攻擊就是不可持續的。
換句話說,雖然這個漏洞的確存在著,但對 DoS攻擊的經濟刺激卻是相當低的。
超發漏洞
從0.15.0版本軟件開始,core軟件引入了一個新的特性,以便更快地查找和存儲UTXO,而這恰恰又引入了另一個漏洞。當一筆具有雙重支付單個交易的區塊納入區塊鏈時,軟件會將其視為有效,而不會出現崩潰的現象。
這就意味著一筆病理性交易(相同UTXO在同一交易中被使用多次,即案例2B),0.14版本的節點會因此而崩潰,而使用0.15版本軟件的節點卻會認為交易是有效的,這基本上是憑空在創建比特幣。
談談它是如何發生的。在0.15中出現的PR 10195 ,引入了很多內容,但它的主要要點在于改變了 UTXO的存儲方式,使得它們更有效地進行查找。因此,它出現了很多變化,包括對早期UpdateCoins函數的更改:
注意,assert(false) 周圍的代碼是如何被完全取出的。注意這一點,0.15.0中的PR 10537也更改了代碼。
assert失敗的條件現在取決于inputs.SpendCoin,它看起來是這樣子的:
本質上,SpendCoin返回“ false”值的唯一方法,就是讓幣不存在于UTXO集中。但正如你所看到的,這需要幣是FRESH的,而不是DIRTY的。這些不是常見的術語,但值得慶幸的是,core開發者Andrew Chow給出了解釋:
“現在的問題是,什么時候UTXO會被標記為FRESH?當它們被添加到UTXO數據庫時,它們就會被標記為FRESH。但是,UTXO數據庫仍然只存在于存儲當中的(作為緩存)。當它被保存到磁盤時,存儲中的條目將不再被標記為FRESH……”
標記為FRESH的幣,是進入交易存儲池(memory pool)中的幣。而攻擊者可以通過 UpdateCoins函數中的assert語句來破壞節點。更糟的是,如果幣是屬于DIRTY的(基本上從磁盤上讀取的),那么這就會導致比特幣的超發。
因此,攻擊者可以欺騙那些運行 0.15.0- 0.16.2版本軟件的礦工接受一個奇怪的、無效的區塊,從而導致比特幣的供應超發。
超發漏洞的嚴重性
這種攻擊的經濟誘因似乎明顯高于DoS攻擊,因為攻擊者可能會憑空制造出比特幣。但你仍然需要有挖礦設備來執行攻擊,但考慮到潛在的經濟誘因,這可能是值得的,或者看起來是這樣的。
下面是使用這種漏洞的一種簡單攻擊方式:
1.創造一筆帶有雙重支付交易的區塊,其會向自己支付兩次,比方說 50 BTC →100 BTC;
2.將該區塊廣播給0.15/0.16版本客戶端的所有礦工;
下面是會發生的一些事:
1.0.14.x 版本節點會崩潰;
2.較舊版本的節點及其它替代客戶端會拒絕這個區塊;
3.很多區塊鏈瀏覽器是運行在自定義軟件上的,而不是基于core,因此,至少有一些瀏覽器會拒絕該區塊,并且不會顯示來自該區塊的任何交易。
4.取決于礦工們運行的軟件,我們可能會迎來鏈分裂;
有可能,所有的礦工都是運行的Bitcoin Core 0.15+版本軟件,在這種情況下,不受攻擊的客戶端可能會停滯不前。也有可能礦工會運行其它東西,在這種情況下,當他們發現一個區塊時,鏈就會發生分叉。
由于這些違規行為,網絡上的人們很快就會追蹤到這一點,可能已提醒一些開發人員,并且core開發者已經修復了它。如果存在分叉,那么在那個時候,關于哪條鏈是正確的共識鏈,將開始得到討論,而出現意外超發的鏈,可能會遭到拋棄。如果真的發生了,那么社區可能會自愿進行一次回滾,以懲罰攻擊者。
所以對于攻擊者來說,這不會帶來50 BTC的收入,更可能的是失去12.5 BTC。如果攻擊者加倍花費,比如說200 BTC,那么超發漏洞將持續存在的可能性會更小,因為攻擊會更明顯。
因此,從攻擊者的角度來看,這并不是一種好的獲利方式。
攻擊者可以獲利的另一種方式,就是事先做空比特幣,然后再執行攻擊。這也是具有風險的,因為攻擊不能保證比特幣價格會下跌,特別是當危機得到迅速和果斷處理時。此外,考慮到大多數交易所提供的杠杠交易,都需要AML/KYC,這可能導致攻擊者很快暴露。
攻擊者不僅面臨著巨大的資金風險,而且還會有身體危險。從經濟角度來看,這并不是一個容易獲利的漏洞。
當然,有一些別有用心的人,可能會用這種漏洞來嚇唬那些比特幣持有者。投資回報率將變得更抽象,因此從理論上來講,這可能會達到這些別有用心者的目的。
結論
毫無疑問,這是一個相當嚴重的漏洞。盡管我和Awemany之間有著分歧,但我很感激他選擇公開地和我辯論。也就是說,考慮到經濟博弈理論,我不認為這個漏洞會像他所描述的那樣嚴重。
即使這個漏洞在被發現之前被壞人所利用,攻擊者可能也不會選擇利用它,因為從經濟學上來講,它是沒有意義的??梢钥隙ǖ氖牵@一技術漏洞應該被修復,并且開發者應該做得更好,但真正能夠利用這種漏洞的對象其實是非常少的,基本上,只有那些想要摧毀比特幣的組織才會這么干。
Bitcoin Core開發者的教訓有很多:
1、任何共識變化(即使是微小的變化,例如9049),也需要更多的人進行審查; 2、需要對病理交易進行更多的檢查; 3、代碼庫中哪些檢查是冗余的,哪些檢查是不冗余的,以及實際代碼將要做些什么,需要變得更加清晰; 4、過去存在漏洞,將來也會存在漏洞?,F在重要的是,學習并反思這一教訓;
評論
查看更多