逆向工程核心原理01
根据《逆向工程核心原理》一书整理而成的笔记,但本博客使用 IDA Pro 而非 Ollydbg 进行逆向分析
相关的下载链接
关于逆向工程
逆向工程(Reverse Engineering,RE)一般指通过分析物体,机械设备或系统,了解其结构、功能、行为等,掌握其中原理并改善不足之处、添加新创意的一系列过程。
分析可执行文件时使用的方法分为静态分析法和动态分析法。
静态分析法
静态分析法是在不执行代码文件的情形下,对代码进行静态分析的一种方法,通过观察代码文件的外部特征,获取文件类型(EXE,DLI,DOC,ZIP)、大小、PE头信息、Improt/Export API、内部字符串、是否运行时解压缩、注册信息、调试信息、数字证书等多重信息。
此外使用反汇编工具查看内部代码、分析代码结构等也属于静态分析的范畴
动态分析法
动态分析法是在程序文件的执行过程中对代码进行动态分析的一种方法,它通过调试来分析代码流,获取内存状态等。
通过动态分析,可以在观察文件、注册表、网络等的同时分析软件程序的行为,动态分析中还常常使用调试器分析程序内部结构和动作原理
源代码、十六进制代码和汇编
掌握程序源代码和二进制代码之间的关系有助于理解代码的逆向分析过程
编写下面这一段代码
1 |
|
编译后使用 010 editor 打开可执行文件

计算机执行时其实就是执行的这一串十六进制/二进制代码,具体原理可以参考深入理解计算机系统。
相对于二进制代码,显然汇编语言更容易理解
而对于汇编代码,采用 IDAPro 打开可以得到汇编的产物

可以看到,汇编的结果更容易让人理解,IDA Pro 还提供了反汇编的伪C代码

当然,伪C代码并不总是准确的,尤其是对于复杂的程序,所以理解汇编代码仍然是逆向工程的核心技能之一。
打补丁和破解
虽然这是一个纯概念性的区别,但我认为有必要在此声明一下,毕竟逆向工程是一把双刃剑,学习逆向工程之前应当提升自己的道德水平,明确自己学习逆向工程的目的。
对应用程序或进程内存内容的修改称为打补丁,破解和它的含义类似,但是破解的意图是非法的、不道德的,故进行区分
逆向分析 Hello World 程序
之前我们编写了一个简单的 Hello World 程序,现在我们来对它进行逆向分析
这一段对入口点分析还涉及到了 Windows 程序的启动过程,了解 Windows 程序的启动过程有助于理解程序的执行流程,但是实际逆向分析中并不需要过多关注这些内容,对于如果理解不了汇编代码也不需要过于纠结,在后面的学习过程中会逐渐理解这些内容。
使用 IDA Pro 打开该程序,IDA Pro 会自动分析该程序并生成汇编代码,在 IDA View 窗口中可以看到完整的汇编代码,Hex View 窗口中可以看到十六进制代码

可以看这一张截图,最左边的 .text 表示这是代码段,.rdata 表示这是只读数据段,.data 表示这是数据段,.idata 表示这是导入数据段,这些可以参考深入理解计算机系统。
.text 冒号右边表示目前程序的地址,紧接着就是汇编代码
从汇编开头开始,使用 IDA Pro 一个接一个分析为伪代码,可以看到调用了以下这些函数
当然,函数内部也有 call 指令,这说明函数内部也调用了其他函数,但在此不做讨论
1 | void __fastcall _mingw_invalidParameterHandler(const wchar_t *a1, const wchar_t *a2, const wchar_t *a3); |
简单来说一下这几个函数
_mingw_invalidParameterHandler 是 MinGW 提供的一个无效参数处理函数,当 CRT 函数(如 printf_s, wcscpy_s 等安全版本)检测到无效参数(如 NULL 指针、缓冲区越界)时,会调用此 Handler
pre_c_init 和 pre_cpp_init 分别是 C 语言和 C++ 语言的初始化函数
pre_c_init 初始化 C 标准库组件,如信号处理 (signal)、浮点数处理单元 (FPU) 状态、以及 IO 缓冲区
pre_cpp_init 遍历 .ctors 段(Constructors),执行所有全局 C++ 对象的构造函数
WinMainCRTStartup 是 Windows 应用程序的入口点,这相当于是 IDA 分析的起点,Windows 内核创建进程后,将控制权交给此函数,用于获取命令行参数 (GetCommandLine)、解析环境变量,并最终调用用户的 main
atexit 用于注册程序退出时调用的函数,atexit 接受一个函数指针,将其加入 LIFO (后进先出) 队列,当 main 返回或调用 exit() 时,OS 会依次执行这些回调,这个是 C++ 特性,当调用全局对象的析构函数时,这样的析构函数会被注册到 atexit 中,这样就保证了全局对象在程序退出时被正确析构
_gcc_register_frame 和 _gcc_deregister_frame 用于注册和注销栈帧信息,主要用于异常处理,即使代码中没有使用 try-catch ,只要链接了标准库,这段代码就会存在以支持库函数抛出异常,但这一段是编译器自动生成的代码,和逆向分析无关,可以直接跳过
最后的 main 函数就是逻辑入口了。
寻找入口点
在 IDA 中寻找 main 函数可以很方便,如图:

