公鏈安全之比特幣任意盜幣漏洞淺析(CVE-2010-5141)
本期DVP漏洞專題是比特幣漏洞CVE-2010-5141,這個漏洞可以導致攻擊者盜取任何人的比特幣,危害十分嚴重。萬幸的是該漏洞並沒有被利用過,而且修復速度極快。該漏洞與比特幣的腳本引擎有關,對公鏈開發者具有參考意義;從當下市場上的公鏈來看,大多數都內置了虛擬機或腳本引擎,以此來打造DApp生態,同時也是區塊鏈的大趨勢之一。
一、比特幣當中的UTXO模型是什麼?
Tips:在漏洞代碼片段中會涉及一些UTXO的相關知識、概念,所以對該漏洞進行理論分析之前需要先了解一下這些知識點,已經了解的可以直接跳過。
Ⅰ、賬戶模型與UTXO模型
我們在看 UTXO模型
之前先說說常見的 賬戶模型
,什麼是 賬戶模型
? 賬戶模型
的數據結構簡單可以理解為「賬號=>餘額」,每個賬號都對應一個餘額。舉個例子:若賬號A向賬號B轉賬200,在賬戶模型中完成這個轉賬操作只需要A-200然後B+200;目前大部分軟件都採用的是賬戶模型,比如銀行系統、以太坊等等。
而比特幣卻使用了自行研發的UTXO模型,UTXO中是沒有「賬號=>餘額」這樣的數據結構的,那怎麼進行轉賬?
Ⅱ、比特幣如何操作轉賬
以上面A向B轉賬為例,在UTXO中完成這個轉賬需要以下操作:
- 找到A賬號下200餘額的來源,也就是意味着要找到A收款200的這筆交易x
- 以x交易為輸入,以向B轉賬200的交易y為輸出,x與y對應且x與y的轉賬金額必須相等
- x交易被標記為已花費,y交易被標記為未花費
兩筆交易的轉賬金額必須相等,簡單解釋就是收到多少就只能轉出多少,實際上確實是這樣。
但是又必須只給別人轉一部分的時候怎麼辦?答案是只向他人轉一部分,然後剩下的一部分轉給自己另外一個號。
Ⅲ、引用兩張來自網絡的圖文:
在本文當中比特幣為什麼採用UTXO模型不是重點,我們了解UTXO的原理即可。
⠀
二、比特幣的腳本引擎
比特幣腳本是非圖靈完備的。比特幣使用自行定義的一種腳本進行交易和其他的操作,為比特幣提供有限的靈活性。實現諸如多重簽名、凍結資金等簡單功能,但更多的就不行了。
比特幣這麼做的原因是犧牲一定的完備性來保障安全性。比特幣腳本的原理是先定義了一堆操作碼,然後腳本引擎基於堆棧來逐個執行每個操作碼。
堆棧很好理解,隊列是先進後出,而堆棧正好相反,是先進先出,將一個元素壓入(push)堆棧後該元素會被最先彈出(pop)。
在比特幣早期的版本中發送一筆標準轉賬( pay-to-pubkey
)交易需要腳本簽名( scriptSig
)和公鑰驗證腳本( scriptPubKey
),具體處理流程如下:
先填入要執行的腳本( Script
),然後簽名( sig
)和公鑰( pubKey
)被壓入堆棧,然後操作碼 OP_CHECKSIG
會去驗證簽名等,若驗證通過就將true壓入堆棧,否則就將false壓入堆棧。
三、CVE-2010-5141漏洞分析
了解以上知識後就可以開始分析 CVE-2010-5141
漏洞了。筆者下載了存在漏洞的版本 0.3.3
,下載地址在github的bitcoin倉庫中找release.
script.cpp
代碼片段 VerifySignature
函數:
執行每個交易都會調用 VerifySignature
函數,該函數用於執行腳本以及驗證簽名,然後給交易標註是否被花費。
首先 txFrom
參數是上一筆交易, txTo
是正在處理的這筆交易,如果理解了上面的章節中講解過的 UTXO模型
,這裡就不難理解了。重點看 1125行
代碼,調用了 EvalScript
函數,第一個參數是 txin.scriptSig
(包含簽名信息)+分隔操作碼 OP_CODESEPARATOR
+ txout.scriptPunKey
(包含公鑰信息、 OP_CHECKSIG
指令),這些就是 EvalScript
函數要執行的腳本,後面的參數可以暫時不用管,只要 EvalScript
函數返回true那麼這筆驗證簽名就通過了。 EvalScript
函數如何才能返回true?
首先堆棧不能是空的,然後棧頂強轉bool後必須是true。筆者簡單解讀為必須要有棧頂而且值不能是0。
然後再看關鍵的 OP_CHECKSIG
操作碼
(註:由於操作碼太多,本文針對 OP_CHECKSIG
操作碼)
上面代碼不難理解,調用
Checksig
函數來驗證簽名,然後返回給FSuccess變量,如果為真就壓一個vchTrue(非0)進棧,否則就壓一個vchFalse(0)進棧;如果opcode是 OP_CHECKSIGVERIFY
而不是 OP_CHECKSIG
的話就讓vchTrue出棧,並開始執行後面的操作碼。
按照 OP_CHECKSIG
的正常邏輯,驗證簽名不成功的話一定會有一個vchFalse留在棧頂,雖然堆棧不為空,但是棧頂的值是0,還是會返回false。
回到之前的代碼, EvalScript
函數執行的腳本主要由以下變量組成:
- txin.scriptSig
- OP_CODESEPARATOR
- txout.scriptPubKey
⠀
第一個簽名信息可控,第二個不用管只是一個分割符,會被刪掉,第三個不可控,因為是來自上一個交易。
第一個變量可控,而且是作為腳本執行,所以這個變量可以不僅僅是簽名信息,還能是opcode,這就好辦了,下面需要引用一個神奇的操作碼 OP_PUSHDATA4
,我們看看比特幣 0.3.3
是怎麼處理這個操作碼的:
首先獲取操作碼。如果操作碼的值小於或者等 於OP_PUSHDATA4
的值就把vchPushValue全壓入堆棧,再跟進 GetOp
函數
經翻閱源碼,發現 OP_PUSHDATA4
指令被定義為 78
,所以當函數遇到 OP_PUSHDATA4
時,指針會向又移 78+4=82
位,其中 78
位數據會被壓入棧,所以只要在 txin.scriptSig
中注入一個 OP_PUSHDATA4
操作碼,後面的公鑰信息以及 OP_CHECKSIG
指令都會被」吃掉」並作為參數入棧,當指針走到最後時,進行最後的判斷:
- 堆棧是否為空?不是
- 棧頂元素是否為0?不是
於是滿足條件 EvalScript
函數返回true,然後 VerifySignature
函數也返回true,既然簽名驗證都繞過了,那別人的比特幣便可以任意花費了。
四、CVE-2010-5141漏洞修復方案
筆者下載了比特幣版本0.3.8,直接看關鍵部分代碼
修復方案也很明確,把 scriptSig
和 scriptPubkey
分開執行,無論你 scriptSig
裏面有什麼也不會影響到後面的 scriptPubkey
執行。
⠀
寫在最後:
因為該比特幣漏洞來自2010年,素材可以說是相當古老,目前漏洞分析主要存在以下難點:
- 漏洞相關資料非常少,大多數漏洞都只有一個CVE編號和簡介,不查閱大量資料無從入手。
- 環境搭建難,難在編譯、私鏈搭建(早期的比特幣甚至沒有私鏈這個概念)等,比特幣早期的源碼編譯需要的依賴現在很多都已經不維護並下線了。
基於這些原因,所以筆者僅做了理論研究,並未進行實踐驗證,如有錯誤之處還請指正。
五、參考鏈接
https://en.bitcoin.it/wiki/Script
原文 : 安全客