1. 程式人生 > >對Windows 平臺下PE檔案數字簽名的一些研究

對Windows 平臺下PE檔案數字簽名的一些研究

Windows平臺上PE檔案的數字簽名有兩個作用:確保檔案來自指定的釋出者和檔案被簽名後沒有被修改過。因此有些軟體用數字簽名來驗證檔案是否來自家廠商以及檔案的完整性,安全軟體也經常通過驗證檔案是否有數字簽名來防誤報。但是因為windows對於常用的數字簽名驗證API- WinVerifyTrust實現的問題,以及一些並不恰當的示例程式碼,很多地方在驗證數字簽名時存在卡慢或者安全性不高的問題。本文將介紹數字簽名的常見使用方法、常見的驗證程式碼、驗證原理、以及在使用過程中碰到的問題,並且提出一種在某些場景下安全性更高,速度更快的驗證方法,以及其他的一點小東西。有些地方並未完全搞明白,估計有些錯誤,如有發現的話,歡迎指出,但是噴的時候輕點啊。

目錄

1           如何使用工具給檔案加簽名驗簽名

1.1   生成證書

平時我們對PE檔案加簽名的時候使用的是從CA簽發的證書,但是我們自己測試的時候也可以用微軟的證書生成工具makecert可以生成測試用證書。下面的命令第一行成了一個自簽名的根證書,然後用根證書籤發一個子證書。

D:\sign_test>makecert -n "cn=root" -r -sv test_root.pvk test_root.cer
Succeeded
D:\sign_test>makecert -n "cn=child" -iv test_root.pvk -ic test_root.cer -
sv test_child.pvk test_child.cer Succeeded

當然這種證書是不被系統所信任的,需要手動把生成的證書匯入到系統。

1.2   嵌入式簽名

使用微軟的signtool工具可以對PE檔案進行嵌入式簽名,執行signtool signwizard即可開啟GUI介面進行嚮導式簽名。

簽名後的效果圖:

1.3   編錄簽名

編錄簽名是將簽名資料放到一個字尾為.cat的編錄檔案中,並不嵌入到PE檔案中,所以右鍵檢視檔案屬性是看不到數字簽名這個標籤的。這種簽名方法可以對任意格式的檔案簽名,並不侷限於PE檔案。微軟的系統檔案基本都是用這種方式簽名的,以至於有人會將這個稱為微軟編錄簽名,其實這種方式不只微軟可以使用。

①,建立一個cdf檔案,以簽名7zFM.exe為例:

[CatalogHeader]Name=softsigntest.cat[CatalogFiles]<hash>7zFM=7zFM.exe

②,從上一步建立的cdf檔案生成cat檔案

C:\signtest&gt;makecat -v softsigntest.cdf
opened:    softsigntest.cdf
processing: 7zFM
Succeeded

③,執行signtool signwizard對softsigntest.cat進行簽名。

④,現在驗證PE檔案的簽名的話,還不能通過驗證,需要把cat檔案匯入到系統中。

C:\signtest&gt;signtool verify -v -pa -a 7zFM.exe
 
Verifying: 7zFM.exe
File is signed in catalog: C:\WINDOWS\system32\CatRoot\{F750E6C3-38EE-11D1-85E5-
00C04FC295EE}\softsigntest.cat
Signing Certificate Chain:
    Issued to: root
    Issued by: root
    Expires:   2040-1-1 7:59:59
    SHA1 hash: B858A2990D04DED1C72334C9764CB9B0F15DCCC8
 
        Issued to: child
        Issued by: root
        Expires:   2040-1-1 7:59:59
        SHA1 hash: 325ADFF8533B319DD3EF98DB1896FB46456DCD18
 
File is not timestamped.
Successfully verified: 7zFM.exe
 
Number of files successfully Verified: 1
Number of warnings: 0
Number of errors: 0

2          常見的驗證簽名程式碼

