PE文件笔记
跟着b站学习完了PE结构,自己再整理和梳理一遍,巩固一下记忆!
文件结构图:
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文件头的相对文件起始地址的偏移。
e_magic是前两个字节,对应的是字符串是MZ,e_ifanew是后四个字节,因为是小端序,所以对应的值是0x100,也就是说PE文件头对应偏移是0x100
在DOS头之后,还有一段DOS块,这段空间是由编译器生成,用来存储一些程序运行的信息,可以随意更改,对程序没有影响
3. PE头解析
PE头又叫NT头,是整个PE文件真正的头部,共有三个字段
DWORD Signature
PE文件头标识,两个字节,一般是504500struct IMAGE_FILE_HEADER FileHeader
结构体,文件头struct IMAGE_OPTIONAL_HEADER32 OptionalHeader
结构体, 可选文件头
文件头结构体包含6个字段
enum IMAGE_MACHINE Machine
WORD类型,程序允许的的CPU型号,为0则支持所有CPUWORD NumberOfSections
文件中区段的数量time_t TimeDateStamp
时间戳DWORD PointerToSymbolTable
DWORD NumberOfSymbols
WORD SizeOfOptionalHeader
可选PE头的大小,32位默认0xE0,64位默认0xF0struct FILE_CHARACTERISTICS Characteristics
WORD,文件属性,通过16位二进制控制是否可执行,是否是dll之类的文件属性
Typora卸载程序在DIE分析结果如下
64位下可选PE头包含以下字段
enum OPTIONAL_MAGIC Magic
标识64位32位,前者20B,后者10BBYTE MajorLinkerVersion
BYTE MinorLinkerVersion
DWORD SizeOfCode
所有代码段的总大小,按照文件对齐后的大小DWORD SizeOfInitializedData
已初始化的数据大小,按照文件对齐DWORD SizeOfUninitializedData
未初始化的数据大小,按照文件对齐DWORD AddressOfEntryPoint
程序入口的OEP 存储的是一个偏移值,程序入口的内存偏移DWORD BaseOfCode
代码开始地址DWORD BaseOfData
数据开始地址DWORD ImageBase
内存镜像地址,有可能是动态的,在DLL_CHARACTERISTICS DllCharacteristics
中定义是否动态DWORD SectionAlignment
内存对齐大小DWORD FileAlignment
文件对齐大小WORD MajorOperatingSystemVersion
WORD MinorOperatingSystemVersion
WORD MajorImageVersion
WORD MinorImageVersion
WORD MajorSubsystemVersion
WORD MinorSubsystemVersion
DWORD Win32VersionValue
DWORD SizeOfImage
文件在内存中的大小,按照内存对齐后的大小DWORD SizeOfHeaders
DOS头+NT头+标准PE头+可选PE头+区段头,按照文件对齐后的大小DWORD CheckSum
enum IMAGE_SUBSYSTEM Subsystem
struct DLL_CHARACTERISTICS DllCharacteristics
DWORD SizeOfStackReserve
DWORD SizeOfStackCommit
DWORD SizeOfHeapReserve
DWORD SizeOfHeapCommit
DWORD LoaderFlags
DWORD NumberOfRvaAndSizes
数据目录表的个数struct IMAGE_DATA_DIRECTORY_ARRAY DataDirArray
数据目录表数组在导出表导入表里分析
在VS中控制该属性的地方:
数据目录表数组以及数据目录表结构体:
1 | //数据目录表数组 |
4. 区段头分析
010中exe模版下的区段头结构体,数量取决于PE文件头的NumberOfSections
1 | typedef struct |
在010中区段头数量和名称分析如下
5. 导出表分析
导出表结构体:
1 | typedef struct |
结构体中地址表。名称表,序号表都指的是内存偏移,也就是RVA,推到公式是RVA = FOA +filebuff
6. 导入表分析
跟导出表不同,导入表一个程序可以有多个
导入表结构体:
1 | typedef struct |
7. 重定位表
1 | typedef struct |
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 | typedef struct _PEB { |
PPEB_LDR_DATA结构体定义
1 | typedef struct _PEB_LDR_DATA { |
LIST_ENTRY结构体定义
1 | typedef struct _LIST_ENTRY { |
获取kernel32.dll或者kernerbase.dll的模块信息
kernel32.dll和kernerbase.dll都是Windows操作系统的重要文件,它们是NT内核系统的底层API接口的DLL文件,与其他应用程序和运行库进行交互。1
kernel32.dll和kernerbase.dll包含GetProcAddress,LoadLibraryA,LoadModule等等
更多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模块基址,通过基址和固定偏移来访问导出表,再通过导出表和定义好的字符串(函数名称)比较来获取模块指定函数——GetProcAddress,LoadLibraryA等等,到这就可以利用函数地址调用指定函数了
mov edx,esi
保存模块基址mov esi,[esi+0x3c]
获取NT头偏移lea esi,[edx+esi]
得到NT头地址mov esi,[esi+0x78]
得到导出表RVAlea esi,[edx+esi]
得到导出表VAmov edi,[esi+0x1c]
得到EAT的RVA(地址表)lea edi,[edi+edx]
得到EAT的RAmov [ebp-0x4],edi
保存在栈中mov edi,[esi+0x20]
得到ENT的RVA(名称表)lea edi,[edx+edi]
得到ENT的VAmov [ebp-0x8],edi
保存在栈中mov edi,[esi+0x24]
得到EOT的RVA(序号表)lea edi,[edx+edi]
得到EOT的VAmov [ebp-0xc],edi
保存在栈中
LDR_DATA_TABLE_ENTRY结构体是一个用来存放模块信息的结构体,它的定义如下:
1 | typedef struct _LDR_DATA_TABLE_ENTRY { |
通过导出表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”
$$
在文件末尾增加N字节给自己的代码提供空间
向最后一个区段后添加一个和text属性相同的区段
在union misc属性下填入N
在VirtualAddress属性下填入M
M = VirtualAddress(上一个区段)+内存对齐值*(max(SizeOfRawData,VirtualSize)/内存对齐值)
在SizeOfRawData属性下填入N
在PointerToRawData属性下填入X
X = SizeOfRawData(上一个区段)+PointerToRawData(上一个区段)
- 在文件头的区段数量属性上加一
- 在可选文件头的SizeOfImage属性上加N、
- 文件末尾填入ShellCode
- 修改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。