Windows蓝牙驱动开发之模拟HID设备(一)(把Windows电脑模拟成蓝牙鼠标和蓝牙键盘等设备)

by fanxiushu 2024-03-14 转载或引用请注明原作者

把Windows电脑模拟成蓝牙鼠标和蓝牙键盘,简单的说,就是把笨重的PC电脑当成鼠标键盘来使用。

这应该是一个挺小众的应用,但有时感觉也应该算比较好玩吧,

毕竟实现一种一般人都感觉没戏的功能,

尤其是windows平台中,并不是简单实现蓝牙服务端,还得做一个小小的破解。

windows电脑当成蓝牙鼠标键盘,这个功能有什么用呢?

可以用来控制手机啊!我最初研究把windows当成蓝牙鼠标键盘,确实是出于这个目的。

我们先来看看手机系统,Android和iOS,

Android不知道从哪个版本开始,已经可以提供用户层API,可以直接调用对应接口,

直接在程序中进行 touch,mouse,keyboard 等控制模拟。也就是说,Android系统是可以通过编程实现被远程控制的。

但iOS比较特殊,至今都没看到集成这样的用户层API来进行输入控制模拟。

因此iOS是没法像目前的通用远程控制软件那样,直接远程控制iOS系统。当然,只是投屏的话是没问题的。

以前开发iOS系统下的xdisp_virt远程控制程序,苦于找不到对应的输入模拟接口,只能把xdisp_virt当成投屏来使用。

后来发现iOS系统虽然不提供对应接口,但是却支持蓝牙鼠标键盘。

于是就打起了模拟蓝牙鼠标键盘的主意。

当然,蓝牙鼠标键盘模拟,不等同于普通意义上的远程控制,因为蓝牙传输距离有限 ,也就顶多几十米。

我们也可以使用一个笨办法:

使用linux系统电脑(因为linux下的蓝牙鼠标键盘更好模拟)模拟出蓝牙鼠标键盘,然后连上iOS苹果手机。

然后linux系统模拟的蓝牙鼠标键盘程序再通过普通网络把控制事件传输出去,这样就能达到普通意义上的远程控制了。

就是麻烦了些。

回到正题,如何在windows平台实现蓝牙鼠标键盘呢?

一般桌面电脑模拟设备终端,都有天然的障碍,因为当初设计就不是为了实现设备功能的。

比如要把桌面电脑模拟成 USB 设备,需要底层硬件支持,需要有UDC硬件控制器。

有了UDC底层硬件还不够,还得系统提供对应接口,

好在linux内核很早前就提供了UDC对应系统接口,windows10以上的系统也提供了UDC接口。

值得庆幸的是蓝牙设计的时候,同时提供了服务端和客户端,也就是同时提供了两端。

这也挺好理解,蓝牙本质就是无线传输,如果蓝牙传输堆栈只提供客户端或者只提供服务端,就像缺胳膊少腿一样。

蓝牙鼠标和蓝牙键盘是作为蓝牙服务端对外提供服务的。而且是作为HID标准的输入设备。

因此本文也只阐述蓝牙服务端的实现过程,至于蓝牙客户端如何实现,可以去查阅WDK下的例子代码。

具体例子代码在bluetooth或者bth目录下的bthecho目录,它同时演示了服务端和客户端,以及如何安装。

例子代码是实现自己的上层传输(ECHO回显),但是作为蓝牙鼠标键盘,其上层传输协议是固定和公开的。

这也是与例子不同的地方,除此之外,流程什么的都是一样的。

开发windows蓝牙服务端,需要实现以下几个部分:

1,初始化驱动,获取 BTH_PROFILE_DRIVER_INTERFACE,

BTHDDI_SDP_PARSE_INTERFACE,BTHDDI_SDP_NODE_INTERFACE

等三个接口,里边全是接口函数,用于后面处理,其中第一个接口主要是BRB分配和释放,第二,三个用于生成SDP信息

2,注册PSM,因为蓝牙鼠标键盘(就是统一的HID输入设备)的PSM是固定的 0x11 和 0x13, 其中0x11用于传输控制信息,

0x13传输具体的鼠标键盘事件。

