逆向工程核心原理01

逆向工程核心原理01

exdoubled Lv4

根据《逆向工程核心原理》一书整理而成的笔记,但本博客使用 IDA Pro 而非 Ollydbg 进行逆向分析

相关的下载链接

源码

例子

关于逆向工程

逆向工程(Reverse Engineering,RE)一般指通过分析物体,机械设备或系统,了解其结构、功能、行为等,掌握其中原理并改善不足之处、添加新创意的一系列过程。

分析可执行文件时使用的方法分为静态分析法和动态分析法。

静态分析法

静态分析法是在不执行代码文件的情形下,对代码进行静态分析的一种方法,通过观察代码文件的外部特征,获取文件类型(EXE,DLI,DOC,ZIP)、大小、PE头信息、Improt/Export API、内部字符串、是否运行时解压缩、注册信息、调试信息、数字证书等多重信息。

此外使用反汇编工具查看内部代码、分析代码结构等也属于静态分析的范畴

动态分析法

动态分析法是在程序文件的执行过程中对代码进行动态分析的一种方法,它通过调试来分析代码流,获取内存状态等。

通过动态分析,可以在观察文件、注册表、网络等的同时分析软件程序的行为,动态分析中还常常使用调试器分析程序内部结构和动作原理

源代码、十六进制代码和汇编

掌握程序源代码和二进制代码之间的关系有助于理解代码的逆向分析过程

编写下面这一段代码

1
2
3
4
5
6
7
#include <windows.h>
#include <tchar.h>

int _tmain(int argc, TCHAR *argv[]){
MessageBox(NULL, _T("Hello, World!"), _T("My First Windows App"), MB_OK);
return 0;
}

编译后使用 010 editor 打开可执行文件

931X478/1-1.png

计算机执行时其实就是执行的这一串十六进制/二进制代码,具体原理可以参考深入理解计算机系统。

相对于二进制代码,显然汇编语言更容易理解

而对于汇编代码,采用 IDAPro 打开可以得到汇编的产物

769X700/1-2.png

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

774X148/1-3.png

当然,伪C代码并不总是准确的,尤其是对于复杂的程序,所以理解汇编代码仍然是逆向工程的核心技能之一。

打补丁和破解

虽然这是一个纯概念性的区别,但我认为有必要在此声明一下,毕竟逆向工程是一把双刃剑,学习逆向工程之前应当提升自己的道德水平,明确自己学习逆向工程的目的。

对应用程序或进程内存内容的修改称为打补丁,破解和它的含义类似,但是破解的意图是非法的、不道德的,故进行区分

逆向分析 Hello World 程序

之前我们编写了一个简单的 Hello World 程序,现在我们来对它进行逆向分析

这一段对入口点分析还涉及到了 Windows 程序的启动过程,了解 Windows 程序的启动过程有助于理解程序的执行流程,但是实际逆向分析中并不需要过多关注这些内容,对于如果理解不了汇编代码也不需要过于纠结,在后面的学习过程中会逐渐理解这些内容。

使用 IDA Pro 打开该程序,IDA Pro 会自动分析该程序并生成汇编代码,在 IDA View 窗口中可以看到完整的汇编代码,Hex View 窗口中可以看到十六进制代码

1152X735/1-4.png

可以看这一张截图,最左边的 .text 表示这是代码段,.rdata 表示这是只读数据段,.data 表示这是数据段,.idata 表示这是导入数据段,这些可以参考深入理解计算机系统。

.text 冒号右边表示目前程序的地址,紧接着就是汇编代码

从汇编开头开始,使用 IDA Pro 一个接一个分析为伪代码,可以看到调用了以下这些函数

当然,函数内部也有 call 指令,这说明函数内部也调用了其他函数,但在此不做讨论

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void __fastcall _mingw_invalidParameterHandler(const wchar_t *a1, const wchar_t *a2, const wchar_t *a3);

__int64 pre_c_init();

__int64 pre_cpp_init();

__int64 WinMainCRTStartup();

int __cdecl atexit(void (__cdecl *a1)());

int _gcc_register_frame();

void _gcc_deregister_frame();

int __fastcall main(int argc, const char **argv, const char **envp);

简单来说一下这几个函数

