C++外部程序修改exe文件属性信息的版本信息

Windows平台可执行文件(exe文件)属性中会有版本信息,包含文件说明、文件版本、版权等信息。但如何通过外部程序修改这些信息呢?当然一些第三方软件已经实现了这些功能,但这不是本文讨论的主题,本文是将设置版本信息的方法公开化。

关于这个问题,我查了msdn,上面只有获取这些信息的api接口,没有设置的接口。下面我们就围绕这个主题分析一下。


简介

Windows下的可执行文件格式属于PE文件格式标准,PE文件标准支持内嵌资源,就是将一个外部文件内嵌到可执行文件中,这样程序启动时只需从自身内部找到这块资源加载就可以了,而不需依赖其他外部的磁盘文件。

当然本文不是讨论如何从PE文件解析获取资源,不过有兴趣可以自己按照PE标准解析内嵌资源。

PE文件支持的内嵌资源都有两个必须的标识:一个是资源类型,一个是资源名称。因此只要知道内嵌资源的这两个标识就能找到对应的资源。可执行文件的版本信息就是以内嵌资源的方式保存在文件中。因此若要修改文件的版本信息,只需修改这块资源就行了。

  • 资源类型和资源名称可以是数字也可以是字符串。
  • 系统已知资源都是以数字来命名的(比如图标资源、版本资源、Manifest资源)。
  • 建议自定义资源都用字符串来命名,避免与系统资源冲突。

文件的版本信息以内嵌资源的方式保存在可执行文件中,它的资源类型是RT_VERSION(数字16),资源名称是 MAKEINTRESOURCE(VS_VERSION_INFO)(数字1)。

微软提供了一组通用API接口,用来获取和设置可执行文件的内嵌资源,请看下面详细介绍。

版本数据获取

获取PE文件的版本信息数据有两种获取方法:

1. 通用内嵌资源获取方式。
2. 专用API获取方式。

1. 通用内嵌资源获取方式

通过下面接口可以获取Windows可执行文件中的任何通用资源。

#include 
#include 

std::string GetPEResource(const char* exepath, const char* type,
                          const char* name, int language = 0)
{
    std::string r = "";

    if (!exepath)
        return r;

    //判定文件是否存在
    if (_access(exepath, 0) != 0)
        return r;

    //加载可执行文件
    HMODULE hexe = LoadLibrary(exepath);
    if (!hexe)
        return r;

    //查找资源
    HRSRC src = FindResourceEx(hexe, type, name, language);
    if (src)
    {
        HGLOBAL glb = LoadResource(hexe, src);
        int sz = SizeofResource(hexe, src);
        r = std::string((char *)LockResource(glb), sz);
        UnlockResource(glb);
        FreeResource(glb);
    }

    //释放可执行文件
    FreeLibrary(hexe);

    return r;
}

通过对 GetPEResource 接口进行简单调用即可获取PE文件中的版本数据。

std::string rc = GetPEResource( "C:\\测试文件.exe",
                                RT_VERSION, 
                                MAKEINTRESOURCE(VS_VERSION_INFO),
                                0);

这样 rc 变量中存储的就是一个完整的、标准的版本信息数据了。

2. 专用API获取方式

微软提供了另外一种专门用于获取文件版本信息的API接口,示例代码如下:

#include 
#include 

std::string GetPEVersionInfo(const char* exepath)
{
    std::string r = "";

    if (!exepath)
        return r;

    if (_access(exepath, 0) != 0)
        return r;

    DWORD sz = GetFileVersionInfoSize(exepath, 0);
    r.resize(sz, 0);
    return GetFileVersionInfo(exepath, 0, sz, (void*)r.c_str()) ? r : "";
}

