☰
CTF逆向入门:经典Mysterious题目完整分析流程
2026/10/12 5:08:34 网站建设 项目流程

前两天有个新手朋友找我诉苦:他说自己刚入逆向门,IDA也装了,随便打开一个exe却不知道从哪下手,尤其是那些窗口程序,入口点藏得深,回调函数一堆,看两眼就晕。我跟他说,想快速建立感觉,就去刷一道CTF里的古典Windows逆向题。比如那道经典的Mysterious,就是一个很好的入门样本:文件不大、没有壳、逻辑简单,而且它几乎把"运行观察、查壳、静态定位、算法还原、动态验证"整套流程都走了一遍。你如果能把这道题独立啃下来,后面再看类似的小程序就会顺很多。

这道题在不少CTF练习平台都能见到,网上也有一堆writeup,但我还是建议你先别急着看答案。我今天就以一个新手的视角,把我自己慢慢摸透它的完整过程写下来,包括我中间卡壳的地方、被伪代码误导的地方,以及最后怎么确定flag的。希望能给你省点时间。

1. 拿到文件后的第一件事:先跑起来而不是直接上IDA

新手最容易犯的毛病,就是把exe拖进IDA就开始啃汇编,结果看了半天入口点也不知道程序想干嘛。我的习惯是反着来:先把它当普通软件一样运行,观察行为和界面,心里先有个"这程序大概是什么样子"的预期,再回头去逆向,效率高得多。

1.1 运行环境与第一次启动观察

Mysterious是一个32位Windows GUI程序,所以你需要一个能跑32位exe的Windows环境。如果是Win10/11的64位系统,直接双击一般也能跑,反正这种小程序不带驱动也不带特殊权限,兼容性没什么问题。我自己的习惯是在虚拟机里放一个精简版Windows 10,专门用来跑这些来路不明的CTF样本,干净又安全。

第一次双击运行,程序会弹出一个对话框,界面上大概有:

  • 一个输入框,让你输入点什么;
  • 一个按钮,点击后触发判断;
  • 可能还有几个静态文本控件,内容就是提示语,类似"请输入密码""check"之类的。

我拿到的这个版本里,按钮的标题是"check"。你在输入框里随便打一串字符,比如"123456",点check,程序会弹一个MessageBox,标题是"error",内容大概是"something wrong"。如果你输入别的长度或内容,它也可能没有任何反应。

这一步看似简单,其实信息量很大:它证明程序逻辑是典型的对话框事件驱动——点击按钮后从输入框取文本,经过某种校验,再弹窗反馈。那么后面逆向时,我们的目标就很明确:找到这个对话框的回调函数,重点看它的WM_COMMAND分支。

1.2 查壳与位数确认:别上来就IDA

运行观察完之后,我还不会直接开IDA,而是先用工具查一下壳和编译信息。用的工具是ExeinfoPE或者PEiD都行,直接把这个exe拖进去,两三秒就能看到结果。这一步的目的是回答两个问题:

  1. 有没有加壳?如果加了UPX之类的常见壳,IDA打开全是乱码,还得先手动脱壳,麻烦很多。
  2. 是32位还是64位?这个决定了后面用哪个版本的调试器、哪个版本的IDA。

我这边的检测结果是无壳,32位PE。常见编译语言的识别结果指向MSVC,也就是用Visual C++那套工具链编出来的,这也解释了为什么对话框控件看着那么"复古"。既然无壳,就可以放心地用IDA Pro直接打开分析了。顺便说一句,如果你机器上只有64位调试工具,像x64dbg,也能调试32位程序,它内部有切换机制,但如果你更熟悉OllyDbg,用OllyDbg会顺手一点。

提示:没有查壳就贸然开始逆向,是新手最典型的坑之一。查壳这事一分钟都不需要,却能帮你避开后续一大半的麻烦。

2. 从WinMain到对话框回调:IDA里如何快速找到关键函数

现在正式进入逆向阶段。我用IDA Pro打开这个exe,看到入口点后,不会傻乎乎地从入口点开始逐行看,而是利用IDA的自动化分析结果,快速跳到真正有业务逻辑的地方。

2.1 入口点与资源的定位思路

一个标准的Win32 GUI程序,入口函数通常是WinMain,它会做几件事:注册窗口类、创建窗口(或者加载对话框资源)、进入消息循环。Mysterious这个程序用的是DialogBoxParam这类模态对话框API,也就是说界面不是一个完整的主窗口,而是一个对话框资源。这类程序的逻辑不会放在WinMain里,而是在对话框过程函数(DialogProc)里。