直接点击 main 就可以找到了,虽然这加快了分析速度,但了解程序究竟是怎么执行的还是很有必要的。
程序的真正入口点是 WinMainCRTStartup 函数,Windows 内核创建进程后,将控制权交给此函数。
这是 WinMainCRTStartup 的汇编代码:
1 | .text:00000000004014B0 public WinMainCRTStartup |
可以看出这是 Windows x64 平台下 MinGW 编译生成的标准 GUI 程序入口桩,主要职责是“构建符合 Windows x64 ABI 的栈帧”并“初始化运行时环境状态”,随后将控制权移交给更核心的初始化函数 __tmainCRTStartup
sub rsp, 28h 是 x64 函数标准开头的强特征,用于为局部变量分配栈空间
mov rax, cs:_refptr_mingw_app_type和 mov dword ptr [rax], 1 获取全局变量 __mingw_app_type 的地址,并将其赋值为 1。
MinGW 运行时库通过这个变量来判断当前程序是 GUI 应用程序还是控制台应用程序
值为 1 表示 GUI 应用程序GUI App (对应 WinMainCRTStartup),值为 0 则表示控制台应用程序Console App (对应 mainCRTStartup)
call __security_init_cookie 调用安全 cookie 初始化函数,用于防范栈溢出攻击
call __tmainCRTStartup 调用 C 运行时初始化函数 __tmainCRTStartup,这是程序初始化的核心部分,负责解析命令行参数 argv、解析环境变量 envp、初始化 C++ 静态对象等,执行完该函数后,程序通常会直接退出,因为 __tmainCRTStartup 内部会调用 exit())
可以跳到 __tmainCRTStartup 函数继续分析,这一部分会有很多初始化代码,主要是一些运行时的初始化工作,所以直接拉到最底部:
1 | loc_401387: |
这里出现了 main,双击跳转即可
可以右键点击 Text View 查看当前地址为 0000000000401550
这个地址是这样来的:
0x400000 来源于 Windows PE 文件的默认建议装载基址,MinGW-w64 的链接器(ld)默认倾向于使用 0x00400000 作为基址,除非显式开启了高熵 ASLR 或指定了 --image-base
0x1550 是 main 函数在代码段中的偏移地址,取决于在 main 之前写了多少代码
前面的内容包括:
- PE 头大小: 文件头、节表(Section Headers)
- 启动代码: 刚才的
WinMainCRTStartup、pre_cpp_init、atexit等所有 CRT 库函数 - 链接顺序: 链接器把
.text段拼接起来时,CRT 的目标文件(object files)通常被放在最前面,而代码(包含main)被放在这些库函数的后面
也就是 基址 + 偏移 决定了 main 函数的实际地址
综上,我们找到了 main 函数,对于初学者而言,直接从左边寻找 main 函数即可,当你对汇编、PE 文件格式、Windows 程序启动过程等有了一定了解后,才能理解以上的内容。
进一步了解 IDA Pro
简单的操作流程
当使用 IDA Pro 打开文件后,软件是在 Graph View 模式

