寻求纯C#实现从签名DLL提取SignedCms对象的正确方案
纯C#实现从已签名程序集中提取SignedCms对象
你的问题核心在于没有正确解析PE文件中证书表的结构——直接使用IMAGE_DIRECTORY_ENTRY_SECURITY的VirtualAddress和Size是不够的,因为这个目录项指向的是一个包含WIN_CERTIFICATE头部的完整证书块,而非直接的CMS签名数据。
问题根源
PE文件的IMAGE_DIRECTORY_ENTRY_SECURITY项(也就是你读取的CertificateTable)指向的是文件中的一个数据块,这个块的开头是标准的WIN_CERTIFICATE结构体:
[StructLayout(LayoutKind.Sequential, Pack = 1)] private struct WIN_CERTIFICATE { public uint dwLength; // 整个证书块的总长度(包括自身) public ushort wRevision; // 证书版本(通常为0x0200) public ushort wCertificateType; // 证书类型(0x0002表示PKCS#7签名) }
这个头部占用8字节,之后才是实际的PKCS#7/CMS签名数据。你之前手动加8字节偏移、减10字节的操作是对这个结构的不完全适配,正确的做法应该是先解析这个结构体,再提取后续的CMS数据。
正确的纯C#实现
下面是完整的实现,包含PE头解析、证书块读取和SignedCms解码:
using System; using System.IO; using System.Security.Cryptography.Pkcs; using System.Runtime.InteropServices; public static class SignedCmsExtractor { // PE文件相关结构体定义 [StructLayout(LayoutKind.Sequential, Pack = 1)] private struct IMAGE_DOS_HEADER { public ushort e_magic; // 省略其他无关字段,只保留NT头偏移 public uint e_lfanew; } [StructLayout(LayoutKind.Sequential, Pack = 1)] private struct IMAGE_FILE_HEADER { public ushort Machine; public ushort NumberOfSections; public uint TimeDateStamp; public uint PointerToSymbolTable; public uint NumberOfSymbols; public ushort SizeOfOptionalHeader; public ushort Characteristics; } [StructLayout(LayoutKind.Sequential, Pack = 1)] private struct IMAGE_OPTIONAL_HEADER32 { public ushort Magic; // 省略其他无关字段,只保留数据目录 [MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)] public IMAGE_DATA_DIRECTORY[] DataDirectory; } [StructLayout(LayoutKind.Sequential, Pack = 1)] private struct IMAGE_OPTIONAL_HEADER64 { public ushort Magic; // 省略其他无关字段,只保留数据目录 [MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)] public IMAGE_DATA_DIRECTORY[] DataDirectory; } [StructLayout(LayoutKind.Sequential, Pack = 1)] private struct IMAGE_DATA_DIRECTORY { public uint VirtualAddress; // 注意:对于安全目录项,这是文件偏移,不是内存地址 public uint Size; } [StructLayout(LayoutKind.Sequential, Pack = 1)] private struct WIN_CERTIFICATE { public uint dwLength; public ushort wRevision; public ushort wCertificateType; } // 安全目录项的索引 private const int IMAGE_DIRECTORY_ENTRY_SECURITY = 4; // PKCS#7证书类型 private const ushort WIN_CERT_TYPE_PKCS_SIGNED_DATA = 0x0002; public static SignedCms GetSignedCmsFromFile(string fileName) { using var fs = new FileStream(fileName, FileMode.Open, FileAccess.Read); using var reader = new BinaryReader(fs); // 读取DOS头,获取NT头偏移 var dosHeader = ReadStruct<IMAGE_DOS_HEADER>(reader); if (dosHeader.e_magic != 0x5A4D) // "MZ"标识 throw new InvalidDataException("不是有效的PE文件"); fs.Seek(dosHeader.e_lfanew, SeekOrigin.Begin); var ntSignature = reader.ReadUInt32(); if (ntSignature != 0x00004550) // "PE\0\0"标识 throw new InvalidDataException("不是有效的PE文件"); // 读取文件头 var fileHeader = ReadStruct<IMAGE_FILE_HEADER>(reader); bool is32Bit = fileHeader.Machine == 0x014C; // x86标识 // 读取可选头 IMAGE_DATA_DIRECTORY securityDir = default; if (is32Bit) { var optionalHeader = ReadStruct<IMAGE_OPTIONAL_HEADER32>(reader); securityDir = optionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_SECURITY]; } else { var optionalHeader = ReadStruct<IMAGE_OPTIONAL_HEADER64>(reader); securityDir = optionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_SECURITY]; } if (securityDir.Size == 0 || securityDir.VirtualAddress == 0) throw new InvalidDataException("文件未签名或无证书信息"); // 跳转到证书块的文件偏移(注意:VirtualAddress在这里是文件偏移,不是内存地址) fs.Seek(securityDir.VirtualAddress, SeekOrigin.Begin); // 读取WIN_CERTIFICATE头部 var certHeader = ReadStruct<WIN_CERTIFICATE>(reader); if (certHeader.wCertificateType != WIN_CERT_TYPE_PKCS_SIGNED_DATA) throw new InvalidDataException("不是PKCS#7签名数据"); // 读取实际的CMS签名数据(总长度减去头部长度) int cmsDataLength = (int)(certHeader.dwLength - Marshal.SizeOf(typeof(WIN_CERTIFICATE))); byte[] cmsData = reader.ReadBytes(cmsDataLength); // 解码为SignedCms对象 var signedCms = new SignedCms(); signedCms.Decode(cmsData); return signedCms; } // 辅助方法:从BinaryReader读取结构体 private static T ReadStruct<T>(BinaryReader reader) where T : struct { int size = Marshal.SizeOf(typeof(T)); byte[] buffer = reader.ReadBytes(size); GCHandle handle = GCHandle.Alloc(buffer, GCHandleType.Pinned); try { return (T)Marshal.PtrToStructure(handle.AddrOfPinnedObject(), typeof(T)); } finally { handle.Free(); } } }
关键说明
- PE头解析:严格按照PE文件格式读取DOS头、NT头、文件头和可选头,确保正确找到安全目录项。
- WIN_CERTIFICATE解析:必须先读取这个头部,确认证书类型为PKCS#7(
WIN_CERT_TYPE_PKCS_SIGNED_DATA),再提取后续的CMS数据。 - 文件偏移处理:PE安全目录项的
VirtualAddress字段在文件中实际存储的是文件偏移,而非内存虚拟地址,这是很多人容易踩的坑。 - 数据长度计算:CMS数据的长度是证书块总长度(
dwLength)减去WIN_CERTIFICATE结构体的大小,而非手动硬编码偏移值。
这个实现完全不需要依赖CRYPT32.DLL,纯C#完成,且能正确处理各种PE文件(32位/64位)中的签名数据。
内容的提问来源于stack exchange,提问作者Martin S
相关产品推荐
相关产品推荐

