首页
论坛
课程
招聘
[原创]Windows内核学习笔记之消息机制
2022-1-10 14:32 4402

[原创]Windows内核学习笔记之消息机制

2022-1-10 14:32
4402

一.Windows子系统

Windows子系统是Windows操作系统不可分割的一部分,它在Windows内核的基础上,为应用程序提供了一个图形用户界面(GUI)环境。

 

Windows子系统结构如下图所示,该子系统提供了模块结构,供应用程序直接调用的API函数位于一组子系统DLL中,这些DLL在根据需要调用内核模块组件(win32k.sys)的功能。这些子系统DLL有时候也被简单地统称为系统DLL。

 

avatar

 

Windows子系统既有用户模块组件,也有内核模块组件;既有注入到每个应用程序进程中的组件模块,也有独立的进程空间。下面列出这些组件的职责:

  • 内核模块win32k.sys,虽然它的名称像是一个驱动程序,但实际上,win32k.sys并不遵从I/O管理器定义的程序模型,而仅仅是Windows内核扩展而已

  • 图像设备驱动程序,这是一些与硬件相关的显示驱动程序,打印驱动程序,以及视频小端口驱动程序

  • Windows环境子系统进程(csrss.exe),它包含如下支持:控制台窗口;创建和删除进程和线程;支持16位虚拟DOS机(VDM, Virtual DOS Machine)进程

  • 子系统DLL,例如user32.dll, advapi32.dll, gdi32.dll和kernel32.dll,它们实现了已经文档化的Windows API函数,可能需要调用内核模块ntoskrnl.exe和win32k.sys中未文档化的系统服务,甚至与环境子系统进程通信

其中Win32k.sys包含了两大功能组成部分:

  • 窗口管理器:负责控制窗口显示,管理屏幕输出,收集来自键盘,鼠标和其他设备的输入,以及将用户消息传递给应用程序

  • GDI:这是一个针对图像输出设备的函数库,包含了有关线程,文本和图像绘制,以及操作各种图形的函数

Windows是一个以图形用户界面为主要输入输出手段的操作系统,但是图形界面功能并非位于内核模块ntoskrnl.exe中,而是在Windows子系统中,这是Windows体系结构的一个重要特性。

二.GUI线程

每个线程在创建之初都是采用SSDT的普通线程,而不是GUI线程。但是,当一个非GUI线程调用的系统服务号超出该线程所指的系统服务表数组定义的界限,并且系统服务号的表索引为1时,系统服务分发代码试图将它转换为GUI线程,这是通过调用PsConvertToGuiThread函数来完成的。因此,当一个非GUI线程调用任何一个子系统服务(服务号为0x1000~0x1FFF)时,由于该线程的ServiceTable成员初始指向KeServiceDescriptorTable,而KeServiceDescriptorTable[1]项的系统服务表不包含任务系统服务,所以,此调用必定导致PsConvertToGuiThread函数被调用,从而让该线程转换为GUI线程。

 