_mingw_invalidParameterHandler 是 MinGW 提供的一个无效参数处理函数,当 CRT 函数(如 printf_s, wcscpy_s 等安全版本)检测到无效参数(如 NULL 指针、缓冲区越界)时,会调用此 Handler

pre_c_initpre_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 函数可以很方便,如图:

408X889/1-5.png

直接点击 main 就可以找到了,虽然这加快了分析速度,但了解程序究竟是怎么执行的还是很有必要的。

程序的真正入口点是 WinMainCRTStartup 函数,Windows 内核创建进程后,将控制权交给此函数。

这是 WinMainCRTStartup 的汇编代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
.text:00000000004014B0                 public WinMainCRTStartup
.text:00000000004014B0 WinMainCRTStartup proc near ; DATA XREF: .pdata:0000000000405030↓o
.text:00000000004014B0
.text:00000000004014B0 ; FUNCTION CHUNK AT .text:00000000004021E0 SIZE 000001E7 BYTES
.text:00000000004014B0
.text:00000000004014B0 ; __unwind { // __C_specific_handler
.text:00000000004014B0 sub rsp, 28h
.text:00000000004014B4
.text:00000000004014B4 _l_startw: ; DATA XREF: .xdata:0000000000406038↓o
.text:00000000004014B4 ; __try { // __except at _l_endw
.text:00000000004014B4 mov rax, cs:_refptr_mingw_app_type
.text:00000000004014BB mov dword ptr [rax], 1
.text:00000000004014C1 call __security_init_cookie
.text:00000000004014C6 call __tmainCRTStartup
.text:00000000004014CB nop
.text:00000000004014CB ; } // starts at 4014B4
.text:00000000004014CC
.text:00000000004014CC _l_endw: ; DATA XREF: .xdata:0000000000406038↓o
.text:00000000004014CC ; __except(_gnu_exception_handler) // owned by 4014B4
.text:00000000004014CC nop
.text:00000000004014CD add rsp, 28h
.text:00000000004014D1 retn
.text:00000000004014D1 ; } // starts at 4014B0
.text:00000000004014D1 WinMainCRTStartup endp

可以看出这是 Windows x64 平台下 MinGW 编译生成的标准 GUI 程序入口桩,主要职责是“构建符合 Windows x64 ABI 的栈帧”并“初始化运行时环境状态”,随后将控制权移交给更核心的初始化函数 __tmainCRTStartup

sub rsp, 28h 是 x64 函数标准开头的强特征,用于为局部变量分配栈空间

mov rax, cs:_refptr_mingw_app_typemov 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
loc_401387:
mov qword ptr [rax], 0
mov cs:argv, rbp
call __main
mov rax, cs:_refptr___imp___initenv
mov rdx, cs:envp
mov ecx, cs:argc ; argc
mov rax, [rax]
mov [rax], rdx
mov r8, cs:envp ; envp
mov rdx, cs:argv ; argv
call main
mov ecx, cs:managedapp
mov cs:mainret, eax
test ecx, ecx
jz loc_40149E

这里出现了 main,双击跳转即可

可以右键点击 Text View 查看当前地址为 0000000000401550

这个地址是这样来的:

0x400000 来源于 Windows PE 文件的默认建议装载基址,MinGW-w64 的链接器(ld)默认倾向于使用 0x00400000 作为基址,除非显式开启了高熵 ASLR 或指定了 --image-base

0x1550main 函数在代码段中的偏移地址,取决于在 main 之前写了多少代码

前面的内容包括:

  • PE 头大小: 文件头、节表(Section Headers)
  • 启动代码: 刚才的 WinMainCRTStartuppre_cpp_initatexit 等所有 CRT 库函数
  • 链接顺序: 链接器把 .text 段拼接起来时,CRT 的目标文件(object files)通常被放在最前面,而代码(包含 main)被放在这些库函数的后面

也就是 基址 + 偏移 决定了 main 函数的实际地址

综上,我们找到了 main 函数,对于初学者而言,直接从左边寻找 main 函数即可,当你对汇编、PE 文件格式、Windows 程序启动过程等有了一定了解后,才能理解以上的内容。

进一步了解 IDA Pro

简单的操作流程

当使用 IDA Pro 打开文件后,软件是在 Graph View 模式

780X702/1-6.png

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

1113X594/1-7.png