怎么找?很简单:

  1. 在IDA的左侧函数窗口(Functions window)里,按名称排序,找找有没有DialogFunc、DlgProc这类函数名;
  2. 如果IDA没识别出名字,就去看WinMain的反编译代码,找到DialogBoxParamA的第二个参数(对话框资源ID)和第三个参数(回调函数指针);
  3. 双击回调函数指针,就跳到了对话框过程函数。

拿我这个样本来说,关键函数名就是DialogFunc,在函数列表里一眼就能看到。双击进去,就能看到一堆消息处理的伪代码。对于新手来说,在这个函数里分析,比在WinMain里看消息循环要有意义得多。

2.2 通过导入表快速锁定敏感API

还有一个更快的定位方法,我经常用:看导入表(Imports)。一般的破解/逆向分析里,几个API的名字一出现,基本就暴露了程序的核心逻辑所在:

  • GetDlgItemTextA/GetWindowTextA:从控件读取用户输入;
  • strcmp/lstrcmpA:字符串比较,十有八九是密码校验;
  • MessageBoxA:弹出提示框,告诉你对还是错。

在IDA里打开Imports窗口,按名字找到GetDlgItemTextA,双击它,然后使用"查看交叉引用"(Xrefs),就能看到哪些函数调用了它。调用它的地方,就是程序读取输入的位置,也就是核心校验逻辑所在。这一招在几乎所有CTF逆向题目里都通用,比从头读代码快太多了。

2.3 F5反编译得到的初始伪代码

定位到DialogFunc后,把光标放在函数名上,按下F5,IDA会生成一份伪代码。我当时看到的版本大概是这样的(具体变量名可能略有差异):

