唐凌 发表于 2020-3-10 12:16:34

【PE文件结构】解析PE文件版本资源

搞这玩意的起因是前些天在论坛群里有人提到这个事情,鉴于网上多数是通过专门的版本解析API来实现获取文件版本信息,不方便移植到运行环境上(比如驱动,UEFI等)。那么本文就谈谈直接解析文件实现获取文件版本信息,所使用的API仅限于普通文件操作的API。
基本常识就不谈了,就从资源表谈起。虽然说资源表在设计上可以是N级的,但实际上Windows用的PE资源都是使用三级表。这三级资源从上到下即:类型,名称,语言。
那么,基本流程如下:
查找映像中的资源表。
在资源表中搜索版本类型。
在版本类型的第一级表入口处直接走到第三级。
取得资源数据后解析。
要找到资源表有两种方法,一是通过PE可选头中第三项数据目录所记录的相对地址找到资源表;二是在PE节表中找到资源节,即.rsrc节,而资源节就是资源表。
两种方法都有意外的情况:
一是PE可选头未必就有数据目录,比如IMAGE_ROM_OPTIONAL_HEADER中没有DataDirectory。
二是PE节在链接时可以合并,此时.rsrc节可能不存在。
三是这个PE文件压根没有资源表。
因此,推荐的方法是先判断OptionalHeader.Magic的魔数值,如果是IMAGE_ROM_OPTIONAL_HDR_MAGIC就选择搜索节表找到资源节。
如果魔数是IMAGE_NT_OPTIONAL_HDR32_MAGIC或者IMAGE_NT_OPTIONAL_HDR64_MAGIC则直接从IMAGE_DIRECTORY_ENTRY_RESOURCE目录记录的相对地址确定资源表地址。
(魔数是干啥的?当然是判断这个PE文件是PE32还是PE32+咯)
如果还是没找到,则直接认为没有资源表。那么在确定资源表时,最优时间复杂度是常值的,而最差时间复杂度则是线性的。
资源表结构是三级表,在第一级表中记录的都是版本类型,因此我们在遍历资源表时只遍历第一级表,那么遍历资源表的时间复杂度是线性的。如果强行遍历三级表呢?那当然是立方的咯(笑)。
首先谈谈资源表用到的结构体。资源表(节)地址直接指向资源目录,其结构体如下:
typedef struct _IMAGE_RESOURCE_DIRECTORY {
    ULONG   Characteristics;
    ULONG   TimeDateStamp;
    USHORTMajorVersion;
    USHORTMinorVersion;
    USHORTNumberOfNamedEntries;
    USHORTNumberOfIdEntries;
//IMAGE_RESOURCE_DIRECTORY_ENTRY DirectoryEntries[];
} IMAGE_RESOURCE_DIRECTORY, *PIMAGE_RESOURCE_DIRECTORY;
这个结构体值得关心的就最后两个,NumberOfNamedEntries和NumberOfIdEntries。它们的和就是这一级资源表的子级表的入口数量。
大家可以注意到有那么一行注释在那,这个注释提示你子级表入口数组就紧跟在这个结构体的后面。因此当前表地址+sizeof(IMAGE_RESOURCE_DIRECTORY)就是子级表入口数组的地址了。
资源表入口数组如下:
typedef struct _IMAGE_RESOURCE_DIRECTORY_ENTRY {
    union {
      struct {
            ULONG NameOffset:31;
            ULONG NameIsString:1;
      } DUMMYSTRUCTNAME;
      ULONG   Name;
      USHORTId;
    } DUMMYUNIONNAME;
    union {
      ULONG   OffsetToData;
      struct {
            ULONG   OffsetToDirectory:31;
            ULONG   DataIsDirectory:1;
      } DUMMYSTRUCTNAME2;
    } DUMMYUNIONNAME2;
} IMAGE_RESOURCE_DIRECTORY_ENTRY, *PIMAGE_RESOURCE_DIRECTORY_ENTRY;
可以看到这是两个联合体拼在一块的,第一个联合体表示这个入口的ID或名称,第二个联合体表示数据偏移或者子级表偏移。
而要注意的是,在资源表结构里,每当提到偏移时,如果没有另行说明,那么偏移就是指相对于资源表的相对地址,而不是相对于PE映像的相对地址。
值得注意的是联合体中还有子结构体,并且还有位域。这就有意思了,那么很明显,只有一位的就当作布尔值,有好几位的就当作整数值。
首先判断Entry.NameIsString是置位还是复位,若置位,那么Entry.NameOffset就是名称偏移,且下一级表的类型是命名的;若复位,则使用Entry.Id,表示下一级表的类型是用ID的。
然后判断Entry.DataIsDirectory是置位还是复位,若置位,那么Entry.OffsetToDirectory就是下一级表的偏移;若复位,则使用Entry.OffsetToData,表示这个资源的数据入口。
先说表入口的名称。既然有命名项,那么就可能需要判断名字,虽然这里不需要,但还是提一下比较好。NameOffset指向名称,是Unicode编码的,它用一个结构体描述:
typedef struct _IMAGE_RESOURCE_DIR_STRING_U {
    USHORTLength;
    WCHAR   NameString[ 1 ];
} IMAGE_RESOURCE_DIR_STRING_U, *PIMAGE_RESOURCE_DIR_STRING_U;
其中Length成员表示它的字符数,不是字节数。需要注意的是NameString这个名称不是零结尾的,因此如果用printf之类的函数打印名称的话一定要限制输出长度,比如:
printf("%.*ws",Name.Length,Name.NameString);
当然咯,对其使用字符串函数时也要使用能限制源字符串长度的函数,比如RtlStringCchCopyN,RtlStringCchCatN等等。
然后再说数据入口,其结构体如下:
typedef struct _IMAGE_RESOURCE_DATA_ENTRY {
    ULONG   OffsetToData;
    ULONG   Size;
    ULONG   CodePage;
    ULONG   Reserved;
} IMAGE_RESOURCE_DATA_ENTRY, *PIMAGE_RESOURCE_DATA_ENTRY;
我们关心OffsetToData和Size就行了。这里要注意了,OffsetToData不是相对于资源表(节)的相对地址,而是相对于PE映像的相对地址了。
那么在解析PE的过程中,版本资源的查找方法如下:
根据前面提的方法,找到资源表的位置。
由于第一级表按类型存放,那么遍历第一级表,找类型为版本的即可。判断方法是看Directory.Id==16。
而二级表一般只有一个,但也可以有好几个,构造这种PE文件的方法就是编译时在多个.rc文件里都放版本资源。
同样的,第三级表一般也只有一个,但也可以有很多个,方法就是在同一个.rc文件里放很多个版本资源。
我们可以把第二级第一个入口中第三级的第一个当作版本资源,也可以多多益善,把所有的版本资源全都列出来。
不过当你右键属性的时候,不管有多少版本资源,你只能看到其中的一个。
到这里,解析PE可谓是到此为止了,我们谈解析版本资源。

