找回密码
 立即注册→加入我们

QQ登录

只需一步,快速开始

搜索
热搜: 下载 VB C 实现 编写
查看: 3453|回复: 14

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

[复制链接]

65

主题

117

回帖

1万

积分

用户组: 超级版主

OS与VM研究学者

UID
1043
精华
35
威望
789 点
宅币
8308 个
贡献
1094 次
宅之契约
0 份
在线时间
2071 小时
注册时间
2015-8-15
发表于 2020-3-10 12:16:34 | 显示全部楼层 |阅读模式

欢迎访问技术宅的结界,请注册或者登录吧。

您需要 登录 才可以下载或查看,没有账号?立即注册→加入我们

×
搞这玩意的起因是前些天在论坛群里有人提到这个事情,鉴于网上多数是通过专门的版本解析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+咯)
如果还是没找到,则直接认为没有资源表。那么在确定资源表时,最优时间复杂度是常值的,而最差时间复杂度则是线性的。
资源表结构是三级表,在第一级表中记录的都是版本类型,因此我们在遍历资源表时只遍历第一级表,那么遍历资源表的时间复杂度是线性的。如果强行遍历三级表呢?那当然是立方的咯(笑)。
首先谈谈资源表用到的结构体。资源表(节)地址直接指向资源目录,其结构体如下:
  1. typedef struct _IMAGE_RESOURCE_DIRECTORY {
  2.     ULONG   Characteristics;
  3.     ULONG   TimeDateStamp;
  4.     USHORT  MajorVersion;
  5.     USHORT  MinorVersion;
  6.     USHORT  NumberOfNamedEntries;
  7.     USHORT  NumberOfIdEntries;
  8. //  IMAGE_RESOURCE_DIRECTORY_ENTRY DirectoryEntries[];
  9. } IMAGE_RESOURCE_DIRECTORY, *PIMAGE_RESOURCE_DIRECTORY;
复制代码

这个结构体值得关心的就最后两个,NumberOfNamedEntries和NumberOfIdEntries。它们的和就是这一级资源表的子级表的入口数量。
大家可以注意到有那么一行注释在那,这个注释提示你子级表入口数组就紧跟在这个结构体的后面。因此当前表地址+sizeof(IMAGE_RESOURCE_DIRECTORY)就是子级表入口数组的地址了。
资源表入口数组如下:
  1. typedef struct _IMAGE_RESOURCE_DIRECTORY_ENTRY {
  2.     union {
  3.         struct {
  4.             ULONG NameOffset:31;
  5.             ULONG NameIsString:1;
  6.         } DUMMYSTRUCTNAME;
  7.         ULONG   Name;
  8.         USHORT  Id;
  9.     } DUMMYUNIONNAME;
  10.     union {
  11.         ULONG   OffsetToData;
  12.         struct {
  13.             ULONG   OffsetToDirectory:31;
  14.             ULONG   DataIsDirectory:1;
  15.         } DUMMYSTRUCTNAME2;
  16.     } DUMMYUNIONNAME2;
  17. } 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编码的,它用一个结构体描述:
  1. typedef struct _IMAGE_RESOURCE_DIR_STRING_U {
  2.     USHORT  Length;
  3.     WCHAR   NameString[ 1 ];
  4. } IMAGE_RESOURCE_DIR_STRING_U, *PIMAGE_RESOURCE_DIR_STRING_U;
复制代码

其中Length成员表示它的字符数,不是字节数。需要注意的是NameString这个名称不是零结尾的,因此如果用printf之类的函数打印名称的话一定要限制输出长度,比如:
  1. printf("%.*ws",Name.Length,Name.NameString);
复制代码

当然咯,对其使用字符串函数时也要使用能限制源字符串长度的函数,比如RtlStringCchCopyN,RtlStringCchCatN等等。
然后再说数据入口,其结构体如下:
  1. typedef struct _IMAGE_RESOURCE_DATA_ENTRY {
  2.     ULONG   OffsetToData;
  3.     ULONG   Size;
  4.     ULONG   CodePage;
  5.     ULONG   Reserved;
  6. } 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上贴出了它的伪定义:
  1. typedef struct {
  2.   WORD             wLength;
  3.   WORD             wValueLength;
  4.   WORD             wType;
  5.   WCHAR            szKey;
  6.   WORD             Padding1;
  7.   VS_FIXEDFILEINFO Value;
  8.   WORD             Padding2;
  9.   WORD             Children;
  10. } 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里。