通过调用此接口,也可正常获取文件中的版本信息数据。但此接口返回的缓冲区是自带备份的,并非标准版本数据。举个例子:假如某exe文件的版本数据大小是N字节,那么调用 GetPEVersionInfo 接口返回的缓冲区大小则是N * 2 + 4字节,前N字节数据为标准的版本数据,中间4字节用 ‘F’ ‘E’ ‘2’ ‘X’字符填充(至于为什么是FE2X,可以打电话给微软咨询一下GetFileVersionInfo接口的实现细节),后N个字节用0填充。微软在后面添加备份缓存是有原因的,通过这种方式来避免开发人员通过调用 VerQueryValue 获取某个字段值后对指针进行写操作,从而导致版本信息数据被破坏,后续的 VerQueryValue 调用就很可能崩溃了。

版本数据解析

版本数据是格式是一个名叫 VS_VERSIONINFO 的结构体,在msdn上可搜到这个结构体的相关描述。该结构可以认为由三部分构成。

  1. 第一部分是一个 VS_FIXEDFILEINFO 对象,包含简要版本信息。
  2. 第二部分是一个 VarFileInfo 对象,该部分可能不存在。
  3. 第三部分是一个 StringFileInfo 对象,该部分可能不存在。

第一部分存放在 VS_VERSIONINFO 结构中的 Value 字段中,第二部分和第三部分(若存在的话)存放在 VS_VERSIONINFO 结构中的 Children 字段中。


  • VS_VERSIONINFO结构体如下:
字段名 字段类型 字段含义
wLength WORD 结构体总长度
wValueLength WORD 后面Value字段的长度
wType WORD 资源类型,0代表二进制数据,1代表字符串数据
szKey WCHAR[] 标识字符串: “VS_VERSION_INFO”
Padding1 WORD[] 填充字节串(以0填充),使后面字段按4字节对齐
Value VS_FIXEDFILEINFO VS_FIXEDFILEINFO对象,存储简要版本信息
Padding2 WORD[] 填充字节串(以0填充),使后面字段按4字节对齐
Children 对象数组 里面最多存放两个对象,要么是 VarFileInfo 要么是 StringFileInfo

1. 第一部分

  • VS_FIXEDFILEINFO结构体如下:
字段名 字段类型 字段含义
dwSignature DWORD 文件填充信息标识,固定值0xFEEF04BD
dwStrucVersion DWORD 结构体版本号,当前值0x10000,高2字节代表主版本号,低2字节代表副版本号
dwFileVersionMS DWORD 4段文件版本号的前2段,每段2字节
dwFileVersionLS DWORD 4段文件版本号的后2段,每段2字节
dwProductVersionMS DWORD 4段产品版本号的前2段,每段2字节
dwProductVersionLS DWORD 4段产品版本号的后2段,每段2字节
dwFileFlagsMask DWORD 参见MSDN
dwFileOS DWORD 参见MSDN
dwFileSubtype DWORD 参见MSDN
dwFileDateMS DWORD 文件创建日期高4字节
dwFileDateLS DWORD 文件创建日期低4字节

2. 第二部分

  • VarFileInfo结构体如下:
字段名 字段类型 字段含义
wLength WORD 该结构体长度
wValueLength WORD 保留,填0
wType WORD 0代表二进制数据,1代表字符串数据。此值为1
szKey WCHAR[] 标识字符串: “VarFileInfo”
Padding WORD[] 填充字节串(以0填充),使后面字段按4字节对齐
Children Var 包含区域语言列表信息
  • Var结构体定义如下
字段名 字段类型 字段含义
wLength WORD 该结构体长度
wValueLength WORD Value字段长度
wType WORD 0代表二进制数据,1代表字符串数据。此值为0
szKey WCHAR[] 标识字符串: “Translation”
Padding WORD[] 填充字节串(以0填充),使后面字段按4字节对齐
Value DWORD 区域语言值

3. 第三部分

  • StringFileInfo结构体定义如下:
字段名 字段类型 字段含义
wLength WORD 该结构体长度
wValueLength WORD 保留,填0
wType WORD 0代表二进制数据,1代表字符串数据。此值为1
szKey WCHAR[] 标识字符串: “StringFileInfo”
Padding WORD[] 填充字节串(以0填充),使后面字段按4字节对齐
Children StringTable[] 版本字段列表
  • StringTable结构体定义如下:
