鼠标宏设计之三-虚拟鼠标的设计

这个系列前几篇文章分别描述了光电鼠标、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));
}

你可能感兴趣的:(游戏辅助,鼠标键盘的实现,计算机外设)