PE文件笔记

PE文件笔记

跟着b站学习完了PE结构,自己再整理和梳理一遍,巩固一下记忆!

文件结构图:

1379525-20191026162634249-935893360

1. PE文件简介

PE文件的全称是Portable Executable,意为可移植的可执行的文件,常见的EXE、DLL、OCX、SYS、COM都是PE文件,PE文件是微软Windows操作系统上的程序文件(可能是间接被执行,如DLL)

PE文件有自己的一种文件格式,叫做PE文件格式,它包含了可执行文件的描述信息,以及程序运行时需要的数据。PE文件格式由以下几个部分组成:

  • MS-DOS头部:这是为了兼容MS-DOS系统而保留的一个头部结构,它包含了一些基本信息,如EXE标志、重定位表偏移量、PE头偏移量等
  • DOS Stub:这是一个可选的字节块,它通常是一个显示“此程序不能在DOS模式下运行”的消息的程序
  • PE头标识:这是一个四字节的标识,其内容固定为“PE00”,用来表示这是一个PE文件
  • 标准PE头:这是一个20字节的结构,它记录了PE文件的全局属性,如运行平台、节的数量、创建时间等
  • 扩展PE头:这是一个224字节的结构,它记录了PE文件执行时的一些重要信息,如入口地址、装载基址、节对齐粒度、校验和等
  • 数据目录:这是一个128字节的结构,它记录了PE文件中一些特殊数据的位置和大小,如导入表、导出表、资源表等
  • 节表:这是一个由若干个40字节的结构组成的表格,它记录了PE文件中每个节的名称、位置、大小、属性等信息
  • 节数据:这是PE文件中实际存放代码和数据的部分,它由若干个节组成,每个节对应于节表中的一项

2. Dos头解析

DOS头是PE文件开头的一个固定长度的结构体,是为了兼容MS-DOS系统而保留的一个头部结构,大小0x40字节。包含如下信息

  • e_magic:一个WORD类型,值是一个常数0x4D5A,用文本编辑器查看该值位 MZ,可执行文件必须都是 MZ 开头
  • e_lfanew:为32位可执行文件扩展的域,用来表示DOS头之后的PE头相对文件起始地址的偏移
  • e_cblp:最后一页中字节数
  • e_cp:文件中页数
  • e_crlc:重定位项数
  • e_cparhdr:头部占用的内存段数
  • e_minalloc:最小需要的附加内存段数
  • e_maxalloc:最大需要的附加内存段数
  • e_ss:初始SS值(相对于段起始地址)
  • e_sp:初始SP值
  • e_csum:校验和
  • e_ip:初始IP值(相对于段起始地址)
  • e_cs:初始CS值(相对于段起始地址)
  • e_lfarlc:重定位表相对于文件起始地址的偏移
  • e_ovno:覆盖号
  • e_res:保留字
  • e_oemid:OEM标识符
  • e_oeminfo:OEM信息
  • e_res2:保留字

在DOS头中比较重要的两个参数是e_magic和e_lfanew,前者是PE文件标识头,后者是PE文件头的相对文件起始地址的偏移。

Typora卸载程序的DOS头

e_magic是前两个字节,对应的是字符串是MZ,e_ifanew是后四个字节,因为是小端序,所以对应的值是0x100,也就是说PE文件头对应偏移是0x100

PE文件头偏移

在DOS头之后,还有一段DOS块,这段空间是由编译器生成,用来存储一些程序运行的信息,可以随意更改,对程序没有影响

3. PE头解析

PE头又叫NT头,是整个PE文件真正的头部,共有三个字段

  1. DWORD Signature PE文件头标识,两个字节,一般是504500
  2. struct IMAGE_FILE_HEADER FileHeader 结构体,文件头
  3. struct IMAGE_OPTIONAL_HEADER32 OptionalHeader结构体, 可选文件头

文件头结构体包含6个字段

  1. enum IMAGE_MACHINE Machine WORD类型,程序允许的的CPU型号,为0则支持所有CPU
  2. WORD NumberOfSections 文件中区段的数量
  3. time_t TimeDateStamp 时间戳
  4. DWORD PointerToSymbolTable
  5. DWORD NumberOfSymbols
  6. WORD SizeOfOptionalHeader 可选PE头的大小,32位默认0xE0,64位默认0xF0
  7. struct FILE_CHARACTERISTICS Characteristics WORD,文件属性,通过16位二进制控制是否可执行,是否是dll之类的文件属性

