#|PE格式系列_0x02(PE头部信息(WinDbg查看))


文章目录

  • 1.notepad基本信息
  • 2.扩展命令!dh
  • 3.映像的头部信息
    • part 1.数据结构
    • part 2.Dos Header(配角)
    • part 3.NT Headers(`主角`)
      • part 3-1.File Header(配角)
      • part 3-2.Optional Header(`主角`)
        • DataDirectory(配角)
    • part 4.Section Headers
      • part 4-1.Section Headers
      • part 4-2.notepad的Section Headers
  • 4.扩展信息
    • 扩展1:区块对齐
    • 扩展2:自定义区块方法
    • 扩展3:区块合并
    • 扩展4:RVA 和 RAW转换
  • 5.参考

使用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 AdressFile version和符号是否加载正确
#|PE格式系列_0x02(PE头部信息(WinDbg查看))
文章图片

最重要信息就是加载基址:00f20000,这个值要记住,后面很多访问内存操作都是以这个基址为前提
2.扩展命令!dh 前面提都过,学习PE文件格式,主要是学习header的结构,那么header里面都有什么?有没有什么比较简介的命令可以一次性的查看头部信息呢?
有,简洁的命令;!dh (display header)
#|PE格式系列_0x02(PE头部信息(WinDbg查看))
文章图片

  • 使用!dh查看头部信息
    #|PE格式系列_0x02(PE头部信息(WinDbg查看))
    文章图片
  • dump节区的头部信息
    #|PE格式系列_0x02(PE头部信息(WinDbg查看))
    文章图片
3.映像的头部信息
简单命令介绍完了,下面介绍一个复杂的查看PE文件格式方法(WinDbg+PE专用工具),目的是更好的理解PE格式;研究anti debug、脱壳和写壳时,一定要熟悉下面的变量之间的对应关系
还记得上面查看的加载基址:00f20000吗?后面会看到
注意:下面标识了配角和主角是为了方便自己回顾PE文件时可以抓住重点
  • 配角:结构里面被关注的部分很少,里面主要是一些引出主角的偏移量等信息,主要起到桥梁作用
  • 主角:PE格式的核心,逆向时要重点学习的地方
下面是头部信息里需要关注信息的层次结构
#|PE格式系列_0x02(PE头部信息(WinDbg查看))
文章图片

part 1.数据结构 下面是PE头信息常用的数据结构,详细可以看WinNT.h中的定义
//WinNT.h中的定义 #define IMAGE_NT_SIGNATURE0x00004550// PE00//File header format typedef struct _IMAGE_FILE_HEADER { WORDMachine; WORDNumberOfSections; DWORDTimeDateStamp; DWORDPointerToSymbolTable; DWORDNumberOfSymbols; WORDSizeOfOptionalHeader; WORDCharacteristics; } IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER; #define IMAGE_SIZEOF_FILE_HEADER20//文件头:CPU拥有的唯一Machine码 #define IMAGE_FILE_MACHINE_I3860x014c// Intel 386. #define IMAGE_FILE_MACHINE_IA640x0200// Intel 64//可选头:可选头的类型 #define IMAGE_NT_OPTIONAL_HDR32_MAGIC0x10b #define IMAGE_NT_OPTIONAL_HDR64_MAGIC0x20b//Directory format typedef struct _IMAGE_DATA_DIRECTORY { DWORDVirtualAddress; DWORDSize; } IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY; #define IMAGE_NUMBEROF_DIRECTORY_ENTRIES16//Directory Entries #define IMAGE_DIRECTORY_ENTRY_EXPORT0// Export Directory #define IMAGE_DIRECTORY_ENTRY_IMPORT1// Import Directory #define IMAGE_DIRECTORY_ENTRY_BASERELOC5// Base Relocation Table #define IMAGE_DIRECTORY_ENTRY_IAT12// 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信息
#|PE格式系列_0x02(PE头部信息(WinDbg查看))
文章图片

说明:DOS首部中主要关注2个字段
  • e_magic:windows平台需要被设置为0x5a4d(是WinNT.h中的定义的宏IMAGE_DOS_SIGNATURE)
    可以用来简单标识一个文件是不是PE格式,当然结合其他信息会更准确
  • e_lfanew:PE文件头的相对偏移(RVA),位于从PE文件开始偏移的3Ch处;这个字段是引出NT头的关键
