导航菜单
首页 » 问答 » 正文

Windows桌面实现之八(DirectX HOOK 方式截取特殊的全屏程序之二)

接上文。

WIN7以上系统WDDM虚拟显卡开发(WDDM /Hook 显卡过滤驱动开发之一) 这篇文章,曾经提到过:

的应用程序中,绘图的基础图形库包括 GDI, , (最新的可能还包括)。

一切的界面都是这三种图形库绘制出来的。

GDI牵涉到的方方面面,GDI加速是在WDDM驱动内核进行硬件加速的。

而其中和有对应的应用层驱动,也就是复杂图形基本上都是在应用层渲染的。

从理论上说,我们如果能截取到每个应用层程序的绘图操作,比如给每个界面程序注入一个DLL,截取绘图函数,

比如GDI中以及相关等消息, 中,中等,获取到对应的RGB图像数据。

然后计算每个窗口Z序,判断遮挡情况,判断哪些该裁剪掉,然后再把这些RGB图像数据混合起来,就能获取整个屏幕的图像了。

然而这只是理论上,实际上这等同于是在做上篇文章所讲的,类似于桌面管理器一样的东西了。

一开始的时候,我也是这么想的,想给增加另一种抓屏功能,就是类似HOOK的方式,

对,窗口HOOK方式截图,然后判断这些DX,窗口位置,然后整个桌面中剩下区域的全用GDI来截取。

结果做下来效果并不好,因此取消了。只专心处理全屏独占这种一般抓屏办法无法截取的情况。

把DLL注入到别的进程来截图,会增加被挂进程的运算负担,影响被挂进程的运行效率。容易造成被挂进程崩溃,

尤其是全世界各类程序那么多,虽然基础图形库就那么三个,但是使用它们的方式千奇百怪,因此截取的时候,

容易因为没考虑到某些情况而直接造成程序crash。

既然HOOK也不是好的方式,为何不一劳永逸的从驱动去解决。

可是也非常可惜,在驱动中的处理情况只会更加糟糕。WDDM驱动版本不断在升级,从WIN7的1.1 到WIN8的1.2, 1.3,再到

WIN10 的2.0一直到现在2.5,像是坐火箭炮似的。而且WDDM驱动中并没一个通用的类似于简单的 的接口。

而实际上GDI画的图我们都可以使用 截取,

,画的图,如果是一般的窗口模式,因为要经过桌面管理器的混合,也一样可以使用截取。

剩下的基本就是独占模式,窗口管理器失效这种情况了。

可是WIN8以上的系统使用DXGI 也能处理这种全屏独占。

(我没测试过所有情况,因为我安装WIN10 的机器是intel集成显卡,不清楚独显的情况,尤其是很强劲的独显,几乎支持所有显卡硬件加速。

况且在测试 “鬼泣2” 这个游戏时候,使用DXGI时好时坏,有时能成功截取,有时又会失败,还不如稳定--是使用程序测试的)

再来看看跟DXGI效率问题比较。

其实不管是还是DXGI,都需要把RGB图像数据从显存Copy到内存中,

而这个Copy速度谁也不会比谁高明,都是老老实实的数据复制。

都是使用DMA传输数据。

在以前文章讲到 DXGI截屏方式,其中 -> 就是复制整个 纹理,

DXGI截屏中的两个纹理,一个在显存,一个在内存,就会启动DMA传输,把整个RGB图像数据复制到内存。

好处是异步传输,函数会马上返回, 我们接着可以做别的事,电脑在后台处理传输数据。

而是阻塞的,直到传输完成才会返回。

DXGI截屏还有一个很大好处,就是能实时捕获绘图操作,因为电脑可能很长时间都不会绘图,处于静止状态。

这也是跟驱动一样值得称道的地方。可以想想,假设电脑在5秒内什么绘图操作都没发生,

按照30fps的速度,因为不知道是否发生了绘图操作,需要在这5秒内截图 5*30=150 次,而DXGI在这5秒内啥都不用做。

回到正题,独占模式都是和绘图的专利。

而的版本非常多,8, 9, 10, 11, 12 一共五个版本,其实还有之前的 , 6, 5等,

因为太老,现在的WIN7以上平台中已经不存在了,WINXP中也找不到他们的身影了。不过DDRAW还保留着。

因此我们也是从DX8开始进行HOOK,至于因为HOOK方法比DX简单得多,也好处理。

先别被这么多的版本吓到,其实只需要掌握他们的核心处理思路就好办多了,