左侧界面是函数列表,可以看到所有的函数,按 ctrl + f 可以搜索函数

双击函数名即可跳转到该函数

F5 可以自动反编译出伪C代码

758X186/1-8.png

ctrl+F5 可以把伪C代码表示出来

按住 shift+F12 可以打开字符串窗口

893X889/1-9.png

所有字符串都在这里展示,在 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
2
3
4
5
6
7
8
9
// 源代码
char key[] = "FLAG";

// 汇编表现 (在 .text 段)
mov [rsp+0], 'F' ; 0x46
mov [rsp+1], 'L' ; 0x4C
mov [rsp+2], 'A' ; 0x41
mov [rsp+3], 'G' ; 0x47
mov [rsp+4], 0
  • 加密/混淆字符串

程序对字符串进行了 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 Pro 7.x/8.x 版本,分类总结如下:

1. 核心导航与视图控制

快捷键功能逆向场景
Space切换图形/文本模式在 Graph View (流程图) 和 Text View (线性汇编) 间切换
GGo to Address跳转到指定地址或符号名 (如 main, 0x401550)
Esc后退返回上一次查看的位置 (类似浏览器的“后退”)
Ctrl + Enter前进返回“后退”前的位置
xCross References (Xref)查看当前函数/变量被谁调用或引用了
Ctrl + EEntry Points查看程序入口点 (如 WinMainCRTStartup, TLS Callback)
Alt + T文本搜索搜索汇编指令或字符串内容
Alt + B二进制搜索搜索十六进制字节序列 (特征码搜索)

2. 数据类型与反汇编修正

当 IDA 分析错误或未识别出代码时使用:

快捷键功能逆向场景
CMake Code强制将当前字节解析为汇编指令 (修复红色的未定义数据)
DMake Data切换数据类型:db (1字节) \(\to\) dw (2) \(\to\) dd (4) \(\to\) dq (8)
UUndefine取消定义。将代码/数据变回原始字节 (用于修复错误的分析)
AASCII String将数据标记为字符串 (以 null 结尾)
PCreate FunctionProcedure。将当前汇编段定义为一个函数 (使其在 F5 中可用)
HHex/Decimal切换立即数的显示格式 十六进制 \(\leftrightarrow\) 十进制
Alt + MMark Position标记当前位置,稍后可用 Ctrl + M 快速跳转回来

3. 重命名与注释

逆向的本质是“将无意义的地址转化为有意义的符号”。

快捷键功能逆向场景
NRename重命名函数、变量、标签 (如 sub_401000 \(\to\) check_password)
;Repeatable Comment添加注释。会在所有引用该处的地方显示
:Regular Comment添加注释。仅在当前位置显示
Alt + KChange Stack Pointer手动调整栈指针偏移 (当 IDA 报 sp-analysis failed 时用)

4. Hex-Rays 伪代码插件

在 F5 生成的 C 伪代码界面中,快捷键逻辑略有不同:

快捷键功能逆向场景
F5Decompile反编译当前函数
Tab伪代码 \(\leftrightarrow\) 汇编跳转到伪代码对应的汇编指令位置 (双向同步)
ySet Type修改变量/函数类型 (如将 int a1 改为 MyStruct *a1)
/Comment在伪代码行尾添加注释
nRename重命名伪代码中的变量 (会自动映射回汇编视图)
****Hide Cast隐藏/显示强制类型转换
=Reset Pointer Type重置变量类型,让 IDA 重新分析

5. 结构体与数据结构

快捷键功能逆向场景
Shift + F1Local Types查看/编辑 C 语言类型定义 (可导入 .h 文件)
Shift + F9Structures 窗口管理结构体定义
InsCreate Structure新建结构体
dCreate Member在结构体视图中定义成员 (结合 D/A 键)
Alt + QStruct Offset将汇编中的立即数 (如 mov eax, [rbx+8]) 映射为 Struct.Member

运行并修改字符串

点击 —> Debugger —> Start Process 可以在 IDA Pro 中运行这个程序,现在尝试修改这个程序中的字符串

直接修改字符串缓冲区

在程序运行时,字符串被加载到内存中,可以直接修改内存中的字符串内容

由于在调试时,IDA Pro 读取的是源程序的映像,而在 IDA 中静态修改的是反汇编后的二进制文件,所以静态修改字符串无法影响调试时的字符串,当然,可以修改后导出保存