space 空格键可以切换到 Text View 模式

左侧界面是函数列表,可以看到所有的函数,按 ctrl + f 可以搜索函数
双击函数名即可跳转到该函数
按 F5 可以自动反编译出伪C代码

ctrl+F5 可以把伪C代码表示出来
按住 shift+F12 可以打开字符串窗口

所有字符串都在这里展示,在 Windows PE 结构中,看到的字符串分布在以下段:
.rdata/.rodata(Read-only Data):C/C++ 代码中的字符串字面量
比如:printf("Access Denied"); 中的 "Access Denied" 会被编译器编译进 .rdata 段,程序运行时通过指针引用
.data(Read-write Data):初始化的全局变量或静态变量
比如:char global_key[] = "Secret123"; 中的 "Secret123" 会被放在 .data 段,因为它是可修改的全局变量
.text(Code Section)
但是看不到以下的信息:
- 栈字符串:编译器(尤其是优化后)或开发者为了反逆向,将字符串打散成单字节移动指令。这些字符变成了代码指令的一部分(立即数),不会出现在 Strings 窗口中,因为它们在静态文件中是不连续的字节。
1 | // 源代码 |
- 加密/混淆字符串
程序对字符串进行了 XOR、Base64 或自定义加密
这会导致静态文件里面存的是乱码,程序运行时会在堆或栈上动态解密
字符编码问题
ASCII (C-Style): 默认显示
Unicode (Wide Char):
L"Hello"在内存中是H\0e\0l\0l\0o\0- 如果 IDA 默认设置没开启 Unicode 扫描,这些字符串可能会显示为碎片
H,e,l或直接忽略 - 在 Strings 窗口右键 ->
Setup-> 勾选Unicode/Pascal等类型
- 如果 IDA 默认设置没开启 Unicode 扫描,这些字符串可能会显示为碎片
基于 IDA Pro 7.x/8.x 版本,分类总结如下:
1. 核心导航与视图控制
| 快捷键 | 功能 | 逆向场景 |
|---|---|---|
| Space | 切换图形/文本模式 | 在 Graph View (流程图) 和 Text View (线性汇编) 间切换 |
| G | Go to Address | 跳转到指定地址或符号名 (如 main, 0x401550) |
| Esc | 后退 | 返回上一次查看的位置 (类似浏览器的“后退”) |
| Ctrl + Enter | 前进 | 返回“后退”前的位置 |
| x | Cross References (Xref) | 查看当前函数/变量被谁调用或引用了 |
| Ctrl + E | Entry Points | 查看程序入口点 (如 WinMainCRTStartup, TLS Callback) |
| Alt + T | 文本搜索 | 搜索汇编指令或字符串内容 |
| Alt + B | 二进制搜索 | 搜索十六进制字节序列 (特征码搜索) |
2. 数据类型与反汇编修正
当 IDA 分析错误或未识别出代码时使用:
| 快捷键 | 功能 | 逆向场景 |
|---|---|---|
| C | Make Code | 强制将当前字节解析为汇编指令 (修复红色的未定义数据) |
| D | Make Data | 切换数据类型:db (1字节) \(\to\) dw (2) \(\to\) dd (4) \(\to\) dq (8) |
| U | Undefine | 取消定义。将代码/数据变回原始字节 (用于修复错误的分析) |
| A | ASCII String | 将数据标记为字符串 (以 null 结尾) |
| P | Create Function | Procedure。将当前汇编段定义为一个函数 (使其在 F5 中可用) |
| H | Hex/Decimal | 切换立即数的显示格式 十六进制 \(\leftrightarrow\) 十进制 |
| Alt + M | Mark Position | 标记当前位置,稍后可用 Ctrl + M 快速跳转回来 |
3. 重命名与注释
逆向的本质是“将无意义的地址转化为有意义的符号”。
| 快捷键 | 功能 | 逆向场景 |
|---|---|---|
| N | Rename | 重命名函数、变量、标签 (如 sub_401000 \(\to\) check_password) |
| ; | Repeatable Comment | 添加注释。会在所有引用该处的地方显示 |
| : | Regular Comment | 添加注释。仅在当前位置显示 |
| Alt + K | Change Stack Pointer | 手动调整栈指针偏移 (当 IDA 报 sp-analysis failed 时用) |
4. Hex-Rays 伪代码插件
在 F5 生成的 C 伪代码界面中,快捷键逻辑略有不同:
| 快捷键 | 功能 | 逆向场景 |
|---|---|---|
| F5 | Decompile | 反编译当前函数 |
| Tab | 伪代码 \(\leftrightarrow\) 汇编 | 跳转到伪代码对应的汇编指令位置 (双向同步) |
| y | Set Type | 修改变量/函数类型 (如将 int a1 改为 MyStruct *a1) |
| / | Comment | 在伪代码行尾添加注释 |
| n | Rename | 重命名伪代码中的变量 (会自动映射回汇编视图) |
| **** | Hide Cast | 隐藏/显示强制类型转换 |
| = | Reset Pointer Type | 重置变量类型,让 IDA 重新分析 |
5. 结构体与数据结构
| 快捷键 | 功能 | 逆向场景 |
|---|---|---|
| Shift + F1 | Local Types | 查看/编辑 C 语言类型定义 (可导入 .h 文件) |
| Shift + F9 | Structures 窗口 | 管理结构体定义 |
| Ins | Create Structure | 新建结构体 |
| d | Create Member | 在结构体视图中定义成员 (结合 D/A 键) |
| Alt + Q | Struct Offset | 将汇编中的立即数 (如 mov eax, [rbx+8]) 映射为 Struct.Member |
运行并修改字符串
点击 —> Debugger —> Start Process 可以在 IDA Pro 中运行这个程序,现在尝试修改这个程序中的字符串
直接修改字符串缓冲区
在程序运行时,字符串被加载到内存中,可以直接修改内存中的字符串内容
由于在调试时,IDA Pro 读取的是源程序的映像,而在 IDA 中静态修改的是反汇编后的二进制文件,所以静态修改字符串无法影响调试时的字符串,当然,可以修改后导出保存

