抓屏技术之MirrorDriver镜像驱动实现

抓屏技术之MirrorDriver镜像驱动实现

在XP和Win7系统下面,如果要对桌面进行录屏/抓屏,很多情况下使用的都是BitBlt函数来实现的,例如一些屏幕录制软件,局域网教学软件,甚至远程工具类软件。

BitBlt函数声明如下:

BOOL BitBlt(
  HDC   hdc,
  int   x,
  int   y,
  int   cx,
  int   cy,
  HDC   hdcSrc,
  int   x1,
  int   y1,
  DWORD rop
);

一般来说,屏幕录制都是将桌面DC的内容到一个兼容DC中,然后从兼容DC中提取出位图。BitBlt这里涉及到从DC中将整个位图拷贝出来,其实整体效率是比较差的。

本文我们来看一种基于XDDM驱动的MirrorDriver镜像驱动技术,可以直接在内核中高效的抓屏技术。当然这种技术在Win8以上的系统已经被新技术取代,如果在Win8以上的系统,可以使用更加简单的Desktop Duplicate技术。

1. 技术概览

在WinXp的系统下面,显示的引擎主要还是支持2D,但是随着游戏等领域的兴起,D3D成为了Windows的核心显示引擎,MirrorDriver是WinXp下面的技术,是一种驱动层面支持绘图的技术。Mirror驱动和普通的XDDM驱动类似,分为两部分实现:

  1. 负责图像渲染的部分,这部分一般是以DLL形式提供,运行于各自的会话空间。
  2. MiniPort驱动,运行在全局内核空间,一般提供硬件层面的显存等硬件信息。

XDDM驱动的架构如下:
在这里插入图片描述

下面分别来看一下各个部分的实现过程。

2. 小端口驱动

对于Mirror驱动,小端口驱动并不提供任何硬件信息(包括显存等),因此Mirror驱动的小端口实现非常简单,只需要提供最基础的框架接口即可:

ULONG
DriverEntry (
    PVOID Context1,
    PVOID Context2
    )
{

    VIDEO_HW_INITIALIZATION_DATA hwInitData;
    ULONG initializationStatus;

    // Zero out structure.

    VideoPortZeroMemory(&hwInitData, sizeof(VIDEO_HW_INITIALIZATION_DATA));

    // Specify sizes of structure and extension.

    hwInitData.HwInitDataSize = sizeof(VIDEO_HW_INITIALIZATION_DATA);

    // Set entry points.

    hwInitData.HwFindAdapter             = &MirrorFindAdapter;
    hwInitData.HwInitialize              = &MirrorInitialize;
    hwInitData.HwStartIO                 = &MirrorStartIO;
    hwInitData.HwResetHw                 = &MirrorResetHW;
    hwInitData.HwInterrupt               = &MirrorVidInterrupt;
    hwInitData.HwGetPowerState           = &MirrorGetPowerState;
    hwInitData.HwSetPowerState           = &MirrorSetPowerState;
    hwInitData.HwGetVideoChildDescriptor = &MirrorGetChildDescriptor;

    hwInitData.HwLegacyResourceList      = NULL; 
    hwInitData.HwLegacyResourceCount     = 0; 

    // no device extension necessary
    hwInitData.HwDeviceExtensionSize = 0;
    hwInitData.AdapterInterfaceType = 0;

    initializationStatus = VideoPortInitialize(Context1,
                                               Context2,
                                               &hwInitData,
                                               NULL);

    return initializationStatus;

}

小端口驱动都是提供一个VideoPortInitialize函数来支持IRP的回调函数,这个是Windows驱动开发中小端口驱动的基本框架;对于这些回调函数,不用做任何处理,提供一个空的即可。

3. 显示驱动

对于显示驱动并不是通过DriverEntry作为入口,而是通过DrvEnableDriver函数作为驱动的入口,这个函数声明如下:

BOOL DrvEnableDriver(
  ULONG         iEngineVersion,
  ULONG         cj,
  DRVENABLEDATA *pded
);

一般情况,我们在DrvEnableDriver函数中提供相关的回调接口给Win32K调用,一般回调函数设置如下:

static DRVFN gadrvfn[] =
{
    {   INDEX_DrvEnablePDEV,            (PFN) DrvEnablePDEV         },
    {   INDEX_DrvCompletePDEV,          (PFN) DrvCompletePDEV       },
    {   INDEX_DrvDisablePDEV,           (PFN) DrvDisablePDEV        },
    {   INDEX_DrvEnableSurface,         (PFN) DrvEnableSurface      },
    {   INDEX_DrvDisableSurface,        (PFN) DrvDisableSurface     },
    {   INDEX_DrvAssertMode,            (PFN) DrvAssertMode         },
    {   INDEX_DrvNotify,                (PFN) DrvNotify             },
    {   INDEX_DrvCreateDeviceBitmap,    (PFN) DrvCreateDeviceBitmap },
    {   INDEX_DrvDeleteDeviceBitmap,    (PFN) DrvDeleteDeviceBitmap },
    {   INDEX_DrvTextOut,               (PFN) DrvTextOut            },
    {   INDEX_DrvBitBlt,                (PFN) DrvBitBlt             },
    {   INDEX_DrvCopyBits,              (PFN) DrvCopyBits           },
    {   INDEX_DrvStrokePath,            (PFN) DrvStrokePath         },
    {   INDEX_DrvLineTo,                (PFN) DrvLineTo             },
    {   INDEX_DrvFillPath,              (PFN) DrvFillPath           },
    {   INDEX_DrvStrokeAndFillPath,     (PFN) DrvStrokeAndFillPath  },
    {   INDEX_DrvStretchBlt,            (PFN) DrvStretchBlt         },
    {   INDEX_DrvAlphaBlend,            (PFN) DrvAlphaBlend         },
    {   INDEX_DrvTransparentBlt,        (PFN) DrvTransparentBlt     },
    {   INDEX_DrvGradientFill,          (PFN) DrvGradientFill       },
    {   INDEX_DrvPlgBlt,                (PFN) DrvPlgBlt             },
    {   INDEX_DrvStretchBltROP,         (PFN) DrvStretchBltROP      },
#if (NTDDI_VERSION >= NTDDI_VISTA)
    {   INDEX_DrvRenderHint,            (PFN) DrvRenderHint         },
#endif
   {   INDEX_DrvEscape,                (PFN) DrvEscape             },
   {INDEX_DrvSetPointerShape,			(PFN)DrvSetPointerShape},
	{INDEX_DrvMovePointer,			  (PFN)DrvMovePointer}
};

在上述回调函数中,有一些回调函数需要稍微介绍一些:

  1. DrvEnableDriver:是显示驱动的初始入口点,这个API入口参数有一个 DRVENABLEDATA结构,这个结构需要使用一个指向驱动入口的函数表来填充。

  2. DrvDisableDriver:当显示驱动卸载时调用这个函数。在这个函数中,可以执行一些必要的清理工作,来清理你在DrvEnableDriver调用中创建的东西。

  3. DrvGetModes:这个API它用来查询设备支持的显示模式。

  4. DrvEnablePDEV: 一旦选定了一种显示模式,DrvEnablePDEV这个API就会被调用,这个API的用途是允许显示驱动创建自己私有的上下文,这个上下文将会被传递给其他的显示入口点。

  5. DrvCompletePDEV:该函数是用在DrvEnablePDEV调用之后,用来通知显示驱动,设备对象现在已经完成。

  6. DrvDisablePDEV:当PDEV不再需要,且应该被销毁时,就调用这个API。

  7. DrvEnableSurface:这个API是用在PDEV完成以后,用来请求显示驱动创建一个表面,当创建一个表面的时候,可以有两种选择:

    • 可以创建一个由显示驱动管理的表面。
    • 也可以创建一个由GDI管理的表面。
  8. DrvDisableSurface:调用这个API以注销在DrvEnableSurface调用中创建的绘画表面。

对于整个显示驱动,其驱动函数的调用序列如下:

  1. DrvEnableDriver:加载驱动
  2. DrvGetModes:获取缓冲尺寸,来存放所有支持的显示模式。
  3. DrvGetModes:得到显示模式。
  4. DrvEnablePDEV:通知显示驱动在DEVMODE数据结构中初始化一个选中的模式,并且返回一个实例句柄。
  5. DrvCompletePDEV:通知驱动,设备初始化已经完成。
  6. DrvEnableSurface:得到驱动来提供一个绘画表面。
  7. <GDI 调用>。
  8. DrvDisableSurface:删除绘画表面。
  9. DrvDisablePDEV:删除实例结构。
  10. DrvDisableDriver:卸载显示驱动。

3.1 关于绘图HOOK

我们在DrvEnableSurface回调函数中,创建Surface的时候,一般需要对我们敢兴趣的绘图函数进行HOOK(也就是设置相关回调函数),一般我们在EngModifySurface这个函数中完成,该函数声明如下:

ENGAPI BOOL EngModifySurface(
  HSURF  hsurf,
  HDEV   hdev,
  FLONG  flHooks,
  FLONG  flSurface,
  DHSURF dhsurf,
  VOID   *pvScan0,
  LONG   lDelta,
  VOID   *pvReserved
);

我们通过设置flHooks来标记哪些绘图操作是我们感兴趣的,对于HOOK标记有如下:

#define HOOK_BITBLT                     0x00000001
#define HOOK_STRETCHBLT                 0x00000002
#define HOOK_PLGBLT                     0x00000004
#define HOOK_TEXTOUT                    0x00000008
#define HOOK_PAINT                      0x00000010      // Obsolete
#define HOOK_STROKEPATH                 0x00000020
#define HOOK_FILLPATH                   0x00000040
#define HOOK_STROKEANDFILLPATH          0x00000080
#define HOOK_LINETO                     0x00000100
#define HOOK_COPYBITS                   0x00000400
#define HOOK_MOVEPANNING                0x00000800      // Obsolete
#define HOOK_SYNCHRONIZE                0x00001000
#define HOOK_STRETCHBLTROP              0x00002000
#define HOOK_SYNCHRONIZEACCESS          0x00004000      // Obsolete
#define HOOK_TRANSPARENTBLT             0x00008000
#define HOOK_ALPHABLEND                 0x00010000
#define HOOK_GRADIENTFILL               0x00020000
#if (NTDDI_VERSION < NTDDI_VISTA)
#define HOOK_FLAGS                      0x0003b5ff
#else
#define HOOK_FLAGS                      0x0003b5ef
#endif

3.2 关于剪切区

在Mirror驱动中,我们可以对改动区域(一般称作脏区域)进行获取,这个区域我们成为剪切区,例如针对DrvTextOut函数声明如下:

BOOL DrvTextOut(
  SURFOBJ  *pso,
  STROBJ   *pstro,
  FONTOBJ  *pfo,
  CLIPOBJ  *pco,
  RECTL    *prclExtra,
  RECTL    *prclOpaque,
  BRUSHOBJ *pboFore,
  BRUSHOBJ *pboOpaque,
  POINTL   *pptlOrg,
  MIX      mix
);

在这个函数中存在FONTOBJ *pfo表示我们的剪切区(也就是我们通常说的脏区域),我们通过如下清理来进行枚举:

do {
    bMore = CLIPOBJ_bEnum(pco, sizeof(buffer), &buffer.c);
    for (i = 0; i < buffer.c; i++) {
    }
} while (bMore);

3.3 关于共享内存

我们在内核显示驱动中,可以通过MirrorDriver获取到绘图信息,但是如果用户层需要这个数据怎么获取呢?由于数据量太大,使用ExtEscape来说是不太可行的;因此需要一种更加高效的拷贝数据的方法,这种方法就是共享内存。Mirror镜像驱动和用户层通信的共享内存一般存在两种方式:

  1. 第一种是通过EngMapFile以文件为背景,映射一个虚拟内存;然后在用户层通过CreateFileMappingMapViewOfFile来实现内存的共享。
  2. 第二种是在内核中通过ZwCreateSectionMmMapViewInSystemSpace来创建系统内存;然后用户层再通过ZwMapViewOfSection来将内存映射到指定的进程中。

对于第二种方法,存在一个困难的点就是显示驱动是运行再会话空间中,不能调用系统层的相关函数,只能调用Win32k提供的EngXXX函数,因此我们要借助MiniPort小端口驱动来实现,显示驱动通过EngDeviceIoControl将请求转发给小端口驱动;让小端口驱动来完成共享内存的创建。

4. 实现效果

通过上述实现之后,安装硬件驱动,我们可以在设备管理器中看到显示器设备,如下:

在这里插入图片描述

通过Mirror镜像驱动,我们可以实现高效的截图操作,对桌面进行录屏操作,示例如下(右侧为录屏窗口的预览):
在这里插入图片描述

5. 关于AERO特效

开启Mirror驱动之后,会导致AERO特效的关闭,这个是为什么呢?因为AERO特效的实现是在DWM.EXE进程里面完成的,但是DWM的合成操作的图形绘制并不会经过Mirror驱动的回调函数,这是系统的特性。

因此我们开启Mirror的时候,就会导致DWM.EXE的AERO特效关闭。

6. Win10的兼容性

WIN7之后我们就使用的WDDM驱动,Win8之后微软使用了新的抓图技术来实现屏幕抓图;那么Win8之后是否还能使用Mirror镜像驱动呢?

在Win10系统中存在一个文件MirrorDrvCompat.dll,该DLL中大致存在如下的伪代码:

int CMitigationManagement::MitigationManagementThread(LPCRITICAL_SECTION lpCriticalSection)
{
    while (TRUE)
    {
        CMitigationManagement::DuplicateDesktop(...);
        Sleep(...);
    }
}

CMitigationManagement::DuplicateDesktop通过名字也可以发现是Win8之后的抓屏技术,其实在这个函数里面就是使用的Desktop Duplication API来抓屏,然将图像信息绘制到MirrorDriver驱动中去。

因此我们在Win10下面仍旧可以使用Mirror镜像驱动的,这里不得不佩服Windows系统兼容性的强大;但是我们不建议在Win10环境下面使用Mirror镜像驱动,应该使用更新的特性Desktop Duplication API来抓屏。

7. 应用

如果了解过远程相关的技术,那么一定听过一个(或者使用过)一个开源工具uvnc(全名是UltraVNC,地址为https://uvnc.com/),这个工具在Win7及其以下系统,高效的录屏方式就是MirrorDriver。

但是UltraVNC并没有将MirrorDriver录屏技术开源(甚至还进行授权和源码收费),后续文章中,我们将通过其公开的SDK,实现兼容https://uvnc.com/downloads/mirror-driver/85-mirror-driver-downloads.html的镜像驱动。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值