逆向工程核心原理-C13-PE文件格式-笔记

逆向工程核心原理-C13-PE文件格式-笔记

前几周粗浅看了一遍 由于是电子版 也没留下多大印象
最近CTF的re题看着太累了 做一个题挺耗时间的 想着是基础还是不够扎实
再者学逆向的目的也不全是CTF 掌握些理论还是很重要的
再来重新学一学 写篇笔记记录一下关键点 也启发自己思考

13.1 介绍

PE32 & PE+/PE32+ 别写PE64 :(闹笑话

13.2 PE文件格式

学习PE文件格式就是学习PE头中的结构体
十分重要!!!

13.2.1 基本结构

DOS头到节区头是PE头部分(4D 5A ... 50 45)
文件中使用偏移(offset) 内存中使用VA(虚拟地址)来表示位置 当文件加载到内存中时情况会改变 内容分为 .text .data .rsrc分别保存

13.2.2 VA&RVA

RVA(相对虚拟地址) + ImageBase(基准位置) = VA
PE头内部信息大多以RVA形式 因为PE(尤其是DLL)加载到进程虚拟内存时由于该位置已经加载其他PE(DLL) 要通过重定位加载到其他空白的位置
用RVA就只需要记录ImageBase就可以得到VA

13.3 PE头

13.3.1 DOS头

重点关注两个成员

  • e_magic: DOS签名 (4D5A => "MZ")
  • e_lfanew: 指示NT头的偏移 (不同文件值可变)

在书给的exe中 e_lfanew的值为 000000E0(小端序!)

13.3.2 DOS存根

40~4D为汇编指令(有兴趣把它写入文件IDA康康)
主要就是用来输出一下"This program ... DOS mode"就退出了 算是一种对于MS-DOS的兼容

13.3.3 NT头

NT头的IMAGE_NT_HEADERS

typedef struct _IMAGE_NT_HEADERS64 {
  DWORD                   Signature;    //签名
  IMAGE_FILE_HEADER       FileHeader;   //文件头
  IMAGE_OPTIONAL_HEADER64 OptionalHeader; //可选头
} IMAGE_NT_HEADERS64, *PIMAGE_NT_HEADERS64;

signature即为50450000("PE"00)
IMAGE_NT_HEADERS结构体总大小为F8 很大

13.3.4 NT头: 文件头

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;
  • Machine: 每个CPU都有唯一的machine码
  • NumberOfSections: 文件中存在的节区数量
  • SizeOfOptionalHeader: IMAGE_OPTIONAL_HEADER64结构体大小
  • Characteristics: 标识文件属性 是否可运行 是否为DLL等
  • TimeDateStamp: 该成员的值不影响运行 用来记录时间而已

13.3.5 NT头: 可选头

typedef struct _IMAGE_OPTIONAL_HEADER {
  WORD                 Magic;
  BYTE                 MajorLinkerVersion;
  BYTE                 MinorLinkerVersion;
  DWORD                SizeOfCode;
  DWORD                SizeOfInitializedData;
  DWORD                SizeOfUninitializedData;
  DWORD                AddressOfEntryPoint;
  DWORD                BaseOfCode;
  DWORD                BaseOfData;
  DWORD                ImageBase;
  DWORD                SectionAlignment;
  DWORD                FileAlignment;
  WORD                 MajorOperatingSystemVersion;
  WORD                 MinorOperatingSystemVersion;
  WORD                 MajorImageVersion;
  WORD                 MinorImageVersion;
  WORD                 MajorSubsystemVersion;
  WORD                 MinorSubsystemVersion;
  DWORD                Win32VersionValue;
  DWORD                SizeOfImage;
  DWORD                SizeOfHeaders;
  DWORD                CheckSum;
  WORD                 Subsystem;
  WORD                 DllCharacteristics;
  DWORD                SizeOfStackReserve;
  DWORD                SizeOfStackCommit;
  DWORD                SizeOfHeapReserve;
  DWORD                SizeOfHeapCommit;
  DWORD                LoaderFlags;
  DWORD                NumberOfRvaAndSizes;
  IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

重点关注以下成员

  • Magic: IMAGE_OPTIONAL_HEADER为32位-10B 64位-20B
  • AddressOfEntryPoint: 持有EP的RVA值!!! 指出程序最先执行代码起始地址!!!
  • ImageBase: 在PE文件被加载到内存时 指出有限装载的地址
    EXE DLL被装载到用户内存的0 ~ 7FFFFFFF SYS被装载到内核内存的80000000 ~ FFFFFFFF
    一般而言 用开发工具创建EXE后 ImageBase值为00400000 DLL文件的ImageBase值为10000000(也可为其他值)
    执行PE文件时 PE装载器先创建进程 再将文件载入内存 然后把EIP设置为ImageBase+AddressOfEntryPoint
  • SectionAlignment FileAlignment: PE文件的body部分被划分为节区
    FileAlignment指定了节区在磁盘文件中的最小单位
    SectionAlignment则指定了节区在内存中的最小单位
  • SizeOfImage: 加载PE文件到内存时 SizeOfImage指定了PE Image在虚存中所占空间大小
  • SizeOfHeaders: 指出整个PE头的大小
    第一节区所在位置与SizeOfHeaders距文件开始的offset相同
  • Subsystem: 区分驱动文件(.sys)和普通的可执行文件(.exe .dll)
  • NumberOfRvaAndSizes: 指定最后一个成员DataDirectory数组的个数(大小不一定为16)
  • DataDirectory:
    img
    重点关注0,1,9 EXPORT IMPORT TLS

13.3.6 节区头

PE文件被创建为多个节区结构(更加安全 eg.一定程度上防止缓冲区溢出)

  • code: 执行 可读取
  • data: 非执行 可读写
  • resource: 非执行 可读取

各个节区的属性被记录在节区头中

IMAGE_SECTION_HEADER

typedef struct _IMAGE_SECTION_HEADER {
  BYTE  Name[IMAGE_SIZEOF_SHORT_NAME];
  union {
    DWORD PhysicalAddress;
    DWORD VirtualSize;
  } Misc;
  DWORD VirtualAddress;
  DWORD SizeOfRawData;
  DWORD PointerToRawData;
  DWORD PointerToRelocations;
  DWORD PointerToLinenumbers;
  WORD  NumberOfRelocations;
  WORD  NumberOfLinenumbers;
  DWORD Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
  • VirtualSize: 内存中节区所占大小
  • VirtualAddress: 内存中节区起始位置(RVA)
  • SizeOfRawData: 磁盘文件中节区所占大小
  • PointerToRawData: 磁盘文件中节区起始位置
  • Characteristics: 节区属性(bit OR)

其中VirtualAddress和PointerToRawData不带有任何值 由可选头中的SectionAlignment和FileAlignment确定
VirtualSize和SizeOfRawData值一般不同!
最后看看Name字段 可以向其中放入任何值(甚至可以NULL填充)

img

13.4 RVA to RAW

img
即磁盘到内存的映射
方法:

  1. 查找RVA所在节区
  2. 公式计算偏移(RAW)
    offset = RAW - PointerToRawData = RVA - VitrualAddress

几个练习:
img

Q1. RVA = 5000时 FileOffset = ?
A1. RVA:第一节区 PTR=RawAddress = 400 VA = 1000 => RAW = 4400

Q2. RVA = 13314
A2. 第三节区 VA = B000 PTR = 8400 => RAW = 10714

Q3. RVA = ABA8
A3. 第二节区 VA=9000 PTR = 7C00 => RAW = 97A8
发现算出来偏移在第三节区 说明"无法定义RVA(ABA8)相对应的RAW值"
原因: 第二节区的 VirtualSize比RawSize大!

13.5 IAT

难点来了
IAT: 导入地址表(Import Address Table)
IAT保存的内容与Windows操作系统的核心进程 内存 DLL结构等有关
简言之 IAT是一种表格 用来记录程序正在使用哪些库中的哪些函数

13.5.1 DLL

相比早期只有库(library) DLL

  • 不需要把库包含在程序中 单独组成DLL文件 需要时调用即可
  • 内存映射技术使加载后的DLL代码 资源 能在多个进程中实现共享
  • 更新库时只需要替换相关DLL文件即可

加载DLL的两种方式

  • 显示链接: 程序使用DLL时加载 使用完毕后释放内存
  • 隐式链接: 程序一开始就一同加载DLL 程序终止时再释放占用的内存 IAT机制与这种链接方式有关

OllyDbg查看notepad.exe

img

这里可以看到
调用CreateFileW()时并非直接调用而是获取01001104处的地址来实现(所有API调用均如此)
地址01001104是.text节区的内存区域(IAT内存区域) 地址的值7645EA70即为加载到exe进程内存中CreateFileW()函数(位于kernel32.dll库)的地址
比较书和本地可以发现 01001104地址的值有差异 这也是为什么通过01001104而不是通过它指向的地址的值来调用
同时由于DLL的重定位 导致无法对实际地址硬编码 同时PE头中表示地址时使用的是RVA而不是VA

13.5.2 IMAGE_IMPORT_DECRIPTOR

IMAGE_IMPORT_DECRIPTOR结构体记录着PE文件要导入哪些库文件
IMAGE_IMPORT_DECRIPTOR结构体数组也被称为IMPORT Directory Table (导入目录)
导入多少个库就意味着有多少个IMAGE_IMPORT_DECRIPTOR结构体

img

  • OriginalFirstThunk(OFT): INT的地址(RVA)
  • Name: 库名称字符串的地址(RVA)
  • FirstThunk(FT): IAT的地址(RVA)

提示:

  • PE头中提到的table均指数组
  • INT和IAT是长整型 以NULL结束
  • INT中各元素的值为IMAGE_IMPORT_BY_NAME结构体指针
  • INT与IAT大小应相同

img

这张图中 INT和IAT各元素同时指向相同地址 但很多情况下他们是不一致的 要注意

简单了解下PE装载器把导入函数输入IAT的顺序:

  1. 读取IID的Name成员 获取库名称字符串("kernel32.dll")
  2. 装载相应库 -> LoadLibrary("kernel32.dll")
  3. 读取IID中OFT成员 获取INT地址
  4. 逐一读取INT数组中的值 获取相应IMAGE_IMPORT_BY_NAME地址(RVA)
  5. 使用IIBN的Hint(ordinal)或Name项 获得相应函数的起始地址
  6. 读取IID的FT(IAT)成员 获得IAT地址
  7. 将上面获取的函数地址输入相应的IAT数组值
  8. 重复4-7 直至INT结束(遇到NULL)

13.5.3 使用notepad.exe练习

首先要明确 IMAGE_IMPORT_DECRIPTOR结构体不在PE头而在PE体中 但查找其位置的信息在PE头中
前面提到过 NT头中可选头最后一个成员DataDirectory的 [1]记录的就是IMAGE_IMPORT_DECRIPTOR结构体的起始位置(RVA)
值为7604
RVA->RAW: 第一节区 RAW=0x7604-0x1000+0x400 = 6A04
WinHEX中找到 可以看到OFT(INT)的RVA值为7990 -> RAW: 6D90 Name RVA:7AAC -> RAW:6EAC FT(IAT) RVA:12C4-> RAW:6C4
跟踪Name可以看到 comdlg.dll字符串
跟踪INT 第一个值为7A7A(RVA) -> RAW:6E7A
INT是IMAGE_IMPORT_BY_NAME结构体指针数组 数组第一个元素指向函数的Ordinal值000F 函数的名称为PageSetupDlgW
跟踪IAT(RAW:6C4) 找到对应comdlg32.dll库

在OllyDbg中查看
该exe的ImageBase是01000000所以PageSetupDlgW函数的IAT地址为010012C4 其值为75B83F20 是API的准确起始地址
在OD中转到75B83F20对应的反汇编可以看到正是函数开始的地址

IAT真的很重要! 这些操作要自己多练才能够熟悉
尤其是注意到IAT的第一个元素被硬编码成76324906 但是OD中实际地址应该是75B83F20!! 所以要利用ImageBase+dll'sIAT(RVA)来找!

13.6 EAT

继续学习EAT
EAT使得不同的应用程序可以调用库文件中提供的函数 对应于DLL/SYS (IAT对应于exe)
只有通过EAT才能准确求得从相应库中导出函数的起始地址
对应 IMAGE_EXPORT_DIRECTORY PE文件中仅有一个IMAGE_EXPORT_DIRECTORY结构体

NT可选头的DataDirectory[0].VitrualAdddress的值即为IED结构体数组的起始地址(RVA)
再复习下: DataDirectory有两个DWORD成员 VitrualAddress和Size
用CFF和WinHex查看
ExportDirectory的RVA偏移为168 值为262C => RAW:1A2C
ED的size偏移为16C 值为6CFD

13.6.1 IMAGE_EXPORT_DIRECTORY

下面介绍IMAGE_EXPORT_DIRECTORY结构体中的重要成员

  • NumberOfFunctions: 实际Export函数的个数
  • NumberOfNames: Export函数中具有名字的函数个数
  • AddressOfFunctions: Export函数地址数组
  • AddressOfNames: 函数名称地址数组
  • AddressOfNameOrdinals: Ordinal地址数组

kernel32.dll:

img

从库中获取函数地址的API为GetProcAddress()函数 该API通过引用EAT来获取指定API的地址
GetProcAddress()操作原理:

  1. 利用AddressOfNames成员转到函数名称数组
  2. 函数名称数组中存储着字符串的地址 通过strcmp比较字符串 查找指定函数的名称(此时数组索引记为name_index)
  3. 利用AddressOfNameOrdinals成员 转到ordinal数组
  4. 在ordinal数组中通过name_index查找相应ordinal的值
  5. 利用AddressOfFunctions成员转到函数地址数组(EAT)
  6. 利用4中求到的ordinal值作为数组索引 在EAT中获得指定函数的起始地址

13.6.2 使用kernel32.dll练习

练习从kernel32.dll文件的EAT中查找AddAtomW函数
前面计算过IMAGE_EXPORT_DIRECTORY的RAW偏移为1A2C(CFF好像转到输出目录直接就能看)
WinHex中找到
AddressOfName: offset:1A4C 值:3538(RVA) -> RAW:2938
找打第三个地址: 4BB3(RVA) -> RAW:3FB3
找到查看 确实是AddAtomw的字符串!

同样可以查到AddAtomW对应的ordinal=2(对应数组第3个元素)
最后查找AddAtomW的实际函数地址
AddressOfFunctions: RAW:1A48 值 2654(RVA) -> RAW:1A54
这是数组起始位置 我们已知ordinal为2 所以找第三个 对应地址1A5C 得到的值:0326D9(RVA)
这里的RVA是相对kernel32.dll的ImageBase的
在NT可选头查看ImageBase的值: 7C800000 所以 AddAtomW函数的VA值为RVA+IB = 7C8326D9
在OD中查看验证(然而我用OD打开看不到这段的内存...)
用IDA打开一样 找到这个地址
img

可以看到确实就是AddAtomw函数的地址!!!


第一遍看的时候只是过了一遍概念 没有跟着书上一起算RVA->RAW在winhex中来找对应值 也没有开OD来看
实操一遍收获还是挺多的 PE文件格式是重中之重!一定要反复看~!

posted @ 2023-12-04 10:12  N0zoM1z0  阅读(125)  评论(0编辑  收藏  举报