PE 签名的"盲区":如何合法嵌入自定义数据
概述
在 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 字段
-
保持 WIN_CERTIFICATE 结构不变
-
安全性考量
示意图说明
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: 签名内容
└─ [附加数据区域] ← 可在此处添加自定义数据
Copy
方法二:利用 PKCS#7 的 unauthenticated 字段
PKCS#7 签名结构中包含一个 unauthenticated 属性字段,该字段不参与签名计算。虽然此字段原本用于存储时间戳,但实际上可以填写任意数据。[5]
知名软件如 Dropbox 的安装程序就采用了这种技术来嵌入配置信息。
代码示例
工具使用
使用 Didier Stevens 开发的 [disitool.py](https://blog.didierstevens.com/programs/disitool/) 工具可以方便地向 PE 文件注入数据:[6]
1
python disitool.py inject --paddata source.exe data.txt output.exe
Copy
读取附加数据的 C++ 实现
以下代码演示如何在运行时读取附加在 WIN_CERTIFICATE 之后的数据:
#include <windows.h>
#include <wintrust.h>
#include <stdio.h>
// 获取安全目录项(兼容 x86 和 x64)
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;
// 判断是 x86 还是 x64
WORD magic = headers->OptionalHeader.Magic;
if (magic == IMAGE_NT_OPTIONAL_HDR32_MAGIC)
{
// x86 架构
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)
{
// x64 架构
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))
{
// 注意:Certificate Table Entry 的 VirtualAddress 实际是文件偏移,不是 RVA
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);
// 检查是否为 Authenticode 签名,且有附加数据
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;
}
Copy
代码要点说明
-
架构兼容性:代码通过检查
Magic字段来区分 x86 和 x64 PE 文件 -
文件偏移 vs RVA:
Certificate Table Entry的VirtualAddress是文件偏移,而非相对虚拟地址(RVA) -
数据提取:通过比较
securitySize和cert->dwLength来判断是否存在附加数据
安全性讨论
CVE-2013-3900 漏洞
历史上,这种技术曾被恶意软件利用(CVE-2013-3900),攻击者可以向合法签名的文件中注入恶意代码而不破坏签名。[2]
微软提供了注册表选项来检测这种行为:
[HKEY_LOCAL_MACHINE\Software\Microsoft\Cryptography\Wintrust\Config]
"EnableCertPaddingCheck"=dword:1
Copy
但为了保持向后兼容性,该选项默认禁用。
合法使用场景
尽管存在安全风险,但这种技术也有合法用途:
-
嵌入每用户配置信息
-
添加部署标识或追踪信息
-
存储非关键的元数据
最佳实践建议
-
仅用于非安全关键数据:不要在附加数据中存储敏感信息
-
额外完整性校验:可以对附加数据单独进行签名或哈希验证
-
文档化使用:明确说明软件使用了这种技术及其用途
-
考虑替代方案:评估是否可以使用资源段或独立配置文件
参考资料
PE 格式 - Win32 apps | Microsoft Learn
RFC 2315 - PKCS #7: Cryptographic Message Syntax Version 1.5
Authenticode 代码签名的注意事项 | Microsoft Learn