目录
一、 Windows消息机制流程
(1) 消息
消息系统对于一个Windows程序来说十分重要,它是一个程序运行的动力源泉。一个消息,是系统定义的一个32位的值,他唯一的定义了一个事件。消息本身是作为一个记录传递给应用程序的,这个记录中包含了消息的类型以及其他信息。例如,对于单击鼠标所产生的消息来说,这个记录中包含了单击鼠标时的坐标。这个记录类型叫做MSG,MSG含有来自windows应用程序消息队列的消息信息,它在Windows中声明如下:
typedef struct tagMSG {
HWND hwnd; //消息所属窗口
UINT message; //消息标识符
WPARAM wParam; //指定消息的附加信息
LPARAM lParam; //指定消息的附加信息
DWORD time; //消息队列中的时间
POINT pt; //鼠标的当前位置
} MSG;
windows中的消息虽然很多,但是种类并不繁杂,大体上有3种:窗口消息、命令消息和控件通知消息。窗口消息是系统中最为常见的消息,它是指由操作系统和控制其他窗口的窗口所使用的消息。例如CreateWindow、DestroyWindow和MoveWindow等都会激发窗口消息。命令消息是一种特殊的窗口消息,他用来处理从一个窗口发送到另一个窗口的用户请求,例如按下一个按钮,他就会向主窗口发送一个命令消息。控件通知消息,是指一个窗口内的子控件发生了一些事情,需要通知父窗口。通知消息只适用于标准的窗口控件如按钮、列表框、组合框、编辑框,以及Windows公共控件如树状视图、列表视图等。例如,单击或双击一个控件、在控件中选择部分文本、操作控件的滚动条都会产生通知消息。其中窗口消息及控件通知消息主要由窗口类即直接或间接由CWND类派生类处理。相对窗口消息及控件通知消息而言,命令消息的处理对象范围就广得多,它不仅可以由窗口类处理,还可以由文档类,文档模板类及应用类所处理。其中,消息用消息标识符进行区分。
从消息的发送途径来看,消息可以分成2种:队列消息和非队列消息,队列消息送到系统消息队列,然后到线程消息队列;非队列消息直接送给目的窗口过程。从处理方式来看,消息队队列可以分成系统消息队列和线程消息队列。系统消息队列由Windows维护,线程消息队列则由每个GUI线程自己进行维护,为避免给non-GUI现成创建消息队列,所有线程产生时并没有消息队列,仅当线程第一次调用GDI函数时系统才给线程创建一个消息队列。
(2) Windows窗体的创建
Windows窗体的创建分为三步:声明WNDCLASS实例、窗体注册、创建窗体。
WNDCLASS是一个由系统支持的结构,用来储存某一类窗口的信息,如ClassStyle,消息处理函数,Icon,Cursor,背景Brush等。声明如下,其中窗体接收Windows消息函数需格外注意。
typedef struct _WNDCLASS {
UINT style; //样式
WNDPROC lpfnWndProc; //设置窗体接收windws消息函数
int cbClsExtra; //窗口类扩展
int cbWndExtra; //窗口实例扩展
HINSTANCE hInstance; //窗体实例名,由windows自动分发
HICON hIcon; //显示上面的图标titlte
HCURSOR hCursor; //窗口光标
HBRUSH hbrBackground; //背景刷
LPCTSTR lpszMenuName; //窗口菜单
LPCTSTR lpszClassName; //窗体类名
} WNDCLASS, *PWNDCLASS;
窗体注册函数是RegisterClass或RegisterClassEx,该函数注册后在调用CreateWindow()或CreatewindowEx函数中使用的窗口类。原型如下:
ATOM WINAPI RegisterClass( _In_ const WNDCLASS *lpWndClass);
//lpWndClass 指向一个 WNDCLASS 结构的指针
窗口创建用到的函数理所是CreateWindow()或CreatewindowEx。这个函数是基于窗口类的,所以还需要指定几个参数来制定特定的窗口。而且像一些不带边框的窗口是怎么创建的也是具有相当的技巧的,就是创建的是不带标题和边框的窗口,然后自己在客户区绘制程序的内容,能够制作个性化的应用程序。函数原型如下,注意函数返回值为创建窗体的句柄,这一点需格外关注。
HWND WINAPI CreateWindow(
_In_opt_ LPCTSTR lpClassName, // 窗口类名称
_In_opt_ LPCTSTR lpWindowName, // 窗口标题
_In_ DWORD dwStyle, // 窗口风格,或称窗口格式
_In_ int x, // 初始 x 坐标
_In_ int y, // 初始 y 坐标
_In_ int nWidth, // 初始 x 方向尺寸
_In_ int nHeight, // 初始 y 方向尺寸
_In_opt_ HWND hWndParent, // 父窗口句柄
_In_opt_ HMENU hMenu, // 窗口菜单句柄
_In_opt_ HINSTANCE hInstance, // 程序实例句柄
_In_opt_ LPVOID lpParam // 创建参数
);
(3).Windows消息的处理
简单讲完消息的定义以及窗体的创建过程,下面开始探究消息如何传递到窗体中。
应用程序中含有一段称作“消息循环”的代码,用来从消息队列中检索这些消息并把它们分发到相应的窗口函数中。Windows为当前执行的每个Windows程序维护一个消息队列。在发生输入事件之后,Windows将事件转换为一个消息并将消息放入程序的消息队列中。程序通过执行一块称之为消息循环的程序代码从消息队列中取出消息。具体执行流程分为三步:消息的发送、消息的接收以及消息的处理。
消息的发送有3种方式:发送、寄送和广播。发送消息的函数有SendMessage、SendMessageCallback、SendNotifyMessage、SendMessageTimeout;寄送消息的函数主要有PostMessage、PostThreadMessage、PostQuitMessage;广播消息的函数我知道的只有BroadcastSystemMessage、BroadcastSystemMessageEx。其中 SendMessage主要是向一个或多个窗口发送一条消息,一直等到消息被处理之后才会返回。不过需要注意的是,如果接收消息的窗口是同一个应用程序的一部分,那么这个窗口的窗口函数就被作为一个子程序马上被调用;如果接收消息的窗口是被另外的线程所创建的,那么窗口系统就切换到相应的线程并且调用相应的窗口函数,这条消息不会被放进目标应用程序队列中。函数的返回值是由接收消息的窗口的窗口函数返回,返回的值取决于被发送的消息。 PostMessage则把一条消息放置到创建hWnd窗口的线程的消息队列中,该函数不等消息被处理就马上将控制返回。
消息的接收主要有3个函数:GetMessage、PeekMessage、WaitMessage。其中GetMessage是从调用线程的消息队列里取得一个消息并将其放于指定的结构。此函数可取得与指定窗口联系的消息和由PostThreadMessage寄送的线程消息。此函数接收一定范围的消息值。GetMessage不接收属于其他线程或应用程序的消息。获取消息成功后,线程将从消息队列中删除该消息。函数会一直等待直到有消息到来才有返回值PeekMessage与GetMessage功能类似,不同之处在于GetMessage不将控制传回给程序,直到从程序的消息队列中取得消息,但是PeekMessage总是立刻传回,而不论一个消息是否出现。WaitMessage功能为线程的消息队列中无其它消息时,该函数就将控制权交给另外的线程,同时将该应用程序挂起,直到一个新的消息被放入应用程序的队列之中才返回。
消息的处理是整个Windows消息处理的重点。典型处理流程如下:
while(GetMessage(&msg, NULL, 0, 0))
{
if(!TranslateAccelerator(msg.hWnd, hAccelTable, &msg))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
首先,GetMessage从进程的主线程的消息队列中获取一个消息并将它复制到MSG结构,如果队列中没有消息,则GetMessage函数将等待一个消息的到来以后才返回。如果将一个窗口句柄作为第二个参数传入GetMessage,那么只有指定窗口的的消息可以从队列中获得。GetMessage也可以从消息队列中过滤消息只接受消息队列中落在范围内的消息。这时候就要利用GetMessage/PeekMessage指定一个消息过滤器。这个过滤器是一个消息标识符的范围或者是一个窗体句柄,或者两者同时指定。当应用程序要查找一个后入消息队列的消息是很有用。WM_KEYFIRST 和 WM_KEYLAST 常量用于接受所有的键盘消息。 WM_MOUSEFIRST 和 WM_MOUSELAST 常量用于接受所有的鼠标消息。然后TranslateAccelerator判断该消息是不是一个按键消息并且是一个加速键消息,如果是,则该函数将把几个按键消息转换成一个加速键消息传递给窗口的回调函数。处理了加速键之后,函数TranslateMessage将把两个按键消息WM_KEYDOWN和WM_KEYUP转换成一个 WM_CHAR,不过需要注意的是,消息WM_KEYDOWN,WM_KEYUP仍然将传递给窗口的回调函数。处理完之后,DispatchMessage函数将把此消息发送给该消息指定的窗口中已设定的回调函数。如果消息是WM_QUIT,则 GetMessage返回0,从而退出循环体。应用程序可以使用PostQuitMessage来结束自己的消息循环。通常在主窗口的 WM_DESTROY消息中调用。通过DispatchMessage函数即将WNDCLASS中的窗体过程函数关联起来。
窗体过程函数是一个用于处理所有发送到这个窗口的消息的函数。任何一个窗口类都有一个窗口过程。同一个类或一个父类的窗口使用同样的窗口过程来响应消息。系统发送消息给窗口过程将消息数据作为参数传递给他,消息到来之后,按照消息类型排序进行处理,其中的参数则用来区分不同的消息,窗口过程使用参数产生合适行为。一个窗口过程不经常忽略消息,如果他不处理,它会将消息传回到执行默认的处理。窗口过程通过调用DefWindowProc来做这个处理。窗口过程被所有属于同一个类的窗口共享,能为不同的窗口处理消息。通常窗口过程函数是通过一个switch语句来实现的,利用HANDLE_MSG消息分流器则可以把switch语句分成更小的函数,每一个消息都对应一个小函数,这样做的好处就是对消息更容易管理。
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
switch (message)
{
HANDLE_MSG(hWnd, WM_COMMAND, MsgCracker);
HANDLE_MSG(hWnd, WM_DESTROY, MsgCracker);
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
int wmId, wmEvent;
PAINTSTRUCT ps;
HDC hdc;
TCHAR szHello[MAX_LOADSTRING];
LoadString(hInst, IDS_HELLO, szHello, MAX_LOADSTRING);
switch (message)
{
case WM_COMMAND:
wmId = LOWORD(wParam);
wmEvent = HIWORD(wParam);
switch (wmId)
{
case IDM_ABOUT:
DialogBox(hInst, (LPCTSTR)IDD_ABOUTBOX, hWnd, (DLGPROC)About);
break;
case IDM_EXIT:
DestroyWindow(hWnd);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
break;
case WM_PAINT:
hdc = BeginPaint(hWnd, &ps);
RECT rt;
GetClientRect(hWnd, &rt);
DrawText(hdc, szHello, strlen(szHello), &rt, DT_CENTER);
EndPaint(hWnd, &ps);
break;
case WM_DESTROY:
PostQuitMessage(0);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}
二、一个简单实例
为分析Windows消息处理过程,首先写了一个最简单的窗体实例作分析。
#include <windows.h>
#include <mmsystem.h>
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); //声名消息处理函数(处理windows和接收windows消息)
//hInstance:系统为窗口分配的实例号,2和3忘了.4是显示方式
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow)
{
static TCHAR szAppName[] = TEXT("Register"); //窗体名
HWND hwnd;//句柄
MSG msg;//消息体
WNDCLASS wndclass;//这义一个窗体类实例
//设置窗体参数
wndclass.style = CS_HREDRAW | CS_VREDRAW; //样式
wndclass.cbClsExtra = 0;
wndclass.cbWndExtra = 0;
wndclass.hInstance = hInstance;//窗体实例名,由windows自动分发
wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION);//显示上面的图标titlte
wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);//窗口光标
wndclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);//背景刷
wndclass.lpszMenuName = NULL;
wndclass.lpfnWndProc = WndProc;//设置窗体接收windws消息函数
wndclass.lpszClassName = szAppName;//窗体类名
if (!RegisterClass(&wndclass))//注册窗体类
{
MessageBox(NULL, TEXT("This program requires Windows NT!"), szAppName, MB_ICONERROR);
return 0;
};
//创建一个窗体。已分配内存。返回一个窗体句柄
hwnd = CreateWindow(szAppName, // window class name
TEXT("The Hello Program"), // window caption
WS_OVERLAPPEDWINDOW, // window style
CW_USEDEFAULT,// initial x position
CW_USEDEFAULT,// initial y position
CW_USEDEFAULT,// initial x size
CW_USEDEFAULT,// initial y size
NULL, // parent window handle
NULL, // window menu handle
hInstance, // program instance handle
NULL);
ShowWindow(hwnd, iCmdShow);//显示窗口
UpdateWindow(hwnd);//更新窗体
while (GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);//翻译消息并发送到windows消息队列
DispatchMessage(&msg);//接收信息
}
return msg.wParam;
}
LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)//消息的处理程序
{
HDC hdc;
PAINTSTRUCT ps;
RECT rect;
switch (message)
{
case WM_CREATE:
return 0;
case WM_PAINT:
hdc = BeginPaint(hwnd, &ps);
TextOut(hdc, 0, 0, "TEST", strlen("TEST"));
EndPaint(hwnd, &ps);
return 0;
case WM_DESTROY:
PostQuitMessage(0);
return 0;
}
return DefWindowProc(hwnd, message, wParam, lParam);
}
这段代码实现的是一个最简单的窗体。效果如下:
下面用OD进行分析。首先观察窗体创建过程,对RegisterClass和CreateWindow这两个模块间调用下断点,注意观察C代码,该窗体类名为"Register”。
数据窗口选中的即为WNDCLASS结构体。最后四字节为窗体类名地址,地址为0xF3A000,数据窗口跟随查看,可知结果符合,结果如下所示:
窗体处理函数地址为WNDCLASS结构体的第二个四字节,为0XF31352。在该函数处下断点。继续向下调试。
从堆栈段可以看到相应参数,其中窗体类名为Register。往下调试一步:
寄存器EAX返回值即为句柄值,0XB1346。查看窗体:
可以看到第一行的句柄、类名均符合。继续F9往下调试。
可发现程序停在0XF31352处,即可说明该处为窗体函数入口。
三、更深入一步分析
刚刚的小例子只是简单进行了分析,并没有深入分析,尤其是窗体处理函数的定位。下面我以一个CrackMe为例子进行分析。在吾爱论坛上面找了一个非常简单的CM进行分析(无复杂算法、无壳)。需要输入正确的注册码然后点击按钮进行破解。效果如下:
这个CM本身难度不大,通过简单的搜索字符串即可破解,但为了详细分析消息机制,我们从正面进行分析。
按照常规套路,对RegisterClass和CreateWindow这两个模块间调用下断点:
观察两个RegisterClass函数中WNDCLASS结构体,发现三个窗体的窗体过程函数的地址均为0X77782280,且该代码位于系统代码段内(ntdll.dll中),与直观认识不符,这个是为什么呢?首先解决第一个问题,即两个窗体注册函数的窗体过程函数为什么一样,这是因为一个窗口类只有一个窗口函数,所有使用该窗口类创建的窗口都是使用同一个窗口函数,除非后来用SetWindowLong改变了窗口函数。查看窗口如下,可以看到窗口均是使用WTWindow创建的。
第二个问题就是为什么窗体过程函数的地址会位于系统代码段内,这是因为程序不是直接跳转至窗体过程函数,而是通过user32.CallWindowProc这个函数来调用窗体过程函数,这个后面结合分析再具体讲解。注意观察类名为"按钮"的窗体创建时返回的句柄值,该值比较重要,后面会用到。
完成窗体注册和窗体创建,下面就是消息处理过程。对TranslateMessage和DispatchMessage函数下断点。
用IDA看可能会更直观:
回到DispatchMessage函数,函数原型如下:
LONG DispatchMessage(CONST MSG*lpmsg)
//lpmsg:指向含有消息的MSG结构的指针。
由MSG结构可知,第一个四字节为窗体句柄,第二个四字节为消息标识符,查看数据段可知,句柄为0X371394,与预期相符;消息标识符为0X202。查表可知,0X202表示按钮松开消息,与操作符合。考虑到该CM的触发条件为按钮,因此可以在此设立条件断点:
以上均是常规分析,现在重点来了,如何定位真正的窗体处理函数用以破解该CM?
解决这个问题的关键就在于CallWindowProc函数,WNDCLASS结构体中的地址其实只是一个跳转地址,程序是通过CallWindowProc函数调用窗体过程函数。CallWindowProc是将消息信息传送给指定的窗口过程的函数。使用函数CallWindowsProc可进行窗口子分类。通常来说,同一类的所有窗口共享一个窗口过程。子类是一个窗口或者相同类的一套窗口,在其消息被传送到该类的窗口过程之前,这些消息是由另一个窗口过程进行解释和处理的。CallWindowProc函数原型如下:
LRESULT CallWindowProc(WNDPROC lpPrevWndFunc, HWND hWnd, UINT Msg, WPARAM wParam, LPARAM IParam);
//hWnd:指向接收消息的窗口过程的句柄。
//Msg:指定消息类型。
//wParam:指定其余的、消息特定的信息。该参数的内容与Msg参数值有关。
//IParam:指定其余的、消息特定的信息。该参数的内容与Msg参数值有关。
//返回值:返回值指定了消息处理结果,它与发送的消息有关。
既然如此,我们在回调函数地址0X77782280与CallWindowProc函数出均设置断点(也可以利用DefWindowProc设置断点,方法类似,但断点条件不一样),当消息标识符为0X202时激活这两个断点(CallWindowProc调用位置较多,需判断哪个才是需要的),利用Run逐步跟踪,记录两个断点内执行过程。结果共计1568条汇编代码。按照正序分析,重点关注应用程序段代码的起始位置代码:
其中0X45AB89处以上代码均为系统代码段,不用关心。利用IDA跟踪0X45AB89至代码段:
这个结构有没有很熟悉,有没有豁然开朗的感觉?下面来逐个解释。DefWindowProc函数是调用缺省的窗口过程来为应用程序没有处理的任何窗口消息提供缺省的处理,确保每一个消息得到处理。CWnd是MFC窗口类的基类,提供了微软基础类库中所有窗口类的基本功能。我们在0X45AB89处设端点,当消息标识符为0X202触发。
详细分析过程可以总结出如下消息过程:
其中消息标识符为0X111标识消息WM_COMMAND。当用户从菜单选中一个命令项目、当一个控件发送通知消息给去父窗口或者按下一个快捷键将发送 WM_COMMAND 消息,窗口则通过WindowProc函数收到此消息。断点条件可更改为0X111。
可见第一个参数为0X371394,与之前窗体创建系统分配的句柄相同,第二个参数为0X111,与消息标识符相同。说明了我们判断的正确性。消息通过系统段代码后执行至0X45AB89处,并将参数传入。
接下来我们继续分析sub_45AB89函数。sub_45AB04函数如下:
afxMapHWND是用于保存CWnd类和HWND之间的映射。在Windows体系中,很多对象都是以句柄的形式展示给开发人员的。比如窗口句柄(HWND),绘图设备(HDC)等等。然后大部分的API函数则围绕 这些句柄做文章。比如ShowWindow,SetWindowText, TextOut等等。这些API函数的第一个参数通常就是句柄了。但是在C++ 体系中,这种对于事物细节的访问,往往是有违其封装精神的。因此MFC做了很多的封装类,来隐藏这些细节。应运而生就是CWnd,CDC等类。通过这些类暴露的方法,可以直接对句柄做操作,而又可以不去关心他。MFC中有大量的全局变量,其中一个全局变量是一张HWND与CWnd的Map表。MFC提供了全局函数afxMapHWND用于获得这个Map表。虽然在MFC中,都是对象在与对象打交道。但是MFC也要与Windows系统打交道。Windows给你的只有句柄,那么如何通过这些句柄找到相对应的类呢?通过Map表就能轻松的解决这个问题。比如在Windows的消息机制中,当WndProc接收到一个消息的时候,只会得到一个HWND hWnd的目标窗口,如何找到匹配的类?从m_pmapHWND中搜索就行了。通过调用CWnd的静态成员函数FromHandlePermanent,我们就能轻松的从Map表中找到与hWnd相对应的CWnd类。FromHandlePermanent的实现也非常简单。首先通过afxMapHWND找到m_pmapHWND,然后通过m_pmapHWND的成员函数LookupPermanent查找与hWnd对应的CWnd指针,最后返回他。CMapPtrToPtr::GetValueAt则用于获取具体值。
我们利用OD在0X45A912处下断点,查看参数值。
可以看到v5[7] =0X321336,符合预期。下面利用IDA再重点分析sub_45A912函数:
红色圈出是需要重点关注的关键函数。利用OD在该处设置断点,观察函数与参数。由此可以猜测,Cwnd为一地址值,通过该地址加上偏移找寻函数地址。
可见函数为0X45B940,第二个参数为消息标识符0X111,第三第四个参数为句柄,0X321336。利用IDA观察sub_45B940:
至此已基本定位关键处理函数,而后按部就班的逐步分析就可以准确定位判断语句。