Typora卸载程序在DIE分析结果如下

在DIE下Typora卸载程序文件头

64位下可选PE头包含以下字段

  1. enum OPTIONAL_MAGIC Magic 标识64位32位,前者20B,后者10B
  2. BYTE MajorLinkerVersion
  3. BYTE MinorLinkerVersion
  4. DWORD SizeOfCode 所有代码段的总大小,按照文件对齐后的大小
  5. DWORD SizeOfInitializedData 已初始化的数据大小,按照文件对齐
  6. DWORD SizeOfUninitializedData 未初始化的数据大小,按照文件对齐
  7. DWORD AddressOfEntryPoint 程序入口的OEP 存储的是一个偏移值,程序入口的内存偏移
  8. DWORD BaseOfCode 代码开始地址
  9. DWORD BaseOfData 数据开始地址
  10. DWORD ImageBase 内存镜像地址,有可能是动态的,在DLL_CHARACTERISTICS DllCharacteristics中定义是否动态
  11. DWORD SectionAlignment 内存对齐大小
  12. DWORD FileAlignment 文件对齐大小
  13. WORD MajorOperatingSystemVersion
  14. WORD MinorOperatingSystemVersion
  15. WORD MajorImageVersion
  16. WORD MinorImageVersion
  17. WORD MajorSubsystemVersion
  18. WORD MinorSubsystemVersion
  19. DWORD Win32VersionValue
  20. DWORD SizeOfImage 文件在内存中的大小,按照内存对齐后的大小
  21. DWORD SizeOfHeaders DOS头+NT头+标准PE头+可选PE头+区段头,按照文件对齐后的大小
  22. DWORD CheckSum
  23. enum IMAGE_SUBSYSTEM Subsystem
  24. struct DLL_CHARACTERISTICS DllCharacteristics
  25. DWORD SizeOfStackReserve
  26. DWORD SizeOfStackCommit
  27. DWORD SizeOfHeapReserve
  28. DWORD SizeOfHeapCommit
  29. DWORD LoaderFlags
  30. DWORD NumberOfRvaAndSizes 数据目录表的个数
  31. struct IMAGE_DATA_DIRECTORY_ARRAY DataDirArray 数据目录表数组在导出表导入表里分析

可选头中动静态地址分析

在VS中控制该属性的地方:

动态基址生成文件

数据目录表数组以及数据目录表结构体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//数据目录表数组
struct IMAGE_DATA_DIRECTORY Export//导出表
struct IMAGE_DATA_DIRECTORY Import//导入表
struct IMAGE_DATA_DIRECTORY Resource//资源
struct IMAGE_DATA_DIRECTORY Exception//异常
struct IMAGE_DATA_DIRECTORY Security//安全
struct IMAGE_DATA_DIRECTORY BaseRelocationTable//重定位表
struct IMAGE_DATA_DIRECTORY DebugDirectory//调试信息
struct IMAGE_DATA_DIRECTORY CopyrightOrArchitectureSpecificData//版权信息
struct IMAGE_DATA_DIRECTORY GlobalPtr//RVA OF GP
struct IMAGE_DATA_DIRECTORY TLSDirectory//TLS directory
struct IMAGE_DATA_DIRECTORY LoadConfigurationDirectory
struct IMAGE_DATA_DIRECTORY BoundImportDirectory
struct IMAGE_DATA_DIRECTORY ImportAddressTable//导入函数地址表
struct IMAGE_DATA_DIRECTORY DelayLoadImportDescriptors
struct IMAGE_DATA_DIRECTORY COMRuntimedescriptor
struct IMAGE_DATA_DIRECTORY Reserved

//表的结构体:
typedef struct
{
DWORD VirtualAddress;//真正表结构体的内存偏移
DWORD Size;//大小,可被修改,没什么用
} IMAGE_DATA_DIRECTORY;


4. 区段头分析

