您当前的位置: 首页 > 

插件开发

暂无认证

  • 1浏览

    0关注

    492博文

    0收益

  • 0浏览

    0点赞

    0打赏

    0留言

私信
关注
热门博文

PE文件格式分析-WinHex工具-文件头-32位PE-部分64位PE

插件开发 发布时间:2022-06-17 08:29:42 ,浏览量:1

文章目录
    • 1.名称来源
    • 2.PE文件基本结构
    • 3.DOS头
    • 4.DOS存根:
    • 5.NT头
      • 5.1.文件头
      • 5.2.可选头
    • 6.节区头表
    • 7.数据目录详解
      • 7.1.导入函数表
      • 7.2.重定位表
    • 8.作者答疑

1.名称来源

  PE即Portable Executable,是Windows OS下使用的可执行文件格式。PE文件是指32位的可执行文件,亦称为PE32。64位的可执行文件称为PE+或PE32+,是PE文件的一种扩展形式。常见PE文件格式如下图所示: 在这里插入图片描述

2.PE文件基本结构 从开始到结束DOS头DOS存根NT头节区头列表信息NULL.textNULL.dataNULL.rsrcNULL 3.DOS头

  PE文件最前面是IMAGE_DOS_HEADER结构体,用来扩展已有的DOS EXE头。该结构体大小为40(h)字节,其中有两个重要的成员e_magic和e_lfanew。   e_magic:DOS签名(4D5Z,即“MZ”)。   e_lfanew:指示NT头的偏移。   如下图所示: 在这里插入图片描述

4.DOS存根:

  DOS存根(stub)在DOS下方,是个可选项,由代码和数据混合而成,大小不固定(即使没有DOS存根,文件也可以正常运行)。Windows OS不会运行其中的代码,但在DOS环境中可以运行。显示上图右边的字符串就终止。汇编语言基础:byte是字节,也就是8位。用来储存char或者char类型指针。word是字,也就是16位。用来储存16位整数或者16位地址。dword是双字,也就是32位。可以用来储存32位整数或者32位内存地址。

5.NT头

  IMAGE_NT_HEADERS结构体大小为0x108个字节,由3个成员组成:Signature签名结构体,其值为50450000h(“PE”00);IMAGE_FILE_HEADER文件头结构体,大小0x14个字节;IMAGE_OPTIONAL_HEADER32可选头结构体,大小0xe0,IMAGE_OPTIONAL_HEADER64可选头结构体,大小0xf0。

5.1.文件头

  IMAGE_FILE_HEADER结构体用于表示文件大致属性。文件数据如下图所示: 在这里插入图片描述   其中有4个重要成员。   1.Machine:每个CPU都有唯一的MAchine码,如AMD64(K8)为8664。   2.NumberOfSections:说明文件中存在的节区数量。以上数据为8个。   3.SizeOfOptionalHeader:说明IMAGE_OPTIONAL_HEADER32结构体的长度。IMAGE_OPTIONAL_HEADER32结构体由C语言编写,因而其大小确定,但Windows的PE装载器需要查看IMAGE_FILE_HEADER结构体SizeOfOptionalHeader值来识别出IMAGE_OPTIONAL_HEADER32结构体的大小。PE32+文件使用的是IMAGE_OPTIONAL_HEADER64结构体,两者大小不同,所以需要SizeOfOptionalHeader来识别结构体的大小。   4.Characteristics:用于识别文件的属性,文件是否是可运行的形态、是否为DLL文件等信息,以bit OR形式组合起来。其中0002h为文件可执行,2000h为DLL文件。

5.2.可选头