在PsConvertToGuiThread函数中,首先会对线程数据进行验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
PAGE:004ADC88 ; _DWORD __stdcall PsConvertToGuiThread()
PAGE:004ADC88 _PsConvertToGuiThread@0 proc near       ; CODE XREF: _KiBBTUnexpectedRange+7↑p
PAGE:004ADC88
PAGE:004ADC88 var_48          = dword ptr -48h
PAGE:004ADC88 var_44          = dword ptr -44h
PAGE:004ADC88 var_40          = dword ptr -40h
PAGE:004ADC88 var_3C          = dword ptr -3Ch
PAGE:004ADC88 var_38          = dword ptr -38h
PAGE:004ADC88 var_34          = dword ptr -34h
PAGE:004ADC88 var_30          = dword ptr -30h
PAGE:004ADC88 var_2C          = dword ptr -2Ch
PAGE:004ADC88 var_28          = byte ptr -28h
PAGE:004ADC88 var_24          = dword ptr -24h
PAGE:004ADC88 var_Process     = dword ptr -20h
PAGE:004ADC88 NewIrql         = byte ptr -19h
PAGE:004ADC88 ms_exc          = CPPEH_RECORD ptr -18h
PAGE:004ADC88
PAGE:004ADC88
PAGE:004ADC88                 push    38h
PAGE:004ADC8A                 push    offset stru_424148
PAGE:004ADC8F                 call    __SEH_prolog
PAGE:004ADC94                 mov     eax, large fs:124h
PAGE:004ADC9A                 mov     esi, eax
PAGE:004ADC9C                 xor     ebx, ebx        ; ebx清0
PAGE:004ADC9E                 cmp     [esi+_KTHREAD.PreviousMode], bl ; 判断先前模式是否为内核模式
PAGE:004ADCA4                 jz      loc_52CA43
PAGE:004ADCAA                 cmp     ds:_PspW32ProcessCallout, ebx ; 判断PspW32ProcessCallout中是否保存函数
PAGE:004ADCB0                 jz      loc_52CA4D
PAGE:004ADCB6                 cmp     [esi+_KTHREAD.ServiceTable], offset _KeServiceDescriptorTable ; 判断线程的调用表是否是SSDT
PAGE:004ADCC0                 jnz     loc_52CA57
PAGE:004ADCC6                 mov     eax, [esi+_KTHREAD.ApcState.Process]
PAGE:004ADCC9                 mov     [ebp+var_Process], eax
PAGE:004ADCCC                 cmp     [esi+_KTHREAD.LargeStack], bl
PAGE:004ADCD2                 jnz     short loc_4ADD18

接下来会调用MmCreateKernelStack函数来创建一个新的大的内核栈,然后调用KeSwitchKernelStack函数来替换内核栈,在调用MmDeleteKernelStack删除原内核栈。在x86平台上,大内核栈大小是0xF000字节,标准的内核栈大小是0x3000字节。内核栈的实际内存分配是按页面计数的,而且还要加上一个不可访问的保护页面,以便检测到内存栈溢出的错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
PAGE:004ADCD4                 xor     eax, eax
PAGE:004ADCD6                 mov     al, [esi+_KTHREAD.InitialNode]
PAGE:004ADCDC                 push    eax
PAGE:004ADCDD                 push    1
PAGE:004ADCDF                 call    _MmCreateKernelStack@8 ; MmCreateKernelStack(x,x)
PAGE:004ADCE4                 mov     edi, eax
PAGE:004ADCE6                 cmp     edi, ebx
PAGE:004ADCE8                 jz      loc_52CA61
PAGE:004ADCEE                 mov     cl, 1           ; NewIrql
PAGE:004ADCF0                 call    ds:__imp_@KfRaiseIrql@4 ; KfRaiseIrql(x)
PAGE:004ADCF6                 mov     [ebp+NewIrql], al
PAGE:004ADCF9                 lea     eax, [edi-3000h]
PAGE:004ADCFF                 push    eax
PAGE:004ADD00                 push    edi
PAGE:004ADD01                 call    _KeSwitchKernelStack@8 ; KeSwitchKernelStack(x,x)
PAGE:004ADD06                 mov     edi, eax
PAGE:004ADD08                 mov     cl, [ebp+NewIrql] ; NewIrql
PAGE:004ADD0B                 call    ds:__imp_@KfLowerIrql@4 ; KfLowerIrql(x)
PAGE:004ADD11                 push    ebx
PAGE:004ADD12                 push    edi
PAGE:004ADD13                 call    _MmDeleteKernelStack@8

调用全局变量PspW32ProcessCallout中保存的WinkProcessCallback函数会创建Windows子系统为它的进程维护的一套平行的数据结构。并通过调用内核的PsSetProcessWin32Process函数,将该结构设置到进程对象偏移0x130处的Win32Process域中

1
2
3
4
5
PAGE:004ADD25                 push    1
PAGE:004ADD27                 push    [ebp+var_Process]
PAGE:004ADD2A                 call    ds:_PspW32ProcessCallout
PAGE:004ADD30                 cmp     eax, ebx
PAGE:004ADD32                 jl      short loc_4ADD4E

