【问题标题】:C library to read EXE version from Linux?C 库从 Linux 读取 EXE 版本?
【发布时间】:2012-09-05 23:23:19
【问题描述】:

是否有我可以在 Linux 中使用的库来返回资源管理器的版本选项卡中列出的 Windows EXE 文件的属性?这些是产品名称、产品版本、描述等字段。

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

【问题讨论】:

  • 我不确定我是否理解 EXE 文件只能从内存中读取的限制。我也不理解您希望避免将 EXE 文件写入磁盘的评论。您描述的各种 EXE 属性是资源、字符串,存储在 EXE 或 DLL 的资源部分中。所以基本的机制是读取 EXE 或 DLL 文件寻找资源部分,然后通过资源部分解析寻找您想要的特定版本等资源并显示它们。
  • 您是否正在通过火线 DMA 访问在另一台机器(从 Linux 到 Windows)上插入正在运行的可执行文件?

标签: c windows linux portable-executable


【解决方案1】:

文件的版本在VS_FIXEDFILEINFO 结构体中,但你必须在可执行数据中找到它。有两种方法可以做你想做的事:

  1. 在文件中搜索VERSION_INFO签名,直接读取VS_FIXEDFILEINFO结构体。
  2. 找到.rsrc部分,解析资源树,找到RT_VERSION资源,解析它并提取VS_FIXEDFILEINFO数据。

第一个比较容易,但很容易在错误的地方偶然找到签名。而且,你要求的其他数据(产品名称、描述等)不在这个结构中,所以我将尝试解释如何获取数据。

PE 格式有点复杂,所以我将代码一块一块地粘贴,使用 cmets,并进行最少的错误检查。我将编写一个将数据转储到标准输出的简单函数。把它写成一个合适的函数留给读者作为练习:)

请注意,我将在缓冲区中使用偏移量,而不是直接映射结构,以避免与结构字段的对齐或填充相关的可移植性问题。无论如何,我已经注释了使用的结构的类型(有关详细信息,请参阅包含文件 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 struct 有两个相关成员: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 对其进行了测试,它似乎工作得很好。

【讨论】:

  • 很棒的答案!当有人问“stackoverflow 有多好?”时,我会回复此链接
  • 我创建了一个要点,将所有 C 列表合并到一个 C 源代码文件中。我已经验证它可以编译和工作。你可以在这里找到它:gist.github.com/djhaskin987/d1860a7d98193913bcfa
  • 如果你也能提供一些参考就更好了。
【解决方案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 版本中,我们 专注于恶意软件分析并将 pev 拆分为各种程序 超越了一个名为 libpe 的库。目前所有 pev 程序都使用 libpe。

【讨论】:

    【解决方案3】:

    安装winelibhttp://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文件,可以复制到内存盘吗?

    【讨论】:

    • 此解决方案是否要求在使用该应用程序的机器上安装 WINE? winelib 是静态库还是具有静态库版本,以便可执行文件可以移植到其他 Linux 机器而无需在该机器上安装 WINE?这是否独立于 Linux 发行版?
    • @RichardChambers 使用 winelib 编译的 AFAIK 应用程序需要 Wine 才能运行。
    【解决方案4】:

    这是支持 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);
    

    【讨论】:

      【解决方案5】:

      这是一个用 Tcl 语言解析 .exe 文件以检索版本信息的示例。

      Reading version information from Win32 executables.

      这个web page describes the .exe header format。我不确定此信息的日期或它是否适用于更新版本的 Windows。然而,这是一个起点。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2010-09-20
        • 1970-01-01
        • 2011-09-13
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2016-05-11
        相关资源
        最近更新 更多