2026/10/20 14:31:11

GKCTF逆向题全复盘:从签到题到VM虚拟机,掌握关键分析技巧

GKCTF逆向题全复盘:从签到题到VM虚拟机,掌握关键分析技巧 2020年那届GKCTF的逆向题我前前后后复盘了两遍第一遍是跟着比赛节奏时停时续地想第二遍是真刀真枪地把每个坑都填平了。整体印象是题量不算大但难度曲线很照顾人从签到题的字符串比较到中等题的加壳和简单加密还原再到压轴的迷宫与虚拟化指令处理几乎把Reverse方向的核心套路都串了一遍。如果你正想入坑逆向或者想在CTF比赛里稳定拿下一血这套题非常值得从头到尾分析一遍。下面我把当时使用的工具链、每道典型题的分析路径以及踩过的那些坑一并写出来希望你能少走弯路。1. 整体盘点这套逆向题到底考了什么1.1 题目分布与难度曲线这一届的逆向题大致维持在8道左右难度是递进的。签到题基本就是“输入一串字符程序拿去和硬编码的flag比较”破解思路非常简单很多人第一次玩也能三分钟出答案。中间两到三道题开始上难度有的是加壳程序有的是对输入做简单变换后再比对需要你动手写脚本解密。最后两道保持在“看到就头大”的水平一道是迷宫另一道是VM虚拟机题如果你之前没有系统性练过大概率要卡很久。这种难度设计其实很科学。主办方没有把逆向直接拉满到虐人状态而是先让你拿到分数建立信心再用中等级题目把真正有实力的人筛选出来最后用压轴题拉开差距。反过来作为练习对象也特别合适新手可以先把签到题和中档题吃透老手则可以拿压轴题检验自己的分析深度。我当时把整套题刷完最直观的感受是自己的“函数定位速度”比之前快了不少。另外值得注意的是2020年这套题里的许多样本到现在依然是很多逆向入门教程的常客。你会反复在各类视频和博客里看到类似的套路因为它们的考点足够经典字符串交叉引用、UPX脱壳、RC4识别、BFS寻路、虚拟指令集提取。把这些题做透等于把逆向入门的路基打了一遍。1.2 逆向题背后要的三项核心能力说回能力模型。逆向表面上是“看懂代码”背后其实是三种硬实力。第一种是静态分析能力。拿到一个二进制文件不用运行它直接通过反汇编、伪代码、字符串、常量、导入表来推测程序想干什么。这就像医生先看X光片不用切开皮肤就能判断病灶在哪。IDA和Ghidra是主力。第二种是动态调试能力。很多时候光看静态代码你根本不知道某个变量在哪一刻变成了什么。通过调试器设置断点、观察寄存器、修改内存就能实时看到程序内部状态。Windows下常用x64dbgLinux下常用GDB。第三种是脚本编写能力。遇到复杂运算靠人脑硬算不现实尤其是迷宫、加密、爆破Python加各类库几乎是必需技能。进阶之后还需要掌握加壳脱壳、反调试绕过、加密算法识别、控制流混淆恢复等专项能力。我通常把这四项称为“逆向的四座小山坡”不是说有多难而是每个方向都有自己的一套套路跨过之后就顺畅了。我一直喜欢用一个比方静态分析看线路动态调试测信号脚本是改造工具脱壳则是撕开包装。四者配合才能把一个可执行文件从外到内彻底翻个底朝天。1.3 赛前工具链的准备在开赛之前把工具准备好能省下大把时间。我那次的比赛目录大概是这样的工具用途备注Detect It Easy查壳、识别编译器和打包器轻量比PEiD更新IDA Pro / Ghidra静态反汇编和反编译一个用顺手一个备用x64dbgWindows动态调试支持插件调试PE必需GDB pwndbgLinux动态调试处理ELF文件的常规选择Python3 pwntools脚本编写、远程交互密码算法题靠它010 Editor十六进制查看和修改查看文件结构UPX一键脱常见UPX壳变种壳则需要手动脱除了工具环境也要提前搭好。建议在虚拟机里准备一套Win10或Win7环境用来跑老样本再准备一套Ubuntu环境跑ELF很多时候样本只能在特定系统上正常运行用物理机容易污染自己环境也容易出兼容性问题。还有一点给逆向专门建一个目录按日期建子文件夹把原始题目、中间产物、脚本、writeup分开存放。比赛结束复盘时会感谢当时的自己。2. 从一道签到题说起静态分析的完整路径2.1 拿到二进制文件后的第一件事不要急着双击运行。先把文件扔给Detect It Easy看看它到底是个什么物种。我记得当时那个签到样本是一个64位Linux的ELF文件DIE显示没有加壳编译器信息大概是GCC。这种情况下直接拖进IDA是没问题的。但如果样本显示有“UPX”字样必须先脱壳再分析否则你在IDA里看到的不是真实逻辑而是壳的启动代码甚至函数列表会乱到没法看。判断文件类型这步虽然基础却能避免后面大量无效劳动。我用file命令确认架构用DIE确认壳和编译器用相关工具查看保护机制。保护机制对远程题目影响比较大本地逆向至少也要知道有没有PIE因为PIE会导致地址随机化影响你在调试时下断点。识别完这些信息后我还会看一眼导入表。如果是Windows PE导入表里出现动态链接相关API可能会动态解密如果导入表很干净大概率是静态链接分析时就要多靠模式识别。导入表和字符串一样是静态分析的第一印象。2.2 用IDA快速定位关键函数把ELF文件拖进IDA后我习惯先看左侧函数窗口按函数名排序找main。双击进入main后直接按F5转换成伪代码。签到题往往就是这样的套路int __cdecl main(int argc, const char **argv) { char input[64]; printf(Please input your flag: ); scanf(%63s, input); if (strcmp(input, flag{just_a_simple_check}) 0) puts(Right!); else puts(Wrong!); return 0; }看到这flag直接就是字符串内容。这种题没有难度但它是整套题的“开胃菜”它训练你最快找到比较逻辑的能力。现实比赛里这种题应该控制在一分钟以内否则后面时间不够用。补充一个细节如果程序是C写的或者用了一些结构体数组main可能有_m_开头或sub_开头的包装函数。这时可以用ShiftF12字符串窗口搜索“input”“flag”“error”等关键词找到引用它的函数再往上层回溯一样能定位到核心函数。2.3 实际操作从字符串交叉引用到flag这里顺手记一下标准操作很多中等题也会用到。第一步在IDA里按ShiftF12打开字符串窗口。第二步用CtrlF搜索形如“flag”“correct”“success”“wrong”的关键字。第三步双击字符串IDA会跳到数据段对应位置然后按X查看交叉引用。第四步点击引用记录跳转到对应代码再按F5看伪代码。这个链路本质是利用“程序员写死的提示文本”反向找回校验逻辑。几乎所有CTF题里提示文本都很难完全抹掉因为程序至少要有输入输出交互除非它做得极其隐蔽。我还遇到过一种情况字符串本身是加密的数据段只看到一堆乱码运行时才在内存里还原。这种题里字符串窗口就抓瞎了。解决思路是动态调试在程序即将调用puts或printf之前下断点这时内存中的字符串就是还原后的明文。所以静态字符串定位不是万能的但它永远是第一选择因为它足够快。签到题总结起来就是DIE查壳IDA找mainF5看比较ShiftF12找字符串。四步走完拿分。3. 核心硬骨头一道典型加密题目的完整还原3.1 从常量特征识别加密算法过了签到题之后大部分逆向题都会加点“调料”对输入做某种变换然后把结果和固定密文比对。这时候如果你能一眼识别出算法解题速度会快一个量级。我是怎么练识别能力的核心就一句话背算法的“指纹”。每种常见算法都有几个很明显的特征点。比如RC4伪代码里会出现一个for i in range(256): S[i] i的初始化循环后面还有两个索引互相交换只要看到这种256字节的S盒十有八九是RC4。XXTEA的特征是常量0x9E3779B9这是黄金分割比例相关常数全球独一份看到就可以判断。AES特征更明显超大的0x100字节S盒替换表开头往往是0x63 0x7C 0x77 0x7B。Base64则是在数据段直接出现“ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789/”这串查表字符串。还有MD5/SHA系列的初值0x67452301、0xEFCDAB89等。把这些指纹整理成一张速记表算法特征指纹提防点RC4256字节S盒初始化、双索引交换密钥长度和填充方式XXTEA0x9E3779B9常量数据块是32位字还是64位字AESS盒表、列混合矩阵可能用在白盒环境需更复杂手段Base64标准字母表字符串可能是变种表MD5/SHA固定IV常量输入是否加了盐不过光背指纹不够因为现在很多题目喜欢把算法“魔改”一下比如把RC4密钥展开改成不同长度把Base64字母表换顺序。这时候只靠识别名称是不行的还要会顺着代码逻辑做数据流还原。建议平时练习时遇到加密算法手动把key、输入、密文这三样标出来画一条它们之间的变换链路时间长了自然就有感觉。3.2 解密脚本复现算法而不是反向一个个试识别出RC4之后怎么快速拿flag我见过很多新手第一反应是“把加密过程反过来”但对于RC4这类流密码解密过程和加密过程完全一致都是生成密钥流后做异或。所以正确的做法是复现加密过程用已知密文再和密钥流异或一次。我给一个当时用来解RC4题的Python脚本骨架def rc4_init(key: bytes) - list: S list(range(256)) j 0 for i in range(256): j (j S[i] key[i % len(key)]) 0xFF S[i], S[j] S[j], S[i] return S def rc4_stream(S: list, length: int) - bytes: i j 0 keystream [] for _ in range(length): i (i 1) 0xFF j (j S[i]) 0xFF S[i], S[j] S[j], S[i] keystream.append(S[(S[i] S[j]) 0xFF]) return bytes(keystream) key bytes.fromhex(...) # 题库中提取的密钥 cipher bytes.fromhex(...) # 题库中提取的密文 S rc4_init(key) flag bytes(c ^ k for c, k in zip(cipher, rc4_stream(S, len(cipher)))) print(flag)看起来很简单但实际坑很多。第一个坑是key的提取题目里key可能是明文形式的字符串也可能是十六进制字节数组你不能想当然地直接当成ASCII字符串。第二个坑是数据宽度C语言中索引j如果被声明成int运算结果要 0xFF否则后面数组访问就错了。第三个坑是密文的副本有些题目会先比较整个加密结果再单独比较长度如果你只把比较函数附近的一部分当成密文可能永远解不出来。所以我在脚本里会加中间值打印比如把rc4_init后的S[0:16]打印出来和IDA伪代码里初始化的S表前几个字节对比如果对不上说明key初始化逻辑还没吃透。这种小技巧能省下大量瞎试时间。3.3 脚本调试时最容易翻车的三个点基于多次踩坑我把最容易翻车的三个点整理在这里。第一大端小端问题。在内存中连续存放的多字节数据IDA显示出来可能是01 02 03 04但实际真实数值可能是0x04030201取决于架构和存储序。写脚本时如果直接把一组字节当作整数很容易反了。遇到这种情况先用Python打印一下结构体的原始字节和解析结果确认无误再继续。第二类型符号问题。C语言中unsigned char和char右移、异或的行为不同如果原程序用char你在Python里模拟时就要考虑符号扩展如果原程序用unsigned char就要确保Python里每个字节都在0~255范围内。第三输出编码问题。解密结果可能是不可打印字符也可能是中文、UTF-8编码字符串直接print可能显示成乱码。先尝试bytes转hex再尝试utf-8解码一般都能解决。我自己在那次比赛里就翻过一个小跟头密钥在IDA里看到的是一串十六进制字符串我复制进Python后忘了对它做bytes.fromhex直接当成普通ASCII字符串用结果出来的是一堆乱码。后来把中间S盒一对比才想起来。那一次的经历让我养成一个习惯凡是遇到密钥和密文第一步先搞清它是原始字节流还是十六进制文本第二步再决定用什么函数转换。4. 迷宫、VM和脱壳题三道容易踩坑的题型4.1 迷宫题不要手推用BFS迷宫题是CTF里的常客基本逻辑是程序内置了一张地图读取玩家输入的方向字符比如w/a/s/d然后在地图上移动移动路径必须满足某个条件比如从起点走到终点。这类题目拆开后核心工作是两步还原地图找出合法路径。还原地图一般靠IDA。你看伪代码时如果看到有一个switch或if-else链分别处理几个字符又看到一维数组被反复按下标访问很可能就是迷宫。地图可能是一维数据按行展开的也可能是二维数组。判断方式很简单看下标表达式如果下标是row*colscol这种形式基本就是二维数组压成一维。拿到地图后千万不要手推。我当时第一次做直接在纸上画了半小时后来才知道用BFS几秒钟出结果。给出一个迷你BFS脚本思路from collections import deque # 地图数据假设来自IDA宽高从代码中推断 maze [] # 起点和终点坐标按需替换 start (0, 1) end (7, 7) directions {a: (0, -1), d: (0, 1), w: (-1, 0), s: (1, 0)} def bfs(): q deque([(start[0], start[1], )]) while q: r, c, path q.popleft() if (r, c) end: return path for step, (dr, dc) in directions.items(): nr, nc r dr, c dc if 0 nr len(maze) and 0 nc len(maze[0]) and maze[nr][nc] .: q.append((nr, nc, path step)) return None真实题目里可能更复杂地图不止二维可能是三维方向字符变成6种可能要求路径满足指定步数起点终点需要从代码里提取。不管怎样核心思维都一样把地图数据提取出来把移动逻辑转成方向数组再用BFS/DFS求路径。手动推真的没必要。还有一类“动态迷宫”地图是程序运行后随机生成的每次运行都不一样。这种题目必须靠动态调试或直接Patch程序让人工输入路径或者从内存中导出地图。总之迷宫题的难点从来不在寻路而在正确还原地图和移动规则。4.2 VM题拿出opcode表才是关键VM虚拟机题是逆向里公认的“劝退题”但只要掌握套路其实没那么可怕。这类题的核心是程序本身不是直接计算你的输入而是把一段自定义字节码放到一个解释器里执行解释器按“指令码—处理函数”的方式一个个执行。分析VM题的第一步是找到分发器。它通常是一个大的switch语句或跳转表根据opcode跳转去对应的handler。你可以在伪代码里看到一个大switchcase值是一个字节码变量。第二步把每个opcode和它对应的handler记录下来整理出一张指令集表。比如我某次整理的简化表opcodehandler逻辑说明0x01reg[op1] imm立即数载入寄存器0x02reg[op1] reg[op2]寄存器加法0x03reg[op1] ^ reg[op2]异或0x10cmp reg[op1], imm比较0x11jnz offset不等于时跳转有了这张表就可以把题目里的字节码翻译成类似汇编的伪代码然后再还原算法。有时候VM的指令特别多不需要每一条都理解只要能确定它最终在做什么计算就行。比如看到一连串xor和add很可能就是某种变换可以直接用Python模拟。VM题最大的陷阱是你容易陷进去研究虚拟机的每个细节最后把时间耗尽。正确策略是抓住数据流输入进到哪个寄存器哪些指令做了关键运算最终和哪个固定值比较只要把这三点问明白就可以不再管无关指令。4.3 脱壳题ESP定律和dump修复脱壳也是CTF逆向的常见大板块。UPX壳是最温柔的一条upx -d命令就完事。但比赛里经常碰到变种壳这时就需要手动脱。ESP定律是我最常用的手动脱壳方法基本流程如下用x64dbg加载程序停在入口点。按F8单步一步此时ESP会发生变化。观察寄存器窗口记下当前ESP值。在内存窗口或命令栏下硬件断点对ESP指向的地址设置断点条件可以设为“每次访问/写入都中断”。按F9继续运行。壳会用pushad保存寄存器环境然后在某个时机通过popad恢复此时程序会跳到原始入口点。在原始入口点处执行Dump用插件把当前内存镜像保存成新exe。用Scylla修复导入表修复IAT然后保存。听起来步骤不多实际操作时经常遇到壳的反调试、自校验、IAT加密。我的经验是遇到反调试先别硬刚下载ScyllaHide之类插件试试遇到自校验可以对比修改前后的文件跳过程序的CRC检查函数遇到IAT加密Scylla的自动修复能力通常够用。脱壳这步很多人会卡在“能不能运行”。其实脱完壳的Dump文件能不能正常运行不重要只要能拖进IDA正常静态分析目的就达到了。当然如果后续需要动态调试最好还是确保Dump文件能跑起来否则会影响断点和内存查看。我之前遇到过一种壳脱完壳之后程序报错一查发现是导入表里多了好几个被重定向的API用Scylla修复几轮才恢复。所以建议脱完壳后马上用DIE再查一次状态如果显示“No packer”并且函数面板可读才算基本成功。5. 常见问题与排查技巧实录5.1 工具与环境类问题先整理工具使用中的高频问题。IDA按F5没反应或报错常见原因包括函数用了花指令导致反编译失败、目标平台脚本不支持、函数过大导致引擎超时。一种解决方法是先把花指令Patch掉再重新F5另一种是改用Ghidra它自己的反编译器对某些混淆容忍度更好。x64dbg里下断点后程序直接退出可能是反调试检测也可能是地址随机化导致断点位置不对。如果程序是PIE需要在模块基址上加偏移下断点或者先关闭ASLR再调试。GDB连接不上检查权限和ptrace限制。Linux下的普通用户可能无法attach调试进程需要临时关闭相关限制。Python脚本依赖缺失在比赛前的虚拟机里确保常用库已经装好不然比赛环境没有外网装包会非常痛苦。总的来说工具问题的本质是环境问题。固定一套环境、提前联调所有插件比临场搜索教程有效得多。5.2 逻辑分析类问题逻辑分析上我遇到次数最多的坑是“伪代码看着很绕但实际逻辑很简单”。比如编译器优化后的代码到处都是临时变量、位运算但你手动还原时会发现核心就是异或一个常数。避免陷进去的方法是做“数据流切片”从最终比较函数出发往上回溯每一个参与比较的变量看它经过了哪些赋值和变换。不断问自己这个变量最初来自哪里中间被哪些运算改过答案基本就是一条从输入到输出的变换链。把这条链子的每一步映射成一行Python代码哪怕一开始很丑也能逐渐调通。还有一种几乎肯定碰到的情况是花指令。花指令是一段永远不会被正常执行、但会扰乱静态分析的垃圾字节。IDA遇到它会把后面的代码误解为数据导致函数逻辑错乱。看到这种用字节码编辑器把花指令改成nop或直接删除再重新分析经常就恢复原状了。5.3 复盘和快速拿分技巧复盘那段时间我给自己总结了一套“快速拿分优先级”先把范围扩大到所有逆向题快速筛选出三类题——字符串题、无壳题、有壳但算法简单题第一轮把能秒的题全部秒掉保住基础分第二轮处理需要写脚本的中等题第三轮才是VM、反调试这种硬骨头。另外团队赛里分工很重要。我习惯是两个人一组一人负责静态分析快速判断题型并给出大致分析一人负责动态调试和写脚本处理内存快照、密钥提取等操作。这样能有效降低“卡在某一步”的团队损失。写writeup也是提升能力的好方法。每道题复盘时我把“题目环境、使用工具、关键步骤、最终脚本”四项模板填完整理成自己的知识库。后来再遇到差不多类型的题直接翻知识库基本可以做到秒杀。反复强调一句整理记录不是浪费时间它是在给自己造加速器。结尾做完GKCTF这套逆向题我个人最大的感受是逆向没有那么多玄学更多是经验的堆叠。第一次看到RC4的S盒循环我还需要翻很久资料第二次再看到心里就已经默认“这就是RC4”。所谓的熟练其实就是在一次次实战里形成的条件反射。最后再分享一个小技巧把比赛用的工具目录、脚本模板、知识点速查表统一放到一个文件夹里每次比赛前检查一遍比赛后补充一遍。长期积累下来你再面对一道陌生题目至少不会在环境配置和基础操作上浪费时间。希望这篇复盘对你也有用也期待你在下次比赛里拿到想要的分数。