在这里插入图片描述   1.Magic:当为IMAGE_OPTIONAL_HEADER32结构体时,为10B;当为IMAGE_OPTIONAL_HEADER64结构体时,为20B。   2.AddressOfEntryPoint:持有EP的RVA值,指出程序最先执行的代码起始地址。   3.ImageBase:指出文件的优先装入地址。EXE、DLL文件被装载到用户内存的0~7FFFFFFF中,SYS文件被装载到内核内存的80000000~FFFFFFFF中。一般地,使用开发工具创建好EXE文件后,其ImageBase值为00400000,DLL文件的为10000000。执行PE文件时,PE装载器先创建进程,在将文件载入内存,然后把EIP寄存器的值设置为ImageBase+AddressOfEntryPoint。   4.SectionAlignment,FileAlignment:SectionAlignment指定了节区在内存中的最小单位,FileAlignment指定了节区在磁盘文件中的最小单位。磁盘文件或对应节区大小比定位FileAlignment或SectionAlignment值的整数倍。   5.SizeOfImage:指定PE Image在虚拟内存中所占空间的大小。一般地,文件的大小与加载到内存中的大小是不同的。   6.SizeOfHeader:指明整个PE头的大小,必须是FileAlignment的整数倍。第一节区所在位置与SizeOfHeader距文件开始偏移的量相同。   7.Subsystem:用来区分系统驱动文件(SYS)和普通的可执行文件(EXE、DLL)。   8.NumberOfRvaAndSizes:用来指定DataDirectory数组的个数。虽然结构体定义中明确指出数组个数为IMAGE_NUMBEROF_DIRECTORY_ENTRIES(16),但PE装载器通过查看NumberOfRvaAndSizes来识别数组大小。   9.DataDirectory:由IMAGE_DATA_DIRECTORY结构体组成。数据目录如下所示: 在这里插入图片描述

6.节区头表

  在NT头后面是节区表,中间空了8个字节,节区头是由IMAGE_SECTION_HEADER(大小为0x28)结构体组成的数组,每个结构体对应一个节区。节头区分为数据目录,如下图所示: 在这里插入图片描述   IMAGE_SECTION_HEADER结构体中几个重要的成员如下:   VirtualSize:内存中节区所占大小。   VirtualAddress:内存中节区起始地址(RVA)。   SizeOfRawData:磁盘文件中节区所占大小。   PointerToRawData:磁盘文件中节区起始位置。   Characteristics:节区属性(bit OR)。   VirtualAddress与PointToRawData不带有任何值,分别由SectionAlignment和FileAlignment确定。SizeOfRawData与VirtualSize一般具有不同的值,即磁盘文件中节区的大小和加载到内存中的节区大小是不同的。   通过节区头表描述了虚拟内存与磁盘文件之间的映射关系。简单来说就是将磁盘文件起始位置后一定的数据加载到目标内存位置。

7.数据目录详解 7.1.导入函数表

  在IMAGE_DATA_DIRECTORY中,有几项的名字都和导入表有关系,其中包括:IMAGE_DIRECTORY_ENTRY_IMPORT,IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT,IMAGE_DIRECTORY_ENTRY_IAT和IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT这几个导入表的作用,如下表所示:

名称解释IMAGE_DIRECTORY_ENTRY_IMPORT通常导入表,在PE文件加载时,会根据这个表里的内容加载依赖的DLL,并填充所需函数的地址。IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT叫做绑定导入表,在第一种导入表导入地址的修正是在PE加载时完成,如果一个PE文件导入的DLL或者函数多那么加载起来就会略显的慢一些,所以出现了绑定导入,在加载以前就修正了导入表,这样就会快一些。IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT延迟导入表,一个PE文件也许提供了很多功能,也导入了很多其他DLL,但是并非每次加载都会用到它提供的所有功能,也不一定会用到它需要导入的所有DLL,因此延迟导入就出现了,只有在一个PE文件真正用到需要的DLL,这个DLL才会被加载,甚至于只有真正使用某个导入函数,这个函数地址才会被修正。IMAGE_DIRECTORY_ENTRY_IAT是导入地址表,前面的三个表其实是导入函数的描述,真正的函数地址是被填充在导入地址表中的。

  导入函数表里描述了当前可执行模块导入了哪些DLL文件中的函数信息。进入之后的首要结构是IMAGE_IMPORT_DESCRIPTOR(大小是0x14),一个IMAGE_IMPORT_DESCRIPTOR内存图如下所示: 在这里插入图片描述