关于如何解析版本资源,这是个大问题。因为在知识点上版本资源不属于PE结构。某种程度上很难百度到你要找的资料
版本资源时VS_VERSIONINFO结构体,但这个结构体不在任何版本的Windows SDK中,因为这个结构体无法用C语言来完整的定义。MSDN上贴出了它的伪定义:
typedef struct {
WORD             wLength;
WORD             wValueLength;
WORD             wType;
WCHAR            szKey;
WORD             Padding1;
VS_FIXEDFILEINFO Value;
WORD             Padding2;
WORD             Children;
} VS_VERSIONINFO;
我们依次解读所有成员的意义:
wLength表示整个VS_VERSIONINFO的大小。
wValueLength表示Value成员的大小,若为零则Value成员不存在。
wType表示版本资源数据类型,若为1则表示它包含文本资源,反之则表示包含二进制资源。
szKey表示一个Unicode字符串,它一定是L"VS_VERSION_INFO"。注意贴出的结构体是个伪定义,szKey的位置应当是一个字符串。
Padding1是用于对齐的,使得Value成员能对齐在32位的边界上。
Padding2同样用于对齐,它使得Children成员能对齐在32位边界上。
Value表示VS_FIXEDFILEINFO结构体。这个结构体是定长的,在verrsrc.h中有所定义,详情我就不多说了,只说这玩意大概是记录啥的。
如果你用过VC编辑过版本资源rc文件的话,应该能看到VC显示的可视化界面是分为上下两块的(如图所示),而上面那一块的内容就记录在VS_FIXEDFILEINFO里。

详情请看MSDN关于这个结构体的描述:https://docs.microsoft.com/en-us/windows/win32/api/verrsrc/ns-verrsrc-vs_fixedfileinfo
通常我们会着重编辑下面那一块,也就是Children成员。
Children成员表示一个数组,每一项可以是StringFileInfo结构体,也可以是VarFileInfo结构体。虽然说是数组,但MSDN中说了,每种最多一个,而通常情况下是各有一个。
具体情况具体分析,我们先贴上两者的结构体伪定义
typedef struct {
WORD      wLength;
WORD      wValueLength;
WORD      wType;
WCHAR       szKey;
WORD      Padding;
StringTable Children;
} StringFileInfo;

