windows消息机制详解

一颗小胡椒2022-06-10 16:15:55

前言

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,而这个函数底层调用的是CreateWindowExACreateWindowExW,我们逆向分析一下CreateWindowExW

首先调用CreateWindowEx

然后调用VerNtUserCreateWindowEx

再调用NtUserCreateWindowEx

通过NtUserCreateWindowEx进入0环

windows窗口都在0环有一个结构体,就是WINDOW_OBJECTpti即窗口对象指向的线程。一个线程可以对应多个窗口,但是在同一个程序里面多个窗口只能对应一个线程

总结

1、窗口是在0环创建的

2、窗口句柄是全局的

3、一个线程可以用多个窗口,但每个窗口只能属于一个线程

一个GUI线程只有一个消息队列,一个线程可以有很多个窗口,一个线程中所有的窗口共享同一个消息队列

消息的接收

首先在3环创建窗口和窗口类的对象,对应0环的_WINDOW_OBJECT结构

消息队列的结构

<1> SentMessagesListHead //接到SendMessage发来的消息
<2> PostedMessagesListHead //接到PostMessage发来的消息
<3> HardwareMessagesListHead //接到鼠标、键盘的消息

如果要取所有队列的消息,则第二个参数设置为NULL,后两个参数全部设置为0

GetMessage的主要功能:循环判断是否有该窗口的消息,如果有,将消息存储到MSG指定的结构,并将消息从列表中删除。

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,如果存在SendMessageGetMessage就会在两个程序的共享内存里面向发送消息的程序发送一个结果,在这个过程中,发送消息的程序是一直处于等待状态的,只有接收到返回的消息才会结束,这称为同步

如果利用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让微软替我们处理即可

内核回调机制

窗口过程函数除了GetMessageDispatchMessage 能够调用,一些在0环的函数也能够直接进行调用。例如CreateWindow不向消息队列里面发送消息,而是直接调用3环提供的函数

这些消息类型可以被直接调用

这里对WM_CREATE进行修改,当创建成功的时候弹窗

这里并没有执行到GetMessageTranslateMessage就弹窗,说明被CreateWindow调用0环函数,0环函数通过回调机制(KeUserModeCallBack),再调用窗口过程函数

所以调用窗口过程只能是以下三种情况

<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放在了堆栈里面

消息队列消息机制
本作品采用《CC 协议》,转载必须注明作者和本文链接
windows消息机制详解
2022-06-10 16:15:55
要想深入理解windows,消息机制的知识是必不可少的。
在Windows大部分应用都是基于消息机制,他们都拥有一个消息过程函数,根据不同消息完成不同功能,windows通过钩子机制来截获和监视系统中的这些消息。一般钩子分局部钩子与全局钩子,局部钩子一般用于某个线程,而全局钩子一般通过dll文件实现相应的钩子函数。
全局钩子注入在Windows大部分应用都是基于消息机制,他们都拥有一个消息过程函数,根据不同消息完成不同功能,windows通过钩子机制来截获和监视系统中的这些消息。一般钩子分局部钩子与全局钩子,局部钩子一般用于某个线程,而全局钩子一般通过dll文件实现相应的钩子函数。
代码框架 想法是尽量用一个通用的注入框架,有异常接收,令牌权限开启,获取进程PID的功能
简介这次实验是在WIN7 X86系统上进程,使用的编译器是VS2017。所谓的DLL注入,其实就是在其他的进程中把我们编写的DLL加载进去。所以DLL注入的核心就是把要注入的DLL的路径写到目标进程中,然后在目标进程中调用LoadLibrary函数,并且指定参数为保存了DLL路径的地址。要实现DLL注入,首先就要创建一个用来注入的DLL。
添加消息的任务我们称为producer,而取出并使用消息的任务,我们称之为consumer。kafka应运而生,它是专门设计用来做消息中间件的系统。这两点也是kafka要解决的核心问题。为此,kafka提出了partition的概念。由于消息不会被删除,因此可以等消费者明确告知kafka这条消息消费成功以后,再去更新游标。对于同一个topic,不同的消费组有各自的游标。
分布式流平台Kafka
2022-08-02 10:13:27
无论消息是否被消费,Kafka集群都会持久的保存所有发布的消息,直到过期。Kafka中采用分区的设计主要有两个目的:第一,当日志大小超过了单台服务器的限制,允许日志进行扩展。在Kafka中实现消费的方式是将日志中的分区划分到每一个消费者实例上,以便在任何时间,每个实例都是分区唯一的消费者。
在介绍Nginx的负载均衡实现之前,先简单的说下负载均衡的分类,主要分为硬件负载均衡和软件负载均衡 ,硬件负载均衡是使用专门的软件和硬件相结合的设备,设备商会提供完整成熟的解决方案,比如F5,在数据的稳定性以及安全性来说非常可靠,但是相比软件而言造价会更加昂贵;软件的负载均衡以Nginx这类软件为主,实现的一种消息队列分发机制
Dll注入
2021-11-08 14:57:41
最近太忙啦XDM,又在做一些列的分析复现工作量有点大,更新要慢一点了。一致,也不会覆盖其他的进程信息。
如果是在消费端丢失数据,那么多次消费结果完全一模一样的几率很低。这时已经fetch的数据还没有处理完成但已经被commit掉,因此没有机会再次被处理,数据丢失。网络负载很高或者磁盘很忙写入失败的情况下,没有自动重试重发消息。
一颗小胡椒
暂无描述