010中exe模版下的区段头结构体,数量取决于PE文件头的NumberOfSections

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
typedef struct
{
BYTE Name[8] <comment="can end without zero">;
//区段名称,字符串不用以0结尾
union
{
DWORD PhysicalAddress;
DWORD VirtualSize;
} Misc;//该区段在内存中的真实大小
DWORD VirtualAddress; //区段在内存中的偏移地址
DWORD SizeOfRawData; //区段在对齐后的大小
DWORD PointerToRawData; //区段在文件中的偏移位置
DWORD PointerToRelocations;//
DWORD PointerToLinenumbers;
WORD NumberOfRelocations;
WORD NumberOfLinenumbers;
SECTION_CHARACTERISTICS Characteristics;//区段属性
} IMAGE_SECTION_HEADER

在010中区段头数量和名称分析如下

image-20230920194909946

5. 导出表分析

导出表结构体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typedef struct
{
DWORD Characteristics;
time_t TimeDateStamp;//时间戳
WORD MajorVersion;
WORD MinorVersion;
DWORD Name;//导出表名称
DWORD Base;//导出表起始序号
DWORD NumberOfFunctions;//导出函数数量 = 终止序号-起始序号+1
DWORD NumberOfNames;//以名称导出函数的个数
DWORD AddressOfFunctions;//EAT 导出函数地址表
DWORD AddressOfNames;//ENT 导出函数名称表
DWORD AddressOfNameOrdinals; //EOT 导出函数序号表
} IMAGE_EXPORT_DIRECTORY ;

结构体中地址表。名称表,序号表都指的是内存偏移,也就是RVA,推到公式是RVA = FOA +filebuff

6. 导入表分析

跟导出表不同,导入表一个程序可以有多个

导入表结构体:

1
2
3
4
5
6
7
8
9
10
11
12
typedef struct
{
union
{
ULONG Characteristics;
ULONG OriginalFirstThunk ;//RVA,指向INT
} DUMMYUNIONNAME;
ULONG TimeDateStamp;//时间戳,如果不与dll绑定时,为0
ULONG ForwarderChain;//
ULONG Name;//RVA dll的名称
ULONG FirstThunk;//RVA to IAT
} IMAGE_IMPORT_DESCRIPTOR

7. 重定位表

1
2
3
4
5
6
typedef struct
{
DWORD VirtualAddress <format=hex,comment=CommentRVA2FOA>;
DWORD SizeOfBlock;
// WORD TypeOffset[1];
} IMAGE_BASE_RELOCATION <comment=CommentImageBaseRelocation>;

8. 利用TEB和PEB找到核心模块的信息

PEB:Process Environment Block 进程环境块,存放进程相关信息

TEB:Thread Environment Block 线程环境块

PEB和TEB分别是进程环境块和线程环境块,它们是存放进程和线程信息的结构体。PEB和TEB的结构体成员有很多,其中一些是用于反调试技术的。PEB和TEB的访问方法有以下几种:

  • 通过FS段寄存器获取TEB的地址,然后通过TEB的ProcessEnvironmentBlock成员(偏移量为0x30)获取PEB的地址
  • 通过Ntdll.NtCurrentTeb()函数获取当前线程的TEB地址,然后通过TEB的ProcessEnvironmentBlock成员(偏移量为0x30)获取PEB的地址
  • 通过GetModuleHandle()函数获取进程的ImageBase地址,然后通过ImageBase地址加上偏移量获取PEB的地址

PEB结构体定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
typedef struct _PEB {
BYTE Reserved1[2];
BYTE BeingDebugged;
BYTE Reserved2[1];
PVOID Reserved3[2];
PPEB_LDR_DATA Ldr; //偏移0xc
PRTL_USER_PROCESS_PARAMETERS ProcessParameters;
PVOID Reserved4[3];
PVOID AtlThunkSListPtr;
PVOID Reserved5;
ULONG Reserved6;
PVOID Reserved7;
ULONG Reserved8;
ULONG AtlThunkSListPtr32;
PVOID Reserved9[45];
BYTE Reserved10[96];
PPS_POST_PROCESS_INIT_ROUTINE PostProcessInitRoutine;
BYTE Reserved11[128];
PVOID Reserved12[1];
ULONG SessionId;
} PEB, *PPEB;

PPEB_LDR_DATA结构体定义