906X254/1-10.png

这里的 lea rdx Text 的意思是把字符串的地址加载到 rdx 寄存器中

也就是 Hello, World! 保存在 Text 这个标签所指向的地址中,右键 Text 标签点击 Jump to operand 跳转到字符串所在位置

2053X1384/1-11.png

如图操作,可以修改字符串

原始的值为:

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!

670X230/1-12.png

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

752X846/1-13.png

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

在这个地方设置一个断点

1052X418/1-14.png

调试运行

image-20260118110244923

这个就是 IDA 的动态调试界面

主界面是反汇编窗口(IDA View-RIP),RIP 是指令指针寄存器,显示当前执行到的位置

右上角是 通用寄存器窗口 (General registers),可以看到各个寄存器储存的地址和数值

左下角是十六进制窗口 (Hex View-1) ,可以看到内存中的十六进制数据

右下角是栈窗口 (Stack view) ,其中 RSP 指向栈顶,栈中会显示当前的函数调用栈、局部变量、返回地址等

右中有个小窗口,这个是 模块与线程 (Modules / Threads) 窗口,可以看到当前进程加载的模块和线程

关注反汇编窗口,按照刚刚修改静态字符串的思路,找到字符串所在位置并修改

F9 继续运行程序,可以看到修改成功了

262X224/1-15.png

这种修改方法有一个隐患:这个方法对新字符串的长度有限制,如果新字符串比原字符串长,可能会覆盖后面的数据,导致程序崩溃

在其他内存区域新建字符串并传递给消息函数

由于在原位置修改字符串容易覆盖到后面的数据,导致程序崩溃,所以我们可以在其他内存区域新建字符串,并传递给消息函数

首先需要了解这个程序在调用消息函数之前做了什么

1
2
3
4
5
6
.text:0000000000401564                 mov     r9d, 0          ; uType
.text:000000000040156A lea r8, Caption ; "My First Windows App"
.text:0000000000401571 lea rdx, Text ; "Hello World!"
.text:0000000000401578 mov ecx, 0 ; hWnd
.text:000000000040157D mov rax, cs:__imp_MessageBoxA
.text:0000000000401584 call rax ; __imp_MessageBoxA

可以看到,在使用 MessageBoxA 函数之前,程序把字符串的地址加载到了寄存器中,也就是 r8rdx

也就是说,需要了解一下 lea 语句的意思

leaLoad Effective Address 的缩写,意思是加载有效地址,在 x64 架构中,lea 指令用于将内存地址加载到寄存器中,Text 是一个标签,代表字符串 Hello, World! 所在的内存地址

MessageBoxA 函数被调用时,它就可以直接使用 r8rdx 中的地址来获取字符串内容

了解了这些知识,就可以尝试修改字符串了

还是先跳转到字符串所在位置,然后往下拉,会看到机器码为 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
1384X689/1-16.png
480X882/1-17.png

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

2053X1391/1-18.png

可以看到这就是要进行的操作

需要把源地址修改为想要的地址

这个指令 leax64 架构中使用的是 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

再右键应用修改,可以看到现在界面变为了

2053X1391/1-19.png

继续运行,成功修改文件

269X227/1-20.png

这里留一个悬念:

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

266X206/1-21.png

这里并不是未保存成功,可执行文件被加载到内存并以进程的形式运行时,文件并非原封不动地载入内存,而是要遵循一定规则进行,在这一过程中,通常进程的内存时存在的,但是相应的文件偏移(offset)并不存在,上面示例中,内存地址 0x4044F5 对应的文件偏移并不存在,所以修改后的文件会成这个样子

可以按住 shift+F7 打开段窗口,找到加入的字符串所在的 .rdata 段,它的开头是这样的

1
2
3
4
5
6
.rdata:0000000000404000 ; Section 3. (virtual address 00004000)
.rdata:0000000000404000 ; Virtual size : 000004F0 ( 1264.)
.rdata:0000000000404000 ; Section size in file : 00000600 ( 1536.)
.rdata:0000000000404000 ; Offset to raw data for section: 00002400
.rdata:0000000000404000 ; Flags 40600040: Data Readable
.rdata:0000000000404000 ; Alignment : 32 bytes

这个意思是说

  • 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.
Comments