摘要:关于在windows中对消息的处理,以及在逆向中的应用。所用到的所有代码在附件。

窗口

windows程序是由一系列的窗口构成的,每个窗口都有自己的窗口过程。窗口过程(WndProc)就是一个函数。当窗口接受到消息,windows会把这个消息自动传递给WndProc函数。然后函数再根据接收到的消息进行处理就行了。

//窗口注册函数
ATOM WINAPI RegisterClass( _In_ const WNDCLASS *lpWndClass);

窗口注册函数参数:

typedef struct _WNDCLASS {
UINT style;// 窗口样式
WNDPROC lpfnWndProc; //设置窗体接收windws消息函数(窗口过程函数)
int cbClsExtra;//窗口扩展
int cbWndExtra;//窗口实例扩展
HINSTANCE hInstance;//窗体实例名,由windows自动分发
HICON hIcon;//窗口的最小化图标
HCURSOR hCursor;//窗口鼠标光标
HBRUSH hbrBackground;//窗口背景色
LPCTSTR lpszMenuName;//窗口菜单
LPCTSTR lpszClassName;// 窗口类名
} WNDCLASS, *LPWNDCLASS;
//窗口过程函数
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM)

窗口类型:

  • 可重叠窗口(Overlapped Window),
  • 弹出窗口(Pop-up Window),
  • 子窗口(Child Window)

线程

一个进程至少拥有一个线程,称为主线程。如果一个线程创建了窗口,拥有GUI资源,那么也称该线程为GUI线程,否则就为工作线程。窗口是由线程创建的,创建窗口的线程就拥有该窗口。这种线程拥有关系的概念对窗口有重要的意义:建立窗口的线程必须是为窗口处理所有消息的线程。当线程结束或者被强行摧毁时,窗口同样会被摧毁。当窗口被摧毁时,这个时候WndProc会接收到WM_DESTROY消息和WM_NCDESTROY消息。每个线程,如果它建立了窗口,都由系统对它分配一个消息队列。这个队列用于窗口消息的分发(dispatch)。为了使窗口接收这些消息,线程必须有它自己的消息循环,消息循环如下:

    // 消息循环
    MSG msg;
    while (GetMessage(&msg, NULL, 0, 0))
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

应用程序不断的从消息队列中获取消息,然后系统通过DispatchMessage函数分派消息到相应窗口的WndProc,使得消息得到处理。当获取到WM_QUIT消息时,GetMessage返回0,循环结束。

消息

消息,就是指windows发出的一个通知,告诉应用程序某个事情发生了。例如单击鼠标,改变窗口尺寸,按下键盘上的一个键都会使Windows发送一个消息给应用程序,它被定义为:

typedef struct tagMsg
{
       HWND    hwnd;            //接受该消息的窗口句柄
       UINT    message;         //消息常量标识符,也就是我们通常所说的消息号
       WPARAM  wParam;     //32位消息的特定附加信息,确切含义依赖于消息值
       LPARAM  lParam;        //32位消息的特定附加信息,确切含义依赖于消息值
       DWORD   time;            //消息创建时的时间
       POINT   pt;                  //消息创建时的鼠标/光标在屏幕坐标系中的位置
}MSG,*PMSG;

一个消息结构体包含了该事件所有完备信息,当应用程序收到该消息时,就可以做出相应处理了。

消息分类

  1. 队列消息和非队列消息

    • 从消息的发送途径上看,消息分两种:队列消息和非队列消息。
      队列消息送到系统消息队列,然后到线程消息队列;非队列消息直接送给目的窗口过程。

    这里,对消息队列阐述如下:
    windows维护一个系统消息队列(System message queue),每个GUI线程有一个线程消息队列(Thread message queue)。鼠标、键盘事件由鼠标或键盘驱动程序换成输入消息并把消息放进系统消息队列,例如WM_MOUSEMOVE、WM_LBUTTONUP、WM_KEYDOWN、WM_CHAR等等。windows每次从系统消息队列移走一个的消息,确定他是送给哪个窗口的和这个窗口是由哪个线程创建的,然后,把它放进窗口创建线程的线程消息队列。线程消息队列接收送给该线程所创建窗口的消息。线程从消息队列取出消息,通过Windows把它送给适当的窗口过程来处理。除了键盘、鼠标消息以外,队列消息还有WM_PAINT、WM_TIMER和WM_QUIT。这些队列消息以外的绝大多数消息是非队列消息。

  2. 系统消息和应用程序消息

    • 从消息的来源来看,可以分为:系统定义的消息和应用程序定义的消息。

    系统消息ID的范围是从0到WM_USER-1,或0X80000到0XBFFFF;应用程序消息从WM_USER(0X0400)到0X7FFF,或0XC000到0XFFFF;WM_USER到0X7FFF范围的消息由应用程序自己使用;0XC000到0XFFFF范围的消息用来和其他应用程序通信,为了ID的唯一性,用::RegisterWindowMessage来得到该范围的消息ID。

  3. 窗口消息,命令消息,控件通知消息
    根据处理过程的不同,可以分为三类:窗口消息,命令消息,控件通知消息。

    • 窗口消息
      一般以WM_开头,如WM_CREATE, WM_SIZE, WM_MOUSEMOVE等标准的Windows消息, 用于窗口相关的事件通知,窗口消息将由系统分配到该窗口的窗口过程处理。
    • 命令消息 (WM_COMMAND)
      一种特殊的窗口消息,它从一个窗口发送到另一个窗口以处理来自用户的请求,通常是从子窗口发送到父窗口。例如,点击按钮时,按钮的父窗口会收到WM_COMMAND消息,用以通知父窗口按钮被点击,经测试:子窗口向父窗口发送WM_COMMAND消息,或者称为父窗口会收到WM_COMMAND消息,操作系统并不是通过将WM_COMMAND消息放入到父窗口的消息队列中去,而是直接调用了父窗口的窗口过程(简单来说WM_COMMAND消息不会经过DispatchMessage函数),以 WM_COMMAND 为消息标识参数(UINT uMsg),这个消息会直接被发向WndProc窗口过程,不会经过DispatchMessage函数(也就是不会经过上面的消息循环);
    • 控件通知消息
      WM_NOTIFY消息,当用户与控件交互(Edit, Button...)时,通知消息会从控件窗口发送到父窗口,这种消息的目的不是为了处理用户命令,而是为了让父窗口能够适时的改变控件。

