ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

CTF逆向入门实战:从easy-100题掌握二进制分析与调试技巧

CTF逆向入门实战:从easy-100题掌握二进制分析与调试技巧

1. 逆向入门题到底在考什么

1.1 从一道100分的题说起

CTF里的逆向工程(Reverse Engineering)方向,入门题往往不会一上来就让你手撕汇编或者对抗混淆,它更像是一道精心设计的“逻辑谜题”——出题人把一段判断逻辑编译成二进制,你需要把它还原成人类能读懂的形式,然后找到那个能让程序输出“正确”的输入。这道easy-100(LCTF)就是典型的入门级逆向题,分值100,难度定位在“刚接触逆向的新手能在30分钟到1小时内独立解出”的水平。

我第一次拿到这类题的时候,习惯性地先跑一遍程序看看它要什么。双击运行,命令行窗口弹出来,提示你输入一个字符串,输入错了就告诉你“wrong”,输入对了大概率会打印flag或者提示你答对了。这种交互模式在逆向入门题里非常常见,因为它把问题简化成了“找到正确的输入”这一个目标,你不需要去理解整个程序的业务逻辑,只需要定位到那个比较函数,把正确的字符串抠出来就行。

这道题适合谁?如果你刚学完C语言基础,知道什么是数组、字符串比较、循环,但还没怎么接触过汇编和调试器,那这道题就是为你准备的。它不会涉及复杂的加密算法,也不会用到反调试或者代码混淆,核心考点就两个:能不能用工具把二进制文件的反汇编代码看懂个大概,以及能不能把关键的判断逻辑翻译回高级语言。

1.2 逆向题的一般解题框架

在展开这道题的具体操作之前,我先把这个类型题目的通用解题框架捋一遍。不管题目怎么变,逆向题的解题流程基本逃不出这几个步骤:

  1. 信息收集:拿到文件先看它是什么类型、什么架构、有没有加壳、用了什么编译器。这一步决定了你后面用什么工具。
  2. 静态分析:用反汇编器或者反编译器把二进制翻译成汇编或伪代码,找到关键的函数和逻辑分支。
  3. 动态调试:如果静态分析看不明白,就用调试器跑起来,下断点、单步执行、观察寄存器和内存的变化。
  4. 逻辑还原:把关键判断逻辑用高级语言重新写一遍,理解它到底在比较什么。
  5. 求解验证:根据还原出来的逻辑计算出正确的输入,跑一遍程序确认结果。

这五步不是线性的,很多时候需要在静态和动态之间反复切换。比如静态分析看到某个函数调用很可疑,就动态跟进去看看它到底干了什么;动态调试发现某个内存地址存了关键字符串,再回到静态分析里去看这个地址是怎么被引用的。

提示:新手最容易犯的错误是一上来就埋头看汇编,从头看到尾。正确的做法是先找“入口”——字符串、系统调用、比较指令,从这些关键点往回推,而不是顺着程序流一条一条读。

2. 工具准备与环境搭建

2.1 静态分析工具的选择

逆向工程用到的工具链比较固定,入门阶段掌握两三个就够用了。我按使用频率排个序:

IDA Pro / IDA Free:这是逆向界的“ Photoshop”,功能极其强大。Free版本对入门题完全够用,支持x86、x64、ARM等多种架构的反汇编,还能生成伪代码(虽然Free版没有Hex-Rays反编译器,但反汇编视图已经足够)。我个人的习惯是拿到二进制先扔进IDA,让它自动分析,然后看函数列表和字符串窗口。

Ghidra:某机构开源的逆向工具,自带反编译器,能把汇编还原成比较像样的C代码。对于入门题来说,Ghidra的反编译结果有时候比IDA的伪代码还容易读。缺点是启动慢、界面操作不如IDA流畅,但胜在免费且功能完整。

objdump / readelf:Linux自带的命令行工具,适合快速查看文件头信息和简单的反汇编。如果你在Linux环境下做题,objdump -d和strings是最快的信息收集手段。

strings:这个命令虽然简单,但在逆向题里出镜率极高。很多入门题会把flag或者关键字符串直接硬编码在二进制里,strings一跑就能看到。当然这道题不会这么简单,但养成先跑strings的习惯没坏处。

