內核中的代碼完整性:深入分析ci.dll


內核中的代碼完整性:深入分析ci.dll

前言

在某些場景中,如果我們希望在允許某個進程進行特定動作前,以一種可靠的方式確認該進程是否可信,那麼驗證該進程的Authenticode簽名是一個不錯的方式。用戶模式下的DLL wintrust提供了專門用於此目的的API。但是,如果我們需要在內核模式下以一種可靠的方式來進行身份驗證,這時應該如何進行呢?在以下的情況中,我們可能會遇到這樣的場景:1、應用程序用戶模式部分不可用,可能是由於正處於開發過程的早期階段,也可能是由於運行失敗或配置出現問題。2、我們希望獲得對進程操作的內聯訪問權限,以便在進程未驗證的情況下阻止它們。3、最典型的一種情況是Windows內核在加載驅動程序時對驅動程序進行驗證,顯然這一過程必須要在內核模式下完成。儘管在不少論壇上,都有人多次提問應該如何操作,但我們還沒有在公開的地方找到解決該問題的任何實現。其中一些方案建議我們自行實現,一些方案則建議將OpenSSL源導入到我們的項目中。而另外一種方案則將這個任務委託給用戶模式下的代碼。但是,上述所有替代方案都有明顯的缺點:1、在解析複雜的ASN1結構時容易出現錯誤;2、不適合將大量源代碼導入驅動程序,因為OpenSSL中的每一個漏洞修復都會導致重新導入該代碼。3、進入用戶模式可能無效,並且用戶模式並非始終都可用。實際上,Microsoft內核模式庫ci.dll中,就包含對文件進行身份驗證的功能。j00ru的研究表明,ntoskrnl通過CiInitialize()函數初始化CI模塊,該函數以回調列表填充函數指針結構。如果我們可以使用這些函數或者其他CI導出來驗證正在運行的進程或文件的完整性和真實性,這將會成為內核驅動程序的一個最佳方案。除了ntoskernel.exe之外,我們還發現了兩個驅動程序,它們都鏈接到ci.dll,並使用其導出文件:

內核中的代碼完整性:深入分析ci.dll

鏈接到ci.dll的驅動程序

內核中的代碼完整性:深入分析ci.dll

鏈接到ci.dll的驅動程序驅動程序可以鏈接到這個模塊,並且調用一些關鍵的函數,例如CiValidateFileObject()。從函數名稱就可以看出,這樣的方式完全可以滿足我們的需求。在本文中,我們將通過一個代碼示例來詳細分析CI,可以以此作為進一步研究的基礎。

背景信息

我們建議各位讀者在詳細分析ci.dll之前,首先熟悉以下相關主題:1、PE安全目錄:PE中包含Authenticode簽名的部分;2、WIN_CERTIFICATE結構:Authenticode簽名之前的標頭;3、PKCS 7 SignedData結構:Authenticode的基礎結構;4、X.509證書結構;5、證書時間戳:通過過期或吊銷證書來延長簽名使用週期的方法。

研究過程

在Windows 10上,CI會導出以下函數:

內核中的代碼完整性:深入分析ci.dll

CI導出功能如前所述,調用CiInitialize()將會返回一個名為g_CiCallbacks的結構,其中包含更多函數(詳情請參考[1][2][5])。而其中的一個函數,CiValidateImageHeader(),將會被ntoskernel.exe用於加載驅動程序以驗證簽名的過程:

內核中的代碼完整性:深入分析ci.dll

調用堆棧以在加載過程中驗證驅動程序簽名在我們的研究中,利用了導出的函數CiCheckSignedFile()以及與之交互的數據結構。稍後我們將看到,這些數據結構也出現在其他CI函數中,我們也可以將研究範圍擴展到這些其他的函數。

CiCheckSignedFile()

CiCheckSignedFile()可以接收8個參數,但目前我們還不清楚這些參數的名稱是什麼。但是,通過檢查內部函數,我們可以推斷出其參數。例如,我們可以檢查MinCryptGetHashAlgorithmFromWinCertificate():

內核中的代碼完整性:深入分析ci.dll