rc.JPG
详情请看MSDN关于这个结构体的描述:https://docs.microsoft.com/en-us/windows/win32/api/verrsrc/ns-verrsrc-vs_fixedfileinfo
通常我们会着重编辑下面那一块,也就是Children成员。
Children成员表示一个数组,每一项可以是StringFileInfo结构体,也可以是VarFileInfo结构体。虽然说是数组,但MSDN中说了,每种最多一个,而通常情况下是各有一个。
具体情况具体分析,我们先贴上两者的结构体伪定义
  1. typedef struct {
  2.   WORD        wLength;
  3.   WORD        wValueLength;
  4.   WORD        wType;
  5.   WCHAR       szKey;
  6.   WORD        Padding;
  7.   StringTable Children;
  8. } StringFileInfo;

  9. typedef struct {
  10.   WORD  wLength;
  11.   WORD  wValueLength;
  12.   WORD  wType;
  13.   WCHAR szKey;
  14.   WORD  Padding;
  15.   Var   Children;
  16. } VarFileInfo;
复制代码

两者除了最后一项都一样,那么我们可以这么定义一个结构体:
  1. typedef struct {
  2.   WORD  wLength;
  3.   WORD  wValueLength;
  4.   WORD  wType;
  5.   WCHAR szKey[1];
  6. } XxxFileInfo;
复制代码

可以用类似wcscmp之类的函数判断那个szKey写了啥即可。如果szKey是L"StringFileInfo",那么这个结构体就表示StringFileInfo;如果是L"VarFileInfo",那么这个结构体就表示VarFileInfo。
先说多数人不太关心的吧,VarFileInfo的内容记录这个程序所支持的所有语言。前三项的意义简单说说,wLength是整个结构体的大小,wValueLength由于结构体中由于没有Value成员,为零,wType的意义和VS_VERSIONINFO说的一样。
而szKey是啥刚才也说了。且说Var类型的Children,其伪定义如下:
  1. typedef struct {
  2.   WORD  wLength;
  3.   WORD  wValueLength;
  4.   WORD  wType;
  5.   WCHAR szKey;
  6.   WORD  Padding;
  7.   DWORD Value;
  8. } 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的可视化版本资源编辑器,在空白处点一下右键:
rcadd.png
看看红框标记的,点一下New Version Info Block选项,是不是就好理解了?
StringTable结构体的伪定义如下:
  1. typedef struct {
  2.   WORD   wLength;
  3.   WORD   wValueLength;
  4.   WORD   wType;
  5.   WCHAR  szKey;
  6.   WORD   Padding;
  7.   String Children;
  8. } StringTable;
复制代码

前三个还是不说了,反正还是那意思。这里szKey是一个八个字符长的Unicode字符串,表示一个16进制数。那么也就是说它可以转换为一个DWORD。它的意义是语言和代码页构成的对子。
还是举例吧:在我的英文版系统的ntdll.dll里,这个szKey是L"040904B0",意思很明显,语言是0x409(1033),即英语(美国);代码页是0x4B0(1200),即UTF-16LE。
而Children是一个String结构体的数组,它也是个伪结构体,伪定义如下:
  1. typedef struct {
  2.   WORD  wLength;
  3.   WORD  wValueLength;
  4.   WORD  wType;
  5.   WCHAR szKey;
  6.   WORD  Padding;
  7.   WORD  Value;
  8. } 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语句的话,就是:
  1. 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
回复

使用道具 举报

55

主题

275

回帖

9356

积分

用户组: 管理员

UID
77
精华
16
威望
237 点
宅币
8221 个
贡献
251 次
宅之契约
0 份
在线时间
255 小时
注册时间
2014-2-22
发表于 2020-3-11 04:42:13 | 显示全部楼层
代码什么的都是浮云。。。以下代码来自某个锁页大牛的发财工程。。。有需要的自己完善一下。。。
游客,如果您要查看本帖隐藏内容请回复
回复 赞! 靠!

使用道具 举报

0

主题

24

回帖

80

积分

用户组: 小·技术宅

UID
5782
精华
0
威望
6 点
宅币
44 个
贡献
0 次
宅之契约
0 份
在线时间
4 小时
注册时间
2020-4-4
发表于 2020-6-2 18:34:11 | 显示全部楼层
学习一下~~~~~~~~~~~
回复

使用道具 举报

0

主题

1

回帖

16

积分

用户组: 初·技术宅

UID
7125
精华
0
威望
1 点
宅币
13 个
贡献
0 次
宅之契约
0 份
在线时间
0 小时
注册时间
2021-5-9
发表于 2021-5-9 03:34:03 | 显示全部楼层
看一下代码拉
回复 赞! 靠!

使用道具 举报

0

主题

1

回帖

18

积分

用户组: 初·技术宅