2.2 动态调试工具的选择

GDB + pwndbg / peda:Linux下调试的不二之选。pwndbg插件把GDB的界面美化得非常好用,寄存器、栈、反汇编窗口一目了然。如果你还没装pwndbg,强烈建议花十分钟装一下,调试体验会提升一个档次。

x64dbg / x32dbg:Windows下的开源调试器,界面友好,适合调试Windows平台的PE文件。如果你在Windows上做这道题,x64dbg比OllyDbg更现代、更好用。

IDA的动态调试功能:IDA本身也支持远程调试,可以在反汇编视图里直接下断点、单步执行,静态和动态无缝切换。不过配置起来稍微麻烦一点,新手可以先从独立的调试器开始。

2.3 这道题的环境判断

拿到easy-100这个文件,第一步是判断它是什么平台、什么架构的。在Linux下用file命令:

file easy-100

输出会告诉你它是ELF还是PE,是32位还是64位,是动态链接还是静态链接。假设输出是ELF 32-bit LSB executable, Intel 80386,那你就知道这是一个32位的Linux可执行文件,需要用32位的调试器来跑。

如果是Windows的PE文件,file命令会显示PE32 executable (console) Intel 80386。这时候你就得在Windows环境下用x32dbg来调试,或者用IDA直接静态分析。

注意:32位和64位的调用约定不一样。32位x86的参数是通过栈传递的,64位x64的前六个参数是通过寄存器(rdi, rsi, rdx, rcx, r8, r9)传递的。看汇编的时候要分清楚,不然容易搞混参数的位置。

3. 静态分析:从字符串到关键函数

3.1 第一轮信息收集

我拿到文件后的第一件事是跑strings,看看有没有明显的提示信息:

strings easy-100 | head -50

通常你会看到一些编译器生成的符号、库函数名,以及程序自己定义的字符串。对于这道题,你大概率会看到类似"input your flag:"、"wrong"、"correct"这样的提示语。这些字符串就是你的“路标”——它们会出现在反汇编代码的某个位置,顺着这些引用就能找到主逻辑。

假设strings的输出里有"please input the flag:"和"wrong!",那你就知道程序会先打印提示,然后读取输入,最后进行比较。比较的结果决定了输出哪个字符串。

3.2 用IDA定位主函数

把文件拖进IDA,等它自动分析完成。在左侧的函数列表里找到main函数(如果没有符号表,就找entry或者start,然后跟进去)。对于入门题,main函数的逻辑通常很直白:

  1. 打印提示字符串
  2. 调用scanf或fgets读取输入
  3. 调用某个函数对输入进行处理
  4. 比较处理后的结果和某个目标值
  5. 根据比较结果打印成功或失败信息

在IDA的反汇编视图里,你会看到类似这样的结构:

push offset aPleaseInputTheFlag call printf ... call scanf ... call sub_401500 ; 这个函数很可疑 ... cmp eax, 1 jz short loc_401600

那个sub_401500就是关键函数。双击进去,看看它做了什么。

3.3 关键函数的逻辑分析

进入sub_401500之后,你会看到一段循环和比较的代码。入门题的逻辑通常长这样:

int check(char *input) { char target[] = {0x66, 0x6c, 0x61, 0x67, ...}; for (int i = 0; i < strlen(input); i++) { if ((input[i] ^ 0x10) != target[i]) { return 0; } } return 1; }

或者更简单一点,直接逐个字符比较:

if (input[0] != 'f' || input[1] != 'l' || input[2] != 'a' ...) { return 0; }

不管是哪种形式,你的目标都是一样的:把比较的条件提取出来,反推出正确的输入。

在IDA里,你可以按F5(如果有Hex-Rays插件)生成伪代码,这样读起来比汇编快得多。如果没有F5,就手动读汇编。关键指令是cmp和jz/jnz,它们决定了程序的分支走向。

实操心得:IDA的交叉引用功能(按X键)非常好用。当你看到一个可疑的字符串或者函数时,按X就能看到所有引用它的地方,快速定位到关键代码。

4. 动态调试:让程序自己告诉你答案

4.1 为什么静态分析不够

有些时候静态分析会遇到困难:比如程序用了复杂的加密算法,或者比较逻辑被拆散在多个函数里,光看汇编很难在脑子里还原出完整的逻辑。这时候动态调试就派上用场了。