typedef struct _IMAGE_IMPORT_DESCRIPTOR {
    union {
        DWORD   Characteristics;            // 0 for terminating null import descriptor
        DWORD   OriginalFirstThunk;         // RVA to original unbound IAT (PIMAGE_THUNK_DATA)
    } DUMMYUNIONNAME;
    DWORD   TimeDateStamp;                  // 0 if not bound,
                                            // -1 if bound, and real date\time stamp
                                            //     in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND)
                                            // O.W. date/time stamp of DLL bound to (Old BIND)
    DWORD   ForwarderChain;                 // -1 if no forwarders
    DWORD   Name;
    DWORD   FirstThunk;                     // RVA to IAT (if bound this IAT has actual addresses)
} IMAGE_IMPORT_DESCRIPTOR;

  OriginalFirstThunk:INT的地址(RVA)。   Name:库名称字符串的地址(RVA)。   FirstThunk:IAT的地址(RVA)。   导入函数表是长整型数组,以NULL结束,两者大小应相同。   INT与IAT是两个列表,他们是IMAGE_IMPORT_BY_NAME结构体数组。

typedef struct _IMAGE_IMPORT_BY_NAME {
    WORD    Hint;//-->IMAGE_THUNK_DATA64/32
    CHAR   Name[1];
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;

typedef struct _IMAGE_THUNK_DATA64 {
    union {
        ULONGLONG ForwarderString;  // PBYTE 
        ULONGLONG Function;         // PDWORD
        ULONGLONG Ordinal;
        ULONGLONG AddressOfData;    // PIMAGE_IMPORT_BY_NAME
    } u1;
} IMAGE_THUNK_DATA64;

typedef struct _IMAGE_THUNK_DATA32 {
    union {
        DWORD ForwarderString;      // PBYTE 
        DWORD Function;             // PDWORD
        DWORD Ordinal;
        DWORD AddressOfData;        // PIMAGE_IMPORT_BY_NAME
    } u1;
} IMAGE_THUNK_DATA32;
typedef IMAGE_THUNK_DATA64 * PIMAGE_THUNK_DATA64;

  PE装载器将导入函数输入至IAT的顺序:   1、读取IID的Name成员,获取库名称字符串;   2、装载相应的库;   3、读取IID的OriginalFirstThunk成员,获取INT地址;   4、逐一读取INT中数组的值,获取相应IMAGE_IMPORT_BY_NAME地址(RVA);   5、使用IMAGE_IMPORT_BY_NAME的Hint(ordinal)或Name项,获取相应函数的起始地址;   6、读取IID的FirstThunk(IAT)成员,获得IAT地址;   7、将上述获得的函数地址输入相应的IAT数组值中;   8、重复步骤4~7,直至INT结束(遇到NULL时)。

  这时我们可以回头看看OriginalFirstThunk与FirstThunk,OriginalFirstThunk指向的IMAGE_THUNK_DATA数组包含导入信息,在这个数组中只有Ordinal和AddressOfData是有用的,因此可以通过OriginalFirstThunk查找到函数的地址。FirstThunk则略有不同,在PE文件加载以前或者说在导入表未处理以前,他所指向的数组与OriginalFirstThunk中的数组虽不是同一个,但是内容却是相同的,都包含了导入信息,而在加载之后,FirstThunk中的Function开始生效,他指向实际的函数地址,因为FirstThunk实际上指向IAT中的一个位置,IAT就充当了IMAGE_THUNK_DATA数组,加载完成后,这些IAT项就变成了实际的函数地址,即Function的意义。还是上个图对比一下: 在这里插入图片描述   上图为导入之前,下图为导入之后: 在这里插入图片描述

7.2.重定位表

  在生成程序的时候,很多涉及到地址的代码,都使用一个绝对的虚拟内存地址(这个虚拟内存地址是假设程序加载到0x400000的地方时才能够使用的),但是当程序的加载基址产生变化的时候,新的加载基址和默认的加载基址就不一样了,那些涉及到地址的代码就不能运行了,此时就需要将那些涉及到地址的代码,把他们的操作数修改(去掉默认加载基址,再加上新的加载基址)才能够使程序运行起来。当代码段使用了其他区段的数据,所生成的代码就会产生重定位的需求,其他段数据的首地址,就是产生重定位需求的根源,凡是出现全局变量的地方,都会导致重定位的产生。32位调用外部模块的函数都会产生重定位。   举一个例子(32位): 假设实际加载基址为0x80 0000默认加载基址为0x40 0000,在0x401000这个地方有这么一条指令:call 0x403000 即0x40 1000 call 0x403000,重定位表中的VA为0x1000(4KB),此时的offset为1,那么此时0x403000这个指令所对应的地址为1000+1+400000,对其进行解引用,就会得到0x403000这个值,然后用0x403000减去0x400000+0x800000即可,得到的值为0x803000,再将0x803000写入原来的地址,则原来那条指令就变成了0x401000 call 0x803000。

typedef struct _IMAGE_BASE_RELOCATION
{
    DWORD VirtualAddress; //这里的VA实际上是一个相对虚拟地址(RVA)
    DWORD SizeOfBlock;
    //WORD TypeOffset[1]; //这里的1表示不定长,长度不知道
}
IMAGE_BASE_RELOCATION;
//重定位表里面 像上面这样的结构体有若干个(类似于结构体数组,但却不是结构体数组,因为结构体数组它是定长的,这里不定长),
//DWORD   VirtualAddress;
//DWORD   SizeOfBlock;这两个是定长的,而TypeOffset[1]紧跟在结构体之后,它是不定长的
//每一个结构体中的VirtualAddress指的是每一个内存分页的起始的RVA
//SizeOfBlock 指的是这个分页到下个分页之间需要重定位的所有数据的相关大小
//实际上有两大块组成1.指的是DWORD   VirtualAddress; DWORD   SizeOfBlock;所在结构体大小
//2.指的是需要重定位的偏移offset, 用offset+VirtualAddress得到的是一个RVA,
//这个RVA是存放 需要修改的那个地址,的地址所对应RVA
//就是说,需要修改的那个地址,作为一个变量,存放在内存里面,而这个内存自己本身,也有自己的地址
//存放需要修改的那个地址的内存的地址是需要通过 RVA + 一个加载基址得到的
//而这个RVA就是通过上面的 offset+VirtualAddress得到的,所对应的这个加载基址是由GetModuleHandle获得的
//得到这个需要修改的那个地址(通常需要解引用,解引用才能得到所要修改的地址)之后,用它减去原来的加载基址(默认的加载基址),
//再加上此时的加载基址(由GetModuleHandle获得的),就得到了加载后的真正的地址

在这里插入图片描述 注意:如果编辑64位的PE文件,重定位表和导入表,原理基本一致,但是数据结构有差别,广大读者需要注意。 参考网址:https://blog.csdn.net/evileagle/article/details/12357155?utm_medium=distribute.wap_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromMachineLearnPai2%7Edefault-2.wap_blog_relevant_pic&depth_1-utm_source=distribute.wap_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromMachineLearnPai2%7Edefault-2.wap_blog_relevant_pic https://blog.csdn.net/richard1230/article/details/82977564

  合理的脚本代码可以有效的提高工作效率,减少重复劳动。

8.作者答疑

  如有疑问,敬请留言。

关注
打赏
1665481431
查看更多评论
立即登录/注册

微信扫码登录

0.0391s