by fanxiushu 2018-09-16 转载或引用请注明原始作者。
这里讨论的都是win7以上平台的WDDM模型的显卡驱动,而不是WINXP之前的XPDM模型的显卡驱动。
实际上没有"显卡过滤驱动"一说, windows 10 1607之前的平台也从来没有支持过显卡过滤驱动这一框架。
而有些应用又不得不获取显卡底层数据。
比如扩展windows桌面,虽然显卡本身支持连接多个显示器,能做到扩展桌面的效果,
但是这局限于HDMI,Display-Port,VGA等接口线,
而如果想采用其他接口,比如通过USB,通过网线通讯等更广泛和廉价的接口,
(比如USB显卡,把显卡的显示通过USB接口线传输到其他设备上等。)
除了显卡硬件本身支持外(显然现在的显卡都不支持网线,USB接口等),就只能采用其他途径来解决问题。
要解决这个问题:
一种办法是开发虚拟显卡,让虚拟显卡和真实显卡一起工作,虚拟显卡扩展windows桌面到网线或USB,
这种办法以后会逐步讲解。
还有一种办法就是本文即将讲述的:给显卡挂载一个过滤驱动,拦截和伪造显卡各种请求,
从而给显卡虚拟出一个额外的显示终端(可以简单理解成虚拟显示器,下同)。
而这个所谓的过滤驱动,其实不是windows提供的标准框架,而是采用类似黑客的HOOK技术,
HOOK某些回调函数,改写这些回调函数的行为,从而达到目的。
还有一种应用,云桌面的问题:
在一台物理服务器上,开辟10几个虚拟机系统,当然每个虚拟机系统可以使用虚拟显卡,这对一般办公使用没有大问题,
但是如果要求更高性能的显示效果,就不得不使用其他办法,
比如 vGPU 方式,物理宿主机上的硬件显卡虚拟出多个显卡给虚拟机使用,性能跟使用真实硬件差不多。
还有一种更廉价的办法:显卡透传。
说的这么专业,其实就是给物理宿主机器插上多个真实显卡,让每块显卡分配给每个虚拟机独占享用。
这样虚拟机的显示性能跟插上一块真实显卡没什么区别。
有了这么一块透传的显卡,在虚拟机安装的也是对应厂商的硬件驱动,接着的问题就是如何采集显示数据。
当然我们在WIN10平台有DXGI采集,或者Indirect DIsplay Driver,(下面会简单讲解),
可是在win7平台,只能使用mirror驱动,
可是mirror驱动的局限性,不能利用硬件加速,透传显卡的性能没能充分压榨。
而且一般的显卡还有一个特性,一般情况下,得给它插上显示器,显卡驱动才能处于正常的工作状态,才能设置各种分辨率等。
如果我们给物理宿主机上的10多个显卡都插上10几个显示器,这种场景该是多么的壮观!
于是我们给显卡驱动挂载过滤驱动,让它虚拟出一个显示终端,这样就不用插真实的显示器,显卡的驱动也能正常工作了。
而且还能从过滤驱动获取到显示的图像数据。
当然从 windows 1607 以后的版本,支持一种 Indirect DIsplay Driver,这个是得到微软官方支持的框架,
能在现有的显卡上生成一个额外的显示终端,这个额外的显示终端不再是真实显卡上的HDMI,DIsplay-Port等接口终端,
而是可以其他接口比如USB接口。具体使用什么接口,由Indirect DIsplay Driver开发者决定。
也许是微软已经意识到上面的这些需求问题,才在最新的系统中增加了这样的功能。
更具体的描述请看如下链接:
https://docs.microsoft.com/en-us/windows-hardware/drivers/display/indirect-display-driver-model-overview
我们再看看 Indirect DIsplay Driver, Indirect Display Driver是 UMD驱动,就是属于应用层驱动。
从windows10 1607以上的操作系统的 \windows\system32\drivers 驱动目录下,我们可以找到一个叫 IndirectKmd.sys 的驱动文件。
这个驱动文件就是 Indirect Display Driver 对应的内核驱动,再打开这个文件的属性,会看到文件说明描述为:
“Indirect displays kernel-mode filter driver” 。很显然,这个就是被微软官方新增加的真正意义上显卡过滤驱动。
我们不能直接使用IndirectKmd.sys,只能通过 Indirect Display Drive框架间接使用。
Indirect Display Driver运行的时候, IndirectKmd.sys 会被挂载到真实显卡驱动上去,从而达到虚拟出额外显示终端的效果。
而在windows 7,windows 8 ,windows10 1607以前的版本,这样的平台,很不幸,没有这样的东西。
于是我们得自己制造一个“显卡过滤驱动”。
开始之前,我们先来看看显卡驱动开发的大致流程。
先看下图,这图摘自MSDN文档:
看这个图估计还是不明白什么意思.
其实我们开发windows应用层用户界面的程序的时候,多少都会跟图形库打交道。
估计大家最熟悉的就是GDI图形库,而游戏开发人员最熟悉应该就是DirectX和OpenGL图像库。
GDI,DirectDX,OpenGL这三种图形库就是Windows为我们提供的基础图形库。
GDI是为了兼容以前的老系统,同时为了兼容目前绝大部分普通界面的应用程序,其实一般人用用GDI基本就足够了。
GDI通过调用驱动的 win32k.sys 来模拟画图,熟悉mirror镜像驱动开发的都应该知道,
win32k.sys导出的一大堆的Eng*前缀的函数,就是利用CPU模拟画图的函数。
GDI也可以利用显卡硬件加速,DRIVER_INITIALIZATION_DATA结构导出的DxgkDdiRenderKm 回调函数就是在显卡里加速GDI的。
DirectX和OpenGL图形库都需要在应用层提供用户模式的驱动,
目的其实就是在应用层渲染图像,这样大量的图像相关计算全在用户层完成。
最终的图像都会通过系统的dxgkrnl.sys驱动,提交给内核模式的显卡驱动去管理和分配。
内核模式的驱动主要功能就是对资源的管理分配,显存和内存之间的DMA数据传输, GPU管理等。
除了GDI加速外,所有复杂图形渲染加速,都交给应用层驱动去完成。
可能这里会有一个疑问:
比如有三个窗口模式的程序,一个是DX开发的游戏,另一个是OpenGL开发的游戏,还有一个是GDI做的普通界面程序。
然后另外一个程序利用 GDI库的 BitBlt 抓取桌面屏幕,居然把三个窗口所有图像都截取到了。
三个互不相关的图形库,如何融合到一起并且被BitBlt抓取到所有图像?
其实WDDM是个混合模型,也就是三种不同图形库的程序生成的图像,最终会到 dwm.exe桌面窗口管理器混合。
混合成统一的图像输出。这样不管是哪种图形库开发的程序,最终都合并到一起了。
BitBlt函数最终获取到混合之后的图像数据。
当然,DirectX屏幕独占模式的程序,则是另外一回事,这也是绝大部分抓屏软件无能为力的事。
我们这里只简单了解内核模式的驱动的大致流程:
在DriverEntry驱动入口函数里,
初始化 DRIVER_INITIALIZATION_DATA 数据结构,
然后调用 DxgkInitialize 函数注册,初始化就完成了,就这么简单,然而真的简单吗?
当你打开 DRIVER_INITIALIZATION_DATA 数据结构的声明,看到至少七八十个的回调函数,
希望你不会立马晕厥,当然如果真的晕厥了得马上就医。
而且WDDM模型,从WIN7 的1.1版本开始,到WIN8 的1.2和1.3版本,再到 WIN10 的 2.0, 2.1, 2.2, 2.3版本,
每个版本都会朝里边塞上更多的回调函数,真亏微软的开发者们能想的出来。
WDDM模型依然是符合WDM规范的miniport小端口驱动,有电源管理,PNP即插即用等。
查看DRIVER_INITIALIZATION_DATA 里边的回调函数,大致可以把他们分成三大类。
一,普通的WDM驱动的回调函数,比如 DxgkDdiAddDevice, DxgkDdiStartDevice等,
一看就明白对应的是 WDM的AddDevice函数和 IRP_MN_START_DEVICE 请求。
二,DDI函数,就是图像相关的,比如图像资源分配,画鼠标形状等。
三,VIDPN函数,用于管理VIDPN。 我们要模拟出一个额外显示终端,就必须改造VIDPN相关函数。
现在关键的问题,如何开发我们的 “显卡过滤驱动” ?
所有显卡,不管是Intel,NvidIA,ATI等,
他们驱动的DriverEntry入口函数都会调用 DxgkInitialize 注册 DRIVER_INITIALIZATION_DATA 数据结构,
我们只要获取DRIVER_INITIALIZATION_DATA 数据结构,就完全掌握他们实现的所有回调函数。
可能首先想到的就是挂钩 DxgkInitialize 系统函数,可是非常可惜,DxgkInitialize 不是某个系统模块导出的接口函数,
而是被静态编译进程序,于是只好逆向工程,分析DxgkInitialize 函数的调用过程,从而找到破解规律。
WDDM出来很长时间,早就已经有人找到规律了,这里也就不再罗嗦,有兴趣可以自己去逆向DxgkInitialize 函数。
DxgkInitialize 会在内部调用ZwLoadDriver 加载 dxgkrnl.sys系统模块,
dxgkrnl.sys的服务在注册表固定为 \REGISTRY\MACHINE\SYSTEM\CURRENTCONTROLSET\SERVICES\DXGKrnl 。
这个就是Microsoft DirectX graphics kernel subsystem,用于管理显卡的miniport小端口驱动,
同时跟应用层的DirectX,OpenGL等通讯。
而dxgkrnle.sys与显卡小端口驱动之间,基本上是通过互相提供回调函数来通讯的。举个例子,
显卡调用 DxgkInitialize 注册DRIVER_INITIALIZATION_DATA ,几十个或者上百个回调函数就注册到dxgkrnl.sys中,
同时在 DxgkDdiStartDevice回调函数的参数,提供一个 PDXGKRNL_INTERFACE参数,
这个参数就是dxgkrnl.sys提供给显卡驱动的回调函数集合,用于显卡驱动调用dxgkrnl.sys提供的回调函数。
dxgkrnl.sys和显卡驱动,就是通过这种机制,互相调用对方提供的回调函数,从而达到通讯的目的。
DxgkInitialize 成功加载dxgkrnl.sys之后,就需要获取dxgkrnl.sys生成的一个设备,设备名为 \Device\Dxgkrnl
因为一会要根据\Device\Dxgkrnl 这个设备,获取到dxgkrnl.sys提供的一个回调函数,这个回调函数的功能是把
DRIVER_INITIALIZATION_DATA 数据结构注册到dxgkrnl.sys中。
整个流程的大致代码如下:
UNICODE_STRING drvPath;
UNICODE_STRING drvName;
RtlInitUnicodeString(&drvPath, L"\\REGISTRY\\MACHINE\\SYSTEM\\CURRENTCONTROLSET\\SERVICES\\DXGKrnl");
RtlInitUnicodeString(&drvName, L"\\Device\\Dxgkrnl");
//加载dxgkrnl.sys驱动
status = ZwLoadDriver(&drvPath);
///获取 \Device\Dxgkrnl 设备对象
status = IoGetDeviceObjectPointer(&drvName, FILE_ALL_ACCESS, &dxgkrnl_fileobj, &dxgkrnl_pdoDevice);
PIRP pIrp = IoBuildDeviceIoControlRequest(
IOCTL_VIDEO_DDI_FUNC_REGISTER, //0x23003F , dxgkrnl.sys 导出注册函数
dxgkrnl_pdoDevice,
NULL,
0,
&dxgkrnl_dpiInit, //获取回调函数地址
sizeof(PDXGKRNL_DPIINITIALIZE),
TRUE, // IRP_MJ_INTERNAL_DEVICE_CONTROL
&evt,
&ioStatus);
status = IoCallDriver( dxgkrnl_pdoDevice, pIrp);
其中 IOCTL_VIDEO_DDI_FUNC_REGISTER 是个IOCTL,跟Device\Dxgkrnl通讯码固定为 0x23003F,定义如下:
CTL_CODE( FILE_DEVICE_VIDEO, 0xF, METHOD_NEITHER, FILE_ANY_ACCESS )
dxgkrnl_dpiInit是回调函数地址,用于注册 DRIVER_INITIALIZATION_DATA 数据结构到dxgkrnl.sys中,定义如下:
typedef __checkReturn NTSTATUS
DXGKRNL_DPIINITIALIZE(
PDRIVER_OBJECT DriverObject,
PUNICODE_STRING RegistryPath,
DRIVER_INITIALIZATION_DATA* DriverInitData
);
typedef DXGKRNL_DPIINITIALIZE* PDXGKRNL_DPIINITIALIZE;
PDXGKRNL_DPIINITIALIZE dxgkrnl_dpiInit;
成功获取到 dxgkrnl_dpiInit地址后,调用 dxgkrnl_dpiInit(DriverObject,RegistryPath,DriverInitData)
就把 DRIVER_INITIALIZATION_DATA数据结构注册到dxgkrnl.sys中了。
所有显卡驱动必须调用DxgkInitialize,而DxgkInitialize是按照上面流程注册DRIVER_INITIALIZATION_DATA的。
于是我们找到了获取显卡 DRIVER_INITIALIZATION_DATA数据结构的一个办法:
给 \Device\Dxgkrnl 设备对象挂载一个FiDO过滤设备,这样 IOCTL_VIDEO_DDI_FUNC_REGISTER 获取注册回调函数地址就能截获到。
然后在 IOCTL_VIDEO_DDI_FUNC_REGISTER 请求中,使用我们自己函数替换dxgknrl.sys提供的函数地址,
然后在我们的函数中,截获到DRIVER_INITIALIZATION_DATA数据结构,同时再次调用dxgkrnl.sys提供的函数,实现真正的注册。
大致流程如下:
1,开发一个非常普通的NT过滤式驱动,在DriverEntry函数中,提供 IRP_MJ_XXX函数地址,
for (UCHAR i = 0; i < IRP_MJ_MAXIMUM_FUNCTION; ++i) {
DriverObject->MajorFunction[i] = commonDispatch;
}
然后完成如下2,3两个步骤:
2, 首先,按照上面代码的步骤,加载dxgkrnl.sys驱动,获取 \Device\Dxgkrnl设备对象,调用IOCTL_VIDEO_DDI_FUNC_REGISTER,
获取到 dxgkrnl_dpiInit,dxgkrnl.sys提供的注册回调函数地址,并且保存起来。
3,然后IoCreateDevice 一个过滤设备,调用 IoAttachDeviceToDeviceStack 挂载到 \Device\Dxgkrnl设备对象上。
4,commonDispatch派遣函数中,如下实现:
static NTSTATUS commonDispatch(PDEVICE_OBJECT devObj, PIRP irp)
{
PIO_STACK_LOCATION irpStack = IoGetCurrentIrpStackLocation(irp);
switch (irpStack->MajorFunction)
{
case IRP_MJ_CREATE:
break;
case IRP_MJ_CLEANUP:
break;
case IRP_MJ_CLOSE:
break;
case IRP_MJ_INTERNAL_DEVICE_CONTROL:
if (irpStack->Parameters.DeviceIoControl.IoControlCode == IOCTL_VIDEO_DDI_FUNC_REGISTER) {
///////显卡驱动在DxgkInitialize函数中调用 IOCTL获取dxgkrnl.sys的注册回调函数,我们hook此处,获取到显卡驱动提供的所有DDI函数
irp->IoStatus.Information = 0;
irp->IoStatus.Status = STATUS_SUCCESS;
///把我们的回调函数返回给显卡驱动.
if (irp->UserBuffer) {
///
irp->IoStatus.Information = sizeof(PDXGKRNL_DPIINITIALIZE);
*((PDXGKRNL_DPIINITIALIZE*)irp->UserBuffer) = DpiInitialize;
}
/////
IoCompleteRequest(irp, IO_NO_INCREMENT);
return STATUS_SUCCESS;
///
}
break;
}
////
return call_lower_driver(irp);
}
其中 DpiInitialize 函数就是我们提供的一个新的注册函数,DpiInitialize如下实现:
NTSTATUS DpiInitialize(
PDRIVER_OBJECT DriverObject,
PUNICODE_STRING RegistryPath,
DRIVER_INITIALIZATION_DATA* DriverInitData)
{
NTSTATUS status = STATUS_SUCCESS;
static BOOLEAN is_hooked = FALSE;
////
UNICODE_STRING vm_str; RtlInitUnicodeString(&vm_str, L"\\Driver\\vm3dmp_loader"); // Vmware 3D
UNICODE_STRING igfx_str; RtlInitUnicodeString(&igfx_str, L"\\Driver\\igfx"); // Intel Graphics
if ( !is_hooked &&
(RtlEqualUnicodeString(&vm_str, &DriverObject->DriverName, TRUE) || RtlEqualUnicodeString(&igfx_str, &DriverObject->DriverName, TRUE) )//vmware里的虚拟显卡或者Intel显卡
)
{//这里只HOOK第一个显卡
is_hooked = TRUE;
///
//这里复制需要注意:
// DRIVER_INITIALIZATION_DATA结构定义,WDDM1.1 到 WDDM2.3 每个都会有不同定义,这里是WDK7下编译,因此只copy WDDM1.1的部分。
RtlCopyMemory(&wf->orgDpiFunc, DriverInitData, sizeof(DRIVER_INITIALIZATION_DATA));
////replace some function
DriverInitData->DxgkDdiAddDevice = DxgkDdiAddDevice;
DriverInitData->DxgkDdiRemoveDevice = DxgkDdiRemoveDevice;
DriverInitData->DxgkDdiStartDevice = DxgkDdiStartDevice;
DriverInitData->DxgkDdiStopDevice = DxgkDdiStopDevice;
DriverInitData->DxgkDdiQueryChildRelations = DxgkDdiQueryChildRelations;
DriverInitData->DxgkDdiQueryChildStatus = DxgkDdiQueryChildStatus;
DriverInitData->DxgkDdiQueryDeviceDescriptor = DxgkDdiQueryDeviceDescriptor;
DriverInitData->DxgkDdiEnumVidPnCofuncModality = DxgkDdiEnumVidPnCofuncModality; ////
DriverInitData->DxgkDdiIsSupportedVidPn = DxgkDdiIsSupportedVidPn;
DriverInitData->DxgkDdiCommitVidPn = DxgkDdiCommitVidPn;
DriverInitData->DxgkDdiSetVidPnSourceVisibility = DxgkDdiSetVidPnSourceVisibility;
DriverInitData->DxgkDdiSetVidPnSourceAddress = DxgkDdiSetVidPnSourceAddress;
// DriverInitData->DxgkDdiPresent = DxgkDdiPresent;
/////
}
///替换了某些函数后,接着调用 dxgkrnl.sys 回调函数注册
return wf->dxgkrnl_dpiInit(DriverObject, RegistryPath, DriverInitData);
}
如上代码,我只HOOK两类显卡,vmware虚拟显卡和Intel显卡,因为我所有电脑中,就只有Intel集成显卡。
代码中,找到我们需要hook的显卡驱动,然后首先保存 DRIVER_INITIALIZATION_DATA结构,接着把DRIVER_INITIALIZATION_DATA
的某些函数替换成我们的函数,这样,我们就修改某些回调函数,从而达到虚拟出一个新显示器的目的。
这里还有一个问题,如何调用我们的“显卡过滤驱动”,以及以怎样的顺序调用?
我们的驱动就是个再简单不过的NT过滤驱动,直接使用WIN32 API函数CreateService创建服务,然后启动服务就可以了。
可是DxgkInitialize 函数是在显卡驱动的 DriverEntry里被调用的,一般显卡驱动在电脑启动的时候,就被加载了,
之后不会再次调用DxgkInitialize 。这样即使我们的驱动运行了,也无法拦截到 IOCTL_VIDEO_DDI_FUNC_REGISTER注册请求。
因此必须在显卡驱动加载之前,加载我们的过滤驱动。这需要了解windows启动的驱动加载过程。
直接查看注册表的服务表项:\HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services,随便找一个驱动服务,
比如Dxgkrnl服务,看到有Start,还有Group,Start决定启动时间,
如果是0,则在BOOT阶段就启动,这是个非常早的阶段,只加载了windows的核心,很多都还没加载起来
如果是 1, 在System Init阶段启动,这个阶段加载了大部分,一般显卡和DxGkrnl都是在这个阶段启动起来的。
如果是 2,则在系统GUI加载,进入登录界面之后,被加载。
如果是3则手动加载,4是禁止加载
如果Start都设置1 ,如何决定处于这阶段的驱动,哪些先加载,哪些后加载,Group帮助解决这问题,
Group是字符串,用于把驱动归类到某个加载组,每个Group的加载顺序根据
\HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\ServiceGroupOrder 提供的顺序来决定,
其中 Dxgknrl.sys属于 Video Init组,而一般显卡属于 Video组,Video Init组先于Video加载。
因此,我们在创建服务的时候,设置Start=1,同时组为Video Init, 然后重启系统,这样我们的驱动基本都先于显卡驱动被加载。
当然可能还有其他情况,需要自己去测试,找到最好办法,不过至少在我的WIN7系统,Intel显卡是这种规律。其他的没试过。
到目前为止,我们的”显卡过滤驱动“已经能正常运行并且HOOK了显卡的回调函数,接下来,就该干实际的事情了。
如何虚拟出一个显示终端出来,这是我们首先需要解决的问题。
有了虚拟显示终端,才能进一步截取到输出到这显示终端的图像数据。
这需要我们首先熟悉VIDPN,VIDPN英文全名 Video Present Network,视频展现网络,
这是一个非常抽象的概念,可能你看MSDN上的文档,看了半天也不明白是什么意思。
VIDPN是连接显卡的Source和Target的路径管理集合,而Source和Target又是什么东西?
Source是显卡的显示源,简单的说,就是显示表面Surface, 桌面图像都会画到这个表面,
现在的显卡一般都有两个或以上的Source,一个是主显示表面,也就是我们大部分时间看到的一个windows桌面,
另外的有些是作为扩展桌面的显示源Source。
比如某块显卡有两个Source,编号分别0和1,Source0作为主显示桌面,Source1可以作为扩展显示桌面。
当然也不是这么硬性规定,主要看显卡厂商自己怎么决定。
接下来就是显示接口的问题,比如使用VGA接口,HDMI,Display-Port接口等。
这个显示接口,就是Target,也就是显示源最后都会朝某个或某几个显示接口输出最终的图像数据。
而VIDPN就是用来管理哪些Source朝哪些Target输出图像数据的一个抽象的管理概念。
Source和Target都有自己的模式集合,所谓模式,就是类似分辨率是多少,颜色深度(32还是16位,当然现在都是32位的了)等。
所有这些模式组成Source和Target的模式集合。
如下图,摘自MSDN:
显卡有两个显示源,Source1和Source2,Source1朝DVI的Target输出图像,
Source2朝HD15和S-Video输出图像,并且这两个图像是一样的,属于Clone,
而Source1和Source2之间是扩展方式,一个是主桌面,另一个则作为扩展桌面。
Source1,2 和DVI,HD15和S-Video之间的连线,就是VIDPN的管理方式,构成Topology拓扑结构。
Source是受显卡本身的设计决定的,也就是我们的“显卡过滤驱动”不能随意添加Source,
比如某块倒霉的显卡,只有一个Source,我们就只能用来做主桌面输出,不能实现扩展桌面功能了。
但是这样的拓扑结构中,Target是可以随意添加的,
这也是我们的”显卡过滤驱动“能成功虚拟出一个新的显示终端,以及WIN10 以后平台的Indirect Display Driver的基础。
当然添加了一个新的Target,就必须重新设置整个拓扑结构,否则就会出问题。
这也是我们的“显卡过滤驱动”该解决的问题。
首先,如何添加一个新的Target?
我们HOOK DxgkDdiStartDevice 函数。此函数原型为:
NTSTATUS DxgkDdiStartDevice(
IN PVOID MiniportDeviceContext,
IN PDXGK_START_INFO DxgkStartInfo,
IN PDXGKRNL_INTERFACE DxgkInterface,
OUT PULONG NumberOfVideoPresentSources,
OUT PULONG NumberOfChildren);
最后两个参数就是分别返回 Source个数,Target个数。
首先调用原始的 DxgkDdiStartDevice,然后增加 最后一个参数就可以了,如下:
status = wf->orgDpiFunc.DxgkDdiStartDevice(MiniportDeviceContext, DxgkStartInfo, DxgkInterface,
NumberOfVideoPresentSources, NumberOfChildren);
wf->vidpn_source_count = *NumberOfVideoPresentSources;
wf->vidpn_target_count = *NumberOfChildren + 1;
*NumberOfChildren = wf->vidpn_target_count; ///增加一个新的Target
接着HOOK DxgkDdiQueryChildRelations,在此函数中,主要给这个新添加的Target设置ID值。如下:
status = wf->orgDpiFunc.DxgkDdiQueryChildRelations(pvMiniportDeviceContext, pChildRelations, ChildRelationsSize);
PDXGK_CHILD_DESCRIPTOR target = &pChildRelations[wf->vidpn_target_count - 1];
....
target->ChildUid = VIDPN_CHILD_UID;// 正确的做法是遍历pChildRelations的CHildUid,
然后设置一个不存在的Uid给这个新的target,这里为了简单,直接定义一个 VIDPN_CHILD_UID宏。
HOOK DxgkDdiQueryChildStatus 和 DxgkDdiQueryDeviceDescriptor 函数,遇到 ChildUid是VIDPN_CHILD_UID的,
都需要自己处理,不再传递给原始驱动。
HOOK了这些函数,好像是完事了,其实更大头的还在VIDPN的处理。
首先HOOK DxgkDdiEnumVidPnCofuncModality,给dxgkrnl.sys报告我们的新Target的模式集合以及Topology连线规则。
具体做法是在DxgkDdiEnumVidPnCofuncModality函数中,枚举VIDPN的所有路径,
如果遇到 路径的终端是VIDPN_CHILD_UID,也就是我们自己的新添加的Target,则设置模式集合。
新增了一个Target,我们必须让真实的显卡驱动不知道有这么一个Target的存在,否则显卡驱动会出错,甚至可能会蓝屏。
前面说过的,显卡驱动和dxgknrl.sys之间通过回调函数来通讯,跟VIDPN相关的是 在 DxgkDdiStartDevice 回调函数的参数提供的
PDXGKRNL_INTERFACE接口里边的 DxgkCbQueryVidPnInterface 函数,
这个函数用于查询VIDP的Topology接口,这个Topology接口提供的又是一堆的接口函数。
大致的HOOK的 DxgkDdiStartDevice 代码如下:
static NTSTATUS DxgkDdiStartDevice(
IN PVOID MiniportDeviceContext,
IN PDXGK_START_INFO DxgkStartInfo,
IN PDXGKRNL_INTERFACE DxgkInterface,
OUT PULONG NumberOfVideoPresentSources,
OUT PULONG NumberOfChildren)
{
NTSTATUS status = STATUS_SUCCESS;
////WDDM1.1 到 WDDM2.3 每个都会有不同定义,这里是WDK7下编译,因此只copy WDDM1.1的部分。
wf->DxgkInterface = *DxgkInterface; /// save interface function,用于VIDPN设置
///////替换原来的接口
DxgkInterface->DxgkCbQueryVidPnInterface = DxgkCbQueryVidPnInterface;
//////
status = wf->orgDpiFunc.DxgkDdiStartDevice(MiniportDeviceContext, DxgkStartInfo, DxgkInterface, NumberOfVideoPresentSources, NumberOfChildren);
////
DxgkInterface->DxgkCbQueryVidPnInterface = wf->DxgkInterface.DxgkCbQueryVidPnInterface;
///
DPT("Hook: DxgkDdiStartDevice status=0x%X.\n", status ); ///
if (NT_SUCCESS(status)) {
DPT("org: DxgkDdiStartDevice, NumberOfVideoPresentSources=%d, NumberOfChildren=%d\n", *NumberOfVideoPresentSources, *NumberOfChildren);
//// 分别增加 1,增加 source 和 target
wf->vidpn_source_count = *NumberOfVideoPresentSources; // +1;
wf->vidpn_target_count = *NumberOfChildren + 1;
//////
*NumberOfVideoPresentSources = wf->vidpn_source_count;
*NumberOfChildren = wf->vidpn_target_count;
////
}
////
return status;
}
而在 我们自己的HOOK的 DxgkCbQueryVidPnInterface 函数中:
首先调用原始的DxgkCbQueryVidPnInterface函数,获取到 DXGK_VIDPN_INTERFACE 接口函数集合。
这个接口里边大部分是模式管理相关,有个 pfnGetTopology 接口,是我们需要HOOK,因为在里边才能屏蔽我们新的Target。
HOOK了这个接口之后,发现pfnGetTopology 被调用的时候,会获取到 DXGK_VIDPNTOPOLOGY_INTERFACE 接口,
里边又是一大堆的回调函数,在 DXGK_VIDPNTOPOLOGY_INTERFACE这个接口中,我们主要HOOK里边的
pfnGetNumPaths, pfnGetNumPathsFromSource,pfnEnumPathTargetsFromSource,pfnAcquireFirstPathInfo,pfnAcquireNextPathInfo
等函数,这些都是跟我们新添加的Target 路径相关的 ,在这些函数中,屏蔽掉我们新添加的UID是VIDPN_CHILD_UID的 Target 。
非常繁琐,这里也就不罗嗦了,我目前发现的主要是上面的函数,如果还有其他没有HOOK的,有兴趣的而且又知道的请提出来。
解决了VIDPN的问题,我们的“显卡过滤驱动”运行之后,终于能在电脑中看到我们新增加的这块显示器了,
下面是效果图:
这个图可能有点看不明白,
大屏幕是台式机,装的win7系统,被作为扩展屏幕,
而笔记本电脑上和大屏幕上的浏览器里边xdisp_virt(xdisp_virt是网页方式远程控制,详细查阅前面的文章)显示的是主桌面。
未完待续。。。