CTF 逆向这条路,很多人一开始就栽在“看不懂汇编”上。明明题目逻辑可能就十行代码,但一丢进 IDA 看到满屏的mov、push、jmp,整个人就懵了。我之前带过不少新人,发现一个共性:不是大家笨,而是上来就啃汇编语法,结果被一堆指令淹没了,完全不知道哪些该重点看。所以这个系列的第二章,我决定先把两块最核心的前置知识讲透——汇编基础怎么学才高效,以及程序从源码到二进制到底经历了什么。这两块搞明白,你后面看逆向题会顺畅非常多。
这篇东西适合谁?刚入门 CTF Reverse、想系统补基础的新人,以及已经刷过几道题但总觉得“能猜但说不清原理”的朋友。我不会堆砌指令表,而是按照逆向实战中最常见的场景来拆:你拿到一个二进制文件之后,首先会碰到的汇编模式有哪些,编译器是怎么把你的 C 代码“翻译”成这些指令的,以及main函数入口到底在哪。把这条链路打通,Reverse 才算是真正入门了。
1. 为什么逆向入门要死磕汇编与编译流程
先说个我自己的体会。早年我学逆向,走了不少弯路,最典型的就是抱着《汇编语言》教材啃了一个月,8086 的寻址方式背得滚瓜烂熟,结果打开一个 64 位 ELF 文件,还是不知道该从哪里看起。后来悟了:CTF 逆向里你需要的汇编知识,不是“能写汇编”,而是“能读懂编译器生成的汇编”。这是两码事。前者的考点是语法和指令集,后者考的是你能不能顺着代码的控制流还原出原始逻辑。
编译流程的重要性也一样。很多新手拿到一个 ELF 文件,第一反应是拖进 IDA,然后对着_start发懵。他们不知道_start是程序真正的入口,不知道__libc_start_main是在做什么,更不知道main函数为什么会被传进去三个参数。这些知识全部来自编译和链接的过程。你只有理解了一个 C 文件从预处理、编译、汇编到链接的完整旅程,才能在逆向的时候快速定位:哪些代码是编译器生成的样板代码,哪些是题目作者写的业务逻辑。
还有一点容易被忽略:不同编译器版本、不同优化级别,生成的汇编差异巨大。O0 的代码冗余重复,O2 的代码各种乱序重排,Clang 和 GCC 的风格也不一样。如果你不理解编译流程,你会觉得反汇编结果毫无规律,但实际上每一步都是编译器根据规则做的变换。知道规则,就能预判它的输出,逆向效率翻倍。
2. 汇编基础:逆向实战中真正高频的指令与模式
我见过太多人死磕指令全集,结果常用的没记住,冷门的背了一堆。实际上 CTF 逆向题里,高频指令翻来覆去就那么几十条。我把它们按实战用途拆成几块来讲。
2.1 先搞懂寄存器:x86/x64 的家族谱系
寄存器就像汇编世界的“变量”,但比变量更底层。64 位下常用的通用寄存器有rax、rbx、rcx、rdx、rsi、rdi、rbp、rsp,还有 8 个以r8到r15命名的寄存器。每个 64 位寄存器还有 32 位、16 位、8 位的“子寄存器”,比如rax的低 32 位是eax,低 16 位是ax,再拆成ah和al两个 8 位寄存器。这个设计是历史兼容的产物,但逆向的时候你必须分清,因为操作 32 位寄存器会把高 32 位清零,而操作 16 位或 8 位寄存器不会。
给你一个最常见的例子:
mov eax, 1 ; 等价于把 rax 整个清成 0,再设置成 1 mov ax, 1 ; 只改 rax 的低 16 位,高 48 位保持原样这个坑我见过不止一个人踩。在做符号扩展和数值还原的时候,如果没注意操作数宽度,你还原出来的变量类型就是错的,进而整个算法逻辑都会跑偏。所以看汇编第一步,先确认每条指令操作的是e还是r开头。
CTF 逆向里,寄存器的功能约定也很关键。在 x64 的 System V 调用约定下,函数参数依次放到rdi、rsi、rdx、rcx、r8、r9,多余的参数压栈传递,返回值放在rax。这意味着你在 IDA 里看到mov rdi, rsi这种指令时,脑子要立刻反应出来:这是在给某个函数传第二个参数。
2.2 数据传送与算术运算:最容易被低估的“基本功”
数据传送指令里,mov是绝对的主角,但有两个变体在逆向里经常出现,一个是lea,一个是movzx/movsx。
lea的全称是 Load Effective Address,它不访问内存,只计算地址。lea rax, [rbx + rcx*4 + 0x10]这种指令在优化后的代码里很常见,经常被用来做乘法或者加法运算。很多人第一次看到会以为它在读内存,其实它只是算了个地址存到rax。我遇到过一个题目,源码里写的是x * 5,编译后直接变成lea eax, [rax + rax*4],如果你不理解lea的这个用法,很容易卡住。
算术指令本身不复杂,add、sub、imul、idiv,注意除法比较特殊,idiv的被除数默认是rdx:rax拼起来的 128 位数,所以做除法之前编译器通常会先放一条cdq或cqo来扩展符号位。这个细节在你逆一个除法运算时非常关键,不然你会发现寄存器里的值怎么都对不上。
位运算指令and、or、xor、not、shl、shr更是家常便饭。CTF 逆向里大量的加解密算法都是位运算堆出来的。你看到shr和shl的时候要留个心眼,它们的移位数量不仅可以是立即数,还可以是cl寄存器的值。我曾经在一道题里找了半天找不到移位变量,最后发现是cl在控制,这种弯弯绕绕在编译器生成的代码里非常常见。
2.3 栈与函数调用:逆向里逃不掉的核心套路
栈这个东西,说白了就是一块先进后出的内存区域,由rsp指向栈顶。push指令先减小rsp,再把值写到栈顶;pop先把栈顶值读出来,再增大rsp。理解栈之后,才能理解函数调用的完整链路。
一个函数调用在汇编层面会经历这些:
; 调用方 sub rsp, 8 ; 栈对齐 mov rdi, [rbp-0x4] ; 传参 call func ; 压入返回地址,跳转 add rsp, 8 ; 恢复栈 ; 被调方开头(函数序言) push rbp ; 保存调用者的 rbp mov rbp, rsp ; 设置新的栈帧底 sub rsp, 0x20 ; 分配局部变量空间 ; 被调方结尾(函数尾声) leave ; 等价于 mov rsp, rbp; pop rbp ret ; 弹出返回地址,跳回去call指令自动把返回地址压栈,ret自动弹出来跳回去,这两个是硬件层面帮你完成的。理解了这个流程,你在逆向时看到一串push rbp/mov rbp, rsp开头的函数,基本上就能确定这是编译器生成的函数序言,可以直接跳过,直接进主体逻辑。
栈上还有个东西叫“栈帧”,就是一个函数独占的那段栈空间。rbp是栈帧底指针,rsp是栈帧顶指针。编译器用rbp + 偏移来访问参数和局部变量。这也是为什么你在 IDA 里经常看到[rbp-0x4]、[rbp+0x10]这种东西。搞清楚谁是谁,你就能把汇编和源码里的变量对上号。
2.4 跳转与比较:还原程序控制流的钥匙
程序不是顺序执行到底的,有if有循环,这些在汇编里全靠跳转指令实现。常见的无条件跳转是jmp,有条件跳转则基于标志寄存器。
比较指令cmp会做减法但不保存结果,只影响标志位。然后由条件跳转指令根据标志位决定是否跳转。这组配对在逆向里出现频率极高:
cmp eax, 5 jle short loc_401020 ; 如果 eax <= 5 就跳转我刚开始逆向的时候,经常忘记jg/jle/jge/jl区分的是有符号数,而ja/jb/jae/jbe区分的是无符号数。这个区别在漏洞利用里会致命,在逆向还原里同样重要——你看到一个数组索引的比较用了ja,那基本能确认这个索引变量是无符号类型。
另外还有test指令,它和cmp类似,只不过做的是按位与。最常见的是test eax, eax; je这种组合,等价于判断eax是否为 0。优化后的代码里,这种模式比cmp eax, 0出现得更多。
还有一类特殊跳转是jz/jnz,配合xor指令可以快速判断两个值是否相等。比如:
xor eax, eax test ecx, ecx jz short loc_401000在逆向里遇到xor不要只想到按位异或,它还经常被编译器用来给寄存器快速清零,因为xor eax, eax的机器码比mov eax, 0短一个字节,而且不会触发部分标志位依赖。
2.5 逆向中必须熟悉的几种典型汇编模式
看多了反汇编,你会发现编译器翻来覆去就那么几个套路。我先列三个最常见的。
套路一:字符串比较模式。CTF 里经典的“输入 flag,比较,输出正确/错误”逻辑,反汇编通常是这样的:先调用strlen或strcmp,然后用test判断返回值,再走jz/jnz分支。你看到test eax, eax后面跟jnz,一般就是“如果字符串不相等就跳到错误分支”。
套路二:循环累加模式。一个简单的for循环在汇编里通常长这样:
mov [rbp+var_4], 0 ; i = 0 loc_loop: cmp [rbp+var_4], 0xA ; i < 10 jge loc_end ; 循环体 add [rbp+var_4], 1 ; i++ jmp loc_loop loc_end:但注意,在开优化的情况下,编译器可能把循环倒过来写,或者用dec+jnz的方式实现,看起来就不是这么直观了。你需要在脑子里能还原出它是循环。
套路三:数组访问模式。arr[i]的访问在汇编里通常表现为基址加变址寻址,比如mov eax, [rbp + rax*4 + arr_offset]。一旦你看到*4、*8这种比例因子,基本就是数组或指针运算。
熟悉这些模式的意义在于,看到反汇编代码的第一眼,你不需要逐条翻译,而是能直接“按块识别”——这里是个循环,那里是个分支,那个区域是字符串处理。这种能力全靠多练,但前提是你知道要往哪些方向去识别。
3. 程序编译流程:从源码到二进制的一次旅行
接下来进入编译流程。这部分很容易被当成理论跳过,但相信我,它对逆向的帮助是实打实的。你知道了源码经历了什么,才能理解最终二进制里为什么会有那些“奇怪的代码”。
以 Linux 下最常见的 GCC 工具链为例,一个hello.c变成hello可执行文件,要经历四个阶段:预处理、编译、汇编、链接。
3.1 预处理阶段:文本替换与文件展开
预处理做的事情主要是处理#开头的指令。#include会把头文件的内容整个插入到当前文件里,#define做宏替换,#ifdef/#ifndef做条件编译。
这个阶段对逆向的影响在于:你需要知道“源码里的宏,在汇编里已经不存在了”。比如题目源码里定义了#define FLAG_LEN 32,但在反汇编里你不会看到FLAG_LEN,只会看到到处都是0x20这个数字。很多时候新手拿着一个0x20发愣,就是因为没想到它是个宏展开后的常量。
命令行里执行gcc -E hello.c -o hello.i就能看到预处理后的结果。你会震惊于一个简单的hello world展开后能有多大,光是一堆头文件就够你翻一会儿的。在 CTF 逆向里,我们很少直接看.i文件,但理解这个过程能帮你建立“源码到二进制之间有映射关系”的直觉。
3.2 编译阶段:高级语言到汇编语言的翻译
这是最核心的一步,也是逆向时需要“反向理解”的关键。编译器拿到预处理后的.i文件,会做词法分析、语法分析、语义分析,然后生成中间表示,最后优化并输出汇编代码。
这里有一个必须懂的概念:优化级别。GCC 和 Clang 都支持-O0、-O1、-O2、-O3、-Os这些选项。CTF 逆向题里最常见的编译选项是-O0,因为很多出题人不改默认配置。-O0的特点是每条源码语句对应多条汇编指令,变量都老老实实地待在栈上,函数调用不被内联,看反汇编代码非常“直观但啰嗦”。
到了-O2,编译器会做大量优化:变量放入寄存器、尾调用优化、循环展开、死代码消除、函数内联等等。反汇编结果会和源码长得“面目全非”。比如:
int add(int a, int b) { return a + b; } int main() { return add(3, 4); }-O2编译后,add函数可能直接被内联进main,最后main就一句话:mov eax, 7。如果你不知道内联优化,你会很困惑“add 函数去哪了”。
还有个细节:编译器可能把switch生成跳转表。当switch分支比较多时,GCC 会生成一张跳转表,运行时直接根据索引跳转。逆向时遇到这种结构,你需要还原原始的分支编号和跳转目标。
3.3 汇编阶段:助记符变成机器码
汇编器把汇编代码翻译成机器码,生成目标文件.o。这一步本质上是一个极其机械的查表过程,每条汇编指令对应一个唯一的助记符、操作数和机器码的映射。
这个阶段有一个 CTF 里非常重要的知识点——指令编码规则。比如mov eax, 1的机器码是B8 01 00 00 00,其中B8就是“把 32 位立即数载入 EAX”的 opcode。如果你会看机器码,你在做 shellcode 题目或者需要手动 patch 程序时就能直接改字节,而不是靠 IDA 的汇编器帮你重新生成。
还有nop指令,机器码是0x90。在逆向里,nop经常被用来填充对齐,或者被反混淆脚本用来覆盖无用的跳转和指令。我在处理带花指令的题目时,最常用的操作就是把花指令的字节全部改成0x90,然后重新分析,程序瞬间就清爽了。
3.4 链接阶段:众多目标文件合成一个可执行文件
链接是程序生成过程的最后一公里,也是很多 CTF 新手最陌生的阶段。它负责把多个.o文件和库文件合并成最终的可执行文件,解析符号引用,完成地址重定位。
这里有个 CTF 里绕不开的概念:动态链接 vs 静态链接。
动态链接的可执行文件体积小,程序运行时才去加载.so共享库。你拿到这种文件,在 IDA 里会看到大量对libc.so等外部函数的引用,比如printf、strlen、memcpy这些,它们的实现不在二进制里,需要调试时用 gdb 跟进去或者看动态链接库的代码。
静态链接则会把所有库代码直接打进二进制,文件体积通常非常大。CTF 里有时能看到静态链接的题目,一个ls都可能好几兆。这类题挑战在于你不能依赖 IDA 自动识别库函数,经常需要自己用FLAIR或FindCrypt之类的工具生成签名来识别。我记得第一次遇到纯静态链接的题,看着满屏的库函数代码头皮发麻,后来才发现可以用rizin的zignatures功能快速识别 libc 函数,效率提升明显。
链接还有一个重要概念是符号表。编译时加不加-s选项(strip),直接决定了二进制文件里有没有符号信息。有符号时,函数名、变量名都在,逆向难度很低;strip 掉之后,IDA 里全变成sub_401000这种无意义名字,你就得靠特征去识别函数功能了。CTF 里用gcc -s编译题目很常见,所以你必须练就在无符号情况下的函数识别能力,比如通过mov rdi, rsi这种传参模式推测函数参数个数和类型。
3.5 ELF 文件基本结构:逆向现场的“地图”
说了这么多,最终你拿到的还是一个 ELF 文件。入门阶段你不需要把 ELF 规范背下来,但几个关键结构必须知道。
ELF 文件最前面是文件头,以0x7F 45 4C 46(也就是\x7fELF)的魔数开头。readelf -h可以查看文件头内容,它告诉你是 32 位还是 64 位、是小端还是大端、入口点地址entry point在哪。
然后是节(Section)和段(Segment)的概念。链接视角看的是节,运行视角看的是段。.text节存放代码,.data和.rodata存放数据,.bss存放未初始化的全局变量。你在 IDA 里看到的字符串基本都在.rodata段。
这里有个实战小技巧:拿到一个二进制文件,第一步不要急着进 IDA,先在终端跑一下:
file ./challenge checksec ./challenge readelf -h ./challenge readelf -s ./challenge | head -50file告诉你是几位的、动态还是静态;checksec告诉你开了哪些保护机制;readelf可以快速看入口和符号。这样你进 IDA 之前心里就有底了。对 CTF 逆向来说,入口点概念尤其重要——ELF 的入口不是main,而是_start。_start会调用__libc_start_main,__libc_start_main才去调用main。所以你在 IDA 里按G跳到入口地址时,看到的不是题目逻辑,而是这段固定的启动代码。
4. 实操串联:从源码到汇编再到逆向的完整链路
讲了这么多理论,咱们把它串起来过一遍。用一个非常典型的 CTF reverse 题结构来演示:程序接收一个输入,和内部存储的 flag 比较,一致就输出 success。整个过程其实就是一个“编译-反编译-还原”的链路。
先写一段简化的 C 源码:
#include <stdio.h> #include <string.h> int main(int argc, char *argv[]) { char input[64]; char flag[] = "flag{this_is_a_sample_flag}"; printf("Enter the flag: "); scanf("%63s", input); if (strcmp(input, flag) == 0) { puts("Correct!"); } else { puts("Wrong!"); } return 0; }用gcc -O0 -o demo demo.c编译,然后用objdump -d demo看关键的main反汇编(这里简化展示核心片段):
push rbp mov rbp,rsp sub rsp,0x60 mov DWORD PTR [rbp-0x54],edi mov QWORD PTR [rbp-0x60],rsi lea rax,[rbp-0x50] mov QWORD PTR [rbp-0x30],rax movabs rax,0x32676c665f67616c mov QWORD PTR [rbp-0x2a],rax ; ... 这里是 flag 字符串的存储看到[rbp-0x54]存的是edi,基本可以判断这就是argc。[rbp-0x60]是argv。然后再往下看核心比较逻辑:
lea rax,[rbp-0x50] mov rsi,rax lea rax,[rbp-0x29] mov rdi,rax call strcmp@plt test eax,eax jne .wrong这里strcmp(input, flag)被翻译成:第一个参数放到rdi,第二个参数放到rsi,然后call strcmp。注意这里的调用顺序——左操作数放到rdi(第一个参数),右操作数放到rsi(第二个参数)。然后test eax, eax+jne的组合意味着“如果返回值不为 0,跳到错误分支”。
看到movabs rax, 0x32676c665f67616c这种指令要留意,movabs是 64 位立即数加载指令,编译器用这种方式把较长的字符串常量直接嵌到指令里。这个值实际上是"flag{2"这几个字符的 ASCII 码拼出来的小端序数值。我在给新手讲的时候经常用这个例子说明:为什么字符串在二进制里是反着看的——因为 x86 是小端序,低位字节存低地址。你把这串十六进制按字节倒过来再转 ASCII,就能还原出原始字符串。
在实际解题时,更快的做法是直接用strings命令先看可打印字符串,或者用 IDA 的字符串窗口(Shift+F12)直接定位到 flag 明文。这个 demo 里的 flag 是明文存储的,属于最简单的签到题;更复杂的题目会先把 flag 加密存储,运行时解密再比较,甚至直接用自写的比较函数替代strcmp,这时候就得真正靠汇编级分析来还原算法了。
但不管题目怎么变,有一个思路是通用的:先定位字符串引用,再回溯到处理函数,然后分析加密逻辑。定位字符串可以用strings、Binutils的objdump -s、还有 IDA 的字符串列表。拿到引用位置后,跳到引用的代码段,从那里往上追函数边界和控制流,基本就能锁定核心逻辑区。
这个实操链路走下来,你会发现汇编基础和编译流程的知识全都用上了:你认识push rbp是函数序言,你知道strcmp的返回值怎么影响分支,你知道movabs是在加载常量字符串,你知道程序入口_start那一堆代码不用管。这些“成熟逆向手”看起来理所当然的能力,其实就是把前面两个主题烂熟于心之后的自然结果。
5. 常见问题与排查技巧实录
整理几个我在实战和带新人的过程中经常遇到的问题,都是踩过的坑,提前排掉。
问题一:在 IDA 里按 F5 反编译,结果一堆__isoc99_scanf不认识怎么办?
这是新手最常见的困惑。__isoc99_scanf就是标准 C 库里的scanf,在较新的 glibc 版本里,编译器默认链接的是带__isoc99_前缀的版本。这不是什么特殊函数,就是 IO 库的正常实现。解决办法很简单:看到__isoc99_scanf就当scanf处理,看清它的格式化字符串就能确定参数个数。类似的还有__printf_chk,对应printf的检查版本。这些都是在 glibc 里为了安全加固加的包装,不影响你理解程序逻辑。
问题二:程序被 strip 过了,函数全叫sub_xxx,怎么定位main?
有符号的时候直接main就完事了,没符号就得靠特征找。方法有不少:先看入口_start,它通常会调用__libc_start_main,而__libc_start_main的第二个参数就是main的地址。在 IDA 里(用zu或者其他分解快捷键)注意__libc_start_main的第一个参数,就是main。另一种方法是搜索字符串,比如 “Enter the flag!” 在rodata段的交叉引用,跳过去往上翻函数开头,找到典型的push rbp; mov rbp, rsp序言,一般来说这就是main或者一个子函数。再不行可以用rizin的afl自动分析所有函数,再根据交叉引用关系判断。
问题三:一道题逻辑分析完了,但死活不输出正确 flag,怎么回事?
先说一个常见原因:程序可能有反调试检测。比如调用了ptrace(PTRACE_TRACEME, 0, 0, 0),检测到自己在调试器里就会走一段错误逻辑。解决办法是运行时用LD_PRELOAD提前注入一个假的ptrace函数,或者在 IDA 里直接把ptrace的调用 patch 掉。还有一个原因是验证逻辑分为多段,你只还原了第一段,后面还有二次校验。这种题目通常会有“先比长度、再比内容”“先过第一层、再过第二层”的结构,分析的时候要全程跟踪所有分支,不要看到第一个strcmp就以为到头了。
问题四:一遇到栈变量偏移就头大,[rbp-0x14]和[rbp-0x10]老是对不上?
这个问题的根源是对栈帧布局没有直观概念。建议你跟着 gdb 调试一次,在函数入口处break住,用info frame看栈帧地址,再用x/20gx $rbp-0x20看栈上的数据变化。我在这里说一个通用规律:优化开启时局部变量可能全在寄存器里,但-O0时局部变量一般按照声明顺序从rbp-4开始往下排。做题时不要死记偏移值,而是关注数据的流向——谁被写入、谁被读取、谁参与了运算。抓住数据流,偏移只是个地址代号。
问题五:C++ 逆向题看不懂vtable和std::string那堆结构?
C++ 逆向是另一个难度层,但入门阶段你只要知道几个点:std::string在小字符串优化下,长度小于等于 15 字节时数据直接存在对象内部,大于 15 字节时是一个堆指针;虚函数的调用在汇编里表现为从对象头部取虚表指针,再按偏移跳转。遇到lea rax, [rbp+var_30]传参,后面又调用了std::operator>>,那大概率就是cin >>读入了字符串。这个阶段先能认出来是 C++ 的输入输出即可,不需要完全弄懂 ABI 细节。
还有一个对比表格,方便快速排查:
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
反汇编里函数特别多且名字全是sub_ | 程序被 strip | 从入口回溯找main,或用 strings 交叉引用定位 |
代码里到处是call到plt段 | 动态链接,外部函数未展开 | 在 IDA 里按G跳到 plt 表,或运行时用 gdbfinish看返回值 |
| 输入的字符串在内存里顺序奇怪 | 小端序存储 | 按字节倒序查看仓库里的数据 |
| 某段代码怎么都跳不进去 | 可能被花指令干扰 | 把可疑字节改成nop,重新分析控制流 |
| 一次输入后程序直接崩溃 | 栈溢出或格式字符串漏洞 | 检查scanf/sprintf的可控参数,分析是否有长度限制 |
6. 工具链与后续进阶建议
这一节聊聊工具。汇编和编译流程是“心法”,但手上得有趁手的“兵器”。CTF Reverse 工具链比较成熟,我按使用场景排个优先级:
反汇编与反编译工具:IDA Pro 是行业标准,功能全、插件多,缺点是贵;在新手学习阶段,免费的Ghidra完全够用,它对编译流程还原能力很强,尤其是-O0的代码还原度非常高。还有一个轻量级的选择是rizin/cutter,启动快,适合快速看个大概。我个人的习惯是:先用 Ghidra 跑一遍反编译找整体逻辑,再用 IDA 看汇编细节。
调试工具:gdb是必须的,配合pwndbg或gef插件可以大大提升效率。在逆向中调试的主要目的不是看功能,而是验证你对代码逻辑的理解:你猜这里是个分支,断点打上跑一次看走哪边;你猜某个寄存器存的是长度,用info registers一看便知。
静态分析辅助:strings、objdump、readelf、checksec这些 binutils 全家桶要熟练。尤其是objdump -d -M intel,在快速看汇编时比 IDA 启动更快。还有find、grep命令配合-R选项可以递归搜索工程里的关键字符串。
工具使用的一个关键思路是:不要依赖单一工具。我有段时间只习惯用 IDA,后来遇到一个文件 IDA 怎么都抱错,换 Ghidra 一下就打开了。工具是手段,理解才是目的。
从进阶路线上说,汇编和编译流程只是 Reverse 的第一阶段。接下来你会慢慢接触到:加密算法识别(比如用 FindCrypt 插件批量扫TEA、AES、RC4的常量签名)、加壳与脱壳(UPX 是最简单的入门壳,理解壳的入口和原始 OEP 找回过程)、反调试对抗(ptrace、Trap Flag、int3断点检测)、以及虚拟机保护(VM 混淆,需要你写脚本去 trace 字节码解释器)。这些方向的底层,都离不开你能否流畅阅读汇编和理解编译产物。
我的建议是:把这一章的内容吃透之后,直接去刷pwnable.tw或者buuoj上的入门 reverse 题。先挑-O0编译的小题,感受“源码到汇编”的一一映射;然后再挑-O2编译的题,感受编译器做了哪些变换;最后挑一道 strip 过的题,练无符号情况下的函数识别。这个梯度走下来,你的汇编阅读和编译流程理解就算彻底过关了。
汇编和编译流程的知识,像是逆向这门外语里的“字母表”和“语法”。字母表背不熟,语法不懂,拿到再好的工具也就是个盲人摸象。但一旦这两块地基扎实了,后面的壳、VM、反调试、算法识别都是水到渠成的事。别急,顺着这条链路一步一个脚印走下去,Reverse 的大门就为你敞开了。