逆向工程核心原理-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:
重点关注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填充)
13.4 RVA to RAW
即磁盘到内存的映射
方法:
- 查找RVA所在节区
- 公式计算偏移(RAW)
offset = RAW - PointerToRawData = RVA - VitrualAddress
几个练习:
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
这里可以看到
调用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结构体
- OriginalFirstThunk(OFT): INT的地址(RVA)
- Name: 库名称字符串的地址(RVA)
- FirstThunk(FT): IAT的地址(RVA)
提示:
- PE头中提到的table均指数组
- INT和IAT是长整型 以NULL结束
- INT中各元素的值为IMAGE_IMPORT_BY_NAME结构体指针
- INT与IAT大小应相同
这张图中 INT和IAT各元素同时指向相同地址 但很多情况下他们是不一致的 要注意
简单了解下PE装载器把导入函数输入IAT的顺序:
- 读取IID的Name成员 获取库名称字符串("kernel32.dll")
- 装载相应库 -> LoadLibrary("kernel32.dll")
- 读取IID中OFT成员 获取INT地址
- 逐一读取INT数组中的值 获取相应IMAGE_IMPORT_BY_NAME地址(RVA)
- 使用IIBN的Hint(ordinal)或Name项 获得相应函数的起始地址
- 读取IID的FT(IAT)成员 获得IAT地址
- 将上面获取的函数地址输入相应的IAT数组值
- 重复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:
从库中获取函数地址的API为GetProcAddress()函数 该API通过引用EAT来获取指定API的地址
GetProcAddress()操作原理:
- 利用AddressOfNames成员转到函数名称数组
- 函数名称数组中存储着字符串的地址 通过strcmp比较字符串 查找指定函数的名称(此时数组索引记为name_index)
- 利用AddressOfNameOrdinals成员 转到ordinal数组
- 在ordinal数组中通过name_index查找相应ordinal的值
- 利用AddressOfFunctions成员转到函数地址数组(EAT)
- 利用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打开一样 找到这个地址
可以看到确实就是AddAtomw函数的地址!!!
第一遍看的时候只是过了一遍概念 没有跟着书上一起算RVA->RAW在winhex中来找对应值 也没有开OD来看
实操一遍收获还是挺多的 PE文件格式是重中之重!一定要反复看~!