15 / 52 插件与被控运行机制

第 15 课:网络库:HPSocket

# 第 15 课:网络库:HPSocket

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

本课定位:防御分析、源码阅读和检测工程。 本课只解释 主插件\HPSocket 目录中的 HPSocket 网络库在工程中的基础设施角色,不提供可直接复用的连接、控制或规避操作步骤。

前面几课已经讲过主控端、协议命令、插件加载和模块缓存。到了这一课,我们把视角切到 主插件\HPSocket 目录中的网络通信库。

新手读这部分代码时,最容易把它理解成“一个 socket 封装”。这个说法不算错,但太粗。更准确地说,它是位于主插件目录下的网络基础设施库:上层模块不直接面对 sendrecv、UDP 握手、缓冲区拆包、心跳和断线处理,而是通过 ISocketBaseCManager 这组统一接口完成通信。

这套封装在安全工程上有很高价值,因为它把很多行为特征集中在一起:固定长度头、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_GETVERSIONTOKEN_HEARTBEATTOKEN_ACTIVED 等命令字

# 3. HPSocket 在整体工程中的角色

先看整体位置。

HPSocket 网络库在工程中的位置

从图里可以看到,HPSocket 不直接决定业务含义。它关心的是“如何把数据可靠地送到业务层”。业务层包括上线模块、插件管理器、后续功能插件等。

可以按三层理解:

层次 代表代码 新手理解方式
业务层 上线模块、各类 KernelManager 要发送什么命令,收到命令后做什么
管理层 CManager 把业务层和 socket 层连接起来
传输层 CTcpSocketCUdpSocket 管连接、收包、发包、心跳、拆包

这一层次划分很重要。做源码阅读时,不要一看到 Send 就直接跳到业务结论。要先问:这是业务发送,还是传输层发送?是在组包,还是在真正调用系统 socket?

# 4. 新手前置概念

什么是 socket 封装

普通 WinSock 编程会直接调用 socketconnectsendrecvclosesocket 等 API。这个工程没有让每个业务模块都直接写这些调用,而是把它们封装到 CTcpSocketCUdpSocket 中。

封装的好处是上层代码简单:上层只管 ConnectSendrun_event_loop。封装的风险是很多安全关键行为被藏到统一层里,如果只看上层业务,很容易漏掉真实的网络行为。

什么是回调

CManager 持有 socket 对象,socket 对象也保存 CManager*。当 TCP/UDP 收到完整业务数据后,会调用:

// 示意位置:主插件\HPSocket\TcpSocket.cpp / UdpSocket.cpp 的收包完成路径
// 防御分析:这不是独立业务逻辑,而是“传输层把完整业务包交给管理层”的回调边界。
// 读源码时要沿着这个回调继续追踪具体 CManager 子类的 OnReceive,
// 这样才能判断网络数据最终触发了文件、注册表、屏幕、音频还是其他敏感能力。
m_pManager->OnReceive(...);
1
2
3
4
5

这就是“传输层回调业务层”。新手可以把它理解为:底层收到包后,通知上层“你可以处理了”。

什么是拆包

TCP 是字节流。一次 recv 不一定正好等于一个业务包,可能半包、粘包、多包连在一起。所以代码必须把收到的字节先放进缓存,再根据长度字段取出完整包。

UDP 本身是数据报,但这个工程仍然使用自己的包头和缓冲处理逻辑,便于和 TCP 保持业务层一致。

什么是心跳

心跳就是定期发送很短的数据,确认对端还活着。如果长时间没有收到有效数据,就断开或停止。安全侧经常把固定间隔心跳作为网络行为识别线索。

# 5. 统一接口:ISocketBase

先看 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];
};
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

从安全工程角度看,ISocketBase 是一个很适合做“横向阅读”的入口。因为只要找到谁继承它,就能找到所有传输实现;只要找到谁调用 Send,就能追到业务命令如何发出。

# 6. 业务桥接:CManager

CManager 不是具体业务插件的全部逻辑,它更像基础管理器。它把 socket 交给自己管理,并提供统一的 SendOnReceive 入口。

// 源码位置:主插件\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;
};
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
// 源码位置:主插件\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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

执行顺序可以这样记:

  1. 上层创建 CManager 或派生管理器。
  2. 构造函数把当前管理器注册给 socket。
  3. 业务层调用 CManager::Send
  4. CManager::Send 转发给具体 socket。
  5. socket 收到完整数据后回调 CManager::OnReceive

这也解释了为什么做动态分析时只盯住 send/recv 不够。真正的业务语义在 OnReceive 的派生实现里,而网络行为在 CTcpSocketCUdpSocket 里。

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

这段代码按顺序做了几件事:

顺序 动作 安全含义
1 重置事件和运行状态 连接生命周期开始
2 timeGetTime 初始化通信标记 包头中会出现与活跃时间相关的字段
3 创建 TCP socket 网络连接开始
4 设置发送/接收缓冲区和超时 行为参数相对固定
5 配置 keepalive 长连接倾向
6 创建收包线程和心跳线程 主机侧可见线程行为

这里要特别注意 m_password。源码把它叫“通信密码”,但从防御角度不能把它等同于强身份认证。它更像一个包级标记,用于在 OnReceive 中过滤不匹配的数据。企业侧检测时应把它看作协议格式特征,而不是安全边界。

# 8. TCP 收包时序

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

这段代码解决的是“从系统 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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

包格式可以写成:

[4 字节总长度][10 字节通信标记][业务载荷]
1

这对新手很关键。后续看 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;
}
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

按执行顺序拆开看:

  1. 把新收到的字节追加到 m_CompressionBuffer
  2. 判断缓存长度是否大于包头长度 14
  3. 从偏移 4 的位置检查 10 字节通信标记。
  4. 从偏移 0 读取完整包长度。
  5. 如果缓存已经包含完整包,就取出 14 字节之后的业务载荷。
  6. 调用 m_pManager->OnReceive
  7. 从缓存中删除已经处理的包。

安全风险不在“有长度字段”本身,而在边界处理是否稳健。企业做检测或加固时,应关注这些点:

位置 风险点 防御观察
长度字段 异常大、异常小、负值解释 记录异常长度并阻断解析
通信标记 被误认为强认证 不应作为唯一可信边界
缓冲区追加 长时间积累半包 监控缓存增长和解析失败
回调业务层 网络数据进入业务逻辑 关联后续 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;
}
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

这一段适合从行为角度理解:

行为 防御意义
周期性发送 1 字节业务 token 心跳流量特征
心跳仍走 Send 组包 实际线上仍带 14 字节包头
activetime 控制断连 可关联最后一次有效收包时间
超过 60 秒断开 TCP 连接生命周期特征

# 12. UDP 封装差异

TCP 和 UDP 在上层看起来一样,都实现 ISocketBase,但底层实现差异明显。

TCP 与 UDP 封装差异

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

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

对检测来说,TCP 和 UDP 应分别建模:

项目 TCP UDP
建连方式 connect Start + 握手事件
接收入口 WorkThreadrecv 事件回调 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);
};
1
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 处理业务
1
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
1
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
1
2
3
4
5
6
7
8
9
10

这两条链最后都回到 CManager::OnReceive,这就是统一接口的意义。

# 15. 企业检测点

HPSocket 检测关联点

可以按五类信号建模。

类型 检测点 说明
主机侧 创建网络工作线程和心跳线程 结合 _beginthreadexCreateEvent、socket API
内存侧 固定包头、接收缓存、长度字段 关注 4+10 字节包头模式和缓存异常增长
网络侧 长连接、周期心跳、分片发送 关注小包周期性、重复重连、异常分片
日志侧 断连、超时、异常包 记录包长不合理、通信标记不匹配、频繁清空缓存
构建侧 常量和符号 MAX_SEND_BUFFERTOKEN_HEARTBEATTOKEN_ACTIVED 等静态线索

一个实用的企业关联逻辑可以这样写成分析思路:

  1. 进程创建 socket 连接,并创建一个接收线程。
  2. 同一进程出现固定周期短包心跳。
  3. 网络数据进入自定义缓冲区,按固定长度头拆包。
  4. 拆出的业务包进入 token 分发或插件加载链。
  5. 出现异常断连后按固定间隔重连。

如果这些信号同时出现,风险等级应明显高于单独看到一次 connect 或一次 send

# 16. 安全检查项

检查项 为什么检查 建议
长度字段是否有上限 防止异常长度导致内存压力或解析异常 设置最大业务包长度并记录异常
通信标记是否被当成认证 10 字节标记不等于强认证 不把它作为可信安全边界
心跳间隔是否固定 固定周期容易形成行为特征 企业侧用于检测,开发侧应显式记录
缓冲区是否可能无限增长 半包长期积累会造成资源风险 增加缓存上限和超时清理
异常包是否有日志 没有日志会降低溯源能力 记录长度、来源、状态,不记录敏感明文
TCP/UDP 是否共用业务入口 方便功能复用,也扩大攻击面 CManager::OnReceive 统一做边界校验

# 17. 常见误区

误区一:看到 m_password 就认为通信是安全的。

它只是包格式中的通信标记。是否具备认证强度,要看密钥生成、保密性、重放防护、完整性校验和协商机制。这里不能把它当成强认证。

误区二:TCP 有长度字段就不会粘包。

长度字段只是解决粘包的一种基础手段。仍然要检查长度是否合理、缓存是否可控、异常包是否能安全退出。

误区三:UDP 是无连接,所以没有状态。

这份代码通过握手事件、ARQ、心跳和 activetime 给 UDP 叠加了状态。分析时不能只按普通 UDP 数据报理解。

误区四:只看业务插件,不看 HPSocket。

很多稳定特征在网络基础设施层:包头、心跳、分片、事件、线程、缓冲区。忽略这一层,会丢失检测证据。

# 18. 合法练习题

  1. 画出 CManager::Send -> CTcpSocket::Send -> send 的调用链,并标出组包位置。
  2. 画出 recv -> CTcpSocket::OnReceive -> CManager::OnReceive 的调用链,并标出拆包位置。
  3. 对比 TCP 和 UDP 的连接流程,写出至少 5 个差异点。
  4. 从源码中整理所有和心跳有关的 token、线程、时间字段和断开条件。
  5. 设计一份企业检测规则思路:同时使用主机线程、网络心跳、固定包头和异常断连四类信号,不需要写成可运行规则。

# 19. 本课小结

主插件\HPSocket 目录下的 HPSocket 是网络通信库,也是插件侧通信的基础设施层。它把 TCP 和 UDP 包装成统一接口,让上层通过 CManager 发送和接收业务数据。

从防御角度看,本课最重要的不是某一个 socket API,而是完整行为链:连接初始化、通信标记生成、固定包头组包、缓存拆包、业务回调、心跳保活、异常断开和重连。后续分析上线模块时,我们会看到这些网络能力如何被具体配置字段和连接循环调用起来。

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

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

社区交流

讨论与留言

前往 GitHub Issues →

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

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