首发于奇安信攻防社区:https://forum.butian.net/share/1609
前言
windows是一个消息驱动的系统,windows的消息提供了应用程序之间、应用程序与windows 系统之间进行通信的手段。要想深入理解windows,消息机制的知识是必不可少的。
基础
进程接收来自于鼠标、键盘等其他消息都是通过消息队列进行传输的
常规模式下,有一个专用的进程来接收这些消息,然后再插入某个进程的消息队列,但是这样的话会涉及到频繁的进程间的通信,效率很差
windows为了解决这一问题,因为高2G的内核空间每个进程都是共用的,所以微软想到把消息的接收放到了0环,使用GUI线程
<1> 当线程刚创建的时候,都是普通线程,指向的是SSDT表
Thread.ServiceTable-> KeServiceDescriptorTable
<2> 当线程第一次调用Win32k.sys
时,会调用一个函数:PsConvertToGuiThread
,我们知道在3环进0环的过程中会取得一个调用号,当调用号在100以下的时候,在ntosknl.exe
里面,当调用号大于100则是图形处理函数,调用Win32k.sys
如果是一个GUI线程,win32Thread
指向的就是THREADINFO
结构,如果是普通线程,这里就是一个空指针
主要做几件事:
a. 扩充内核栈,必须换成64KB的大内核栈,因为普通内核栈只有12KB大小。
b.创建一个包含消息队列的结构体,并挂到KTHREAD
上。对应的就是MessageQueue
属性
c.Thread.ServiceTable-> KeServiceDescriptorTableShadow,把Thread.ServiceTable
指向SSDTShadow表,这个表既包含了SSDT表里面的函数,又包含了win32k.sys
里面的图形函数
d.把需要的内存数据映射到本进程空间
总结:
<1> 消息队列存储在0环,通过KTHREAD.Win32Thread
可以找到
<2> 并不是所有线程都要消息队列,只有GUI线程才有消息队列
<3> 一个GUI线程对应1个消息队列
窗口与线程
我们知道创建windows窗口使用的是CreateWindow
,而这个函数底层调用的是CreateWindowExA
和CreateWindowExW
,我们逆向分析一下CreateWindowExW
首先调用CreateWindowEx
然后调用VerNtUserCreateWindowEx
再调用NtUserCreateWindowEx
通过NtUserCreateWindowEx
进入0环
windows窗口都在0环有一个结构体,就是WINDOW_OBJECT
,pti
即窗口对象指向的线程。一个线程可以对应多个窗口,但是在同一个程序里面多个窗口只能对应一个线程
总结
1、窗口是在0环创建的
2、窗口句柄是全局的
3、一个线程可以用多个窗口,但每个窗口只能属于一个线程
一个GUI线程只有一个消息队列,一个线程可以有很多个窗口,一个线程中所有的窗口共享同一个消息队列
消息的接收
首先在3环创建窗口和窗口类的对象,对应0环的_WINDOW_OBJECT
结构
消息队列的结构
代码语言:javascript复制<1> SentMessagesListHead //接到SendMessage发来的消息
<2> PostedMessagesListHead //接到PostMessage发来的消息
<3> HardwareMessagesListHead //接到鼠标、键盘的消息
如果要取所有队列的消息,则第二个参数设置为NULL,后两个参数全部设置为0
GetMessage的主要功能:循环判断是否有该窗口的消息,如果有,将消息存储到MSG指定的结构,并将消息从列表中删除。
代码语言:javascript复制GetMessage( LPMSG lpMsg, //返回从队列中摘下来的消息
HWND hWnd, //过滤条件一:发个这个窗口的消息
UNIT wMsgFilterMin, //过滤条件
UNIT wMsgFilterMax //过滤条件
);
使用GetMessage()
获取信息,另外一个程序利用SendMessage
发送给窗口,这里GetMessage
会接收到消息并直接处理
NtUserGetMessage
User32!GetMessage
调用 w32k!NtUserGetMessage
do
{
//先判断SentMessagesListHead是否有消息 如果有处理掉
do
{
....
KeUserModeCallback(USER32_CALLBACK_WINDOWPROC,
Arguments,
ArgumentLength,
&ResultPointer,
&ResultLength);
....
}while(SentMessagesListHead != NULL)
//以此判断其他的6个队列,里面如果有消息 返回 没有继续
}while(其他队列!=NULL)
SendMessage/PostMessage
SendMessage
为同步,PostMessage
为异步,GetMessage
只处理第一个链表即SentMessagesListHead
里面的消息
当一个程序利用SendMessage
向另外一个程序发送消息时,另外一个程序会用GetMessage
接收,这个过程GetMessage
会在0环的SentMessagesListHead
链表里面搜索是否存在SendMessage
,如果存在SendMessage
,GetMessage
就会在两个程序的共享内存里面向发送消息的程序发送一个结果,在这个过程中,发送消息的程序是一直处于等待状态的,只有接收到返回的消息才会结束,这称为同步
如果利用PostMessage
发送消息,处于第二个链表里面,GetMessage
不会处理,而程序发完消息之后也会立即结束,不会有等待的过程,这成为异步,如果要处理,使用DispatchMessage()
处理
MSG msg;
while(GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
消息的分发
这里如果只有GetMessage
的话,关闭窗口是关闭不了的
DispatchMessage
User32!DispatchMessage
调用 w32k!NtUserDispatchMessage
<1> 根据窗口句柄找到窗口对象
<2> 根据窗口对象得到窗口过程函数,由0环发起调用
如果使用DispatchMessage
分发消息,根据窗口句柄调用相关的窗口过程,即可关闭
因为很多个消息共用一个消息队列,所以通过GetMessage
取出消息之后,需要用DispatchMessage
进行消息的分发
DispatchMessage
通过GetMessage
取出的句柄,进入0环找到Window_Object
对象,再找到对应的窗口过程调用
TranslateMessage
是用来处理键盘输出的函数,定义一个函数
case WM_CHAR:
{
sprintf(szBuffer, "Down : %c", wParam);
MessageBox(hwnd, szBuffer, "", 0);
return 0;
}
这里如果不使用TranslateMessage
,则没有WM_CHAR
这个消息,需要自己定义WM_KEYDOWN
case WM_KEYDOWN:
{
sprintf(szBuffer, "Down : %d", wParam);
MessageBox(hwnd, szBuffer, "", 0);
return 0;
}
消息有很多,但是不是每个消息都需要我们自己去处理,所以与我们无关的消息就使用windows提供的DefWindowProc
让微软替我们处理即可
内核回调机制
窗口过程函数除了GetMessage
和DispatchMessage
能够调用,一些在0环的函数也能够直接进行调用。例如CreateWindow
不向消息队列里面发送消息,而是直接调用3环提供的函数
这些消息类型可以被直接调用
这里对WM_CREATE
进行修改,当创建成功的时候弹窗
这里并没有执行到GetMessage
和TranslateMessage
就弹窗,说明被CreateWindow
调用0环函数,0环函数通过回调机制(KeUserModeCallBack
),再调用窗口过程函数
所以调用窗口过程只能是以下三种情况
代码语言:javascript复制<1> GetMessage()在处理SentMessagesListHead中消息时
<2> DispatchMessage()在处理其他队列中的消息时
<3> 内核代码
1、从0环调用3环函数的几种方式:
APC、异常、内核回调
2、凡是有窗口的程序就有可能0环直接调用3环的程序。回调机制中0环调用3环的的代码是函数:KeUserModeCallback
3、回到3环的落脚点:
APC:ntdll!KiUserApcDispatcher
异常:ntdll!KiUserExceptionDispatcher
KeUserModeCallback
KeUserModeCallback
在0环对应NtUserDispatchMessage
,调用IntDispatchMessage
。通过UserGetWindowObject
获得一个Window_Object
类型,通过对象得到当前窗口的对应的窗口函数,然后调用co_IntCallWindowProc
。
调用KeUserModeCallback
,第一个值为索引,第二个值为窗口回调过程中所有有用的信息。第一个索引值, KeUserModeCallback
函数的第一个参数就是索引,其实它是一个宏,有很多个对应的值
内核回调在3环的落脚点,有很多个地方,我们拿着索引去3环里面找回调函数地址表,如果索引为0,则取表里面的第一个函数,如果索引为1,则取表里面的第二个函数
PEB 0x2C 回调函数地址表,由
user32.dll
提供
这里打开一个exe,通过fs:[0]
找到TEB
TEB的0x30偏移为PEB
PEB的0x2C偏移即为回调地址函数表
这里通过KeUserModeCallback
的第一个值,即索引找到函数之后,这个函数再去调用窗口过程函数,窗口过程函数已经通过Arguments
放在了堆栈里面