动态调试的核心思路是:让程序跑起来,在关键位置下断点,观察寄存器和内存的值,然后反推出正确的输入。对于这道easy-100,最常用的手法是在比较指令处下断点,看看程序期望的值到底是什么。

4.2 用GDB调试的完整流程

假设你在Linux环境下,用GDB加载程序:

gdb ./easy-100

先跑一遍看看程序的行为:

(gdb) run Starting program: ./easy-100 please input the flag: test wrong! [Inferior 1 (process 12345) exited normally]

程序提示输入flag,输入错误就打印wrong。现在我们需要找到比较的位置。用info functions看看有哪些函数:

(gdb) info functions

如果符号被剥离了,函数名可能都是sub_xxx的形式。这时候可以结合静态分析的结果,找到关键函数的地址,然后在GDB里下断点:

(gdb) break *0x401500 (gdb) run

程序跑到断点处停下来,然后单步执行,观察寄存器的变化:

(gdb) ni (gdb) info registers

重点关注eax、ebx、esi、edi这些寄存器,以及栈上的值。当程序执行到cmp指令时,看看两边比较的值分别是什么。如果一边是你的输入,另一边是目标值,那目标值就是你要找的答案。

4.3 用x64dbg调试的流程

如果你在Windows下用x64dbg,流程类似:

  1. 打开文件,程序停在入口点
  2. 按F9运行,程序提示输入
  3. 输入一个测试字符串,比如"test"
  4. 程序打印wrong后,在反汇编窗口搜索字符串"wrong",找到引用它的代码
  5. 在比较指令处下断点(按F2)
  6. 重新运行,输入测试字符串,程序断在比较处
  7. 观察寄存器和栈,找到目标值

x64dbg的界面比GDB友好很多,寄存器窗口、栈窗口、反汇编窗口同时显示,不需要手动敲命令。对于新手来说,x64dbg的上手门槛更低。

4.4 动态调试的关键技巧

技巧一:在字符串引用处下断点。与其盲目地单步执行,不如直接找到"wrong"或"correct"字符串的引用位置,在那里下断点。程序跑到那里时,比较逻辑刚刚执行完,寄存器里还保留着比较的结果。

技巧二:修改标志位绕过检查。如果只是想快速验证某个输入是否正确,可以在比较指令后直接修改零标志位(ZF),强制程序走“正确”的分支。这在调试时很有用,但最终提交的flag还是得靠正确的输入。

技巧三:观察栈上的局部变量。很多程序会把目标字符串存在栈上,然后逐个字符比较。在栈窗口里可以看到这些值,直接读出来就行。

注意:动态调试时输入的长度要和目标flag的长度一致,否则程序可能在比较之前就退出了。如果不确定长度,可以先输入一个较长的字符串,看看程序的行为。

5. 逻辑还原与求解

5.1 把汇编翻译回高级语言

假设你在静态分析中看到了这样的汇编片段:

mov eax, [ebp+var_10] movzx ecx, byte ptr [eax] xor ecx, 0x10 cmp ecx, 0x66 jnz short wrong

这段代码的意思是:取输入字符串的一个字符,和0x10做异或运算,然后和0x66比较。如果相等就继续,不等就跳转到wrong。

翻译成C语言就是:

if ((input[i] ^ 0x10) != 0x66) { goto wrong; }

那么正确的字符就是0x66 ^ 0x10 = 0x76,也就是字符'v'。

5.2 批量提取目标值

如果程序是逐个字符比较的,你需要把所有的目标值都提取出来。在IDA里,你可以看到类似这样的代码:

cmp byte ptr [ebp+var_10], 0x66 jnz short wrong cmp byte ptr [ebp+var_10+1], 0x6c jnz short wrong cmp byte ptr [ebp+var_10+2], 0x61 jnz short wrong ...

这些0x66、0x6c、0x61就是目标字符的ASCII码。把它们按顺序收集起来,就是flag的各个字符。

如果比较逻辑用了异或、加减等运算,你需要反向计算。比如:

  • 如果是input[i] + 5 == 0x66,那input[i] = 0x66 - 5 = 0x61
  • 如果是input[i] ^ 0x10 == 0x66,那input[i] = 0x66 ^ 0x10 = 0x76
  • 如果是input[i] - 3 == 0x66,那input[i] = 0x66 + 3 = 0x69