UID
7174
精华
0
威望
1 点
宅币
15 个
贡献
0 次
宅之契约
0 份
在线时间
0 小时
注册时间
2021-5-28
发表于 2021-5-28 09:43:35 | 显示全部楼层
必须学习一下
回复 赞! 靠!

使用道具 举报

29

主题

315

回帖

1561

积分

用户组: 上·技术宅

UID
3808
精华
11
威望
105 点
宅币
702 个
贡献
165 次
宅之契约
0 份
在线时间
404 小时
注册时间
2018-5-6
发表于 2021-5-31 18:58:40 | 显示全部楼层
厉害了,学习一下!
Passion Coding!
回复 赞! 靠!

使用道具 举报

0

主题

14

回帖

57

积分

用户组: 小·技术宅

UID
7247
精华
0
威望
2 点
宅币
39 个
贡献
0 次
宅之契约
0 份
在线时间
2 小时
注册时间
2021-6-21
发表于 2021-6-21 07:10:53 | 显示全部楼层
不错不错
回复

使用道具 举报

1

主题

1

回帖

12

积分

用户组: 初·技术宅

UID
4063
精华
0
威望
2 点
宅币
6 个
贡献
0 次
宅之契约
0 份
在线时间
1 小时
注册时间
2018-7-17
发表于 2022-3-17 11:09:37 | 显示全部楼层
回复看看是啥
回复 赞! 靠!

使用道具 举报

0

主题

3

回帖

35

积分

用户组: 初·技术宅

UID
7754
精华
0
威望
2 点
宅币
28 个
贡献
0 次
宅之契约
0 份
在线时间
4 小时
注册时间
2022-3-26
发表于 2022-3-28 14:35:33 | 显示全部楼层
好咚咚 感謝
回复 赞! 靠!

使用道具 举报

0

主题

17

回帖

13

积分

用户组: 初·技术宅

UID
333
精华
0
威望
0 点
宅币
-4 个
贡献
0 次
宅之契约
0 份
在线时间
2 小时
注册时间
2014-6-4
发表于 2023-10-13 17:19:47 | 显示全部楼层
11111111111111
回复 赞! 靠!

使用道具 举报

0

主题

4

回帖

130

积分

用户组: 小·技术宅

UID
6727
精华
0
威望
2 点
宅币
122 个
贡献
0 次
宅之契约
0 份
在线时间
15 小时
注册时间
2021-3-10
发表于 2024-3-22 11:19:54 | 显示全部楼层
手动解析PE资源是写天书一样
回复 赞! 靠!

使用道具 举报

1

主题

44

回帖

991

积分

用户组: 大·技术宅

UID
7437
精华
0
威望
112 点
宅币
612 个
贡献
110 次
宅之契约
0 份
在线时间
133 小时
注册时间
2021-9-11
发表于 5 天前 | 显示全部楼层
还是需要看看代码
回复 赞! 靠!

使用道具 举报

9

主题

179

回帖

1万

积分

用户组: 真·技术宅

UID
4293
精华
6
威望
441 点
宅币
8683 个
贡献
850 次
宅之契约
0 份
在线时间
339 小时
注册时间
2018-9-19
发表于 3 天前 | 显示全部楼层
话说,驱动程序不是也可以用 Ldr 开头的那几个资源操作API吗?只有UEFI和非Windows系统下读取PE资源才要这么做吧。
回复 赞! 靠!

使用道具 举报

65

主题

117

回帖

1万

积分

用户组: 超级版主

OS与VM研究学者

UID
1043
精华
35
威望
789 点
宅币
8308 个
贡献
1094 次
宅之契约
0 份
在线时间
2071 小时
注册时间
2015-8-15
发表于 3 天前 | 显示全部楼层
YY菌 发表于 2024-4-23 10:11
话说,驱动程序不是也可以用 Ldr 开头的那几个资源操作API吗?只有UEFI和非Windows系统下读取PE资源才要这 ...

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

使用道具 举报

9

主题

179

回帖

1万

积分

用户组: 真·技术宅

UID
4293
精华
6
威望
441 点
宅币
8683 个
贡献
850 次
宅之契约
0 份
在线时间
339 小时
注册时间
2018-9-19
发表于 前天 09:09 | 显示全部楼层
tangptr@126.com 发表于 2024-4-23 17:11
少用乃至不用API对于安全行业来说是日常。到现在已经养成不搜API的习惯了。
而且Ldr的那些函数并不能直接 ...

Soga
回复 赞! 靠!

使用道具 举报

QQ|Archiver|小黑屋|技术宅的结界 ( 滇ICP备16008837号 )|网站地图

GMT+8, 2024-4-26 19:41 , Processed in 0.055136 second(s), 34 queries , Gzip On.

Powered by Discuz! X3.5

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表