概述
在 Windows 平台上,数字签名是验证软件可信度的重要机制。然而,在某些场景下,开发者需要向已签名的 PE 文件中嵌入额外信息(如配置数据、版本标识等),同时又不能破坏原有的数字签名。本文将介绍如何利用 PE 文件签名机制的特性,实现这一目标。
技术背景
数字签名的作用
数字签名为软件安全提供了三重保障:
- 身份认证:通过证书验证软件发布者的真实身份
- 完整性验证:确保文件在签名后未被篡改
- 信任建立:提升用户对软件的信任度,避免安全警告
PE 文件签名机制
Windows PE 文件采用 Authenticode 签名机制,其核心流程包括:
- 哈希计算:对 PE 文件的特定部分计算哈希值(排除某些字段以允许签名嵌入)
- 签名生成:使用私钥对哈希值进行签名,生成
WIN_CERTIFICATE 结构
- 签名嵌入:将签名数据嵌入到 PE 文件的安全目录(Security Directory)中
- 签名验证:系统使用公钥验证签名,确保文件完整性
哈希计算的排除区域
关键在于:Authenticode 在计算文件哈希时,会排除以下三个部分:[1][2]
- PE Header 中的 CheckSum 字段:该字段会因文件内容变化而改变
- Certificate Table Entry:位于数据目录表的第 4 项(
IMAGE_DIRECTORY_ENTRY_SECURITY),记录签名数据的位置和大小
- 签名数据本身:即
WIN_CERTIFICATE 结构及其内容
这种设计使得签名可以嵌入文件本身,而不会导致”签名包含自身”的循环依赖问题。
实现原理
方法一:在 WIN_CERTIFICATE 之后追加数据
由于签名验证时只会读取 WIN_CERTIFICATE 结构中 dwLength 字段指定的长度,因此在该结构之后追加的数据不会被包含在哈希计算中,从而不影响签名有效性。[3][4]
实现要点
- 修改 Security Directory Entry 的 Size 字段
- 将
IMAGE_DIRECTORY_ENTRY_SECURITY.Size 增加为:原签名长度 + 附加数据长度
- 注意:附加数据大小需要填充到 8 字节对齐
- 保持 WIN_CERTIFICATE 结构不变
dwLength 字段保持原值,仍指向原始签名数据的长度
- 这样签名验证时不会读取附加的数据
- 安全性考量
- Windows 默认不会将这种附加数据视为签名失效
- 微软曾提供注册表选项(
EnableCertPaddingCheck)来检测这种行为,但该选项默认禁用以保持兼容性[2]
示意图说明
1 2 3 4 5 6 7 8 9 10 11 12
| PE File Structure: ├─ DOS Header ├─ PE Header ├─ Section Headers ├─ Sections (.text, .data, etc.) └─ Security Directory ├─ WIN_CERTIFICATE (签名数据) │ ├─ dwLength: 原始签名长度 │ ├─ wRevision: WIN_CERT_REVISION_2_0 │ ├─ wCertificateType: WIN_CERT_TYPE_PKCS_SIGNED_DATA │ └─ bCertificate: 签名内容 └─ [附加数据区域] ← 可在此处添加自定义数据
|
方法二:利用 PKCS#7 的 unauthenticated 字段
PKCS#7 签名结构中包含一个 unauthenticated 属性字段,该字段不参与签名计算。虽然此字段原本用于存储时间戳,但实际上可以填写任意数据。[5]
知名软件如 Dropbox 的安装程序就采用了这种技术来嵌入配置信息。
代码示例
工具使用
使用 Didier Stevens 开发的 disitool.py 工具可以方便地向 PE 文件注入数据:[6]
1
| python [disitool.py](http://disitool.py/) inject --paddata source.exe data.txt output.exe
|
读取附加数据的 C++ 实现
以下代码演示如何在运行时读取附加在 WIN_CERTIFICATE 之后的数据:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159
| #include <windows.h> #include <wintrust.h> #include <stdio.h>
BOOL GetSecurityDirectoryEntry( LPBYTE map, PIMAGE_DATA_DIRECTORY* securityDir, PDWORD securitySize) { *securityDir = nullptr; *securitySize = 0;
auto dosHeader = (PIMAGE_DOS_HEADER)map; if (dosHeader->e_magic != IMAGE_DOS_SIGNATURE) return FALSE;
auto headers = (PIMAGE_NT_HEADERS)(map + dosHeader->e_lfanew); if (headers->Signature != IMAGE_NT_SIGNATURE) return FALSE;
WORD magic = headers->OptionalHeader.Magic;
if (magic == IMAGE_NT_OPTIONAL_HDR32_MAGIC) { auto headers32 = (PIMAGE_NT_HEADERS32)(map + dosHeader->e_lfanew);
if (headers32->OptionalHeader.NumberOfRvaAndSizes > IMAGE_DIRECTORY_ENTRY_SECURITY) { *securityDir = &headers32->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_SECURITY]; *securitySize = headers32->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_SECURITY].Size; } } else if (magic == IMAGE_NT_OPTIONAL_HDR64_MAGIC) { auto headers64 = (PIMAGE_NT_HEADERS64)(map + dosHeader->e_lfanew);
if (headers64->OptionalHeader.NumberOfRvaAndSizes > IMAGE_DIRECTORY_ENTRY_SECURITY) { *securityDir = &headers64->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_SECURITY]; *securitySize = headers64->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_SECURITY].Size; } } else { return FALSE; }
if (*securityDir && (*securityDir)->VirtualAddress != 0 && (*securityDir)->Size != 0) { return TRUE; }
return FALSE; }
void ReadAuthenticodeTail(PCWSTR path, LPBYTE* tailData, LPDWORD tailSize) { *tailData = nullptr; *tailSize = 0; PIMAGE_DATA_DIRECTORY securityDir = nullptr; DWORD securitySize = 0;
auto file = CreateFile(path, GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, 0, nullptr); if (file == INVALID_HANDLE_VALUE) return;
auto mapping = CreateFileMapping(file, nullptr, PAGE_READONLY, 0, 0, nullptr); if (!mapping) { CloseHandle(file); return; }
auto map = (LPBYTE)MapViewOfFile(mapping, FILE_MAP_READ, 0, 0, 0); if (!map) { CloseHandle(mapping); CloseHandle(file); return; }
if (GetSecurityDirectoryEntry(map, &securityDir, &securitySize)) { auto va = securityDir->VirtualAddress;
if (va + securitySize > GetFileSize(file, nullptr)) { wprintf(L"Security Directory extends beyond file size.\n"); } else { auto cert = (LPWIN_CERTIFICATE)(map + va);
wprintf(L"Revision: %u\n", cert->wRevision); wprintf(L"Certificate Type: %u\n", cert->wCertificateType); wprintf(L"Certificate Length: %u\n", cert->dwLength); wprintf(L"Size of Directory Entry: %u\n", securitySize);
if (cert->wRevision == WIN_CERT_REVISION_2_0 && cert->wCertificateType == WIN_CERT_TYPE_PKCS_SIGNED_DATA && cert->dwLength < securitySize) { *tailSize = securitySize - cert->dwLength;
*tailData = new BYTE[*tailSize];
if (*tailData) { CopyMemory(*tailData, map + va + cert->dwLength, *tailSize); } else { *tailSize = 0; wprintf(L"Error: Memory allocation failed.\n"); } } } }
UnmapViewOfFile(map); CloseHandle(mapping); CloseHandle(file); }
int main() { LPBYTE tail; DWORD size;
wchar_t path[MAX_PATH]; GetModuleFileName(NULL, path, MAX_PATH);
ReadAuthenticodeTail(path, &tail, &size);
if (tail) { wprintf(L"Successfully read %u bytes of tail data.\n", size); delete[] tail; } else { wprintf(L"Did not find valid Authenticode tail data.\n"); }
return 0; }
|
代码要点说明
- 架构兼容性:代码通过检查
Magic 字段来区分 x86 和 x64 PE 文件
- 文件偏移 vs RVA:
Certificate Table Entry 的 VirtualAddress 是文件偏移,而非相对虚拟地址(RVA)
- 数据提取:通过比较
securitySize 和 cert->dwLength 来判断是否存在附加数据
安全性讨论
CVE-2013-3900 漏洞
历史上,这种技术曾被恶意软件利用(CVE-2013-3900),攻击者可以向合法签名的文件中注入恶意代码而不破坏签名。[2]
微软提供了注册表选项来检测这种行为:
1 2
| [HKEY_LOCAL_MACHINE\Software\Microsoft\Cryptography\Wintrust\Config] "EnableCertPaddingCheck"=dword:1
|
但为了保持向后兼容性,该选项默认禁用。
合法使用场景
尽管存在安全风险,但这种技术也有合法用途:
- 嵌入每用户配置信息
- 添加部署标识或追踪信息
- 存储非关键的元数据
最佳实践建议
- 仅用于非安全关键数据:不要在附加数据中存储敏感信息
- 额外完整性校验:可以对附加数据单独进行签名或哈希验证
- 文档化使用:明确说明软件使用了这种技术及其用途
- 考虑替代方案:评估是否可以使用资源段或独立配置文件
参考资料
PE 格式 - Win32 apps | Microsoft Learn
RFC 2315 - PKCS #7: Cryptographic Message Syntax Version 1.5
Authenticode 代码签名的注意事项 | Microsoft Learn
在 PE 文件中嵌入数据同时保持数字签名完整及对应的检测方法 | CrackMe.net
Disitool | Didier Stevens