这里的 lea rdx Text 的意思是把字符串的地址加载到 rdx 寄存器中
也就是 Hello, World! 保存在 Text 这个标签所指向的地址中,右键 Text 标签点击 Jump to operand 跳转到字符串所在位置

如图操作,可以修改字符串
原始的值为:
1 | 48 65 6C 6C 6F 2C 20 57 6F 72 6C 64 21 00 00 00 |
它的意思是 Hello World! 对应的 ASCII 码,修改为:
1 | 48 65 6C 6C 6F 2C 20 52 65 76 65 72 73 65 21 00 |
代表的是 Hello, Reverse!

此时另存为一个新的可执行文件,执行就可以得到修改的结果

此时 F9 运行调试还是会显示原始的字符串,因为调试时读取的是源程序的映像,但 IDA 也可以做到修改调试时的内存内容
在这个地方设置一个断点

调试运行

这个就是 IDA 的动态调试界面
主界面是反汇编窗口(IDA View-RIP),RIP 是指令指针寄存器,显示当前执行到的位置
右上角是 通用寄存器窗口 (General registers),可以看到各个寄存器储存的地址和数值
左下角是十六进制窗口 (Hex View-1) ,可以看到内存中的十六进制数据
右下角是栈窗口 (Stack view) ,其中 RSP 指向栈顶,栈中会显示当前的函数调用栈、局部变量、返回地址等
右中有个小窗口,这个是 模块与线程 (Modules / Threads) 窗口,可以看到当前进程加载的模块和线程
关注反汇编窗口,按照刚刚修改静态字符串的思路,找到字符串所在位置并修改
F9 继续运行程序,可以看到修改成功了

