2012-09-12 43 views
8

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

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

+6

http://stackoverflow.com/questions/1291570/native-linux-app-to-edit-win32-pe-like-reshacker?rq=1 – piokuc

+0

我不确定我是否理解EXE文件只能从内存中读取。我也不理解您希望避免将EXE文件写入磁盘的评论。您描述的各种EXE属性是资源,字符串,存储在EXE或DLL的资源部分。所以基本的机制是读取EXE或DLL文件寻找资源部分,然后解析资源部分寻找你想要的特定版本等资源并显示它们。 –

+0

你是否在另一台机器上(从Linux到Windows)拨动正在运行的可执行文件,也许是通过火线DMA访问? – ixe013

回答

21

该文件的版本位于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); 

这是一个指向资源数据的指针。所有的个人资源都是以树的形式建立的,具有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; 

而且我们得到的真正的资源,嗯,其实包含了实际资源的位置和大小,但我们没有一个struct关心大小。

  DWORD verVa = READ_DWORD(verDir); 

这是版本资源的VA,它很容易转换为指针。

  const char *verPtr = raw + (verVa - vaSec); 
      return verPtr; 

完成!如果没有找到返回NULL

 } 
     return NULL; 
    } 
    return NULL; 
} 

现在找到了版本资源,我们必须解析它。它实际上是一个树(还有什么)对“name”/“value”。有些值是众所周知的,这就是你正在寻找的,只是做一些测试,你会发现哪些。

注意:所有的字符串存储在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; 
    } 

更多填充,如果neccesary:

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,它似乎工作得很好。

+0

太棒了!当有人问道“stackoverflow有多好?”时,这个链接将成为我的回答。 – TheCodeArtist

+0

太棒了!谢谢。 – craig65535

+0

我创建了一个将所有C列表组合成一个C源代码文件的要点。我已经验证它编译和工作。你可以在这里找到它:https://gist.github.com/djhaskin987/d1860a7d98193913bcfa – djhaskin987

1

安装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磁盘?

+0

此解决方案是否需要将WINE安装在正在使用该应用程序的机器上? winelib是一个静态库还是有一个静态库版本,这样可以将一个可执行文件移植到其他Linux机器上,而无需在该机器上安装WINE?这是独立于Linux发行版吗? –

+0

@RichardChambers使用winelib编译的AFAIK应用程序需要Wine才能运行。 – PiotrNycz

0

如果你必须从内存中读取它,我认为你必须自己实现一些东西。 一个良好的开端是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

+0

PE头和资源是不同的,几乎没有发现,恐怕... – ixe013

+1

是的,他们是两回事。可执行文件的PE头包含很多部分。其中一个部分是资源之一。如果找到资源部分的开头,那么嵌入资源文件的格式非常重要。他们已经在wrestool上从Windows可执行文件中提取图标和字符串。你想要的版本信息也在那里。 – nmenezes

1

下面是支持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); 
2

我知道pev是Ubuntu的一个工具,它可以让你看到这个信息,有很多其他的PE头信息一起。我也知道它写在C. Maybe you'll want to have a look at it。从history section在文档中的位:

PEV先后诞生于2010年,从一个简单的需求:一个程序来找出PE32文件 版本(文件版本)和可以在Linux下运行。 此版本号存储在资源(.rsrc)部分,但在 时间,我们已决定在整个 二进制文件中简单搜索字符串,而不进行任何优化。

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

直到版本0.40,PEV是为解析一个独特的方案PE标头 和部分(现在readpe负责此)。在版本0.50中,我们 专注于恶意软件分析,并将其分解为各种程序 ,超出了一个名为libpe的库。目前所有的pev程序都使用libpe。