5.3 用Python脚本自动化求解

手动一个一个算太慢了,写个Python脚本几行就搞定:

target = [0x66, 0x6c, 0x61, 0x67, 0x7b, 0x...] flag = ''.join(chr(t ^ 0x10) for t in target) print(flag)

如果比较逻辑更复杂,比如涉及多个运算步骤,就把还原出来的逻辑写成函数,然后反向求解。对于入门题来说,通常不会超过异或和加减法这个复杂度。

5.4 验证结果

算出flag之后,跑一遍程序确认:

./easy-100 please input the flag: flag{...} correct!

如果程序输出correct或者直接打印了flag,那就说明你的答案是对的。如果还是wrong,检查一下是不是漏掉了某个字符,或者运算方向搞反了。

实操心得:CTF逆向题里flag的格式通常是flag{...}或者ctf{...},你可以先用这个格式去猜前几个字符,验证你的逆向逻辑是否正确。比如如果你算出来第一个字符是'f',那大概率方向是对的。

6. 常见问题与排查技巧

6.1 程序跑不起来怎么办

问题:在Linux下运行ELF文件提示Permission denied。

解决:给文件加上可执行权限:

chmod +x easy-100

问题:运行32位程序提示No such file or directory,但文件明明存在。

解决:这是缺少32位运行库的典型症状。在基于Debian的系统上安装:

sudo apt-get install libc6:i386 libncurses5:i386 libstdc++6:i386

6.2 静态分析看不到关键逻辑

问题:IDA反汇编出来的代码全是db字节,没有正常的指令。

解决:这说明IDA没有正确识别代码区域。在可疑的地址上按C键,强制IDA把这段字节转换成代码。如果还是不行,可能是文件被加了壳,需要先脱壳。

问题:函数列表里找不到main函数。

解决:程序可能被剥离了符号表。从entry点开始跟,或者搜索__libc_start_main的调用,main函数的地址会作为参数传给它。

6.3 动态调试时断点不生效

问题:在GDB里下了断点,但程序直接跑完了,没有停下来。

解决:检查断点地址是否正确。如果是PIE(位置无关可执行文件),每次运行的基址都不一样,需要用break *main或者break *0x401500这种相对地址。在GDB里可以用info files查看程序的加载基址。

问题:x64dbg里程序跑飞了,断点没触发。

解决:可能是程序用了反调试技术,检测到调试器就改变了执行流程。可以尝试在x64dbg的设置里开启反反调试选项,或者用插件隐藏调试器。

6.4 常见问题速查表

问题现象可能原因解决方法
程序提示输入但无法输入程序从文件或管道读取输入用echo "test" | ./easy-100方式运行
IDA反汇编结果全是数据代码区域未正确识别按C键强制转换,或调整加载基址
断点地址每次运行都变PIE可执行文件使用符号名下断点,或关闭ASLR
比较指令处的值看不懂数据被加密或编码跟进解密函数,或动态观察内存变化
算出的flag程序不认漏掉了某个变换步骤重新检查循环边界和运算顺序

6.5 独家避坑技巧

技巧一:先用ltrace和strace看程序调用了哪些库函数。这两个命令能快速告诉你程序用了strcmp、scanf、printf等函数,帮你缩小分析范围。

技巧二:在IDA里用Shift+F12打开字符串窗口,双击可疑字符串跳转到引用位置。这比手动搜索快得多。

技巧三:如果程序用了strcmp比较字符串,直接在strcmp处下断点,参数就是两个要比较的字符串。在GDB里可以用break strcmp,然后x/s $rdi和x/s $rsi(64位)或者x/s *(int*)($esp+4)和x/s *(int*)($esp+8)(32位)查看参数。

技巧四:注意大小端问题。x86架构是小端序,多字节数据在内存里的存储顺序和书写顺序是反的。如果你从内存里读到一个4字节的值,记得按小端序解析。

技巧五:flag的格式通常是固定的。如果题目没有特别说明,先假设flag是flag{...}格式,用这个去验证你的逆向结果。如果前几个字符对不上,说明你的逻辑有问题。