这种修改方法有一个隐患:这个方法对新字符串的长度有限制,如果新字符串比原字符串长,可能会覆盖后面的数据,导致程序崩溃
在其他内存区域新建字符串并传递给消息函数
由于在原位置修改字符串容易覆盖到后面的数据,导致程序崩溃,所以我们可以在其他内存区域新建字符串,并传递给消息函数
首先需要了解这个程序在调用消息函数之前做了什么
1 | .text:0000000000401564 mov r9d, 0 ; uType |
可以看到,在使用 MessageBoxA 函数之前,程序把字符串的地址加载到了寄存器中,也就是 r8 和 rdx 中
也就是说,需要了解一下 lea 语句的意思
lea 是 Load Effective Address 的缩写,意思是加载有效地址,在 x64 架构中,lea 指令用于将内存地址加载到寄存器中,Text 是一个标签,代表字符串 Hello, World! 所在的内存地址
当 MessageBoxA 函数被调用时,它就可以直接使用 r8 和 rdx 中的地址来获取字符串内容
了解了这些知识,就可以尝试修改字符串了
还是先跳转到字符串所在位置,然后往下拉,会看到机器码为 00 00 … 的区域,这一部分是由于编译器为了对齐而填充的空白区域
选中一段空白区域,新建一个标签 Mytext ,我这里选取的地址是 0x4044F5,一样的方法修改字符串
修改为 Hello Reversing World!!!
1 | 48 65 6C 6C 6F 20 52 65 76 65 72 73 69 6E 67 20 57 6F 72 6C 64 21 21 21 |


然后按 Esc 返回到反汇编窗口,修改调用消息函数的地方

可以看到这就是要进行的操作
需要把源地址修改为想要的地址
这个指令 lea 在 x64 架构中使用的是 RIP 相对寻址,意思是储存的是目标地址和下一条指令的相对距离 \[ 偏移量 = 目标地址 - 下一条指令的起始地址 \] 当前指令 lea rdx , Text 地址为 0x401571
当前指令长度 7 个字节(48 8D 15 + 4字节偏移 = 7字节)
下一条指令地址 (RIP):0x401571 + 7 = 0x401578
偏移 0x404015 - 0x401578 = 0x2A9D,由于是小端序,所以每个字节需要反过来写,这样就和下面的 Hex View 显示的相同了
之前储存的 Mytext 的地址是 0x4044F5,相同方法可以计算出偏移是 0x2F7D,修改即可
右键需要修改的地方,点击 Edit... 将 48 8D 15 9D 2A 00 00 改为 48 8D 15 7D 2F 00 00
再右键应用修改,可以看到现在界面变为了

继续运行,成功修改文件

这里留一个悬念:
如果按照方法二将修改后的二进制文件保存,然后直接打开这个文件,会得到:

这里并不是未保存成功,可执行文件被加载到内存并以进程的形式运行时,文件并非原封不动地载入内存,而是要遵循一定规则进行,在这一过程中,通常进程的内存时存在的,但是相应的文件偏移(offset)并不存在,上面示例中,内存地址 0x4044F5 对应的文件偏移并不存在,所以修改后的文件会成这个样子
可以按住 shift+F7 打开段窗口,找到加入的字符串所在的 .rdata 段,它的开头是这样的
1 | .rdata:0000000000404000 ; Section 3. (virtual address 00004000) |
这个意思是说
- Virtual address: 00004000 (内存起始地址)
- Virtual size: 000004F0 (有效数据大小)
那么它的有效结束位置是:0x4044F0,而放置字符串的地址是 0x4044F5,已经超出了有效数据范围
如果需要让字符串生效,可以在程序中找足够大的有效位置或者直接修改 PE 头,这个后面再讨论
小结
这一节我们了解了逆向工程的基本概念,学习了如何使用 IDA Pro 进行逆向分析,并通过一个简单的 Hello World 程序实践了字符串的修改方法,当然现在看不懂也是正常的,逆向工程是一个循序渐进的过程,需要不断学习和实践,后续章节会逐步深入讲解更多的逆向技术和方法。
这一节主要是体验了一下简单的逆向分析流程,后续章节会逐步讲解涉及到的知识点,比如汇编语言、Windows 程序结构、调试技术等。
- Title: 逆向工程核心原理01
- Author: exdoubled
- Created at : 2026-01-13 10:00:00
- Updated at : 2026-01-18 19:15:48
- Link: https://github.com/exdoubled/exdoubled.github.io.git/reverse/reverse1/
- License: This work is licensed under CC BY-NC-SA 4.0.