资讯动态

CTF逆向实战:用IDA快速定位主函数与加密逻辑的完整思路

发布时间:2026/10/19 6:30:12 来源:尧图企业网站定制
又到了 CTF Reverse 模块系列分享。前两篇我聊过环境搭建和二进制分析的基础工具这一期正式进入重头戏用 IDA 实战通关彻底搞明白两个最核心的能力——怎么找到主函数怎么顺着主函数定位到加密逻辑。这篇文章我会按照自己平时做题的真实顺序来写拿到一个题目先做什么打开 IDA 之后第一眼该看哪遇到带壳或 strip 过的文件怎么处理最后再拿一个典型的入门模拟题从入口点一路还原到 flag把中间每一步的思考过程完整摊开。不管你是刚开始接触逆向的萌新还是刷了几道题但总觉得思路散的选手这篇文章都适合你。我会把很多“老师傅嘴上说但不会写下来”的细节也放进去包括 F5 伪代码失效时怎么办、哪些新手习惯会在比赛里浪费你大量时间。1. 拿到题目别急着按F5先花两分钟确认文件类型和运行环境我见过太多人打开 IDA 直接按 F5然后被变量名绕晕绕了一小时才发现自己连文件是 64 位还是 32 位都没看清。说到底IDA 只是一个工具它替你完成的是“反汇编”和“伪代码生成”但它不会替你做“分析决策”。分析决策的第一步永远是搞清楚你手上到底是个什么东西。先用终端跑一下file命令这是在 Linux 做题时最快捷的探路方式$ file sample_rev sample_rev: ELF 64-bit LSB executable, x86-64, dynamically linked, not stripped这行输出信息量非常大。ELF 说明它是 Linux 下的标准可执行文件64-bit 决定了 IDA 加载时要选对处理器类型x86-64 指令集dynamically linked 说明它动态链接了系统库至少用到了 libcnot stripped 说明符号表还在函数名没有被剥掉。如果是 32 位你在 IDA 里看到的调用约定会变成 cdecl 或 stdcall参数压栈方式和 64 位完全不同。我曾经在模拟赛里遇到一个伪装成 64 位的文件实际是 32 位程序套了一个 ELF64 头一旦加载错整个反汇编结果都会错位分析半天全是噪音。所以“确认位数”不是浪费时间它决定了你后面的每一步是否正确。接下来确认是否加壳。加壳的本质是程序启动时先把壳代码跑起来把真正的代码解压再到内存里执行所以你静态反汇编时看到的全是壳的代码主逻辑根本不在文件里。怎么快速判断file命令之外我会用strings扫一遍$ strings sample_rev | head -50 /lib64/ld-linux-x86-64.so.2 __gmon_start__ Enter your flag: Congratulations! Try again!如果程序是加过壳的正常的字符串通常会隐藏得很深你很难直接看到这种明文的提示信息。UPX 加壳的 ELF 会在 strings 里直接出现UPX!或者UPX0、UPX1这样的区段名称看到基本可以确定就是 UPX 壳。UPX 壳的比赛题非常常见解法也很固定直接脱壳再分析。然后是运行试一下。这一步看起来和 IDA 无关但极其重要。它会告诉你这个程序的大概行为逻辑相当于给了你一张“预期地图”。$ ./sample_rev Enter your flag: test Try again!看到这样的输出我脑子里立刻就会有一个初步判断这是一个典型的 flag 校验程序读取输入经过某个逻辑处理然后和内置的目标数据比较。后面在 IDA 里我只需要找到Enter your flag:这个字符串的交叉引用就能一击命中核心函数。在 Windows 模拟题中我一般会用 Exeinfo PE 或 Detect It Easy 来识别编译器和壳。这两个工具会把 PE 文件的编译器特征、区段信息、壳类型直接列出来省去手动翻的麻烦。UNIX 系题目的话Linux 下的checksec也值得跑一下虽然它主要显示保护机制NX、PIE、Canary 等但对判断出题人意图很有帮助。开启 PIE 的分析难度会增加不少因为你看到的地址都是相对的在 IDA 里初始基址通常是 0。总结这一阶段的心得花两分钟看文件类型、壳、字符串、运行行为比在 IDA 里迷茫两小时高效得多。你后面每按一次 F5都会因为你提前知道了“程序大概是干嘛的”而多一分底气。2. IDA里找主函数的三条高频路径哪个快用哪个很多人以为找主函数就是双击main就完事了但 CTF 里哪有这么便宜的事。有些题的main只是门面真正的校验函数藏在 callback 里有些题把符号表剥了函数名全是sub_4012A0还有些题用 C 写入口点经过大量初始化代码main反而不好找。所以找主函数不能靠唯一路径要掌握几条路互相兜底。2.1 从入口点跟着启动代码走到main未 strip 且用标准编译的 ELF 程序IDA 加载后会帮你识别出main函数左侧的函数列表里直接能看到main、_start、__libc_csu_init这一串。但如果你是手工分析或者 IDA 没有自动识别出来就需要从入口点手动跟。ELF 真正的入口点是_start或start。它做的事情很简单设置栈初始化一些寄存器然后调用__libc_start_main。关键点在于__libc_start_main的第二个参数就是main函数的地址。在 IDA 反汇编窗口里找到_start往下翻几行你会看到类似这样的汇编lea rdi, main ; main call __libc_start_main这里的rdi指向的就是主函数。如果 IDA 已经把main标出来了直接双击过去就行。这个路径最“正统”但也是效率最低的因为大部分普通题用不上它。它真正的用武之地是程序被 strip 了、函数名全没了但你仍然能用入口点定位到主函数再从主函数一步步发散。2.2 用提示字符串交叉引用一步到位这招是我在入门阶段用得最多的也是我觉得新手最应该先掌握的。程序要提示用户输入几乎一定会用printf、puts、scanf这些东西字符串会留在.rodata段或.data段。IDA 的Strings窗口ShiftF12会把二进制里所有可打印字符串列出来你只要在里面找和输入提示相关的句子。在Strings窗口里双击Enter your flag:IDA 会跳到该字符串所在的数据段地址。此时按X键查看交叉引用即哪些代码引用了它。弹窗里会告诉你它被哪个函数、哪条指令引用了。点进去直接跳转到引用它的代码。如果这个字符串是被printf引用的那你基本可以确定那个函数就是程序的输入处理函数多半就是我们要分析的主函数或者核心校验函数。如果字符串是被puts引用的同理不过puts只负责输出它附近往往就跟着关键比较逻辑。我在模拟赛里遇到过一种情况成功提示字符串Congratulations!和失败提示字符串Try again!使用了同一个引用函数而输入提示字符串却被包在另一个函数里。这时优先去看Correct!或者Congratulations!这类结果提示的交叉引用因为它们通常离最终比较点更近。你看到的流程往往是比较完 → 跳转到两个输出分支 → 其中一个输出成功。顺藤摸瓜能更快摸到核心。2.3 函数列表排序与可疑大函数当你面对一个剥了符号的二进制函数名变成了sub_401000、sub_402100这种字符串交叉引用也断掉的时候就要靠“大函数优先”策略了。IDA 的Functions窗口Inserts → Functions Window或快捷键 ShiftF3里有一列值记录的是每个函数的字节大小。按这列排序最大的几个函数最有可能包含核心逻辑。为什么因为一个正常的校验逻辑尤其是涉及循环加密的它的指令量不会小。会疯狂展开一堆简单异或、查表、调子函数凡是你看到函数较长且内部有scanf、strlen、memcmp特征的地方都值得优先点开看看。我一般会把这类大函数都点一遍发现输入处理相关的直接按N重命名成input_func或check_flag之类有意义的名称。重命名不只是为了好看你分析得越久越会发现改写函数名是降低心智负担最有效的手段。后面再看到这个名字马上就能回忆起这一部分的职责。这三条路径我推荐的使用顺序是字符串交叉引用 函数列表排序 入口点跟踪。日常百分之八十的题第一条路直接命中被 strip 的题第二条路也基本能扛住万不得已才从入口点慢慢走。不过三条都得会因为出题人永远比你想象的鸡贼。3. 从主函数到加密逻辑识别运算特征和算法常数找到主函数只是第一步真正的战斗在“读懂主函数里到底干了什么”。尤其是定位加密逻辑你需要有很强的“模式感”。我自己的经验拿到一段伪代码不要从头到尾逐行读而是先扫结构找三类东西——输入来源、处理循环、最终比较。这三样一确定整个函数的骨架就出来了。3.1 校验类程序的典型三段式结构CTF 里百分之七八十的 Reverse 题目是“flag 校验器”它们无一例外地遵循三段式输入段调scanf、gets、fgets、read等函数读入用户输入存放在某个字符数组中处理段对输入做变换常见的是for循环配合异或、加减、移位、查表、字节置换比较段调用strcmp、memcmp比对两个数据或逐字节if判断我在伪代码里看到第一件事是用鼠标点一下输入字符串对应的位置看看它的来源。IDA 会把scanf这类函数的参数显示得很清楚比如scanf(%24s, buf)这个buf就是输入存储位置。从buf出发顺着代码往后扫凡是引用了buf的地方就是处理逻辑的入口。然后我会重点找比较点。最明显的是memcmp(a, b, n)a或b其中一个必然是我们输入处理后的结果另一个是有可能内嵌在程序里的目标数据。如果题目不用memcmp而是逐字节判断你会看到一堆if (byte_4038[ i ] ! v5) return 0;这样的模式本质依然是“处理后数据 vs 内置目标数据”。处理段就是加密逻辑所在。常见的有单字节异或、多字节循环异或、加固定 key、乘固定系数、旋转位、查表替换S盒以及更复杂的 TEA、RC4、AES 等分组算法。识别难度是递增的但识别路径一致看循环体内部的运算再看运算过程中是否出现了特征常数。3.2 常见算法常数速查表我在分析时会把一些很典型的算法特征记在脑子里这里整理给大家遇到直接对照算法/模式核心特征一眼识别点单字节XOR^操作配合固定key循环体中存在^和常量多字节循环XORi % key_len或key[i % n]数组索引里出现取模累加型混淆 0x1234567或* 0x55555555常量巨大常伴随取低字节TEA系列常数0x9E3779B9delta 一出现就是 TEARC4256字节数组初始化 两个下标 swap大数组 S盒交换循环AES256字节 S盒表 轮常量静态超长常数表BASE6464个可见字符组成的编码表ABCDEFGHIJKLMNOPQRSTUVWXYZ...MD5/SHA一长串常数值参与轮函数多个固定的十六进制大常量单字节 XOR 在入门题里出现最多它的特征是循环体内只有一次^ key且key是固定的。多字节循环 XOR 稍微复杂一点但本质就是用一个密钥串逐个异或输入。遇到后我会立刻在伪代码里把那个密钥串标出来然后写 Python 脚本还原。看到0x9E3779B9这个数字就要条件反射想到 TEA 家族。看到 256 字节表加交换循环大概率是 RC4。这些常数是你快速识破“出题人想考什么”的钥匙。我建议新手不要死背代码而是把常见算法都用 Python 或 C 各写一遍写的过程中自然就记住了特征常数。3.3 看着伪代码画数据流静态分析最容易犯的错是“一头扎进细节”。我会在拿到伪代码后不急着追究每个变量怎么算的而是先画一条数据流线输入 → 变换1 → 变换2 → 比较目标。变换1、变换2 分得越清后面写脚本就越顺。画数据流的时候重点关注几个方向循环变量是否参与运算有没有查表操作有没有跨多个数组的交换有没有看似无用但实际决定方向的逻辑出题人经常把加密拆成两三层来干扰你但最内层的运算必然是你能逆回去的小操作。有些题在伪代码里会出现*(_BYTE *)(a1 i)这种写法其实是对输入数组按字节读取。这种写法在 IDA 里特别常见你需要把它在脑子里翻译成buf[i]。还有的会显示成v5[i] ^ v6[i % 5]一眼就能看懂是循环异或。伪代码比汇编友好得多但也不完全是 C需要一点点翻译熟练度。4. 一次完整的实战复盘从入口点一路还原到flag接下来用一个典型的入门模拟题把前面所有方法串起来走一遍。为了方便说明我假定这是一个 Linux ELF 64 位程序未加壳未 strip文件名就叫mock_rev。考点是简单的异或加密加常数加法足够说明“找主函数”和“定位加密逻辑”的全过程。4.1 观察行为与收集线索先执行程序$ ./mock_rev Enter your flag: aaaa Try again!再用file看类型确认是 x86-64 ELFstrings里除了提示字符串没有出现明显的算法表特征。运行行为告诉我们输入不固定长度但可能会校验长度。我猜想出题人至少做了一次长度比较。4.2 用字符串交叉引用定位主函数打开 IDA加载mock_rev。在 Strings 窗口里看到Enter your flag: Try again!双击Try again!按 X 看交叉引用跳到它的引用函数。这个函数名是sub_4011B0。我按 N 把它重命名为check_flag。再翻一下发现Enter your flag:也在同一个函数里调用。这样基本确定这就是核心函数。进入 F5 伪代码看到大概这样的结构__int64 __fastcall check_flag(void) { char buf[32]; unsigned __int64 v2; v2 __readfsqword(0x28u); // 栈保护忽略 printf(Enter your flag: ); scanf(%24s, buf); if ( strlen(buf) ! 16 ) { puts(Try again!); exit(0); } if ( (unsigned __int8)crypt_check(buf) ) puts(Correct!); else puts(Try again!); return 0LL; }这里crypt_check很可疑总共出现两层check_flag负责输入和显示结果真正干活的在别的函数。双击跳进去这就是我们要找的“加密逻辑所在”。4.3 加密逻辑还原crypt_check的伪代码简化后大概是这样_BOOL8 __fastcall crypt_check(__int64 a1) { char input[16]; char target[16]; int i; char v5; // 输入复制到 input memcpy(input, a1, 16u); for ( i 0; ; i ) { if ( i 16 ) break; v5 input[i] ^ (i * 7 5); input[i] (v5 33) 0xFF; } // 把内置密文复制到 target qmemcpy(target, \x74\x71\x83\x8E\x6B\x8B\x2D\x51\x73\x86\x49\x1E\x30\x26\x1A\x24, 16); return memcmp(input, target, 16) 0; }这一段就是我们需要的全部信息。加密逻辑非常直白对输入的每个字节先和(i * 7 5)异或再加0x21因为十进制 33最后取低字节。比较的目标是一串十六进制密文。看伪代码的时候我经常会先用笔在纸上写出逆运算公式再写脚本。这是最不容易出错的顺序。测试数据是最后才用的不要指望“扫一眼”就看懂整条链路。这里逆运算公式是flag_byte[i] (target[i] - 33) ^ (i * 7 5)注意取负数的时候要用 0xFF保证是无符号字节。4.4 脚本解法与验证有了公式就非常稳了。写一段 Python 脚本target [ 0x74, 0x71, 0x83, 0x8E, 0x6B, 0x8B, 0x2D, 0x51, 0x73, 0x86, 0x49, 0x1E, 0x30, 0x26, 0x1A, 0x24 ] flag [] for i, c in enumerate(target): # 第一步减去 0x21取低字节 tmp (c - 0x21) 0xFF # 第二步异或回原始值 flag_char tmp ^ ((i * 7 5) 0xFF) flag.append(chr(flag_char)) print(.join(flag))跑一下输出flag{R3v_1s_Fun}再回到 IDA 里核对数据的字节序确定 target 数组的顺序和脚本里一致没有反着来。整个流程到此结束主函数找到了加密逻辑定位到了算法还原了flag 拿出来了。5. 入门阶段最容易忽略的现场操作细节到这里主流程已经走完一遍但我还想起几个“没人专门告诉你但你迟早踩坑”的细节。把这些补上你的逆向基本功就算真正立起来了。5.1 伪代码不是万能的F5 生成的伪代码本质是 IDA 根据汇编行为还原出的一个“近似表达”它不是原始源码。你会看到很怪异的写法比如*(_BYTE *)(v4 i)、__CFADD__这种带进位标志的函数、把指针当整数到处转的类型转换。这些都不是 bug是 IDA 对汇编的翻译结果。遇到这种情况千万不要在伪代码里死磕切换到反汇编窗口看对应的汇编指令往往瞬间就明白了。我处理这种问题的习惯是在伪代码窗口里选中目标行按 Tab 切换汇编窗口对照指令一条条看。尤其当你发现伪代码里出现了一个看似不受控的变量十有八九是 IDA 类型推断出了问题。这时候按 Y 修改变量的类型重新 F5通常能恢复正常。5.2 F5失效时的退路伪代码生成失败的情况也有通常是恶意嵌套的逆向循环、复杂的花指令、栈帧被篡改或者代码段还没有被 IDA 识别为代码。遇到这种情况先不要慌。回到汇编层找到目标地址按U取消 IDA 对该处的代码/数据定义然后按C强制它把这段数据当成指令重新分析再按 F5 试试。如果仍然失败我会直接改用调试器动态分析。在 Linux 下用 gdb 也好用 IDA 自带调试器也好核心思路很明确把断点下在比较函数或者数据访问处运行程序观察内存里数据的变化。动态调试的本质是“让程序帮你演一遍”很多静态分析想不通的逻辑动不动就看明白了。5.3 养成边分析边记笔记的习惯我早期做题最大的教训就是“只分析不记录”结果分析到一半回头想这个函数是干嘛的得重新点开。后来养成习惯每确定一个函数职责立即在 IDA 里按 N 重命名每确定一段数据处理流程就写到临时脚本或备注里每拿到一段密文就复制到单独文件保存。这个习惯看着小实际省下的时间远超想象。比赛时心态一紧张唯一能依靠的就是你留下来的“分析痕迹条”。还有一个容易被忽视的细节复制 IDA 里的十六进制数据时注意字节顺序。比如 IDA 展示\x74\x71如果按 Dword 型数据复制得到的是0x7174会导致脚本里所有字节全部错位。我在实战里就因为这个浪费过十五分钟最后困在自己的笔记里才发现是从这里开始反的。处理办法是在 IDA 里把目标数组的显示格式切到 C 语言数组直接复制原始字节列表不要用 Address/Hex 混合显示。5.4 新手最常见的几个错误复盘我汇总一下在指导其他新手时反复看到的失误基本都是同一个套路找错了分析目标或者对数据格式不够敏感。第一个错误是急着读代码而忽略程序运行时的输入长度限制。很多时候出题人会在主函数里先校验输入长度不满足就直接退出或失败。如果只盯着加密函数分析却不知道前面还有长度校验后面即使你把算法还原了也可能因为明文长度不对导致无法触发比较逻辑。第二个错误是忽略字符编码。有些十六进制串在 IDA 里显示为aAbBcC这类可见字符串看起来像英文实际上是加密后的密文直接拿去跑逆向脚本会得到乱码。以flag{R3v_1s_Fun}为例它的 ASCII 码只有一部分是可见字符其余都在非打印区域不能靠肉眼判断目标数据的含义。第三个错误是只学工具不学思维。遇到新题直接翻 IDA 找 flag 字符串找不到就懵。实际上 CTF 里的 flag 字符串很少明文存在真正的目标永远是“算法本身”。你把“找字符串”当常规武器把“读代码”当核武器这个主次不要搞反。第四个错误是过早尝试动态调试。静态分析还没理清数据流向就忙着下断点很容易陷入“单步了半天却不知道自己在等什么”的境地。我的原则是先静态把结构看懂再用调试验证假设。静态是主线动态是补充。最后说点个人的实战体会拿做题来说IDA 这套方法论的核心不是“会用快捷键”而是“每个步骤都有明确目的”。我每次都先想清楚要找主函数是因为我要知道程序把输入放到哪了要定位加密逻辑是因为我要知道输入经过什么变换要看交叉引用是因为我要从已知结果反向锁定必经之路。当你始终带着“下一步要解决哪个问题”的念头去操作IDA 就不再是软件而是你思维的延伸。这套思路也不只适用于 CTF日常工作里分析样本、排查二进制崩溃、理解一段没有源码的历史代码都可以复用同一个框架。做逆向没什么玄学就是用合适工具把信息一层层剥开在关键节点停下来问一句这个数据的流向下一个地方在哪。答案找齐了逻辑自然就通了。下一期我会接着聊动调实战重点讲断点怎么下、内存怎么读、以及那些“一眼看上去像加密其实只是解压”的迷惑套路。如果这篇里的思路你还没消化完可以先拿几道入门题练练手标准就一条不靠作弊工具完全通过 IDA 静态分析还原出题人的整个校验流程。能独立走完你算是真正入门了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