📄 017.txt
字号:
● AddressOfEntryPoint字段
指出文件被执行时的入口地址,这是一个RVA地址(RVA的含义在下一节中详细介绍)。如果在一个可执行文件上附加了一段代码并想让这段代码首先被执行,那么只需要将这个入口地址指向附加的代码就可以了。
● ImageBase字段
指出文件的优先装入地址。也就是说当文件被执行时,如果可能的话,Windows优先将文件装入到由ImageBase字段指定的地址中,只有指定的地址已经被其他模块使用时,文件才被装入到其他地址中。链接器产生可执行文件的时候对应这个地址来生成机器码,所以当文件被装入这个地址时不需要进行重定位操作,装入的速度最快,如果文件被装载到其他地址的话,将不得不进行重定位操作,这样就要慢一点。
对于EXE文件来说,由于每个文件总是使用独立的虚拟地址空间,优先装入地址不可能被其他模块占据,所以EXE总是能够按照这个地址装入,这也意味着EXE文件不再需要重定位信息。对于DLL文件来说,由于多个DLL文件全部使用宿主EXE文件的地址空间,不能保证优先装入地址没有被其他的DLL使用,所以DLL文件中必须包含重定位信息以防万一。因此,在前面介绍的IMAGE_FILE_HEADER 结构的Characteristics字段中,DLL文件对应的IMAGE_FILE_RELOCS_STRIPPED位总是为0,而EXE文件的这个标志位总是为1。
在链接的时候,可以通过对link.exe指定/base:address选项来自定义优先装入地址,如果不指定这个选项的话,一般EXE文件的默认优先装入地址被定为00400000h,而DLL文件的默认优先装入地址被定为10000000h。
● SectionAlignment字段和FileAlignment字段
SectionAlignment字段指定了节被装入内存后的对齐单位。也就是说,每个节被装入的地址必定是本字段指定数值的整数倍。而FileAlignment字段指定了节存储在磁盘文件中时的对齐单位。
● Subsystem字段
指定使用界面的子系统,它的取值如表17.3所示。这个字段决定了系统如何为程序建立初始的界面,链接时的/subsystem:xxx选项指定的就是这个字段的值,在前面章节的编程中我们早已知道:如果将子系统指定为Windows CUI,那么系统会自动为程序建立一个控制台窗口,而指定为Windows GUI的话,窗口必须由程序自己建立。
表17.3 界面子系统的取值和含义
取 值
Windows.inc中的预定义值
含 义
0
IMAGE_SUBSYSTEM_UNKNOWN
未知的子系统
1
IMAGE_SUBSYSTEM_NATIVE
不需要子系统(如驱动程序)
2
IMAGE_SUBSYSTEM_WINDOWS_GUI
Windows图形界面
3
IMAGE_SUBSYSTEM_WINDOWS_CUI
Windows控制台界面
5
IMAGE_SUBSYSTEM_OS2_CUI
OS2控制台界面
7
IMAGE_SUBSYSTEM_POSIX_CUI
POSIX控制台界面
8
IMAGE_SUBSYSTEM_NATIVE_WINDOWS
不需要子系统
9
IMAGE_SUBSYSTEM_WINDOWS_CE_GUI
Windows CE图形界面
● DataDirectory字段
这个字段可以说是最重要的字段之一,它由16个相同的IMAGE_DATA_DIRECTORY结构组成,虽然PE文件中的数据是按照装入内存后的页属性归类而被放在不同的节中的,但是这些处于各个节中的数据按照用途可以被分为导出表、导入表、资源、重定位表等数据块,这16个IMAGE_DATA_DIRECTORY结构就是用来定义多种不同用途的数据块的(如表17.4所示)。IMAGE_DATA_DIRECTORY结构的定义很简单,它仅仅指出了某种数据块的位置和长度。
IMAGE_DATA_DIRECTORY STRUCT
VirtualAddress DWORD ? ;数据的起始RVA
isize DWORD ? ;数据块的长度
IMAGE_DATA_DIRECTORY ENDS
表17.4 数据目录列表的含义
索 引
索引值在Windows.inc中的预定义值
对应的数据块
0
IMAGE_DIRECTORY_ENTRY_EXPORT
导出表
1
IMAGE_DIRECTORY_ENTRY_IMPORT
导入表
2
IMAGE_DIRECTORY_ENTRY_RESOURCE
资源
3
IMAGE_DIRECTORY_ENTRY_EXCEPTION
异常(具体资料不详)
4
IMAGE_DIRECTORY_ENTRY_SECURITY
安全(具体资料不详)
5
IMAGE_DIRECTORY_ENTRY_BASERELOC
重定位表
6
IMAGE_DIRECTORY_ENTRY_DEBUG
调试信息
7
IMAGE_DIRECTORY_ENTRY_ARCHITECTURE
版权信息
8
IMAGE_DIRECTORY_ENTRY_GLOBALPTR
具体资料不详
9
IMAGE_DIRECTORY_ENTRY_TLS
Thread Local Storage
10
IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG
具体资料不详
11
IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT
具体资料不详
12
IMAGE_DIRECTORY_ENTRY_IAT
导入函数地址表
13
IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT
具体资料不详
14
IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR
具体资料不详
15
未使用
在PE文件中寻找特定的数据时就是从这些IMAGE_DATA_DIRECTORY结构开始的,比如要存取资源,那么必须从第3个IMAGE_DATA_DIRECTORY结构(索引为2)中得到资源数据块的大小和位置;同理,如果要查看PE文件导入了哪些DLL文件的哪些API函数,那就必须首先从第2个IMAGE_DATA_DIRECTORY结构得到导入表的位置和大小。
17.1.4 节表和节
从排列位置来看,PE文件在DOS部分和PE文件头部分以后就是节表和多个不同的节(如图17.1中的③和④所示)。要理解什么是节表,什么是节以及它们之间的关系,那就首先要了解Windows是如何将PE文件映射到内存的。
1. PE文件到内存的映射
在执行一个PE文件的时候,Windows并不在一开始就将整个文件读入内存,而是采用与内存映射文件类似的机制,也就是说,Windows装载器在装载的时候仅仅建立好虚拟地址和PE文件之间的映射关系,只有真正执行到某个内存页中的指令或者访问某一页中的数据时,这个页面才会被从磁盘提交到物理内存,这种机制使文件装入的速度和文件大小没有太大的关系。
图17.2 PE文件到内存的映射
但是系统装载可执行文件的方法又不完全等同于内存映射文件。当使用内存映射文件时,系统对“原著”非常忠实,如果将磁盘文件和内存映像对比一下,可以发现不管是数据本身还是数据之间的相对位置都是完全相同的。而装载可执行文件的时候,有些数据在装入前会被预先处理(如需要重定位的代码),而装入以后,数据之间的相对位置也可能改变,如图17.2所示,一个节的偏移和大小在装入内存前后可能是完全不同的。
Windows装载器在装载DOS部分、PE文件头部分和节表部分时不进行任何处理,而装载节的时候将根据节的属性做不同的处理,一般需要处理以下几个方面的内容。
● 内存页的属性
对于磁盘映射文件来说,所有的页都是按照磁盘映射文件函数指定的属性设置的,但是装载可执行文件时,与节对应的内存页的属性要按照节的属性来设置。所以在同属一个模块的内存页中,从不同节映射过来的内存页的属性是不同的。
● 节的偏移地址
节的起始地址在磁盘文件中是按照IMAGE_OPTIONAL_HEADER32结构的FileAlignment字段的值对齐的,而被装载到内存中时是按照同一结构中的SectionAlignment字段的值对齐的,两者的值可能不同,所以一个节被装入内存后相对于文件头的偏移和在磁盘文件中的偏移可能是不同的。
节是相同属性数据的组合,当节被装入到内存中的时候,同一个节对应的内存页将被赋予相同的页属性,Windows系统对内存属性的设置是以页为单位来进行的,所以节在内存中的对齐单位必须至少是一个页的大小,对于Win32来说,这个值是4 Kb(1000h),而对于Win64来说,这个值是8 Kb(2000h)。
节在磁盘文件中的对齐单位就没有最小4 Kb的限制,为了减少磁盘文件的大小,文件对齐的单位一般要小于内存对齐的单位(FileAlignment的值一般为200h),这样,在磁盘中就不必为每个节最后的零头数据补足4 Kb的大小了。
● 节的尺寸
对节尺寸的处理有两个方面,首先是由于磁盘映像和内存映像中节对齐单位的不同而造成的长度扩展;其次是对包含未初始化数据的节的处理(如图17.2中的.data节)。
对于未初始化数据来说(比如在源代码中定义的.data?段),没必要为它们在磁盘文件中预留空间,只要在可执行文件被装载到内存中后为它们分配空间就可以了,所以包含未初始化数据的节在磁盘文件中的长度被定义为0,但是被装载到内存中的地址和大小是被明确指定的。
对于这种节来说,它所包含的内存页并没有磁盘文件内容与之对应,这些内存页是Windows装载器根据节的定义额外开辟出来的。
● 不进行映射的节
有些节中包含的数据仅仅在装载的时候用到,当文件装载完毕的时候,它们不会被递交到物理内存页。最典型的例子就是包含重定位数据的节(如图17.2中的.reloc节),重定位数据对于文件的执行代码来说是透明的,它只供Windows装载器使用,执行代码根本不会去访问它们,一旦装载完毕,继续为它们提交内存页是一种浪费。所以这些节存在于磁盘文件中,但并不会被映射到内存中。
2. 节表
PE文件中所有节的属性都被定义在节表中,节表由一系列的IMAGE_SECTION_HEADER结构排列而成,每个结构用来描述一个节,结构的排列顺序和它们描述的节在文件中的排列顺序是一致的。全部有效结构的最后以一个空的IMAGE_SECTION_HEADER结构作为结束,所以节表中总的IMAGE_SECTION_HEADER结构数量等于节的数量加一。节表总是被存放在紧接在PE文件头的地方,也就是从PE文件头(注意:不是文件本身的头部)开始的偏移为00f8h的地方。
IMAGE_SECTION_HEADER结构的定义如下:
IMAGE_SECTION_HEADER STRUCT
Name1 db IMAGE_SIZEOF_SHORT_NAME dup(?) ;8个字节的节区名称
union Misc
PhysicalAddress dd ?
VirtualSize dd ? ;节区的尺寸
ends
VirtualAddress dd ? ;节区的RVA地址
SizeOfRawData dd ? ;在文件中对齐后的尺寸
PointerToRawData dd ? ;在文件中的偏移
PointerToRelocations dd ? ;在OBJ文件中使用
PointerToLinenumbers dd ? ;行号表的位置(供调试用)
NumberOfRelocations dw ? ;在OBJ文件中使用
NumberOfLinenumbers dw ? ;行号表中行号的数量
Characteristics dd ? ;节的属性
IMAGE_SECTION_HEADER ENDS
结构中的有些字段是供COFF格式的obj文件使用的,对可执行文件来说不代表任何意义,在分析的时候可以不予理会,真正有用的几个字段说明如下。
● Name1字段
这个字段的字段名原来应该是“Name”,但是这个名称和MASM中的关键字冲突,所以在定义的时候改为“Name1”,Name1字段定义了节的名称,字段的长度为8个字节。
PE文件中的节的名称是一个由ANSI字符组成的字符串,但并没有规定以0结束,如果节的名称字符串长度小于8个字节的话,后面以0补齐,但是字符串长度达到8个字节的话,后面就没有0字符了,所以在处理的时候要注意字符串的结束方式。
每个节的名称是惟一的,不能有同名的两个节,但是节的名称不代表任何含义,它仅仅是为了查看方便而设置的一个标记而已,可以选择任何名称甚至将它空着也可以,将包含代码的节命名为“DATA”或者将包含数据的节命名为“CODE”都是合法的。
各种编译器都以自己的方式对节进行命名,所以,在PE文件中可以看到各式各样的节名称,比如,在MASM32产生的可执行文件中,代码节被命名为“.text”;可读写的数据节被命名为“.data”;包含只读数据、导入表以及导出表的节被命名为“.rdata”;而资源节被命名为“.rsrc”等。但是在其他一些编译器中,导入表被单独放在“.idata”中;而代码节可能被命名为“.code”。
当从PE文件中读取需要的节时,不能以节的名称作为定位标准,正确的方法是按照IMAGE_OPTIONAL_HEADER32结构中的数据目录字段定位。
笔者曾看过一篇介绍如何存取PE文件资源的文章,其中用查找“.rsrc”节的方法得到资源,虽然大部分情况下用这种方法也可以正确地找到资源,但是严格地讲,只有数据目录的IMAGE_DIRECTORY_ENTRY_RESOURCE项才永远正确地指向资源数据。
● VirtualSize字段
代表节的大小,这是节的数据在没有进行对齐处理前的实际大小。
● VirtualAddress字段
指出节被装载到内存中后的偏移地址,这是一个RVA地址。这个地址是按照内存页对齐的,它的数值总是SectionAlignment的值的整数倍。
● PointerToRawData字段
指出节在磁盘文件中的所处的位置。这个数值是从文件头开始算起的偏移量。
● SizeOfRawData字段
指出节在磁盘文件中所占的空间大小,这个数值等于VirtualSize字段的值按照FileAlignment的值对齐以后的大小。
依靠这4个字段的值,装载器就可以从PE文件中找出某个节(从PointerToRawData偏移开始的SizeOfRawData字节)的数据,并将它映射到内存中去(映射到从模块基地址开始偏移VirtualAddress的地方,并占用以VirtualSize的值按照页的尺寸对齐后的空间大小)。
● Characteristics字段
这是节的属性标志字段,其中的不同数据位代表了不同的属性,具体的定义如表17.5所示,这些数据位组合起来描述了节的属性。
表17.5 节的属性标志位含义
位
数据位在Windows.inc中的预定义值以及为1时的含义
5
(IMAGE_SCN_CNT_CODE或00000020h)节中包含代码
6
(IMAGE_SCN_CNT_INITIALIZED_DATA或00000040h)节中包含已初始化数据
7
(IMAGE_SCN_CNT_UNINITIALIZED_DATA或00000080h)节中包含未初始化数据
25
(IMAGE_SCN_MEM_DISCARDABLE或02000000h)节中的数据在进程开始以后将被丢弃,前面举例的包含重定位表的.reloc节就是一个例子
26
(IMAGE_SCN_MEM_NOT_CACHED或04000000h)节中的数据不会经过缓存
27
(IMAGE_SCN_MEM_NOT_PAGED或08000000h)节中的数据不会被交换到磁盘
28
(IMAGE_SCN_MEM_SHARED或10000000h)表示节中的数据将被不同的进程所共享,在第11章的钩子例子中的共享数据的节就设置了这个属性标志
29
(IMAGE_SCN_MEM_EXECUTE或20000000h)映射到内存后的页面包含可执行属性
30
(IMAGE_SCN_MEM_READ或40000000h)映射到内存后的页面包含可读属性
31
(IMAGE_SCN_MEM_WRITE或80000000h)映射到内存后的页面包含可写属性
代码节的属性一般为60000020h,也就是可执行、可读和“节中包含代码”;数据节的属性一般为c0000040h,也就是可读、可写和“包含已初始化数据”;而常量节(对应源代码中的.const段)的属性为40000040h,也就是可读和“包含已初始化数据”;资源节的属性和常量节的属性一般是相同的。
当然节属性的定义不一定就是这些值,比如,当PE文件被压缩工具压缩以后,包含代码的节往往被同时设置了可执行、可读和可写属性,因为解压部分需要将解压后的代码回写到代码段中。读者可以做个实验:在程序中往代码段写数据,编译链接完成后执行一下肯定会引发异常,然而用Upx等压缩软件压缩后再执行,就会发现文件可以正常执行了,这就是因为压缩软件为了解压的需要而将节的属性设置为可写了。
3. RVA和文件偏移的转换
在前面的内容中已经多次提到“RVA”一词,对于初次接触PE文件的读者来说,RVA是个比较费解的概念,特别是在一开始就去接触RVA的情况下。RVA的概念是和PE文件从磁盘到内存的映射息息相关的,在了解了这方面的内容后,再来看RVA就不成问题了。
RVA是相对虚拟地址(Relative Virtual Address)的缩写,顾名思义,它是一个“相对”地址,也可以说是“偏移量”,PE文件的各种数据结构中涉及到地址的字段大部分都是以RVA表示的。
准确地说,RVA就是当PE文件被装载到内存中后,某个数据的位置相对于文件头的偏移量。举个例子,如果Windows装载器将一个PE文件装入00400000h处的内存中,而某个节中的某个数据被装入0040xxxxh处,那么这个数据的RVA就是(0040xxxxh-00400000h)=xxxxh,反过来说,将RVA的值加上文件被装载的基地址,就可以找到数据在内存中的实际地址。
图17.3 RVA的含义
很明显,PE文件中出现RVA的概念是因为PE的内存映像和磁盘文件映像是不同的,如图17.3中的A和A~所示,同一数据相对于文件头的偏移量在内存中和在磁盘文件中可能是不同的,为了提高效率,PE文件头中使用的都是内存映像中的偏移量,也就是RVA。从图17.3中也可以得到另一个结论,那就是RVA仅仅是对于处于节中的数据而言的,对于文件头和节表来说无所谓RVA和文件偏移,因为它们在被映射到内存中后不管是大小还是偏移都不会有任何改变。
使用RVA,使文件装入内存后的数据定位变得方便,然而却给处理磁盘上的静态PE文件带来了很大的麻烦,举例来说,假如要读取PE文件中的资源(如图17.3中的A所示),第一个步骤就是从PE文件头的数据目录中访问第3个IMAGE_DATA_DIRECTORY结构,并从结构中得到资源所处的偏移量,但是这样得到的偏移量是个RVA,它只能用于在内存中查找由A~位置所指示的资源。用它直接在磁盘文件中定位A位置是错误的。
当处理PE文件时,任何的RVA必须经过到文件偏移的换算,才能用来定位并访问文件中的数据,但换算却无法用一个简单的公式来完成,事实上,惟一可用的方法就是最土最笨的方法:
(1)循环扫描节表并得到每个节在内存中的起始RVA(根据VirtualAddress字段),并根据节的大小(SizeOfRawData字段)算出节的结束RVA,最后比较判断目标RVA是否落在某个节之内。
(2)如果目标RVA处于某个节之内,那么用目标RVA减去节的起始RVA,这样就得到了目标RVA相对于节起始地址的偏移量RVA~。
(3)在节表中获取节在文件中所处的偏移(PointerToRawData字段),将这个偏移值加上上一步得到的RVA~值,这才是数据在文件中的真正偏移位置。
这里是两个通用的函数,其中_RVAToOffset函数将RVA转换成文件偏移,输入的参数是已经读取到内存中的文件头的地址和RVA的值;_GetRVASection函数用来获取RVA所在的节的名称。这两个函数被保存在_RvaToFileOffset.asm文件中,并将在以后的例子中用到,函数中使用的算法就是上面3个步骤中列出的算法。
.const
szNotFound db ~无法查找~,0
.code
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 将 RVA 转换成文件偏移
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
_RVAToOffset proc _lpFileHead,_dwRVA
local @dwReturn
pushad
mov esi,_lpFileHead
assume esi:ptr IMAGE_DOS_HEADER
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -