学习课程-鱼C-小甲鱼【系统篇】《解密系列》单看课程记不住,想着好记性不如烂笔头,特此记录学习加深理解。
PE结构的概念
EXE与DLL文件的区别
EXE与DLL文件之间的区别完全是语意上面的,因为他们使用了完全相同的PE格式。唯一的区别在与是用一个字段标示出这个文件是EXE还是DLL文件格式。
64位与32位PE文件的区别
64位的Windows仅仅知识对PE格式做了一些简单的修饰,新格式叫做PE32+ 并没有任何新的结构加进去,改变的只是简单的将32位字段扩展为64位,一般会在名称上表现出来:例如IMAGE_NT_HEADERS32 或 IMAGE_NT_HEADER64 来表示此结构用于32位还是64位PE文件。
PE格式的定义
PE格式定义的主要地方位于头文件winnt.h,这个头文件中几乎能找到所有关于PE文件的定义。
PE文件的架构结构
基本概念
PE文件使用的是一个平面地址空间,所有代码和数据都被合并在一起,组成一个很大的结构。
文件的内容被分割为不同的区块,块中包含代码或数据。各个区块按页边界来对齐,区块没有大小限制,是一个连续的结构。
此外,区块中的每个块有自己在内存中的一套属性,比如说这个区块是否包含代码,数据,是否可读或可写等权限的限制。
PE文件并不是作为单一内存映射文件而被装入内存的;Windows装载器(PE装载器)遍历PE文件并决定文件的哪一部分被映射,这种映射方式是将文件较高的偏移位置映射到 映射到较高的内存地址。映射后其结构某项的偏移地址可能区别于原始的偏移地址,但文件的整体结构不会发生改变。
磁盘文件一旦被映射装入内存,磁盘上的数据结构布局和内存中的数据结构布局是一致的。
数据之间的相对位置可能会发生改变,其某项的偏移地址可能会区别与原始的偏移地址,但不管如何,所有表现出来的信息都允许(接受)从磁盘文件偏移到内存偏移的转换。
PE文件块之间之所以会产生空隙,是因为需要进行对齐,便于磁盘内存管理。
PE文件通过Windows装载器装载进内存中后,DOS头、PE头和区块表的偏移位置与大小均不会发生改变,而各个区块映射到内存中后,其偏移位置则会发生改变。
磁盘中的PE文件与内存中的模块之间的偏移位置有可能会发生变化,是由于IMAGE_OPTIONAL_HEADER结构中的FileAlignment 与 SectionAlignment之间的值不同导致对齐标准不一而造成的。
PE结构的几个概念
基地址 (ImageBase)
是PE文件映射到内存文件后的PE结构的头地址,这个地址被称之为基地址。PE文件被映射到内存中后我们可以称之为一个模块(Module) , 其内存中的基地址就是模块的句柄(HModule),获得句柄之后,也就是拿到了Pe结构的头部,根据头部中所存放的信息,我们拿到整个PE文件。
1 | //LPCTSTR lpModulename 存有模块名的指针 |
文件偏移地址
PE文件的头地址在自身PE结构中都以0为开始地址,其他区块较头地址所相差的地址即为偏移地址
虚拟地址(VA)与相对虚拟地址(RVA)
虚拟地址:各个区块映射至内存之中可能会发生比例变化,为指出在内存中已经发生比例变化的各个区块的 地址,引出了**虚拟地址(VA)**的概念 。
相对虚拟地址:与PE文件的偏移地址相似,某一虚拟地址-基地址=相对虚拟地址
各大部分
MS-DOS头部
根据上文的结构图可知,PE文件首个部分便是DOS头,有了DOS头,我们才能在DOS环境下执行PE文件,DOS系统才可识别出这是一个有效的执行体来从而执行。
PE文件的第一个字节起始于一个传统的MS-DOS头部,被称为IMAGE_DOS_HEADER。
IMAGE_DOS_HEADER(左侧+0h一列是文件头的偏移量)
1 | IMAGE_DOS_HEADER STRUCT |
PE文件头
- PE文件头(PE Header)紧挨着DOS stub
- PE Header是PE相关结构NT映像头(IMAGE_NT_HEADER)的简称,里面包含着许多PE装载器用到的重要字段
- 执行体在支持PE文件结构的操作系统中执行时,PE装载器将从IMAGE_DOS_HEADER结构中的e_lfanew字字段里找到PE Header的起始偏移量,加上基地址就得到PE文件头的指针
- PNTHeader = ImageBase + dosHeader ->e_lfanew
IMAGE_NT_HEADERS结构
PE Header 是PE相关结构NT映像头(IMAGE_NT_HEADER)的简称,里面包含着许多PE装载器要用到的重要字段
1 | IMAGE_NT_HEADERS STRUCT |
Signature
在一个有效的 PE 文件里,Signature 字段被设置为00004550h, ASCII 码字符是“PE00”。标志这 PE 文件头的开始,于16进制编辑器中为50 45 00 00h ,进制转换后为00 00 45 50h。
DOS头部的指针e_lfanew指向PE头的首地址,从而找到PE文件头
IMAGE_FILE_HEADER 结构
1 | typedef struct _IMAGE_FILE_HEADER |
(1)Machine:可执行文件的目标CPU类型,WORD类型 长度为4字节
(2)NumberOfSection:区块的数目(IMAGE_NT_HEADERS ) WORD类型 2字节
(3)TimeDataStamp: 表明文件是何时被创建的 DWORD 4字节
TimeDataStamp: 表明文件是何时被创建的。
这个值是自1970年1月1日以来用格林威治时间(GMT)计算的秒数,这个值是比文件系统(FILESYSTEM)的日期时间更加精确的指示器
VC的话可以用_ctime 函数或者 gmtime 函数。
(4)PointerToSymbolTable: COFF 符号表的文件偏移位置,现在基本没用了
(5)NumberOfSymbols: 如果有COFF 符号表,它代表其中的符号数目,COFF符号是一个大小固定的结构,如果想找到COFF 符号表的结束位置,则需要这个变量。
(6)SizeOfOptionalHeader: 紧跟着IMAGE_FILE_HEADER 后边的数据结构(IMAGE_OPTIONAL_HEADER)的大小。(对于32位PE文件,这个值通常是00E0h;对于64位PE32+文件,这个值是00F0h )。
SizeOfOptionalHeader相较PE头偏移14h注意,偏移量是16进制的,拿到x64位系统下的IMAGE_OPTIONAL_HEADER32的结构大小0x00F0 (注意大小头问题!!!!)
(7)Characteristics: 文件属性,有选择的通过几个值可以运算得到。( 这些标志的有效值是定义于 winnt.h 内的 IMAGE_FILE_** 的值,具体含义见下表。普通的EXE文件这个字段的值一般是 0100h,DLL文件这个字段的值一般是 210Eh。)多种属性可以通过 “或运算” 使得同时拥有!
Value | Meaning |
---|---|
IMAGE_FILE_RELOCS_STRIPPED 0x0001 | Relocation information was stripped from the file. The file must be loaded at its preferred base address. If the base address is not available, the loader reports an error. |
IMAGE_FILE_EXECUTABLE_IMAGE0x0002 | The file is executable (there are no unresolved external references). |
IMAGE_FILE_LINE_NUMS_STRIPPED0x0004 | COFF line numbers were stripped from the file. |
IMAGE_FILE_LOCAL_SYMS_STRIPPED0x0008 | COFF symbol table entries were stripped from file. |
IMAGE_FILE_AGGRESIVE_WS_TRIM0x0010 | Aggressively trim the working set. This value is obsolete as of Windows 2000. |
IMAGE_FILE_LARGE_ADDRESS_AWARE0x0020 | The application can handle addresses larger than 2 GB. |
IMAGE_FILE_BYTES_REVERSED_LO0x0080 | The bytes of the word are reversed. This flag is obsolete. |
IMAGE_FILE_32BIT_MACHINE0x0100 | The computer supports 32-bit words. |
IMAGE_FILE_DEBUG_STRIPPED0x0200 | Debugging information was removed and stored separately in another file. |
IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP0x0400 | If the image is on removable media, copy it to and run it from the swap file. |
IMAGE_FILE_NET_RUN_FROM_SWAP0x0800 | If the image is on the network, copy it to and run it from the swap file. |
IMAGE_FILE_SYSTEM0x1000 | The image is a system file. |
IMAGE_FILE_DLL0x2000 | The image is a DLL file. While it is an executable file, it cannot be run directly. |
IMAGE_FILE_UP_SYSTEM_ONLY0x4000 | The file should be run only on a uniprocessor computer. |
IMAGE_FILE_BYTES_REVERSED_HI0x8000 | The bytes of the word are reversed. This flag is obsolete. |
IMAGE_OPTIONAL_HEADER结构
1 | typedef struct _IMAGE_OPTIONAL_HEADER |
以上结构中的大部分字段都不重要,我们可以从注释中进行理解使用,不必死记硬背,接下来解释其中较为重要的字段
AddressOfEntryPoint字段(+28h) DWORD 32位下4 byte
指出文件被执行时的入口地址,这是一个RVA地址(相对虚拟地址)如果在一个可执行文件上附加了一段代码并想让这段代码首先被执行,那么只需要让这个入口地址指向附加的代码就可以了。
PE头基地址为140h+28h偏移并根据大小端模式可看出入口的相对虚拟地址为0X000C0CB4
ImageBase(+34h)
ImageBase字段指出文件的优先装入地址。也就是说当文件被执行时如果可能的话,Windows优先将文件装入指定的内存地址,若该内存地址已被其他模块占用时,文件才被装入到其他空余的内存地址当中。链接器产生可执行文件的时候对应这个地址来生成机器码。所以当文件可装入这个内存地址时,不需要重定向操作,装入的速度最快,若ImageBase指定的内存地址被占用,那么链接器将不得不重定向空余内存地址将PE文件装入,相比之下,会慢上一些。
PE头基地址为140h+34h
另外,虚拟地址空间与物理地址空间并不是一个东西,不可以混为一谈,我们可以这么理解,我们c/c++程序中访问的内存地址,并不是实际上的物理内存地址,而是虚拟内存地址,程序访问内存地址时,先是访问虚拟地址,通过页表等手段将虚拟地址映射到物理内存地址上,如此进行间接的访问物理内存地址。并且由于每个程序都有自己的虚拟内存地址,其映射出的物理内存地址也不同,可通过此手段对内存地址进行隔离。
对于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字段制定了节被装入福内存后的对其单位。也就是说每个节被装入的地址必定是本字段指定数值的整数倍。而FileAligment字段制定了节存储在磁盘文件中时的对齐单位。
IMAGE_DATA_DIRECTORY [IMAGE_NUMBEROF_DIRECTORY_ENTRIES]
这个字段可以说是最重要的字段之一,他由16个相同的IMAGE_DATA_DIRECTORY结构组成,虽然PE文件中的数据是按照装入内存后的页属性归类而被放在不同的节中的,但这些处于各个节中的数据按照用途可以分为导出表、导入表、资源、重定位表等数据块,这16个IMAGE_DATA_DIRECTORY结构就是用来定义多种不同用途的数据块的。IMAGE_DATA_DIRECTORY 结构定义比较简单,它仅仅指出了某种数据块的长度和位置。
1
2
3
4typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress; //数据的相对虚拟地址(RVA)
DWORD Size; //数据的大小
}
各个数据目录列表的含义:
在PE文件中找寻特定的数据时就是从这些IMAGE_DATA_DIRECTORY结构开始的,比如要存取资源,那么就必须从第三个IMAGE_DATA_DIRECTORY结构(索引为2)中获得资源数据块的大小和位置;同理,如果要查看PE文件导入了哪些DLL文件的那些API函数,那就必须首先从第二个IMAGE_DATA_DIRECTORY结构得到导入表的位置与大小。
IMAGE_SECTION_HEADER结构
区块表(节表):
PE文件中所有节的属性都被定义在节表中,节表由一系列的IMAGE_SECTION_HEADER结构排列而成,每个结构用来 描述一个节,结构的排列顺序和它们描述的节在文件中的排列顺序是一致的。全部有效结构的最后以一个空的IMAGE_SECTION_HEADER结构作为 结束,所以节表中总的IMAGE_SECTION_HEADER结构数量等于节的数量加一。节表总是被存放在紧接在PE文件头的地方。
另外,节表中 IMAGE_SECTION_HEADER 结构的总数总是由PE文件头 IMAGE_NT_HEADERS 结构中的 FileHeader.NumberOfSections 字段来指定的。
此结构体共占40个字节
1 | typedef struct _IMAGE_SECTION_HEADER |
Name: 区块名。这是一个由8位的ASCII 码名,用来定义区块的名称。多数区块名都习惯性以一个“.”作为开头(例如:.text),这个“.” 实际上是不是必须的。值得我们注意的是,如果区块名超过 8 个字节,则没有最后的终止标志“NULL” 字节。并且前边带有一个“$” 的区块名字会从连接器那里得到特殊的待遇,前边带有“$” 的相同名字的区块在载入时候将会被合并,在合并之后的区块中,他们是按照“$” 后边的字符的字母顺序进行合并的。
另外每个区块的名称都是唯一的,不能有同名的两个区块。但事实上节的名称不代表任何含义,他的存在仅仅是为了正 规统一编程的时候方便程序员查看方便而设置的一个标记而已。所以将包含代码的区块命名为“.Data” 或者说将包含数据的区块命名为“.Code” 都是合法的。当我们要从PE 文件中读取需要的区块时候,不能以区块的名称作为定位的标准和依据,正确的方法是按照 IMAGE_OPTIONAL_HEADER32 结构中的数据目录字段结合进行定位。
- Virtual Size: 该表对应的区块大小,这是区块的数据在没有进行对齐处理前的实际大小
- Virtual address: 该区块装载到内存中的RVA 地址。这个地址是按照内存页来对齐的,因此它的数值总是 SectionAlignment 的值的整数倍。在Microsoft 工具中,第一个快的默认 RVA 总为1000h。在OBJ 中,该字段没有意义地,并被设为0。
- SizeOfRawData: 该区块在磁盘中所占的大小。在可执行文件中,该字段的大小是已经被FileAlignment进行对齐过的长度。(FileAlignment 是磁盘中PE文件对齐标准的字段,默认大小为200h,SectionAlignment 是内存中PE文件对齐标准的字段,默认大小为1000h )
- PointerToRawData: 该区块在磁盘中的偏移。 这个数值是从文件头开始算起的偏移量
- PointerToRelocations:这哥们在EXE文件中没有意义,在OBJ 文件中,表示本区块重定位信息的偏移值。(在OBJ 文件中如果不是零,它会指向一个IMAGE_RELOCATION 结构的数组)
- PointerToLinenumbers:行号表在文件中的偏移值,文件的调试信息,于我们没用,鸡肋。
- NumberOfRelocations:这哥们在EXE文件中也没有意义,在OBJ 文件中,是本区块在重定位表中的重定位数目来着。
- Characteristics:该区块的属性。该字段是按位来指出区块的属性(如代码/数据/可读/可写等)的标志。
区块描述、对齐值以及RVA详解
通常,区块中的数据在逻辑上是关联的。PE 文件一般至少都会有两个区块:一个是代码块,另一个是数据块。每一个区块都需要有一个截然不同的名字,这个名字主要是用来表达区块的用途。例如有一个区块叫.rdata,表明他是一个只读区块。注意:区块在映像中是按起始地址(RVA)来排列的,而不是按字母表顺序。
另外,使用区块名字只是人们为了认识和编程的方便,而对操作系统来说这些是无关紧要的。微软给这些区块取了个有特色的名字,但这不是必须的。当编程从PE 文件中读取需要的内容时,如输入表、输出表,不能以区块名字作为参考,正确的方法是按照数据目录表中的字段来进行定位。
下表中的区块名称以及意义:
我们再Visual C++ 中也可以自己命名区块,用#pragma来声明, 告诉编译器插入数据到一个区块中,格式如下:
#pragma data_msg(“FC_data”)
以上语句告诉编译器将数据全都放入”FC-data” 的区块中,而不是默认的.data区块内。区块一般是从obj文件开始,被编译器防止。链接器用于合并OBJ和库中需要的块,使其称为一个最终合适的区块。链接器会遵循一套相当完整的规则,他会判断哪些区块将被合并以及如何被合并。
合并区块: 链接器可以合并区块。如果两个区块有相似、一致性的属性,那么他们在链接的时候能够被合并成一个单一的区块。这取决于编译器是否开启了/merge开关。由于区块存在对齐问题,如果PE文件中存在大量相似的区块而不进行合并,这样会对内存资源造成极大的浪费 *注意:(我们不可以将.rsrc、.reloc、.pdata 合并到***的区块中。
之前我们简单了解过区块是要对齐的,无论是在内存中存放还是在磁盘中存放~ 但他们一般的对齐值是不同的。
PE 文件头里边的FileAligment 定义了磁盘区块的对齐值。每一个区块从对齐值的倍数的偏移位置开始存放。而区块的实际代码或数据的大小不一定刚好是这么多,所以在多余的地方一般以00h 来填充,这就是区块间的间隙。
区块的对齐值
例如,在PE文件中,一个典型的对齐值是200h ,这样,每个区块都将从200h 的倍数的文件偏移位置开始,假设第一个区块在400h 处,长度为90h,那么从文件400h 到490h 为这一区块的内容,而由于文件的对齐值是200h,所以为了使这一区块的长度为FileAlignment 的整数倍,490h 到 600h 这一个区间都会被00h 填充,这段空间称为区块间隙,下一个区块的开始地址为600h 。
PE 文件头里边的SectionAligment 定义了内存中区块的对齐值。PE 文件被映射到内存中时,区块总是至少从一个页边界开始。
一般在X86 系列的CPU 中,页是按4KB(1000h)来排列的;在IA-64 上,是按8KB(2000h)来排列的。所以在X86 系统中,PE文件区块的内存对齐值一般等于 1000h,每个区块按1000h 的倍数在内存中存放。
PE文件到内存的映射
PE文件到内存的映射
- 在执行一个PE文件的时候,Windows并不是在一开始就将整个文件读入内存的,而是采用与内存映射文件类似的机制但又不完全相同;内存映射所写入物理内存中的文件与磁盘文件相比,相对位置完全相同,而Windows装载器装载的EXE等文件时,会产生重定位对某些数据进行预处理,装载到物理内存等待系统使用,使得磁盘文件与物理内存文件的相对位置不同。
- 也就是Windows装载器在装载的时候仅仅建立好了虚拟地址与PE文件之间的映射关系,与我上文写的一致。
- 当且仅当真正执行至某个内存页中的指令或访问某一页中的数据时,这个页面才会被从磁盘提交到物理内存之中,这种机制使得文件装入的速度和文件的大小并没有太大关系,而是与CPU关系大。
- Windows装载器在装载DOS部分、PE文件头部分和区块表(节表)部分是不进行任何特殊处理的,而在装载节(区块)的时候则会自动对照区块表(节表)的属性做不同的处理
- 一般情况下,它会处理以下几个方面的内容:
- 内存页的属性;
- 节的偏移地址;
- 节的尺寸;
- 不进行映射的节;
内存页的属性:
对于磁盘映射文件来说,所有的页都是按照磁盘映射文件函数指定的属性来设置的。但是在装载可执行文件的时候,与节对应的内存页属性需要按照节的属性来设置。所以在同属于一个模块的内存页中,从不同节映射来的内存页的属性是不同的。
节的偏移地址:
节的起始地址在磁盘文件中是按照IMAGE_OPTIONAL_HEADER32结构的 FileAlignment 字段的值进行对齐,而当被加载到内存中时是按照同一结构的SectionAlignment 字段的值对齐的,两者的值可能不同,所以当一个节被装入内存后相对于文件头的偏移和在磁盘文件中的偏移可能是不同的。
这就是为什么PE文件在载入虚拟空间地址后偏移地址会发生比例改变的原因。
注意:节实际上就是相同属性数据的组合 当节被装入内存中时,相同一个内存所对应的内存页都将被赋予相同的页属性,实际上,windows系统对内存属性的设置时以页为单位进行的,所以节在内存中的对齐单位必须至少是一个页的大小。
对于32位操作系统来说,这个值一般是4KB==1000H; 对于64位操作系统这个值一般是8KB==2000H)
当我们需要从PE文件中读取区块的时候,不能以区块的名称作为定位的标准或依据,正确的方法是按照IMAGE_OPTIONAL_HEADER32结构中的数据目录字段进行定位
实际操作展示
利用IMAGE_DOS_HEADER进行跳转
e_magic变量的值为0X4D5A 对应的MZ 即为DOS头部的标示,DOS可执行文件标示。
利用e_lfanew使pe文件由DOS头位置跳转至PE文件头位置。
e_lfanew处的偏移量由于应该是高位地址在前,低位地址在后,顺序应颠倒->00 00 01 40 即 140h 此处偏移地址是根据基地址来看,因此应该是00 00 00 00 + 00 00 01 40 = 00 00 01 40
根据偏移地址可以找到140h 即为PE文件头的起始偏移量,加上基地址就可找到PE文件头的指针,来到PE文件头
找出IMAGE_NT_HEADERS中IMAGE_OPTIONAL_HEADER32的地址
利用IMAGE_FILE_HEADER结构中SizeOfOptionalHeader(+14h)找出IMAGE_OPTIONAL_HEADER32结构的大小。
根据PE头+14h可找到SizeOFoptionHeader的值0X00F0,转化为10进制可得出IMAGE_OPTIONAL_HEADER32结构大小为240
其次,根据IMAGE_NT_HEADERS结构体的IMAGE_OPTIONAL_HEADER32(+18h)拿到IMAGE_OPTIONAL_HEADER32的起始地址
利用起始地址+结构大小=整个结构 可知,IMAGE_OPTIONAL_HEADER32的结构的起始地址为158h结束地址为248h 第一次算没把158当成16进制,直接算错了(笑)