字段名 字段类型 字段含义
wLength WORD 该结构体长度
wValueLength WORD 保留,填0
wType WORD 0代表二进制数据,1代表字符串数据。此值为1
szKey TCHAR[] 语言和编码中文字符一般为Unicode “000004b0”
Padding WORD[] 填充字节串(以0填充),使后面字段按4字节对齐
Children String[] String对象列表,版本字段全部存放在这个列表中
  • String结构体定义如下:
字段名 字段类型 字段含义
wLength WORD 该结构体长度
wValueLength WORD 后面Value字段占用的WORD大小
wType WORD 0代表二进制数据,1代表字符串数据。此值为1
szKey WCHAR[] “Comments” “CompanyName” “FileDescription” “FileVersion”等等
Padding WORD[] 填充字节串(以0填充),使后面字段按4字节对齐
Value WORD[] 值的内容,注意:填充Value的值要使得后续的对象按4字节对齐

版本数据重组

理解了版本数据的格式后,可以按照它的规则将文件版本数据进行重组。规则其实并不难,就是稍繁琐一点。

关于版本数据重组,我封装了一个C#版本的类,用于输出版本信息的二进制流,代码如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;

namespace monkey256
{
    class PEVersion
    {
        public string VersionString { get; set; }
        public string Comments { get; set; }
        public string CompanyName { get; set; }
        public string FileDescription { get; set; }
        public string InternalName { get; set; }
        public string LegalCopyright { get; set; }
        public string OriginalFilename { get; set; }
        public string ProductName { get; set; }

        public byte[] GenVSVersionInfo()
        {
            BinaryWriter bw = CreateBinaryStream();
            bw.Write((ushort)0);
            bw.Write((ushort)0x34);
            bw.Write((ushort)0);
            AppendUnicodeString(bw, "VS_VERSION_INFO");
            PaddingBinary(bw);
            bw.Write(GenFixedFileInfo());
            PaddingBinary(bw);
            bw.Write(GenVarFileInfo());
            bw.Write(GenStringFileInfo());

            return GetBytesWithRectifyHead(bw);
        }

        #region 辅助函数
        private BinaryWriter CreateBinaryStream()
        {
            MemoryStream ms = new MemoryStream();
            return new BinaryWriter(ms, Encoding.Unicode);
        }

        private void AppendUnicodeString(BinaryWriter bw, string str)
        {
            bw.Write(Encoding.Unicode.GetBytes(str));
            bw.Write((ushort)0);
        }

        private void PaddingBinary(BinaryWriter bw)
        {
            int n = (int)bw.BaseStream.Length % 4;
            int c = (4 - n) % 4;
            for (int i = 0; i < c; i++)
                bw.Write((byte)0);
        }

        private byte[] GetBytesFromBinaryStream(BinaryWriter bw)
        {
            byte[] r = new byte[bw.BaseStream.Length];
            bw.Seek(0, SeekOrigin.Begin);
            bw.BaseStream.Read(r, 0, r.Length);
            return r;
        }

        private byte[] GetBytesWithRectifyHead(BinaryWriter bw)
        {
            bw.Seek(0, SeekOrigin.Begin);
            bw.Write((ushort)bw.BaseStream.Length);

            return GetBytesFromBinaryStream(bw);
        }
        #endregion

        #region 第一部分 VS_FIXEDFILEINFO
        private byte[] GenFixedFileInfo()
        {
            BinaryWriter bw = CreateBinaryStream();
            bw.Write((UInt32)0xFEEF04BD);
            bw.Write((UInt32)0x10000);

            int v1 = 1;
            int v2 = 0;
            int v3 = 0;
            int v4 = 1;

            string[] arr = VersionString.Split('.');
            if (arr.Length == 4)
            {
                int.TryParse(arr[0], out v1);
                int.TryParse(arr[1], out v2);
                int.TryParse(arr[2], out v3);
                int.TryParse(arr[3], out v4);
            }

            UInt32 vm = (UInt32)((v1 << 16) | v2);
            UInt32 vl = (UInt32)((v3 << 16) | v4);

            bw.Write(vm);
            bw.Write(vl);
            bw.Write(vm);
            bw.Write(vl);

            bw.Write((UInt32)0x3F);
            bw.Write((UInt32)0);
            bw.Write((UInt32)4);
            bw.Write((UInt32)1);
            bw.Write((UInt32)0);
            bw.Write((UInt32)0);
            bw.Write((UInt32)0);

            return GetBytesFromBinaryStream(bw);
        }
        #endregion

        #region 第二部分 VarFileInfo
        private byte[] GenVar()
        {
            BinaryWriter bw = CreateBinaryStream();
            bw.Write((ushort)0);
            bw.Write((ushort)4);
            bw.Write((ushort)0);
            AppendUnicodeString(bw, "Translation");
            PaddingBinary(bw);
            bw.Write((ushort)0);
            bw.Write((ushort)0x4b0);

            return GetBytesWithRectifyHead(bw);
        }

        private byte[] GenVarFileInfo()
        {
            BinaryWriter bw = CreateBinaryStream();
            bw.Write((ushort)0);
            bw.Write((ushort)0);
            bw.Write((ushort)1);
            AppendUnicodeString(bw, "VarFileInfo");
            PaddingBinary(bw);
            bw.Write(GenVar());

            return GetBytesWithRectifyHead(bw);
        }
        #endregion

        #region 第三部分 StringFileInfo
        private byte[] GenString(string key, string value)
        {
            BinaryWriter bw = CreateBinaryStream();

            bw.Write((ushort)0);
            bw.Write((ushort)(value.Length + 1));
            bw.Write((ushort)1);
            AppendUnicodeString(bw, key);
            PaddingBinary(bw);
            AppendUnicodeString(bw, value);
            PaddingBinary(bw);

            return GetBytesWithRectifyHead(bw);
        }

        private byte[] GenStringTable()
        {
            BinaryWriter bw = CreateBinaryStream();
            bw.Write((ushort)0);
            bw.Write((ushort)0);
            bw.Write((ushort)1);
            AppendUnicodeString(bw, "000004b0");
            PaddingBinary(bw);
            bw.Write(GenString("Comments", Comments));
            bw.Write(GenString("CompanyName", CompanyName));
            bw.Write(GenString("FileDescription", FileDescription));
            bw.Write(GenString("FileVersion", VersionString));
            bw.Write(GenString("InternalName", InternalName));
            bw.Write(GenString("LegalCopyright", LegalCopyright));
            bw.Write(GenString("OriginalFilename", OriginalFilename));
            bw.Write(GenString("ProductName", ProductName));
            bw.Write(GenString("ProductVersion", VersionString));
            bw.Write(GenString("Assembly Version", VersionString));

            return GetBytesWithRectifyHead(bw);
        }

        private byte[] GenStringFileInfo()
        {
            BinaryWriter bw = CreateBinaryStream();
            bw.Write((ushort)0);
            bw.Write((ushort)0);
            bw.Write((ushort)1);
            AppendUnicodeString(bw, "StringFileInfo");
            PaddingBinary(bw);
            bw.Write(GenStringTable());

            return GetBytesWithRectifyHead(bw);
        }
        #endregion
    }
}

版本数据注入

上面通过重组数据得到一个内存块,如何将这些数据呈现到exe可执行文件的属性中,就需将此数据注入到文件中,通过以下函数可以完成此操作。

#include 

bool SetPEResource(const char* exepath, const char* type,
                   const char* name, const std::string& value, 
                   int language = 0)
{
    HANDLE hexe = BeginUpdateResource(exepath, FALSE);

    if (!hexe)
        return false;

    BOOL r = UpdateResource(hexe, type, name, language,
        (LPVOID)value.c_str(), (DWORD)value.size());

    BOOL er = EndUpdateResource(hexe, FALSE);

    return (r && er);
}

通过上面的接口即可将数据注入到可执行文件中,假设版本数据保存在变量 str 中,调用代码如下:
SetPEResource("C:\\测试文件.exe", RT_VERSION, MAKEINTRESOURCE(VS_VERSION_INFO), str, 0);

你可能感兴趣的:(C++)