Windows平台可执行文件(exe文件)属性中会有版本信息,包含文件说明、文件版本、版权等信息。但如何通过外部程序修改这些信息呢?当然一些第三方软件已经实现了这些功能,但这不是本文讨论的主题,本文是将设置版本信息的方法公开化。
关于这个问题,我查了msdn,上面只有获取这些信息的api接口,没有设置的接口。下面我们就围绕这个主题分析一下。
Windows下的可执行文件格式属于PE文件格式标准,PE文件标准支持内嵌资源,就是将一个外部文件内嵌到可执行文件中,这样程序启动时只需从自身内部找到这块资源加载就可以了,而不需依赖其他外部的磁盘文件。
当然本文不是讨论如何从PE文件解析获取资源,不过有兴趣可以自己按照PE标准解析内嵌资源。
PE文件支持的内嵌资源都有两个必须的标识:一个是资源类型,一个是资源名称。因此只要知道内嵌资源的这两个标识就能找到对应的资源。可执行文件的版本信息就是以内嵌资源的方式保存在文件中。因此若要修改文件的版本信息,只需修改这块资源就行了。
文件的版本信息以内嵌资源的方式保存在可执行文件中,它的资源类型是RT_VERSION(数字16),资源名称是 MAKEINTRESOURCE(VS_VERSION_INFO)(数字1)。
微软提供了一组通用API接口,用来获取和设置可执行文件的内嵌资源,请看下面详细介绍。
获取PE文件的版本信息数据有两种获取方法:
1. 通用内嵌资源获取方式。
2. 专用API获取方式。
通过下面接口可以获取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
变量中存储的就是一个完整的、标准的版本信息数据了。
微软提供了另外一种专门用于获取文件版本信息的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上可搜到这个结构体的相关描述。该结构可以认为由三部分构成。
- 第一部分是一个
VS_FIXEDFILEINFO
对象,包含简要版本信息。- 第二部分是一个
VarFileInfo
对象,该部分可能不存在。- 第三部分是一个
StringFileInfo
对象,该部分可能不存在。
第一部分存放在 VS_VERSIONINFO
结构中的 Value
字段中,第二部分和第三部分(若存在的话)存放在 VS_VERSIONINFO
结构中的 Children
字段中。
字段名 | 字段类型 | 字段含义 |
---|---|---|
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 |
字段名 | 字段类型 | 字段含义 |
---|---|---|
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字节 |
字段名 | 字段类型 | 字段含义 |
---|---|---|
wLength | WORD | 该结构体长度 |
wValueLength | WORD | 保留,填0 |
wType | WORD | 0代表二进制数据,1代表字符串数据。此值为1 |
szKey | WCHAR[] | 标识字符串: “VarFileInfo” |
Padding | WORD[] | 填充字节串(以0填充),使后面字段按4字节对齐 |
Children | Var | 包含区域语言列表信息 |
字段名 | 字段类型 | 字段含义 |
---|---|---|
wLength | WORD | 该结构体长度 |
wValueLength | WORD | Value字段长度 |
wType | WORD | 0代表二进制数据,1代表字符串数据。此值为0 |
szKey | WCHAR[] | 标识字符串: “Translation” |
Padding | WORD[] | 填充字节串(以0填充),使后面字段按4字节对齐 |
Value | DWORD | 区域语言值 |
字段名 | 字段类型 | 字段含义 |
---|---|---|
wLength | WORD | 该结构体长度 |
wValueLength | WORD | 保留,填0 |
wType | WORD | 0代表二进制数据,1代表字符串数据。此值为1 |
szKey | WCHAR[] | 标识字符串: “StringFileInfo” |
Padding | WORD[] | 填充字节串(以0填充),使后面字段按4字节对齐 |
Children | StringTable[] | 版本字段列表 |
字段名 | 字段类型 | 字段含义 |
---|---|---|
wLength | WORD | 该结构体长度 |
wValueLength | WORD | 保留,填0 |
wType | WORD | 0代表二进制数据,1代表字符串数据。此值为1 |
szKey | TCHAR[] | 语言和编码中文字符一般为Unicode “000004b0” |
Padding | WORD[] | 填充字节串(以0填充),使后面字段按4字节对齐 |
Children | 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);