是否有我可以在 Linux 中使用的库来返回资源管理器的版本选项卡中列出的 Windows EXE 文件的属性?这些是产品名称、产品版本、描述等字段。
对于我的项目,只能从内存中读取 EXE 文件,不能从文件中读取。我想避免将 EXE 文件写入磁盘。
是否有我可以在 Linux 中使用的库来返回资源管理器的版本选项卡中列出的 Windows EXE 文件的属性?这些是产品名称、产品版本、描述等字段。
对于我的项目,只能从内存中读取 EXE 文件,不能从文件中读取。我想避免将 EXE 文件写入磁盘。
该文件的版本在VS_FIXEDFILEINFO
结构中,但您必须在可执行数据中找到它。有两种方法可以做你想做的事:
VS_FIXEDFILEINFO
直接读取结构。.rsrc
节,解析资源树,找到RT_VERSION
资源,解析它并提取VS_FIXEDFILEINFO
数据。第一个更容易,但很容易在错误的地方偶然找到签名。此外,您要求的其他数据(产品名称,描述等)不在此结构中,因此我将尝试解释如何以困难的方式获取数据。
PE 格式有点复杂,所以我将代码一块一块地粘贴,带有注释,并且错误检查最少。我将编写一个将数据转储到标准输出的简单函数。把它写成一个适当的函数留给读者作为练习:)
请注意,我将在缓冲区中使用偏移量,而不是直接映射结构,以避免与结构字段的对齐或填充相关的可移植性问题。无论如何,我已经注释了所使用的结构的类型(有关详细信息,请参阅包含文件 winnt.h)。
首先是一些有用的声明,它们应该是不言自明的:
typedef uint32_t DWORD;
typedef uint16_t WORD;
typedef uint8_t BYTE;
#define READ_BYTE(p) (((unsigned char*)(p))[0])
#define READ_WORD(p) ((((unsigned char*)(p))[0]) | ((((unsigned char*)(p))[1]) << 8))
#define READ_DWORD(p) ((((unsigned char*)(p))[0]) | ((((unsigned char*)(p))[1]) << 8) | \
((((unsigned char*)(p))[2]) << 16) | ((((unsigned char*)(p))[3]) << 24))
#define PAD(x) (((x) + 3) & 0xFFFFFFFC)
然后是一个在可执行映像中查找版本资源的函数(不检查大小)。
const char *FindVersion(const char *buf)
{
EXE 中的第一个结构是 MZ 标头(为了与 MS-DOS 兼容)。
//buf is a IMAGE_DOS_HEADER
if (READ_WORD(buf) != 0x5A4D) //MZ signature
return NULL;
MZ 标头中唯一感兴趣的字段是 PE 标头的偏移量。PE头是真实的。
//pe is a IMAGE_NT_HEADERS32
const char *pe = buf + READ_DWORD(buf + 0x3C);
if (READ_WORD(pe) != 0x4550) //PE signature
return NULL;
实际上,PE 头很无聊,我们想要 COFF 头,它包含所有的符号数据。
//coff is a IMAGE_FILE_HEADER
const char *coff = pe + 4;
我们只需要这个字段中的以下字段。
WORD numSections = READ_WORD(coff + 2);
WORD optHeaderSize = READ_WORD(coff + 16);
if (numSections == 0 || optHeaderSize == 0)
return NULL;
可选标头实际上在 EXE 中是强制性的,它就在 COFF 之后。32 位和 64 位 Windows 的魔力不同。我假设从这里开始是 32 位。
//optHeader is a IMAGE_OPTIONAL_HEADER32
const char *optHeader = coff + 20;
if (READ_WORD(optHeader) != 0x10b) //Optional header magic (32 bits)
return NULL;
有趣的部分来了:我们想要找到资源部分。它有两个部分:1. 部分数据,2. 部分元数据。
数据位置位于可选标题末尾的表中,每个部分在该表中都有一个众所周知的索引。资源部分在索引 2 中,因此我们通过以下方式获取资源部分的虚拟地址 (VA):
//dataDir is an array of IMAGE_DATA_DIRECTORY
const char *dataDir = optHeader + 96;
DWORD vaRes = READ_DWORD(dataDir + 8*2);
//secTable is an array of IMAGE_SECTION_HEADER
const char *secTable = optHeader + optHeaderSize;
要获取节元数据,我们需要迭代节表以查找名为 的节.rsrc
。
int i;
for (i = 0; i < numSections; ++i)
{
//sec is a IMAGE_SECTION_HEADER*
const char *sec = secTable + 40*i;
char secName[9];
memcpy(secName, sec, 8);
secName[8] = 0;
if (strcmp(secName, ".rsrc") != 0)
continue;
section 结构有两个相关成员:section 的 VA 和 section 在文件中的偏移量(也是 section 的大小,但我没有检查它!):
DWORD vaSec = READ_DWORD(sec + 12);
const char *raw = buf + READ_DWORD(sec + 20);
现在文件中对应于vaRes
我们之前得到的 VA 的偏移量很容易。
const char *resSec = raw + (vaRes - vaSec);
这是一个指向资源数据的指针。所有的单个资源都以树的形式设置,分为3个层次:1)资源类型,2)资源标识符,3)资源语言。对于该版本,我们将获得第一个正确类型的版本。
首先,我们有一个资源目录(用于资源类型),我们获取目录中的条目数,包括命名和未命名并迭代:
WORD numNamed = READ_WORD(resSec + 12);
WORD numId = READ_WORD(resSec + 14);
int j;
for (j = 0; j < numNamed + numId; ++j)
{
对于每个资源条目,我们获取资源的类型,如果它不是 RT_VERSION 常量 (16),则将其丢弃。
//resSec is a IMAGE_RESOURCE_DIRECTORY followed by an array
// of IMAGE_RESOURCE_DIRECTORY_ENTRY
const char *res = resSec + 16 + 8 * j;
DWORD name = READ_DWORD(res);
if (name != 16) //RT_VERSION
continue;
如果它是 RT_VERSION,我们将进入树中的下一个资源目录:
DWORD offs = READ_DWORD(res + 4);
if ((offs & 0x80000000) == 0) //is a dir resource?
return NULL;
//verDir is another IMAGE_RESOURCE_DIRECTORY and
// IMAGE_RESOURCE_DIRECTORY_ENTRY array
const char *verDir = resSec + (offs & 0x7FFFFFFF);
并继续到下一个目录级别,我们不关心 id。这个:
numNamed = READ_WORD(verDir + 12);
numId = READ_WORD(verDir + 14);
if (numNamed == 0 && numId == 0)
return NULL;
res = verDir + 16;
offs = READ_DWORD(res + 4);
if ((offs & 0x80000000) == 0) //is a dir resource?
return NULL;
第三级是资源的语言。我们也不在乎,所以只要抓住第一个:
//and yet another IMAGE_RESOURCE_DIRECTORY, etc.
verDir = resSec + (offs & 0x7FFFFFFF);
numNamed = READ_WORD(verDir + 12);
numId = READ_WORD(verDir + 14);
if (numNamed == 0 && numId == 0)
return NULL;
res = verDir + 16;
offs = READ_DWORD(res + 4);
if ((offs & 0x80000000) != 0) //is a dir resource?
return NULL;
verDir = resSec + offs;
我们得到了真正的资源,嗯,实际上是一个包含实际资源的位置和大小的结构,但我们不关心大小。
DWORD verVa = READ_DWORD(verDir);
那是版本资源的VA,很容易转换为指针。
const char *verPtr = raw + (verVa - vaSec);
return verPtr;
并做了!如果没有找到返回NULL
。
}
return NULL;
}
return NULL;
}
既然找到了版本资源,我们就必须对其进行解析。它实际上是一对“名称”/“值”的树(还有什么)。有些值是众所周知的,这就是您要寻找的值,只需进行一些测试,您就会发现哪些值。
注意:所有字符串都存储在 UNICODE (UTF-16) 中,但我的示例代码将哑转换为 ASCII。此外,不检查溢出。
该函数将指向版本资源的指针和到此内存的偏移量(0 表示开始)并返回分析的字节数。
int PrintVersion(const char *version, int offs)
{
首先,偏移量必须是 4 的倍数。
offs = PAD(offs);
然后我们得到版本树节点的属性。
WORD len = READ_WORD(version + offs);
offs += 2;
WORD valLen = READ_WORD(version + offs);
offs += 2;
WORD type = READ_WORD(version + offs);
offs += 2;
节点的名称是一个以 Unicode 零结尾的字符串。
char info[200];
int i;
for (i=0; i < 200; ++i)
{
WORD c = READ_WORD(version + offs);
offs += 2;
info[i] = c;
if (!c)
break;
}
更多填充,如果需要的话:
offs = PAD(offs);
如果type
不为 0,则为字符串版本数据。
if (type != 0) //TEXT
{
char value[200];
for (i=0; i < valLen; ++i)
{
WORD c = READ_WORD(version + offs);
offs += 2;
value[i] = c;
}
value[i] = 0;
printf("info <%s>: <%s>\n", info, value);
}
否则,如果名称是VS_VERSION_INFO
则它是一个VS_FIXEDFILEINFO
结构。否则它是二进制数据。
else
{
if (strcmp(info, "VS_VERSION_INFO") == 0)
{
我只是打印文件和产品的版本,但您可以轻松找到此结构的其他字段。当心混合字节序。
//fixed is a VS_FIXEDFILEINFO
const char *fixed = version + offs;
WORD fileA = READ_WORD(fixed + 10);
WORD fileB = READ_WORD(fixed + 8);
WORD fileC = READ_WORD(fixed + 14);
WORD fileD = READ_WORD(fixed + 12);
WORD prodA = READ_WORD(fixed + 18);
WORD prodB = READ_WORD(fixed + 16);
WORD prodC = READ_WORD(fixed + 22);
WORD prodD = READ_WORD(fixed + 20);
printf("\tFile: %d.%d.%d.%d\n", fileA, fileB, fileC, fileD);
printf("\tProd: %d.%d.%d.%d\n", prodA, prodB, prodC, prodD);
}
offs += valLen;
}
现在进行递归调用以打印完整的树。
while (offs < len)
offs = PrintVersion(version, offs);
在返回之前还有一些填充。
return PAD(offs);
}
最后,作为奖励,一个main
功能。
int main(int argc, char **argv)
{
struct stat st;
if (stat(argv[1], &st) < 0)
{
perror(argv[1]);
return 1;
}
char *buf = malloc(st.st_size);
FILE *f = fopen(argv[1], "r");
if (!f)
{
perror(argv[1]);
return 2;
}
fread(buf, 1, st.st_size, f);
fclose(f);
const char *version = FindVersion(buf);
if (!version)
printf("No version\n");
else
PrintVersion(version, 0);
return 0;
}
我已经用一些随机的 EXE 对其进行了测试,它似乎工作得很好。
我知道pev
是 Ubuntu 上的一个工具,它允许您查看此信息以及许多其他 PE 标头信息。我也知道它是用 C语言编写的。也许你会想看看它。来自文档中的历史部分:
pev 诞生于 2010 年,源于一个简单的需求:一个用于查找 PE32 文件的版本(文件版本)并且可以在 Linux 中运行的程序。此版本号存储在资源 (.rsrc) 部分中,但当时我们决定在整个二进制文件中简单地搜索字符串,而不进行任何优化。
稍后我们决定解析 PE32 文件,直到到达 .rsrc 部分并获取文件版本字段。为了做到这一点,我们意识到我们必须解析整个文件,我们想我们是否可以打印出所有的字段和值......
在 0.40 版之前,pev 是一个独特的程序,用于解析 PE 头和节(现在 readpe 负责这个)。在 0.50 版本中,我们专注于恶意软件分析,并将 pev 拆分为一个名为 libpe 的库之外的各种程序。目前所有 pev 程序都使用 libpe。
安装 winelib http://www.winehq.org/docs/winelib-guide/index 这是 MS Windows API 到其他系统的端口,包括 linux。
然后使用 MS Windows API。像GetFileVersionInfo
http://msdn.microsoft.com/en-us/library/windows/desktop/ms647003(v=vs.85).aspx
或任何其他功能。
我从来没有这样做过,但我会从这些发现开始。
关于内存限制中的exe文件,您可以将其复制到RAM磁盘吗?
这是支持 PE32+ 的代码补丁。对一些文件进行了测试,似乎可以工作。
//optHeader is a IMAGE_OPTIONAL_HEADER32
const char *optHeader = coff + 20;
WORD magic = READ_WORD(optHeader);
if (magic != 0x10b && magic != 0x20b)
return NULL;
//dataDir is an array of IMAGE_DATA_DIRECTORY
const char *dataDir = optHeader + (magic==0x10b ? 96: 112);
DWORD vaRes = READ_DWORD(dataDir + 8*2);
这是一个使用 Tcl 语言解析 .exe 文件以检索版本信息的示例。
此网页描述 .exe 标头格式。我不确定此信息的日期或它是否适用于更新版本的 Windows。然而,这是一个起点。