将线程的ServiceTable域切换到指向KeServiceDescriptorTableShadow,从而使该线程以后可以使用win32k,sys提供的系统服务

1
PAGE:004ADD34                 mov     [esi+_KTHREAD.ServiceTable], offset _KeServiceDescriptorTableShadow

调用全局变量PspWin32ThreadCallout中保存的Win32kThreadCallback函数创建Windows子系统为每个GUI线程维护的数据结构,该函数通过调用内核PsSetThreadWin32Thread函数将该数据结构设置到线程对象偏移0x130的Win32Thread域中。最后退出函数

1
2
3
4
5
6
7
8
9
10
PAGE:004ADD3E                 push    ebx
PAGE:004ADD3F                 push    esi
PAGE:004ADD40                 call    ds:_PspW32ThreadCallout
PAGE:004ADD46                 cmp     eax, ebx
PAGE:004ADD48                 jl      loc_52CAE5
PAGE:004ADD4E
PAGE:004ADD4E loc_4ADD4E:                             ; CODE XREF: PsConvertToGuiThread()+AA↑j
PAGE:004ADD4E                                         ; PsConvertToGuiThread()+7EDC0↓j ...
PAGE:004ADD4E                 call    __SEH_epilog
PAGE:004ADD53                 retn

将一个内核线程转换为GUI线程,这表明了Windows子系统已接纳该线程及其所属的进程。PspW32ProcessCallout和PspW32ThreadCallout分别执行了当该线程和所属进程加入到Windows子系统中时的初始化工作(当然,当线程或进程结束时,这两个函数也负责相应的清除工作)。一旦线程被接纳到Windows子系统中,它就可以方便地使用Windows子系统提供的两个重要功能:窗口管理和GUI。

 

其中进程对象的Win32Process中保存的结构是_W32PROCESS结构,该结构定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typedef struct _W32PROCESS
{
  LIST_ENTRY ClassList;
  LIST_ENTRY MenuListHead;
  FAST_MUTEX PrivateFontListLock;
  LIST_ENTRY PrivateFontListHead;
  FAST_MUTEX DriverObjListLock;
  LIST_ENTRY DriverObjListHead;
  struct _KBL* KeyboardLayout;
  ULONG Flags;
  LONG GDIObjects;
  LONG UserObjects;
 
  W32HEAP_USER_MAPPING HeapMappings;
  PW32PROCESSINFO ProcessInfo;
} W32PROCESS, *PW32PROCESS;

线程对象的Win32Thread中保存的结构则是_W32THREAD结构,该结构定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
typedef struct _W32THREAD
{
  struct _USER_MESSAGE_QUEUE* MessageQueue;
  LIST_ENTRY WindowListHead;
  LIST_ENTRY W32CallbackListHead;
  struct _KBL* KeyboardLayout;
  struct _DESKTOP_OBJECT* Desktop;
  HANDLE hDesktop;
  DWORD MessagePumpHookValue;
  BOOLEAN IsExiting;
  SINGLE_LIST_ENTRY  ReferencesList;
 
  PW32THREADINFO ThreadInfo;
} W32THREAD, *PW32THREAD;

其中的第一个成员MessageQueue是个指针,指向一个USER_MESSAGE_QUEUE数据结构,该结构就是线程的视窗报文队列。所有发送给线程的消息都会保存在该队列中,线程需要从该队列取出消息才可以完成对消息的处理。

 

