先说个结论:如果你正在找逆向入门的练手题,[FlareOn2] very_success 和 [FlareOn3] Challenge1 是两堂绕不开的必修课。前者是一道标准的 Windows PE 动态调试题,考的是在调试器里观察程序行为、定位校验分支、修改内存状态让校验“按你的想法走”;后者则是一道纯粹的 JavaScript 混淆逆向题,考的是从一堆故意写乱的前端代码里还原出真正的验证逻辑。两道题一个偏二进制、一个偏脚本,正好覆盖了逆向工程里两个最基础也最核心的能力:读懂运行时的程序,以及剥离混淆直达本质。无论你是刚看完汇编基础想找题练手的新手,还是准备参加 CTF 但还没碰过 FlareOn 的选手,这两个题都值得花一个下午认真做一遍。
1. 题目定位与考点拆解
1.1 FlareOn 关卡设计逻辑与这两道题的坐标
FlareOn 是每年一度的逆向挑战赛,题目从易到难排列,前几题通常承担着一个共同任务:让参赛者热手。它的题目有个很明显的特点,不会故意堆砌超高难度的花指令或虚拟机壳,而是把精力放在“模拟真实工作中会遇到的情况”上。比如一个程序不给你源码,不告诉你校验逻辑是什么,甚至故意把代码写得绕来绕去,你要做的就是在有限的信息里找到突破口。
[FlareOn2] very_success 在当年的关卡序列里属于前段题目,难度定位是“会使用调试器就能做出来”。它不要求你拥有高深的密码学知识,也不要求你精通脱壳,核心考点就是一件事:程序里有个校验逻辑,你能否找到它,并且通过修改内存或寄存器状态让程序进入成功分支。这道题对新手最友好的地方在于,它的算法逻辑并不复杂,你完全可以用静态分析还原整个校验过程,也可以完全不看算法,直接在动态调试里修改一个字节就过关。两条路都能走通,这对培养“逆向思维”非常有帮助。
[FlareOn3] Challenge1 则换了一个完全不同的方向。很多人一提到逆向就想到 PE 文件、汇编指令,但现实中大量业务逻辑都跑在脚本引擎里,JavaScript 混淆是 Web 前端和个人软件里最常见的保护手段之一。这道题给你的是一个网页文件,里面有一段被人为打乱的 JavaScript 代码,你需要从这段代码里分析出它到底在做什么,然后构造出正确的输入,或者说直接还原出最终答案。它考察的不是汇编阅读能力,而是对脚本语言执行逻辑的理解,以及在混乱代码中保持清醒的能力。
两道题放在一起看,恰好构成了一组完整的入门训练:先学会在二进制程序里用调试器观察和干预程序行为,再学会在动态语言代码里做静态分析和动态验证。很多新手一上来就扎进复杂的壳和反调试里,反而把这两块基本功忽略了。
1.2 两道题覆盖的能力矩阵与学习价值
如果你仔细拆解这两道题,会发现它们共同覆盖了四层能力。
第一层是信息收集能力。拿到 very_success 的 exe 文件,你要先判断它是 32 位还是 64 位,有没有加壳,用的什么编译特征;拿到 Challenge1 的 HTML 文件,你要先判断 JavaScript 代码是内联的还是外链的,有没有经过打包器处理。很多新手拿到文件直接双击运行,看到程序提示输入错误就一脸茫然,这其实已经输在了起点上。
第二层是静态分析能力。对 very_success 来说,你要在 IDA 或 Ghidra 里定位到校验函数,理解输入的字符串经过什么变换之后和什么数据比较;对 Challenge1 来说,你要在格式化的 JavaScript 里追踪变量的流向,找到最终的比较函数和数据。这一层不要求你把每一条指令都读明白,但要求你能把程序的大致逻辑框架勾画出来。
第三层是动态调试能力。二进制题里,你需要在 x64dbg 或 OllyDbg 中下断点、单步跟踪、查看寄存器与内存,在合适的时机修改关键数据;JavaScript 题里,你需要在浏览器开发者工具中下断点,观察函数调用栈和变量值,甚至直接在控制台里调用内部函数来验证你的判断。动态调试是这两道题共同的核心,也是很多自学逆向的人最薄弱的一环。
第四层是耐心与沉淀能力。这两道题的难度都不算高,但它们要求你养成一个习惯:遇到看不懂的逻辑,先记下来,换一个角度再看,而不是死磕一处。逆向工程的学习曲线很陡,很多人不是被题目难倒的,而是被自己的急躁情绪打败的。
2. very_success:动态调试内存改写的经典课
2.1 文件侦察:先搞清楚对象再动手
拿到 very_success 这道题,第一步不是急着运行,而是先把它放到工具里过一遍。我当时用的流程是:先用 DIE 或者 ExeinfoPE 看文件类型和编译信息,确认它是一个 32 位还是 64 位的 PE 文件,有没有壳,有没有常见的混淆特征。然后丢进 IDA 里看导入表,重点关注它调用了哪些 API。一个程序只要调用了输入输出相关的函数,你就有了第一个下断点的位置。
印象中这道题是个很规矩的 PE 文件,没有加壳,导入表里有和用户输入、控制台输出相关的 API。这就意味着你可以直接在关键 API 上下断点,快速定位到程序处理用户输入的位置。如果你用的是 x64dbg,可以在入口点直接运行,然后通过命令行参数或者交互输入的方式触发程序逻辑,再观察它调用了哪些函数。
这里有一个新手容易犯的错误:上来就在 IDA 里从头到尾按 F5 看伪代码,试图把整个程序的逻辑完全搞清楚再动手。这个思路不能说错,但对这类题目来说效率太低了。更好的做法是先在调试器里让程序跑起来,观察它的行为,再带着问题回到静态分析里。比如说,你看到程序先接收输入,然后进入一个循环做了一堆运算,最后有一个跳转指令决定输出成功还是失败,那你的核心目标就变得非常明确:找到那个跳转指令。
2.2 静态还原校验逻辑,找到关键分支
在 IDA 或 Ghidra 里定位到校验函数之后,你会发现 very_success 的校验逻辑并不复杂。它大致上就是把输入的字符串逐字节取出来,和某个或某几个固定的值做异或运算,然后再和内存中另一段预先存放的数据做比较。比较的结果直接决定程序走向哪个分支。
这种模式在 CTF 里太常见了,几乎可以说是一道送分题。它的核心就是一个 XOR 循环:把用户输入作为 key,把一段密文逐字节解密,然后看解密结果是否为某个预期的明文。如果你对密码学稍有了解,就会知道 XOR 有一个很好的性质:如果 A XOR B = C,那么 A XOR C = B。所以当你看到这种模式时,你甚至不需要去修改程序行为,直接在静态分析里把参与异或的两个值提取出来,自己异或一下,就能把预期的输入或者最终的 flag 算出来。
不过我要提醒一句:静态分析能还原逻辑,但还原逻辑和实际调试是两回事。考试的时候可能时间充裕,你可以慢慢算;但如果在实战或比赛中,动态调试往往更快、更直接。这也是 very_success 这道题最有意思的地方:它给了你两条路,你可以选择做数学题,也可以选择做手术。我个人的建议是两条路都走一遍,先静态算出来,再用动态调试去验证,这样你对程序的理解会深得多。
2.3 动态调试实操:断点、单步、内存修补
动态调试的核心思路很简单:你不是去逆向它的算法,而是直接干预它的决策。具体到 very_success 这道题,操作流程一般是下面这样的。
第一步,在调试器里加载程序,在接收输入之后的函数调用处下断点。你可以通过搜索字符串的方式快速定位,比如在 x64dbg 里用字符串搜索功能找到提示输入的那段文字,然后在引用它的指令处下断。这样程序一执行到这里就会停下来,你就可以单步跟踪。
第二步,单步跟踪进入校验循环。你会看到程序逐字节处理你的输入,然后在一个比较指令处停下。这个比较指令通常是一个 CMP,紧跟着一个条件跳转指令,比如 JNZ 或 JZ。如果你输入的字符串是错的,条件跳转会走向失败分支;如果你想让程序认为你输入对了,最简单的办法就是在调试器里修改标志寄存器的 ZF 位,或者直接把跳转指令改成 NOP,甚至把跳转条件反过来。
第三步,修改内存。比较指令通常是比较两个内存地址里的数据,其中一个是用户输入的变换结果,另一个是程序内置的预期值。你可以在内存窗口里直接找到那个预期值,把它改成和你输入变换结果一致的值。这样哪怕你输入的字符串完全是乱写的,程序也会认为校验通过。
关键的问题来了:改跳转和改数据,哪个更优雅?这个没有标准答案,但在实际调试中有一个经验法则:如果能通过修改数据达到目的,优先修改数据,而不是修改代码流程。原因是修改数据往往能让你观察到程序原本设计好的输出行为,而修改跳转虽然也能过关,但可能会跳过程序里的一些后续处理,导致输出结果不完整或者程序崩溃。我在做 very_success 时就遇到过类似情况,只改跳转虽然能看到成功提示,但程序后续还有一些收尾工作没做完,直接退出,反而不如老老实实把内存里的数据改掉,让整个流程自然走完。
2.4 这道题真正的价值点
很多人做完 very_success 之后会觉得:就这?改一个字节就完了?如果你只停留在“改字节过关”这个层面,那确实浪费了这道题。
这道题真正的价值在于,它让你第一次体会到“程序不是黑盒子,而是可以干预的”。你在调试器里修改内存的那个瞬间,其实就是在和程序作者博弈:他设计了一个校验,但你通过观察找到了校验的位置,并且在运行时改变了程序的状态。这种能力在真实的恶意代码分析、漏洞研究、软件破解里都是最基础也最核心的。
另外,这道题还训练了你对汇编指令的敏感度。CMP 和 JNZ 的组合你看过一次之后,以后再遇到类似的模式,就不再需要一条一条去读指令了,扫一眼就能判断出哪个是校验点、哪个是分支点。这种“模式识别”能力是逆向工程中非常重要的加速器,而它只能通过大量看题做题培养出来。
3. Challenge1:JavaScript 混淆的逆向破译
3.1 认识题目形态:一个 HTML 文件里的乾坤
[FlareOn3] Challenge1 换了个赛道,它直接给你一个 HTML 文件。很多做惯了 PE 逆向的人看到这个会有点懵,不知道从哪下手。其实思路是相通的:先看文件内容,再找代码入口,再追踪逻辑。
打开 HTML 文件之后,你会发现里面有一段比较“壮观”的 JavaScript 代码。所谓壮观,不是说代码量有多大,而是说它被故意写得非常难看:变量名是一堆毫无意义的字母组合,字符串被拆成碎片再拼接,逻辑里穿插着大量看似无用的计算和判断。这些都是典型的混淆手法。
第一步要做的事情,不是急着分析代码内容,而是先看清楚代码的整体结构。建议你把 HTML 里的 JavaScript 代码复制出来,放到一个独立的 JS 文件里,然后用 Prettier 之类的格式化工具把代码排版整理一下。这一操作不会改变代码逻辑,但能让变量的作用域、函数调用的嵌套关系一目了然。很多新手一看到压缩成一行甚至几行的代码就头皮发麻,其实格式化之后往往发现逻辑并没有想象中那么复杂。
格式化之后,你还需要给变量重新命名。这一步可以手动做,也可以用一些辅助工具半自动做。比如看到一个函数接收两个参数,作用是把两个字符串拼接起来,你就可以在分析过程中把它重命名为 concat 函数;看到一个变量最终和输入比较,你就可以把它命名为 expected。重命名的过程其实就是你理解代码的过程,等你把代码里的关键变量都改成了有意义的名字,整个程序的逻辑通常也就浮出水面了。
3.2 代码格式化和混淆模式识别
JavaScript 混淆有很多流派,有的侧重字符串加密,有的侧重控制流平坦化,有的侧重变量名混淆。Challenge1 这道题采用的是一些比较基础的混淆手段,但组合在一起还是能劝退不少新手。你需要学会识别这些模式。
最常见的模式是字符串拼接。正常代码里写的是"flag{",混淆后可能是一大串字符通过 charCodeAt 和 fromCharCode 动态生成。当你看到代码里频繁出现 String.fromCharCode 或者大段数字数组时,基本可以确定字符串被加密了。处理方法也很简单:在浏览器控制台里直接执行这段代码,看看它输出什么。
第二个常见模式是函数封装。正常逻辑是 a 调用 b,b 调用 c;混淆后可能变成 a 通过一个中间函数去调用 b,而那个中间函数可能是通过数组下标动态选择的。这种模式的目的就是打断你从头到尾线性阅读代码的节奏,让你没办法一眼看出函数调用关系。应对方法是找一个 JavaScript 调试器,在关键函数上下断点,运行时看调用栈。
第三个常见模式是逻辑冗余。代码里会插入大量永远不被执行的分支,或者计算出来但不影响结果的表达式。这些冗余代码的目的就是消耗你的精力,让你在无关的信息里迷失方向。识别这些代码的一个技巧是:如果你的静态分析进行到某一步之后,发现某段代码无论怎么执行都不会影响后面的比较结果,那就大胆跳过它,不用纠结每一条语句的含义。
3.3 关键逻辑的定位与复现
在处理混淆代码时,我习惯先向后看,再向前看。所谓向后看,就是先找到程序的最终输出点,比如页面上的某个提示文字、某个显示 flag 的区域,或者某个提交按钮的点击事件。从这个点往前回溯,就能找到它依赖的数据是什么。
Challenge1 这道题的逻辑最终会落在某个比较函数上:把你输入的内容做一系列变换之后,和一个预先设定的值做比较。你在格式化后的代码里搜索比较运算符,或者搜索成功提示字符串,就能找到这个比较函数的所在位置。
找到之后,不要急着去逐行理解整个变换流程。先看它调用了哪些函数,每个函数做什么。把每个函数单独拎出来,在控制台里用不同的输入去调用,观察它的输出。这个“黑盒测试”的思路非常管用:你不需要知道函数内部怎么写的,只要知道输入是什么、输出是什么,就能推算它的行为。当你把整个链路上每个函数的输入输出关系都摸清楚之后,最后一步就是数学问题了:哪个输入经过变换之后能变成那个预设值。这个过程通常叫“逆推”,在 Challenge1 里可以通过手动构造输入或者写一段小脚本来完成。
我当时做这道题的时候,遇到一个比较典型的坑:我发现代码里有一个函数对字符串做了一轮看似很复杂的变换,但运行之后其实什么都没变。后来才意识到那是作者故意放进去的干扰项。这种干扰在真实场景里也很常见,所以做 JavaScript 逆向时一定要保持警觉:代码里出现的每一个函数,不都是有用处的,你要学会找到那条真正影响结果的主线。
3.4 动态验证与其他思路
静态分析到一定程度之后,一定要回到动态验证上来。浏览器开发者工具是你最强大的盟友。你可以在比较函数上下断点,然后随便输入一个字符串触发校验,观察实际参与比较的值。这个时候,如果静态分析得出的预期值和动态观察到的实际值不一致,那说明前面的分析里有某个环节理解错了,需要回头排查。
一种很实用的操作是:在控制台里直接修改变量的值。比如你知道比较函数比较的是两个值,一个来自用户输入,另一个是程序内置的预期值,那你可以在断点暂停时直接修改比较结果,让程序走成功分支。这个操作和 very_success 里修改内存数据本质上是一回事,只是平台从汇编调试器换成了浏览器控制台。
还有一种思路是直接导出函数或者解密数据。如果你发现预期值在内存中是以加密形式存放的,那你可以在加密函数执行完毕之后,从内存或变量里把解密后的值提取出来。这有点像二进制逆向里的 dump 内存,只不过这里 dump 的是 JavaScript 运行时中的变量。你甚至不需要完全理解加密算法,只需要在正确的时间点读取正确的变量即可。
4. 两道题串起来的方法论与踩坑记录
4.1 静态与动态的配合节奏
做完这两道题,我发现它们有一个共同的节奏:先静态搭框架,再动态填细节。如果你一上来就沉浸在汇编指令或者混淆代码的细节里,很容易被绕晕;反过来,如果你只依赖动态调试,盲目地单步跟踪,也容易迷失方向。正确的方式是:用静态分析找到程序的关键路径,再通过动态分析确认这条路径上的每个关键点。
具体来说,very_success 这道题,你在 IDA 里花二十分钟找到校验函数和分支位置,然后在调试器里花五分钟验证并修改数据,整个过程非常高效。Challenge1 这道题,你在格式化代码里花半小时找到比较函数和数据,然后在浏览器控制台里花几分钟验证,也是同样的节奏。静态分析负责告诉你“程序大概在做什么”,动态调试负责告诉你“程序实际在做什么”,两者结合才能形成完整的判断。
4.2 常见问题速查表
我把做这两道题时最容易遇到的问题整理成了表格,方便你遇到问题时快速对照排查。
| 常见问题 | 出现原因 | 排查方法 |
|---|---|---|
| 运行 exe 后程序直接崩溃 | 可能对输入格式有要求,或程序存在反调试 | 先用静态分析查看输入处理的入口,确认输入格式 |
| 修改跳转后看得到成功提示但程序异常退出 | 只改了分支,跳过了后续必要的处理逻辑 | 优先修改内存数据,而不是修改跳转指令 |
| 调试器里找不到比较指令 | 搜索字符串时可以尝试搜索成功或失败的提示文本,再通过交叉引用定位 | 在字符串引用处下断点,运行时观察寄存器 |
| JS 代码格式化后仍然看不懂 | 变量名混淆和逻辑冗余分散了注意力 | 给变量重命名,删除不影响结果的冗余分支 |
| 浏览器控制台执行报错 | 部分变量依赖原有 HTML 环境 | 直接在页面的开发者工具里执行,或补充缺失的对象 |
| 找到了比较函数但不知道预期值 | 预期值可能是动态生成的 | 断点暂停时查看变量,或调用生成函数得到结果 |
| 输入正确却仍然失败 | 可能还存在二次校验 | 搜索所有比较和跳转,逐一排查 |
4.3 个人实操中的几条经验
第一,不要迷信某一种工具。very_success 这道题,我用 IDA 做静态分析,用 x64dbg 做动态调试;Challenge1 这道题,我用 VS Code 做代码编辑,用浏览器开发者工具做动态调试。每个工具都有自己的长处,重要的是组合使用,而不是在一个工具里死磕到底。
第二,调试时要大胆修改,但要记录修改了什么。我曾经在 x64dbg 里同时改了好几个字节,结果程序行为变得很奇怪,又说不清是哪个修改导致的。后来养成习惯:每次只改一处,记录地址和原始值,这样即使改错了也能快速还原。
第三,做 JavaScript 逆向时,控制台是你的好朋友。很多你觉得难分析的函数,直接在控制台里调用一下,把返回值打印出来,立刻就能明白它的作用。不要害怕运行代码,在可控的环境里大胆实验,效率远高于纸上谈兵。
第四,也是最重要的一点:做完题之后要回头总结。这两道题的价值不在答案本身,而在你通过做题建立起来的分析流程。做完 very_success,你总结一下在调试器里定位校验分支的方法;做完 Challenge1,你总结一下识别混淆模式和逆推逻辑的方法。这些流程才是真正属于你的能力。
最后再分享一个小技巧:做这类题目时,把最终的正确输入和成功输出都截图保存下来。一方面是为了写 writeup 时有材料,另一方面是当你过段时间再回顾时,看到这些截图能快速唤醒当时的分析思路。我自己的习惯是每做完一道题都写一个简短的记录,哪怕只有几百字,也远比做完就忘要有价值得多。希望这两个题目能成为你逆向路上的两块坚实垫脚石。