但是windows系统把0x11和0x13作为保留值,也就是说,我们在自己的蓝牙驱动中,是无法注册这两个值的。

这就是windows最大的坑,而且是一开始就让你觉得没戏的坑。

因为它无法通过修改某些配置信息改变,而是被硬编码到windows系统组件中。

3,注册 L2CAP Server, 并且设置接收回调函数,也就是说如果有蓝牙客户端连上来,这个回调函数就会被调用,

从而建立起连接,传输数据。

4,创建并且发布带有 HID 报告描述的 SDP 。

5,从第3步骤注册的L2CAP Server的回调函数中,接收到蓝牙客户端的连接请求,然后回复之后,连接就建立起来了。

客户端会发起 0x11 和 0x13 共两条连接,根据 4 步骤的HID SDP配置,会在0x11控制传输中收到某些控制命令,

响应这些命令,然后就可以通过 0x13这个连接,发送固定格式的鼠标键盘事件数据。

通过已上步骤,一个蓝牙鼠标键盘就模拟成功了。

通过以上我们也能发现,这个跟socket网络编程的服务端很像:

第1步骤就像是创建socket, 第2步骤是bind绑定socket,第3,4步骤在listen,

第5步骤就是 accept了。最后就是send和recv 了。

当然,为了更好的理解和开发蓝牙鼠标键盘驱动,我们还得去网上下载 蓝牙的 HID规范文档,

因为里边规定了蓝牙HID数据传输格式,SDP协议格式等。

同时也得准备windows的WDK开发包中 tools目录下的 bluetooth ,其中 sdpverify.exe 可以帮我们查看 SDP协议格式,

而蓝牙HID 格式内容较多,光看规范文档,是很头大的,所以还不如找个现成的蓝牙鼠标,然后用sdpverify程序查看SDP格式,

再然后仿照它建立自己的HID SDP 。

首先初始化蓝牙驱动,这就按照一般的 即插即用wdm驱动开发就可以了,

需要特别主意的是,它的安装方式比较特别,

我们需要使用 应用层WIN32API 函数 BluetoothSetLocalServiceInfo 创建一个底层设备,然后再把我们的驱动安装上去,

只有这样我们的驱动才是蓝牙驱动,才能获取到BTH_PROFILE_DRIVER_INTERFACE等接口。

接着在驱动的AddDevice初始化函数中,获取到BTH_PROFILE_DRIVER_INTERFACE等接口。

如果你是使用KMDF框架的(微软例子里也是KMDF框架)可以直接使用 WdfFdoQueryForInterface 函数获取。

而我的驱动是基于WDM的(以下阐述的都是基于WDM实现的蓝牙驱动)。所以得自己实现,其实也不难。如下:

NTSTATUS query_interface(PDEVICE_OBJECT device_object,

LPCGUID InterfaceType, PINTERFACE Interface,

USHORT Size, USHORT Version, PVOID InterfaceSpecificData)

{

NTSTATUS status = STATUS_SUCCESS;

KEVENT event;

PIRP irp;

IO_STATUS_BLOCK ioStatusBlock;

PIO_STACK_LOCATION irpStack;

KeInitializeEvent(&event, NotificationEvent, FALSE);

irp = IoBuildSynchronousFsdRequest(IRP_MJ_PNP,

device_object,

NULL,

0,

NULL,

&event,

&ioStatusBlock);

if (irp == NULL) {

status = STATUS_INSUFFICIENT_RESOURCES;

return status;

}

irpStack = IoGetNextIrpStackLocation( irp );

irpStack->MinorFunction = IRP_MN_QUERY_INTERFACE;

irpStack->Parameters.QueryInterface.InterfaceType =

(LPGUID)InterfaceType;

irpStack->Parameters.QueryInterface.Size = Size;

irpStack->Parameters.QueryInterface.Version = Version;

irpStack->Parameters.QueryInterface.Interface =

(PINTERFACE)Interface;

irpStack->Parameters.QueryInterface.InterfaceSpecificData = InterfaceSpecificData;

//

// Initialize the status to error in case the bus driver does not

// set it correctly.

irp->IoStatus.Status = STATUS_NOT_SUPPORTED ;

status = IoCallDriver( device_object, irp );

if (status == STATUS_PENDING) {

status = KeWaitForSingleObject( &event, Executive, KernelMode, FALSE, NULL);

status = ioStatusBlock.Status;

}

return status;

}