USER_MESSAGE_QUEUE结构定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
typedef struct _USER_MESSAGE_QUEUE
{
  /* Reference counter, only access this variable with interlocked functions! */
  LONG References;
 
  /* Owner of the message queue */
  struct _ETHREAD *Thread;
  /* Queue of messages sent to the queue. */
  LIST_ENTRY SentMessagesListHead;
  /* Queue of messages posted to the queue. */
  LIST_ENTRY PostedMessagesListHead;
  /* Queue of sent-message notifies for the queue. */
  LIST_ENTRY NotifyMessagesListHead;
  /* Queue for hardware messages for the queue. */
  LIST_ENTRY HardwareMessagesListHead;
  /* List of timers, sorted on expiry time (earliest first) */
  LIST_ENTRY TimerListHead;
  /* Lock for the hardware message list. */
  KMUTEX HardwareLock;
  /* Pointer to the current WM_MOUSEMOVE message */
  PUSER_MESSAGE MouseMoveMsg;
  /* True if a WM_QUIT message is pending. */
  BOOLEAN QuitPosted;
  /* The quit exit code. */
  ULONG QuitExitCode;
  /* Set if there are new messages specified by WakeMask in any of the queues. */
  PKEVENT NewMessages;
  /* Handle for the above event (in the context of the process owning the queue). */
  HANDLE NewMessagesHandle;
  /* Last time PeekMessage() was called. */
  ULONG LastMsgRead;
  /* Current window with focus (ie. receives keyboard input) for this queue. */
  HWND FocusWindow;
  /* Count of paints pending. */
  ULONG PaintCount;
  /* Current active window for this queue. */
  HWND ActiveWindow;
  /* Current capture window for this queue. */
  HWND CaptureWindow;
  /* Current move/size window for this queue */
  HWND MoveSize;
  /* Current menu owner window for this queue */
  HWND MenuOwner;
  /* Identifes the menu state */
  BYTE MenuState;
  /* Caret information for this queue */
  PTHRDCARETINFO CaretInfo;
 
  /* Window hooks */
  PHOOKTABLE Hooks;
 
  /* queue state tracking */
  WORD WakeMask;
  WORD QueueBits;
  WORD ChangedBits;
 
  /* extra message information */
  LPARAM ExtraInfo;
 
  /* messages that are currently dispatched by other threads */
  LIST_ENTRY DispatchingMessagesHead;
  /* messages that are currently dispatched by this message queue, required for cleanup */
  LIST_ENTRY LocalDispatchingMessagesHead;
 
  /* Desktop that the message queue is attached to */
  struct _DESKTOP_OBJECT *Desktop;
} USER_MESSAGE_QUEUE, *PUSER_MESSAGE_QUEUE;

可以看到该结构中共有七种不同类型的队列,Thread成员则指向了该队列的所属线程。

三.窗口对象

消息最开始是由窗口捕获的,窗口捕获到消息以后,要将消息发送给窗口的所属的线程的消息队列。这样,窗口所属线程才可以从消息队列中取出消息进行处理。因此,要想捕获消息,首先就需要创建一个窗口。

 

在用户层可以通过CreateWindow函数来创建窗口,该函数通过调用win32k.sys的NtUserCreateWindowEx来在内核中创建一个窗口对象,窗口对象的的结构是_WINDOW_OBJECT,该结构的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
typedef struct _WINDOW_OBJECT
{
  /* Pointer to the thread information */
  PW32THREADINFO ti;
  /* Pointer to the desktop */
  PDESKTOP Desktop;
  union
  {
    /* Pointer to a call procedure handle */
    PCALLPROC CallProc;
    /* Extra Wnd proc (windows of system classes) */
    WNDPROC WndProcExtra;
  };
  /* Pointer to another call procedure handle (used for returning the previous
     window proc in SetWindowLongPtr) */
  PCALLPROC CallProc2;
  /* Indicates whether the window is derived from a system class */
  BOOL IsSystem;
  /* Pointer to the window class. */
  PWINDOWCLASS Class;
  /* Extended style. */
  DWORD ExStyle;
  /* Window name. */
  UNICODE_STRING WindowName;
  /* Style. */
  DWORD Style;
  /* Context help id */
  DWORD ContextHelpId;
  /* system menu handle. */
  HMENU SystemMenu;
  /* Handle of the module that created the window. */
  HINSTANCE Instance;
  /* Entry in the thread's list of windows. */
  LIST_ENTRY ListEntry;
  /* Pointer to the extra data associated with the window. */
  PCHAR ExtraData;
  /* Size of the extra data associated with the window. */
  ULONG ExtraDataSize;
  /* Position of the window. */
  RECT WindowRect;
  /* Position of the window's client area. */
  RECT ClientRect;
  /* Handle for the window. */
  HWND hSelf;
  /* Window flags. */
  ULONG Flags;
  /* Window menu handle or window id */
  UINT IDMenu;
  /* Handle of region of the window to be updated. */
  HANDLE UpdateRegion;
  /* Handle of the window region. */
  HANDLE WindowRegion;
  /* Pointer to the owning thread's message queue. */
  PUSER_MESSAGE_QUEUE MessageQueue;
  struct _WINDOW_OBJECT* FirstChild;
  struct _WINDOW_OBJECT* LastChild;
  struct _WINDOW_OBJECT* NextSibling;
  struct _WINDOW_OBJECT* PrevSibling;
  /* Entry in the list of thread windows. */
  LIST_ENTRY ThreadListEntry;
  /* Handle to the parent window. */
  struct _WINDOW_OBJECT* Parent;
  /* Handle to the owner window. */
  HWND hOwner;
  /* DC Entries (DCE) */
  PDCE Dce;
  /* Property list head.*/
  LIST_ENTRY PropListHead;
  ULONG PropListItems;
  /* Scrollbar info */
  PWINDOW_SCROLLINFO Scroll;
  LONG UserData;
  BOOL Unicode;
  WNDPROC WndProc;
  PETHREAD OwnerThread;
  HWND hWndLastPopup; /* handle to last active popup window (wine doesn't use
pointer, for unk. reason)*/
  PINTERNALPOS InternalPos;
  ULONG Status;
  /* counter for tiled child windows */
  ULONG TiledCounter;
  /* WNDOBJ list */
  LIST_ENTRY WndObjListHead;
} WINDOW_OBJECT; /* PWINDOW_OBJECT already declared at top of file */
名称 作用
MessageQueue 指向的是一个报文队列,一般就是所属线程的报文队列,也就是上面受得包含七种不同类型的USER_MESSAGE_QUEUE类型的报文队列。一个线程只有一个报文队列,但是一个线程可以由多个视窗,如果那样则每个视窗的指针MessageQueue都指向同一个报文队列,就是其所属线程的报文队列。
WndProc 函数指针,每个窗口接收到一个报文,在要对其做出反应的时候,就需要调用窗口的这个函数,所以这是视图报文的处理函数
OwnerThread 指向具体视窗所属线程的ETHREAD数据结构

四.消息的接收

在用户层可以通过GetMessageA或GetMessageW来接收消息,GetMessageW的函数实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
BOOL STDCALL
GetMessageW(LPMSG lpMsg,
        HWND hWnd,
        UINT wMsgFilterMin,
        UINT wMsgFilterMax)
{
  BOOL Res;
  MSGCONVERSION Conversion;
  NTUSERGETMESSAGEINFO Info;
  PUSER32_THREAD_DATA ThreadData = User32GetThreadData();
 
  MsgConversionCleanup(lpMsg, FALSE, FALSE, NULL);
  Res = NtUserGetMessage(&Info, hWnd, wMsgFilterMin, wMsgFilterMax);    // 系统调用
 
  if (-1 == (int) Res)
  {
      return Res;
  }
 
  Conversion.LParamSize = Info.LParamSize;
  Conversion.KMMsg = Info.Msg;    // 接收到的报文
 
  // 将内核模式的报文转换为用户模式
  if (! MsgiKMToUMMessage(&Conversion.KMMsg, &Conversion.UnicodeMsg))
  {
     return (BOOL) -1;
  }
 
  *lpMsg = Conversion.UnicodeMsg;    // 返回转换好的用户模式报文
  Conversion.Ansi = FALSE;
  Conversion.FinalMsg = lpMsg;
  MsgConversionAdd(&Conversion);
 
  if (Res && lpMsg->message != WM_PAINT && lpMsg->message != WM_QUIT)
  {
     ThreadData->LastMessage = Info.Msg;    // 复制内核模式报文
  }
 
  return Res;
}

参数lpMsg是个指针,用来返回获取的报文,报文是通过系统调用NtUserGetMessage从内核获取的,但是从内核获取的报文是“内核模式”的报文,与用户空间实际使用的报文在格式上有所不同,所以要经过MsgiKMToUMMessage函数将其转换为"用户模式"的报文。视图子系统的保卫呢是针对窗口而不仅仅是线程或进程的,所以在获取报文时需要说明是哪一个窗口,参数hWnd就是一个已创建(或已打开)窗口的句柄。其余两个参数就不那么重要了,那是用来选择所获取报文的类型范围的。

 

系统调用NtUserGetMessage从内核获取报文时,需要准备好NTUSERGETMESSAGEINFO数据结构,让内核往里面填写,该结构定义如下:

1
2
3
4
5
typedef struct tagNTUSERGETMESSAGEINFO
{
  MSG Msg;
  ULONG LParamSize;
} NTUSERGETMESSAGEINFO, *PNTUSERGETMESSAGEINFO;

这里Msg是个MSG数据结构,所谓窗口报文,实际上就是MSG数据结构,结构定义如下:

1
2
3
4
5
6
7
8
typedef struct tagMSG {
    HWND hwnd;
    UINT message;
    WPARAM wParam;
    LPARAM lParam;
    DWORD time;
    POINT pt;
} MSG,*LPMSG,*PMSG;

可见报文是以具体的窗口为目标的,但是MSG结构中并没有用来挂入队列的指针或队列头。实际上,挂入报文队列的是USER_MESSAGE数据结构,而MSG是其内部的核心成员,该结构的定义如下:

1
2
3
4
5
6
typedef struct _USER_MESSAGE
{
  LIST_ENTRY ListEntry;
  BOOLEAN FreeLParam;
  MSG Msg;
} USER_MESSAGE, *PUSER_MESSAGE;

在系统调用NtUserGetMessage中会通过调用co_IntPeekMessage从消息队列中获取消息

1
2
3
4
5
6
do
 {
    GotMessage = co_IntPeekMessage(&Msg, hWnd, MsgFilterMin, MsgFilterMax, PM_REMOVE);
    // 省略以下代码
 }
 while (! GotMessage);

在co_IntPeekMessage中会通过线程对象的Win32Thread来获取线程的消息队列,增加删除队列的标志,然后调用co_MsgDispatchOneSentMessage来首先循环获取和处理SendMessage队列中的消息

1
2
3
4
5
ThreadQueue = (PUSER_MESSAGE_QUEUE)PsGetCurrentThreadWin32Thread()->MessageQueue;
 
RemoveMessages = RemoveMsg & PM_REMOVE;
 
while (co_MsqDispatchOneSentMessage(ThreadQueue));

通过SendMessage发送的消息是针对线程的,而不是针对某个窗口的。因此,在这种情况下,线程的消息循环必须直接处理它们,而不是交给DispatchMessage来处理。而且,这种通过消息来实现跨进程通信的方式仅限于同属一个桌面的进程之间才可以进行

 

当处理完成SendMessage的消息以后,才开始从其他队列中获取消息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
/* Now check for normal messages. */
  Present = co_MsqFindMessage(ThreadQueue,
                              FALSE,
                              RemoveMessages,
                              hWnd,
                              MsgFilterMin,
                              MsgFilterMax,
                              &Message);
  if (Present)
  {
     RtlCopyMemory(Msg, Message, sizeof(USER_MESSAGE));
     if (RemoveMessages)
     {
        MsqDestroyMessage(Message);
     }
     goto MessageFound;
  }
 
  /* Check for hardware events. */
  Present = co_MsqFindMessage(ThreadQueue,
                              TRUE,
                              RemoveMessages,
                              hWnd,
                              MsgFilterMin,
                              MsgFilterMax,
                              &Message);
  if (Present)
  {
     RtlCopyMemory(Msg, Message, sizeof(USER_MESSAGE));
     if (RemoveMessages)
     {
        MsqDestroyMessage(Message);
     }
     goto MessageFound;
  }
 
  /* Check for sent messages again. */
  while (co_MsqDispatchOneSentMessage(ThreadQueue))
     ;
 
  /* Check for paint messages. */
  if (IntGetPaintMessage(hWnd, MsgFilterMin, MsgFilterMax, PsGetCurrentThreadWin32Thread(), &Msg->Msg, RemoveMessages))
  {
     Msg->FreeLParam = FALSE;
     return TRUE;
  }
 
  /* Check for WM_(SYS)TIMER messages */
  Present = MsqGetTimerMessage(ThreadQueue, hWnd, MsgFilterMin, MsgFilterMax,
                               &Msg->Msg, RemoveMessages);
  if (Present)
  {
     Msg->FreeLParam = FALSE;
     goto MessageFound;
  }

五.消息的分发

用户层通过DispathMessage来完成消息的分发,函数实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
LRESULT STDCALL
DispatchMessageW(CONST MSG *lpmsg)
{
  NTUSERDISPATCHMESSAGEINFO Info;
  LRESULT Result;
 
  Info.Ansi = FALSE;
  Info.Msg = *lpmsg;
  Result = NtUserDispatchMessage(&Info);
  if (! Info.HandledByKernel)
    {
      /* We need to send the message ourselves */
      SPY_EnterMessage(SPY_DISPATCHMESSAGE, Info.Msg.hwnd, Info.Msg.message,
                       Info.Msg.wParam, Info.Msg.lParam);
      Result = IntCallWindowProcW(Info.Ansi, Info.Proc, Info.Msg.hwnd,
                                  Info.Msg.message, Info.Msg.wParam, Info.Msg.lParam);
      SPY_ExitMessage(SPY_RESULT_OK, Info.Msg.hwnd, Info.Msg.message, Result,
                      Info.Msg.wParam, Info.Msg.lParam);
    }
  MsgConversionCleanup(lpmsg, FALSE, TRUE, &Result);
 
  return Result;
}

该首先调用NtUserDispatchMessage内核函数,该函数会将返回值保存到提供的NTUSERDISPATCHMESSAGEINFO结构中,结构体定义如下:

1
2
3
4
5
6
7
typedef struct tagNTUSERDISPATCHMESSAGEINFO
{
  BOOL HandledByKernel;
  BOOL Ansi;
  WNDPROC Proc;
  MSG Msg;
} NTUSERDISPATCHMESSAGEINFO, *PNTUSERDISPATCHMESSAGEINFO;

这里最关键的就是Proc字段,该字段是个函数指针,指向的函数就是由用户指定的消息处理过程函数。

 

而在NtUserDispatchMessage函数中,会首先通过窗口句柄获取窗口对象,在根据窗口对象获取窗口过程函数来填充上述的结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
PWINDOW_OBJECT Window;
 
/* Get the window object. */
Window = UserGetWindowObject(MsgInfo.Msg.hwnd);
if (NULL == Window)
{
   MsgInfo.HandledByKernel = TRUE;
   Result = 0;
}
else
{
   if (Window->OwnerThread != PsGetCurrentThread())
   {
      DPRINT1("Window doesn't belong to the calling thread!\n");
      MsgInfo.HandledByKernel = TRUE;
      Result = 0;
   }
   else
   {
      /* FIXME: Call hook procedures. */
 
      MsgInfo.HandledByKernel = FALSE;
      Result = 0;
 
      if (Window->IsSystem)
      {
          MsgInfo.Proc = (!MsgInfo.Ansi ? Window->WndProc : Window->WndProcExtra);
      }
      else
      {
          MsgInfo.Ansi = !Window->Unicode;
          MsgInfo.Proc = Window->WndProc;
      }
   }
}

获取消息处理例程以后,就通过IntCallWindowProcW来调用消息处理例程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
LRESULT FASTCALL
IntCallWindowProcW(BOOL IsAnsiProc,
                   WNDPROC WndProc,
                   HWND hWnd,
                   UINT Msg,
                   WPARAM wParam,
                   LPARAM lParam)
{
  MSG AnsiMsg;
  MSG UnicodeMsg;
  LRESULT Result;
 
  if (WndProc == NULL)
  {
      DPRINT("IntCallWindowsProcW() called with WndProc = NULL!\n");
      return FALSE;
  }
 
  if (IsAnsiProc)
    {
      UnicodeMsg.hwnd = hWnd;
      UnicodeMsg.message = Msg;
      UnicodeMsg.wParam = wParam;
      UnicodeMsg.lParam = lParam;
      if (! MsgiUnicodeToAnsiMessage(&AnsiMsg, &UnicodeMsg))
        {
          return FALSE;
        }
      Result = WndProc(AnsiMsg.hwnd, AnsiMsg.message, AnsiMsg.wParam, AnsiMsg.lParam);
 
      if (! MsgiUnicodeToAnsiReply(&AnsiMsg, &UnicodeMsg, &Result))
        {
          return FALSE;
        }
      return Result;
    }
  else
    {
      return WndProc(hWnd, Msg, wParam, lParam);
    }
}

六.Win32k的用户空间回调机制

Win32k.sys中的回调机制是由KeUserModeCallback来实现的,该函数的定义如下:

1
2
3
4
5
6
7
NTSTATUS
NTAPI
KeUserModeCallback(IN ULONG RoutineIndex,
                   IN PVOID Argument,
                   IN ULONG ArgumentLength,
                   OUT PVOID *Result,
                   OUT PULONG ResultLength)

其中第一个参数指定了回调函数的索引。根据索引就可以从预先设置好的“回调函数表”中调用相应的函数,而回调函数表则保存在PEB结构偏移0x02C的KernelCallbackTable中。根据索引的不同,回调函数表中保存的回调函数也不相同,其中索引号定义如下:

1
2
3
4
5
6
#define USER32_CALLBACK_WINDOWPROC            (0)
#define USER32_CALLBACK_SENDASYNCPROC         (1)
#define USER32_CALLBACK_LOADSYSMENUTEMPLATE   (2)
#define USER32_CALLBACK_LOADDEFAULTCURSORS    (3)
#define USER32_CALLBACK_HOOKPROC              (4)
#define USER32_CALLBACK_MAXIMUM               (4)

在KeUserModeCallback中会首先准备好陷阱帧,然后通过调用KiCallUserMode来返回用户层,随后在恢复栈指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/* Get the current user-mode stack */
 UserEsp = KiGetUserModeStackAddress();
 OldStack = *UserEsp;
 
 /* Calculate and align the stack size */
 NewStack = (OldStack - ArgumentLength) & ~3;
 
 /* Make sure it's writable */
 ProbeForWrite((PVOID)(NewStack - 6 * sizeof(ULONG_PTR)),
                   ArgumentLength + 6 * sizeof(ULONG_PTR),
                   sizeof(CHAR));
 
 /* Copy the buffer into the stack */
 RtlCopyMemory((PVOID)NewStack, Argument, ArgumentLength);
 
 /* Write the arguments */
 NewStack -= 24;
 *(PULONG)NewStack = 0;
 *(PULONG)(NewStack + 4) = RoutineIndex;
 *(PULONG)(NewStack + 8) = (NewStack + 24);
 *(PULONG)(NewStack + 12) = ArgumentLength;
 
  /* Save the exception list */
  Teb = KeGetCurrentThread()->Teb;
  ExceptionList = Teb->Tib.ExceptionList;
 
  /* Jump to user mode */
  *UserEsp = NewStack;
  CallbackStatus = KiCallUserMode(Result, ResultLength);          
  if (CallbackStatus != STATUS_CALLBACK_POP_STACK)
     {
         /* Only restore the exception list if we didn't crash in ring 3 */
         Teb->Tib.ExceptionList = ExceptionList;
         CallbackStatus = STATUS_SUCCESS;
     }
     else
     {
         /* Otherwise, pop the stack */
         OldStack = *UserEsp;
     }

七.参考资料

  • 《Windows内核源码情景分析》

  • 《Windows内核原理与实现》


【公告】 [2022大礼包]《看雪论坛精华22期》发布!收录近1000余篇精华优秀文章!

收藏
点赞0
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回