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的驅動程序驅動程序可以鏈接到這個模塊,并且調用一些關鍵的函數,例如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導出功能如前所述,調用CiInitialize()將會返回一個名為g_CiCallbacks的結構,其中包含更多函數(詳情請參考[1][2][5])。而其中的一個函數,CiValidateImageHeader(),將會被ntoskernel.exe用于加載驅動程序以驗證簽名的過程:
調用堆棧以在加載過程中驗證驅動程序簽名在我們的研究中,利用了導出的函數CiCheckSignedFile()以及與之交互的數據結構。稍后我們將看到,這些數據結構也出現在其他CI函數中,我們也可以將研究范圍擴展到這些其他的函數。
CiCheckSignedFile()
CiCheckSignedFile()可以接收8個參數,但目前我們還不清楚這些參數的名稱是什么。但是,通過檢查內部函數,我們可以推斷出其參數。例如,我們可以檢查MinCryptGetHashAlgorithmFromWinCertificate():
檢查WIN_CERTIFICATE的結構成員我們發現,對于WIN_CERTIFICATE結構來說,常量0x200和2是比較常見的值,該結構為我們提供了第四個和第五個參數。我們可以通過類似的方式找到其余的輸入參數。而對于輸出參數來說,則方法完全不同,我們將在后文中詳細描述。進行一些逆向之后,我們得到了函數簽名:
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);該函數的工作方法如下:1、調用方位函數提供文件摘要(緩沖區和算法類型),以及指向Authenticode簽名的指針。2、該函數通過以下方式驗證簽名和摘要:(1)遍歷文件簽名,并獲取使用特定摘要算法的簽名;(2)驗證簽名(和證書),并提取其中顯示的文件摘要;(3)將提取的摘要與調用方提供的摘要進行比較。3、除了驗證文件簽名之外,該函數還為調用方提供有關已驗證簽名的各種詳細信息。該函數后面一部分的工作原理非常值得關注,因為僅僅知道文件已經經過正確簽名是不夠的,我們還需要知道是由誰進行簽名的。在下一節中,我們將解決這一問題。
PolicyInfo結構
到目前為止,我們已經將所有輸入參數輸入到CiCheckSignedFile()并且能夠進行調用。但是,我們除了其大小(在Windows 10 x64上為0x30)之外,對于PolicyInfo結構幾乎一無所知。作為輸出參數,我們希望該結構能以某種方式提供有關簽名者身份的提示。因此,我們調用該函數,并對內存進行檢查,以確認哪些數據填充到PolicyInfo之中。在內存中,似乎包含一個地址和一些較大的數字。該結構正在內部函數MinCryptVerifyCertificateWithPolicy2()中填充:
填充PolicyInfo結構該函數中的某些代碼似乎正在檢查該值是否在特定范圍之內。對于證書驗證的過程來說,我們推測這個范圍是證書有效的時間范圍,事實上證明這是正確的:
檢查證書有效期這將引向以下結構:
typedef struct _PolicyInfo{int structSize; NTSTATUS verificationStatus; int flags; PVOID someBuffer; // later known as certChainInfo; FILETIME revocationTime; FILETIME notBeforeTime; FILETIME notAfterTime;} PolicyInfo, *pPolicyInfo;盡管證書的有效期非常值得關注,但是這并不能直接定位到簽名者。稍后我們將發現,大多數信息都位于成員certChainInfo之中,我們將在稍后討論。
CertChainInfo緩沖區
在檢查PolicyInfo的內存時,我們可以看到它指向結構外部的內存位置——動態分配的緩沖區。該分配位于I_MinCryptAddChainInfo()中,其函數名稱表明了緩沖區的用途。我們通過檢查其內存布局來逆向這一緩沖區:1、在前幾個字節中,有指向緩沖區內部各個位置的指針。2、在這些指向的位置中,存在重復的模式和指向緩沖區內部更遠位置的指針。3、在最后指向的這些位置中,我們找到了一些文本,看起來像是證書的摘要。該緩沖區中包含有關整個證書鏈的數據,既有解析格式(位于子結構中),也有原始數據格式(包含證書、密鑰、EKU的ASN.1證書)。這一部分使調用方可以輕松地查看證書的主題、頒發者、證書鏈的組成,以及用于創建每個證書的哈希算法。為了更好地解釋這個緩沖區的格式,以及我們從中得到的子結構,我們將分析其在32位計算機上的內存布局。如果使用32位計算機,可以減少混亂的情況,這里可以利用更少的填充字節來滿足對齊要求。下面是由Microsoft簽名的Notepad.exe的示例:
CertChainInfo緩沖區的內存視圖我們在這里可以發現:1、緩沖區的頂部有兩個4字節的數字。其中的一個表明在哪里可以找到一系列CertChainMember類型結構的地址,另一個是可以指示其中有多少個結構的計數器。2、第一個CertChainMember位于地址0x89BF45C8中(以黑色標出),我們將其格式化如下:(1)在CertChainMembers的末尾,以藍色標出的地址0x89BF4688處,有純文本格式的主題名稱。(2)在橙色標出的地址0x89BF4699處,有純文本格式的發行者名稱。(3)在紅色箭頭指出的地址0x89BF46BE處,包含實際證書的ASN.1 blob的開頭。內存以小端對齊的4字節為一組顯示,因此證書的前兩個字節實際上是0x3082,而不是如圖所示的0x3131。
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;這就是我們之前所說的解析數據。我們無需自行解析證書,就可以獲取到主題或頒發者。該結構中的最后一個字節指向緩沖區內部更遠的位置。接下來的96個字節包含第二個CertChainMember,出于可讀性的考慮,未將其標出。其中包含有關鏈的下一個證書的信息。對于公鑰和EKU(擴展密鑰用法)來說,存在一系列類似的指針和結構。換而言之,CI從證書中獲取了一些關鍵數據,并且使其以子結構的形式提供給調用方。但是,如果調用方還需要其他的一些內容,那么其中還可能會包括未解析的原始數據。注意:PolicyInfo和CertChainInfo結構都以結構的大小開始。由于這些結構是可以在OS版本之間實現擴展的,因此在嘗試訪問其他結構成員之前,必須要檢查這里的大小。在存儲庫中的文件ci.h中,可以找到CertChainInfo緩沖區的完整分類和各種子結構。
CiFreePolicyInfo()
該函數將釋放PolicyInfo的certChainInfo緩沖區,該緩沖區由CiCheckSignedFile()和其他填充PolicyInfo結構的CI函數分配。該函數還會重置其他結構成員。在這里,必須要對其進行調用,以避免內存泄漏。
CiFreePolicyInfo()的實現由于該函數會在內部檢查是否有可用的內存,因此即使是未填充PolicyInfo,也可以安全地對其進行調用。
CiValidateFileObject()
如前文所述,在調用CiCheckSignedFile()之前需要首先完成一些工作。調用方必須計算文件哈希值并解析PE,以便為函數提供簽名的位置。但是,函數CiValidateFileObject()可以為調用方完成這部分工作。我們不需要從頭開始,因為它與CiCheckSignedFile()共享一些參數:
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);該函數在內核空間中映射文件,并提取其簽名:
通過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中獲取導出的函數:
dumpbin /EXPORTS c:windowssystem32ci.dll2、創建一個.def文件,如下所示:LIBRARY ci.dllEXPORTSCiCheckSignedFileCiFreePolicyInfoCiValidateFileObject3、使用lib實用程序生成.lib文件:
lib /def:ci.def /machine:x64 /out:ci.lib32位
這里的情況比較棘手,因為在32位系統中,函數反射參數的總和(以字節為單位),例如:
CiFreePolicyInfo@4但是ci.dll會導出沒有這部分的函數,因此我們需要創建一個.lib文件以進行這樣的轉換,所以我們使用了[3]和[4]文章中所描述的方法。1、如同64位中的第1步和第2步所述,創建一個.def文件。2、使用具有相同簽名的偽裝實體的函數stub創建一個C++文件。我們基本上可以模仿廠商從其代碼導出函數時的操作。例如:
extern "C" __declspec(dllexport) PVOID _stdcall CiFreePolicyInfo(PVOID policyInfoPtr){ return nullptr;}3、將其編譯成OBJ文件。4、使用lib實用工具生成.lib文件,這次使用OBJ文件:
Lib /def:ci.def /machine:x86 /out:ci.lib在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
總結
以上是生活随笔為你收集整理的dll 导出函数 下划线_内核中的代码完整性:深入分析ci.dll的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 实现iframe_单点登录的三种实现方式
- 下一篇: 做diff_Virtual Dom am