序言
在阅读代码书籍的时候,看到了很多骚操作,但是要想掌握其中的原理和手法,还是需要大量底层代码和逻辑的支撑。
所谓知己知彼,方能百战百胜。
但是我上手 MFC 的时候,我根本就看不懂,什么是单文档,多文档有什么区别,我只会最傻瓜式的图形化操作。
浏览了一圈之后发现,单文档应该是最简单的,但是创建之后我还是一点都看不懂。
后来发现 MFC 是 Win32 API 的封装框架,那么咱们直接去研究 Win32 API,把它的父类拿捏了,MFC 不就轻轻松松了吗?
根据微软 Win32 API 开发文档,我创建了一个最原始的 Win32 窗口程序。
Win32窗口程序
我们研究这个 Win32 程序,最主要关注的就是它的 WNDCLASS 结构体和 WindowProc 方法,这两个才是今天的重点。
WNDCLASS 里面的 lpfnWndProc 保存了 WindowProc 的地址,而我们重点研究的就是 WindowProc。
它作为消息分发处理函数十分重要,但是让人捉摸不透的是,它是一个回调函数。
回调函数比较麻烦,因为它的调用关系并不直观,很多时候不好直接定位它的位置。
在这个 Win32 程序中还比较简单,因为我们把 WindowProc 写在了 wWinMain 外部,通过 IDA 可以轻松定位到这个函数。
#include <windows.h>
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam){
switch (uMsg){
case WM_DESTROY:
PostQuitMessage(0);
return 0;
case WM_PAINT:{
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hwnd, &ps);
FillRect(hdc, &ps.rcPaint, (HBRUSH)(COLOR_WINDOW + 1));
EndPaint(hwnd, &ps);
}
return 0;
}
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPWSTR lpCmdLine, int nShowCmd) {
const wchar_t CLASS_NAME[] = L"Sample Window Class";
/*
WNDCLASS
*/
WNDCLASS wc = { };
wc.lpfnWndProc = WindowProc;
wc.hInstance = hInstance;
wc.lpszClassName = CLASS_NAME;
RegisterClass(&wc);
struct _win {int width;int height;int x;int y;} win = { 500,350,GetSystemMetrics(SM_CXSCREEN) / 2 - (win.width / 2), GetSystemMetrics(SM_CYSCREEN) / 2 - (win.height / 2) };
HWND hWnd = CreateWindowExW(0, CLASS_NAME, L"窗口", WS_OVERLAPPEDWINDOW, win.x, win.y, win.width, win.height, NULL, NULL, hInstance, NULL);
ShowWindow(hWnd, nShowCmd);
MSG msg = { };
while (GetMessage(&msg, NULL, 0, 0) > 0){
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return 0;
}
MFC应用程序
当我已经拿下来 Win32 程序的时候,我认为 MFC 也是手到擒来了。
但是让我万万没想到的是,MFC 才是噩梦的开始。
我编译了一份 VS 默认的单文档版本 MFC 应用程序,当我准备定位它的 WindowProc 时,我发现,我还是小瞧了回调函数,根本就没有找到。
我在 MFC 的 AfxWinMain 里面想找到 WNDCLASS,这样我就可以定位到 WindowProc。
但是在 MFC 默认的 AfxWinMain 里面,根本就找不到 lpfnWndProc 这个参数。
AfxWinMain 伪代码
__int64 __fastcall AfxWinMain(HINSTANCE__ *hInstance, HINSTANCE__ *hPrevInstance, wchar_t *lpCmdLine, int nCmdShow)
{
unsigned int v8; // r15d
CWinThread *Thread; // r14
CWinApp *m_pCurrentWinApp; // r12
CWnd *m_pMainWnd; // rcx
unsigned int v12; // eax
v8 = -1;
Thread = AfxGetThread();
m_pCurrentWinApp = AfxGetModuleState()->m_pCurrentWinApp;
if ( AfxWinInit(hInstance, hPrevInstance, lpCmdLine, nCmdShow) != 0
&& (m_pCurrentWinApp == nullptr || m_pCurrentWinApp->InitApplication(this: m_pCurrentWinApp) != 0) )
{
if ( Thread->InitInstance(this: Thread) != 0 )
{
v12 = Thread->Run(this: Thread);
}
else
{
m_pMainWnd = Thread->m_pMainWnd;
if ( m_pMainWnd != nullptr )
m_pMainWnd->DestroyWindow(this: m_pMainWnd);
v12 = Thread->ExitInstance(this: Thread);
}
v8 = v12;
}
AfxWinTerm();
return v8;
}
AfxWinInit 伪代码
__int64 __fastcall AfxWinInit(HINSTANCE__ *hInstance, HINSTANCE__ *hPrevInstance, wchar_t *lpCmdLine, int nCmdShow)
{
UINT v7; // eax
AFX_MODULE_STATE *ModuleState; // rax
CWinApp *m_pCurrentWinApp; // rcx
v7 = SetErrorMode(uMode: 0);
SetErrorMode(uMode: v7 | 0x8001);
ModuleState = AfxGetModuleState();
ModuleState->m_hCurrentInstanceHandle = hInstance;
ModuleState->m_hCurrentResourceHandle = hInstance;
m_pCurrentWinApp = AfxGetModuleState()->m_pCurrentWinApp;
if ( m_pCurrentWinApp != nullptr )
{
m_pCurrentWinApp->m_hInstance = hInstance;
m_pCurrentWinApp->m_lpCmdLine = lpCmdLine;
m_pCurrentWinApp->m_nCmdShow = nCmdShow;
CWinApp::SetCurrentHandles(this: m_pCurrentWinApp);
}
if ( AfxGetModuleState()->m_bDLL == 0 )
AfxInitThread();
return 1;
}
WindowProc流程篇
既然找不到我们想要的 lpfnWndProc 参数,那我们只能另辟蹊径了。
在 user32.dll 里面有一个函数 DispatchMessageWorker,它可以看作是 WindowProc 函数上一层消息处理流程中的关键函数。
整体消息流程大概如下:
程序入口
↓
创建窗口
↓
注册窗口类(WNDCLASS/WNDCLASSEX)
↓
CreateWindowEx 创建窗口
↓
进入消息循环
↓
GetMessage / PeekMessage
↓
TranslateMessage
↓
DispatchMessage
↓
WindowProc(窗口过程)
↓
处理 WM_xxx 消息
既然定位不到 WindowProc,那么我们就定位到 DispatchMessageWorker 这个函数。
IDA 反编译 User32!DispatchMessageWorker 精简版:
_int64 __fastcall DispatchMessageWorker(const MSG *lpMsg, DWORD flags)
{
if ( (lpMsg->message & 0xFFFE0000) != 0 ){
v20 = 87;
goto LABEL_47;
}
HWND hwnd = lpMsg->hwnd;
if ( hwnd != nullptr ){
PHWND v5 = (HWND *)ValidateHwnd(a1: hwnd);
PHWND v6 = v5;
if ( v5 == nullptr )
return 0;
lpMsg->hwnd = *v5;
v7 = nullptr;
}
else {
v7 = nullptr;
v6 = nullptr;
}
DWORD message = lpMsg->message;
/*
在此处以下进行了大量删减
*/
if ( (_DWORD)message == 275 || (_DWORD)message == 280 ){
lParam = lpMsg->lParam;
if ( lParam != 0 )
{
if ( (_DWORD)message == 280 )
return NtUserDispatchMessage(a1: lpMsg);
if ( *(_DWORD *)&gfServerProcess == 0 ){
if ( (unsigned int)NtUserValidateTimerCallback(a1: lParam) != 0 ){
if ( v6 != nullptr )
v7 = v6[28];
return UserCallWinProc(
a1: v7,
a2: lpMsg->lParam,
a3: lpMsg->hwnd,
a4: lpMsg->message,
a5: lpMsg->wParam,
a6: (unsigned int)(((unsigned __int64)MEMORY[0x7FFE0004] * MEMORY[0x7FFE0320]) >> 24));
}
return 0;
}
}
}
v17 = UserCallWinProcCheckWow(
a1: v6[28],
a2: v11,
a3: lpMsg->hwnd,
a4: lpMsg->message,
a5: lpMsg->wParam,
a6: lpMsg->lParam);
v33 = v17;
lpMsg->wParam = wParam;
return v17;
}
从 DispatchMessageWorker 中可以发现,UserCallWinProcCheckWow 和 UserCallWinProc 十分类似,它们很可能就是调用 WindowProc 回调函数的位置。
根据调用所需要的参数,我们大胆猜测v6[28] 和 v7 就是回调函数的地址,向上查找 v7 = v6, v6 = v5, v5是ValidateHwnd函数的返回值,所以关键点就在ValidateHwnd
ValidateHwnd 简略模型代码:
PHWND ValidateHandle(HANDLE handle, UINT uType){
PUSER_HANDLE_ENTRY pEntry = GetUser32Handle(handle);
if (g_ObjectHeapTypeShared[uType])
ret = SharedPtrToUser(pEntry->ptr);
else
ret = DesktopPtrToUser(pEntry->ptr);
}
通过代码可以看出,ValidateHwnd 不出意外会给我们返回一个tagWND *这样的结构体。
这个结构体比较复杂,微软在 Windows 7 之前应该存在过部分相关资料,但是之后经过多次修改,所以我们拿到这个结构体之后,需要进入它的内存空间中寻找。
不过有一点可以确定:
这里面肯定存在 WindowProc 相关的函数地址。
需要注意的是,tagWND 属于 Windows 内部结构,并不是公开 API,因此不同 Windows 版本之间字段偏移可能发生变化。
WindowProc实践
用 x64dbg 打开一个 GUI 程序(我仅测试过 Win32、MFC、QT,但是流程应该都是差不多的)。
定位到 DispatchMessageWorker
mov qword ptr ss:[rsp+10],rbx
mov qword ptr ss:[rsp+18],rsi
mov qword ptr ss:[rsp+8],rcx
push rdi
push r12
push r13
push r14
push r15
sub rsp,50
mov r14d,edx
mov rbx,rcx
test dword ptr ds:[rcx+8],FFFE0000
jne user32.7FFDEC88AB97
mov rcx,qword ptr ds:[rcx]
test rcx,rcx
je user32.7FFDEC88A8AA
call <user32.ValidateHwnd>
mov rdi,rax <== 在这里下断点
根据 ValidateHwnd 和 call 指令的特性可以得知,tagWND* 的地址已经存放在 RAX 寄存器里面了。
通过内存查看器查看 RAX 里面的数据:
$ ==> 0000000000CB07B6 <== 程序句柄 hWnd
$+8 0000000000028310
$+10 8000070040020049
$+18 14CFC00020080900
$+20 0000000140000000 <== 程序基址
$+28 0000000000000000
$+30 00000000000010E0
$+38 000000000008F710
$+40 0000000000000000
$+48 00000000000A4000
$+50 000000000006CB40
$+58 0000011E0000011E
$+60 0000051D00000B32
$+68 0000015100000126
$+70 0000051500000B2A
$+78 0000000140006810 <== WindowProc 地址
我是 Windows 11 25H2 26200,根据系统版本的不同,tagWND 的结构也会发生变化。
现在我们只需要在 x64dbg 里面 Ctrl + G 输入 [rax+78]
即可成功跳转到疑似 WindowProc 的位置。
当然也会出现特殊情况。
你可能发现跳转过去的这个地址并不是你熟悉的 WindowProc,也不用太担心。
首先需要核实一下程序句柄是否是当前窗口发送的消息,如果不是,那么这次消息可能和目标 WindowProc 没有直接关系。
其次,如果句柄一致,但是跳转过去之后依然看不懂,这并不是程序出错了。
有很大概率是当前程序拥有自己的消息分发层。
你跳转过去的可能只是当前程序的第一层 WindowProc,后续还会继续进行二次分发。
拿 Qt 举例:
Qt 的社区版和商业版可能使用不同的窗口消息处理流程,但是最终都会经过 Qt 自己的窗口处理模块,例如:qwindows.dll
MFC 也是类似:
静态编译时,WindowProc 相关代码可能位于程序自身地址空间;
动态编译时,可能位于:mfc140ud.dll 地址空间内。
还有一个小技巧。
如果你定位发现干扰因素过于复杂,可以直接定位到DispatchMessageWorker
mov qword ptr ss:[rsp+10],rbx <== 在这里下断点
mov qword ptr ss:[rsp+18],rsi
mov qword ptr ss:[rsp+8],rcx
根据 DispatchMessageWorker 的参数,可以查看 RCX 寄存器。
$ ==> 0000000000CB07B6 <== 程序句柄 hWnd
$+8 0000000000000010 <== 程序消息 uMsg
$+10 0000000000000000 <== 消息参数 wParam
$+18 0000000000000000 <== 消息参数 lParam
通过 RCX 中的数据,可以提前确认是否是目标窗口发出的消息。
这样可以避免在大量无关消息中进行分析。
扩展篇
如果你想进一步深入分析,可以参考下面几个建议:
dbp 文件真的很好用,在巨人的肩膀上会轻松很多。
可以先动态调试到 DispatchMessageWorker,同时打开 IDA 静态分析 user32.dll,两者结合分析。
通过前面的思路跳转到疑似 WindowProc 后,查看符号,确认模块名称和基址。
使用 IDA 打开对应文件,修改一致的基址后,通过 G 快速跳转。
将反编译代码交给 AI 辅助分析,同时结合汇编进行理解。
相信看到这里,你应该已经掌握了如何通过 Windows 消息机制找到 WindowProc 的入口。