PE文件

  1. 文件是存储数据的实体,不同的文件是给不同的软件去使用,不同的文件主要是格式不同

    格式就是数组的排列组织方式,软件读取文件按照固定形式去解析文件

  2. PE文件:Portable Executable,可执行文件

    供 Windows 系统解析,解析完后就能创建出进程去运行

  3. 学习PE文件就是学习一堆结构体

  4. FOA (Offset):文件偏移,某个数据距离文件开头的偏移

    VA:虚拟地址,程序运行时是将PE文件加载到进程的内存空间中,这块内存空间称之为虚拟内存空间,32位虚拟内存空间以字节为单位,每个字节都有一个编号,从 0x00000000 到 0xFFFFFFFF

    RVA:相对虚拟地址,PE文件不会占满整个虚拟内存空间,而是会占用一部分,那么就会有一个起始位置,这个起始位置也称为加载基址,PE文件中的数据相对于加载基址的偏移就是相对虚拟基址

    EXE默认加载基址是 0x400000, DLL文件默认基址是 0x10000000,需注意基址不是程序的入口点

    虚拟地址(VA) = 基地址 + 相对虚拟地址(RVA)

    FOA - 该区段在文件中的起始地址 = RVA - 该区段在内存中的起始地址

    系统加载PE文件,是将PE文件原封不动地复制到内存中,那么某一个数据的FOA和RVA就是相等的

DOS头

  1. Windows 系统中的可执行文件在设计时考虑到兼容性问题,在正常的可执行文件开始部分嵌入一个DOS可执行文件,作用就是在MS-DOS系统下能够输出一行“这个程序不是运行在此系统下的”

    只有2个字段是有用的:

    • 第一个 e_magic:永远是 0x5A4D
    • 最后一个 e_ifanew:真正可执行文件的起始位置

NT头

  1. DWORD Signature:永远是 0x00004550

  2. IMAGE_FILE_HEADER:文件头

    • NumberOfSection:区段数量
    • SizeOfOptionalHeader:扩展头大小,因扩展头中数据目录表的个数是不确定的,故需要一个大小
  3. IMAGE_OPTIONAL_HEADER:扩展头

    • ImageBase:程序默认的加载基址

    • AddressOfEntryPoint:程序入口点(EP)

    • SectionAlignment:内存对齐,0x1000(1页内存4KB)

    • FileAlignment:文件对齐,0x200

    • SizeOfImage:映像大小,即PE文件被加载到内存占用的空间大小

    • SizeOfHeader:头部大小,DOS头 + NT头 + 区块表大小

    • DllCharacteristics:PE的一组属性

    • DataDirectory:数据目录表,描述了PE文件中16个或更多非常重要的数据块的大小和位置

      导入表、导出表、重定位表、资源表、TLS表、……

区段表

结构体数组,数组的元素个数由头文件中的 NumberOfSection 决定,区段表的一个元素描述的就是一个区段的信息

  1. Name:区段名
  2. PointerToRawData:在文件中的位置
  3. SizeOfRawData:在文件中的大小
  4. VirtualAddress:在内存中的位置
  5. VirtualSize:在内存中的大小
  6. Characteristics:区段的属性:可读、可写、可执行

导出表

  1. 一个程序的运行实际由多个部分组成,通常有一个 exe 和多个 dll,dll 会提供函数、变量给其他模块使用,但并非所有 dll 中的函数都能提供给其他模块使用,只有在编写 dll 时,函数、变量被导出了才能提供给其他模块使用,导出表就是专门用来记录本文件导出信息的一个数据结构

  2. 导出函数地址表的 RVA:AddressOfFunctions

    导出函数名称表的 RVA:AddressNames

    导出函数序号表的 RVA:AddressNameOrdinals

    名称表元素个数和序号表元素个数是相同的

    地址表中元素可能会比序号表和名称表元素个数要多

    Windows 的 PE 文件支持2种导出方式:无论何种导出方式,都有函数地址

    1. 名称导出:函数既有名称又有序号
    2. 序号导出:函数只有序号,没有名称

    地址表中多出来的就是没有名称的函数或无效函数

  3. 通过导出表能够获得一个模块任何导出函数的地址,相当于自己能够实现 GetProcAddress

    • 若导入地址表被破坏了,可以修复导入地址表
    • 可以检测 IAT-Hook,主要思路就是获取 IAT 位置原始的函数地址
    • 适用于不方便使用 GetProcAddress 而需要通过函数名或序号获取函数地址的情况

导入表

  1. 该模块使用了其他哪些模块提供的哪些函数,需要记录这些信息,记录的信息就在导入表中

    重要字段:

    1. OriginalFirstThunk(INT):导入名称表的 RVA
    2. FirstThunk(IAT):导入地址表的 RVA
    3. Name:导入的 dll 名称的 RVA

    INT 和 IAT 在还是文件的时候(即还未运行),里面存储的内容是一样的,因为在程序未运行时,无法得知此模块会加载到什么位置,也就无法得知函数的地址是多少,程序运行后,系统会将 IAT 填充上函数的地址,所有调用其他模块函数的代码全都是 call ds:[IAT地址]

  2. 通过数据目录表的第1项,得到 RVA,就能找到 IMAGE_IMPORT_DESCRIPTOR 结构体的数组,数组以全0元素结尾

  3. 知道导入表能够知道模块之间是如何进行配合的

    • 可以做 IAT-Hook,替换 IAT 表中的内容,就可以 Hook
    • 知道一个 exe 用到了哪些模块的哪些函数,可以根据函数名猜测功能,利于分析程序

延迟加载

1
2
3
4
5
6
7
8
9
10
#include<windows.h>
#include<delayimp.h>
#pragma comment(lib, "Delayimp.lib")
//设置“链接器”->“输入”->“延迟加载的DLL”选项中的值
//为我们需要延迟加载的DLL名称(大小写必须完全一致)
int main()
{
MessageBox(0, 0, 0, 0);
return 0;
}

延迟加载前

image-20191112212232982

延迟加载

image-20191112212321148

image-20191112212424734