Ox00 前言
Ox01 PE文件结构
1. 基本概念
-
PE文件
-
是一种文件格式,目前所学的 exe,dll,sys等都是PE文件
-
通过检查DOS头的**"MZ"(0x5A4D)和NT头的"PE"(0x00004550)**来判断一个文件是不是有效的PE文件
-
-
术语解释
-
RVA: 数据在虚拟内存中的偏移地址
-
VA: 数据在虚拟内存中的绝对地址
- VA = 实际加载基址 + RVA
-
FOA: 数据在文件中的偏移地址
- FOA = 需要转换的RVA - 所在的区段RVA + 所在区段的FOA.
-
2. PE 头信息
-
IMAGE_DOS_HEADER
-
e_magic: "MZ" - 0x5A4D
-
e_lfanew:指向NT头的偏移
-
-
IMAGE_NT_HEADERS
-
signature: "PE" - 0x00004550
-
IMAGE_FILE_HEADER
-
NumberOfSections:区段的数量
-
SizeOfOptionalHeader: 扩展头的大小,会被用于查找区段表
-
Characteristics:可以判断当前是不是DLL,是不是32位,重定位已分离(表示不需要重定位)
-
-
IMAGE_OPTIONAL_HEADER32
-
Magic:CPU架构:x32(0x10B)x64(0x20B)
-
AddressOfEntryPoint:程序的入口点(OEP) RVA
-
ImageBase:镜像的默认加载基址
-
SectionAlignment :内存对齐粒度,通常是0x1000,要求大于文件粒度
-
FileAlignment:文件对齐粒度,通常是0x200, 拉伸
-
SizeOfImage:镜像大小,即内存中的大小, 最后一个区段的RVA + 最后一个区段的大小
-
SizeOfHeaders:整个PE头部大小,通常是 0x400
-
DllCharacteristics :特征标识0x40 有随机基址
-
NumberOfRvaAndSizes:数据目录表个数 0x10
-
DataDirectory:数据目录表
-
-
IMAGE_SECTION_HEADER(重要)
-
Name[8]:区段名
-
virtualSize:内存中大小
-
VirtualAddress:内存位置RVA
-
SizeOfRawData:文件中大小
-
PointerToRawData:文件位置FOA
-
Characteristics:区段属性(可读可写可执行),是否有代码
-
-
3. 导入表
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics;
DWORD OriginalFirstThunk; // INT
} DUMMYUNIONNAME;
DWORD TimeDateStamp;
DWORD ForwarderChain;
DWORD Name; // 模块名称
DWORD FirstThunk; // IAT
} IMAGE_IMPORT_DESCRIPTOR;
导入表中保存的是一组 以全0为结尾 的 MAGE_IMPORT_DESCRIPTOR 结构。
-
INT:导入名称表,指向了一组以0结尾的 IMAGE_THUNK_DATA 结构,可以找到函数的ID或名称,但是有些时候其中会被填充为0,在文件和内存中它们保存的值是相同的。
-
当 IMAGE_THUNK_DATA 字段的最高位为1,表示这是一个序号导入的函数
- 序号的值为 0x0000???? & 0x0000FFFF = 0x0000????
-
最高位为0,表示这是一个名称导入的函数,
- 名称和序号保存在 0x???????? 的位置 IMAGE_IMPORT_BY_NAME
-
-
IAT:导入地址表,在文件中存放的是INT的数据,当程序被加载到内存时,会被填充为函数的真实地址。
-