数据结构:
//WinNT.h中的定义 #define IMAGE_DOS_SIGNATURE0x5A4D// MZ typedef struct _IMAGE_DOS_HEADER {// DOS .EXE header WORDe_magic; // Magic number ... LONGe_lfanew; // File address of new exe header } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

问题:DOS首部关注的变量这么少,为什么还保留呢?
主要是为了对DOS文件的兼容,后面的NT头部分才是PE文件格式的主角;由于DOS首部里面有很多不关心的变量,这部分会被一些壳利用进行特殊操作的
使用PE相关程序,查看的Dos Header
#|PE格式系列_0x02(PE头部信息(WinDbg查看))
文章图片

重点关注一下e_lfanew 变量的值:0x100=256,定位NT Headers的关键偏移,这个后面还会反复用到
#|PE格式系列_0x02(PE头部信息(WinDbg查看))
文章图片

part 3.NT Headers(主角) 大小是0xF8个字节,NT Headers才是PE文件格式的重点; 对于逆向来说,Optional Header和DataDirectory里面指向的一张张表才是重点
  • WinDbg求结构体大小
下面有时会求一个结构体的大小,因此总结了WindDbg常用的命令
#|PE格式系列_0x02(PE头部信息(WinDbg查看))
文章图片

  • 示例
查看notepad.exe的NT headers的整体信息如下;就是一个结构体而已,包含文件头(偏移是0x004)和可选头(偏移是0x018);注意这里不是指针,是一个真实的结构
#|PE格式系列_0x02(PE头部信息(WinDbg查看))
文章图片

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
#|PE格式系列_0x02(PE头部信息(WinDbg查看))
文章图片

主要关注的信息说明如下,这些信息错误可能会导致程序不能运行
提示:后续章节中写一个壳的示例中会看到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_IMAGE0x0002//文件可执行 #define IMAGE_FILE_32BIT_MACHINE0x0100//32位机器

扩展:借助_IMAGE_DOS_HEADER的e_lfanew 和 _IMAGE_FILE_HEADER的SizeOfOptionalHeader,可以创建一个不同于常规的PE文件
  • 专用工具查看
Stud_PE查看结果
#|PE格式系列_0x02(PE头部信息(WinDbg查看))
文章图片

CFF的查看结果(截图是后补充的,TimeDateStamp对不上是正常的)
#|PE格式系列_0x02(PE头部信息(WinDbg查看))
文章图片

