谁调用了窗口过程
先来看一个问题,谁调用了窗口过程?根据前面的学习,可以得出:
- GetMessage()在处理SentMessagesListHead中消息时,会调用窗口过程。
- DispatchMessage()在处理其它队列中的消息时,会调用窗口过程。
但实际上还有一种,就是内核代码本身会调用窗口处理函数。
(实验:注释掉DispatchMessage(),设置WM_CREATE类型消息的窗口处理函数,看是否会被调用。此处省略,以后补上)
这是什么原理呢?在调用CreateWindow()时,必然会进入0环调用NtUserCreateWindowEx(),这个函数会调用内核回调函数向窗口发送消息(在窗口创建出来之前),而这个消息甚至不会出现在消息队列中,而是通过内核回调函数发送给窗口过程函数,消息类型属于WM_CREATE。NtUserCreateWindowEx()之所以这样设计是因为,如果程序需要在窗口创建之时就做一些事情,但窗口没创建出来时它是接收不了消息的,因此就有了这样的设计,在窗口创建前就调用WM_CREATE消息对应的窗口过程函数。利用这一点,即使没有DispatchMessage()也会有消息调用窗口过程。这就是第三种调用窗口过程的情况。
内核回调机制
从0环调用3环函数的几种方式
先来看一下,0环调用3环函数有哪几种方式:
- 用户APC的执行
- 用户异常的处理(内核调试器与用户调试器均不存在或不处理的情况下,会从Ring0进入Ring3)
- 内核回调(Ring0代码调用窗口过程函数)
KeUserModeCallback
先来回顾一个函数KeUserModeCallback,这个函数之前已经出现过2次,GetMessage()底层调用的NtUserGetMessage()会在一个循环里调用KeUserModeCallback()来处理SentMessagesListHead队列中的消息;同样,DispatchMessage()底层调用的NtUserDispatchMessage()也是如此,这里简单看一下NtUserDispatchMessage()的调用关系。
首先NtUserDispatchMessage()会调用IntDispatchMessage()
其次IntDispatchMessage()内部又会调用co_IntCallWindowProc()
最后co_IntCallWindowProc()会调用KeUserModeCallback()
显然,NtUserDispatchMessage最终也要通过调用KeUserModeCallback()回到3环。现在可以确定KeUserModeCallback()就是内核回调机制下,0环回到3环的核心函数。以下为函数原型:
1 | NTSTATUS NTAPI KeUserModeCallback( |
有两个参数较为重要,一个是Argument,另一个是RoutineIndex。先来看Argument
顾名思义,Argument主要负责提供参数,包括提供窗口过程函数的地址。而另一个参数RoutineIndex则与落脚点有关。
回到3环的落脚点
关于落脚点,在处理用户APC与用户异常时,0环回到3环的落脚点是确定的:
- APC:ntdll!KiUserApcDispatcher
- 异常:ntdll!KiUserExceptionDispatcher
而内核回调的3环落脚点比较特殊,前面提到了RoutineIndex的值与落脚点有关,先来看它的取值:
在callback.h的头文件中,可以看到RoutineIndex有至少18个取值,这些取值就相当于索引。用来在回调函数表中定位返回3环的落脚点。回调函数表包含多个回调函数,供0环的KeUserModeCallback()调用,这些回调函数均由user32.dll提供,回调函数表位置如下:
1 | fs[0] -> TEB -> PEB(TEB+0x30) -> 回调函数表(PEB+0x2C) |
下面任意打开一个进程,查看进程的回调函数表:
这就是回调函数表,基本上每个进程都有。而KeUserModeCallback()的参数RoutineIndex就是在表中的索引,若值为0,3环落脚点就是表中第一个函数;若值为1,落脚点就是表中第二个函数,以此类推。
确定落脚点后,KeUserModeCallback()便会通过落脚点函数进入3环,接下来,落脚点函数会从Argument中取出窗口过程函数的地址,并完成调用。
小聪明
内核回调机制是非常适合做手脚的地方之一,比起Hook异常或者APC的处理函数,或者在它们返回0环时对TrapFrame做手脚,对回调函数表中的函数做手脚要隐蔽的多,首先这些回调函数是直接从0环发起调用的,并且没有线程的信息,什么时候调用也很难查出来。如果手动写一个驱动,自己在0环发起调用,那隐蔽性就更高了。
消息机制总结
- 消息队列:
- 引入消息队列的概念
- 找到消息队列的方式:KTHREAD.Win32Thread.THREADINFO.MessageQueue,非GUI线程Win32Thread的值为空
- 了解GUI线程:调用图形界面API的线程就会变成GUI线程
- 窗口与线程:
- 了解窗口的创建与窗口句柄:窗口是在0环创建的;窗口句柄是全局的。
- 窗口与线程的关系:一个线程可以有多个窗口,但每个窗口只能属于一个线程。
- 消息的接收:
- GetMessage:1.接收消息;2.处理SendMessage发来的消息(位于SentMessagesListHead队列中)
- 消息的分发:
- TranslateMessage:翻译键盘发来的消息。
- DispatchMessage:处理其它队列中的消息。
- 默认的窗口过程处理函数DefWindowProc
- 内核回调机制:
- 0环如何回调3环窗口过程函数
参考资料
参考教程:
- 海哥逆向中级预习班
参考链接:
- https://blog.csdn.net/weixin_42052102/article/details/83826893 (My classmates-内核回调机制笔记)