从Linux中读取EXE版本的C库?

10

在Linux中,是否有一个库可以返回Windows可执行文件的属性,这些属性在资源管理器中的版本选项卡中列出?这些字段包括产品名称、产品版本、描述等。

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


6
http://stackoverflow.com/questions/1291570/native-linux-app-to-edit-win32-pe-like-reshacker?rq=1 - piokuc
我不确定我理解EXE文件只能从内存中读取的限制。我也不明白您想避免将EXE文件写入磁盘的评论。您描述的各种EXE属性是资源,字符串,存储在EXE或DLL的资源部分中。因此,基本机制是读取EXE或DLL文件,查找资源部分,然后通过资源部分寻找特定版本等资源,并显示它们。 - Richard Chambers
你是否正在通过Firewire DMA访问从Linux到Windows的另一台计算机上运行的可执行文件? - ixe013
5个回答

24

文件的版本信息在 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;

section结构体有两个相关成员:该section的虚拟地址和该section在文件中的偏移量(也是section的大小,但我没检查!):

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

现在,与我们之前得到的vaRes虚拟地址相对应的文件偏移量很容易确定。

        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);

这是版本资源的虚拟地址,可以轻松转换为指针。

            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文件,看起来一切正常。


1
太棒了!这个链接将成为我回答任何人问“stackoverflow有多好?”的答案! - TheCodeArtist
1
我创建了一个Gist,将所有的C语言清单合并到一个单独的C源代码文件中。我已经验证它可以编译和工作。你可以在这里找到它:https://gist.github.com/djhaskin987/d1860a7d98193913bcfa - djhaskin987
最好能够提供一些参考资料。 - Vishal Santharam

3

我知道pev是Ubuntu上的一个工具,允许您查看此信息以及许多其他PE头信息。我也知道它是用C编写的。也许您想看一下。从文档中的历史部分中摘取一些内容:

pev于2010年诞生,起源于一个简单的需求:找到PE32文件的版本(文件版本),并且可以在Linux中运行。这个版本号存储在资源(.rsrc)部分中,但当时我们决定在整个二进制文件中简单地搜索字符串,没有任何优化。

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

直到0.40版本,pev是唯一用于解析PE头和节的程序(现在readpe负责此项工作)。在0.50版本中,我们专注于恶意软件分析,并将pev拆分为各种程序,除了名为libpe的库之外。目前,所有pev程序都使用libpe。


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);

1

这个解决方案需要在应用程序所在的计算机上安装WINE吗?winelib是一个静态库还是有一个静态库版本,以便将可执行文件移植到其他Linux计算机而无需在该计算机上安装WINE?这是否独立于Linux发行版? - Richard Chambers
据我所知,使用winelib编译的应用程序需要Wine才能运行。 - PiotrNycz

0

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接