按钮事件的处理流程

按钮事件处理流程

按钮的本质就是一个子窗口,当鼠标点击按钮时,DispatchMessage函数会依次分派513(WM_LBUTTONDOWN), 514(WM_LBUTTONUP) 消息到系统的Winproc函数,然后系统提供的Winproc函数会向我们自己定义的窗口过程WndProc发送一个WM_COMMAND消息。下面对273(WM_COMMAND)消息进行具体分析,示例程序 windows消息2.exe。

  1. 首先是WndProc函数的堆栈结构

    WndProc的堆栈结构.jpg

  2. 在窗口过程WndProc处下条件断点

    WndProc条件断点.jpg

  3. 当左键单击按钮时会触发断点,此时堆栈内容如下

    WndProc堆栈参数分析.jpg

    此时hwnd=0x130BE0,uMsg=0x111,wParam=0x3E9,lParam=0x50C62。其中hwnd代表父窗口的句柄也就是消息是发给父窗口的,uMsg代表(273)WM_COMMAND的十六进制,wParam代表的是按钮需要做什么,lParam代表按钮的句柄也就是消息从哪来。(注意:鼠标点击按钮 在 DispatchMessage函数和窗口过程WndProc可以下514(WM_LBUTTONUP)条件断点,但有的时候514(WM_LBUTTONUP)不会经过窗口过程WndProc,例如附件中的windows消息.exe和123.exe
    总结起来就是 消息从哪里来,被发送到哪去,需要做什么。

在逆向程序中定位WndProc函数

  1. windows消息.exe:RegisterClassA断点

    windows消息.jpg

    0x401005就是WndProc函数所在的地址。

  2. windows消息2.exe:RegisterClassEx断点

    windows消息2.jpg

    0x401014就是WndProc函数所在的函数地址。可以看到0x401014是个jmp指令,跟进去跳转就是真正的WndProc函数所在的函数地址0x401380。

  3. 123.exe:消息断点+代码段内存读取断点
    123.exe通过RegisterClassA断点的方式已经行不通了,会发现Winproc函数是一个系统地址

    windows消息3.jpg

    原因是因为CallWindowProc函数(但是诡异的是当同时在WndProc和CallCallWindowProc下例如0x202断点时,先在WndProc断一次,然后才在CallWindowProc断一次。此外0x101断点不会经过CallwindowProc,回直接发送给窗口过程函数),WNDCLASS结构体中的地址其实只是一个跳转地址,程序是通过CallWindowProc函数调用窗体过程函数。CallWindowProc是将消息信息传送给指定的窗口过程的函数。使用函数CallWindowsProc可进行窗口子分类。通常来说,同一类的所有窗口共享一个窗口过程。子类是一个窗口或者相同类的一套窗口,在其消息被传送到该类的窗口过程之前,这些消息是由另一个窗口过程进行解释和处理的。CallWindowProc函数原型如下:

    LRESULT CallWindowProc(WNDPROC lpPrevWndFunc, HWND hWnd, UINT Msg, WPARAM wParam, LPARAM IParam);
    //hWnd:指向接收消息的窗口过程的句柄。
    //Msg:指定消息类型。
    //wParam:指定其余的、消息特定的信息。该参数的内容与Msg参数值有关。
    //IParam:指定其余的、消息特定的信息。该参数的内容与Msg参数值有关。
    //返回值:返回值指定了消息处理结果,它与发送的消息有关。

    换个思路,在按钮上下一个消息断点,然后运行到消息断点后,然后再在程序的代码段下一个内存访问断点。
    在按钮上下消息断点,然后F9

    windwos消息31.jpg

    在程序的代码段下一个内存访问断点,然后F9
    windows消息32.jpg

    然后就到了WndProc函数

    widnwos消息33.jpg

    0x45AB89就是WndProc函数所在的函数地址。

从WndProc筛选出具体的按钮事件

参照上面的按钮事件的处理流程,然后在WndProc函数的入口地址下载条件断点[esp+8]==0x111,然后F9,删除条件断点,然后在堆栈里的wParam参数上下内存访问断点即可。下面以windows消息2.exe举例。

在在WndProc函数的入口地址下载条件断点[esp+8]==0x111,并F9

按钮事件1.jpg

删除条件断点并在堆栈里的wParam参数上下内存访问断点

按钮事件2.jpg

F9运行就可以定位到具体的按钮触发的事件

按钮事件3.jpg

缺点 无法跟踪CallWindowProc函数。

常用api断点

user32.dll CallWindowProcA [esp+8]==0x202

消息断点1.jpg

user32.dll DispatchMessageA [[esp]+4]==0x202

消息断点2.jpg

实验代码

代码:实验资料.rar

文章目录