7. 从这道题延伸出去的学习路径

7.1 逆向工程的核心能力树

做完这道easy-100,你算是摸到了逆向工程的门槛。接下来要提升的话,需要按下面的能力树逐层往上爬:

第一层:工具熟练度。IDA、GDB、x64dbg这三个工具要练到“肌肉记忆”的程度。知道什么场景用什么工具,快捷键不用想就能按出来。

第二层:汇编阅读能力。x86汇编的常见指令(mov, push, pop, call, jmp, cmp, test, lea)要能一眼看懂。知道函数调用时栈帧是怎么建立的,参数是怎么传递的。

第三层:高级语言还原能力。能把汇编片段翻译成C代码,理解循环、条件分支、数组访问、结构体在汇编层面是什么样子。

第四层:对抗技术。加壳、混淆、反调试、虚拟机保护,这些是进阶逆向题里常见的障碍,需要专门学习对应的脱壳和绕过技术。

第五层:算法识别。能识别出常见的加密算法(AES、RC4、TEA、Base64等)和哈希算法(MD5、SHA系列),知道它们的特征常量。

7.2 推荐练习平台与题目类型

入门阶段建议从CTF平台的逆向入门题开始刷,每道题控制在1小时以内。刷题的顺序可以是:

  1. 纯字符串比较题:flag直接硬编码在二进制里,strings就能找到。
  2. 简单异或题:输入经过异或运算后和目标值比较,需要写脚本反推。
  3. 简单加减法题:输入经过加减运算后比较,逻辑同上。
  4. 多步变换题:输入经过多个函数的依次处理,需要跟踪整个数据流。
  5. 简单算法题:涉及Base64、RC4等常见算法的识别和逆向。

每道题做完之后,不要只满足于拿到flag,要回头总结:这道题的核心考点是什么?我用了哪些工具?有没有更快的解法?把解题过程写成笔记,积累自己的“逆向套路库”。

7.3 这道题在实际工作中的映射

你可能会问,CTF逆向题和实际工作有什么关系?关系其实很大。逆向工程在安全领域有广泛的应用:

  • 恶意软件分析:分析病毒、木马的行为,提取IOC(入侵指标)。
  • 漏洞挖掘:逆向闭源软件,寻找内存破坏漏洞。
  • 协议逆向:分析私有协议的通信格式,实现互操作性。
  • 软件保护:理解加壳和混淆技术,设计更安全的软件保护方案。
  • 固件分析:逆向IoT设备的固件,发现安全问题。

CTF逆向题虽然简化了很多,但核心技能是相通的:读懂二进制、理解程序逻辑、找到关键数据。这三项能力在实际工作中每天都会用到。

提示:如果你对恶意软件分析感兴趣,可以进一步学习沙箱技术、行为监控、内存取证等方向。如果对漏洞挖掘感兴趣,可以学习模糊测试、符号执行、污点分析等技术。

7.4 学习资源与进阶方向

入门之后,推荐的学习路径是:

  1. 系统学习x86/x64汇编:找一本经典的汇编教材,把常见指令和调用约定吃透。
  2. 深入IDA使用:学习IDAPython脚本编写,用脚本自动化分析流程。
  3. 学习常见加密算法:自己用C语言实现一遍AES、RC4、TEA,理解它们的汇编特征。
  4. 刷更多CTF题:从入门题逐步过渡到中等难度题,重点练习动态调试和算法识别。
  5. 参与实际项目:分析开源软件的二进制、参与漏洞赏金项目、写逆向分析报告。

这道easy-100只是一个起点,它帮你建立了“拿到二进制→分析→求解”的基本流程。后面遇到的题目会越来越复杂,但只要这个流程是清晰的,你就有能力一步步拆解下去。

我个人在做这类入门题的时候,最大的体会是:不要怕汇编。刚开始看汇编确实很痛苦,满屏的mov、push、call让人头晕。但入门题的汇编其实很有规律,看多了就会发现,大部分代码都是在做“取数据→运算→比较→跳转”这四件事。把这几件事的模式记住了,读汇编的速度会越来越快。另外,动态调试是新手最好的朋友,静态看不明白的地方,跑起来看一眼寄存器和内存,往往就豁然开朗了。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进