typedef struct {
WORDwLength;
WORDwValueLength;
WORDwType;
WCHAR szKey;
WORDPadding;
Var   Children;
} VarFileInfo;
两者除了最后一项都一样,那么我们可以这么定义一个结构体:
typedef struct {
WORDwLength;
WORDwValueLength;
WORDwType;
WCHAR szKey;
} XxxFileInfo;
可以用类似wcscmp之类的函数判断那个szKey写了啥即可。如果szKey是L"StringFileInfo",那么这个结构体就表示StringFileInfo;如果是L"VarFileInfo",那么这个结构体就表示VarFileInfo。
先说多数人不太关心的吧,VarFileInfo的内容记录这个程序所支持的所有语言。前三项的意义简单说说,wLength是整个结构体的大小,wValueLength由于结构体中由于没有Value成员,为零,wType的意义和VS_VERSIONINFO说的一样。
而szKey是啥刚才也说了。且说Var类型的Children,其伪定义如下:
typedef struct {
WORDwLength;
WORDwValueLength;
WORDwType;
WCHAR szKey;
WORDPadding;
DWORD Value;
} Var;
wLength表示整个Var结构体的大小,wValueLength表示Value的大小,wType用于区分这是文本数据还是二进制数据。
这里szKey一定是Unicode字符串L"Translation"。
而Value是一个DWORD数组,每一项表示语言和代码页构成的对子。如果这个程序支持好多语言,那就是好多个对子。因此此时wType必然为零,wValueLength表示这个数组的大小(字节数)
比方说,在我的英文版系统的ntdll.dll里,这个Value只有一项,值为0x04B00409。(那么wValueLength显而易见一定是4)
那么代码页就是0x4B0(1200),而语言就是0x409(1033)。根据百科,代码页1200是UTF-16LE,也就是小端16位Unicode;而语言1033是英语(美国)。
关于VarFileInfo到此为止,下面是StringFileInfo。
它的重点在于StringTable类型的Children成员,要说明的是,Children成员是个数组,它可以有好几个StringTable。不理解?回到VC的可视化版本资源编辑器,在空白处点一下右键:

看看红框标记的,点一下New Version Info Block选项,是不是就好理解了?
StringTable结构体的伪定义如下:
typedef struct {
WORD   wLength;
WORD   wValueLength;
WORD   wType;
WCHARszKey;
WORD   Padding;
String Children;
} StringTable;
前三个还是不说了,反正还是那意思。这里szKey是一个八个字符长的Unicode字符串,表示一个16进制数。那么也就是说它可以转换为一个DWORD。它的意义是语言和代码页构成的对子。
还是举例吧:在我的英文版系统的ntdll.dll里,这个szKey是L"040904B0",意思很明显,语言是0x409(1033),即英语(美国);代码页是0x4B0(1200),即UTF-16LE。
而Children是一个String结构体的数组,它也是个伪结构体,伪定义如下:
typedef struct {
WORDwLength;
WORDwValueLength;
WORDwType;
WCHAR szKey;
WORDPadding;
WORDValue;
} String;
前三个又是那个意思,不说了。这里szKey和Value就是核心了。szKey表示这个项的类型,而Value表示其值。虽然说这里wValueLength表示Value字符串的字符数,不过Value是有零结尾的,所以printf时不用特别限制长度。
比方说szKey可以是L"CompanyName",而Value可以是L"Microsoft Corporation"。
String结构体的数组中szKey一般会是L"Comments" L"CompanyName" L"FileDescription" L"FileVersion" L"InternalName" L"LegalCopyright" L"LegalTrademarks" L"OriginalFilename" L"PrivateBuild" L"ProductName" L"ProductVersion" L"SpecialBuild"这么几种之一。但要注意,其实这个szKey可以是任意的。
那么查询文件厂商的话我们就找szKey为L"CompanyName"的那一项,然后Value就是文件厂商了。

