PE文件的证书表(Certificate Table)是用于存储数字签名信息的关键数据结构。它通常被称为安全目录(Security Directory),是实现对可执行文件进行 Authenticode 签名的基础,使 Windows 系统能够验证文件的完整性与发布者身份,确保其未被篡改且来源可信。
证书表概述
证书表,也称为安全目录,是PE文件格式中专门存放 Authenticode 数字签名的区域。这一设计使得操作系统能够验证可执行文件的完整性和来源,从而确认文件是否被修改,以及其发布者是否可信。
证书表结构
证书表主要由以下部分组成:
1. 数据目录项
证书表在数据目录表中的位置是第5个元素(索引为4):
// 数据目录中的证书表项
DataDirectory[IMAGE_DIRECTORY_ENTRY_SECURITY]
重要提示:与其他数据目录项不同,证书表的VirtualAddress字段存储的是文件偏移量,而非RVA。
2. WIN_CERTIFICATE结构
证书表包含一个或多个WIN_CERTIFICATE结构:
typedef struct _WIN_CERTIFICATE {
DWORD dwLength; // 证书结构的总长度
WORD wRevision; // 结构版本
WORD wCertificateType; // 证书类型
BYTE bCertificate[ANYSIZE_ARRAY]; // 证书数据
} WIN_CERTIFICATE, *LPWIN_CERTIFICATE;
各字段说明:
- dwLength:整个
WIN_CERTIFICATE结构的大小(包括头部和证书数据),按8字节对齐。
- wRevision:结构版本号,通常为
WIN_CERT_REVISION_2_0(0x0200)。
- wCertificateType:证书类型,常见值如下:
WIN_CERT_TYPE_X509(0x0001):标准X.509证书。
WIN_CERT_TYPE_PKCS_SIGNED_DATA(0x0002):PKCS#7签名数据。
WIN_CERT_TYPE_RESERVED_1(0x0003):保留类型。
- bCertificate:实际的证书或签名数据。
证书类型
Windows支持多种证书格式:
- X.509证书:标准的数字证书格式。
- PKCS#7签名数据:最常见的类型,包含完整的签名信息。
- PKCS#1签名数据:较少使用。
目前,绝大多数Windows可执行文件都采用PKCS#7签名数据格式。
证书表在PE文件中的位置
证书表的位置和大小信息存储在可选头的数据目录数组中,索引为IMAGE_DIRECTORY_ENTRY_SECURITY(值为4):
// 数据目录中的安全目录项
DataDirectory[IMAGE_DIRECTORY_ENTRY_SECURITY]
Authenticode签名过程
Authenticode签名的生成过程主要包括以下步骤:
- 计算文件的哈希值(需排除证书表部分)。
- 创建包含文件哈希、发布者证书等信息的PKCS#7签名数据。
- 将签名数据附加到PE文件的末尾,形成证书表。
- 更新数据目录中证书表项的信息。
在计算文件哈希时,以下部分会被排除:
- 可选头中的
CheckSum字段。
- 数据目录表中证书表项的内容。
- 证书表数据区本身。
实际示例
以下是读取并解析PE文件证书表的示例代码。通过调用系统底层的 Win32 API,我们可以深入理解文件签名的验证机制。
#include <windows.h>
#include <stdio.h>
#include <wintrust.h>
#include <softpub.h>
// 证书类型定义
#define WIN_CERT_REVISION_1_0 0x0100
#define WIN_CERT_REVISION_2_0 0x0200
#define WIN_CERT_TYPE_X509 0x0001
#define WIN_CERT_TYPE_PKCS_SIGNED_DATA 0x0002
#define WIN_CERT_TYPE_RESERVED_1 0x0003
void PrintCertificateInfo(PWIN_CERTIFICATE cert) {
printf("=== 证书信息 ===\n");
printf("证书长度: %lu 字节\n", cert->dwLength);
printf("版本: 0x%04X\n", cert->wRevision);
printf("证书类型: 0x%04X ", cert->wCertificateType);
switch(cert->wCertificateType) {
case WIN_CERT_TYPE_X509:
printf("(X.509 证书)\n");
break;
case WIN_CERT_TYPE_PKCS_SIGNED_DATA:
printf("(PKCS#7 签名数据)\n");
break;
case WIN_CERT_TYPE_RESERVED_1:
printf("(保留类型)\n");
break;
default:
printf("(未知类型)\n");
break;
}
printf("证书数据大小: %lu 字节\n", cert->dwLength - sizeof(WIN_CERTIFICATE));
}
void PrintCertificateTable(const char* filename) {
HANDLE hFile = CreateFile(filename, GENERIC_READ, FILE_SHARE_READ,
NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if(hFile == INVALID_HANDLE_VALUE) {
printf("无法打开文件: %s\n", filename);
return;
}
HANDLE hMapping = CreateFileMapping(hFile, NULL, PAGE_READONLY, 0, 0, NULL);
if(!hMapping) {
printf("创建文件映射失败\n");
CloseHandle(hFile);
return;
}
LPVOID lpBase = MapViewOfFile(hMapping, FILE_MAP_READ, 0, 0, 0);
if(!lpBase) {
printf("映射文件失败\n");
CloseHandle(hMapping);
CloseHandle(hFile);
return;
}
// 获取DOS头
PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)lpBase;
// 验证DOS签名
if(dosHeader->e_magic != IMAGE_DOS_SIGNATURE) {
printf("不是有效的PE文件\n");
UnmapViewOfFile(lpBase);
CloseHandle(hMapping);
CloseHandle(hFile);
return;
}
// 获取NT头
PIMAGE_NT_HEADERS ntHeaders = (PIMAGE_NT_HEADERS)((LPBYTE)lpBase + dosHeader->e_lfanew);
// 验证PE签名
if(ntHeaders->Signature != IMAGE_NT_SIGNATURE) {
printf("不是有效的PE文件\n");
UnmapViewOfFile(lpBase);
CloseHandle(hMapping);
CloseHandle(hFile);
return;
}
// 获取证书表信息
PIMAGE_DATA_DIRECTORY certDir =
&ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_SECURITY];
if(certDir->VirtualAddress == 0) {
printf("该文件没有数字签名\n");
UnmapViewOfFile(lpBase);
CloseHandle(hMapping);
CloseHandle(hFile);
return;
}
// 注意:证书表使用文件偏移而不是RVA
PWIN_CERTIFICATE cert = (PWIN_CERTIFICATE)((LPBYTE)lpBase + certDir->VirtualAddress);
printf("=== 证书表信息 ===\n");
printf("证书表文件偏移: 0x%08X\n", certDir->VirtualAddress);
printf("证书表大小: %d 字节\n", certDir->Size);
// 遍历证书表中的所有证书
DWORD offset = 0;
int certIndex = 0;
while(offset < certDir->Size) {
PWIN_CERTIFICATE currentCert = (PWIN_CERTIFICATE)((LPBYTE)cert + offset);
if(currentCert->dwLength == 0) {
break;
}
printf("\n证书 %d:\n", certIndex++);
PrintCertificateInfo(currentCert);
// 按8字节对齐
offset += ((currentCert->dwLength + 7) / 8) * 8;
}
UnmapViewOfFile(lpBase);
CloseHandle(hMapping);
CloseHandle(hFile);
}
// 使用Windows API验证文件签名
BOOL VerifyFileSignature(const char* filename) {
WINTRUST_FILE_INFO fileInfo = {0};
fileInfo.cbStruct = sizeof(WINTRUST_FILE_INFO);
fileInfo.pcwszFilePath = (LPCWSTR)filename;
fileInfo.hFile = NULL;
fileInfo.pgKnownSubject = NULL;
WINTRUST_DATA trustData = {0};
trustData.cbStruct = sizeof(WINTRUST_DATA);
trustData.pPolicyCallbackData = NULL;
trustData.pSIPClientData = NULL;
trustData.dwUIChoice = WTD_UI_NONE;
trustData.fdwRevocationChecks = WTD_REVOKE_NONE;
trustData.dwUnionChoice = WTD_CHOICE_FILE;
trustData.dwStateAction = WTD_STATEACTION_VERIFY;
trustData.hWVTStateData = NULL;
trustData.pwszURLReference = NULL;
trustData.dwProvFlags = WTD_SAFER_FLAG;
trustData.dwUIContext = 0;
trustData.pFile = &fileInfo;
GUID actionId = WINTRUST_ACTION_GENERIC_VERIFY_V2;
LONG result = WinVerifyTrust(NULL, &actionId, &trustData);
switch(result) {
case ERROR_SUCCESS:
printf("文件签名验证成功\n");
return TRUE;
case TRUST_E_NOSIGNATURE:
printf("文件没有数字签名\n");
return FALSE;
case TRUST_E_EXPLICIT_DISTRUST:
printf("文件签名被明确拒绝\n");
return FALSE;
case TRUST_E_SUBJECT_NOT_TRUSTED:
printf("文件签名不被信任\n");
return FALSE;
default:
printf("文件签名验证失败,错误码: 0x%08X\n", result);
return FALSE;
}
}
证书表的重要性
证书表在Windows安全体系中扮演着核心角色:
- 文件完整性验证:通过数字签名验证文件自签名后是否被篡改。
- 来源验证:确认文件发布者的身份,建立信任链。
- 恶意软件防护:帮助安全软件和系统策略识别、阻止未签名或签名无效的可疑程序。
- 企业安全策略:支持"仅允许运行已签名应用"等组策略的实施。
- 驱动程序验证:Windows内核要求加载的驱动程序必须具有有效的数字签名。
总结
证书表是PE文件格式中用于存储数字签名信息的关键结构。它通过WIN_CERTIFICATE结构存储PKCS#7签名数据,使得Windows能够验证可执行文件的完整性和来源。深入理解证书表的结构、存放位置(使用文件偏移而非RVA)以及在哈希计算中被排除的特性,对于软件安全开发、恶意软件分析及数字签名验证等工作至关重要。掌握这些底层数据结构和解析逻辑是进行系统安全研究的坚实基础。