INT_PTR __stdcall DialogFunc(HWND hDlg, INT a2, WPARAM a3, LPARAM a4) { char input[24]; // 输入缓冲 char output[24]; // 变换结果缓冲 int sum = 0; int i = 0; if ( a2 == 0x111 ) // WM_COMMAND { if ( (unsigned __int16)a3 == 1000 ) // 按钮ID { GetDlgItemTextA(hDlg, 1002, input, 20); // 读取编辑框内容 if ( strlen(input) > 6 ) // 长度校验 { for ( i = 0; i < strlen(input); ++i ) { sum += input[i]; output[i] = sum % 10 + 48; } output[i] = 0; if ( strcmp(output, "199999") == 0 ) // 与目标字符串比较 MessageBoxA(hDlg, "You are right!", "well done", 0); else MessageBoxA(hDlg, "something wrong!", "error", 0); } } } return 0; }

第一次看到这份伪代码时,我也有点懵:它不像传统CTF题那样直接比较输入和某个常量,而是先做了一通循环变换,再比较变换结果。别急,接下来我们就逐行拆。

3. 核心算法逐行拆解:那串看似凌乱的加减乘除到底在干什么

很多人卡在这一步,是因为伪代码里的变量名(v5、v6、v8)完全是IDA自动生成的,没有任何语义。你只有把代码里的每一个操作"翻译"成人话,才能明白它在做什么。

3.1 WM_COMMAND与控件ID对应关系

先看消息分发:

  • a2 == 0x111:0x111是WM_COMMAND的消息值。Windows所有窗口消息都是数值,WM_COMMAND就是用户点了按钮或菜单后发来的通知。
  • (unsigned __int16)a3 == 1000:a3的低16位是控件的ID。也就是说,这个对话框里编号为1000的控件被操作了,才会进入分支。

那么编号1000是谁?返回去看对话框资源(在IDA里按快捷键Shift+F12或查看资源段),就能看到ID为1000的控件就是那个"check"按钮。ID为1002的控件是编辑框。这里有人会问:为什么编辑框是1002,不是1000?这是对话框资源里静态定义的,没有特殊规律,纯粹是创建资源时随手填的。

所以这段代码的意思很简单:当用户点击"check"按钮时,程序从ID为1002的编辑框控件中取出文本内容。

3.2 循环里的累加与取模:一段手工哈希

再来看最核心的循环:

for ( i = 0; i < strlen(input); ++i ) { sum += input[i]; output[i] = sum % 10 + 48; }

这段代码每隔一个字符做一次:

  1. 取input[i]的ASCII码,累加到一个整数sum上;
  2. 计算sum % 10,也就是累加和的个位数;
  3. 加上48(即字符'0'的ASCII码),把数字转成字符存入output[i]。

举个简单例子:如果输入的前两个字符是A(ASCII 65)和B(ASCII 66),那么:

  • 第0轮:sum = 65,65 % 10 = 5,output[0] = '5';
  • 第1轮:sum = 65 + 66 = 131,131 % 10 = 1,output[1] = '1';
  • 依此类推。

所以这本质上是一个逐字符前缀和的个位数编码,或者说是一种"手工哈希"。它不是加密算法,也不存在什么密钥,就是一道很直接的计算题。难点只在于你要意识到:它的输入是任意字符串,输出是确定的一串数字字符,最终的校验是拿这个输出和常量"199999"做比较。

提示:% 10 + 48这段,很多新手会问为什么是48而不是'0'。因为C语言里字符'0'的ASCII值就是48,写成sum % 10 + '0'更直观,编译器处理后是一样的。看到+48或+0x30,第一反应就应该是"在把数字转成数字字符"。

3.3 比较目标与flag的关系

当程序执行到strcmp(output, "199999")时,如果两个字符串相等,就弹窗"You are right!",否则弹"something wrong!"。那么我们的问题来了:目标字符串是"199999",那输入是什么?flag又是什么?

很多writeup会直接告诉你答案:flag就是flag{199999}。为什么?因为程序在正确分支里并没有直接打印flag,而是只弹了一个MessageBox。但题目本身要求提交flag,那么flag字符串实际上藏在两个地方:

  • 一个是数据段里那个"199999"常量,你可以在IDA的字符串窗口(Strings)里直接看到它;
  • 一个是题目约定俗成的flag格式,包上flag{}就是最终答案。

那有没有可能通过反推输入,让变换结果等于"199999"?当然可以,但没必要。因为这道题考察的是你是否能读懂这个变换逻辑、找到比较目标,而不是让你构造一个能通过校验的输入序列。如果这是一道"输入密码才会出flag"的动态题,你才需要反推输入。这里既然flag就在常量字符串旁边,直接套格式即可。

我倒是有个习惯动作:哪怕已经确定flag,我也会顺手验证一下。随便构造一个满足条件的输入试试。比如设一个六字符输入iloveu,你可以在草稿纸上算,也可以动态调试时看寄存器,最终发现output完全不是"199999"。这能帮你加深理解"变换前后不一定相同"这个概念。

4. 不用死磕伪代码:两条更快的解题路线

如果时间紧,或者你对循环、取模这类代码还不够熟练,其实还有更快的解法。逆向好玩的地方就在于,同一道题可以有很多条路通向答案,不一定非要抱着反编译代码逐行啃。

4.1 字符串直接定位法

在IDA中打开字符串窗口(Shift+F12),你能看到一个列表,里面大概率直接包含:

  • "199999"
  • "something wrong!"
  • "You are right!"
  • "error"
  • "well done"

这个列表就是程序用到的所有明文字符串。其中"199999"非常显眼:它不是普通的提示文本,而是一串纯数字。双击它,IDA会跳转到.rdata数据段,能看到这个字符串旁边还围着其他常量。接着按X键查看这个字符串的交叉引用,IDA会列出所有引用它的地方,你点进去就能看到strcmp调用所在的那段代码。这个方法本质上和看伪代码是一回事,但步骤更少,也更适合遇到大型程序时快速聚焦。

还有一个小技巧:如果你看到字符串窗口里有"flag"这类字样,可以直接在IDA中按Alt+T搜索文本"flag",很多时候flag就被硬编码在数据段里。Mysterious这道题的现代版本里,有的会直接把flag{...}写进去,你搜索flag{就能找到;我手头这个版本没有直接把完整flag写出来,只有"199999",所以需要你套上格式。

4.2 动态调试验证法

如果你更喜欢动调,可以用OllyDbg或者x64dbg的32位模式打开程序,做这样几步:

  1. 在GetDlgItemTextA调用处下断点;
  2. 在strcmp调用处再下一个断点;
  3. 运行程序,输入一串字符,点击check;
  4. 程序会先断在读取输入的位置,按F8继续,就会断在strcmp;
  5. 这时看寄存器或栈窗口,能看到两个参数:一个指向你刚输入的字符经过变换后的结果,另一个指向常量字符串"199999"。

动态调试的额外好处是,你可以直接修改寄存器的值或标志位,强制让strcmp返回0,从而跳过算法验证,直接进入正确分支。虽然对拿flag没什么帮助,但对训练调试手感非常有用。我第一次用这种方式过掉校验时,才真正理解"程序流程是可以被强改的"这句逆向圈的老话。

提示:32位程序在调试时,注意观察strcmp的返回值和参数地址。返回值是0表示相等、非0表示不等,这个方向千万别搞反,否则你会在判断逻辑里绕晕。

5. 复盘:新手做逆向题最容易踩的坑

最后聊聊我这次做题复盘时想到的几个坑,每一个都是新手期容易反复踩的,写出来给大家提个醒。

5.1 反编译变量名不是真相

IDA F5反编译生成的变量名,像v5、v8、a1,只是工具自动生成的占位符,完全不具备语义。你如果去记"v8是用户输入",或者"a1是某个参数",很容易在复杂函数里越看越乱。正确做法是:读懂逻辑后,自己在心里给这些变量重新命名。比如把input读作"用户输入",把sum读作"累加器",把output读作"变换结果"。很多逆向工程师都习惯直接在IDA里按N键修改变量名,这会让伪代码更接近真实意图。

5.2 长度校验为什么要先检查

这道题有个长度校验:strlen(input) > 6。什么意思?只有当输入长度大于6时,才会进入变换逻辑。于是很多人就会想:"那我输入长度小于等于6,是不是就能绕过校验?"不好意思,绕过校验不等于拿到flag。长度校验本身不是安全机制,它的作用是防止缓冲区溢出或者避免无意义的短输入。在CTF题目里,程序作者设置长度限制,更多是为了让循环次数至少能产生6个字符,正好对应目标字符串"199999"的长度。如果你输入"abc",程序根本不会执行比较,自然也没法得到任何提示。

新手容易在这个地方想入非非,觉得长度检查是个可以利用的漏洞。实际上就这道题而言,你没有必要绕过它,只需要理解"长度必须大于6,循环才会跑出足够的字符去和目标比较"。

5.3 32位程序在64位系统上的调试注意

Mysterious是32位PE,如果你用64位版本的OllyDbg直接打开,会提示无法加载或者各种异常。解决方法是:

  • 用x64dbg的话,它会自动识别并切换到32位调试模式;
  • 用OllyDbg的话,请使用32位版本(OllyDbg本身就是32位调试器);
  • 用IDA分析时,32位程序就用IDA 32位版本,或者确认你安装的IDA同时支持反编译32位PE。

还有一个容易忽略的点:在64位Windows上,32位程序的系统目录是SysWOW64,不是System32。假如你之后想下断点拦截API,注意断点地址可能是user32.dll等系统库的32位版本,如果你不慎加载了64位DLL去下断,断点大概率不生效。

5.4 不要在"逆输入"上钻牛角尖

我见过一些新手卡在这道题上,倒不是因为找不到代码,而是因为找到了"199999"之后,非要反推出一个能通过校验的输入字符串,觉得"只有输入正确的密码才叫破解"。其实CTF逆向的交付物是flag,不是"密码"。程序数据段里藏着的明文常量,往往就是flag的载体;即使它是一个变换结果而不是输入,题目想考察的知识点也已经完成了——你能不能从代码追踪到那个常量。

当然,如果你眼下就是想练习反向推导,也可以动手算算:要让前缀和的个位依次是1、9、9、9、9、9,第一个输入的ASCII码个位必须是1,然后逐步调整后续字符,使累加和的个位每次都落在9上。这是一个很有趣的小数学游戏,但对解题速度没有帮助,做完记得把注意力拉回flag上就好。

写在最后的一点个人经验

Mysterious这道题,是我印象中非常适合逆向新手练手的小程序:它没有壳,没有反调试,没有虚拟机保护,逻辑也就二十来行。你把它的完整分析流程走一遍,就相当于把"运行观察→查壳→导入表定位→反编译→算法还原→动态验证"这条常规链路练熟了。之后遇到再复杂的Windows程序,套路还是这一套,只是每个环节的深度会加大而已。

我个人更想强调的是:别老想着背答案,而是要背流程和思路。比如"先看导入表里有哪些敏感API""字符串窗口永远是第一情报源""WM_COMMAND的低16位是控件ID"这类经验,才是能迁移到下一道题的东西。你如果能把Mysterious从头到尾独立复现一遍,甚至不看任何writeup,那你离能独立啃下中等难度逆向题也就不远了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询