1
2
3
4
5
6
7
typedef struct _PEB_LDR_DATA {
BYTE Reserved1 [8];
PVOID Reserved2 [3];
LIST_ENTRY InMemoryOrderModuleList;//载入顺序排序的dll
LIST_ENTRY InLoadOrderModuleList;//内存排序的dll
LIST_ENTRY InInitializationOrderModuleList;//初始化排序的dll,偏移0x1c
} PEB_LDR_DATA, *PPEB_LDR_DATA;

LIST_ENTRY结构体定义

1
2
3
4
typedef struct _LIST_ENTRY {
struct _LIST_ENTRY *Flink;//下一个节点的指针
struct _LIST_ENTRY *Blink;//上一个节点的指针
} LIST_ENTRY, *PLIST_ENTRY, *PRLIST_ENTRY;

获取kernel32.dll或者kernerbase.dll的模块信息

kernel32.dll和kernerbase.dll都是Windows操作系统的重要文件,它们是NT内核系统的底层API接口的DLL文件,与其他应用程序和运行库进行交互。1

kernel32.dll和kernerbase.dll有相同的功能,但是它们的区别在于,kernel32.dll是一个向后兼容的文件,它包含了一些旧版本的Windows API函数,而kernerbase.dll是一个新的文件,它包含了一些新版本的Windows API函数。

kernel32.dll和kernerbase.dll包含GetProcAddressLoadLibraryALoadModule等等

更多API->Geoff Chappell, Software Analyst

user32.dll->User32大全详解-CSDN博客

mov esi,dword ptr fs:[0x30] 获取PEB地址

mov esi,[esi+0xc] 获取Ldr地址

mov esi,[esi+0x1c] 获取InInitializationOrderModuleList

mov esi,[esi] 获取InInitializationOrderModuleList的第二项,esi既是LIST_ENTRY结构体地址,也是下一个节点指针的地址

mov esi,[esi+0x8] 模块信息,kernel32或者kernerbase模块基址

拿到了dll模块基址,通过基址和固定偏移来访问导出表,再通过导出表和定义好的字符串(函数名称)比较来获取模块指定函数——GetProcAddressLoadLibraryA等等,到这就可以利用函数地址调用指定函数了

mov edx,esi 保存模块基址
mov esi,[esi+0x3c] 获取NT头偏移
lea esi,[edx+esi] 得到NT头地址
mov esi,[esi+0x78] 得到导出表RVA
lea esi,[edx+esi] 得到导出表VA
mov edi,[esi+0x1c] 得到EAT的RVA(地址表)
lea edi,[edi+edx] 得到EAT的RA
mov [ebp-0x4],edi 保存在栈中
mov edi,[esi+0x20] 得到ENT的RVA(名称表)
lea edi,[edx+edi] 得到ENT的VA
mov [ebp-0x8],edi 保存在栈中
mov edi,[esi+0x24] 得到EOT的RVA(序号表)
lea edi,[edx+edi] 得到EOT的VA
mov [ebp-0xc],edi 保存在栈中

LDR_DATA_TABLE_ENTRY结构体是一个用来存放模块信息的结构体,它的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
typedef struct _LDR_DATA_TABLE_ENTRY {
PVOID Reserved1[2];
LIST_ENTRY InMemoryOrderLinks;//双向链表连接所有模块信息
PVOID Reserved2[2];
PVOID DllBase;//模块基址
PVOID EntryPoint;//模块入口点
PVOID Reserved3;
UNICODE_STRING FullDllName;//模块完整名称
BYTE Reserved4[8];
PVOID Reserved5[3];
union {
ULONG CheckSum;
PVOID Reserved6;
};
ULONG TimeDateStamp;
} LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY;

其中,Reserved1,Reserved2,Reserved3,Reserved4,Reserved5和Reserved6都是保留供操作系统内部使用的字段,不对外公开。DllBase是模块的基址,EntryPoint是模块的入口点,FullDllName是模块的完整名称,CheckSum是模块的校验和,TimeDateStamp是模块的时间戳。InMemoryOrderLinks是一个双向链表节点,它用来将所有的模块信息连接起来。

通过导出表kernel32.dll获得其中的“LoadLibraryA” 和 “GetProAddress”

9. 增加区段头向PE文件中植入代码

