这个系列前几篇文章分别描述了光电鼠标、HID硬件鼠标,它们相对于主机来说,是在鼠标硬件层面的实现,那么其实也有相对主机来说的软件方案。
注意: 这篇文章并不是囊括所有这方面的软件方案的原理描述,实际上由于本人的技术局限性,也许有的方案我自己也没有了解过,遗漏在所难免!
在软件设计中,某些时候跟踪数据流是一个剖析整个体系的好办法,由于鼠标和键盘最终就是数据流的船体,所以我们跟踪数据流,会对整个方案有一些了解,
鼠标数据通过PCI总线,如果是USB鼠标,这个数据最终回到USB总线控制器上,然后下发给对应的HID驱动,HID驱动将鼠标的数据进行处理后,转发给操作系统,我怀疑这里是给了窗口管理器,不过我没时间去详细调试。
在收到消息之后,系统会根据鼠标目前的位置知道要发给哪个窗口,然后使用SendMessage或者PostMessage一类的将消息投递对应的线程的队列中,然后唤醒线程,让它去处理这个事件。
这个过程中,有些是系统公开的,有些是需要调试的,所以我们可以把它理解为一个数据链,在这个数据链上的任意节点都可以实现虚拟鼠标和键盘。不过为了方便,我们简单将它们分为应用层方案和内核层方案,同时我们避免讨论Hook技术对方案的影响。
应用层方案本质上是对Post/SendMessage/sendInput(mouse_event/keybd_event)的处理,对于任意窗口来说,它们都可以通过SendMessage之类的函数接收到鼠标键盘消息。
//**********************************************************************
//
// Sends Win + D to toggle to the desktop
//
//**********************************************************************
void ShowDesktop()
{
OutputString(L"Sending 'Win-D'\r\n");
INPUT inputs[4] = {};
ZeroMemory(inputs, sizeof(inputs));
inputs[0].type = INPUT_KEYBOARD;
inputs[0].ki.wVk = VK_LWIN;
inputs[1].type = INPUT_KEYBOARD;
inputs[1].ki.wVk = 'D';
inputs[2].type = INPUT_KEYBOARD;
inputs[2].ki.wVk = 'D';
inputs[2].ki.dwFlags = KEYEVENTF_KEYUP;
inputs[3].type = INPUT_KEYBOARD;
inputs[3].ki.wVk = VK_LWIN;
inputs[3].ki.dwFlags = KEYEVENTF_KEYUP;
UINT uSent = SendInput(ARRAYSIZE(inputs), inputs, sizeof(INPUT));
if (uSent != ARRAYSIZE(inputs))
{
OutputString(L"SendInput failed: 0x%x\n", HRESULT_FROM_WIN32(GetLastError()));
}
}
在我看来sendInput是对SendMessage系列函数的封装。
我们可以在应用层这么做,但是这些都会有些限制,游戏本身也是在应用层,这个层面无限PK最终回到内核层。
在内核层,事情会简单许多,任何属于Mouse类的设备对象都可以直接上报鼠标数据到窗口管理器,重点就在于打算怎么做?
1. 创建一个虚拟的mouse驱动,然后挂载即可,同时在标准的IOCTL之外创建一些非标准的IOCTL,用来接收应用层的通讯请求。
2. 创建一个USB过滤驱动,然后挂载在鼠标驱动之上,同时在标准的IOCTL之外创建一些非标准的IOCTL,用来接收应用层的通讯请求。
3. 使用libusb驱动作为过滤驱动,然后和应用层通讯即可。
内核驱动会比想象中要简单许多,但是需要对协议很了解才行。下面是I/O函数,这个案例是微软的鼠标驱动,可以作为参考,这部分代码后续我会上传。
目前已经将完整的代码上传:
VOID
MouFilter_EvtIoInternalDeviceControl(
IN WDFQUEUE Queue,
IN WDFREQUEST Request,
IN size_t OutputBufferLength,
IN size_t InputBufferLength,
IN ULONG IoControlCode
)
/*++
例程描述:
此例程是内部设备控制请求的调度例程。有两种特定的控制代码值得关注:
IOCTL_INTERNAL_MOUSE_CONNECT:
存储旧的上下文和函数指针,并将其替换为我们自己的。这比拦截RIT发送的IRP要简单得多,在返回途中对其进行修改。
IOCTL_INTERNAL_I8042_HOOK_MOUSE:
添加必要的函数指针和上下文值,这样我们就可以更改ps/2鼠标的初始化方式。
注意:如果您所要做的就是过滤MOUSE_INPUT_DATA。您可以删除处理代码和所有相关的设备扩展字段,以及起到节省空间的作用。
--*/
{
PDEVICE_EXTENSION devExt;
PCONNECT_DATA connectData;
PINTERNAL_I8042_HOOK_MOUSE hookMouse;
NTSTATUS status = STATUS_SUCCESS;
WDFDEVICE hDevice;
size_t length;
UNREFERENCED_PARAMETER(OutputBufferLength);
UNREFERENCED_PARAMETER(InputBufferLength);
PAGED_CODE();
hDevice = WdfIoQueueGetDevice(Queue);
devExt = FilterGetData(hDevice);
switch (IoControlCode)
{
// 将鼠标类设备驱动程序连接到端口驱动程序。
case IOCTL_INTERNAL_MOUSE_CONNECT:
// 只允许一个连接
if (devExt->UpperConnectData.ClassService != NULL)
{
status = STATUS_SHARING_VIOLATION;
break;
}
// 将连接参数复制到设备扩展名。
status = WdfRequestRetrieveInputBuffer(Request,
sizeof(CONNECT_DATA),
&connectData,
&length);
if(!NT_SUCCESS(status))
{
DebugPrint(("WdfRequestRetrieveInputBuffer failed %x\n", status));
break;
}
devExt->UpperConnectData = *connectData;
// 钩住报告链。每次向报告鼠标数据包时
// 系统将调用MouFilter_ServiceCallback
connectData->ClassDeviceObject = WdfDeviceWdmGetDeviceObject(hDevice);
connectData->ClassService = MouFilter_ServiceCallback;
break;
// 断开鼠标类设备驱动程序与端口驱动程序的连接。
case IOCTL_INTERNAL_MOUSE_DISCONNECT:
// 清除设备扩展中的连接参数。
// devExt->UpperConnectData.ClassDeviceObject = NULL;
// devExt->UpperConnectData.ClassService = NULL;
status = STATUS_NOT_IMPLEMENTED;
break;
// 将此驱动程序附加到的初始化和字节处理
// i8042(即PS/2)鼠标。只有当你想进行PS/2时,这才是必要的
// 特定函数,否则挂接CONNECT_DATA就足够了
case IOCTL_INTERNAL_I8042_HOOK_MOUSE:
DebugPrint(("hook mouse received!\n"));
// 从请求中获取输入缓冲区
// (Parameters.DeviceIoControl.Type3InputBuffer)
status = WdfRequestRetrieveInputBuffer(Request,
sizeof(INTERNAL_I8042_HOOK_MOUSE),
&hookMouse,
&length);
if(!NT_SUCCESS(status))
{
DebugPrint(("WdfRequestRetrieveInputBuffer failed %x\n", status));
break;
}
// 设置isr例程和上下文,并记录该驱动程序之上的任何值
devExt->UpperContext = hookMouse->Context;
hookMouse->Context = (PVOID) devExt;
if (hookMouse->IsrRoutine) {
devExt->UpperIsrHook = hookMouse->IsrRoutine;
}
hookMouse->IsrRoutine = (PI8042_MOUSE_ISR) MouFilter_IsrHook;
// 存储我们将来可能需要的所有其他功能
devExt->IsrWritePort = hookMouse->IsrWritePort;
devExt->CallContext = hookMouse->CallContext;
devExt->QueueMousePacket = hookMouse->QueueMousePacket;
status = STATUS_SUCCESS;
break;
// 可能想在未来多做点什么。现在,把这些I/O请求传下去
// 堆栈。这些查询必须成功,RIT才能进行通信
// 用鼠标。
case IOCTL_MOUSE_QUERY_ATTRIBUTES:
default:
break;
}
if (!NT_SUCCESS(status)) {
WdfRequestComplete(Request, status);
return ;
}
MouFilter_DispatchPassThrough(Request,WdfDeviceGetIoTarget(hDevice));
}