我们这里只是截取经过渲染之后的RGB图像数据,而不是要我们自己去渲染图像。

早在DDRAW中,或者我们使用GDI绘制普通桌面程序的时候,都曾使用到一种叫后台缓存技术,

就是先创建一个后台 DC,

memdc = (); 其中memdc是后台DC,就是显示dc,

然后我们在memdc中绘制各类图像,

最终使用 把 memdc 翻转到 中,这样屏幕中就呈现了我们绘制的图像。

因为如果直接在上画图的话,屏幕闪烁得非常厉害,闪烁得简直是无法直视。

这也是我们绘图的通用做法:就是在后台缓存中先画图,画好之后以极快的速度提交到前台显示,

然后接着在后台画图,然后再提交,如此循环往复。

现在谁也不会傻到直接在前台绘图,尤其是动画。

既然是绘图的通用做法,和也不例外,都是按照这种思路来展现图像的。这就是核心思路所在。

我们只要在和把后台缓存数据展现到前台之前,获取到它画到后台缓存中的RGB图像数据,就能截取到程序绘制的图像了。

他们把后台数据展现到前台,总得需要调用一个函数,就像GDI中一样。

在中是函数,而且各个版本的DX中都有这样的函数,而中的是 。

我们只要HOOK这样的函数,就能成功截取到图像了。

下图摘自MSDN关于WDDM的介绍。WIN7以上平台关于和WDDM的交换流程

这个图很复杂,简单解释一下。

首先我们创建一个3D设备,比如 中

函数创建一个DX11设备,

调用此函数的时候,(应用层的3D系统组件,以下简称 )就会与

(就是.sys内核系统组件,以下简称)通讯,然后就会调用显卡内核驱动的回调函数,然后显卡就会创建各种内核资源。

成功之后,接着调用应用层的显卡驱动,应用层显卡驱动的回调函数就会被调用,

在回调函数中接着调用 的 来分配一个设备资源,然后做些其他初始化工作。

这样一个3D设备就创建成功了。

当我们在DX要创建一个表面,比如调用 创建一个纹理,

就会调用显卡应用层驱动的回调函数,在中再次调用 的,

此函数中, 会跟通讯,让调用显卡内核驱动的ion回调函数分配相关的内核资源。

接着就是绘图,渲染等等各种3D绘图指令。

画完图之后,上面说过的,需要把画好的后台缓存图像最终提交到显示终端,

于是在应用程序中调用的函数,比如 -> ,

于是 会调用显卡应用层驱动的回调函数,

在这个回调函数中做些其他工作,然后反过来再调用的, 于是提交命令到,

调用显卡的内核驱动的回调函数, 接下来全是显卡内核驱动需要完成的事。

上图从 10-16,主要目标把这个后台图像缓存提交到最终的显示终端。

可以看出流程之复杂。

还好,我们只需要折腾应用层中的就可以了,上面的图示主要为了加深对的认识理解,不理解也不影响后面的HOOK。

接着看看都隐身在各个版本的何处。

开始前,先来理解一个叫交换链的概念。

前面说过了,我们在电脑上画图,基本上都是在后台画,画好之后才最终把整个画好的提交到前台显示。

假设前台是 ,后台可能有多个备用,比如 ,,形成一个连接队列链条

画好之后提交给,提交完成后再挂载到队列尾部。

这个时候原来的变成1, 原来的跑到后面去了。如此循环。

而如果只交换指针的Flip,就是画完之后,直接把前台指针指向,这个时候它变成 了,

原来的再被挂载到队列尾部。这样其实就形成了一个环形队列,称为交换链,就是不停在做数据交换。

函数内部就是维护着这样的一个环形队列,每调用一次,相应的就改变一次。

中,主要出现在 接口中,每个 设备至少包含一个隐含的主交换链,

同时可以调用 Chain 创建一个附加交换链,交换链接口是 。

跟比较类似,主要出现在 中,每个至少包含一个隐含主交换链。

同时中,我们还可以调用 获取到这个隐含的主交换链,这是跟DX8不同的地方。

同时可以调用 Chain 创建一个附加交换链,接口是 ,

这里还有一个地方,就是 接口中还提供了一个叫的扩展函数。

可以看出 ,,中,交换链和具体的3D设备是不分家的,混杂在一起。

而到了DX10以后,微软专门把 交换链独立出来专门实现,取名叫DXGI,接口名字叫 。

具体实现在 dxgi.dll 动态库中, 我们可以从接口变化中看出了他们对设计得进步和完善。

