C库从Linux读取EXE版本?

有一个我可以在Linux中使用的库,它将返回Explorer版本选项卡中列出的Windows EXE文件的属性? 这些是像产品名称,产品版本,说明等字段。

对于我的项目,EXE文件只能从内存中读取,而不能从文件中读取。 我想避免将EXE文件写入磁盘。

该文件的版本位于VS_FIXEDFILEINFO结构中,但您必须将其查找到可执行数据。 有两种方法可以做你想做的事情:

  1. 搜索文件中的VERSION_INFO签名并直接读取VS_FIXEDFILEINFO结构。
  2. 找到.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; 

部分结构有两个相关的成员:部分VA和部分到文件的偏移量(也是部分的大小,但我没有检查!):

  DWORD vaSec = READ_DWORD(sec + 12); const char *raw = buf + READ_DWORD(sec + 20); 

现在文件中对应于我们之前获得的vaRes VA的偏移很容易。

  const char *resSec = raw + (vaRes - vaSec); 

这是一个指向资源数据的指针。 所有的个人资源都是以树的形式建立的,具有三个层次:1)资源的类型,2)资源的标识符,3)资源的语言。 对于这个版本,我们会得到正确类型的第一个。

首先,我们有一个资源目录(对于资源的类型),我们得到目录中的条目数,包括named和unnamed,并迭代:

  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中运行。 这个版本号存储在Resources(.rsrc)部分,但是当时我们已经决定只搜索整个二进制文件中的字符串,而没有进行任何优化。

稍后我们决定解析PE32文件,直到到达.rsrc节并获取文件版本字段。 为了做到这一点,我们意识到我们必须解析整个文件,我们想如果我们可以打印出所有的字段和值…

在版本0.40之前,pev是解析PE头和段的唯一程序(现在readpe负责这个)。 在版本0.50中,我们专注于恶意软件分析,并将其分解为一个库以外的各种程序,称为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); 

如果你不得不从内存中读取它,我认为你必须自己实现一些东西。 wrestool是一个好的开始:

 wrestool --type=16 -x --raw Paint.NET.3.5.10.Install.exe 

它在Ubuntu的icoutils上可用。 版本信息只是嵌入在可执行文件中的资源文件上的一系列字符串。 我想你可以看看wrestool的源代码,并检查他们如何找到资源块的开始。 一旦找到VERSION_INFO资源,就可以找出如何将它们翻译成可打印的信息(使用十六进制编辑器,它是可读的)。

icoutils是GPL …所以你不能只是抓住它的一部分,不要污染你的程序。 但是源代码是免费的,所以你可以检查他们做了什么,并写一个自定义的解决方案。

PE文件格式也可在互联网上获得。 你可以从这里开始: http : //msdn.microsoft.com/library/windows/hardware/gg463125

下面是Tcl语言中的一个例子,通过解析.exe文件来检索版本信息。

从Win32可执行文件读取版本信息 。

这个网页描述.exe头格式 。 我不知道这个信息的日期,或者它是否适用于更新版本的Windows。 不过这是一个起点。