文章目录
使用CFF、Stud_PE等专用工具可以直接查看PE文件的格式,那还有必要学习PE的格式吗?前面学习目的就是答案
单纯的看PE格式介绍没什么意思,下面结合分析notepad软件来学习PE格式
工具:调试器是WinDbg、PE查看工具是Stud_PE和PEview(任何一款PE工具都可以)
提示:WinDbg是windows下调试的神器,如果有时间的话,建议学习一个使用
1.notepad基本信息
先使用WinDbg简单了解一下C:\Windows\SysWOW64\notepad.exe
的基本信息,主要看一下Base Adress
、File version
和符号是否加载正确
最重要信息就是加载基址:00f20000
,这个值要记住,后面很多访问内存操作都是以这个基址为前提
2.扩展命令!dh
前面提都过,学习PE文件格式,主要是学习header的结构,那么header里面都有什么?有没有什么比较简介的命令可以一次性的查看头部信息呢?
有,简洁的命令;!dh
(display header)
- 使用
!dh
查看头部信息
- dump节区的头部信息
3.映像的头部信息
简单命令介绍完了,下面介绍一个复杂的查看PE文件格式方法(WinDbg+PE专用工具),目的是更好的理解PE格式;研究anti debug、脱壳和写壳时,一定要熟悉下面的变量之间的对应关系
还记得上面查看的加载基址:00f20000
吗?后面会看到
注意:下面标识了配角和主角是为了方便自己回顾PE文件时可以抓住重点
- 配角:结构里面被关注的部分很少,里面主要是一些引出主角的偏移量等信息,主要起到桥梁作用
- 主角:PE格式的核心,逆向时要重点学习的地方
下面是头部信息里需要关注信息的层次结构
part 1.数据结构
下面是PE头信息常用的数据结构,详细可以看WinNT.h
中的定义
//WinNT.h中的定义
#define IMAGE_NT_SIGNATURE 0x00004550 // PE00
//File header format
typedef struct _IMAGE_FILE_HEADER {
WORD Machine;
WORD NumberOfSections;
DWORD TimeDateStamp;
DWORD PointerToSymbolTable;
DWORD NumberOfSymbols;
WORD SizeOfOptionalHeader;
WORD Characteristics;
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
#define IMAGE_SIZEOF_FILE_HEADER 20
//文件头:CPU拥有的唯一Machine码
#define IMAGE_FILE_MACHINE_I386 0x014c // Intel 386.
#define IMAGE_FILE_MACHINE_IA64 0x0200 // Intel 64
//可选头:可选头的类型
#define IMAGE_NT_OPTIONAL_HDR32_MAGIC 0x10b
#define IMAGE_NT_OPTIONAL_HDR64_MAGIC 0x20b
//Directory format
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
#define IMAGE_NUMBEROF_DIRECTORY_ENTRIES 16
//Directory Entries
#define IMAGE_DIRECTORY_ENTRY_EXPORT 0 // Export Directory
#define IMAGE_DIRECTORY_ENTRY_IMPORT 1 // Import Directory
#define IMAGE_DIRECTORY_ENTRY_BASERELOC 5 // Base Relocation Table
#define IMAGE_DIRECTORY_ENTRY_IAT 12 // Import Address Table
//加载基址
+0x01c ImageBase //文件优先载入的地址(dll有可能要重定位,exe不需要)
//C++开发的exe,此值默认是0x1000,dll是0x10000000(可修改)
//PE装载器:1.创建进程,2.将文件载入内存,3.把EIP的值设置成
part 2.Dos Header(配角)
大小是40个字节,WinDbg附加32位的notepad.exe,查看notepad的header信息
说明:DOS首部中主要关注2个字段
-
e_magic
:windows平台需要被设置为0x5a4d
(是WinNT.h中的定义的宏IMAGE_DOS_SIGNATURE)可以用来简单标识一个文件是不是PE格式,当然结合其他信息会更准确
-
e_lfanew
:PE文件头的相对偏移(RVA),位于从PE文件开始偏移的3Ch处;这个字段是引出NT头的关键
数据结构:
//WinNT.h中的定义
#define IMAGE_DOS_SIGNATURE 0x5A4D // MZ
typedef struct _IMAGE_DOS_HEADER { // DOS .EXE header
WORD e_magic; // Magic number
...
LONG e_lfanew; // File address of new exe header
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
问题:DOS首部关注的变量这么少,为什么还保留呢?
主要是为了对DOS文件的兼容,后面的NT头部分才是PE文件格式的主角;由于DOS首部里面有很多不关心的变量,这部分会被一些壳利用进行特殊操作的
使用PE相关程序,查看的Dos Header
重点关注一下e_lfanew
变量的值:0x100=256,定位NT Headers的关键偏移,这个后面还会反复用到
part 3.NT Headers(主角
)
大小是0xF8个字节,NT Headers才是PE文件格式的重点;对于逆向来说,Optional Header和DataDirectory里面指向的一张张表才是重点
- WinDbg求结构体大小
下面有时会求一个结构体的大小,因此总结了WindDbg常用的命令
- 示例
查看notepad.exe的NT headers的整体信息如下;就是一个结构体而已,包含文件头(偏移是0x004
)和可选头(偏移是0x018
);注意这里不是指针,是一个真实的结构
part 3-1.File Header(配角)
- WinDbg查看
NT头基址有如下公式:pe_header = ImageBase + dos_header->elfanew
notepad.exe示例基址计算:ImageBase(0x00f20000
) + dos_header->elfanew(0x100
) = 0x00f20100
;因此File Header的起始地址应该是0x00f20100 + 4
主要关注的信息说明如下,这些信息错误可能会导致程序不能运行
提示:后续章节中写一个壳的示例中会看到
LIEF
源码处理有问题,导致SizeOfOptionalHeader长度少8个bytes,致使程序不能运行的案例
变量 | 说明 |
---|---|
Machine | 运行平台,14Ch:Intel 386;0x0200:Intel 64 |
NumberOfSections | 区块(Section)的数目 |
SizeOfOptionalHeader | _IMAGE_OPTIONAL_HEADER 结构体的长度;32位:00E0h;64位:00F0h |
Characteristics | 文件属性;普通exe一般是010fh,dll是2102h |
Characteristics取值0x102是下面的组合:
#define IMAGE_FILE_EXECUTABLE_IMAGE 0x0002 //文件可执行
#define IMAGE_FILE_32BIT_MACHINE 0x0100 //32位机器
扩展:借助_IMAGE_DOS_HEADER的e_lfanew 和 _IMAGE_FILE_HEADER的SizeOfOptionalHeader,可以创建一个不同于常规的PE文件
- 专用工具查看
Stud_PE查看结果
CFF的查看结果(截图是后补充的,TimeDateStamp对不上是正常的)
part 3-2.Optional Header(主角
)
别看名字是可选,但是妥妥的是主角;对于普通的exe、dll等文件,一定是存在的且这个结构才是PE文件的核心
NT headers中可以看到,可选头的偏移是0x18
,下面是可选头的信息
关于可选头有2点要注意
- 注意1:上面信息量明显增加了,为什么不删减呢?不删减是因为真的很重要,要不是篇幅限制,都想把整个可选头列出来了
- 注意2:第一次看可能觉得这些信息没什么用,逆向软件时自然会反复看这里,因此不用死记,多看几次自然就会了
PE专用工具查看的可选头内存数据:
下面对在逆向中经常使用的3个成员进行说明
-
1.
AddressOfEntryPoint
:程序执行入口RVA脱壳时梦寐以求的值,即源程序的OEP,壳程序的EP都用到这个变量
执行PE文件时,PE装载器先创建进程,再将文件载入内存,最后将EIP寄存器的值设置为
ImageBase + AddressOfEntryPoint
二进制文件(.exe)和动态链接库(.dll)区别:
- 对于dll:这个入口点在进程/线程的创建和销毁都会被调用
- 对于exe:这个地址不是main、DllMain等函数,而是执行运行时的库代码(来调用main函数)
-
2.
ImgeBase
:文件在内存中的优先默认载入地址exe基本就是按照这个地址加载(不支持ASLR时),dll有可能有重定向现象(就是想加载的基址的坑被别人用了,换一个地方加载的现象)
-
3.
DataDirectory
:数据表索引,下面单独列出一个部分讨论
DataDirectory(配角)
里面的输入表,重定位表等知识是写壳和脱壳的必备知识,hook函数也会经常用这里,这个目录表就是一个索引
- WinDbg查看的信息
这也是一个配角,类似于虚函数表一样,只是程序里各种表的相对地址的一个集合而已
- PE专用工具查看的信息
输入表的起始地址(0x2647c
)和大小(0x21c
)与上面查看的相同
输入表的整体结构,可以看到一张输入表里面实际就是一个个dll的记录而已,每个dll都有一个记录(dll名称和相应的导出函数)
可以看到dll之间的记录都是以一个0数值的DWORD进行区分的
part 4.Section Headers
PE文件将具有相似属性的数据统一保存在被称为节区的地方,然后将各个节区属性记录在节区表;节区表用IMAGE_SECTION_HEADER
结构体表示的
part 4-1.Section Headers
这个部分也叫做Section Table,最有用的几个信息下面已经添加了注释
//WinNT.h中的定义
#define IMAGE_SIZEOF_SHORT_NAME 8
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; //区块名字(8字节)
union { //内存中区块的尺寸
DWORD PhysicalAddress;
DWORD VirtualSize;
} Misc;
DWORD VirtualAddress; //内存中偏移RVA
DWORD SizeOfRawData; //磁盘中区块的尺寸
DWORD PointerToRawData; //磁盘中偏移RVA
DWORD PointerToRelocations;
DWORD PointerToLinenumbers;
WORD NumberOfRelocations;
WORD NumberOfLinenumbers;
DWORD Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
#define IMAGE_SIZEOF_SECTION_HEADER 40
常用的一些区块信息,也是_IMAGE_SECTION_HEADER
中Name常用的取值
名称 | 说明(仅做参考,没有强制规定) |
---|---|
.text | 代码块,内容都是code;linker会将目标文件的.text链接成一个大的.text块 |
.data | 读/写数据区块;一般的全局变量,static变量存放在这里 |
.rdata | 只读数据区块 |
.idata | 输入表 (包含其他使用的dll的函数和数据信息),一般会合并到其他如.rdata区块 |
.rsrc | 资源,只读,不能别合并 |
.bss | 未初始化数据 |
.reloc | 基址重定位,一般dll需要 |
.didat | 延迟载入的输入数据,非Release才有可能有 |
注意:上面都是编译器产生的标准区块,不了解这些也不影响正常编程,但是学习逆向要了解这些
part 4-2.notepad的Section Headers
-
1.WinDbg查看notepad.exe的Section Headers信息
-
2.WinDbg查看
.text
的Section Headers
-
3.专用PE工具进行查看
可以看到notepad.exe有6个节区,且每个节区的主要信息都列举出来,分析壳时会重点分析这里
里面最有用的信息就是内存中的起始地址偏移(
VirtualOffset
)和磁盘中的起始地址偏移(RawOffset
),偏移是针对基址说的
4.扩展信息
扩展1:区块对齐
如果细心观察,就会发现,内存中的起始地址偏移(
VirtualOffset
)是0x1000的整数倍;磁盘中的起始地址偏移(RawOffset
)是0x200的整数倍,这是谁规定的呢?PE文件中
ntdll!_IMAGE_OPTIONAL_HEADER
里面的2个变量规定的
FileAlignment
:定义了磁盘区块的对齐值,在PE文件中,典型的对齐值是200h;PointerToRawData
必须是FileAlignment
的整数倍SectionAlignment
:定义了内存中区块的对齐值,文件映射到内存中,区块至少从一个页边界开始;32位:内存页是按照4K(1000h)排列;62位:内存页是按照8K(2000h)排列
扩展2:自定义区块方法
如果想要定义自己的区块,可以使用下面的方法;当然也可以使用第三方库
LIEF
(这里先不介绍,后续写壳时会介绍这个库)
//用#pragma进行声明,可以让编译器将输入放入自定义的区块
//下面处理后,编译器会将数据放在名称为my_test_data的区块,而不是默认的.data区块
#pragma data_seg("my_test_data")
扩展3:区块合并
写壳时,通常外壳dll有用的部分只有代码和数据段,这几个段(
.text
、.data
、.rdata
)最终都要迁移到原程序中;为了方便代码和数据迁移,在外壳dll中将代码和数据合并
#pragma comment(linker, "/merge:.data=.text")
#pragma comment(linker, "/merge:.rdata=.text")
#pragma comment(linker, "/section:.text,RWE")
扩展4:RVA 和 RAW转换
PE文件被加载到内存时,磁盘文件的地址(
RAW
)和加载到内存后的实际地址(RVA
)之间有什么关系?(注意这里说都是相对地址);一般使用如下方法计算2者关系:
- 1.查找RVA所在的节区(因为RVA是以所在节区为基准的偏移)
- 2.根据映射进内存前后,偏移不变的原理,有如下公式:
RAW - PointerToRawData = RVA - VirtualAddress
PE文件的头部信息就先介绍这么多吧…,如果想专门研究PE,可以看《Windows PE权威指南》(看过一部分,现在用来垫显示器了…)
5.参考
- 1.《加密与解密》第11章 PE文件格式
- 2.《逆向工程核心原理》第13章 PE文件格式