现在来总结一下在各个版本的中主要HOOK哪些函数:

:

接口中的函数, Reset函数(在设备丢失的时候清理我们自己创建的资源)

中的 函数,

还可以HOOK 函数,用于在设备释放之后清理我们创建的资源。

对应动态库是d3d8.dll

接口中的函数, Reset函数,

接口中的下函数, 函数

接口中的 函数。

还可以HOOK 函数,用于在设备释放之后清理我们创建的资源

对应动态库是 d3d9.dll

,11,12:

接口中的函数 ,函数,

还可以HOOK 函数,用于在设备释放之后清理我们创建的资源

对应动态库是 dxgi.dll

HOOK采用的方式,就是替换函数的前5个字节实现直接跳转,使用微软的开源库,当然可以使用任何其他一样的开源库,

比如 , 等,都是一样的。

以为例,

void* = ; //设置原来的函数地址

int ( HWND hWnd, const char* ,const char* , UINT uType)

("this is my .\n");

(hWnd, , , uType);

in();

(());

((PVOID*)&, ); // hook

mit();

就这么简单。

使用的COM库,都是C++的纯虚类,而 HOOK都是具体的C标准格式的函数。看起来不好HOOK。

其实也是非常简单。

把HOOK相关代码写到 .c 后缀的纯 c源代码文件中,所有接口调用都变成C函数格式了。比如:

* swap;

swap->->(swap, .....);

获取到swap对象之后,简单的说就是数据结构的swap变量之后,

函数地址就是 swap->->

这比要定位C++虚拟表,然后一个个的去数在哪个位置,然后硬编码要好得多。

前一篇文章中简单提到,把我们的dll注入到别的进程的办法,当注入dll后,入口函数就会被调用。

我们在 中创建一个线程,这个线程就是定时检测并且HOOK对应的DX。

当然,还包括其他一些数据处理,毕竟需要把别的进程中获取到的图像数据传递到我们自己的进程中。

如何检测并且Hook呢?以 为例。

当某个进程是以画图的,它必定会调用d3d11.dll这个动态库。于是检测的时候调用 ("d3d11.dll");

判断是否加载了d3d11.dll,如果加载了,再判断 dxgi.dll是否加载,因为DX10以后的函数都实现在dxgi.dll中。

如果都加载了,说明这个程序是使用D3D11来画图。

于是调用 获取到 的接口变量swap,从而进一步获取到 函数地址。

接着调用来HOOK这个函数。

这里可能会有个疑问,担心这个函数只是我们自己调用创建的函数地址,

而不是程序中其他同样的交换链的地址,其实熟悉C++特性的,都不会这么问,同一个类的函数,

被编译器最终生成的都是同一个固定的函数,只是第一个参数是this指针而已。

HOOK成功了之后,当程序要交换后台数据,我们hook的就会被调用,然后我们在中判断是否第一次调用,

是的话,创建相关的资源,比如创建一个CPU可以访问的纹理表面,以及其他相关初始化。

然后调用 ->获取到第一个后台缓存,这个就是即将要被展现到前台的后台图像数据缓存区。

然后调用复制RGB数据到我们创建的纹理中,接着Map这个纹理,就可以从中获取到原始的RGB图像数据了。

获取到图像数据之后,需要传递到我们自己的进程中,这个时候使用共享内存方式是最高效的。

但是有些程序,比如UWP程序,是不能访问共享内存的,可以使用传递消息方式,但是这种方式效率比较低。

,,,基本是类似做法。接口比较新,但是可以把它转成的方式进行处理。

至于的HOOK这里也就不再赘述了,差不多都是一样。

只是关注的是主要HOOK 函数,以及怎样从后台缓存获取图像数据。

需要注意的是, hook之后,就不要 了,同时注入的dll,除非进程自己结束,也不要中途退场了。

否则程序崩溃的几率更大,尤其是 HOOK之后,因为你也HOOK,他也HOOK,无非就是更加增加程序的运行负担。

HOOK了不倒也没什么大问题,会形成一个调用链。

并且顺序不对,就会造成程序崩溃。

下图是程序采用的方式远程显示WIN7中 ”极品飞车17“ 的画面:

不过目前因为鼠标键盘是在应用层进行模拟的,所有暂时还无法在浏览器中用鼠标键盘控制游戏。

新版本的还支持多显示器的显示,如下三个显示器合并在一起,大小是

整个画面像狗啃了似的:

评论(0)

二维码