Skip to content

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 追踪

  1. 运行程序:看界面,找到目标按钮。

  2. 获取 ID:用 Resource Hacker 或 Spy++ 查看按钮的 Control ID (例如 1001)。

  3. 搜十六进制:在 IDA 中搜 0x3E9。

  4. 找结构体:在数据段找到消息映射表,直接跳转到处理函数。

API 断点

如果找不到 ID,或者 ID 是动态生成的。

  1. 下断点:在 x64dbg/OllyDbg 中,对读取输入的关键 API 下断点。

    • GetWindowTextW / GetWindowTextA

    • GetDlgItemTextW

  2. 触发事件:点击按钮。

  3. 栈回溯:程序断下后,查看调用堆栈 (Call Stack)。会发现是“用户代码”调用了这些 API。

字符串搜索(通用)

  1. 搜提示语:搜 "Success", "Failed", "Wrong Flag"。

  2. 交叉引用:看是谁引用了这些字符串。

  3. 注意编码:MFC 程序默认使用 Unicode(宽字符),字符串在内存中是 W.r.o.n.g(每个字母中间有个 00),IDA 默认只显示 ASCII,需要右键 Setup 开启 Unicode 才能搜到。