结语
本文决定不贴出代码,不过还是要谈谈编码时的一些注意事项。
虽然我们经常跳过wLength wValueLength wType这三项的意义解释,但wLength这一项还是很有用的。这一项可以作为偏移量,告诉你下一项在何方。换言之,这个操作就像单向链表一样。
不过,我们要着重注意的是遍历的终止条件,别把wLength为零作为终止条件,而是要把上一级的wLength作为终止条件。比方说,用for语句的话,就是:
for(String i=StringTable->Children;(ULONG_PTR)i<StringTable+StringTable->wLength;i=(String)((ULONG_PTR)i+i->wLength))
这是个伪代码,不过相信大家能理解其含义吧。

我们虽然可以直接解析那些已经装载进内存的模块,比如ntdll.dll。但也要注意一点,那就是资源节在程序入口函数执行完成后是可能会被释放的。
以Windows 7 x64为例,hal.dll中.rsrc节的Characteristics值是0x42000040,也就是说资源节带有以下标志:
IMAGE_SCN_MEM_READ:该节可读
IMAGE_SCN_MEM_DISCARDABLE:有必要时,该节的内存可以释放
IMAGE_SCN_CNT_INITIALIZED_DATA:该节包含已初始化的数据
重点在于IMAGE_SCN_MEM_DISCARDABLE这个标志位被置位了,那么读它的数据将有极大的概率出错——你访问了被释放的内存。

为了深入理解PE结构,在解析PE时,除了读写文件还有操作内存的一些基本API要用外,别的API一概别用。比如RtlImageDirectoryEntryToData之类的就不要用。

编写本文时使用到的有关PE结构的参考资料来自MSDN和头文件ntimage.h:
MSDN PE Format https://docs.microsoft.com/en-us/windows/win32/debug/pe-format
编写本文时使用到的全部有关版本资源的参考资料均来自MSDN:
MSDN VS_VERSIONINFO structure https://docs.microsoft.com/en-us/windows/win32/menurc/vs-versioninfo
MSDN VS_FIXEDFILEINFO structure https://docs.microsoft.com/en-us/windows/win32/api/verrsrc/ns-verrsrc-vs_fixedfileinfo
MSDN VarFileInfo structure https://docs.microsoft.com/en-us/windows/win32/menurc/varfileinfo
MSDN Var structure https://docs.microsoft.com/en-us/windows/win32/menurc/var-str
MSDN StringFileInfo structure https://docs.microsoft.com/en-us/windows/win32/menurc/stringfileinfo
MSDN StringTable structure https://docs.microsoft.com/en-us/windows/win32/menurc/stringtable
MSDN String structure https://docs.microsoft.com/en-us/windows/win32/menurc/string-str

Golden Blonde 发表于 2020-3-11 04:42:13

代码什么的都是浮云。。。以下代码来自某个锁页大牛的发财工程。。。有需要的自己完善一下。。。**** Hidden Message *****

maktom 发表于 2020-6-2 18:34:11

学习一下~~~~~~~~~~~

PassP 发表于 2021-5-9 03:34:03

看一下代码拉

sunshineFly 发表于 2021-5-28 09:43:35

必须学习一下

watermelon 发表于 2021-5-31 18:58:40

厉害了,学习一下!

clhenyan 发表于 2021-6-21 07:10:53

不错不错

dsb2468 发表于 2022-3-17 11:09:37

回复看看是啥

QOO 发表于 2022-3-28 14:35:33

好咚咚 感謝

xtfpg 发表于 2023-10-13 17:19:47

11111111111111

悠哉日常 发表于 2024-3-22 11:19:54

手动解析PE资源是写天书一样

tlwh163 发表于 2024-4-21 09:04:21

还是需要看看代码

YY菌 发表于 2024-4-23 10:11:20

话说,驱动程序不是也可以用 Ldr 开头的那几个资源操作API吗?只有UEFI和非Windows系统下读取PE资源才要这么做吧。

tangptr@126.com 发表于 2024-4-23 17:11:38

YY菌 发表于 2024-4-23 10:11
话说,驱动程序不是也可以用 Ldr 开头的那几个资源操作API吗?只有UEFI和非Windows系统下读取PE资源才要这 ...

少用乃至不用API对于安全行业来说是日常。到现在已经养成不搜API的习惯了。
而且Ldr的那些函数并不能直接解析出公司名称什么的。拿到版本资源后还得自己解析,这一段比资源表复杂多了。

YY菌 发表于 2024-4-24 09:09:59

tangptr@126.com 发表于 2024-4-23 17:11
少用乃至不用API对于安全行业来说是日常。到现在已经养成不搜API的习惯了。
而且Ldr的那些函数并不能直接 ...

Soga
页: [1]
查看完整版本: 【PE文件结构】解析PE文件版本资源