part 3-2.Optional Header(主角
别看名字是可选,但是妥妥的是主角;对于普通的exe、dll等文件,一定是存在的且这个结构才是PE文件的核心
NT headers中可以看到,可选头的偏移是0x18,下面是可选头的信息
#|PE格式系列_0x02(PE头部信息(WinDbg查看))
文章图片

关于可选头有2点要注意
  • 注意1:上面信息量明显增加了,为什么不删减呢?不删减是因为真的很重要,要不是篇幅限制,都想把整个可选头列出来了
  • 注意2:第一次看可能觉得这些信息没什么用,逆向软件时自然会反复看这里,因此不用死记,多看几次自然就会了
PE专用工具查看的可选头内存数据:
#|PE格式系列_0x02(PE头部信息(WinDbg查看))
文章图片

下面对在逆向中经常使用的3个成员进行说明
  • 1.AddressOfEntryPoint:程序执行入口RVA
    脱壳时梦寐以求的值,即源程序的OEP,壳程序的EP都用到这个变量
    执行PE文件时,PE装载器先创建进程,再将文件载入内存,最后将EIP寄存器的值设置为ImageBase + AddressOfEntryPoint
    #|PE格式系列_0x02(PE头部信息(WinDbg查看))
    文章图片

    二进制文件(.exe)和动态链接库(.dll)区别:
    • 对于dll:这个入口点在进程/线程的创建和销毁都会被调用
    • 对于exe:这个地址不是main、DllMain等函数,而是执行运行时的库代码(来调用main函数)
  • 2.ImgeBase:文件在内存中的优先默认载入地址
    exe基本就是按照这个地址加载(不支持ASLR时),dll有可能有重定向现象(就是想加载的基址的坑被别人用了,换一个地方加载的现象)
    #|PE格式系列_0x02(PE头部信息(WinDbg查看))
    文章图片

  • 3.DataDirectory:数据表索引,下面单独列出一个部分讨论
DataDirectory(配角) 里面的输入表,重定位表等知识是写壳和脱壳的必备知识,hook函数也会经常用这里,这个目录表就是一个索引
  • WinDbg查看的信息
这也是一个配角,类似于虚函数表一样,只是程序里各种表的相对地址的一个集合而已
#|PE格式系列_0x02(PE头部信息(WinDbg查看))
文章图片

  • PE专用工具查看的信息
输入表的起始地址(0x2647c)和大小(0x21c)与上面查看的相同
#|PE格式系列_0x02(PE头部信息(WinDbg查看))
文章图片

输入表的整体结构,可以看到一张输入表里面实际就是一个个dll的记录而已,每个dll都有一个记录(dll名称和相应的导出函数)
#|PE格式系列_0x02(PE头部信息(WinDbg查看))
文章图片

可以看到dll之间的记录都是以一个0数值的DWORD进行区分的
#|PE格式系列_0x02(PE头部信息(WinDbg查看))
文章图片

part 4.Section Headers PE文件将具有相似属性的数据统一保存在被称为节区的地方,然后将各个节区属性记录在节区表;节区表用IMAGE_SECTION_HEADER结构体表示的
part 4-1.Section Headers
这个部分也叫做Section Table,最有用的几个信息下面已经添加了注释
//WinNT.h中的定义 #define IMAGE_SIZEOF_SHORT_NAME8 typedef struct _IMAGE_SECTION_HEADER { BYTEName[IMAGE_SIZEOF_SHORT_NAME]; //区块名字(8字节) union {//内存中区块的尺寸 DWORDPhysicalAddress; DWORDVirtualSize; } Misc; DWORDVirtualAddress; //内存中偏移RVA DWORDSizeOfRawData; //磁盘中区块的尺寸 DWORDPointerToRawData; //磁盘中偏移RVA DWORDPointerToRelocations; DWORDPointerToLinenumbers; WORDNumberOfRelocations; WORDNumberOfLinenumbers; DWORDCharacteristics; } IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER; #define IMAGE_SIZEOF_SECTION_HEADER40

常用的一些区块信息,也是_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信息
    #|PE格式系列_0x02(PE头部信息(WinDbg查看))
    文章图片

  • 2.WinDbg查看.text的Section Headers
    #|PE格式系列_0x02(PE头部信息(WinDbg查看))
    文章图片

  • 3.专用PE工具进行查看
    可以看到notepad.exe有6个节区,且每个节区的主要信息都列举出来,分析壳时会重点分析这里
    里面最有用的信息就是内存中的起始地址偏移(VirtualOffset)和磁盘中的起始地址偏移(RawOffset),偏移是针对基址说的
    #|PE格式系列_0x02(PE头部信息(WinDbg查看))
    文章图片

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:区块合并
【#|PE格式系列_0x02(PE头部信息(WinDbg查看))】写壳时,通常外壳dll有用的部分只有代码和数据段,这几个段(.text.data.rdata)最终都要迁移到原程序中;为了方便代码和数据迁移,在外壳dll中将代码和数据合并
#pragma comment(linker, "/merge:.data=https://www.it610.com/article/.text") #pragma comment(linker, "/merge:.rdata=https://www.it610.com/article/.text") #pragma comment(linker, "/section:.text,RWE")

扩展4:RVA 和 RAW转换
PE文件被加载到内存时,磁盘文件的地址(RAW)和加载到内存后的实际地址(RVA)之间有什么关系?(注意这里说都是相对地址);一般使用如下方法计算2者关系:
  • 1.查找RVA所在的节区(因为RVA是以所在节区为基准的偏移)
  • 2.根据映射进内存前后,偏移不变的原理,有如下公式:RAW - PointerToRawData = https://www.it610.com/article/RVA - VirtualAddress
PE文件的头部信息就先介绍这么多吧…,如果想专门研究PE,可以看《Windows PE权威指南》(看过一部分,现在用来垫显示器了…)
5.参考
  • 1.《加密与解密》第11章 PE文件格式
  • 2.《逆向工程核心原理》第13章 PE文件格式

    推荐阅读