前言
在初级班时曾学习过Win32相关API的用法,可以自己编写一些窗口界面,向窗口发送消息实现交互。但仍然有些问题是无法回答的:
- 什么是窗口句柄?在哪里?有什么用?
- 什么是消息?什么是消息队列?消息队列在哪?
- 什么是窗口过程?窗口过程是谁调用的?没有消息循环窗口过程会执行吗?
- 为什么要有w32k.sys这个模块?
- 为什么只有使用图形界面的程序才可以访问KeServiceDescriptorTableShadow?
- 界面”卡死”的时候为什么鼠标还可以动?
为了弄清楚这些问题,就必须进入0环,从底层弄清楚消息机制的本质,本篇从消息队列开始介绍。
什么是消息队列
先来看一个小实验,编写运行如下代码(环境:Windows XP,编译器:VC++6.0):
1 | // MessageQueue.cpp : Defines the entry point for the console application. |
代码逻辑不在此展开,默认是有Win32基础的,实验运行结果如下:
![](/2020/09/06/%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97/experiment_1.png)
![](/2020/09/06/%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97/experiment_2.png)
![](/2020/09/06/%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97/experiment_3.png)
运行程序后,会在桌面的左上角画上一个窗口,接着我们可以通过发送消息来与窗口互动,当敲下“a”后,窗口的颜色会变深,敲下“b”后,窗口的颜色会变浅。
这是一个非常简单的交互程序,通过发送消息与窗口进行交互,但是这个代码有一个弊端,就是只能接收键盘发来的消息,对于鼠标亦或是其它进程发来的消息则无能为力。
想要能够接收并处理所有类型的消息,就必须有一个容器,而这个容器,就是消息队列。
![](/2020/09/06/%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97/experiment_4.png)
消息队列放哪
前面提到,得有一个消息队列,用来接收所有的消息,并分别处理,那么问题来了,这个消息队列放在什么位置?
专用进程
有一种思路是每个进程中放置一个消息队列,通过一个专用进程对不同类型的消息进行分发,如下图所示:
![](/2020/09/06/%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97/position_1.png)
Linux操作系统采用的就是这种方式,另起一个专用进程负责接收消息,并将消息发送至不同的进程中去处理。这种方法需要有一个专用进程对消息进行处理,避免不了造成过多的跨进程通信,显然会使效率有所降低。
内核存储
微软使用了另一种策略,由于在0环中,不同进程的地址空间往往是相同的,利用这一点,就可以省去专用进程处理消息。
前面的实验中,我们通过手动画了一个窗口,Windows也提供了一部分图形界面API,尽管那些API的底层实现也是手动画图形界面。
在KThread+0x130处有一个成员Win32Thread
![](/2020/09/06/%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97/position_2.png)
平时这个Win32Thread指向的值为空,一旦线程调用了图形界面API,它就会指向一个叫做_THREADINFO的结构体,消息队列就位于这个结构体中。根据ReactOS,THREADINFO结构如下:
![](/2020/09/06/%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97/ThreadInfo_1.png)
![](/2020/09/06/%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97/ThreadInfo_2.png)
GUI线程
当线程调用了图形界面API后,该线程的KTHREAD.Win32Thread就会指向一个叫做THREADINFO的结构,这个结构体中就包含了消息队列。此时这个线程不再是普通线程了,而是GUI线程。
- 当线程刚创建的时候,都是普通线程:Thread.ServiceTable -> KeServiceDescriptorTable(只有一张表可见)
- 当线程第一次调用Win32k.sys(图形界面API在0环的实现)时,会调用一个函数:PsConvertToGuiThread,这个函数主要做几件事:
- 扩充内核栈,必须换成64KB的大内核栈,因为普通内核栈只有12KB大小。
- 创建一个包含消息队列的结构体,并挂到KTHREAD上。
- Thread.ServiceTable -> KeServiceDescriptorTableShadow(此时两张表均可见)
- 把需要的内存数据映射到本进程空间。
总结
- 消息队列存储在0环,通过KTHREAD.Win32Thread可以找到。
- 并不是所有线程都要消息队列,只有GUI线程才有消息队列
- 1个GUI线程对应1个消息队列。
参考资料
参考教程:
- 海哥逆向中级预习班
参考链接:
- https://blog.csdn.net/weixin_42052102/article/details/83787929 (My classmates-消息机制学习笔记)
- https://reactos.org/wiki/Techwiki:Win32k/THREADINFO (ReactOS-Win32k/THREADINFO)
- https://doxygen.reactos.org/d9/df6/struct__THREADINFO.html (ReactOS-Struct THREADINFO)