avatar

Catalog
窗口与线程

前言

前一篇,弄清楚了消息队列与线程的关系,简单来说就是,一个GUI线程对应一个消息队列。什么是GUI,什么是消息队列,参考这里,本篇不再赘述。本篇将解决以下3个问题:

  • 消息从哪里来?
  • 消息到哪里去?
  • 谁来做这些事情?

消息从哪里来

进入正题,首先讨论消息从哪里来。

鼠标消息、键盘消息

这张图源于VC++6.0的插件,Spy++,它可以捕捉窗口所接收到的消息。当把鼠标放在窗口上时,移动或者点击,则会接收到鼠标发送的消息,敲击键盘则会接收到键盘发送的消息(图中未显示,测试时消息界面会显示敲击下的键盘符号)。

其它进程消息

另一种情况是其它进程发送的消息。使用CreateWindow创建一个窗口,会获得一个窗口句柄,之前在句柄表中曾提过,此句柄非彼句柄,之前说到的句柄属于内核对象的句柄,而今天提到的句柄则是窗口句柄,相同的是这两类句柄都只是一个编号,用于给3环使用的。窗口句柄的特点,它是全局的,因此只要获取到窗口的句柄,任意进程都可以通过SendMessage或者PostMessage函数给这个窗口发送消息进行交互。

结论

  • 消息来源于鼠标,键盘以及其它进程。

消息到哪里去

根据上面的分析,消息会因为鼠标,键盘,以及其它进程与某一个窗口交互时产生,所以表面上来说,消息会到窗口。窗口又是什么呢?

窗口的形成

通常创建窗口使用CreateWindow函数,它有CreateWindowA(ASCII)与CreateWindowW(Unicode)两个宏,会根据当前使用的编码自动调用其中的一种。这两个宏最终都会调用CreateWindowEx,CreateWindowEx的内部调用如下:

  1. 调用_VerNtUserCreateWindowEx

  2. 调用_NtUserCreateWindow

  3. 进入_NtUserCreateWindow内部

    这个函数是进入0环的入口,可以得出两点信息:

    • 窗口的创建是在0环
    • 系统服务号大于0x1000,因此调用的是第二张系统服务表指向的函数地址表中的函数,属于Win32k.sys。

InitInputImpl函数

创建窗口必然要调用win32k.sys中的服务,在初始化Win32k.sys的服务时,会调用一个函数InitInputImpl,其原型如下:

c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
NTSTATUS FASTCALL InitInputImpl(VOID)
{
NTSTATUS Status;
KeInitializeEvent(&InputThreadsStart, NotificationEvent, FALSE);
MasterTimer = ExAllocatePoolWithTag(NonPagedPool, sizeof(KTIMER), TAG_INPUT);
KeInitializeTimer(MasterTimer);
Status = PsCreateSystemThread(&RawInputThreadHandle,THREAD_ALL_ACCESS,NULL,NULL,
&RawInputThreadId,RawInputThreadMain,NULL);

//键盘输入线程:KeyboardThreadMain
Status = PsCreateSystemThread(&KeyboardThreadHandle,THREAD_ALL_ACCESS,NULL,NULL,
&KeyboardThreadId,KeyboardThreadMain,NULL);

//鼠标输入线程:MouseThreadMain
Status = PsCreateSystemThread(&MouseThreadHandle,THREAD_ALL_ACCESS,NULL,NULL,
&MouseThreadId,MouseThreadMain,NULL);

InputThreadsRunning = TRUE;//标志现在可以开始读取键盘鼠标输入
KeSetEvent(&InputThreadsStart, IO_NO_INCREMENT, FALSE);

return STATUS_SUCCESS;
}

InitInputImpl会在0环创建2个线程,一个监控键盘,另一个监控鼠标,并将消息存储到对应线程的消息队列中。这也能解释为什么有时候程序突然卡死了,但是鼠标还可以动,原因是鼠标有着自己独立的线程。

结论

  • 窗口是通过调用win32k.sys系统服务在0环创建的。
  • 初始化win32k.sys会调用InitInputImpl函数,InitInputImpl创建的监控线程会将消息存储到对应线程的消息队列中。

窗口找消息队列

现在知道消息从哪来,以及消息会到哪去,接下来就会有一个问题,如何通过窗口找到消息队列。

由图,打开3个窗口,鼠标进行点击与移动,操作系统是怎样准确的将消息发送给不同窗口对应的消息队列的呢?

  • 首先,这张图上不止是3个窗口,每个进程窗口内部的按钮,表格,都属于窗口,所以一个进程可以有多个窗口,但是这些窗口只能属于一个进程。
  • 前面提到窗口是进入0环后通过win32k.sys提供的服务画出来的,所以,窗口其实是个0环的结构。与进程,线程类似,它们都有对应的内核结构体EPROCESS与ETHREAD,窗口也有自己对应的内核结构体_WINDOW_OBJECT。可惜的是,这个结构体并没有通过符号表导出,我在网上搜也没能搜到,所以只能根据海哥所说的来看。

窗口对象_WINDOW_OBJECT中有一个成员pti,类型是PTHREADINFO,指向THREADINFO结构,就是前一篇讲到的Win32Thread指向的结构。这样就将线程与窗口联系起来了。原本KTHREAD.Win32Thread处指向的值为空,当线程调用win32k.sys中的函数创建一个窗口后,Win32Thread就会指向THREADINFO结构体,该线程也由普通线程变成了GUI线程,此时窗口对应的内核结构WINDOW_OBJECT中的成员pti也会指向这个结构体,而消息队列,正是位于这个结构体中,这样窗口就可以其所在线程的消息队列。

总结

  1. 窗口是在0环创建的。
  2. 窗口句柄是全局的。
  3. 一个线程可以有多个窗口,但每个窗口只能属于一个线程。

参考资料

参考教程:

  • 海哥逆向中级预习班

参考链接:

Author: cataLoc
Link: http://cataloc.gitee.io/blog/2020/09/07/%E7%AA%97%E5%8F%A3%E4%B8%8E%E7%BA%BF%E7%A8%8B/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.
Donate
  • 微信
    微信
  • 支付寶
    支付寶