Win GUI
从“面向过程”的逆向思维,转变为“面向事件”的逆向思维。
第一部分:核心思维的转变(控制台 vs GUI)
在逆向 C 语言写的控制台程序(Console App)时,思维是线性的:
-
程序结构:
main()是起点。 -
执行流: 第一行 -> 第二行 -> 函数调用 -> 结束。
-
逆向策略: 找到
main,F5 一看,逻辑全在里面。
但在 GUI(图形用户界面)程序 中,这种思维完全失效。
1. 它是“事件驱动”的 (Event-Driven)
GUI 程序启动后,绝大部分时间在挂起等待。它不会主动执行逻辑,而是等待用户操作。
-
点一下鼠标 -> 产生一个消息 (Message)。
-
按一下键盘 -> 产生一个消息。
-
程序内部有一个死循环(消息循环),不断抓取这些消息,然后分发给对应的函数去处理。
第二部分:MFC
1. MFC 的本质
Windows 原生的 API(Win32 API)写界面非常繁琐。为了创建一个窗口,可能要写 100 行代码。
微软为了方便开发者,用 C++ 的类 (Class) 把这些繁琐的 C 语言 API 封装了起来,这就是 MFC。
2. 静态链接
在 CTF 中,为了保证程序在任何选手的电脑上都能运行,出题人通常选择静态链接 MFC 库。
这意味着:微软写好的几百万行 MFC 库代码,全部被塞进了这个 EXE 里。
- 99% 的代码是微软写的库代码(处理窗口绘制、鼠标移动、内存管理),只有 1% 是作者写的核心逻辑(Flag 验证)。
3. main 函数
MFC 封装得非常深,它把 WinMain(Windows 程序的真实入口)隐藏在库内部了。程序一启动,MFC 会先初始化一个全局的 CWinApp 对象。
- 在 MFC 逆向中,找
main函数毫无意义,因为真正的逻辑散落在各个“事件处理函数”里。
第三部分:关键机制——消息映射 (Message Map)
这是你刚才解题的关键。因为没有 main 函数串联逻辑,MFC 必须有一种机制:当用户点击 ID 为 1001 的按钮时,应该执行哪个函数?
这个机制就是 消息映射表。
1. 工作机制
开发者写 MFC 代码时,会写类似这样的宏:
BEGIN_MESSAGE_MAP(CMyDialog, CDialog)
ON_BN_CLICKED(1001, OnBnClickedCheck) // 当 ID 1001 被点击,执行 OnBnClickedCheck
END_MESSAGE_MAP()
2. 编译后的样子
编译器会把上面的宏,转换成内存中的一个结构体数组(通常在 .rdata 段)。这个结构体大概长这样:
| 字段 (Field) | 含义 | 你刚才看到的 |
|---|---|---|
| Message ID | 消息类型 | 0x111 (即 WM_COMMAND,代表按钮/菜单被触发) |
| Control ID | 控件 ID | 0x3E9 (即 1001,按钮的身份证号) |
| Code | 通知码 | (通常是 0 或特定的操作码) |
| Function Ptr | 函数指针 | offset sub_70BBAB (这就是处理逻辑!) |
第四部分:GUI 逆向的三种核心方法
基于以上知识,总结出针对 Windows GUI 程序的逆向方法论:
ID 追踪
-
运行程序:看界面,找到目标按钮。
-
获取 ID:用 Resource Hacker 或 Spy++ 查看按钮的 Control ID (例如 1001)。
-
搜十六进制:在 IDA 中搜
0x3E9。 -
找结构体:在数据段找到消息映射表,直接跳转到处理函数。
API 断点
如果找不到 ID,或者 ID 是动态生成的。
-
下断点:在 x64dbg/OllyDbg 中,对读取输入的关键 API 下断点。
-
GetWindowTextW/GetWindowTextA -
GetDlgItemTextW
-
-
触发事件:点击按钮。
-
栈回溯:程序断下后,查看调用堆栈 (Call Stack)。会发现是“用户代码”调用了这些 API。
字符串搜索(通用)
-
搜提示语:搜 "Success", "Failed", "Wrong Flag"。
-
交叉引用:看是谁引用了这些字符串。
-
注意编码:MFC 程序默认使用 Unicode(宽字符),字符串在内存中是
W.r.o.n.g(每个字母中间有个 00),IDA 默认只显示 ASCII,需要右键 Setup 开启 Unicode 才能搜到。