PE结构&整体叙述

PE体系

PE结构&整体叙述

PE结构&导入表

PE结构&导出表

PE结构&基址重定位表

PE结构&绑定导入实现

PE结构&延迟加载导入表

文章目录

声明

以下皆为手打,并非复制粘贴,一方面是为了自己以后复习,另一方面希望能方便大家。

初识PE

PE(Portable Executeable File Format,可移植的执行体文件格式),使用该格式的目标是使连接生成的EXE能在不同的CPU工作指令下工作。
可执行文件的格式是操作系统工作方式的真实写照。Windows操作系统中可执行程序有好多种,比如COM,PIF,SCR,EXE等,这些文件的格式大部分都继承自PE。其中,EXE是最常见的PE 文件,动态链接库(大部分以dll为扩展名的文件)也是PE文件

内存映射文件与PE内存映像

内存映射文件:是指将硬盘上的文件不做修改地装载到内存中。这样,文件中字节与字节之间就是顺序排列的了。
解释:
在硬盘上,文件被分割成若干簇,这些簇不一定会按照文件内容顺序排列在一起,当我们访问磁盘上的文件时,需要计算机首先将不同位置的内容读取到内存。有了内存映射文件,访问就会变得更轻松和快捷,由于读取磁盘的操作集中到一起
执行,读写效率会提高很多。被一次性读取到内存的文件字节按线性排列,访问相对简单,速度也提升了不少。

PE内存映像:是指将PE文件按照一定的规则装载到内存中,装入后的整个文件头内容不会发送变化,但PE文件的某一部分如节的内容会按照字段中的
对齐方式在内存中对齐,从而使得内存中的PE映像与装载前的PE文件不同。

为什么PE内存映像不能和一般的内存映射文件一样呢?
因为PE文件是由操作性装载进内存的,其目的是为了运行。为了配合操作系统的运行,方便调度,提高运行效率,PE映像必须按照一定的格式对齐,所以内存中的PE映像和原来硬盘上的文件时不同的,当然与内存映射文件也就不同。

PE文件中的对齐

数据在内存中的对齐

由于Windows操作系统对内存属性结构的设置以页为单位,所以通常情况下,节在内存中的对齐单位必须至少是一个页的大小。对32位的Windows XP操作系统来说,这个值是4KB(1000h);而对于64位操作系统来说,这个值就是8KB(2000h)。

数据在文件中的对齐

为了提高磁盘利用率,通常情况下,定义的节在文件中的对齐单位要远小于内存对齐单位;通常会以一个物理扇区的大小作为对齐粒度的值,即512字节,十六进制是200h

处于节约资源考虑,操作系统允许节在内存和文件中的对齐尺度不一致。这就直接造成PE在文件中和在内存中的大小也会不一致。通常情况下,PE在内存中的尺寸要比在文件中的尺寸要大。用户可以自定义这些对齐的值。

注意:
如果内存对齐被定义为小于操作系统页的大小,则文件对齐和内存对齐的值必须一致

资源文件中资源数据的对齐

资源文件中,资源字节码部分一般会要求以双字(4个字节)方式对齐,在资源表中详解。

在这里插入图片描述
在PE格式中,每一个大的部分的对齐方式就是按照200h大小对齐的

在汇编语言中,Unicode字符串被定义为一个结构体,定义如下:

typedef struct _NUICODE_STRING{
	USHORT Length;     	//字符串的长度(字节数)
	USHORT	MaximumLength;//字符串缓冲区的长度(字节数)
	PWSTR	Buffer;	//字符串缓冲区
}	UNICODE_STRING , *PUNICODE_STRING;

16系统下的PE结构

DOS头部分的存在见证了PE强大兼容性。为了保持与16位系统的兼容,在PE里依旧保留了16位系统下的标准可执行程序执行时所必需的文件头部(DOS MZ头)和指令代码(DOS Stub)。
在16位操作系统下,PE结构可以分为两部分:DOS头和冗余数据,如下
在这里插入图片描述
在16位系统下,PE 的四部分内容被重新组合成两部分——可以在16位系统下运行的DOS头和冗余数据。把Windows下的PE文件存储到DOS系统并运行,它就是DOS系统下的一个EXE文件。
DOS头分为两部分,DOS MZ头和DOS Stub(即指令字节码)。大部分情况下,这些指令实现的功能都非常简单,根本不会涉及重定位信息。再往后的PE头和PE数据区可以看成是16操作系统下的可执行文件的冗余数据。

DOS MZ 头(长度0x40

13B7:0100  4D 5A 90 00 03 00 00 00-04 00 00 00 FF FF 00 00   MZ..............
13B7:0110  B8 00 00 00 00 00 00 00-40 00 00 00 00 00 00 00   ........@.......
13B7:0120  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00   ................
13B7:0130  00 00 00 00 00 00 00 00-00 00 00 00 B0 00 00 00   ................

在Windows的PE格式中,DOS MZ头定义如下:

IMAGE_DOS_EADER STRUCT
e_magic				WORD			?	;0000h -  EXE标志,“MZ”
e_cblp				WORD			?	;0002h	-	最后(部分页中的字节数)
c_cp				WORD			?	;0004h	-	文件中的全部和部分页数
e_crlc				WORD			?	;0006h	-	重定位表中的指针
e_cparhdr			WORD			?	;0008h	-	头部尺寸,以段落为单位
e_minalloc			WORD			?	;000ah	-	所需的最小附加段
e_maxalloc			WORD			?	;000ch	-	所需的最大附加段
e_ss				WORD			?	;000eh	-	初始的SS值(相对偏移)
e_sp				WORD			?	;0010h	-	初始的SP值
e_csum				WORD			?	;0012h	-	补码校验值
e_ip				WORD			?	;0014h	-	初始的IP值
e_cs				WORD			?	;0016h	-	初始的CS值
e_lfarlc			WORD			?	;0018h	-	重定位表的字节偏移量
e_ovmo				WORD			?	;001ah	-	覆盖号
e_res				WORD		4 dup(?);001ch	-	保留字
e_oemid				WORD			?	;0024h	-	OEM标识符(相对e_oeminfo)
e_oeminfo			WORD			?	;0026h	-	OEM信息
e_res2				WORD		10dup(?);0028h	-	保留字
e_lfanew			WORD			?	;003ch	-	PE头相对于文件的偏移地址

如上所示,偏移1c以后(包括1c)在16系统下是没有定义的。由于其开始的标志字为“MZ”(Mark Zbikowski,它是DOS操作系统的开发者之一),所以称它为“DOS MZ头”。

注意:DOS MZ头部分的字节码(包括DOS Stub程序字节码)的添加是由链接程序link.exe自动实现的。所以在源程序.asm中是找不到相应的定义语句的。

DOS Stub(长度不固定)

由于DOS Stub的大小 不固定,因此DOS头 的大小也是不固定的。DOS Stub部分是该程序在DOS 系统下运行的指令字节码
举个例子:
一个程序的DOS Stub(指令字节码)如下:

13B7:0140  0E 1F BA 0E 00 B4 09 CD-21 B8 01 4C CD 21 54 68   ........!..L.!Th
13B7:0150  69 73 20 70 72 6F 67 72-61 6D 20 63 61 6E 6E 6F   is program canno
13B7:0160  74 20 62 65 20 72 75 6E-20 69 6E 20 44 4F 53 20   t be run in DOS 
13B7:0170  6D 6F 64 65 2E 0D 0D 0A-24 00 00 00 00 00 00 00   mode....$.......
13B7:0180  5D 5C 6D C1 19 3D 03 92-19 3D 03 92 19 3D 03 92   ]\m..=...=...=..
13B7:0190  97 22 10 92 1E 3D 03 92-E5 1D 11 92 18 3D 03 92   ."...=.......=..
13B7:01A0  52 69 63 68 19 3D 03 92-00 00 00 00 00 00 00 00   Rich.=..........

那么这些指令都做了哪些工作:
首先将这个程序名更改为“123”,然后复制到C:\Documents and Settings \administrator中。在命令提示符下输入下面指令:

dubug 123 -U 0140 014d

然后显示

13B7:0140 0E            PUSH	CS         ;将CS段地址给DS                        
13B7:0141 1F            POP	DS                                 
13B7:0142 BA0E00        MOV	DX,000E         ;DS:DX指向要显示的字符串014e处                   
13B7:0145 B409          MOV	AH,09           ;调用9号中断,屏幕显示字符串                  
13B7:0147 CD21          INT	21                                 
13B7:0149 B8014C        MOV	AX,4C01         ;调用4C号中断,正常退出程序                         
13B7:014C CD21          INT	21                                 
13B7:014C 5468……………………	数据区“This program”	;要显示的字符串  

它就调用了int 21中断的9号功能,实现在屏幕输出一段字符串。

32位系统下的PE结构

16位系统中,PE头和PE数据部分被当成是冗余数据;在32位系统中,刚好相反,即DOS头称为冗余数据。所谓冗余,是针对DOS头不参与32位系统运行过程而言的。尽管该部分不参与运行,但也不能把这些数据从PE结构除去。在DOS MZ头中有一个IMAGE_DOS_HEADER.e_lfanew,没有它操作系统就定位不到标准的PE头部,这个可执行程序就会被操作系统认为是非法的PE映像

定位标准PE头

DOS头放在PE 的起始位置,然后在DOS MZ头中有一个字段e_lfanew专门去找标准PE头的偏移。该地段的值是一个相对偏移量。绝对定位的时候得加上DOS MZ 的基地址。

PE文件结构

在32位系统下,最重要的部分就是PE头和PE数据区。如下图
在这里插入图片描述
32位操作系统下的PE文件结构被划为为5个部分:
DOS MZ头,DOS Stub,PE头,节表和节内容。
节表和节内容两部分其实就是图中的PE数据区。DOS MZ头的大小是0x40(十进制:64)个字节,PE头的大小是456个字节(由于数据目录表项不一定是16个,准确地说,PE头也是一个不能确定大小的结构,该结构的实际大小由字段IMAGE_FILE_HEADER.SizeOfOptionalHeader来确定)。节表的大小之所以不固定,因为每个PE中节的数量是不固定的。但是每个节的描述信息是则是个固定值,共40个字节,节表是由不确定数量的节描述信息组成,其大小等于节的数量 X 40,节的数量由字段IMAGE_FILE_HEARDER.NumberOfSections来定义。DOS Stub和节内容都是大小不确定的。(前面所说的是节的描述信息是固定的,40字节,并非说节的内容是固定的,别混淆。)
虽然节的描述信息确定,但是节的数量却是不确定的,所以呢,节内容自然也就不确定喽。
PE文件头部 = DOS头 +PE头。

总览PE结构

在这里插入图片描述
标准的PE文件一般由四大部分组成:

  1. DOS头
  2. PE头(IMAGE_NT_HEADERS
  3. 节表(多个IMAGE_SECTION_HEADERS结构)
  4. 节内容

详述:
DOS头的话,分为DOS MZ头 和DOS Stub

PE头(IMAGE_NT_HEADERS)包括了4字节的标识符号(Signature),20个字节的基本头信息(IMAGE_FILE_HEADER),216个字节的扩展头信息(IMAGE_OPTIONAL_HEADER32
(即PE头=Signature+IMAGE_FILE_HEADER+IMAGE_OPTIONAL_HEADER32

PE文件头部=DOS头+PE头 +节表(PE头和PE文件头部不是一个意思,别混淆。)

PE文件身体 = 节内容

PE文件头部解析

DOS MZ头IMAGE_DOS_HEADER

IMAGE_DOS_EADER STRUCT
e_magic				WORD			?	;0000h -  EXE标志,“MZ”
e_cblp				WORD			?	;0002h	-	最后(部分页中的字节数)
c_cp				WORD			?	;0004h	-	文件中的全部和部分页数
e_crlc				WORD			?	;0006h	-	重定位表中的指针
e_cparhdr			WORD			?	;0008h	-	头部尺寸,以段落为单位
e_minalloc			WORD			?	;000ah	-	所需的最小附加段
e_maxalloc			WORD			?	;000ch	-	所需的最大附加段
e_ss				WORD			?	;000eh	-	初始的SS值(相对偏移)
e_sp				WORD			?	;0010h	-	初始的SP值
e_csum				WORD			?	;0012h	-	补码校验值
e_ip				WORD			?	;0014h	-	初始的IP值
e_cs				WORD			?	;0016h	-	初始的CS值
e_lfarlc			WORD			?	;0018h	-	重定位表的字节偏移量
e_ovmo				WORD			?	;001ah	-	覆盖号
e_res				WORD		4 dup(?);001ch	-	保留字
e_oemid				WORD			?	;0024h	-	OEM标识符(相对e_oeminfo)
e_oeminfo			WORD			?	;0026h	-	OEM信息
e_res2				WORD		10dup(?);0028h	-	保留字
e_lfanew			WORD			?	;003ch	-	PE头相对于文件的偏移地址

注意:
注释后的偏移是基于IMAGE_DOS_HEADER头的

DOS Stub (略过)

DOS MZ 头的下面是DOS Stub。整个DOS Stub是一个字节块,其内容随着链接时使用的链接器不同而不同,PE中并没有与之对应的相关结构,所以不需要纠结。

PE头标识Signature

紧跟在DOS Stub后面的是PE头标识Signature。与大部分文件格式的头部结构一样,PE头部信息中有一个四字节的标识,该标识位于指针IMAGE_DOS_HEADER.e_lfanew指向的位置。其内容固定,对应于ASCII码的字符串“PE\0\0”

标准PE头IMAGE_FILE_HEADER

标准PE头IMAGE_FILE_HEADER紧跟于PE标识头后,位于IMAGE_DOS_HEADERe_lfanew+4的位置。由此位置开始的20字节为数据结构标准PE头IMAGE_FILE_HEADER的内容,此结构在微软的官方文档中被称标准通用对象文件格式(Common Object File Format,COFF)头。它记录了PE文件的全局属性,定义如下:

IMAGE_FILE_HEADER STRUCT
	Machine					WORD  	?;0004h	-	运行平台
	NumberOfSections		WORD	?;0006h	-	PE中节的数量
	TimeDateStamp			DWORD	?;0008h	-	文件创建日期和时间
	PointerToSymbolTable	DWORD	?;000ch	-	指向符号表(用于调试)
	NumberOfSymbols			DWORD	?;0010h	-	符号表中的符号数量
	SizeOfOptionalHeader	WORD	?;0014h	-	扩展头结构的长度
	Characteristics			WORD	?;0016h	-	文件属性

注意:
注释后的偏移是基于IMAGE_NT_HEADERS头的

标准PE头IMAGE_OPTIONAL_HEADER32

详细定义如下:

IMAGE_OPTIONAL_HEADER32	STRUCT
Magic WORD										?;0018h	-魔术字 107h=ROM Image 10Bh=exe Image
MajorLinkerVersion			BYTE				?;001ah	-链接器版本号
MinorLinkerVersion			BYTE				?;001bh	-
SizeOfCode					DWORD				?;001ch	-所有含代码的节的总大小
SizeOfInitializedData		DWORD				?;0020h	-所有含已初始化数据的节的总大小
SizeOfUninitializedData		DWORD				?;0024h	-所有含未初始化数据的节的大小
AddressOfEntryPoint			DWORD				?;0028h	-程序执行入口RVA
BaseOfCode					DWORD				?;002ch	-代码的节的起始RVA
BaseOfData					DWORD				?;0030h	-数据的节的起始RVA
ImageBase					DWORD				?;0034h	-程序的建议装载地址
SectionAlignment			DWORD				?;0038h	-内存中的节的对齐粒度
FileAlignment				DWORD				?;003ch	-文件中的节的对齐粒度
MajorOperatingSystemVersion	WORD				?;0040h	-操作系统版本号
MinorOperatingSystemVersion	WORD				?;0042h	-
MajorImageVersion			WORD				?;0044h	-该PE的版本号
MinorImageVersion			WORD				?;0046h	-
MajorSubsystemVersion		WORD				?;0048h	-所需子系统的版本号
MinorSubsystemVersion		WORD				?;004ah	-
Win32VersionValue			DWORD				?;004ch	-未用
SizeOfImage					DWORD				?;0050h	-内存中的整个PE映像尺寸
SizeOfHeaders				DWORD				?;0054h	-所有头+节表的大小
CheckSum					DWORD				?;0058h	-校验和
Subsystem					WORD				?;005ch	-文件的子系统
DllCharacteristics			WORD				?;005eh	-DLL文件特性
SizeOfStackReserve			DWORD				?;0060h	-初始化时的栈大小
SizeOfStackCommit			DWORD				?;0064h	-初始化时实际提交的栈大小
SizeOfHeapReserve			DWORD				?;0068h	-初始化时保留的堆大小
SizeOfHeapCommit			DWORD				?;006ch	-初始化时实际提交的堆大小
LoaderFlags					DWORD				?;0070h	-与调试有关
NumberOfRvaAndSizes			DWORD				?;0074h	-下面的数据目标结构的项目数量
DataDirectory	IMAGE_DATA_DIRECTORY 16dup(<>)	?;0078h	-	
IMAGE_OPTIONAL_HEADER32	ENDS

注意:
注释后的偏移是基于IMAGE_NT_HEADERS头的

文件执行时的入口地址,文件被操作系统装入内存后的默认基地址,以及节在磁盘和内存中的对齐单位等信息均可以在上面结构中找到。

PE头IMAGE_NT_HEADERS

这个结构是广义上的PE头,在标准的PE文件中其大小为456字节。它是SignatureIMAGE_FILE_HEADERIMAGE_OPTIONAL_HEADER32这三个数据结构的组合。
在这里插入图片描述
该结构的详细定义如下:

IMAGE_NT_HEADERS STRUCT
	Signature		DOWRD						?;			-PE文件标识,"PE\0\0"
	FileHeader		IMAGE_FILE_HEADER			<>;0004h	-PE标准头
	OptionalHeader	IMAGE_OPTIONAL_HEADER32		<>;0018h	-PE扩展头

数据目录项 IMAGE_DATA_DIRECTORY

IMAGE_OPTIONAL_HEADE32(扩展PE头)结构的最后一个字段为DataDirectory。该字段定义了PE文件中出现的所有不同类型的数据的目录信息。结构定义如下:

IMAGE_DATA_DIRECTORY	STRUCT
	VirtualAddress			DWORD		?;0000h	-数据的起始RVA
	isize					DWORD		?;0004h	-数据块的长度

总的数据目录一共由16个相同的IMAGE_DATA_DIRECTORY结构连续排列在一起组成,示意图如下:
在这里插入图片描述
这16个元组的数据每一项均代表PE中的某一个类型的数据,数据类型如下:

0			Export table address  and size      				导出表地址和大小
1			Import table address  and size      				导入表地址和大小
2			Resource table address  and size      				资源表地址和大小
3			Exception table address  and size      				异常表地址和大小
4			Certificate table address  and size      			属性证书数据地址和大小
5			base relocation table address  and size      		基址重定位表地址和大小
6			Debugging information starting address  and size      				调试信息地址和大小
7			Architecture-specific data      				预留为0
8			Global pointer register relative virtual address     				指向全局指针寄存器的值
9			Thread local storage(TLS) table address  and size   线程局部存储地址和大小
10			Load configuration table address  and size      加载配置表地址和大小
11			Bound import table address  and size      绑定导入表地址和大小
12			Import  address table and size      		导入函数地址表和大小
13			Delay import descriptor address and size      延迟导入表地址和大小
14			CLR	Runtime Header	address and size      CLR运行时头部数据地址和大小
15			Reserved      										系统保留

节表项 IMAGE_SECTION_HEADER

PE头IMAGE_NT_HEADERS后紧跟着节表。它由许多个节表项(IMAGE_SECTION_HEADER)组成,每个节表项记录了PE中某个特定的节有关的信息,如节的属性,节的大小,在文件和内存中的起始位置等。节表中节的数量由字段IMAGE_FILE_HEADER.NumberOfSections来定义。节表项的数据结构定义如下:

IMAGE_SECTION_HEADER	STRUCT
	Namel db	IMAGE_SIZEOF_SHORT_NAME dup(?);0000h	-8个字节节名
	union Misc
		PhysicalAddress						dd?
		VirtualSize						 	dd?;0008h	-	节区的尺寸
	ends
	VirtualAddress							dd?;000ch	-	节区的RVA地址
	SizeOfRawData							dd?;0010h	-在文件中对齐后的尺寸
	PointerToRawData						dd?;0014h	-在文件中的偏移
	PointerToRelocations					dd?;0018h	-在OBJ文件中使用
	PointerToLinenumbers					dd?;001ch	-行号表的位置(供调试用)
	NumberOfRelocations						dw?;0020h	-在OBJ文件中使用
	NumberOfLinenumbers						dw?;0022h	-行号表中行号的数量
	Characteristics							dd?;0024	-节的属性
	IMAGE_SECTION_HEADER	ENDS

注意:
注释后的偏移是基于IMAGE_SECTION_HEADER头的

数据结构字段详解

IMAGE_NT_HEADER.Signature

+0000h,双字。PE文件标识,被定义为00004550h。也就是“P”,“E”加上两个0,0x45就是“E”,0x50就是“P”,这也是PE这个称呼的由来.如果更改其中的任何一个字节,操作系统就无法把该文件识别为正确的PE文件。通过修改这个字段,会导致PE文件在32位系统中加载失败,但由于文件的其他部分(特别是DOS头)并没有破坏,系统还是可以识别出其为DOS系统下的可执行程序,并通过调用纯DOS环境来运行DOS Stub中的程序代码。
确认操作系统中的某个PE文件携带病毒,并且开机后会被加载进内存运行,最简单的处理办法是通过Windows PE盘启动系统,在系统中找到病毒文件,使用记事本简单地修改其中任何一个字符,保存文件,重新开机启动后即可防止病毒文件被加载

注意:
此PE非彼PE,windows PE是一个操作系统,其全称为:Windows PreInstallation Environment,即Windows的预安装环境。该操作系统区别于Windows XP/2000/Vista等,可以从光盘引导。

IMAGE_NT_HEADER.FileHeader

+0004,结构。该结构指向IMAGE_FILE_HEADER,由于PE扩展自通用COFF规范,所以,该字段在官方文档中被称为标准COFF头

IMAGE_NT_HEADER.OptionalHeader

+0018h,结构。该结构指向IMAGE_OPTIONAL_HEADER32。在符合COFF规范的“.obj”目标文件中该部分并不存在,所以被称为OptionalHeader(“可选头”)
可选头又分为两部分,前10个字段原属于COFF,用来加载和执行一个可执行文件后21个字段则是通过链接器追加的。作为PE扩展部分,用于描述可执行文件的一些信息,供PE加载器加载使用。

IMAGE_FILE_HEADER的字段详解

IMAGE_FILE_HEADER.Machine

+0004,单字。用来指定PE文件运行的平台。由于Windows最初被设计为可以运行在Intel,Sun,Dec,IBM等多种硬件平台上,或者能模拟这些平台的软件环境中,而不同的硬件平台其指令的机器码不相同,因此为不同平台编译的EXE是无法通用的。
下面列出常见值:
在这里插入图片描述

IMAGE_FILE_HEADER.NumberOfSections

+0006h,单字。文件中存在的节的总数。Windows XP中,可以有0个节,但数值不能小于1,也不能超过96。如果将该值设置为0,则操作系统装载时会提示不是有效的win32程序。如果想在PE中增加或删除节,必须变更此处的值。
另外,这个值既不能比实际内存中存在的节多,也不能比它少,否则装载时会发生错误,提示不是有效的Win32应用程序

IMAGE_FILE_HEADER.TimeDateStamp

+0008h,双字。编译器创建此文件时的时间戳。低32为存放的值是字1970年1月1日00:00时开始到创建时间为止的总秒数。
该数值可以随意修改而不会影响程序运行。所以,有的链接器在这里填入固定的值,有的则随意写入任何值,这对用户创建的文件并没有实际的意义。另外,这个时间值与操作系统文件属性可以看到的三个时间(创建时间,修改时间,访问时间)也没用任何联系

IMAGE_FILE_HEADER.PointerToSymbolTable

+000Ch,双字。COFF符号表的偏移。如果不存在COFF符号表,此值为0。对于映像文件来说,此值为0,因为微软已经不赞成在PE中使用COFF调试信息。

IMAGE_FILE_HEADER.NumberOfSymbols

+0010h,双字。符号表中元素的数目。由于字符串表紧跟在符号表后,所以可以利用这个值来定位字符串表。对于映像文件来说,此值为0,主要用于调试。

IMAGE_FILE_HEADER.SizeOfOptionalHeader

+0014h。单字。指定结构IMAGE_OPTIONAL_HEADER32的长度,默认情况下这个值等于00e0h;如果是64位PE文件,该结构的默认大小为00F0h
用户可以自己定义这个值的大小,不过需要注意两点:
(1)更改完以后,需要自行将文件中IMAGE_OPTIONAL_HEADER32的大小扩充为你指定的值(一般以0补足)
(2)扩充完以后,要维持文件中的对齐特性(比如在HelloWorld.exe中,此处增加了8个字节后,一定要在后面相应删除8个字节,以保证.text节起始位置处于0400h

IMAGE_FILE_HEADER.Characteristics

+0016h,单字。文件属性标志字段,它的不同数据位定义了不同的文件属性。
在这里插入图片描述

解释:

  1. 当位13为1时,这表示是一个DLL文件,那么系统将使用调用DLL入口函数的方式执行文件入口函数;当位13为0时,表示这是一个普通的可执行文件,系统直接跳到入口处执行。对于普通的可执行PE文件来说,这个字段的值一般是0fh,而对于DLL文件来说,这个字段的值一般是210eh。
  2. 当第0位为1时,表明此文件不包含基址重定位信息,因此必须将其加载到文件头中指定的基地址字段位置。如果进程空间此处的基地址被占用,加载器会报错。在程序运行前如果发现文件中存在可重定位信息,链接器会执行移出可执行文件中的重定位信息的操作。
  3. 当第1位为1时,表明此映像文件是合法的,可以运行。如果未设置此标志,表明出现链接器错误
  4. 当第7位为1时,表明文件是小尾方式,即内存中,最低有效位LSB位于最高有效位MSB的前面,与第15位的大尾方式(MSB在前,LSB在后)一样,都不赞成使用该标志,最好将其设置为0
  5. 当第10位为1时,如果此映像文件在可移动存储介质上,那么加载器将完全加载它并把它复制到内存交换文件中
  6. 当第11位为1时,如果此映像文件在网络上,那么加载器也将完全加载它并把它复制到内存交换文件中
  7. 当第13位为1时,表明此映像文件是动态链接库(DLL)。这样的文件总被认为是可执行文件,尽管它们并不能直接运行
  8. 可执行文件的标志设置为010fh,即第0,1,2,3,8位分别设置为1,表明该文件为可执行文件,不含重定位信息,不含符号和行号信息,文件只在32位平台运行。

IMAGE_OPTIONAL_HEADER32的字段详解

IMAGE_OPTIONAL_HEADER32.Magic

+0018h,单字。魔术字,说明文件的类型,如果为010BH,则表示该文件为PE32;如果为0107h,则表示文件为ROM映像;如果为020BH,则表示该文件为PE32+,即64位下的PE文件

IMAGE_OPTIONAL_HEADER32.MajorLinkerVersion

IMAGE_OPTIONAL_HEADER32.MinorLinkerVersion

+001ah,单字。这两个字段都是字节型,指定链接器版本号,对执行没有任何影响

IMAGE_OPTIONAL_HEADER32.SizeOfCode

+001h,双字。所有代码节的总和(以字节计算),该大小是基于文件对齐后的大小,而非内存对齐后的大小。稍后还会介绍一个字段SizeOfmage,它是基于内存对齐后的大小。需要注意一点:判断某个节是否包含代码的方法不是根据节的属性中是否含有IMAGE_SCN_MEM_EXECUTE标志,而是根据节的属性是否含有IMAGE_SCN_CNT_CODE标志

IMAGE_OPTIONAL_HEADER32.SizeOfInitializedData

+0020h,双字。所有包含已经初始化的数据的节的总大小。

IMAGE_OPTIONAL_HEADER32.SizeOfUninitializedData

+0024h,双字。所有包含为初始化的数据的节的总大小。这些数据被定为未初始化,在文件中不占用空间;但在内加载到内存以后,PE加载程序应该为这些数据分配适当大小虚拟地址空间

IMAGE_OPTIONAL_HEADER32.AddressOfEntryPoint

+0028h,双字。在Windows中,可执行程序运行在虚拟地址空间中,由于4GB空间对于程序是唯一的,所以这里的虚拟空间可以简单地理解为真实的地址。该字段的值是一个RVA,它记录了启动代码距离该PE加载后的起始位置到底有多少个字节。
如果在一个可执行文件中附加了一段自己的代码,并且想让这段代码首先被执行,一般都要修改这里的值使之指向自己的代码位置。对于一般程序映像来说,它就是启动地址;对于设备驱动程序来说,它是初始化函数的地址。入口点对于DLL来说是可选的,如果不存在入口点,这个字段必须设置为0。

IMAGE_OPTIONAL_HEADER32.BaseOfCode

+002Ch,双字。代码节的起始RVA,表示映像被加载内存时代码节的开头相对于映像基址的偏移地址。一般情况下,代码节紧跟在PE头部后面,节的名称通常为“.text”

IMAGE_OPTIONAL_HEADER32.BaseOfData

+0030h,双字。数据节的起始RVA,表示映像被加载进内存时数据节的开头相对于映像基地址的偏移地址。一般情况下,数据节位于文件末尾,节的名称通常为“.data”

IMAGE_OPTIONAL_HEADER32.ImageBase

+0034h,双字。该字段指出了PE映像的优先装入地址。也就是在IMAGE_OPTIONAL_HEADER32.AddressOfEntryPoint中说的程序被加载到内存后的起始VA。那么为什么要设置这个地址呢?因为链接器在产生可执行文件的时候,是对应这个地址来生成机器码的。如果操作系统也是按照这个地址加载机器码到内存中,那么指令中的许多重定位信息就不需要修改了,这样运行速度就会更快一些。

前面说过,对于EXE文件来说,每个文件使用的都是独立的虚拟地址空间,所以,优先装入的地址通常不会被其他模块占据。也就是说,EXE文件总是能按照这个地址装入,这就意味着装入后的EXE文件不需要进行重定位了

在链接的时候,可以使用参数“-base”来指定优先装入的地址,如果不确定,那么链接器默认装入EXE的地址就是0x00400000。而相对于DLL文件来说,它默认优先装入地址则是0x10000000。如果一个进程用到了多个DLL文件,其装入地址可能会发生冲突。PE加载器会调整其中的地址,使所有的DLL文件都能被正确装入。所以,不要错误地认为内存中动态链接库的基地址和其文件头字段IMAGE_OPTIONAL_HEADER32.ImageBase指定的完全一样。

你可以自己定义这个值,但取值有限制:第一,取值不能超出边界,即取的值必须在进程地址空间中;第二,该值必须是64KB的整数倍

IMAGE_OPTIONAL_HEADER32.SectionAlignment

+0038h,双字。内存中节的对齐粒度,该字段指定了节被装入内存后的对齐单位。

解释:
为什么16位汇编里取数时要从偶地址开始?(取一个字从偶地址开始,只需要一个CPU周期就可以取到;而从奇地址取一个字,则需要两个CPU周期)其实对齐和它一个道理,内存中的数据存取以页面为单位。

win32的页面大小是4KB,所以Win32 PE中节的内存对齐粒度一般都选择4KB大小。十六进制表示为01000h,

SectionAlignment必须大于或等于FileAlignment,当它小于系统页面大小时,必须保证SectionAlignment和FileAlignment

IMAGE_OPTIONAL_HEADER32.FileAlignment

+003ch,双字。文件中节的对齐粒度。文件中的节对齐并不是提高本身代码的执行效率,同样也是为了提高文件从磁盘加载的效率。Windows XP同来组织硬盘的所有文件系统都是基于簇(分配单元)的,每个簇包含几个物理扇区。扇区是磁盘物理存取的最小单位。簇越大,硬盘存储信息的容量就越大,但存取所花费的时间也越长。通常情况下,Windows会选择使用(200h)512字节的簇大小(1个物理扇区的大小)来格式化分区,最大可以达到4KB。

IMAGE_OPTIONAL_HEADER32.MajorOperatingSystemVersion

IMAGE_OPTIONAL_HEADER32.MinorOperatingSystemVersion

+0040h,23和24标准的两个字段都为单字,共计为双字。标识操作系统的版本号,分为主版本号和次版本号两部分

IMAGE_OPTIONAL_HEADER32.MajorImageVersion

IMAGE_OPTIONAL_HEADER32.MinorImageVersion

+0044h,双字。本PE文件映像的版本号

IMAGE_OPTIONAL_HEADER32.MajorSubsystemVersion

IMAGE_OPTIONAL_HEADER32.MinorSubsystemVersion

+0048h,双字。运行所需要的子系统的版本号。

IMAGE_OPTIONAL_HEADER32.Win32VersionValue

+004ch,双字。子系统版本的值,暂时保留未用,必须设置为0,比如将此处的值更改为696C6971h,程序运行将失败。错误如下:
在这里插入图片描述

IMAGE_OPTIONAL_HEADER32.SizeOfHeaders

+0054h,双字。所有头+节表按照文件对齐粒度对齐后的大小(即含补足的0)。在PE文件中,该部分数据是严格按照200h对齐的,如果不对齐,系统在加载时会提示出错

IMAGE_OPTIONAL_HEADER32.CheckSum

+0058h,双字。校验和,在大多数的PE文件中,该值是0,但在一些内核模式的驱动程序和系统DLL中,该值则是必须存在且正确的,比如kernel32.dll中PE的检验和是0011E97Eh。Windows系统目录下有一个动态链接库IMAGEHELP.DLL,它是Win32中专门用来操作PE文件的函数库,这里面的函数CheckSumMappedFile就是用来计算文件头检验和的,对于整个PE文件也有一个检验和函数MapFileAndCheckSum。该动态链接库中还包括其他一些常用的函数。

IMAGE_OPTIONAL_HEADER32.Subsystem

+005Ch,单字。指定使用界面的子系统,取值如下表。这个字段决定了系统如何为程序建立初始的界面,链接时使用的参数-subsystem:xxx选项指定的就是这个字段的值,如果将子系统指定为Windows命令行用户交互模式(Command User Interface,CUI),那么系统会自动为程序建立一个控制台窗口;如果指定为Windows GUI,窗口程序代码必须由用户自己建立。

取值常量符号含义
0IMAGE_SUBSYSTEM_UNKNOWN未知的子系统
1IMAGE_SUBSYSTEM_NATIVE设备驱动程序和Native Windows进程
2IMAGE_SUBSYSTEM_WINDOWS_GUIWindows图形用户界面
3IMAGE_SUBSYSTEM_WINDOWS_CUIWindows字符模式(控制台)
7IMAGE_SUBSYSTEM_POSIX_CUIPOSIX字符模式(控制台)
9IMAGE_SUBSYSTEM_WINDOWS_CE_CUIWindows CE图形界面
10IMAGE_SUBSYSTEM_EFI_APPLICATION可扩展固件接口(EFI)应用程序
11IMAGE_SUBSYSTEM_EFI_BOOT_SERVICE_DRIIVER带引导服务的EFI驱动程序
12IMAGE_SUBSYSTEM_EFI_RUNTIME_DRIIVER带运行时服务的EFI驱动程序
13IMAGE_SUBSYSTEM_EFI_ROMEFI ROM映像
14IMAGE_SUBSYSTEM_XBOXXBOX

MASM32的link程序的链接开关-subsystem的常见选项如下表

链接开关取值常见文件尾
-subsystem:nativesubsystem=1.sys
-subsystem:windowssubsystem=2.exe
-subsystem:consolesubsystem=3.exe

IMAGE_OPTIONAL_HEADER32.DllCharacteristics

+005eh,单字。DLL文件属性,它是一个标志集,不是针对DLL文件,而是针对所有的PE文件的。

数据位常量符号为1时的含义
0保留,必须为0
1保留,必须为0
2保留,必须为0
3保留,必须为0
6IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASEDLL可以在加载时被重定位
7IMAGE_DLLCHARACTERISTICS_FORCE_INTEGRITY强制代码实施完整性验证
8IMAGE_DLLCHARACTERISTICS_NX_COMPAT该映像兼容DEP
9IMAGE_DLLCHARACTERISTICS_DO_ISOLATION可以隔离,但并不隔离此映像
10IMAGE_DLLCHARACTERISTICS_NO_SEH映像不使用SEH
11IMAGE_DLLCHARACTERISTICS_NO_BIND不绑定映像
12保留,必须为0
13IMAGE_DLLCHARACTERISTICS_WDM_DRIVER该映像为一个WDM driver
14保留,必须为0
15IMAGE_DLLCHARACTERISTICS_TERMINAL_SERVER_AWARE可用于终端服务器

这个字段定义了PE文件装载时的一些特性

IMAGE_OPTIONAL_HEADER32.SizeOfStackReserve

+0060h,双字,初始化时保留的栈大小。该字段表示为初始线程的栈而保留的虚拟内存数量,然而并不是留出的所有虚拟内存都可以用栈(真正的栈大小由下一个字段SizeOfStackCommit决定)。该字段的默认值为0x100000(1MB),如果调用API函数CreatThread时,把NULL当做传入的参数,那么创建出来的栈大小也会是1MB

IMAGE_OPTIONAL_HEADER32.SizeOfStackCommit

+0064h,双字,初始化时实际提交的栈大小。保证初始线程的栈实际占用内存空间的大小,它是被系统提交的,这些提交的栈不存在于交换文件里,而是存在于内存里。对于Microsoft的链接器来说,这个域的初始值为0x1000字节(1页),对于TLINK32,则为2页。

IMAGE_OPTIONAL_HEADER32.SizeOfHeapReserve

+0068h,双字,初始化时保留的堆大小。用来保留给初始进程堆使用的虚拟内存,这个堆的句柄可以通过调用GetProcessHeap函数获得。每一个进程至少会有一个默认的进程堆,该堆在进程启动的时候被创建,而且说进程的生命期中永远不会被删除。默认值为1MB,我们可以通过链接器的“-heap”参数指定起始的保留堆内存大小和实际提交的堆大小。

IMAGE_OPTIONAL_HEADER32.SizeOfHeapCommit

+006Ch,双字。初始化时实际提交的堆大小,在进程初始化时设定的堆所占用的内存空间,默认值为1页。

IMAGE_OPTIONAL_HEADER32.LoaderFlags

+0070h,双字。加载标志

IMAGE_OPTIONAL_HEADER32.NumberOfRvaAndSize

+0074h,双字。定义数据目录结构的数量,一般为00000010h,即16个。该值由字段SizeOfOptionalHeaders决定,实际应用中可以取2~16的值

IMAGE_OPTIONAL_HEADER32.DataDirectory

+0078h,结构。由16个IMAGE_DATA_DIRECTORY结构线性排列而成,用于定义PE中16种不同类别的数据所在的位置和大小。以下是对这16数据的说明:

  • 导出数据所在的节通常被命名为.edata,它包含一些可被其他EXE程序访问的符号的相关信息,比如导出函数和资源等。这些符号通常出现在DLL中,但DLL也可以包含导入符号,而且在某些EXE中也可以有导出符号。
  • 导入数据所在的节通常被命名为.idata,它包含了PE映像中所有导入的符号。导入信息在EXE和DLL中几乎都存在
  • 异常表数据所在的节通常被命名为.pdata。该节是由用于异常处理的函数表项组成的数组。可选文件头中的ExceptionTable(异常表)字段指向它。在将他们放进最终的映像文件之前,这些表项必须按函数地址进行排序,并且这些函数表项的描述必须符合特定的目标平台。该部分的数据主要用于基于表的异常处理,适用于除X86之外的所有CPU
  • 资源数据所在的节通常被命名为.rsrc。该节是一个多层的二叉排序树,该树的节点指向PE中各种类型的资源,如图标,对话框,菜单等。树的深度可达231层,但是PE中经常使用的只有3层:类型层,名称层,语言代码层。
  • 属性证书数据的作用类似PE文件的校验和或者MD5码,通过这种属性证书方式可以验证一个PE文件是否被非法修改过,为PE文件添加属性证书表可以使该PE与属性证书相关联。属性证书表是由一组连续的按八进制(从任意字节边界开始的16个连续字节)边界对齐的属性证书表项组成,每个属性证书表项指向WIN_CERTIFICATE结构。此结构可以在WinTrust.H文件中找到,结构定义如下:
WIN_CERTIFICATE STRUCT
	dwLength			DWORD		?;0000h
	wRevision			WORD		?;0004h
	wCertificateType	WORD		?;0006h
	bCertificate		byte		?;0008h
	WIN_CERTIFICATE  ENDS

注意:
该数据并不作为映像的一部分被映射到内存,因此,DataDirectory.Certificate_VirtualAddress字段是文件偏移,而不是RVA

DataDirectory.Certificate_VirtualAddress字段给出了属性证书表中第一个属性证书表项在文件中的偏移,与后续的属性证书表项,可以通过当前属性证书表项的文件便宜加上WIN_CERTIFICATE.dwLength字段的值,并将结果向上舍入为8个字节的倍数来访问。后续的属性证书表项可以一直以这种方式访问,直到这些WIN_CERTIFICATE.dwLength字段(已经向上舍入为8字节的倍数)的和等于可选文件头中的DataDirectory.Certificate_isize的值。如果上述的值最后不等于isize字段的值,要么是属性证书表被破坏了,要是isize域被修改了。

  • 基址重定位信息所处的节通常被命名为.reloc,基址重定位表包含了映像中所有需要重定位的内容。它被划分别许多块,每一块表示一个4KB页面范围内的基址重定位信息,它必须从32位边界开始。一般情况下,Windows加载器是不需要处理由链接器解析的基址重定位信息,除非该映像不能被如约地加载到IMAGE_OPTIONAL_HEADER32.ImageBase指定的位置。
  • 调试数据所处的节通常被命名为.debug,它指向IMAGE_DEBUG_DIRECTORY结构数组。其中的每个元素都描述了PE中的一些调试信息。要获得IMAGE_DEBUG_DIRECTORY结构的数目,可以用isize字段除以IMAGE_DEBUG_DIRECTORY结构的大小
    注意:
    在默认情况下,调试信息并不会映射到映像到虚拟地址空间中。调试目录可以位于一个可丢弃的.debug节(如果存在)中,或者位于PE文件的其他节中,或者不任何节中。所以,它可能被加载到虚拟内存中,而大部分情况下是被丢弃的。
  • 预留,必须为0
  • Global Ptr数据描述的是被存在全局指针寄存器中的一个值
  • 线程本地存储数据所处的节,通常命名为.tls。线程本地存储(TLS)是Windows支持的一种特殊存储类别,其中的数据对象不是栈变量,是对应于运行相应代码的单个线程。因此,每个线程都可以为使用TLS定义的变量来维护一个不同有其它线程的值
    当创建线程时,PE加载器通过线程环境块(TEB)的地址放入FS寄存器来传递线程的TLS数组地址,距TEB开头0x2C的位置处有一个指针指向该TLS数据。线程本地存储技术是特定于intel x86平台。
  • 加载配置信息用于包含保留SEH技术,该技术基于x86的32位系统,它提供了一个安全的结构化异常处理程序列表,操作系统在进行异常处理时要用到这些异常处理程序
  • 绑定导入数据的存在主要是为了优化导入信息,提高PE的加载效率。当PE文件被加载到内存时,加载器会先检查导入表,然后把需要加载的DLL载入到地址空间中。加载器还有一项比较重要的工作是根据导入信息的描述使用动态链接库里输入函数的实际地址取替换IAT表的内容,这个步骤会花去一部分时间。但是,如果程序员(或者链接器)可以完全知道函数的地址,就可以直接把数据中的元素替换为地址,这能节省想当多的时间,这种方法称为绑定。简单来说,绑定是由程序员或链接器代替Windows PE加载器完成了一部分对导入表的处理工作(在加载时)
  • IAT是导入地址表的英文缩写。准确地讲,它是导入表的一部分,这个双字数组里定义了所有导入函数的VA,程序可以直接通过跳转指令跳转到该VA处执行
  • 延迟导入数据也和动态链接库调用有关,这种数据的存在是为了给“应用程序直到首次调用某个DLL中的函数或数据时才加载这个DLL(即延迟加载)”这种行为提供一种统一的访问机制
  • CLR数据所处的节通常被命名为.cormeta,该信息是.NET框架的一个重要组成部分,所有基于.NET框架开发的程序,其初始化部分都是通过访问这部分定义而实现的。PE加载时将通过该结构加载代码托管机制需要的所有动态链接库文件,并完成与CLR有关的一些其他操作。
  • 系统预留,未定义

IMAGE_DATA_DIRECTORY

IMAGE_DATA_DIRECTORY.VirtualAddress:
+0000h,双字。如上所述,这个字段记录了特定类型的起始RVA。当然针对不同的数据结构,该字段包含的数据含义并不一样,有的数据甚至还不是RVA(如属性证书数据中该字段的值表示的值FOA)
IMAGE_DATA_DIRECTORY.isize:
+0004h,双字。该字段记录了特定类型的数据块的长度。

节表项IMAGE_SECTION_HEADER的字段

IMAGE_SECTION_HEADER.Name1

+0000h,8字节,该字段一共就8节,一般情况下是一个以“\0”结尾的ASCII码字符串来标识节的名称,内容可以自行定义
该名称并不遵循Ansi字符串必须以“\0”结尾的规则,如果不以“\0”结尾,系统依然会认为它是一个字符串,但会根据8个字节的长度对其进行截断处理

IMAGE_SECTION_HEADER.Misc

+0008h,双字。该字段是一个union型的数据,这是节的数据在没有对齐前的真实尺寸,步过很多PE文件该值并不准确

IMAGE_SECTION_HEADER.VirtualAddress

+000ch,双字。节区的RVA地址

IMAGE_SECTION_HEADER.SizeOfRawData

+0010h,双字。节咋文件中对齐后的尺寸。一般512(200h)字节。

IMAGE_SECTION_HEADER.PointerToRawData

+0014h,双字。节区起始数据在文件中的偏移。

IMAGE_SECTION_HEADER.PointerToRelocations

+0018h,双字。在“.obj”文件中使用,指向在重定位表的指针

IMAGE_SECTION_HEADER.PointerToLinenumbers

+001ch,双字,行号表的位置(供调试用)

IMAGE_SECTION_HEADER.NumberOfRelocations

+0020h,单字。重定位表的个数(在OBJ文件中使用)

IMAGE_SECTION_HEADER.NumberOfLinenumbers

+0022h,单字。行号表中行号的数量

IMAGE_SECTION_HEADER.Characteristics

+0024h,双字。节的属性。这个字段很重要,这是节的属性标志字段,其中不同的数据为代表不同的属性,如下:

数据位常量符号位为1时的含义
5IMAGE_SCN_CNT_CODE或00000020h节中包含代码
6IMAGE_SCN_CNT_INITIALIZED_DATA或00000040h节中包含已初始化数据
7IMAGE_SCN_CNT_UNINITIALIZED_DATA或00000080h节中包含未初始化数据
8IMAGE_SCN_CNT_LINK_OTHER或00000100h保留供将来使用
25IMAGE_SCN_MEM_DISCARDABLE或02000000h节中的数据在进程开始以后将丢弃,如.reloc
26IMAGE_SCN_MEM_NOT_CACHED或04000000h节中的数据不会经过缓存
27IMAGE_SCN_MEM_NOT_PAGED或08000000h节中的数据不会被交换到磁盘
28IMAGE_SCN_MEM_SHARED或10000000h表示节中的数据将被不同的进程所共享
29IMAGE_SCN_MEM_EXECUTE或20000000h映射到内存后的页面包含可执行属性
30IMAGE_SCN_MEM_READ或40000000h映射到内存后的页面包含可读属性
31IMAGE_SCN_MEM_WRITE或80000000h映射到内存后的页面包含可写属性

代码节的属性一般为60000020h,也就是可执行,可读和“节中包含代码”;数据节的属性一般为c0000040h,也就是可读,可读和“包含已初始化数据”;而常量节(对应源代码中的.const段)的属性为40000040h,也就是可读和“包含已初始化数据”;资源节的属性和常量节的属性一般是相同的。
节属性的定义不一定必须是这些值。比如,PE文件被压缩工具压缩以后,包含代码的节往往被同时设置成具有可执行,可读和可写属性,因为解压部分需要将解压后的代码回写到代码段中

  • 25
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

寻梦&之璐

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值