第 15 课:网络库:HPSocket
# 第 15 课:网络库:HPSocket
免责声明:本专栏仅用于安全工程学习研究,禁止使用本专栏介绍的技术做其他用途,否则后果自负,与本号无关。
本课定位:防御分析、源码阅读和检测工程。 本课只解释
主插件\HPSocket目录中的 HPSocket 网络库在工程中的基础设施角色,不提供可直接复用的连接、控制或规避操作步骤。
前面几课已经讲过主控端、协议命令、插件加载和模块缓存。到了这一课,我们把视角切到 主插件\HPSocket 目录中的网络通信库。
新手读这部分代码时,最容易把它理解成“一个 socket 封装”。这个说法不算错,但太粗。更准确地说,它是位于主插件目录下的网络基础设施库:上层模块不直接面对 send、recv、UDP 握手、缓冲区拆包、心跳和断线处理,而是通过 ISocketBase 与 CManager 这组统一接口完成通信。
这套封装在安全工程上有很高价值,因为它把很多行为特征集中在一起:固定长度头、10 字节通信标记、心跳、重连、TCP/UDP 双协议、接收缓存、业务回调和异常断开。
# 1. 本课学习目标
学完本课,你应该能回答这些问题:
| 问题 | 本课回答 |
|---|---|
HPSocket 在工程里做什么 | 给上线模块和功能插件提供统一网络接口 |
ISocketBase 为什么重要 | 它把 TCP 和 UDP 封装成同一套调用面 |
CManager 起什么作用 | 连接 socket 层和业务层,统一发送与接收回调 |
| TCP 收包后怎么进入业务处理 | WorkThread -> recv -> OnReceive -> CBuffer -> CManager::OnReceive |
| UDP 和 TCP 最大差异是什么 | UDP 多了握手事件和 ARQ 可靠发送封装,分片大小也不同 |
| 包格式如何理解 | 前 4 字节长度,后 10 字节通信标记,再后面是业务载荷 |
| 企业怎么检测 | 关联线程、socket、心跳、分片、长度头、异常断开和插件请求行为 |
# 2. 本课涉及源码目录和文件
| 源码文件 | 关键位置 | 本课关注点 |
|---|---|---|
主插件\HPSocket\ISocketBase.h | 8-67 行 | 发送/接收上限、简易锁、统一 socket 接口 |
主插件\HPSocket\Manager.h | 16-35 行 | CManager 对业务回调和发送接口的抽象 |
主插件\HPSocket\Manager.cpp | 12-62 行 | 绑定 socket、转发发送、错误回传 |
主插件\HPSocket\TcpSocket.h | 7-41 行 | TCP 类成员、工作线程、心跳线程 |
主插件\HPSocket\TcpSocket.cpp | 55-300 行 | TCP 连接、收包、拆包、心跳、事件循环 |
主插件\HPSocket\UdpSocket.h | 60-251 行 | UDP 类成员、事件处理、ARQ 发送 |
主插件\HPSocket\UdpSocket.cpp | 652-823 行 | UDP 连接、握手、发送、收包、心跳 |
主插件\HPSocket\Buffer.h | 6-52 行 | 接收缓存对象接口 |
主插件\HPSocket\Buffer.cpp | 39-226 行 | 写入、删除、清空、长度计算 |
主插件\HPSocket\stdafx.h | 29-68 行 | TOKEN_GETVERSION、TOKEN_HEARTBEAT、TOKEN_ACTIVED 等命令字 |
# 3. HPSocket 在整体工程中的角色
先看整体位置。

从图里可以看到,HPSocket 不直接决定业务含义。它关心的是“如何把数据可靠地送到业务层”。业务层包括上线模块、插件管理器、后续功能插件等。
可以按三层理解:
| 层次 | 代表代码 | 新手理解方式 |
|---|---|---|
| 业务层 | 上线模块、各类 KernelManager | 要发送什么命令,收到命令后做什么 |
| 管理层 | CManager | 把业务层和 socket 层连接起来 |
| 传输层 | CTcpSocket、CUdpSocket | 管连接、收包、发包、心跳、拆包 |
这一层次划分很重要。做源码阅读时,不要一看到 Send 就直接跳到业务结论。要先问:这是业务发送,还是传输层发送?是在组包,还是在真正调用系统 socket?
# 4. 新手前置概念
什么是 socket 封装
普通 WinSock 编程会直接调用 socket、connect、send、recv、closesocket 等 API。这个工程没有让每个业务模块都直接写这些调用,而是把它们封装到 CTcpSocket 和 CUdpSocket 中。
封装的好处是上层代码简单:上层只管 Connect、Send、run_event_loop。封装的风险是很多安全关键行为被藏到统一层里,如果只看上层业务,很容易漏掉真实的网络行为。
什么是回调
CManager 持有 socket 对象,socket 对象也保存 CManager*。当 TCP/UDP 收到完整业务数据后,会调用:
// 示意位置:主插件\HPSocket\TcpSocket.cpp / UdpSocket.cpp 的收包完成路径
// 防御分析:这不是独立业务逻辑,而是“传输层把完整业务包交给管理层”的回调边界。
// 读源码时要沿着这个回调继续追踪具体 CManager 子类的 OnReceive,
// 这样才能判断网络数据最终触发了文件、注册表、屏幕、音频还是其他敏感能力。
m_pManager->OnReceive(...);
2
3
4
5
这就是“传输层回调业务层”。新手可以把它理解为:底层收到包后,通知上层“你可以处理了”。
什么是拆包
TCP 是字节流。一次 recv 不一定正好等于一个业务包,可能半包、粘包、多包连在一起。所以代码必须把收到的字节先放进缓存,再根据长度字段取出完整包。
UDP 本身是数据报,但这个工程仍然使用自己的包头和缓冲处理逻辑,便于和 TCP 保持业务层一致。
什么是心跳
心跳就是定期发送很短的数据,确认对端还活着。如果长时间没有收到有效数据,就断开或停止。安全侧经常把固定间隔心跳作为网络行为识别线索。
# 5. 统一接口:ISocketBase
先看 ISocketBase。它是这套网络封装的核心抽象。

// 源码位置:主插件\HPSocket\ISocketBase.h:8-67
// 防御分析注释:
// 1. MAX_SEND_BUFFER / MAX_RECV_BUFFER 固定为 256KB,是静态特征之一。
// 2. ISocketBase 把 TCP 和 UDP 统一成同一组虚函数,方便上层隐藏传输差异。
// 3. m_password 是 10 字节通信标记,不应理解为真正的强认证机制。
// 4. activetime、m_bIsRunning、m_hEvent 是检测连接状态和线程状态的关键字段。
#define MAX_SEND_BUFFER 262144
#define MAX_RECV_BUFFER 262144
class ISocketBase
{
friend class CManager;
public:
virtual void Disconnect() = 0;
virtual BOOL IsRunning() = 0;
virtual int Send(LPBYTE lpData, UINT nSize) = 0;
virtual void setManagerCallBack(CManager* pManager) = 0;
virtual bool Connect(LPCTSTR lpszHost, UINT nPort) = 0;
virtual void run_event_loop() = 0;
HANDLE m_hEvent{ NULL };
CLCS m_clcs;
DWORD activetime;
BOOL m_bIsRunning;
CManager* m_pManager;
byte m_password[10];
};
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
从安全工程角度看,ISocketBase 是一个很适合做“横向阅读”的入口。因为只要找到谁继承它,就能找到所有传输实现;只要找到谁调用 Send,就能追到业务命令如何发出。
# 6. 业务桥接:CManager
CManager 不是具体业务插件的全部逻辑,它更像基础管理器。它把 socket 交给自己管理,并提供统一的 Send 和 OnReceive 入口。
// 源码位置:主插件\HPSocket\Manager.h:16-35
// 防御分析注释:
// 1. CManager 是 socket 层和业务层之间的桥。
// 2. OnReceive 是虚函数,派生类可覆盖它处理不同业务。
// 3. Send 会回到 ISocketBase::Send,最终进入 TCP/UDP 具体实现。
// 4. bStop 是发送路径上的状态控制点。
class CManager
{
friend class ISocketBase;
typedef int (*SENDPROC)(LPBYTE lpData, UINT nSize);
public:
CManager(ISocketBase* pClient);
~CManager();
virtual void OnReceive(LPBYTE lpBuffer, UINT nSize);
int Send(LPBYTE lpData, UINT nSize);
ISocketBase* m_pClient;
BOOL IsConnect();
void Disconnect();
BOOL bStop;
HANDLE m_hEventDlgOpen;
void SendLastError(int mode = 0, TCHAR* s_error_first = _T(""), TCHAR* s_error_next = _T(""));
private:
SENDPROC m_pSendProc;
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 源码位置:主插件\HPSocket\Manager.cpp:12-45
// 防御分析注释:
// 1. 构造函数调用 setManagerCallBack(this),建立“socket -> manager”的回调链。
// 2. Send 不是直接系统调用,而是转发给 m_pClient->Send。
// 3. 如果要追踪业务数据出站,应从 CManager::Send 继续追到 CTcpSocket/CUdpSocket::Send。
CManager::CManager(ISocketBase* pClient)
{
m_pClient = pClient;
m_pClient->setManagerCallBack(this);
m_hEventDlgOpen = CreateEvent(NULL, TRUE, FALSE, NULL);
bStop = FALSE;
}
int CManager::Send(LPBYTE lpData, UINT nSize)
{
if (!bStop)
return m_pClient->Send(lpData, nSize);
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
执行顺序可以这样记:
- 上层创建
CManager或派生管理器。 - 构造函数把当前管理器注册给 socket。
- 业务层调用
CManager::Send。 CManager::Send转发给具体 socket。- socket 收到完整数据后回调
CManager::OnReceive。
这也解释了为什么做动态分析时只盯住 send/recv 不够。真正的业务语义在 OnReceive 的派生实现里,而网络行为在 CTcpSocket 或 CUdpSocket 里。
# 7. TCP 连接初始化
CTcpSocket::Connect 负责建立 TCP 连接、设置 socket 参数、初始化通信标记,并启动工作线程和心跳线程。
// 源码位置:主插件\HPSocket\TcpSocket.cpp:55-123
// 防御分析注释:
// 1. activetime 被写入 m_password 前 8 字节,后续包头会携带该 10 字节标记。
// 2. m_password[8] = TOKEN_ACTIVED,m_password[9] 区分 x86/x64。
// 3. SO_SNDBUF / SO_RCVBUF 使用 MAX_SEND_BUFFER / MAX_RECV_BUFFER。
// 4. 连接成功后创建 WorkThread 和 ThreadHeartbeat,是主机侧行为检测点。
bool CTcpSocket::Connect(LPCTSTR lpszHost, UINT nPort)
{
ResetEvent(m_hEvent);
InterlockedExchange((LPLONG)&m_bIsRunning, FALSE);
activetime = timeGetTime();
ZeroMemory(m_password, 10);
memcpy(m_password, &activetime, 8);
m_password[8] = TOKEN_ACTIVED;
#ifdef _WIN64
m_password[9] = 1;
#else
m_password[9] = 0;
#endif
m_Socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
// 安全化节选:省略域名解析、connect、setsockopt、keepalive 等实现细节,
// 仅保留防御分析所需的连接生命周期。
InterlockedExchange((LPLONG)&m_bIsRunning, TRUE);
m_hWorkerThread = (HANDLE)_beginthreadex(NULL, 0, WorkThread, (void*)this, 0, &nThreadID);
m_hThreadHeartWorker = (HANDLE)_beginthreadex(NULL, 0, ThreadHeartbeat, (void*)this, 0, &nThreadID);
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
这段代码按顺序做了几件事:
| 顺序 | 动作 | 安全含义 |
|---|---|---|
| 1 | 重置事件和运行状态 | 连接生命周期开始 |
| 2 | 用 timeGetTime 初始化通信标记 | 包头中会出现与活跃时间相关的字段 |
| 3 | 创建 TCP socket | 网络连接开始 |
| 4 | 设置发送/接收缓冲区和超时 | 行为参数相对固定 |
| 5 | 配置 keepalive | 长连接倾向 |
| 6 | 创建收包线程和心跳线程 | 主机侧可见线程行为 |
这里要特别注意 m_password。源码把它叫“通信密码”,但从防御角度不能把它等同于强身份认证。它更像一个包级标记,用于在 OnReceive 中过滤不匹配的数据。企业侧检测时应把它看作协议格式特征,而不是安全边界。
# 8. TCP 收包时序
TCP 收包的主线如下。

源码对应 WorkThread。
// 源码位置:主插件\HPSocket\TcpSocket.cpp:128-169
// 防御分析注释:
// 1. recv 每次最多读取 MAX_RECV_BUFFER。
// 2. nSize <= 0 时断开连接,是异常断连和超时分析点。
// 3. 收到数据后不直接处理业务,而是进入 OnReceive 做拆包。
unsigned CTcpSocket::WorkThread(LPVOID lparam)
{
CTcpSocket* pThis = (CTcpSocket*)lparam;
char buff[MAX_RECV_BUFFER] = {};
while (pThis->m_bIsRunning)
{
int nSize = recv(pThis->m_Socket, buff, MAX_RECV_BUFFER, 0);
if (nSize <= 0)
{
pThis->Disconnect();
break;
}
if (nSize > 0)
{
pThis->OnReceive((LPBYTE)buff, nSize);
}
}
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
这段代码解决的是“从系统 socket 取字节”的问题,还没有解决“业务包边界”的问题。业务包边界在 OnReceive 中处理。
# 9. 传输包布局
先看包布局。

发送端组包逻辑在 CTcpSocket::Send。
// 源码位置:主插件\HPSocket\TcpSocket.cpp:200-209
// 防御分析注释:
// 1. 包总长度 = 业务数据长度 + 14。
// 2. 前 4 字节写长度,后 10 字节写 m_password。
// 3. 业务数据通过 CBuffer::Write 写入,第四个参数仍然使用 m_password。
// 4. 固定 14 字节头是协议检测的重要线索。
int CTcpSocket::Send(LPBYTE lpData, UINT nSize)
{
m_clcs.lock();
m_WriteBuffer.ClearBuffer();
LONG nBufLen = nSize + 14;
m_WriteBuffer.Write((PBYTE)&nBufLen, 4);
m_WriteBuffer.Write((PBYTE)m_password, 10);
m_WriteBuffer.Write(lpData, nSize, 1, m_password);
SendWithSplit(m_WriteBuffer.GetBuffer(), m_WriteBuffer.GetBufferLen(), MAX_SEND_BUFFER);
m_clcs.unlock();
return nSize;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
包格式可以写成:
[4 字节总长度][10 字节通信标记][业务载荷]
这对新手很关键。后续看 OnReceive 时,只要抓住这个结构,就不会迷失在缓冲区操作里。
# 10. TCP 拆包和回调
OnReceive 负责把原始字节流还原成完整业务包。
// 源码位置:主插件\HPSocket\TcpSocket.cpp:275-304
// 防御分析注释:
// 1. 新收到的数据先追加到 m_CompressionBuffer。
// 2. 至少超过 14 字节后,才检查通信标记和包长度。
// 3. 如果通信标记不匹配,直接清空缓存并返回。
// 4. 完整包解出后调用 m_pManager->OnReceive,业务层从这里开始接管。
// 5. 防御侧应关注异常长度、频繁清空缓存、短时间大量回调等行为。
bool CTcpSocket::OnReceive(const BYTE* pData, int iLength)
{
m_CompressionBuffer.Write((LPBYTE)pData, iLength);
while ((int)(m_CompressionBuffer.GetBufferLen()) > 14)
{
if (memcmp(m_password, m_CompressionBuffer.GetBuffer(4), 10) != 0)
{
m_CompressionBuffer.ClearBuffer();
return FALSE;
}
int nSize = 0;
CopyMemory(&nSize, m_CompressionBuffer.GetBuffer(0), sizeof(int));
if (nSize && (int)m_CompressionBuffer.GetBufferLen() >= nSize)
{
activetime = timeGetTime();
m_DeCompressionBuffer.ClearBuffer();
m_DeCompressionBuffer.Write(m_CompressionBuffer.GetBuffer(14), nSize - 14, 1, m_password);
m_pManager->OnReceive(m_DeCompressionBuffer.GetBuffer(0), m_DeCompressionBuffer.GetBufferLen());
m_CompressionBuffer.Delete(nSize);
}
else
{
break;
}
}
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
按执行顺序拆开看:
- 把新收到的字节追加到
m_CompressionBuffer。 - 判断缓存长度是否大于包头长度
14。 - 从偏移
4的位置检查 10 字节通信标记。 - 从偏移
0读取完整包长度。 - 如果缓存已经包含完整包,就取出
14字节之后的业务载荷。 - 调用
m_pManager->OnReceive。 - 从缓存中删除已经处理的包。
安全风险不在“有长度字段”本身,而在边界处理是否稳健。企业做检测或加固时,应关注这些点:
| 位置 | 风险点 | 防御观察 |
|---|---|---|
| 长度字段 | 异常大、异常小、负值解释 | 记录异常长度并阻断解析 |
| 通信标记 | 被误认为强认证 | 不应作为唯一可信边界 |
| 缓冲区追加 | 长时间积累半包 | 监控缓存增长和解析失败 |
| 回调业务层 | 网络数据进入业务逻辑 | 关联后续 token 分发和插件加载 |
# 11. TCP 心跳与断连
// 源码位置:主插件\HPSocket\TcpSocket.cpp:174-197
// 防御分析注释:
// 1. 心跳 token 为 TOKEN_HEARTBEAT。
// 2. 循环中多次 Sleep,形成相对固定的周期性行为。
// 3. activetime 超过阈值后断开连接。
// 4. 网络侧可把短包心跳、长连接和超时断开组合成检测线索。
unsigned CTcpSocket::ThreadHeartbeat(LPVOID thisContext)
{
CTcpSocket* pThis = (CTcpSocket*)thisContext;
BYTE Token = TOKEN_HEARTBEAT;
while (pThis->m_bIsRunning)
{
for (int i = 0; i < 1000; i++)
{
Sleep(10);
if (!pThis->m_bIsRunning)
break;
}
pThis->Send(&Token, 1);
if (timeGetTime() - pThis->activetime > 60000)
pThis->Disconnect();
}
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
这一段适合从行为角度理解:
| 行为 | 防御意义 |
|---|---|
| 周期性发送 1 字节业务 token | 心跳流量特征 |
心跳仍走 Send 组包 | 实际线上仍带 14 字节包头 |
activetime 控制断连 | 可关联最后一次有效收包时间 |
| 超过 60 秒断开 | TCP 连接生命周期特征 |
# 12. UDP 封装差异
TCP 和 UDP 在上层看起来一样,都实现 ISocketBase,但底层实现差异明显。

// 源码位置:主插件\HPSocket\UdpSocket.cpp:652-686
// 防御分析注释:
// 1. UDP 同样初始化 m_password,并写入 TOKEN_ACTIVED 和位数标记。
// 2. Start 后等待 m_hEvent_run,说明 UDP 连接建立依赖握手事件。
// 3. 成功后启动 ThreadHeartbeat。
bool CUdpSocket::Connect(LPCTSTR lpszHost, UINT nPort)
{
ResetEvent(m_hEvent);
ResetEvent(m_hEvent_run);
activetime = timeGetTime();
InterlockedExchange((LPLONG)&m_bIsRunning, FALSE);
ZeroMemory(m_password, 10);
memcpy(m_password, &activetime, 8);
m_password[8] = TOKEN_ACTIVED;
#ifdef _WIN64
m_password[9] = 1;
#else
m_password[9] = 0;
#endif
BOOL ret = Start(lpszHost, nPort);
DWORD dw = WaitForSingleObject(m_hEvent_run, 6000);
if (dw == WAIT_OBJECT_0)
{
InterlockedExchange((LPLONG)&m_bIsRunning, TRUE);
m_hThreadHeartWorker = (HANDLE)_beginthreadex(NULL, 0, ThreadHeartbeat, (void*)this, 0, &nThreadID);
return true;
}
return false;
}
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
UDP 的发送仍然采用类似包头,但拆分大小不同:
// 源码位置:主插件\HPSocket\UdpSocket.cpp:717-733
// 防御分析注释:
// 1. UDP 组包仍然是 4 字节长度 + 10 字节通信标记 + 业务载荷。
// 2. SendWithSplit 使用 4000 字节分片,而不是 TCP 的 MAX_SEND_BUFFER。
// 3. 实际发送调用 ArqSend,说明它在 UDP 上叠加可靠传输语义。
int CUdpSocket::Send(LPBYTE lpData, UINT nSize)
{
m_clcs.lock();
m_WriteBuffer.ClearBuffer();
LONG nBufLen = nSize + 14;
m_WriteBuffer.Write((PBYTE)&nBufLen, 4);
m_WriteBuffer.Write((PBYTE)m_password, 10);
m_WriteBuffer.Write(lpData, nSize, 1, m_password);
SendWithSplit(m_WriteBuffer.GetBuffer(), m_WriteBuffer.GetBufferLen(), 4000);
m_clcs.unlock();
return nSize;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
UDP 的接收和 TCP 很像,同样会把完整业务包交给 m_pManager->OnReceive。
// 源码位置:主插件\HPSocket\UdpSocket.cpp:789-821
// 防御分析注释:
// 1. UDP 也检查 10 字节通信标记。
// 2. 解析完整包后更新 activetime。
// 3. 最终仍回调到同一个业务管理器接口。
EnHandleResult CUdpSocket::OnReceive(CUdpSocket* pSender, const BYTE* pData, int iLength)
{
m_CompressionBuffer.Write((LPBYTE)pData, iLength);
while ((int)(m_CompressionBuffer.GetBufferLen()) > 14)
{
if (memcmp(m_password, m_CompressionBuffer.GetBuffer(4), 10) != 0)
{
m_CompressionBuffer.ClearBuffer();
return HR_ERROR;
}
int nSize = 0;
CopyMemory(&nSize, m_CompressionBuffer.GetBuffer(0), sizeof(int));
if (nSize && (int)m_CompressionBuffer.GetBufferLen() >= nSize)
{
activetime = timeGetTime();
m_DeCompressionBuffer.ClearBuffer();
m_DeCompressionBuffer.Write(m_CompressionBuffer.GetBuffer(14), nSize - 14, 1, m_password);
m_pManager->OnReceive(m_DeCompressionBuffer.GetBuffer(0), m_DeCompressionBuffer.GetBufferLen());
m_CompressionBuffer.Delete(nSize);
}
else
{
break;
}
}
return HR_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
对检测来说,TCP 和 UDP 应分别建模:
| 项目 | TCP | UDP |
|---|---|---|
| 建连方式 | connect | Start + 握手事件 |
| 接收入口 | WorkThread 调 recv | 事件回调 OnReceive |
| 发送拆分 | MAX_SEND_BUFFER | 4000 字节 |
| 保活 | WinSock keepalive + 心跳线程 | ARQ/握手 + 心跳线程 |
| 超时 | 约 60 秒活跃检查 | 约 30 秒活跃检查 |
# 13. CBuffer 在拆包中的作用
CBuffer 是一个动态缓冲对象。它本身不是业务逻辑,但它决定了字节如何被追加、删除、清空和读取。
// 源码位置:主插件\HPSocket\Buffer.h:6-52
// 防御分析注释:
// 1. CBuffer 是 TCP/UDP OnReceive 的中间缓存。
// 2. Write、Delete、ClearBuffer 对应追加数据、消费数据、重置状态。
// 3. 缓冲区长度异常增长、频繁清空、重复 Delete 都是排障和检测线索。
class CBuffer
{
public:
CBuffer();
CBuffer(UINT nSize);
virtual ~CBuffer();
void ClearBuffer();
LPBYTE GetBuffer(UINT nPos = 0);
UINT GetBufferLen();
void Write(PBYTE lpBuf, UINT nSize);
void Write(PBYTE lpBuf, UINT nSize, int key, PBYTE pKey);
UINT Delete(UINT nSize);
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
新手可以这样理解:
recv/UDP回调拿到字节
|
v
CBuffer 先存起来
|
v
够一个完整包才取出来
|
v
CManager::OnReceive 处理业务
2
3
4
5
6
7
8
9
10
这就是“字节流”和“业务包”的分界线。
# 14. 调用链整理
把本课所有内容串起来,TCP 的核心调用链是:
CManager::Send
-> CTcpSocket::Send
-> CBuffer::Write 组包
-> CTcpSocket::SendWithSplit
-> send
CTcpSocket::WorkThread
-> recv
-> CTcpSocket::OnReceive
-> CBuffer::Write 追加
-> 检查长度和通信标记
-> CBuffer::Delete 消费完整包
-> CManager::OnReceive
2
3
4
5
6
7
8
9
10
11
12
13
UDP 的核心调用链是:
CManager::Send
-> CUdpSocket::Send
-> CBuffer::Write 组包
-> CUdpSocket::SendWithSplit
-> ArqSend
CUdpSocket::OnReceive
-> CBuffer::Write 追加
-> 检查长度和通信标记
-> CManager::OnReceive
2
3
4
5
6
7
8
9
10
这两条链最后都回到 CManager::OnReceive,这就是统一接口的意义。
# 15. 企业检测点

可以按五类信号建模。
| 类型 | 检测点 | 说明 |
|---|---|---|
| 主机侧 | 创建网络工作线程和心跳线程 | 结合 _beginthreadex、CreateEvent、socket API |
| 内存侧 | 固定包头、接收缓存、长度字段 | 关注 4+10 字节包头模式和缓存异常增长 |
| 网络侧 | 长连接、周期心跳、分片发送 | 关注小包周期性、重复重连、异常分片 |
| 日志侧 | 断连、超时、异常包 | 记录包长不合理、通信标记不匹配、频繁清空缓存 |
| 构建侧 | 常量和符号 | MAX_SEND_BUFFER、TOKEN_HEARTBEAT、TOKEN_ACTIVED 等静态线索 |
一个实用的企业关联逻辑可以这样写成分析思路:
- 进程创建 socket 连接,并创建一个接收线程。
- 同一进程出现固定周期短包心跳。
- 网络数据进入自定义缓冲区,按固定长度头拆包。
- 拆出的业务包进入 token 分发或插件加载链。
- 出现异常断连后按固定间隔重连。
如果这些信号同时出现,风险等级应明显高于单独看到一次 connect 或一次 send。
# 16. 安全检查项
| 检查项 | 为什么检查 | 建议 |
|---|---|---|
| 长度字段是否有上限 | 防止异常长度导致内存压力或解析异常 | 设置最大业务包长度并记录异常 |
| 通信标记是否被当成认证 | 10 字节标记不等于强认证 | 不把它作为可信安全边界 |
| 心跳间隔是否固定 | 固定周期容易形成行为特征 | 企业侧用于检测,开发侧应显式记录 |
| 缓冲区是否可能无限增长 | 半包长期积累会造成资源风险 | 增加缓存上限和超时清理 |
| 异常包是否有日志 | 没有日志会降低溯源能力 | 记录长度、来源、状态,不记录敏感明文 |
| TCP/UDP 是否共用业务入口 | 方便功能复用,也扩大攻击面 | 在 CManager::OnReceive 统一做边界校验 |
# 17. 常见误区
误区一:看到 m_password 就认为通信是安全的。
它只是包格式中的通信标记。是否具备认证强度,要看密钥生成、保密性、重放防护、完整性校验和协商机制。这里不能把它当成强认证。
误区二:TCP 有长度字段就不会粘包。
长度字段只是解决粘包的一种基础手段。仍然要检查长度是否合理、缓存是否可控、异常包是否能安全退出。
误区三:UDP 是无连接,所以没有状态。
这份代码通过握手事件、ARQ、心跳和 activetime 给 UDP 叠加了状态。分析时不能只按普通 UDP 数据报理解。
误区四:只看业务插件,不看 HPSocket。
很多稳定特征在网络基础设施层:包头、心跳、分片、事件、线程、缓冲区。忽略这一层,会丢失检测证据。
# 18. 合法练习题
- 画出
CManager::Send -> CTcpSocket::Send -> send的调用链,并标出组包位置。 - 画出
recv -> CTcpSocket::OnReceive -> CManager::OnReceive的调用链,并标出拆包位置。 - 对比 TCP 和 UDP 的连接流程,写出至少 5 个差异点。
- 从源码中整理所有和心跳有关的 token、线程、时间字段和断开条件。
- 设计一份企业检测规则思路:同时使用主机线程、网络心跳、固定包头和异常断连四类信号,不需要写成可运行规则。
# 19. 本课小结
主插件\HPSocket 目录下的 HPSocket 是网络通信库,也是插件侧通信的基础设施层。它把 TCP 和 UDP 包装成统一接口,让上层通过 CManager 发送和接收业务数据。
从防御角度看,本课最重要的不是某一个 socket API,而是完整行为链:连接初始化、通信标记生成、固定包头组包、缓存拆包、业务回调、心跳保活、异常断开和重连。后续分析上线模块时,我们会看到这些网络能力如何被具体配置字段和连接循环调用起来。
安全工程知识交流加wx:easy_coder,黑灰产勿扰,企业单位合作请出示有效证件。
本留言区仅对应当前文章,欢迎补充观点、提出问题或帮助修正文中疏漏。 留言由 GitHub/Gitalk 提供,需要使用 GitHub 登录。
社区交流
讨论与留言