首页 > 其他分享 >PE学习——PE文件整体结构解析,写得很精致,可以对照案例实践

PE学习——PE文件整体结构解析,写得很精致,可以对照案例实践

时间:2023-05-11 15:22:42浏览次数:59  
标签:SCN WORD IMAGE 对照 PE DWORD 解析 define

PE文件结构:

 

PE加载到内存后的映射:

images/download/attachments/26902549/image2021-11-23_22-0-33.png

 

我们本章节主要看上述细节。本文最核心的图就是PE在做image内存展开的样子:

images/download/attachments/26902549/image2021-11-24_22-17-31.png

 

PE文件整体结构解析

之前我们已经按照PE文件的整体结构对实际的PE文件进行了大致上的了解了,现在我们需要来看看每个结构的意义和作用。

DOS头

在之前,我们已经了解过PE文件的整体结构了,并且我们进行了静动态差异的文件分析,其开头部分就是DOS部分,包含了DOS MZ文件头和DOS块,那么我们来了解一些DOS部分的结构和其相关意义。

DOS MZ文件头

DOS MZ文件头就是一个结构体IMAGE_DOS_HEADER,其定义如下所示:

typedef struct _IMAGE_DOS_HEADER { // DOS .EXE header WORD e_magic; // Magic number WORD e_cblp; // Bytes on last page of file WORD e_cp; // Pages in file WORD e_crlc; // Relocations WORD e_cparhdr; // Size of header in paragraphs WORD e_minalloc; // Minimum extra paragraphs needed WORD e_maxalloc; // Maximum extra paragraphs needed WORD e_ss; // Initial (relative) SS value WORD e_sp; // Initial SP value WORD e_csum; // Checksum WORD e_ip; // Initial IP value WORD e_cs; // Initial (relative) CS value WORD e_lfarlc; // File address of relocation table WORD e_ovno; // Overlay number WORD e_res[4]; // Reserved words WORD e_oemid; // OEM identifier (for e_oeminfo) WORD e_oeminfo; // OEM information; e_oemid specific WORD e_res2[10]; // Reserved words LONG e_lfanew; // File address of new exe header } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

它有很多成员,但我们并不需要去深入的理解每个成员的含义和作用,这是因为这个结构体是给16位平台看的,而我们现在的环境大部分都是32位和64位的,所以现在的平台不再需要这个完整的结构体了,只需要其中的两个成员e_magic和e_lfanew。

你可以尝试在16进制的编辑器中去编辑某个EXE文件保留两个成员e_magic和e_lfanew,其他的以0x00填充,然后保存文件,你会发现修改后的文件还是可以正常运行的:

images/download/attachments/26902549/image2021-11-23_23-4-14.png

保留这两个成员的原因是因为它们代表着我们之前所说的PE指纹,操作系统也是根据这个来识别是否是PE文件的,所以不能够更改、删除(e_magic是一种标识,e_lfanew则表示PE文件头的位置)。

DOS块

DOS块就是夹在DOS MZ文件头和PE文件头之间的内容,这里面的内容可以根据自己的需要随意的修改和添加,并不会影响文件的正常运行。

images/download/attachments/26902549/image2021-11-23_23-9-26.png

PE头

PE头整体就是如下这个结构体:

typedef struct _IMAGE_NT_HEADERS { DWORD Signature; // PE标识 IMAGE_FILE_HEADER FileHeader; // 标准PE头 IMAGE_OPTIONAL_HEADER32 OptionalHeader; // 扩展PE头 } IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;

第一个成员就是PE标识,该标识不能破坏,因为操作系统在启动一个程序的时候会检测这个标识。

标准PE头

标准PE头是PE头的第二个成员,它是如下所示的结构体:

typedef struct _IMAGE_FILE_HEADER { WORD Machine; // 可以运行在什么样的CPU上 WORD NumberOfSections; // 表示节的数量 DWORD TimeDateStamp; // 编译器填写的时间戳 DWORD PointerToSymbolTable; // 调试相关 DWORD NumberOfSymbols; // 调试相关 WORD SizeOfOptionalHeader; // 扩展PE头的大小 WORD Characteristics; // 文件属性 } IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

其第一个成员Machine表示可以运行在什么样的CPU上,如果它的值为0x0则表示可以运行在任意的CPU上,支持在Intel 386以及后续的型号CPU运行则值为0x14c,支持64位的CPU型号则值为0x8664

我们可以分别在32位、64位系统上提取notepad.exe进行对比来看看这个成员(010 Editor → Tools → Compare Files...):

images/download/attachments/26902549/image2021-11-24_10-59-51.png

第二个成员NumberOfSections表示当前PE文件中节的数量,也就是节表中有几个结构体;第三个成员TimeDateStamp表示编译器编译的时候插入的时间戳,与文件属性里面的创建时间和修改时间是无关的。

第四、第五个成员是调试相关的,我们暂时不用去了解;第六个成员SizeOfOptionalHeader表示扩展PE头的大小默认情况下32位PE文件对应值位0xE0,64位PE文件对应值为0xF0

第七个成员Characteristics用来记录当前PE文件的一些属性,该成员是16位(2字节)大小,其每一数据位对应的属性如下所示:

images/download/attachments/26902549/image2021-11-24_14-52-7.png

扩展PE头

扩展PE头在32位和64位环境下是不一样的,在本章节中只介绍32位扩展PE头。如下结构体就是32位的扩展PE头:

typedef struct _IMAGE_OPTIONAL_HEADER { WORD Magic; // PE32:10B PE32+:20B BYTE MajorLinkerVersion; // 链接器版本号 BYTE MinorLinkerVersion; // 链接器版本号 DWORD SizeOfCode; // 所有代码节的总和(文件对齐后的大小),编译器填的(没用) DWORD SizeOfInitializedData; // 包含所有已经初始化数据的节的总大小(文件对齐后的大小),编译器填的(没用) DWORD SizeOfUninitializedData; // 包含未初始化数据的节的总大小(文件对齐后的大小),编译器填的(没用) DWORD AddressOfEntryPoint; // 程序入口 DWORD BaseOfCode; // 代码开始的基址,编译器填的(没用) DWORD BaseOfData; // 数据开始的基址,编译器填的(没用) DWORD ImageBase; // 内存镜像基址 DWORD SectionAlignment; // 内存对齐 DWORD FileAlignment; // 文件对齐 WORD MajorOperatingSystemVersion; // 标识操作系统版本号,主版本号 WORD MinorOperatingSystemVersion; // 标识操作系统版本号,次版本号 WORD MajorImageVersion; // PE文件自身的版本号 WORD MinorImageVersion; // PE文件自身的版本号 WORD MajorSubsystemVersion; // 运行所需子系统版本号 WORD MinorSubsystemVersion; // 运行所需子系统版本号 DWORD Win32VersionValue; // 子系统版本的值,必须为0 DWORD SizeOfImage; // 内存中整个PE文件的映射的尺寸 DWORD SizeOfHeaders; // 所有头加节表按照文件对齐后的大小,否则加载会出错 DWORD CheckSum; // 校验和 WORD Subsystem; // 子系统,驱动程序(1)、图形界面(2) 、控制台/DLL(3) WORD DllCharacteristics; // 文件特性 DWORD SizeOfStackReserve; // 初始化时保留的栈大小 DWORD SizeOfStackCommit; // 初始化时实际提交的大小 DWORD SizeOfHeapReserve; // 初始化时保留的堆大小 DWORD SizeOfHeapCommit; // 初始化时实践提交的大小 DWORD LoaderFlags; // 调试相关 DWORD NumberOfRvaAndSizes; // 目录项数目 IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; // 表,结构体数组 } IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

扩展PE头的成员有很多,但我们不需要每个都记住,大概的了解一下即可,重点关注如下这几个成员:

成员Magic表示当前PE文件是32位还是64位,32位时该值对应0x10B,64位时该值对应0x20B。

成员AddressOfEntryPoint表示当前程序入口的地址,这个成员要与成员ImageBase相加才能得出真正的入口地址,成员ImageBase用来表示内存镜像基址,也就是PE文件在内存中按内存对齐展开后的首地址,我们可以在实际PE文件中看下,如下图所示就是PE文件静态状态下的两个成员值,AddressOfEntryPoint为0x739D,ImageBase为0x1000000,那么最终的程序在内存中的入口地址就是0x100739D:

images/download/attachments/26902549/image2021-11-24_16-28-5.png

那么如何证实推断的结果是正确的呢,我们可以直接使用DTDebug之类的调试器打开这个PE文件,调试器会自动在程序入口断点,如下图所示则表示我们的推测是正确的:

images/download/attachments/26902549/image2021-11-24_16-50-43.png

成员FileAlignment、SectionAlignment和SizeOfHeader在之前的章节中已经了解过了,这里不再赘述。

成员SizeOfImage表示在内存中整个PE文件映射的大小,可比实际的值大(内存对齐之后的大小,也就表示必须是SectionAlignment的整数倍)。

成员CheckSum表示校验和,是用来判断文件是否被修改的,它的计算方法就是文件的两个字节与两个字节相加,最终的值(不考虑溢出情况)就是校验和。

最后一个需要我们了解的成员是DllCharacteristics,它用来表示PE文件的特性,但不要被名字所迷惑,它不是针对DLL文件的;它的数据宽度是16位(4字节),其每一数据位对应的属性如下所示:

images/download/attachments/26902549/image2021-11-24_17-11-3.png

PE节表

在PE中,节数据有几个,分别对应着什么类型以及其他相关的属性都是由PE节表来决定的,PE节表是一个结构体数组,结构体的定义如下所示:

#define IMAGE_SIZEOF_SHORT_NAME 8 typedef struct _IMAGE_SECTION_HEADER { BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; // ASCII字符串(节名),可自定义,只截取8个字节,可以8个字节都是名字 union { // Misc,双字,是该节在没有对齐前的真实尺寸,该值可以不准确 DWORD PhysicalAddress; // 真实宽度,这两个值是一个联合结构,可以使用其中的任何一个 DWORD VirtualSize; // 一般是取后一个 } Misc; DWORD VirtualAddress; // 在内存中的偏移地址,加上ImageBase才是在内存中的真正地址 DWORD SizeOfRawData; // 节在文件中对齐后的尺寸 DWORD PointerToRawData; // 节区在文件中的偏移 DWORD PointerToRelocations; // 调试相关 DWORD PointerToLinenumbers; // 调试相关 WORD NumberOfRelocations; // 调试相关 WORD NumberOfLinenumbers; // 调试相关 DWORD Characteristics; // 节的属性 } IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

代码中的注释可以大致了解到每个成员的作用,其中有2个成员来描述节的大小,分别是没有对齐前的真实尺寸和对齐后的宽度,这时候会出现一种情况就是对齐前的真实尺寸大于对齐后的宽度,这就是存在全局变量没有赋予初始值导致的,在文件存储中全局变量没有赋予初始值也就不占空间,但是在内存中是必须要赋予初始值的,这时候宽度就大了一些,所以在内存中节是谁大就按照谁去展开。

images/download/attachments/26902549/image2021-11-24_22-17-31.png

与其他结构体一样,PE节也有属性,这就是成员Characteristics,其数据宽度是16位(4字节),其每一数据位对应的属性如下所示:

images/download/attachments/26902549/image2021-11-24_22-19-0.png

更多可以参考如下:

// // Section characteristics. // // IMAGE_SCN_TYPE_REG 0x00000000 // Reserved. // IMAGE_SCN_TYPE_DSECT 0x00000001 // Reserved. // IMAGE_SCN_TYPE_NOLOAD 0x00000002 // Reserved. // IMAGE_SCN_TYPE_GROUP 0x00000004 // Reserved. #define IMAGE_SCN_TYPE_NO_PAD 0x00000008 // Reserved. // IMAGE_SCN_TYPE_COPY 0x00000010 // Reserved.   #define IMAGE_SCN_CNT_CODE 0x00000020 // Section contains code. #define IMAGE_SCN_CNT_INITIALIZED_DATA 0x00000040 // Section contains initialized data. #define IMAGE_SCN_CNT_UNINITIALIZED_DATA 0x00000080 // Section contains uninitialized data.   #define IMAGE_SCN_LNK_OTHER 0x00000100 // Reserved. #define IMAGE_SCN_LNK_INFO 0x00000200 // Section contains comments or some other type of information. // IMAGE_SCN_TYPE_OVER 0x00000400 // Reserved. #define IMAGE_SCN_LNK_REMOVE 0x00000800 // Section contents will not become part of image. #define IMAGE_SCN_LNK_COMDAT 0x00001000 // Section contents comdat. // 0x00002000 // Reserved. // IMAGE_SCN_MEM_PROTECTED - Obsolete 0x00004000 #define IMAGE_SCN_NO_DEFER_SPEC_EXC 0x00004000 // Reset speculative exceptions handling bits in the TLB entries for this section. #define IMAGE_SCN_GPREL 0x00008000 // Section content can be accessed relative to GP #define IMAGE_SCN_MEM_FARDATA 0x00008000 // IMAGE_SCN_MEM_SYSHEAP - Obsolete 0x00010000 #define IMAGE_SCN_MEM_PURGEABLE 0x00020000 #define IMAGE_SCN_MEM_16BIT 0x00020000 #define IMAGE_SCN_MEM_LOCKED 0x00040000 #define IMAGE_SCN_MEM_PRELOAD 0x00080000   #define IMAGE_SCN_ALIGN_1BYTES 0x00100000 // #define IMAGE_SCN_ALIGN_2BYTES 0x00200000 // #define IMAGE_SCN_ALIGN_4BYTES 0x00300000 // #define IMAGE_SCN_ALIGN_8BYTES 0x00400000 // #define IMAGE_SCN_ALIGN_16BYTES 0x00500000 // Default alignment if no others are specified. #define IMAGE_SCN_ALIGN_32BYTES 0x00600000 // #define IMAGE_SCN_ALIGN_64BYTES 0x00700000 // #define IMAGE_SCN_ALIGN_128BYTES 0x00800000 // #define IMAGE_SCN_ALIGN_256BYTES 0x00900000 // #define IMAGE_SCN_ALIGN_512BYTES 0x00A00000 // #define IMAGE_SCN_ALIGN_1024BYTES 0x00B00000 // #define IMAGE_SCN_ALIGN_2048BYTES 0x00C00000 // #define IMAGE_SCN_ALIGN_4096BYTES 0x00D00000 // #define IMAGE_SCN_ALIGN_8192BYTES 0x00E00000 // // Unused 0x00F00000   #define IMAGE_SCN_LNK_NRELOC_OVFL 0x01000000 // Section contains extended relocations. #define IMAGE_SCN_MEM_DISCARDABLE 0x02000000 // Section can be discarded. #define IMAGE_SCN_MEM_NOT_CACHED 0x04000000 // Section is not cachable. #define IMAGE_SCN_MEM_NOT_PAGED 0x08000000 // Section is not pageable. #define IMAGE_SCN_MEM_SHARED 0x10000000 // Section is shareable. #define IMAGE_SCN_MEM_EXECUTE 0x20000000 // Section is executable. #define IMAGE_SCN_MEM_READ 0x40000000 // Section is readable. #define IMAGE_SCN_MEM_WRITE 0x80000000 // Section is writeable.

RVA与FOA的转换

想象一下,如果你想通过逆向的方式改变一个全局变量的初始值,该怎么做?首先我们可以写一个程序,输出一个全局变量的地址和值:

int a = 0x12345678;   int main() { printf("Address: 0x%x \n", &a); printf("Value: 0x%x \n", a); getchar(); return 0; }

images/download/attachments/26902549/image2021-12-6_21-20-44.png

我们运行程序可以看见相应的值,那么我们可以是否可以在文件中直接搜索对应的值然后修改呢?这种方法没有毛病,但是文件中也许会存在很多个0x12345678,你无法准确的知道哪一个才是全局变量;那么,又是否可以通过已经给出的这个地址0x42ba30直接去寻找呢?当然也是不行的,因为在之前章节的学习中我们了解到,PE文件有2种状态(动静态),在这2种状态下,文件的对齐方式会发生变化,所以当前的地址是PE文件运行时(动态)的地址,你需要转换成在磁盘上(静态)的地址
images/download/attachments/26902549/image2021-12-6_21-35-26.png

这两种状态的地址相互转换,我们可以称之为RVA与FOA的转换,RVA就是相对虚拟地址,FOA就是文件偏移地址;从RVA转换到FOA,就是从文件运行时(动态)的地址转换成在磁盘上(静态)的地址,按如下公式可以进行转换:

  1. RVA地址由内存地址减去ImageBase地址(PE文件在内存中的开始位置是由扩展PE头中的ImageBase决定);

  2. 判断RVA地址是否位于PE头中:

    1. 如果是,那么RVA等于FOA

    2. 如果不是,判断RVA位于哪个节:==》见后面逆向工程核心原理书中所述

      1. 当满足RVA地址大于等于节.VirtualAddress和RVA地址小于等于节.VirtualAddress加上当前节内存对齐后的大小时,就表示RVA地址在该节中。

      2. RVA地址减去节.VirtualAddress等于差值,FOA地址就是根据节.PointerToRawData加上差值

在一些较老的编译器中,编译出来的文件会区分文件对齐、内存对齐,但是在现在的编译器编译出来的程序,文件对齐与内存对齐时完全一样的,所以我们不用费这么大的周折,我们只需要算出RVA的值就可以得出FOA的值。

例如,在当前程序中就是这样,根据0x42BA30-0x400000(ImageBase)得出0x2BA30,其是RVA,也是FOA,直接使用Winhex打开找到:

images/download/attachments/26902549/image2021-12-6_22-4-37.png

可以直接修改它然后保存运行,这时候你就会发现全局变量的值已经发生了改变:

images/download/attachments/26902549/image2021-12-6_22-7-35.png

 ==》在我的机器上实践下:

32位,release编译,vs2017出来的结果:

二者并不一样!

 换成64位,二者也不相同。

 

 

 

补充:

关于RVA和FOA(RAW)二者的转换,在逆向工程核心原理里提到过:

 

 

 

 

我们以书中第一个问题为例,核心计算方式就是section里的偏移是一致的!Image在内存中虽然变大,但是都是填充的NULL!

 

标签:SCN,WORD,IMAGE,对照,PE,DWORD,解析,define
From: https://www.cnblogs.com/bonelee/p/17388067.html

相关文章

  • openssh win
    checkifisadmin(New-ObjectSecurity.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)#StartthesshdserviceStart-Servicesshd#OPTIONALbutrecommende......
  • 模板元编程--TypeList算法--类型分割
    将一个数据列表按照要求尽心分割为两个类型。usinglist1=TypeList<char,double,float,longlong,int,int>;将大于4字节的分为一组,其他分为一组:贴入Fold函数,一会儿用到:template<typenameT>conceptTL=requires{typenameT::isTypeList;typenameT::ty......
  • zookeeper相关命令
    1、启动zookeeper服务:bin目录下执行,./zkServer.sh start。2、关闭zookeeper服务:bin目录下执行,./zkServer.sh stop。3、查看zookeeper服务:bin目录下执行,./zkServer.sh status。未启动Zookeeper服务。启动了Zookeeper服务。4、重启zookeeper服务:bin目录下执行,./zkServer......
  • java代码中fastjson生成字符串和解析字符串的方法和javascript文件中字符串和json数组
    1.java代码中fastjson生成字符串和解析字符串的方法List<TemplateFull>templateFulls=newArrayList<TemplateFull>();JSONArrayjsonArr=newJSONArray();jsonArr.addAll(templateFulls);StringjsonStr=jsonArr.toJSONString();System.out.pr......
  • 直播网站程序源码,【openpyxl】只读模式、只写模式
    直播网站程序源码,【openpyxl】只读模式、只写模式1.只读模式只读模式,如果你需要读取很大的Excel文件,但是又不改变和保存,例如只读取数值用于其他数据分析,这时候我们完全可以使用只读模式提供性能 fromopenpyxlimportload_workbook#加载Excel文件时使用read_only指定只读模......
  • open与openat
    原文:https://evian-zhang.github.io/introduction-to-linux-x86_64-syscall/src/filesystem/open-openat-name_to_handle_at-open_by_handle_at-open_tree.html系统调用号open为2,openat为257。函数原型内核接口 asmlinkagelongsys_open(constchar__user*filename,int......
  • experiment 5
    1#include<stdio.h>#defineN4intmain(){intx[N]={1,9,8,4};inti;int*p;for(i=0;i<N;++i)printf("%d",x[i]);printf("\n");return0;}    1.2 #include<stdio.h>intmain(){intx[2][4]={{1,9,8,4},{2,0,4,9}};inti,......
  • Experiment5
    #include<stdio.h>#defineN4intmain(){intx[N]={1,9,8,4};inti;int*p;for(i=0;i<N;++i)printf("%d",x[i]);printf("\n");for(p=x;p<x+N;++p)pri......
  • 在 CentOS 7上安装和使用 FFmpeg
    网上找了那么多资料,还是这个最靠谱...ChatGPT也是在乱教人根据解压包进行安装ffmpeg会导致我的golang使用exec.Command函数的时候没办法调用ffmpeg命令,即使在终端中我可以使用ffmpeg命令。会报不在%PATH的错误,即使我根据网上的教程把他加入了%PATH也还是不行。还是下述最靠谱,都......
  • 步进电机Irms电流 Ipeak电流的关系
    大多数精密步进电机应用——尤其是自动化和运动控制应用——使用一种称为微步进的控制方法,它产生正弦(或接近正弦)电流波形,而不是典型的全步控制产生的电流方波。步进电机的额定电流取决于功率大小,因此也取决于电机绕组可以承受的热量。功率与电阻和电流的平方直接相关(P=I 2 R......