檢查WIN_CERTIFICATE的結構成員我們發現,對於WIN_CERTIFICATE結構來說,常量0x200和2是比較常見的值,該結構為我們提供了第四個和第五個參數。我們可以通過類似的方式找到其餘的輸入參數。而對於輸出參數來說,則方法完全不同,我們將在後文中詳細描述。進行一些逆向之後,我們得到了函數簽名:

<code>NTSTATUS CiCheckSignedFile(
__In__ const PVOID digestBuffer,
__In__ int digestSize,
__In__ int digestIdentifier,
__In__ const LPWIN_CERTIFICATE winCert,
__In__ int sizeOfSecurityDirectory,
__Out__ PolicyInfo* policyInfoForSigner,
__Out__ LARGE_INTEGER* signingTime,
__Out__ PolicyInfo* policyInfoForTimestampingAuthority
);
/<code>

該函數的工作方法如下:1、調用方位函數提供文件摘要(緩衝區和算法類型),以及指向Authenticode簽名的指針。2、該函數通過以下方式驗證簽名和摘要:(1)遍歷文件簽名,並獲取使用特定摘要算法的簽名;(2)驗證簽名(和證書),並提取其中顯示的文件摘要;(3)將提取的摘要與調用方提供的摘要進行比較。3、除了驗證文件簽名之外,該函數還為調用方提供有關已驗證簽名的各種詳細信息。該函數後面一部分的工作原理非常值得關注,因為僅僅知道文件已經經過正確簽名是不夠的,我們還需要知道是由誰進行簽名的。在下一節中,我們將解決這一問題。

PolicyInfo結構

到目前為止,我們已經將所有輸入參數輸入到CiCheckSignedFile()並且能夠進行調用。但是,我們除了其大小(在Windows 10 x64上為0x30)之外,對於PolicyInfo結構幾乎一無所知。作為輸出參數,我們希望該結構能以某種方式提供有關簽名者身份的提示。因此,我們調用該函數,並對內存進行檢查,以確認哪些數據填充到PolicyInfo之中。在內存中,似乎包含一個地址和一些較大的數字。該結構正在內部函數MinCryptVerifyCertificateWithPolicy2()中填充:

內核中的代碼完整性:深入分析ci.dll

填充PolicyInfo結構該函數中的某些代碼似乎正在檢查該值是否在特定範圍之內。對於證書驗證的過程來說,我們推測這個範圍是證書有效的時間範圍,事實上證明這是正確的:

內核中的代碼完整性:深入分析ci.dll

檢查證書有效期這將引向以下結構:

<code>typedef struct _PolicyInfo
{
int structSize;
NTSTATUS verificationStatus;
int flags;
PVOID someBuffer; // later known as certChainInfo;
FILETIME revocationTime;
FILETIME notBeforeTime;
FILETIME notAfterTime;
} PolicyInfo, *pPolicyInfo;
/<code>

儘管證書的有效期非常值得關注,但是這並不能直接定位到簽名者。稍後我們將發現,大多數信息都位於成員certChainInfo之中,我們將在稍後討論。

CertChainInfo緩衝區

在檢查PolicyInfo的內存時,我們可以看到它指向結構外部的內存位置——動態分配的緩衝區。該分配位於I_MinCryptAddChainInfo()中,其函數名稱表明了緩衝區的用途。我們通過檢查其內存佈局來逆向這一緩衝區:1、在前幾個字節中,有指向緩衝區內部各個位置的指針。2、在這些指向的位置中,存在重複的模式和指向緩衝區內部更遠位置的指針。3、在最後指向的這些位置中,我們找到了一些文本,看起來像是證書的摘要。該緩衝區中包含有關整個證書鏈的數據,既有解析格式(位於子結構中),也有原始數據格式(包含證書、密鑰、EKU的ASN.1證書)。這一部分使調用方可以輕鬆地查看證書的主題、頒發者、證書鏈的組成,以及用於創建每個證書的哈希算法。為了更好地解釋這個緩衝區的格式,以及我們從中得到的子結構,我們將分析其在32位計算機上的內存佈局。如果使用32位計算機,可以減少混亂的情況,這裡可以利用更少的填充字節來滿足對齊要求。下面是由Microsoft簽名的Notepad.exe的示例:

內核中的代碼完整性:深入分析ci.dll

CertChainInfo緩衝區的內存視圖我們在這裡可以發現:1、緩衝區的頂部有兩個4字節的數字。其中的一個表明在哪裡可以找到一系列CertChainMember類型結構的地址,另一個是可以指示其中有多少個結構的計數器。2、第一個CertChainMember位於地址0x89BF45C8中(以黑色標出),我們將其格式化如下:(1)在CertChainMembers的末尾,以藍色標出的地址0x89BF4688處,有純文本格式的主題名稱。(2)在橙色標出的地址0x89BF4699處,有純文本格式的發行者名稱。(3)在紅色箭頭指出的地址0x89BF46BE處,包含實際證書的ASN.1 blob的開頭。內存以小端對齊的4字節為一組顯示,因此證書的前兩個字節實際上是0x3082,而不是如圖所示的0x3131。

<code>typedef struct _CertChainMember
{
int digestIdetifier; // e.g. 0x800c for SHA256
int digestSize; // e.g. 0x20 for SHA256
BYTE digestBuffer[64]; // contains the digest itself
CertificatePartyName subjectName; // pointer to the subject name
CertificatePartyName issuerName; // pointer to the issuer name
Asn1BlobPtr certificate; // pointer to actual certificate in ASN.1
} CertChainMember, * pCertChainMember;
/<code>

這就是我們之前所說的解析數據。我們無需自行解析證書,就可以獲取到主題或頒發者。該結構中的最後一個字節指向緩衝區內部更遠的位置。接下來的96個字節包含第二個CertChainMember,出於可讀性的考慮,未將其標出。其中包含有關鏈的下一個證書的信息。對於公鑰和EKU(擴展密鑰用法)來說,存在一系列類似的指針和結構。換而言之,CI從證書中獲取了一些關鍵數據,並且使其以子結構的形式提供給調用方。但是,如果調用方還需要其他的一些內容,那麼其中還可能會包括未解析的原始數據。注意:PolicyInfo和CertChainInfo結構都以結構的大小開始。由於這些結構是可以在OS版本之間實現擴展的,因此在嘗試訪問其他結構成員之前,必須要檢查這裡的大小。在存儲庫中的文件ci.h中,可以找到CertChainInfo緩衝區的完整分類和各種子結構。

CiFreePolicyInfo()

該函數將釋放PolicyInfo的certChainInfo緩衝區,該緩衝區由CiCheckSignedFile()和其他填充PolicyInfo結構的CI函數分配。該函數還會重置其他結構成員。在這裡,必須要對其進行調用,以避免內存洩漏。

內核中的代碼完整性:深入分析ci.dll

CiFreePolicyInfo()的實現由於該函數會在內部檢查是否有可用的內存,因此即使是未填充PolicyInfo,也可以安全地對其進行調用。

CiValidateFileObject()

如前文所述,在調用CiCheckSignedFile()之前需要首先完成一些工作。調用方必須計算文件哈希值並解析PE,以便為函數提供簽名的位置。但是,函數CiValidateFileObject()可以為調用方完成這部分工作。我們不需要從頭開始,因為它與CiCheckSignedFile()共享一些參數:

<code>NTSTATUS CiValidateFileObject(
__In__ struct _FILE_OBJECT* fileObject,
__In__ int a2,
__In__ int a3,
__Out__ PolicyInfo* policyInfoForSigner,
__Out__ PolicyInfo* policyInfoForTimestampingAuthority,
__Out__ LARGE_INTEGER* signingTime,
__Out__ BYTE* digestBuffer,
__Out__ int* digestSize,
__Out__int* digestIdentifier
);
/<code>

該函數在內核空間中映射文件,並提取其簽名:

內核中的代碼完整性:深入分析ci.dll

通過CiValidateFileObject()在系統空間中映射文件。該函數還會計算文件摘要,如果為其提供了足夠長的非空緩衝區,將會使用摘要來進行填充。注意:由於該函數僅在最新的Windows版本上添加,因此我們並未將研究的重點放在這個函數上。如果我們要繼續研究,我們會專注於分析其驗證的策略。在這裡,使用了比CiCheckSignedFile()更為嚴格的策略,這意味著它有可能無法驗證通過此前經過CiCheckSignedFile()驗證的PE。這裡可能會受到第2個和第3個參數值的影響,但我們還沒有對其進行逆向。

GitHub Repo

為了演示如何利用ci.dll來驗證PE簽名,我們使用了GitHub存儲庫來對本文進行了補充。該存儲庫中,包含一個簡單的驅動程序,可以用於測試我們上述的研究成果:1、註冊用於新進程通知的回調;2、嘗試使用ci.dll函數來驗證每個新進程的PE簽名;3、如果成功驗證了文件的簽名,驅動程序將解析輸出PolicyInfo結構,以提取簽名證書及其詳細信息。我們鼓勵大家嘗試使用這個repo,以初步瞭解CI,並擴大研究的範圍。

與CI鏈接

最後,我們要分析如何與這個未記錄的庫相鏈接的過程。儘管使用CI的過程看起來非常枯燥,但我們發現它並不簡單,如果大家對其中的更多函數進行擴展研究,可能需要執行與本文相同的步驟。在與特定的dll鏈接時,通常使用廠商提供的導入庫。在我們的案例中,Microsoft並沒有提供.lib文件,我們必須自己生成該文件。在生成之後,該文件應該作為鏈接器輸入添加到項目屬性中。下面是生成.lib文件所需的步驟。

64位

1、使用dumpbin實用程序從dll中獲取導出的函數:

<code>dumpbin /EXPORTS c:windowssystem32ci.dll
/<code>

2、創建一個.def文件,如下所示:LIBRARY ci.dllEXPORTSCiCheckSignedFileCiFreePolicyInfoCiValidateFileObject3、使用lib實用程序生成.lib文件:

<code>lib /def:ci.def /machine:x64 /out:ci.lib
/<code>

32位

這裡的情況比較棘手,因為在32位系統中,函數反射參數的總和(以字節為單位),例如:

<code>CiFreePolicyInfo@4
/<code>

但是ci.dll會導出沒有這部分的函數,因此我們需要創建一個.lib文件以進行這樣的轉換,所以我們使用了[3]和[4]文章中所描述的方法。1、如同64位中的第1步和第2步所述,創建一個.def文件。2、使用具有相同簽名的偽裝實體的函數stub創建一個C++文件。我們基本上可以模仿廠商從其代碼導出函數時的操作。例如:

<code>extern "C" __declspec(dllexport) 
PVOID _stdcall CiFreePolicyInfo(PVOID policyInfoPtr)
{
return nullptr;
}
/<code>

3、將其編譯成OBJ文件。4、使用lib實用工具生成.lib文件,這次使用OBJ文件:

<code>Lib /def:ci.def /machine:x86 /out:ci.lib  

/<code>

在GitHub存儲庫中,包含stub的代碼。

總結

本文演示瞭如何使用CI API中的一部分。我們通過這種方式,成功在內核模式下驗證了Authenticode簽名,而無需再自行實現。我們希望本文能為大家對這個dll的後續研究鋪平道路。在這裡,我想向對本篇文章提供幫助的幾位研究人員表示感謝,他們分別是Yuval Kovacs、Allie Mellen、Philip Tsukerman和Michael Maltsev。

參考文章

[1] Microsoft Windows FIPS 140 驗證安全策略文檔(https://csrc.nist.gov/csrc/media/projects/cryptographic-module-validation-program/documents/security-policies/140sp3093.pdf)[2] Windows驅動簽名繞過(作者:derusbi)(https://www.sekoia.fr/blog/windows-driver-signing-bypass-by-derusbi/)[3] 如何創建32位導入庫(https://qualapps.blogspot.com/2007/08/how-to-create-32-bit-import-libraries.html)[4] Q131313: 如何創建沒有.OBJ或源代碼的32位導入庫(https://jeffpar.github.io/kbarchive/kb/131/Q131313/)[5] j00ru關於CI的博客文章(https://j00ru.vexillium.org/2010/06/insight-into-the-driver-signature-enforcement/)


原文鏈接:https://www.anquanke.com/post/id/200478


分享到:


相關文章: