文章目录
Windows键盘和鼠标输入过程
通常情况下面,我们使用Windows都是通过鼠标和键盘来和系统进行交互,那么键盘和鼠标是怎么输入和转换到应用程序的各种消息的呢?本文来看一下一个最基本的问题,Windows对于键盘鼠标输入的响应。
1. RawInputThread
鼠标和键盘的输入离不开一个重要的进程就是CSRSS,在这个进程启动的时候就会创建一个输入线程RawInputThread
, 这个线程会一直不停的接收从键盘和鼠标过来的消息。
在子系统进程中,这个线程函数的创建过程如下:
06 88252c04 94efd7c7 00000004 00000002 882c7300 win32k!RawInputThread+0x486
07 88252c18 94fb2b5d 00000004 01cafbe8 88252c34 win32k!xxxCreateSystemThreads+0x4a
08 88252c28 83e431ea 00000004 01cafc28 778d70b4 win32k!NtUserCallNoParam+0x1b
09 88252c28 778d70b4 00000004 01cafc28 778d70b4 nt!KiFastCallEntry+0x12a
0a 01cafbd8 75a019ec 75a0288b 00000004 00000000 ntdll!KiFastSystemCallRet
0b 01cafbdc 75a0288b 00000004 00000000 77895e7a winsrv!NtUserCallNoParam+0xc
0c 01cafbe8 77895e7a 00000000 765c35e7 00000000 winsrv!StartCreateSystemThreads+0x12
0d 01cafc28 778f37c8 75a02879 00000000 00000000 ntdll!__RtlUserThreadStart+0x28
0e 01cafc40 00000000 75a02879 00000000 00000000 ntdll!_RtlUserThreadStart+0x1b
在这个函数中会针对鼠标和键盘,投递一个读请求,过程如下:
PDEVICEINFO StartDeviceRead(
PDEVICEINFO pDeviceInfo)
{
//...
pDeviceInfo->ReadStatus = ZwReadFile(
pDeviceInfo->handle,
NULL, // hReadEvent
InputApc, // InputApc()
pDeviceInfo, // ApcContext
&pDeviceInfo->iosb,
(PVOID)((PBYTE)pDeviceInfo + pDevTpl->offData),
pDevTpl->cbData,
PZERO(LARGE_INTEGER), NULL);
//...
return NULL;
}
从这里我们可以发现,这个是一个异步读取操作,如果读取到了鼠标或者键盘信息,那么将会调用APC例程InputApc
, 然后再APC例程中继续投递读请求(调用StartDeviceRead
).
2. xxxProcessKeyEvent
当RawInputThread
从键盘读取到击键信息之后,就会调用xxxProcessKeyEvent
来处理键盘响应,如下:
3: kd> kb
# ChildEBP RetAddr Args to Child
00 882528d4 95036c0a 00000020 00000039 00030195 win32k!xxxKeyEvent
01 8825290c 94fd7313 88252920 00000000 00000000 win32k!xxxProcessKeyEvent+0x229
02 8825294c 94fd7737 ffa18900 20a18930 00000001 win32k!ProcessKeyboardInputWorker+0x2dc
03 8825296c 94f00f58 ffa18930 a17e8ed0 882529c4 win32k!ProcessKeyboardInput+0x68
04 8825297c 83eb67b4 ffa18930 ffa18958 00000000 win32k!InputApc+0x4e
05 882529c4 83e83685 00000000 00000000 00000000 nt!KiDeliverApc+0x17f
06 88252a08 83e824f7 8d35bb90 8940fc80 89410074 nt!KiSwapThread+0x24e
07 88252a30 83e7e4a4 8940fc80 89410008 00000000 nt!KiCommitThreadWait+0x1df
08 88252ba8 94eed276 00000004 8940c0d0 00000001 nt!KeWaitForMultipleObjects+0x535
09 88252c04 94efd7c7 00000004 00000002 882c7300 win32k!RawInputThread+0x486
0a 88252c18 94fb2b5d 00000004 01cafbe8 88252c34 win32k!xxxCreateSystemThreads+0x4a
0b 88252c28 83e431ea 00000004 01cafc28 778d70b4 win32k!NtUserCallNoParam+0x1b
0c 88252c28 778d70b4 00000004 01cafc28 778d70b4 nt!KiFastCallEntry+0x12a
0d 01cafbd8 75a019ec 75a0288b 00000004 00000000 ntdll!KiFastSystemCallRet
0e 01cafbdc 75a0288b 00000004 00000000 77895e7a winsrv!NtUserCallNoParam+0xc
0f 01cafbe8 77895e7a 00000000 765c35e7 00000000 winsrv!StartCreateSystemThreads+0x12
10 01cafc28 778f37c8 75a02879 00000000 00000000 ntdll!__RtlUserThreadStart+0x28
11 01cafc40 00000000 75a02879 00000000 00000000 ntdll!_RtlUserThreadStart+0x1b
从这里看,我们响应键盘是xxxKeyEvent
处理的,这个函数基本流程如下:
if ((pHook = PhkFirstValid(ptiCurrent, WH_KEYBOARD_LL)) != NULL)
成立,那么调用xxxCallHook2(pHook, HC_ACTION, (DWORD)msg, (LPARAM)&kbds, &bAnsiHook)
先处理WH_KEYBOARD_LL
钩子,其中如果按键不是热键(例如CTRL + ALT + DELETE)并且被WH_KEYBOARD_LL
过滤掉,那么直接返回。- 调用
xxxDoHotKeyStuff
处理热键,如果是基本热键,处理完成直接返回。 - 如果是普通的需要响应的键盘消息,调用
PostInputMessage
发送键盘消息,PostInputMessage
接着就会调用SetWakeBit
来通知相关线程处理消息。 - 调用
PostInputMessage(gpqForeground,xxxx)
给顶层窗口发消息,然后匹配QEVENT_APPCOMMAND
是不是windows增强消息,比如键盘按键直接打开浏览器等上网。 - 在
PostInputMessage
中,先调用StoreQMessage
存储在消息队列,然后调用WakeSomeone
唤醒目标进程处理. - 在
WakeSomeOne
中,匹配消息类型,是wm_keydown
,wm_char
,wm_mousemove
等,然后调用StoreQMessagePti
存在目标线程即顶层窗口队列中,当轮到目标线程执行时,处理wm_keydown
和wm_char
消息。
整体的过程如下:
3: kd> kb
# ChildEBP RetAddr Args to Child
00 88252820 94f71eed ffb54370 00000001 0000011b win32k!SetWakeBit
01 88252840 94f6afad fe8288f0 00000100 fd4056f0 win32k!WakeSomeone+0x189
02 88252860 9503481e fe8288f0 00000000 00000100 win32k!PostInputMessage+0x126
03 882528d4 95036c0a 00000020 00000039 00030195 win32k!xxxKeyEvent+0x8a6
04 8825290c 94fd7313 88252920 00000000 00000000 win32k!xxxProcessKeyEvent+0x229
05 8825294c 94fd7737 ffa18900 20a18930 00000001 win32k!ProcessKeyboardInputWorker+0x2dc
06 8825296c 94f00f58 ffa18930 a17e8ed0 882529c4 win32k!ProcessKeyboardInput+0x68
07 8825297c 83eb67b4 ffa18930 ffa18958 00000000 win32k!InputApc+0x4e
08 882529c4 83e83685 00000000 00000000 00000000 nt!KiDeliverApc+0x17f
09 88252a08 83e824f7 8d35bb90 8940fc80 89410074 nt!KiSwapThread+0x24e
0a 88252a30 83e7e4a4 8940fc80 89410008 00000000 nt!KiCommitThreadWait+0x1df
0b 88252ba8 94eed276 00000004 8940c0d0 00000001 nt!KeWaitForMultipleObjects+0x535
0c 88252c04 94efd7c7 00000004 00000002 882c7300 win32k!RawInputThread+0x486
0d 88252c18 94fb2b5d 00000004 01cafbe8 88252c34 win32k!xxxCreateSystemThreads+0x4a
0e 88252c28 83e431ea 00000004 01cafc28 778d70b4 win32k!NtUserCallNoParam+0x1b
0f 88252c28 778d70b4 00000004 01cafc28 778d70b4 nt!KiFastCallEntry+0x12a
10 01cafbd8 75a019ec 75a0288b 00000004 00000000 ntdll!KiFastSystemCallRet
11 01cafbdc 75a0288b 00000004 00000000 77895e7a winsrv!NtUserCallNoParam+0xc
12 01cafbe8 77895e7a 00000000 765c35e7 00000000 winsrv!StartCreateSystemThreads+0x12
13 01cafc28 778f37c8 75a02879 00000000 00000000 ntdll!__RtlUserThreadStart+0x28
14 01cafc40 00000000 75a02879 00000000 00000000 ntdll!_RtlUserThreadStart+0x1b
SetWakeBit
就是通知消息循环开始消息响应,如下:
VOID SetWakeBit(
PTHREADINFO pti,
UINT wWakeBit)
{
//...
KeSetEvent(pti->pEventQueueServer, 2, FALSE);
//...
}
这里有一个比较重要的东西,很多开发经验不足的人员希望通过WH_KEYBOARD_LL
来拦截CTRL + ALT + DEL按键,其实这个是无法生效的,因为在xxxKeyEvent
的流程如下:
VOID xxxKeyEvent(
USHORT usFlaggedVk,
WORD wScanCode,
DWORD time,
ULONG_PTR ExtraInfo,
#ifdef GENERIC_INPUT
HANDLE hDevice,
PKEYBOARD_INPUT_DATA pkei,
#endif
BOOL bInjected)
{
//...
if ((pHook = PhkFirstValid(ptiCurrent, WH_KEYBOARD_LL)) != NULL) {
//...
if (xxxCallHook2(pHook, HC_ACTION, (DWORD)msg, (LPARAM)&kbds, &bAnsiHook)) {
//...
if (IsSAS(VkHanded, &fsModifiers)) {
RIPMSG0(RIP_WARNING, "xxxKeyEvent: SAS ignore bad response from low level hook");
} else {
return;
}
}
//...
}
//...
}
这里SAS是Secure Attention Sequence的缩写,表示安全键,在Windows下面这个键别设置Ctrl + Alt + Del,这也是Ctrl + Alt + Del无法使用消息钩子拦截的原因。
PostInputMessage
表示消息投递的函数,这个函数实现如下:
BOOL PostInputMessage(
PQ pq,
PWND pwnd,
UINT message,
WPARAM wParam,
LPARAM lParam,
DWORD time,
ULONG_PTR dwExtraInfo)
{
//...
pqmsgInput = AllocQEntry(&pq->mlInput);
if (pqmsgInput == NULL) {
return FALSE;
}
StoreQMessage(pqmsgInput, pwnd, message, wParam, lParam, time, 0, dwExtraInfo);
WakeSomeone(pq, message, pqmsgInput);
return TRUE;
}
从这里可以得知,我们先从pq->mlInput
鼠标和键盘队列中分配一个数据结构,然后使用StoreQMessage
填充结构内容,最后使用WakeSomeone
通知目标进行处理。
3. gpqForeground
我们知道SendMessage
发送给了线程中的消息队列了,那么鼠标键盘的消息放到哪里去了呢?这个结构就是gpqForeground
,这个类型如下:
typedef struct tagQ {
MLIST mlInput; // raw mouse and key message list.
PTHREADINFO ptiSysLock; // Thread currently allowed to process input
ULONG_PTR idSysLock; // Last message removed
ULONG_PTR idSysPeek; // Last message peeked
PTHREADINFO ptiMouse; // Last thread to get mouse msg.
PTHREADINFO ptiKeyboard;
PWND spwndCapture;
PWND spwndFocus;
PWND spwndActive;
PWND spwndActivePrev;
UINT codeCapture;
UINT msgDblClk;
WORD xbtnDblClk;
DWORD timeDblClk;
HWND hwndDblClk;
POINT ptDblClk;
BYTE afKeyRecentDown[CBKEYSTATERECENTDOWN];
BYTE afKeyState[CBKEYSTATE];
CARET caret;
PCURSOR spcurCurrent;
int iCursorLevel;
DWORD QF_flags; // QF_ flags go here
USHORT cThreads; // Count of threads using this queue
USHORT cLockCount; // Count of threads that don't want this queue freed
UINT msgJournal;
LONG_PTR ExtraInfo;
} Q;
从名字来看gpqForeground
这个是前景窗口的信息了,那么这个东西是什么时候设置的呢?当我们调用SetForegroundWindow
设置前景窗口的时候,有如下设置:
BOOL xxxSetForegroundWindow2(
PWND pwnd,
PTHREADINFO pti,
DWORD fFlags)
{
//...
if (pwnd != NULL) {
//...
gpqForeground = GETPTI(pwnd)->pq;
//...
}
//...
}
我们可以看一下这个结构信息:
3: kd> dd win32k!gpqForeground L1
950ffc00 fe959570
3: kd> dt win32k!tagQ fe959570
+0x000 mlInput : tagMLIST
+0x00c ptiSysLock : (null)
+0x010 idSysLock : 0
+0x014 idSysPeek : 0
+0x018 ptiMouse : 0xfd1734f8 tagTHREADINFO
+0x01c ptiKeyboard : 0xfd1734f8 tagTHREADINFO
+0x020 spwndCapture : (null)
+0x024 spwndFocus : 0xfea1b250 tagWND
+0x028 spwndActive : 0xfea1a700 tagWND
+0x02c spwndActivePrev : (null)
+0x030 codeCapture : 0
+0x034 msgDblClk : 0
+0x038 xbtnDblClk : 0
+0x03c timeDblClk : 0
+0x040 hwndDblClk : (null)
+0x044 ptDblClk : tagPOINT
+0x04c ptMouseMove : tagPOINT
+0x054 afKeyRecentDown : [32] ""
+0x074 afKeyState : [64] ""
+0x0b4 caret : tagCARET
+0x0ec spcurCurrent : 0xffb55c88 tagCURSOR
+0x0f0 iCursorLevel : 0n0
+0x0f4 QF_flags : 0x40
+0x0f8 cThreads : 1
+0x0fa cLockCount : 0
+0x0fc msgJournal : 0
+0x100 ExtraInfo : 0n0
+0x104 ulEtwReserved1 : 0x2000
所以从PostInputMessage(gpqForeground, NULL, message, wParam, lParam, time, ExtraInfo);
我们就可以知道,鼠标和键盘消息都是发给前景窗口线程的。
4. 消息钩子
在Windows中,可以使用SetWindowsHookExW
来设置鼠标和键盘钩子
HHOOK SetWindowsHookExW(
int idHook,
HOOKPROC lpfn,
HINSTANCE hmod,
DWORD dwThreadId
);
对于键盘,有两种消息WH_KEYBOARD
和WH_KEYBOARD_LL
,从上述的分析我们可以知道,对于底层消息钩子,可以捕获到所有的按键信息,然而对于CTRL + ALT + DEL键信息,底层消息钩子并没有进行拦截。而对于普通消息钩子,很明显的是无法捕捉到热键按下的信息,至于热键我们使用WH_KEYBOARD_LL
依旧可以进行拦截,关于热键的注册使用如下函数:
BOOL RegisterHotKey(
HWND hWnd,
int id,
UINT fsModifiers,
UINT vk
);
关于消息钩子的具体实现原理,我们可以参考(Windows消息钩子实现原理)。
5. 从击键到Win32k
当我们按下键盘的时候,大致过程如下:
- 键盘产生硬件中断信号发到总线,总线将信号发送到ioapic寄存器接收,ioapic寄存器存储了键盘中断的中断idt的索引号,和发给哪个cpu来处理(针对多核),然后发给该cpu的local apic 寄存器。
- local apic接收到信号,里面存储了中断idt 索引号到idt键盘中断处理地址的映射。然后发给键盘中断处理例程,该例程再依次传给键盘端口驱动,端口类驱动,将按键的break code放到端口类驱动的设备扩展中。自下而上的过程结束。
针对i8042,这个中断向量为i8042prt!I8042KeyboardInterruptService
,此时我们可以看到整个调用关系如下:
0: kd> kb
# ChildEBP RetAddr Args to Child
00 83f2d940 83e3f7ad 8d4a3a00 8d3edb10 83f2d96c i8042prt!I8042KeyboardInterruptService
01 83f2d940 84219ca6 8d4a3a00 8d3edb10 83f2d96c nt!KiInterruptDispatch+0x6d
02 83f2d9dc 8421ca4d 8d4a3780 8d400b90 83f2dabc hal!HalpGenerateInterrupt+0x1d2
03 83f2d9fc 8421cc7f 83f2da14 83e3f808 5eb8ae02 hal!HalpLowerIrqlHardwareInterrupts+0xf5
04 83f2da04 83e3f808 5eb8ae02 00000061 83f2dabc hal!HalEndSystemInterrupt+0x23
05 83f2da04 90d6c46a 5eb8ae02 00000061 83f2dabc nt!KiInterruptDispatch+0xc8
06 83f2dabc 90d6c74a 634b0160 83f2daec 83f2dafb E1G60I32!RxPacketAssemble+0x126
07 83f2dafc 90d6b77e 00000001 8d50a460 00000000 E1G60I32!RxProcessReceiveInterrupts+0x5e
08 83f2db14 86ce389a 014b0160 00000000 83f2db40 E1G60I32!E1000HandleInterrupt+0x80
09 83f2db50 86c8ea0f 8d50a474 0050a460 00000000 ndis!ndisMiniportDpc+0xe2
0a 83f2db78 83e7d1b5 8d50a474 8d50a460 00000000 ndis!ndisInterruptDpc+0xaf
0b 83f2dbd4 83e7d018 83f30d20 83f3a380 00000000 nt!KiExecuteAllDpcs+0xf9
0c 83f2dc20 83e7ce38 00000000 0000000e 00000000 nt!KiRetireDpcList+0xd5
0d 83f2dc24 00000000 0000000e 00000000 00000000 nt!KiIdleLoop+0x38
我们知道,中断响应例程一般都是不做事情,交给DPC去完成,我们找到这个DPC为I8042KeyboardIsrDpc
,调用如下:
0: kd> kb
# ChildEBP RetAddr Args to Child
00 83f2db78 83e7d1b5 8d3edd74 8d3edb10 00000000 i8042prt!I8042KeyboardIsrDpc
01 83f2dbd4 83e7d018 83f30d20 83f3a380 00000000 nt!KiExecuteAllDpcs+0xf9
02 83f2dc20 83e7ce38 00000000 0000000e 00000000 nt!KiRetireDpcList+0xd5
03 83f2dc24 00000000 0000000e 00000000 00000000 nt!KiIdleLoop+0x38
然后读取端口信息,将信息传递给Win32k。i8042键盘和鼠标的驱动在DDK中有例子,如果感兴趣可以看一下DDK的例子更加明确。
6. 总结
从上面分析我们可以知道针对鼠标和键盘有两种处理方案。
首先使用SetWindowsHookExW
来设置鼠标键盘钩子(或者低级钩子),这样就可以在鼠标和键盘响应之前捕获消息,但是如果需要针对CTRL + ALT + DELETE键进行处理,那这两个操作就达不到要求了。
鉴于SetWindowsHookExW
的局限,网上有人实现了一种比较偏门的键盘热键拦截的方案,就是HOOK函数xxxKeyEvent
,从上面的分析我们可以发现,这个函数在所有键盘事件处理之前,因此肯定可以拦截所有消息了。但是对于这种方案的稳定性和兼容性本人没法做相关保证,因此如果正式项目中还是谨慎使用。