4. 导出表
typedef struct _IMAGE_EXPORT_DIRECTORY {
// 省略了无意义的字段
DWORD Name; // 模块名RVA
DWORD Base; // 序号的起始数值
DWORD NumberOfFunctions; // 函数的数量
DWORD NumberOfNames; // 有名字的函数的数量
DWORD AddressOfFunctions; // 导出地址表的的RVA 【DWORD】
DWORD AddressOfNames; // 导出名称表的RVA【DWORD-RVA】
DWORD AddressOfNameOrdinals; // 导出序号表的RVA【WORD】
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
- 导出表只能有一个
- 名称表的数量和序号表的数量相同
- 导出函数的序号 = 基址(Base) + 索引(函数地址表下标)
5. 重定位表
typedef struct _IMAGE_BASE_RELOCATION {
DWORD VirtualAddress;
DWORD SizeOfBlock;
// WORD TypeOffset[1];
} IMAGE_BASE_RELOCATION;
- 重定位表由多组重定位块组成,每个重定位块保存的是一个分页(RVA)中需要重定位的所有数据。
- 重定位项的个数 = (SizeOfBlock– sizeof(IMAGE_BASE_RELOCATION)) / 2
- 每一个重定位项都是一个WORD类型的值,标识了当前数据的**类型**以及偏移(相对VirtualAddress)
- 需要重定位的数据所在的位置 = 实际加载基址 + VirtualAddress + 重定位数据的偏移,其中保存的是一个偏移
- 重定位公式:需要重定位的地址 - 默认加载基址 + 实际加载基址
6. 资源表
-
三层结构
-
一层:资源种类;二层:此种资源的个数,名称; 三层:资源数据
-
每一层都是 IMAGE_RESOURCE_DIRECTORY 开始,后面跟着一组 IMAGE_RESOURCE_DIRECTORY_ENTRY结构体,保存的是资源的类型和名称
- 结构体个数 = NumberOfNamedEntries + NumberOfIdentries
-
-
第一层:第二层:
- 资源的类型名称,当 NameIsString 为1时,标识这是一个自定义的名称,使用 NameOffset字段可以找到 IMAGE_RESOURCE_DIR_STRING_U 字段,其中保存了字符串名称。
-
第三层:
- 指向 IMAGE_RESOURCE_DATA_ENTRY,保存了数据的偏移和大小.
7. 其他表
- TLS表 IMAGE_TLS_DIRECTORY
- 使用 __declspec(thread) int nNum = 99; 定义TLS变量
- TLS 函数的原型如下,和DllMain相比,它们只有返回值类型不同
- Void NTAPI t_TlsCallBackA(PVOID DllHandle,DWORD Reason,PVOID Red);
- TLS 函数的调用顺序位于main函数前,通常用于进行反调试
Ox10 程序OEP特征
程序特征的识别
-
通过区段进行识别
- 参考逆向 05 课上笔记
-
通过连链接器版本识别
| VS | 连接器版本 |
| :---: | :---: |
| VC 4.2 | XXX |
| VC 5.0 | 5.0 |
| VC 6.0(98) | 6.0 |
| VS 2003 | 7.0 / 7.1 |
| VS 2005 | 8.0 |
| VS 2008 | 9.0 |
| VS 2010 | 10.0 |
| VS 2012 | 11.0 |
| VS 2013 | 12.0 |
| VS 2015 | 14.0 |
| VS 2017 | 14.2 |
| VS 2019 | 14.22 | -
通过入口特征进行识别
-
VC 6.0 & 易语言
- sub esp, 0x58; call dword ptr [GetVersion];
-
Delphi 程序
- 连续的5个call,紧跟着一串0,第一个call内调用了 GetModuleHandle
-
Borland C++ 程序
- 第一条语句是JMP ,有明显的 "fb:C++HOOK" 字符串
-
C\C++ 程序:
- debug版本为连续的两个call,release版本为 call jmp
-
Ox20 IAT修复的方法
- 脱壳的基本步骤
-
- 找OEP -> [还原IAT] -> dump文件 -> 修复IAT
- 通过在 OEP 位置查找 FF15 和 FF25 可以找到 IAT 设置硬件写入断点,重新运行程序
- 具体的步骤见课上笔记
-
- aspck: 抹除重定位
- FSG: IAT表中间有 FFFF
- ImpRec
Ox30 常见脱壳技巧
- ESP定律
- 原程序代码段内存执行断点
- 单步跟踪
