You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

寻求纯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();
        }
    }
}

关键说明

  1. PE头解析:严格按照PE文件格式读取DOS头、NT头、文件头和可选头,确保正确找到安全目录项。
  2. WIN_CERTIFICATE解析:必须先读取这个头部,确认证书类型为PKCS#7(WIN_CERT_TYPE_PKCS_SIGNED_DATA),再提取后续的CMS数据。
  3. 文件偏移处理:PE安全目录项的VirtualAddress字段在文件中实际存储的是文件偏移,而非内存虚拟地址,这是很多人容易踩的坑。
  4. 数据长度计算:CMS数据的长度是证书块总长度(dwLength)减去WIN_CERTIFICATE结构体的大小,而非手动硬编码偏移值。

这个实现完全不需要依赖CRYPT32.DLL,纯C#完成,且能正确处理各种PE文件(32位/64位)中的签名数据。

内容的提问来源于stack exchange,提问作者Martin S

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.12 04:42:40