BOOL CheckFileTrust( LPCWSTR lpFileName )
{
	BOOL bRet = FALSE;
	WINTRUST_DATA wd = { 0 };
	WINTRUST_FILE_INFO wfi = { 0 };
	WINTRUST_CATALOG_INFO wci = { 0 };
	CATALOG_INFO ci = { 0 };
 
	HCATADMIN hCatAdmin = NULL;
	if ( !CryptCATAdminAcquireContext( &amp;hCatAdmin, NULL, 0 ) )
	{
		return FALSE;
	}
 
	HANDLE hFile = CreateFileW( lpFileName, GENERIC_READ, FILE_SHARE_READ,
		NULL, OPEN_EXISTING, 0, NULL );
	if ( INVALID_HANDLE_VALUE == hFile )
	{
		CryptCATAdminReleaseContext( hCatAdmin, 0 );
		return FALSE;
	}
 
	DWORD dwCnt = 100;
	BYTE byHash[100];
	CryptCATAdminCalcHashFromFileHandle( hFile, &amp;dwCnt, byHash, 0 );
	CloseHandle( hFile );
 
	LPWSTR pszMemberTag = new WCHAR[dwCnt * 2 + 1];
	for ( DWORD dw = 0; dw &lt; dwCnt; ++dw )
	{
		wsprintfW( &amp;pszMemberTag[dw * 2], L"%02X", byHash[dw] );
	}
 
	HCATINFO hCatInfo = CryptCATAdminEnumCatalogFromHash( hCatAdmin,
		byHash, dwCnt, 0, NULL );
	if ( NULL == hCatInfo )   // 編錄中沒有則驗證是否有嵌入式簽名
	{
		wfi.cbStruct       = sizeof( WINTRUST_FILE_INFO );
		wfi.pcwszFilePath  = lpFileName;
		wfi.hFile          = NULL;
		wfi.pgKnownSubject = NULL;
 
		wd.cbStruct            = sizeof( WINTRUST_DATA );
		wd.dwUnionChoice       = WTD_CHOICE_FILE;
		wd.pFile               = &amp;wfi;
		wd.dwUIChoice          = WTD_UI_NONE;
		wd.fdwRevocationChecks = WTD_REVOKE_NONE;
		wd.dwStateAction       = WTD_STATEACTION_IGNORE;
		wd.dwProvFlags         = WTD_SAFER_FLAG;
		wd.hWVTStateData       = NULL;
		wd.pwszURLReference    = NULL;
	}
	else  // 編錄中有,驗證編錄檔案的簽名是否有效
	{
		CryptCATCatalogInfoFromContext( hCatInfo, &amp;ci, 0 );
		wci.cbStruct             = sizeof( WINTRUST_CATALOG_INFO );
		wci.pcwszCatalogFilePath = ci.wszCatalogFile;
		wci.pcwszMemberFilePath  = lpFileName;
		wci.pcwszMemberTag       = pszMemberTag;
 
		wd.cbStruct            = sizeof( WINTRUST_DATA );
		wd.dwUnionChoice       = WTD_CHOICE_CATALOG;
		wd.pCatalog            = &amp;wci;
		wd.dwUIChoice          = WTD_UI_NONE;
		wd.fdwRevocationChecks = WTD_STATEACTION_VERIFY;
		wd.dwProvFlags         = 0;
		wd.hWVTStateData       = NULL;
		wd.pwszURLReference    = NULL;
	}
	GUID action = WINTRUST_ACTION_GENERIC_VERIFY_V2;
	HRESULT hr  = WinVerifyTrust( NULL, &amp;action, &amp;wd );
	bRet        = SUCCEEDED( hr );
 
	if ( NULL != hCatInfo )
	{
		CryptCATAdminReleaseCatalogContext( hCatAdmin, hCatInfo, 0 );
	}
	CryptCATAdminReleaseContext( hCatAdmin, 0 );
	delete[] pszMemberTag;
	return bRet;
}

這段網上的程式碼基本流程是沒有問題的,很多同學自用驗簽名程式碼或許也是從這個抄來的。但是這裡也有幾個小毛病:

①,如果只是要驗證嵌入式簽名的話,對於沒有簽名或者簽名非嵌入式的檔案這裡會白白進行了整個檔案的讀取和HASH計算。這段網上的程式碼基本流程是沒有問題的,很多同學自用驗簽名程式碼或許也是從這個抄來的。但是這裡也有幾個小毛病:

②,有個特殊情況,檔案被進行嵌入式簽名以後還是可以進行編錄簽名。如果編錄簽名失效而嵌入式簽名有效的話,這裡會返回驗證失敗。呃,當然,估計一般人沒毛病也不會在一個檔案上做兩種方式的簽名。

③,這裡似乎有一處筆誤。WINTRUST_DATA::fdwRevocationChecks的賦值在編錄簽名和嵌入式簽名的分支不一樣,而且WTD_STATEACTION_VERIFY是給dwStateAction填的列舉值,雖然值是一樣的,但這麼還是不妥。看到這裡的同學可以看看自己的驗證程式碼是不是從這裡抄來的,是不是錯的一樣。

3         驗證原理

3.1  嵌入式簽名

數字簽名的驗證在取出簽名後大概分為這幾個步驟,首先從PE檔案中取出數字簽名,然後校驗檔案本身的簽名,然後是校驗證書鏈一直到根證書,最後對比檔案摘要是否與數字簽名中攜帶的一致。下面的描述僅針對PE檔案的數字簽名驗證。先上一張微軟文件裡面的圖,大概說明了數字簽名在PE檔案中所處的位置,以及自身的格式:

12

①,取出數字簽名

PE頭的Data Directories中Certificate Table裡面指明瞭WIN_CERTIFICATE的存放位置和大小,WIN_CERTIFICATE的bCertificate就是是SignedData格式的簽名。