ShellCode:–执行打印helloworld的代码
$$
“\x60\x8B\xEC\x6A\x00\x68\x72\x6C\x64\x21\x68\x6F\x20\x77\x6F\x68\x68\x65\x6C\x6C\x68\x73\x73\x00\x00\x68\x64\x64\x72\x65\x68\x72\x6F\x63\x41\x68\x47\x65\x74\x50\x6A\x00\x68\x61\x72\x79\x41\x68\x4C\x69\x62\x72\x68\x4C\x6F\x61\x64\x8B\xCC\x51\xE8\x8F\x00\x00\x00\x8B\xE5\x61\xE9\xF7\x70\xFB\xFF\xC3\x55\x8B\xEC\x83\xEC\x20\x56\x64\x8B\x35\x30\x00\x00\x00\x8B\x76\x0C\x8B\x76\x1C\x8B\x36\x8B\x76\x08\x8B\xC6\x5E\x8B\xE5\x5D\xC3\x55\x8B\xEC\x83\xEC\x20\x8B\x55\x08\x8B\x72\x3C\x8D\x34\x16\x8B\x76\x78\x8D\x34\x32\x8B\x7E\x1C\x8D\x3C\x17\x89\x7D\xFC\x8B\x7E\x20\x8D\x3C\x17\x89\x7D\xF8\x8B\x7E\x24\x8D\x3C\x17\x89\x7D\xF4\x33\xC0\xEB\x01\x40\x8B\x75\xF8\x8B\x34\x86\x8D\x34\x16\x8B\x7D\x0C\x8B\x4D\x10\xF3\xA6\x8B\x75\xF4\x75\xE9\x8B\x75\xF4\x33\xFF\x66\x8B\x3C\x46\x8B\x55\xFC\x8B\x34\xBA\x8B\x55\x08\x8D\x04\x16\x8B\xE5\x5D\xC2\x0C\x00\x55\x8B\xEC\x83\xEC\x10\xE8\x6F\xFF\xFF\xFF\x89\x45\xFC\x8D\x4D\x0C\x6A\x0C\x51\x50\xE8\x80\xFF\xFF\xFF\x89\x45\xF8\x6A\x0E\x8D\x4D\x1C\x51\xFF\x75\xFC\xE8\x6F\xFF\xFF\xFF\x89\x45\xF4\x68\x6C\x6C\x00\x00\x68\x72\x74\x2E\x64\x68\x6D\x73\x76\x63\x36\x8D\x4D\xE4\x51\xFF\x55\xF8\x68\x74\x66\x00\x00\x68\x70\x72\x69\x6E\x8D\x4D\xDC\x51\x50\xFF\x55\xF4\x8D\x4D\x2C\x51\xFF\xD0\x8B\xE5\x5D\xC3”
$$

  1. 在文件末尾增加N字节给自己的代码提供空间

  2. 向最后一个区段后添加一个和text属性相同的区段

  3. 在union misc属性下填入N

  4. 在VirtualAddress属性下填入M

M = VirtualAddress(上一个区段)+内存对齐值*(max(SizeOfRawData,VirtualSize)/内存对齐值)

  1. 在SizeOfRawData属性下填入N

  2. 在PointerToRawData属性下填入X

X = SizeOfRawData(上一个区段)+PointerToRawData(上一个区段)

  1. 在文件头的区段数量属性上加一
  2. 在可选文件头的SizeOfImage属性上加N、
  3. 文件末尾填入ShellCode
  4. 修改OEP

附录,收集常见段的含义

rdata段

rdata段是PE文件中的一个区段,它存储了程序用到的资源数据,如图标、字符串、位图等1。rdata段也可以存储一些只读的常量数据,如函数名、类名、导入表等2

rdata段是可选的,不是所有的PE文件都有它。如果有rdata段,它通常位于text段和data段之间3。rdata段的属性是只读的,不能被修改4

bss段

bss段是PE文件中的一个区段,它存储了程序中未初始化或初始化为零的全局变量和静态局部变量1。bss段的大小从可执行文件中得到,然后链接器得到这个大小的内存块,紧跟在数据段后面2。当这个内存区进入程序的地址空间后全部清零,包含data和bss段的整个区段此时通常称为数据区2

bss段是可选的,不是所有的PE文件都有它。如果有bss段,它通常位于text段和data段之间3。bss段的属性是只读的,不能被修改4