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

您所在的位置:网站首页 directd3d Windows桌面实现之八(DirectX HOOK 方式截取特殊的全屏程序之二)

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

2023-08-28 13:34| 来源: 网络整理| 查看: 265

                                                                  by fanxiushu 2019-04-18 转载或引用请注明原始作者。 接上文。 WIN7以上系统WDDM虚拟显卡开发(WDDM Filter/Hook Driver 显卡过滤驱动开发之一) 这篇文章,曾经提到过: windows的应用程序中,绘图的基础图形库包括 GDI, DirectX, OpenGL(最新的可能还包括Vulkan)。 一切的windows界面都是这三种图形库绘制出来的。 GDI牵涉到windows的方方面面,GDI加速是在WDDM驱动内核进行硬件加速的。 而其中DirectX和OpenGL有对应的应用层驱动,也就是复杂图形基本上都是在应用层渲染的。 从理论上说,我们如果能截取到每个应用层程序的绘图操作,比如给每个界面程序注入一个DLL,截取绘图函数, 比如GDI中BitBlt以及相关WM_PAINT等消息, DirectX中Present,OpenGL中wglSwapBuffers等,获取到对应的RGB图像数据。 然后计算每个窗口Z序,判断遮挡情况,判断哪些该裁剪掉,然后再把这些RGB图像数据混合起来,就能获取整个屏幕的图像了。 然而这只是理论上,实际上这等同于是在做上篇文章所讲的,类似于windows桌面管理器一样的东西了。 一开始的时候,我也是这么想的,想给xdisp_virt增加另一种抓屏功能,就是类似HOOK的方式, 对DirectX,OpenGL窗口HOOK方式截图,然后判断这些DX,OpenGL窗口位置,然后整个桌面中剩下区域的全用GDI来截取。 结果做下来效果并不好,因此取消了。只专心处理全屏独占这种一般抓屏办法无法截取的情况。 把DLL注入到别的进程来截图,会增加被挂进程的运算负担,影响被挂进程的运行效率。容易造成被挂进程崩溃, 尤其是全世界各类程序那么多,虽然基础图形库就那么三个,但是使用它们的方式千奇百怪,因此截取的时候, 容易因为没考虑到某些情况而直接造成程序crash。 既然HOOK也不是好的方式,为何不一劳永逸的从驱动去解决。 可是也非常可惜,在驱动中的处理情况只会更加糟糕。WDDM驱动版本不断在升级,从WIN7的1.1 到WIN8的1.2, 1.3,再到 WIN10 的2.0一直到现在2.5,像是坐火箭炮似的。而且WDDM驱动中并没一个通用的类似于简单的 FrameBuffer 的接口。 而实际上GDI画的图我们都可以使用BitBlt 截取, DirectX,OpenGL画的图,如果是一般的窗口模式,因为要经过桌面管理器的混合,也一样可以使用BitBlt截取。 剩下的基本就是独占模式,窗口管理器失效这种情况了。 可是WIN8以上的系统使用DXGI Desktop Duplication 也能处理这种全屏独占。 (我没测试过所有情况,因为我安装WIN10 的机器是intel集成显卡,不清楚独显的情况,尤其是很强劲的独显,几乎支持所有显卡硬件加速。 况且在测试 “鬼泣2” 这个游戏时候,使用DXGI时好时坏,有时能成功截取,有时又会失败,还不如mirror稳定--是使用xdisp_virt程序测试的) 再来看看BitBlt跟DXGI效率问题比较。 其实不管是BitBlt还是DXGI,都需要把RGB图像数据从显存Copy到内存中, 而这个Copy速度谁也不会比谁高明,都是老老实实的数据复制。 都是使用DMA传输数据。 在以前文章讲到 DXGI截屏方式,其中 ID3D11DeviceContext->CopyResource 就是复制整个 ID3D11Texture2D 纹理, DXGI截屏中的两个纹理,一个在显存,一个在内存,就会启动DMA传输,把整个RGB图像数据复制到内存。 CopyResource好处是异步传输,函数会马上返回, 我们接着可以做别的事,电脑在后台处理传输数据。 而BitBlt是阻塞的,直到传输完成才会返回。 DXGI截屏还有一个很大好处,就是能实时捕获绘图操作,因为电脑可能很长时间都不会绘图,处于静止状态。 这也是跟mirror驱动一样值得称道的地方。可以想想,假设电脑在5秒内什么绘图操作都没发生, 按照30fps的速度,BitBlt因为不知道是否发生了绘图操作,需要在这5秒内截图 5*30=150 次,而DXGI在这5秒内啥都不用做。 回到正题,独占模式都是DirectX和OpenGL绘图的专利。 而DirectX的版本非常多,8, 9, 10, 11, 12 一共五个版本,其实还有之前的 DirectX7, 6, 5等, 因为太老,现在的WIN7以上平台中已经不存在了,WINXP中也找不到他们的身影了。不过DDRAW还保留着。 因此我们也是从DX8开始进行HOOK,至于OpenGL因为HOOK方法比DX简单得多,也好处理。 先别被这么多的DirectX版本吓到,其实只需要掌握他们的核心处理思路就好办多了, 我们这里只是截取经过渲染之后的RGB图像数据,而不是要我们自己去渲染图像。 早在DDRAW中,或者我们使用GDI绘制普通桌面程序的时候,都曾使用到一种叫后台缓存技术, 就是先创建一个后台 DC,  memdc = CreateCompatibleDC(displaydc); 其中memdc是后台DC,displaydc就是显示dc, 然后我们在memdc中绘制各类图像, 最终使用 BitBlt 把 memdc 翻转到 displaydc中,这样屏幕中就呈现了我们绘制的图像。 因为如果直接在displaydc上画图的话,屏幕闪烁得非常厉害,闪烁得简直是无法直视。 这也是我们绘图的通用做法:就是在后台缓存中先画图,画好之后以极快的速度提交到前台显示, 然后接着在后台画图,然后再提交,如此循环往复。 现在谁也不会傻到直接在前台绘图,尤其是动画。 既然是绘图的通用做法,DirectX和OpenGL也不例外,都是按照这种思路来展现图像的。这就是核心思路所在。 我们只要在DirectX和OpenGL把后台缓存数据展现到前台之前,获取到它画到后台缓存中的RGB图像数据,就能截取到程序绘制的图像了。 他们把后台数据展现到前台,总得需要调用一个函数,就像GDI中BitBlt一样。 在Directx中是Present函数,而且各个版本的DX中都有这样的函数,而OpenGL中的是wglSwapBuffers 。 我们只要HOOK这样的函数,就能成功截取到图像了。 下图摘自MSDN关于WDDM的介绍。WIN7以上平台关于DirectX和WDDM的交换流程https://docs.microsoft.com/en-us/windows-hardware/drivers/display/windows-vista-and-later-display-driver-model-operation-flow 这个图很复杂,简单解释一下。 首先我们创建一个3D设备,比如DirectX11 中 D3D11CreateDevice 函数创建一个DX11设备, 调用此函数的时候,Direct3DRuntime(应用层的3D系统组件,以下简称 DxRuntime)就会与 DIrectX Graphic Kernel System(就是dxgkrnl.sys内核系统组件,以下简称dxgkrnl)通讯,然后dxgkrnl就会调用显卡内核驱动的DxgkDdiCreateDevice回调函数,然后显卡就会创建各种内核资源。 成功之后,DxRuntime接着调用应用层的显卡驱动,应用层显卡驱动的CreateDevice回调函数就会被调用, 在CreateDevice回调函数中接着调用 DxRuntime的 pfnCreateContextCb 来分配一个设备资源,然后做些其他初始化工作。 这样一个3D设备就创建成功了。 当我们在DX要创建一个表面,比如DirectX11调用 CreateTexture2D 创建一个纹理, DxRuntime就会调用显卡应用层驱动的CreateResource回调函数,在CreateResource中再次调用 DxRuntime的pfnAllocateCb, 此函数中,DxRuntime 会跟dxgknrl通讯,让dxgknrl调用显卡内核驱动的DxgkDdiCreateAllocation回调函数分配相关的内核资源。 接着就是绘图,渲染等等各种3D绘图指令。 画完图之后,上面说过的,需要把画好的后台缓存图像最终提交到显示终端, 于是在应用程序中调用DirectX的Present函数,比如 IDXGISwapChain->Present , 于是 DxRuntime会调用显卡应用层驱动的Present回调函数, 在这个Present回调函数中做些其他工作,然后反过来再调用DxRuntime的pfnPresentCb, 于是DxRuntime提交命令到dxgknrl, dxgknrl调用显卡的内核驱动的DxgkDdiPresent回调函数, 接下来全是显卡内核驱动需要完成的事。 上图从 10-16,主要目标把这个后台图像缓存提交到最终的显示终端。 可以看出流程之复杂。 还好,我们只需要折腾应用层中的Present就可以了,上面的图示主要为了加深对Present的认识理解,不理解也不影响后面的HOOK。 接着看看Present都隐身在各个DirectX版本的何处。 开始前,先来理解一个叫交换链的概念。 前面说过了,我们在电脑上画图,基本上都是在后台画,画好之后才最终把整个画好的提交到前台显示。 假设前台是 front_buffer,后台可能有多个备用buffer,比如 back_buffer1,back_buffer2,形成一个连接队列链条 back_buffer1画好之后提交给front_buffer,提交完成后再挂载到队列尾部。 这个时候原来的back_buffer2变成1, 原来的back_buffer1跑到后面去了。如此循环。 而如果只交换指针的Flip,就是back_buffer1画完之后,直接把前台指针指向back_buffer1,这个时候它变成 fron_buffer了, 原来的front_buffer再被挂载到队列尾部。这样其实就形成了一个环形队列,称为交换链,就是不停在做数据交换。 Present函数内部就是维护着这样的一个环形队列,每调用一次Present,相应的Buffer就改变一次。 DirectX8中,Present主要出现在 IDirect3DDevice8 接口中,每个IDirect3DDevice8 设备至少包含一个隐含的主交换链, 同时可以调用 CreateAdditionalSwapChain 创建一个附加交换链,交换链接口是 IDirect3DSwapChain8。 DirectX9 跟DirectX8比较类似,Present主要出现在 IDirect3DDevice9中,每个IDirect3DDevice9至少包含一个隐含主交换链。 同时DirectX9中,我们还可以调用GetSwapChain 获取到这个隐含的主交换链,这是跟DX8不同的地方。 同时可以调用 CreateAdditionalSwapChain  创建一个附加交换链,接口是 IDirect3DSwapChain9, 这里还有一个地方,就是 IDirect3DDevice9Ex接口中还提供了一个叫PresentEx的扩展函数。 可以看出 ,DirectX8,DirectX9中,交换链和具体的3D设备是不分家的,混杂在一起。 而到了DX10以后,微软专门把 交换链独立出来专门实现,取名叫DXGI,接口名字叫 IDXGISwapChain。 具体实现在 dxgi.dll 动态库中, 我们可以从接口变化中看出了他们对DirectX设计得进步和完善。 现在来总结一下在各个版本的DirectX中主要HOOK哪些函数: DirectX8:           IDirect3DDevice8接口中的Present函数, Reset函数(在设备丢失的时候清理我们自己创建的资源)          IDirect3DSwapChain8中的Present 函数,          还可以HOOK Release函数,用于在设备释放之后清理我们创建的资源。          对应动态库是d3d8.dll DirectX9:          IDirect3DDevice9接口中的Present函数, Reset函数,          IDirect3DDevice9Ex接口中的PresentE下函数, ResentEx函数          IDirect3DSwapChain9接口中的 Present函数。          还可以HOOK Release函数,用于在设备释放之后清理我们创建的资源         对应动态库是 d3d9.dll DirectX10,11,12:         IDXGISwapChain 接口中的Present函数 ,ResizeBuffer函数,         还可以HOOK Release函数,用于在设备释放之后清理我们创建的资源         对应动态库是 dxgi.dll HOOK采用inline的方式,就是替换函数的前5个字节实现直接跳转,使用微软的detours开源库,当然可以使用任何其他一样的开源库, 比如 minihook, easyhook等,都是一样的。 以detours为例, void* real_msgbox= MessageBoxA; //设置原来的函数地址int CALLBACK my_MessageBox( HWND hWnd, const char* lpText,const char*  lpCaption, UINT uType) {      printf("this is my msgbox.\n");    return real_msgbox(hWnd, lpText, lpCation, uType); } DetourTransactionBegin(); DetourUpdateThread(GetCurrentThread()); DetourAttach((PVOID*)&real_msgbox, my_MessageBox);  // inline hook DetourTransactionCommit(); 就这么简单。 DirectX使用的COM库,都是C++的纯虚类,而inline HOOK都是具体的C标准格式的函数。看起来不好HOOK。 其实也是非常简单。 把HOOK相关代码写到 .c 后缀的纯 c源代码文件中,所有接口调用都变成C函数格式了。比如: IDXGISwapChain* swap; swap->lpVtbl->Present(swap, .....); 获取到swap对象之后,简单的说就是IDXGISwapChain数据结构的swap变量之后, Present函数地址就是 swap->lpVtbl->Present 这比要定位C++虚拟表,然后一个个的去数Present在哪个位置,然后硬编码要好得多。 前一篇文章中简单提到,把我们的dll注入到别的进程的办法,当注入dll后,DllMain入口函数就会被调用。 我们在 DLL_PROCESS_ATTACH 中创建一个线程,这个线程就是定时检测并且HOOK对应的DX。 当然,还包括其他一些数据处理,毕竟需要把别的进程中获取到的图像数据传递到我们自己的进程中。 如何检测并且Hook呢?以DirectX11 为例。 当某个进程是以DirectX11画图的,它必定会调用d3d11.dll这个动态库。于是检测的时候调用 GetModuleHandle("d3d11.dll"); 判断是否加载了d3d11.dll,如果加载了,再判断 dxgi.dll是否加载,因为DX10以后的Present函数都实现在dxgi.dll中。 如果都加载了,说明这个程序是使用D3D11来画图。 于是调用 D3D11CreateDeviceAndSwapChain获取到 IDXGISwapChain的接口变量swap,从而进一步获取到 Present函数地址。 接着调用detours来HOOK这个Present函数。 这里可能会有个疑问,担心这个Present函数只是我们自己调用D3D11CreateDeviceAndSwapChain创建的函数地址, 而不是程序中其他同样的交换链的Present地址,其实熟悉C++特性的,都不会这么问,同一个类的函数, 被编译器最终生成的都是同一个固定的函数,只是第一个参数是this指针而已。 HOOK成功了之后,当程序要交换后台数据,我们hook的myPresent就会被调用,然后我们在myPresent中判断是否第一次调用, 是的话,创建相关的资源,比如创建一个CPU可以访问的Texture2D纹理表面,以及其他相关初始化。 然后调用 IDXGISwapChain->GetBuffer获取到第一个后台缓存,这个就是即将要被展现到前台的后台图像数据缓存区。 然后调用CopyResource复制RGB数据到我们创建的Texture2D纹理中,接着Map这个纹理,就可以从中获取到原始的RGB图像数据了。 获取到图像数据之后,需要传递到我们自己的进程中,这个时候使用共享内存方式是最高效的。 但是有些程序,比如UWP程序,是不能访问共享内存的,可以使用WM_COPYDATA传递消息方式,但是这种方式效率比较低。 DirectX10,DirectX9,DirectX8,基本是类似做法。DirectX12接口比较新,但是可以把它转成DirectX11的方式进行处理。 至于OpenGL的HOOK这里也就不再赘述了,差不多都是一样。 只是关注的是主要HOOK wglSwapBuffers函数,以及怎样从OpenGL后台缓存获取图像数据。 需要注意的是,inline hook之后,就不要 unhook了,同时注入的dll,除非进程自己结束,也不要中途退场了。 否则程序崩溃的几率更大,尤其是inline HOOK之后,因为你也HOOK,他也HOOK,无非就是更加增加程序的运行负担。 HOOK了不UnHook倒也没什么大问题,会形成一个调用链。 UnHook并且UnHook顺序不对,就会造成程序崩溃。 下图是xdisp_virt程序采用DXHOOK的方式远程显示WIN7中 ”极品飞车17“ 的画面: 不过目前因为鼠标键盘是在应用层进行模拟的,所有暂时还无法在浏览器中用鼠标键盘控制游戏。 新版本的xidsp_virt还支持多显示器的显示,如下三个显示器合并在一起,大小是 4468X2160 整个画面像狗啃了似的: 有兴趣可关注:https://github.com/fanxiushu/xdisp_virt 上的最新版本。  



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3