深入分析CVE-2020-0601 CurveBall漏洞
概述
在2020年1月,Microsoft發布的月度安全補丁中包含針對CVE-2020-0601的修復程序,該漏洞是由美國國家安全局(NSA)發現,漏洞位於Windows CryptoAPI系統中的一個核心加密庫組件,影響加密證書的驗證過程。該漏洞被稱為CurveBall或「Chain of Fools」,攻擊者可能會利用這一漏洞創建他們自己的加密證書,這些證書會成為Windows默認情況下完全信任的合法證書。
在漏洞披露的兩天之內,概念深入分析CVE-2020-0601 CurveBall漏洞。
證明(PoC)開始在網路上浮出水面。隨之而來的是關於該漏洞所設計的橢圓曲線密碼學(ECC)概念的幾種解釋。
本文將主要分析漏洞存在的代碼,重點關注應用程序可能如何使用CryptoAPI處理證書的上下文,特別是通過傳輸層安全性(TLS)進行通信的應用程序上下文,以找到漏洞的根本原因。
證書分析
X.509是國際電信聯盟(ITU)標準,使用ASN.1表示方法規定了公鑰證書的結構。基本上,證書是一個包含三個「第一層」項目的序列,包括證書、簽名演算法標識符和對證書進行驗證的簽名。下面的ASN.1代碼片段展示了這種結構:
證書本身是包含多個嵌套項目的幾個組件的序列:
其中,特別重要的是SubjectPublicKeyInfo項,這個序列中包含有關公鑰所使用的演算法的信息,後面跟著實際的公鑰:
AlgorithmIdentifier結構用於存儲公鑰和簽名演算法的信息和參數,它由對象標識符(OID)和可選參數組成,具體取決於由OID標識的特定演算法:
在公鑰的上下文中,演算法欄位可能是眾多OID中的一個,例如rsaEncryption,其OID為1.2.840.113549.1.1.1,dsa的OID為1.2.840.10040.4.1,ecPublicKey的OID為1.2.840.10045.2.1。當OID與ecPublicKey對應時,表示公鑰基於橢圓曲線密碼學。在這種情況下,會將參數欄位設置為與RFC 3279中定義的EcpkParameters對應的選項之一:
換而言之,可以通過提供與眾所周知的「命名曲線」之一(隱式指定曲線的參數)相對應的OID,或通過在ecParameters中顯式定義曲線參數的方式,來定義橢圓曲線。當攻擊者提供帶有ecParameters,而不是命名曲線的特定證書時,就會產生這個漏洞。但是,為什麼會發生這樣的情況呢?首先我們迅速介紹一下橢圓曲線。
關於橢圓曲線
橢圓曲線是由以下方程式定義:
y^2 = x^3 ax b
在橢圓曲線密碼學中,該方程的解是在有限域的範圍內計算的。有限域(也稱為Galois域)是具有有限數量元素的集合,通常是通過使用質數或質數的冪(可以表示為GF(pn))對域進行模運算來創建的。針對質數集合(也就是冪的n為1),指定橢圓曲線中的點是由範圍內的x和y坐標組成。集合中元素的數量稱為域的階,所以橢圓曲線的階是由曲線上的所有點組成。
用於ECC的橢圓曲線定義了基點(Generator Point),這是曲線上的一個特定點,可以用於「生成」曲線上的任何其他點。這一過程是通過將點乘以有限域的階範圍內的某個整數來實現的。對於命名曲線來說,係數a和b、域標識符(通常是質數p)和基點都是預先確定的,並且在每條曲線的官方標準中進行了記錄。
但是,如果證書中定義了顯示的ecParameters,則曲線的所有參數都會被明確選擇並顯示在證書中,如ECParameters的ASN.1結構所示:
在漏洞的上下文中,可以以允許攻擊者使用其創建的私鑰生成證書的方式操縱這些參數。公用密鑰與現有證書的公用密鑰相同。通常,需要修改基點,並使所有其他曲線參數與原始證書上使用的命名曲線的預定參數相同。
隨後,攻擊者可以使用這個新製作的「受信任」證書和私鑰來對其他證書進行簽名,然後將製作的證書和稱為「最終」證書的其他證書同時提供給目標。然後,目標將嘗試使用攻擊者提供的證書以及Windows證書存儲區中包含的受信任證書的組合,對證書鏈進行驗證。而這一驗證過程中存在漏洞,因此,我們首先研究安全補丁中產生的改動,以深入了解Crypto API的工作原理。
分析補丁修改內容
在Windows CryptoAPI中包括多個不同的庫,而其中的crypt32.dll中存在漏洞。我們對比最新未修復版本(10.0.18362.476)的DLL和已修復版本(10.0.18362.592)的DLL之間的二進位差異,發現了這兩個文件之間的細微變化:
在已修復的版本中,增加了5個值得關注的新函數,並且對5個現有函數進行了更改。我們判斷,很可能是由於增加了新的函數,所以需要對現有函數進行更新以利用這些新的函數。值得慶幸的是,Microsoft為絕大多數Windows組件提供了調試符號,因此我們僅通過檢查函數的名稱就可以獲得很多信息。
首先,我們來看看變更函數的名稱,crypt32.dll中已修改函數的名稱如下:
我們可以看到,「CCertObject」類的構造函數和析構函數都已經進行了略微更改,但最大幅度的修改位於ChainGetSubjectStatus()和CCertObjectCache::FindKnownStoreFlags()之中。
接下來,我們查看新增函數的名稱,crypt32.dll中新增函數的名稱如下:
其中的幾個新函數值得我們進行關注,我們找到了其中一個適合起步的函數,我們就從它開始入手。ChainLogMSRC54294Error()函數是一個新的日誌記錄功能,可以幫助Windows事件日誌記錄潛在的漏洞利用嘗試。可以通過跟蹤以下塊來確定該函數的作用:
在這裡,將一個包含CVE編號的字元串傳遞到名為CveEventWrite的外部庫函數,這是一個相對較新的事件跟蹤API函數,將基於CVE的事件寫入到Windows事件日誌中。
藉助這個信息,我們可以檢查對這一新函數的交叉引用,以發現有關記錄事件的上下文的更多詳細信息。在這裡,對於ChainLogMSRC54294Error()的唯一直接引用是在ChainGetSubjectStatus()函數中,而這個函數存在較大的修改。
對函數ChainGetSubjectStatus()進行分析,以查看對ChainLogMSRC54294Error()的引用:
我們對新日誌記錄函數調用的周邊環境進行查看,發現調用該函數是根據CryptVerifyCertificateSignatureEx()和ChainComparePublicKeyParametersAndBytes()的調用中特定結果而進行調用的,其中的ChainComparePublicKeyParametersAndBytes()是補丁中添加的新函數之一。因此,通過分析該函數的改動以及對其進行調用的上下文,發現ChainGetSubjectStatus()是一個不錯的目標。
CryptoAPI中證書驗證的內部工作原理分析
為了了解ChainGetSubjectStatus()是如何工作的,我們必須首先研究通常情況下程序是如何使用CryptoAPI來處理證書。
實際上,我們的分析工作是在PowerShell的Invoke-Webrequest cmdlet上執行的,但是其他TLS客戶端可能會以類似的方式來運行。首次載入PowerShell時,將通過調用CertOpenStore來獲取包含顯式受信任證書的系統證書存儲句柄,以便在必要時可以使用這些存儲。隨後,這些證書存儲會被添加到一個「集合」中,該集合實際上相當於一個大型的證書存儲集中地。
當使用像Invoke-Webrequest這樣的命令,通過TLS向伺服器發送HTTP請求時,伺服器將發出TLS證書握手消息,其中包含最終證書以及可能用於驗證證書鏈的其他證書。在收到這些證書後,將創建一個額外的「內存中」存儲,並額外調用CertOpenStore()。隨後,通過函數CertAddEncodedCertificateToStore()將接收到的證書添加到這個新的存儲中,該函數將創建一個CERT_CONTEXT結構,該結構包含證書存儲的句柄、指向原始編碼證書的指針以及指向一個CERT_INFO結構(該結構基本對應於證書的ASN.1結構)的指針。
舉例來說,下面這些是在最終證書調用CertAddEncodedCertificateToStore()後創建的結構。
通過調用CertAddEncodedCertificateToStore()創建的結構:
其中有一些地方非常關鍵。頒發者應該與該證書的簽名方所屬的證書標題完全匹配,而SubjectPublicKeyInfo結構中包含相同名稱的ASN.1結構,其中包含相同名稱的ASN.1結構中包含的精確信息。例如,由於演算法標識符OID為1.2.840.10045.2.1,我們可以看到最終證書公鑰的橢圓曲線:
我們還可以通過查看參數的第一個位元組,來迅速確定是命名曲線還是顯式曲線參數:
在這裡,0x6是對象標識符的DER編碼標籤,表示已經指定了命名曲線。如果提供了顯式的ecParameter,則該標籤的值將為0x30,與序列相對應。
一旦將所有接收到的證書添加到內存存儲中後,PowerShell就會使用CERT_CONTEXT結構調用CertGetCertificateChain()函數作為最終證書,以構造證書鏈的上下文,其中也包括所有收到的中間證書,如果可能的話會返回到受信任的根證書。
具體而言,CertGetCertificateChain()將返回CERT_CHAIN_CONTEXT結構,該結構是證書鏈的數組,以及包含有關鏈的有效性的數據可信狀態結構。這一過程主要在CCertChainEngine::GetChainContext()函數中進行處理,該函數最終將返回一個CERT_CHAIN_CONTEXT結構,其中包含證書鏈的驗證狀態。
CCertChainEngine::GetChainContext()調用CCertChainEngine::CreateChainContextFromPathGraph(),該方法首先創建一個新的集合。然後,從集合中找到最終證書(所有證書都存儲於這個集合中),並使用最終證書的CERT_INFO調用ChainCreateCertObject()。ChainCreateCertObject()嘗試在創建的緩存中找到現有的CCertObject,如果無法找到,則會實例化一個新的CCertObject。
CcertObject的構造函數執行以下操作:初始化一些欄位,從CERT_INFO結構中複製各種屬性(包括簽名哈希值、密鑰標識符等),查找證書擴展中是否設置了任何策略,並檢查CryptoAPI是否不支持任何標記為「關鍵」的擴展。
然後,構造函數調用ChainGetIssuerMatchInfo()來檢索CERT_AUTHORITY_KEY_ID_INFO結構,該結構標識符用於簽署證書的密鑰。然後,它檢查證書是否是自簽名的,以及公鑰是否使用長度較弱的RSA演算法。
在創建最終證書的CCertObject後,將創建一個CChainPathObject。在執行了CChainPathObject的主要初始化之後,構造函數將調用CChainPathObject::FindAndAddIssuers(,這最終將導致對CChainPathObject::FindAndAddIssuersFromStoreByMatchType()的調用,以查找與最終證書的發行者相匹配的證書,這一過程通常根據最終證書頒發者的哈希值進行比較。如果在存儲區中找到了另一個證書(即,該證書與最終證書一同發送),則會再次調用ChainCreateCertObject(),這次使用最終證書頒發者的證書的CERT_INFO結構。
這一過程將循環重複執行,直到找到自簽名證書(也就是根證書)為止,這會導致對ChainGetSelfSignedStatus()的調用。首先,這一過程會檢查主題和頒發者是否匹配,如果匹配,將會調用CryptVerifyCertificateSignatureEx()以驗證證書上的簽名,從證書的CERT_INFO結構中獲得CERT_PUBLIC_KEY_INFO結構中提供的公鑰和密鑰演算法信息。
有很多驗證過程是在函數I_CryptCNGVerifyEncodedSignature()中執行的。如果證書提供與指定曲線相反的顯式橢圓曲線參數,則調用函數I_ConvertPublicKeyInfoToCNGECCKeyBlobFull()來填充包含曲線參數的BCRYPT_ECCFULLKEY_BLOB結構,這個結構沒有出現在正式的文檔中,但可以在Windows SDK的bcrypt.h標頭中找到。隨後,在調用CNGECCVerifyEncodedSignature()時會使用這些參數,這裡會調用bcrypt庫函數BCryptVerifySignature()來執行給定參數和簽名的實際驗證。
如果驗證通過,則會調用CCertIssuerList::AddIssuer()函數,該函數最終將創建新的CChainPathObject。然後,在對CCertIssuerList::CreateElement()的調用中使用最終證書和自簽名證書的CChainPathObjects,這個調用過程首先會進行一些初始化,然後使用兩個CChainPathObjects調用ChainGetSubjectStatus()。
ChainGetSubjectStatus()首先使用與每個CChainPathObjects相關聯的CCertObjects來調用ChainGetMatchInfoStatus(),以驗證兩個標題是否相同。然後,檢查CCertObject中與最終證書關聯的標誌是否存在。
調用ChainGetMatchInfoStatus()並檢查與最終證書關聯的CCertObjects中的標誌:
在創建CCertObject時,這個標誌初始化為0。這將導致對CryptVerifyCertificateSignatureEx()的調用,以驗證自簽名證書是否是最終證書的有效頒發者。
調用CryptVerifyCertificateSignatureEx()進行證書驗證:
如果認為簽名有效,則將CERT_ISSUER_PUBLIC_KEY_MD5_HASH屬性添加到最終證書,並將上述標誌設置為3。
將CERT_ISSUER_PUBLIC_KEY_MD5_HASH屬性添加到最終證書:
一旦CCertIssuerList::AddIssuer()函數返回,並且所有其他函數返回到原來的CChainPathObject::FindAndAddIssuers()調用,就會檢查最終證書中的上述標誌,如果設置了該標誌,則會再次調用CChainPathObject::FindAndAddIssuersFromStoreByMatchType()和CChainPathObject::FindAndAddIssuersFromStoreByMatchType()。
這次,將會搜索包含系統證書信任列表的集合存儲,並為FindElementInCollectionStore()提供一個包含搜索參數的CERT_STORE_PROV_FIND_INFO結構,該結構將遍歷集合中的存儲,在每個存儲中搜索是否存在相匹配的自簽名證書公鑰MD5哈希值。
如果找到相應的公鑰哈希值,則再次調用ChainCreateCertObject(),這次使用從系統存儲中檢索到的證書的CERT_CONTEXT結構,創建新的CCertObject,由於證書來源於「已知存儲」,所以會在這一過程進行標記。然後,將該對象作為頒發者對象添加到CCertObjectCache,並添加到頒發者列表,再次創建新的CChainPathObject並使用CChainPathObjects調用CCertIssuerList::CreateElement()以獲得自簽名證書和從存儲中檢索到的受信任證書。
在調用ChainGetSubjectStatus()時,現在會設置上述標誌,從而導致會將此前在自簽名證書上設置的CERT_ISSUER_PUBLIC_KEY_MD5_HASH與來自受信任存儲區證書中公鑰的MD5哈希值進行比對。
如果找到匹配項,則不會再對從受信任存儲中檢索到的證書進行自簽名證書的進一步驗證。這表明,此時已經認為所提供的證書是可信的,因為它已經與證書存儲中檢索到的公鑰哈希值相匹配。並且,提供的自簽名證書已經成功驗證最終證書的簽名。如果攻擊者向證書提供與受信任根證書相同的公鑰,並使用顯式定義的橢圓曲線參數製作,那麼最終證書的簽名將會受到信任,就如同合法的根證書籤名一樣。
一旦所有函數返回到CCertChainEngine::CreateChainContextFromPathGraph(),就會產生這種信任。認證過程中的每個對象(即:最終證書和製作的根證書)都執行額外的檢查,例如通過對比當前時間與證書吊銷狀態來確認證書的有效性。然後,通過調用CChainPathObject::CreateChainContextFromPath()來創建CERT_CHAIN_CONTEXT結構,並設置其中包含的CERT_TRUST_STATUS結構,以反映證書鏈的有效性。
一旦CertGetCertificateChain()函數返回,主應用程序就可以檢查CERT_CHAIN_CONTEXT結構中包含的證書鏈驗證狀態,該結構在我們所描述的攻擊場景中將顯示為「有效」。
驗證CERT_CHAIN_CONTEXT結構中包含的證書鏈的狀態:
總結
總而言之,根據我們對CVE-2020-0601的分析,得出了以下幾點結論:
1、最終證書的簽名,使用特製的根證書和任意橢圓曲線參數進行驗證。
2、如果再次使用其中包含的任何橢圓曲線參數,都將導致特製的根證書的簽名被驗證為自簽名證書。
3、通過查找公鑰的哈希值,在系統證書存儲區中找到了與特製根證書匹配的值,從而將特製的根證書視為合法的根證書。
4、檢查特製根證書和合法根證書的公鑰哈希值,如果哈希值匹配,那麼就不會再對特製根證書進行進一步驗證,從而通過了特製根證書的全部驗證過程。
經過對Microsoft的補丁程序進行分析,我們發現他們添加了對新函數ChainComparePublicKeyParametersAndBytes()的調用,替換了頒發者和受信任根公鑰哈希值之間的簡單比較,取而代之的是將受信任根證書與證書之間的公鑰參數和位元組進行了比較,以此來驗證最終證書上的簽名是否合法。
如果比較結果不匹配,則會調用CryptVerifySignatureEx(),使用實際的受信任根證書、參數等所有內容來重新驗證最終證書上的簽名,從而有效發現與實際受信任根證書加密參數不同的特製根證書。
安全建議與解決方案
我們建議個人和組織應該儘快安裝Microsoft的最新補丁,以防止CurveBall漏洞的進一步利用。用戶還可以通過漏洞評估工具確認是否受到CVE-2020-0601的威脅。
通過Trend Micro深度安全防護系統和漏洞防護解決方案,可以保護系統和用戶免受該漏洞的威脅:
1010130-Microsoft Windows CryptoAPI Spoofing Vulnerability (CVE-2020-0601)
1010132-Microsoft Windows CryptoAPI Spoofing Vulnerability (CVE-2020-0601) – 1
使用MainlineDV過濾器,可以有效防範利用CVE-2020-0601的威脅和攻擊:
36956: HTTP: Microsoft Windows CryptoAPI Spoofing Vulnerability
本文參考自:https://blog.trendmicro.com/trendlabs-security-intelligence/an-in-depth-technical-analysis-of-curveball-cve-2020-0601/