然后如下调用

status = query_interface(fdo->LowerDeviceObject,

&GUID_BTHDDI_SDP_PARSE_INTERFACE, (PINTERFACE)&fdo->sdp_parse_interface,

sizeof(fdo->sdp_parse_interface), BTHDDI_SDP_PARSE_INTERFACE_VERSION_FOR_QI, NULL);

就获取到了 BTH_PROFILE_DRIVER_INTERFACE 接口,使用同样办法获取其他两个用于操作 SDP 的接口。

同USB驱动类似, 蓝牙驱动使用 BRB 结构来传输数据,因此我们先实现一些通用函数,比如如下同步提交brb的函数:

///同步提交BRB

NTSTATUS bth_sync_call_driver(PDEVICE_OBJECT device_object, PVOID Brb )

{

NTSTATUS status = STATUS_SUCCESS;

KEVENT event;

IO_STATUS_BLOCK ioStatus;

KeInitializeEvent(&event, NotificationEvent, FALSE);

PIRP irp = IoBuildDeviceIoControlRequest(IOCTL_INTERNAL_BTH_SUBMIT_BRB,

device_object, NULL, 0, NULL,

0, TRUE, &event, &ioStatus);

///

if (!irp){

return STATUS_INSUFFICIENT_RESOURCES;

}

PIO_STACK_LOCATION nextStack = IoGetNextIrpStackLocation(irp);

nextStack->Parameters.Others.Argument1 = Brb;

status = IoCallDriver(device_object, irp);

if (status == STATUS_PENDING){

KeWaitForSingleObject(&event, Executive, KernelMode, FALSE, NULL); /// wait for ever

status = ioStatus.Status;

}

return status;

}

然后还可以实现异步传输 NTSTATUS bth_async_call_driver(

PDEVICE_OBJECT device_object, PVOID Brb,

void(*brb_complete)(brb_async_context* brb_ctx), PVOID ctx)

其中BrB就是BRB指针,brb_complete 是当完成这个brb请求时候的完成函数。brb_async_context是自己定义的结构体。

这里不再罗嗦列出代码了。

当初始化驱动完成,我们就需要开始注册 PSM了。这些需要注册 0x11和0x13,两个都需要注册。

PSM注册伪代码如下:

struct _BRB_PSM * brb;

brb = (struct _BRB_PSM*)brb_alloc(fdo, bRegister ? BRB_REGISTER_PSM : BRB_UNREGISTER_PSM );

brb->Psm = PSM; // 这里应该是 0x11和0x13,两个都需要分别注册,

status = bth_sync_call_driver(fdo->LowerDeviceObject, brb);

brb_free(fdo, brb);

是的,注册PSM就这么简单,上面代码把注册和注销放到一起了。

但是注册0x11和0x13,肯定不会成功,既然这个都不会成功,那么接下来注册L2CAP等步骤也没有了意义。

这是因为在PSM注册过程中,windows系统组件会对PSM值做判断,如果是系统保留的,自然就会失败。

因此,为了保证注册成功,我们就得想办法让这个判断失效,

这等于是在做破解,不过破解的不是应用层的dll,

而是驱动sys文件,具体完成这个注册的系统组件是 bthport.sys ,bthport.sys如同应用层的dll一样,

是个扩展库,主要是帮忙实现蓝牙驱动的核心功能,比如注册,传输等各类都是在bthport.sys中完成。

所以可以对bthport.sys做逆向,比如使用 IDA工具软件进行分析,然后就会发现 里边有个未公开的函数

BthIsSystemPSM ,就是判断是否是系统保留的PSM,我们只要让这个函数失效,自然就能成功注册0x11和0x13了。

下面的连接(就有阐述如何破解BthIsSystemPSM函数)
Windows Kernel | Nadav's Blog

至于关于这方面的更多的内容,以后的章节会详细阐述。

接着,我们需要注册L2CAP Server,注册这个不会有什么系统保留限制,只要不出其他问题,都会成功

如下伪代码,注册L2CAP Server:

struct _BRB_L2CA_REGISTER_SERVER *brb;

brb = (struct _BRB_L2CA_REGISTER_SERVER*)brb_alloc(fdo, BRB_L2CA_REGISTER_SERVER);

brb->BtAddress = BTH_ADDR_NULL;

brb->PSM = 0; //we have already registered the PSM

brb->IndicationCallback = &SrvIndicationCallback;

brb->IndicationCallbackContext = userCtx;

brb->IndicationFlags = 0;

brb->ReferenceObject = fdo->DeviceObject;

status = bth_sync_call_driver(fdo->LowerDeviceObject, brb ); //调用同步提交BRB函数,

fdo->L2CAPServerHandle = brb->ServerHandle; /// save ptr,保持此句柄,再注销的时候会使用到

brb_free(fdo, brb);

static void SrvIndicationCallback(

__in PVOID Context,

__in INDICATION_CODE Indication,

__in PINDICATION_PARAMETERS Parameters

{

bth_hid_user_t* user = (bth_hid_user_t*)Context;

switch (Indication)

{

case IndicationRemoteConnect: 有客户端连接上来,

{

DPT("@@@@@@ Ctrl Connect --- PSM=0x%X; addr=%p\n",

Parameters->Parameters.Connect.Request.PSM,

Parameters->BtAddress );

bth_connect_remote(user, Parameters); 调用我们的函数,初始化有新客户端连上的各种结构,并且回复客户端

break;

}

}

}

以上就是注册L2CAP Server的过程,接下来就是如何创建 HID SDP 和发布SDP了。

HID SDP 的内容有点多,创建起来有点麻烦,以下是我的HID SDP内容:

包括的内容基本如上图所描述的那样,其中红色框中的 HID Descriptor List 包含的就是HID REPORT DESC

这个就是 HID REPORT DESC 则是标准的HID报告描述符,关于这个细节可以去查阅HID的规范文档。

至于如何生成 SDP报告, 则是全程使用 BTHDDI_SDP_NODE_INTERFACE 来构建各种NODE,

最终合并到 PSDP_TREE_ROOT_NODE 根 root tree 中,代码调用细节可以去查阅 微软的bthecho例子代码。

然后调用 BTHDDI_SDP_PARSE_INTERFACE 接口中的 SdpConvertTreeToStream 函数把root tree序列化为 stream,

假设序列化为Stream, 大小为StreamSize,再通过 IOCTL_BTH_SDP_SUBMIT_RECORD 把这个SDP Publish出去,

让蓝牙客户端能够看到我们的蓝牙HID设备,伪代码如下:

KEVENT event;

IO_STATUS_BLOCK ioStatus;

KeInitializeEvent(&event, NotificationEvent, FALSE);

HANDLE_SDP handle = HANDLE_SDP_NULL;

PIRP irp = IoBuildDeviceIoControlRequest(IOCTL_BTH_SDP_SUBMIT_RECORD, / 发布SDP的 IOCTL

fdo->LowerDeviceObject, Stream, StreamSize, &handle,

sizeof(HANDLE_SDP), FALSE, &event, &ioStatus);

status = IoCallDriver(fdo->LowerDeviceObject, irp);

if (status == STATUS_PENDING) {

KeWaitForSingleObject(&event, Executive, KernelMode, FALSE, NULL); /// wait for ever

status = ioStatus.Status;

}

至此之后,我们就可以安心等待蓝牙客户端连上来。

当有蓝牙客户端连上来之后,上面注册L2CAP Server时候设置的SrvIndicationCallback回调函数就会被调用。

我们响应 IndicationRemoteConnect 请求,在这个请求中,我们需要构建 _BRB_L2CA_OPEN_CHANNEL 的 BRB,

然后回答给客户端,只有这样才能真正建立起一条新的连接。

大致伪代码如下:

NTSTATUS bth_connect_remote(bth_hid_user_t* user, PINDICATION_PARAMETERS Parameters)

{

。。其他初始化代码

_BRB_L2CA_OPEN_CHANNEL* brb

brb = (_BRB_L2CA_OPEN_CHANNEL*)brb_alloc(user->fdo, BRB_L2CA_OPEN_CHANNEL_RESPONSE);

///init brb

brb->Hdr.ClientContext[0] = client; /client是我们新建的结构体,代表一个蓝牙连接

brb->BtAddress = Parameters->BtAddress;

brb->Psm = Parameters->Parameters.Connect.Request.PSM;

brb->ChannelHandle = Parameters->ConnectionHandle;

brb->Response = CONNECT_RSP_RESULT_SUCCESS;

brb->ChannelFlags = CF_ROLE_EITHER;

brb->ConfigOut.Flags = 0;

brb->ConfigIn.Flags = 0;

brb->ConfigOut.Flags |= CFG_MTU;

brb->ConfigOut.Mtu.Max = L2CAP_DEFAULT_MTU;

brb->ConfigOut.Mtu.Min = L2CAP_MIN_MTU;

brb->ConfigOut.Mtu.Preferred = L2CAP_DEFAULT_MTU;

brb->ConfigIn.Flags = CFG_MTU;

brb->ConfigIn.Mtu.Max = brb->ConfigOut.Mtu.Max;

brb->ConfigIn.Mtu.Min = brb->ConfigOut.Mtu.Min;

brb->ConfigIn.Mtu.Preferred = brb->ConfigOut.Mtu.Max;

//

// Get notifications about disconnect

//设置对方断开连接的时候的回调函数

brb->CallbackFlags = CALLBACK_DISCONNECT;

brb->Callback = &BthSvrConnectionIndicationCallback;

brb->CallbackContext = client;

brb->ReferenceObject = user->fdo->DeviceObject;

/// 采用异步方式调用BRB,以免阻塞系统的回调函数,

status = bth_async_call_driver(user->fdo->LowerDeviceObject, brb, open_channel_response_complete, client);

。。。。

}

static void

BthSvrConnectionIndicationCallback(

__in PVOID Context,

__in INDICATION_CODE Indication,

__in PINDICATION_PARAMETERS Parameters

)

{

。。。。

switch(Indication)

{

case IndicationRemoteDisconnect: / 客户端已经关闭了此连接,我们也需要关闭以及释放相关结构

。。。。

break;

}

}

static void open_channel_response_complete( brb_async_context* brbctx)

{

NTSTATUS status = brbctx->Irp->IoStatus.Status;

。。。。 回答客户端已经完成,通过判断 status 来确定是否已经成功建立了连接。

if (NT_SUCCESS(status)) { /// response success..

client->handle = brb->ChannelHandle; 需要使用此handle 来接收和发送蓝牙数据包

client->address = brb->BtAddress; 远端蓝牙地址

client->OutMTU = brb->OutResults.Params.Mtu;

client->InMTU = brb->InResults.Params.Mtu;

。。。。其他处理。。。。

}

}

至此,客户端发起的一个连接就建立了起来,我们可以通过这个连接收和发送蓝牙数据。'

未完待续。。。

相关推荐
武汉万象奥科2 小时前
什么是UGFC?模块电脑(核心板)规范标准简介四
电脑·rk3568·rk3562·ugfc
易我数据恢复大师2 小时前
忘记家里的WiFi密码后,如何从电脑和手机上查密码?
wifi·电脑·安卓手机·wifi密码·ios手机
今天我刷leetcode了吗2 小时前
docker 配置同宿主机共同网段的IP 同时通过通网段的另一个电脑实现远程连接docker
tcp/ip·docker·电脑
科技象限2 小时前
电脑禁用U盘的四种简单方法(电脑怎么阻止u盘使用)
大数据·网络·电脑
2202_754421544 小时前
一个计算频率的模块
驱动开发·fpga开发
系统之家装机大师4 小时前
Win11 22H2/23H2系统11月可选更新KB5046732发布!
windows·电脑
系统之家装机大师4 小时前
微软发布Win11 24H2系统11月可选更新KB5046740!
windows·电脑
Name_NaN_None5 小时前
Win11 24H2新BUG或影响30%CPU性能,修复方法在这里
科技·电脑·游戏机
戎梓漩6 小时前
windows下安装curl,并集成到visual studio
ide·windows·visual studio
蓝田~8 小时前
观察者模式和订阅模式
windows·观察者模式