这是「从0开始的逆向工程」系列的第一篇文章。本文将介绍 PE 文件的基础结构,以及如何正确使用 IDA Pro 进行逆向分析。
Table of contents
Open Table of contents
概述
在开始逆向一个 Windows 程序之前,你需要了解两件事:
- 你面对的是什么 - PE 文件格式
- 你用什么工具 - IDA Pro 的正确使用方法
PE 文件
PE(Portable Executable) 是 Windows 操作系统下的可执行文件格式,包括:
| 扩展名 | 类型 |
|---|---|
.exe | 可执行文件 |
.dll | 动态链接库 |
.sys | 内核驱动 |
.ocx | ActiveX 控件 |
PE 文件整体结构
+------------------+
| DOS Header | ← 64 字节,兼容 DOS 系统
+------------------+
| DOS Stub (可选) | ← "This program cannot be run in DOS mode"
+------------------+
| PE Signature | ← "PE\0\0" (4 字节)
+------------------+
| COFF Header | ← 机器类型、节数量等
+------------------+
| Optional Header | ← 入口点、镜像大小等
+------------------+
| Section Headers | ← 节表,描述各个节
+------------------+
| Sections | ← .text / .data / .rdata 等
+------------------+
更详细的数据结构可以直接参考微软文档:https://learn.microsoft.com/zh-cn/windows/win32/debug/pe-format
关键结构解析
DOS Header
typedef struct _IMAGE_DOS_HEADER {
WORD e_magic; // "MZ" (0x5A4D)
// ... 省略其他字段 ...
LONG e_lfanew; // PE 签名的偏移 (重要!)
} IMAGE_DOS_HEADER;
e_magic:必须是MZ,用于识别 PE 文件e_lfanew:指向 PE 签名的 RVA,是定位 PE 头的关键
PE Signature & COFF Header
// PE 签名
DWORD Signature; // "PE\0\0" (0x00004550)
// COFF 头
typedef struct _IMAGE_FILE_HEADER {
WORD Machine; // 0x14c = i386, 0x8664 = AMD64
WORD NumberOfSections; // 节数量
DWORD TimeDateStamp; // 编译时间戳
DWORD PointerToSymbolTable;
DWORD NumberOfSymbols;
WORD SizeOfOptionalHeader; // Optional Header 大小
WORD Characteristics; // 文件属性
} IMAGE_FILE_HEADER;
Optional Header
这是 PE 文件中最重要的头之一:
typedef struct _IMAGE_OPTIONAL_HEADER64 {
WORD Magic; // 0x10b = PE32, 0x20b = PE32+
// ... 省略 ...
DWORD AddressOfEntryPoint; // 入口点 RVA (重要!)
DWORD BaseOfCode; // 代码节起始 RVA
ULONGLONG ImageBase; // 首选加载地址
DWORD SectionAlignment; // 内存对齐
DWORD FileAlignment; // 文件对齐
// ... 省略 ...
DWORD NumberOfRvaAndSizes;
IMAGE_DATA_DIRECTORY DataDirectory[16]; // 数据目录表
} IMAGE_OPTIONAL_HEADER64;
📋 重要字段说明
| 字段 | 说明 | 逆向中的用途 |
|---|---|---|
AddressOfEntryPoint | 程序入口点 | 程序开始执行的位置 |
ImageBase | 首选加载地址 | 计算 VA = ImageBase + RVA |
DataDirectory[1] | 导入表 | 找程序调用了哪些 API |
DataDirectory[0] | 导出表 | DLL 导出了哪些函数 |
Section Header(节表)
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[8]; // 节名称,如 ".text"
DWORD VirtualSize; // 内存中的大小
DWORD VirtualAddress; // 内存中的 RVA
DWORD SizeOfRawData; // 文件中的大小
DWORD PointerToRawData; // 文件中的偏移
// ... 省略 ...
DWORD Characteristics; // 节属性
} IMAGE_SECTION_HEADER;
常见节及其用途:
| 节名 | 内容 | 属性 |
|---|---|---|
.text | 代码 | 可执行、可读 |
.data | 已初始化数据 | 可读、可写 |
.rdata | 只读数据 | 可读 |
.bss | 未初始化数据 | 可读、可写 |
.rsrc | 资源 | 可读 |
.reloc | 重定位表 | 可读 |
RVA 与 VA 的转换
逆向中最常遇到的地址概念:
VA (Virtual Address) = ImageBase + RVA (Relative Virtual Address)
FOA (File Offset Address) = 文件中的实际偏移
RVA → FOA 转换公式:
FOA = PointerToRawData + (RVA - VirtualAddress)
🔧 转换示例
假设:
- ImageBase = 0x140000000
- RVA = 0x1010
- .text 节:VirtualAddress = 0x1000, PointerToRawData = 0x400
计算:
- VA = 0x140000000 + 0x1010 = 0x140001010
- FOA = 0x400 + (0x1010 - 0x1000) = 0x410
导入表(IAT)
导入表记录了程序从其他 DLL 导入的函数:
程序调用 MessageBoxA
↓
IAT 中查找 MessageBoxA 地址
↓
从 user32.dll 加载实际地址
逆向时,查看导入表可以快速了解程序的功能:
ws2_32.dll→ 网络通信advapi32.dll→ 注册表、服务操作crypt32.dll→ 加密相关
IDA
IDA 界面介绍
C:\blog\src\assets\images\从0开始的逆向工程\1.png
常用快捷键
| 快捷键 | 功能 | 使用场景 |
|---|---|---|
F5 | 反编译 | 查看伪代码 |
N | 重命名 | 给函数/变量起有意义的名字 |
X | 交叉引用 | 查看谁引用了这个地址 |
G | 跳转地址 | 快速跳到指定地址 |
; | 添加注释 | 记录分析心得 |
/ | 添加结构体偏移注释 | 标记结构体字段 |
Space | 切换视图 | 文本/图形视图切换 |
Ctrl+S | 搜索字符串 | 查找字符串 |
Ctrl+F | 当前窗口搜索 | 局部搜索 |
Shift+F12 | 字符串窗口 | 查看所有字符串 |
Ctrl+E | 入口点列表 | 查看程序入口 |
IDA 工作流程
1. 切入点
方法一:从字符串入手
Shift+F12 → 搜索关键字符串 → 双击 → X 查看交叉引用
通过软件在关键时刻暴露出的字符串来定位到PE文件的关键函数。
方法二:从 API 入手
查看导入表 → 找感兴趣的 API → X 查看交叉引用
通过找到关键函数一定会调用的API,然后反向查看交叉引用找到关键函数。
方法三:从入口点入手
从 main/WinMain 开始 → 跟踪调用链
最常见,直接主函数硬上。
方法四:从导出表函数入手
某些情况,导出函数也是非常关键的。
直接看PE文件的导出函数
2. 分析函数
F5 反编译 → 阅读伪代码 → 重命名变量 → 添加注释
命名规范
良好的命名习惯能让分析事半功倍:
| 类型 | 命名格式 | 示例 |
|---|---|---|
| 函数 | sub_功能描述 | sub_init_socket |
| 变量 | v_类型_描述 | v_socket_fd |
| 结构体 | 直接用原名 | _IMAGE_DOS_HEADER |
| 全局变量 | g_描述 | g_config_ptr |