avatar

Catalog
内核回调机制

谁调用了窗口过程

先来看一个问题,谁调用了窗口过程?根据前面的学习,可以得出:

  • 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()的调用关系。

  1. 首先NtUserDispatchMessage()会调用IntDispatchMessage()

  2. 其次IntDispatchMessage()内部又会调用co_IntCallWindowProc()

  3. 最后co_IntCallWindowProc()会调用KeUserModeCallback()

显然,NtUserDispatchMessage最终也要通过调用KeUserModeCallback()回到3环。现在可以确定KeUserModeCallback()就是内核回调机制下,0环回到3环的核心函数。以下为函数原型:

c
1
2
3
4
5
6
7
NTSTATUS NTAPI KeUserModeCallback(	
IN ULONG RoutineIndex,
IN PVOID Argument,
IN ULONG ArgumentLength,
OUT PVOID * Result,
OUT PULONG ResultLength
)

有两个参数较为重要,一个是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提供,回调函数表位置如下:

c
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环窗口过程函数

参考资料

参考教程:

  • 海哥逆向中级预习班

参考链接:

Author: cataLoc
Link: http://cataloc.gitee.io/blog/2020/09/10/%E5%86%85%E6%A0%B8%E5%9B%9E%E8%B0%83%E6%9C%BA%E5%88%B6/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.
Donate
  • 微信
    微信
  • 支付寶
    支付寶