typedef struct _IMAGE_DATA_DIRECTORY {
	DWORD   VirtualAddress;  // PE檔案的偏移
	DWORD   Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
 
typedef struct _WIN_CERTIFICATE
{
	DWORD       dwLength; // WIN_CERTIFICATE 的長度(含bCertificate的大小)
	WORD        wRevision;
	WORD        wCertificateType;
	BYTE        bCertificate[ANYSIZE_ARRAY]; // signedData開始的位置
 
} WIN_CERTIFICATE, *LPWIN_CERTIFICATE;

②,校驗檔案本身的簽名

檔案簽名本身是遵循PKCS7標準中的SignedData格式,用ASN1表述的格式如下:

SignedData ::= SEQUENCE {
  version Version,
  digestAlgorithms DigestAlgorithmIdentifiers,
  contentInfo ContentInfo,  -- 這個裡面包含了PE檔案的Hash
  certificates  --證書的陣列(不包括根證書)
    	[0] IMPLICIT ExtendedCertificatesAndCertificates
       OPTIONAL,
  Crls
    [1] IMPLICIT CertificateRevocationLists OPTIONAL,
  signerInfos SignerInfos }  -- 簽名者的資訊
 
SignerInfos ::= SET OF SignerInfo

PKCS7是加密訊息的語法標準,後面會提的X509是證書的格式。ASN1是一種描述物件結構的語法,在一行的定義中可以簡單的認為前面的是變數名後面的是型別。ASN1並未定義編碼方法,後面會提到的DER是一種常見的編碼方法。

SignerInfos的結構如下:

SignerInfo ::= SEQUENCE {
  version Version,
  issuerAndSerialNumber IssuerAndSerialNumber,
  digestAlgorithm DigestAlgorithmIdentifier,
  authenticatedAttributes -- 內含SignedData中contentInfo的摘要
    [0] IMPLICIT Attributes OPTIONAL,
  digestEncryptionAlgorithm
    DigestEncryptionAlgorithmIdentifier,
  encryptedDigest EncryptedDigest, -- 加密後的摘要
  unauthenticatedAttributes
    [1] IMPLICIT Attributes OPTIONAL }
IssuerAndSerialNumber ::= SEQUENCE {
  issuer Name,
  serialNumber CertificateSerialNumber }
EncryptedDigest ::= OCTET STRING

authenticatedAttributes包含了contentType和messageDigest,messageDigest內就是對SignedData的ContentInfo做的摘要。對authenticatedAttributes做摘要得到一個DigestInfo結構的資料,DigestInfo的結構如下:

   DigestInfo ::= SEQUENCE {
     digestAlgorithm DigestAlgorithmIdentifier,
     digest Digest }
 
   Digest ::= OCTET STRING

用IssuerAndSerialNumber找到簽名者的證書,使用裡面的公鑰解密EncryptedDigest得到一個DigestInfo結構(一般是RSA演算法),將這個結構與authenticatedAttributes做摘要得到的結構對比,一致的話才進行下一步。

③,驗證證書鏈

相關結構如下:

   -- X509的證書格式
   Certificate  ::=  SEQUENCE  {
        tbsCertificate       TBSCertificate,   -- 證書主體
        signatureAlgorithm   AlgorithmIdentifier,  -- 簽名用的演算法,一般為sha1RSA
        signatureValue       BIT STRING  }     -- 證書的簽名
 
   TBSCertificate  ::=  SEQUENCE  {
        version         [0]  EXPLICIT Version DEFAULT v1, -- PE檔案數字簽名用的版本為3
        serialNumber         CertificateSerialNumber,
        signature            AlgorithmIdentifier,
        issuer               Name,
        validity             Validity, -- 有效期
        subject              Name,
        subjectPublicKeyInfo SubjectPublicKeyInfo,  -- 含有這個證書的公鑰
        issuerUniqueID  [1]  IMPLICIT UniqueIdentifier OPTIONAL,
                             -- If present, version MUST be v2 or v3        
        subjectUniqueID [2]  IMPLICIT UniqueIdentifier OPTIONAL,
                             -- If present, version MUST be v2 or v3
        extensions      [3]  EXPLICIT Extensions OPTIONAL  -- 擴充套件
                             -- If present, version MUST be v3
        }
 
   -- 含有公鑰的資訊
   SubjectPublicKeyInfo  ::=  SEQUENCE  {
        algorithm            AlgorithmIdentifier,
        subjectPublicKey     BIT STRING  }

首先構建證書鏈,從終端簽發數字簽名的證書一直到自簽名的根證書。這時要了解到證書最後一個成員為擴充套件,擴充套件是一列其他的資料,其中兩項比較重要的是AuthorityKeyIdentifier和SubjectKeyIdentifier,結構分別如下

   AuthorityKeyIdentifier ::= SEQUENCE {
      keyIdentifier             [0] KeyIdentifier           OPTIONAL,
      authorityCertIssuer       [1] GeneralNames            OPTIONAL,
      authorityCertSerialNumber [2] CertificateSerialNumber OPTIONAL  }
 
   KeyIdentifier ::= OCTET STRING
 
   SubjectKeyIdentifier ::= KeyIdentifier

組建證書鏈時,將把A證書的中的AuthorityKeyIdentifier(簡稱AKID) 的keyIdentifier、authorityCertIssuer、authorityCertSerialNumber與B證書的SubjectKeyIdentifier(簡稱SKID)、issuer 、serialNumber分別匹配,如果匹配上則B證書為A證書的簽發者。如果A證書的上面三項與自己對應資料匹配上,則A證書為自簽名的證書,證書鏈構建完畢。

C:\Users\MTIANY~1\AppData\Local\Temp\ScreenClip.png

然後,校驗證書鏈中每個證書的簽名、有效期和用法(是否可以用於程式碼簽名)。簽名驗證的演算法為證書中的signatureAlgorithm,簽名是signatureValue,被簽名的資料為tbsCertificate,公鑰從父證書的subjectPublicKeyInfo裡面拿。

④,計算PE檔案的Hash,並與簽名資料中的Hash對比。

簽名資料中的Hash演算法和Hash在SignedData的contentInfo中,contentinfo的結構為:

ContentInfo ::= SEQUENCE {
  contentType ContentType,
  content
    [0] EXPLICIT ANY DEFINED BY contentType OPTIONAL 
}

contentType 是SPC_INDIRECT_DATA_OBJID (1.3.6.1.4.1.311.2.1.4),表明了content的型別。content是是一個SpcIndirectDataContent結構的資料。

SpcIndirectDataContent ::= SEQUENCE {
    data                    SpcAttributeTypeAndOptionalValue,
    messageDigest           DigestInfo
} --#public—
 
DigestInfo ::= SEQUENCE {
    digestAlgorithm     AlgorithmIdentifier,
    digest              OCTETSTRING
}

digestAlgorithm就是Hash演算法,一般為sha1。digest就是檔案的Hash。

Hash的計算原則為排除且僅排除掉簽名過程中可能會改動的資料以及數字簽名本身。大概計算過程如下:

除去PE頭中checksum和Certificate Table計算PE頭的HASH(含 Section Table),按每個節偏移的順序依次對每個節的資料算HASH,對PE附加資料算HASH。附加資料的起始偏移為PE頭大小+每個節的大小,附加資料大小=檔案大小-(PE頭+每個節)-簽名的大小,簽名的大小是Optional Header Data Directories[Certificate Table].Size。

另外,由這個Hash演算法可以看出,PE檔案的簽名資料都是放到PE檔案最尾部的,因為只有附加資料最末尾一段為簽名資料大小的資料是沒有計算在PE的Hash內的。

3.2  編錄簽名

編錄檔案的簽名微軟並未公開格式文件,所以大概就是靠微軟一些邊角的資料去猜。

編錄檔案的簽名資料格式應該是跟嵌入式簽名一樣的。這裡說下不一樣的地方,微軟編錄檔案的簽名和PE檔案是分開的,cat檔案中存放著檔案的Hash,而且可以存放多個檔案的Hash,對cat檔案中的多個Hash進行了一個簽名,懷疑微軟就是為了這個能節約體積做出來編錄簽名這個東西。編錄簽名匯入系統後存放在%windir%\system32\catroot。但是這個在驗證上就帶來了問題,驗證某個檔案時,那麼多cat檔案都要開啟看下有沒有這個檔案的Hash?效率太低了吧,微軟似乎有個很聰明的作法,做個服務Cryptographic Services都給載入起來嘛,驗證的時候跨程序通訊來找我。呃,這些都是我猜的,誰知道的話,請告訴我。

C:\Users\mtian\AppData\Local\Temp\ScreenClip.png

4         使用時遇到的問題

4.1  WinVerifyTrust API的卡慢

WinVerifyTrust,這玩意裡面的問題很多。目前已知有在以下幾個地方可能有問題。

①,驗證CRL(銷證書列表)時使用的WinINet系列API效能不好。這個系列的API是出了名的不穩定,而且據說可能會導致堆破壞,實際使用中我們也發現過因為這裡導致卡死的情況,但是找不到DUMP了,└(T_T;)┘。

②,列舉證書時的卡死,這是我們拿到的一個DUMP,此處卡了近10分鐘。

1

③,如果驗證編錄簽名的話,我們曾經發現過一個跨程序通訊的卡死,在呼叫棧中可以看到驗證簽名的執行緒的棧頂上有幾個執行緒有RPC的字樣。

④,列舉簽名時的死迴圈。據說在XP系統上WinVerfiyTrust在校驗PE檔案嵌入式簽名時,迴圈列舉簽名時沒有判斷簽名的長度,如果PE檔案被破壞,簽名長度為0的話,WinVerfiyTrust會陷入死迴圈。

4.2  安全性

4.2.1              惡意軟體可能會匯入證書到系統中

惡意軟體是在執行後可以通過CertAddCertificateContextToStore自己匯入根證書,雖然windows系統會彈出警告框,但是可以模擬點選滑鼠來點選警告框的確認。

下面是一個效果圖:

自己匯入證書的動畫

4.2.2              濫發的證書

一般的CA都是有基本的安全概念的,對於證書的頒發都會比較謹慎。但是某些有途徑向使用者電腦插入根證書的廠商對於證書的頒發並不謹慎,比如工行網銀的U盾中的證書並未限制用途,可以給PE檔案簽名。見下圖

C:\Users\mtian\Desktop\icbc.jpg

支付寶以前也爆過這個漏洞,現在已經被補了,下面這個是烏雲上的截圖。

C:\Users\mtian\Desktop\11144822911e60e1fb23e9f91025d17d916bf90f.jpg

這種證書籤名的檔案在對應裝了工行或者支付寶根證書的機器上都是可以通過數字簽名驗證的,但是由於其證書是頒發給個人的,所以檔案並不能認為是安全的。

5         一種更加快速安全的驗證方法和一段驗證程式碼

上面說了了數字簽名驗證在實際使用中的不少問題,那麼,如果想快速驗證簽名而且保證安全的話,那麼要怎麼辦呢?提出的問題主要是在於兩點,一個是WinVerfiyTrust的卡慢問題,一個是證書被濫用或者使用者環境被汙染的問題。那麼,要搞的話,可以寫驗證簽名的程式碼,並且帶一個可信的根證書列表下去,驗證通過後,將整個證書鏈所有證書的資訊都通過自己的CS協議傳送到自己的後臺來驗證證書是否可信,這樣我們還可以通過後臺來幹掉吊銷列表中的證書。

只是對於PE檔案的的簽名校驗,卻有不少程式碼要寫,包括簽名資料ASN1編碼格式資料的解析,簽名的驗證和證書鏈的驗證,但是這種通用的程式碼我們可以從開源庫裡去找比如OpenSSL,下面是一段東拼西湊抄來的程式碼,可以驗證PE檔案的數字簽名,但是這段程式碼很粗糙,有些地方並未完全搞明白,而且為了縮短篇幅去掉了大部分錯誤處理,輕噴,只是為了試下這條路能不能走通。

do 
{
	LPWIN_CERTIFICATE pCert;
	// .. 省略程式碼,從PE檔案獲取pCert 的地址	 
	const unsigned char* pCertificate = pCert-&gt;bCertificate; 
	DWORD dwPKCS7Len = pCert-&gt;dwLength - offsetof(WIN_CERTIFICATE, bCertificate);
	PKCS7* pPkcs7 = d2i_PKCS7(NULL, &amp;pCertificate, dwPKCS7Len);  // DER編碼轉換為openssl的內部格式
 
 
	// 獲取PE檔案的摘要,這個可以直接呼叫CryptCATAdminCalcHashFromFileHandle,也可以按照文件的描述來自己計算
	CAutoVectorPtr pSignSha1;
	DWORD dwSignSha1Len = 0;
	GetFileHashForSign(lpszPEPath, pSignSha1, dwSignSha1Len);
 
	// 對比檔案的HASH與簽名中的hash,抄來的程式碼
	BOOL bMathc = IsHashMatch(pPkcs7, pSignSha1, dwSignSha1Len);
	if (!bTmp)
	{
		cout &lt;&lt; endl &lt;&lt; "PE Image Hash dismatch" &lt;&lt; endl;
		break;
	}
	cout &lt;&lt; "PE Image Hash match" &lt;&lt; endl;
 
	// 把根證書放到X509_STORE中
	X509_STORE *pX509Store = X509_STORE_new();
	AddRootCerToStore(pX509Store, lpszCertPath);
	X509_STORE_set_purpose(pX509Store, X509_PURPOSE_ANY);
 
	// PKCS7_verify 驗證的時候是把內容當做V_ASN1_OCTET_STRING 來驗證的,
	// 但PE檔案的數字簽名裡這裡是一個V_ASN1_SEQUENCE
	// 所以需要手動把V_ASN1_SEQUENCE 的內容取出來放到BIO 裡面給PKCS7_verify 來驗證
	// 畢竟openssl不是為了PE檔案數字簽名驗證寫的,用起來還是有點彆扭的
	cout &lt;&lt; "Verifying PKCS #7..."; 	int seqhdrlen = asn1_simple_hdr_len(pPkcs7-&gt;d.sign-&gt;contents-&gt;d.other-&gt;value.sequence-&gt;data,
		pPkcs7-&gt;d.sign-&gt;contents-&gt;d.other-&gt;value.sequence-&gt;length);
	BIO* pContentBio = BIO_new_mem_buf(pPkcs7-&gt;d.sign-&gt;contents-&gt;d.other-&gt;value.sequence-&gt;data + seqhdrlen,
		pPkcs7-&gt;d.sign