10 / 52 Windows、主控与网络

第 10 课:网络通信模型

# 第 10 课:网络通信模型

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

第 8 课讲了主控端架构:界面、连接列表、功能窗口和插件调度如何串起来。本课继续往下一层看,重点讲网络通信模型。

新手读这类源码时容易犯一个错误:一看到 SendOnReceiveTCPUDP,就以为网络层已经知道“文件管理”“屏幕监控”“终端命令”这些业务含义。实际上不是这样。网络层只负责几件事:

  1. 监听端口。
  2. 接受连接或握手。
  3. 给连接绑定 ClientContext
  4. 收到字节后写入缓存。
  5. 判断是否拼出完整业务包。
  6. 通过回调把事件交给主框架。
  7. 发送时把业务数据包装成网络包。
  8. 断开时清理上下文。

真正解释业务含义的是后面的 TOKEN_*、功能窗口和插件调度。第 11 课会继续讲协议和命令分发,本课先把网络层看清楚。

# 1. 本课学习目标

学完本课,你应该能回答这些问题:

问题 本课回答
HPSocket 在工程里做什么 它负责底层 TCP/UDP 监听、收发事件和连接 ID 管理
ISocketBase 的作用是什么 它把 TCP 和 UDP 封装成统一入口
ClientContext 为什么重要 它是一条连接的状态档案,保存连接 ID、缓存、密码、窗口和统计信息
TCP 与 UDP 的主要差异在哪里 TCP 在 OnAccept 创建上下文,UDP 在 OnHandShake 创建上下文
收包为什么要缓存 网络到达的是字节片段,必须判断完整包后才能分发
企业如何检测这类通信 结合进程监听、长连接、固定包头、心跳周期、异常分片和回调行为

# 2. 本课涉及源码目录和文件

源码文件 本课关注点
主控\Quick\ISocketBase.h 统一网络入口、SsocketServerMapNOTIFYPROC
主控\Quick\ISocketBase.cpp 根据 TCP/UDP 创建服务、统一发送、断开和关闭
主控\Quick\HpTcpServer.h TCP 服务类、HPSocket TCP 监听器接口
主控\Quick\HpTcpServer.cpp TCP 初始化、接入、收包、分包发送和断开清理
主控\Quick\HpUdpServer.h UDP 服务类、HPSocket UDP 监听器接口
主控\Quick\HpUdpServer.cpp UDP 握手、心跳检测、收包、分包发送和断开清理
主控\Quick\macros.h ClientContexte_socketNC_* 回调码、缓冲区大小
主控\Quick\MainFrm.cpp NotifyProc 接收网络事件并转给主框架处理

本课不讲如何搭建外联链路,也不讲如何投递或运行远控组件。我们只从安全工程角度分析源码结构、危险信号、检测点和审计方法。

# 3. 网络层在整体工程中的位置

先看这一层的结构图:

第 10 课图 1:网络层结构地图

这张图要按从上到下理解:

层级 代表对象 说明
界面配置 SettingDlgBuildDlg 用户在界面里配置监听协议和端口
统一入口 ISocketBase 屏蔽 TCP/UDP 差异,给主控端提供统一 Send / Disconnect
TCP 服务 CHpTcpServer 使用 HPSocket 的 TCP Pull Server 模型
UDP 服务 CHpUdpServer 使用 HPSocket 的 UDP ARQ Server 模型
主框架回调 CMainFrame::NotifyProc 接收 NC_RECEIVENC_RECEIVE_COMPLETE、断开等事件

对新手来说,可以把 ISocketBase 理解成“总插座”:上层不直接关心当前连接到底是 TCP 还是 UDP,而是把 ClientContext 交给 ISocketBase,由它根据 switchsocket 分发到具体服务类。

# 4. 先补课:几个必须理解的概念

# 4.1 HPSocket 是什么

HPSocket 是一个 Windows 网络通信库。工程中通过它提供的接口完成监听、收包、发包、断开和连接 ID 管理。

在本源码里,你不需要一开始就理解 HPSocket 的全部实现。先记住这个模型:

角色 白话解释
CTcpPullServerPtr TCP 服务对象,收到数据后由上层主动 Fetch 取数据
CUdpArqServerPtr UDP ARQ 服务对象,在 UDP 之上维护类似连接的可靠传输语义
CONNID HPSocket 给一条连接分配的 ID
SetConnectionExtra 给连接 ID 绑定自己的业务对象
GetConnectionExtra 通过连接 ID 找回业务对象
Listener 回调 网络事件发生后调用上层函数,如 OnAcceptOnReceiveOnClose

# 4.2 IOCP 是什么

IOCP 是 Windows 的高性能异步 I/O 模型。它适合处理大量连接,因为网络事件不会简单阻塞在一个线程里。

本工程里文件名和变量名仍然保留了一些 iocp 字样,但本课关注的是 HPSocket 封装后的事件模型。你可以先理解成:

底层网络事件 -> HPSocket 回调 -> ClientContext -> NotifyProc -> 主控业务分发

# 4.3 字节流和业务包不是一回事

TCP 收到的是字节流,UDP 收到的是数据报,二者都不等于“业务命令”。源码需要自己定义业务包结构,再从缓存中判断一个包是否收齐。

本工程的简化包结构如下:

第 10 课图 4:业务包结构

Debug 场景下,可以把包头理解为:

字段 作用 防御关注点
4 字节长度 表示总包长 固定长度字段、异常大包、长度和实际数据不一致
10 字节认证/位数信息 保存通信标识和客户端位数等信息 固定偏移字段、认证设计弱、可被伪造风险
加密业务数据 真正交给上层的业务内容 解包、解密、命令分发和插件数据

源码中 m_headerlength = itnnum + 10。Debug 分支里 itnnum = 4,正好对应“4 字节长度 + 10 字节标识”。Release 分支受保护宏影响,审计时要结合编译条件确认真实头部长度。

# 5. 关键常量和结构

先看 macros.h 里的几个基础定义。

// 源码位置:主控\Quick\macros.h:79-97
// 防御分析:e_socket 决定连接走 TCP 还是 UDP;NC_* 是网络层向主框架上报的事件类型。
enum e_socket
{
    tcp,
    udp,
};

#define NC_CLIENT_CONNECT       0x0001
#define NC_CLIENT_DISCONNECT    0x0002
#define NC_TRANSMIT             0x0003
#define NC_RECEIVE              0x0004
#define NC_RECEIVE_COMPLETE     0x0005
#define NC_SEND_SHELCODE_32     0x0006
#define NC_SEND_SHELCODE_64     0x0007

// 防御分析:固定的大缓冲区上限会形成网络行为特征,也需要关注异常大包。
#define MAX_SEND_BUFFER         65535
#define MAX_RECV_BUFFER         65535
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

这段常量定义解决了连接类型、网络回调、完整包回调和发送分片上限几个问题。相关防御判断已经放在代码注释里:ISocketBase::Send 会依据连接类型选择发送实现,NC_RECEIVE_COMPLETE 才适合进入协议解析,NC_SEND_SHELCODE_* 只做高风险分支标记,MAX_SEND_BUFFER 会影响大块传输的网络形态。

再看 ClientContext 的网络相关字段。

// 源码位置:主控\Quick\macros.h:222-268
struct ClientContext
{
    ULONG_PTR m_Socket;

    // 防御分析:发送缓存、收包缓存、解包后缓存,是网络审计最关键的三块缓冲区。
    CBuffer m_WriteBuffer;
    CBuffer m_CompressionBuffer;
    CBuffer m_DeCompressionBuffer;

    // 防御分析:功能窗口绑定在连接上下文里,网络回调可能间接触碰 UI。
    int m_Dialog[2];

    int       m_allpack_rev;
    long long m_alldata_rev;
    int       m_allpack_send;
    long long m_alldata_send;

    int  IsConnect;
    CLCS m_clcs_send_rec_close;

    BOOL     m_bIsMainSocket;
    e_socket switchsocket;
    TCHAR    szAddress[20];
    USHORT   usPort;
    PVOID    m_server;
    byte     m_password[10];

    TCHAR m_ip[255];
    USHORT m_port;
    BOOL bisx86;
    LOGININFO* LoginInfo;
};
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

学习这个结构时,可以把它拆成五组:

字段组 代表字段 说明
连接身份 m_Socketswitchsocketm_server 标识连接 ID、协议类型和所属服务对象
缓冲区 m_WriteBufferm_CompressionBufferm_DeCompressionBuffer 发送、接收和解包都围绕它们完成
统计信息 m_allpack_revm_alldata_send 可用于流量画像和异常计数
状态与同步 IsConnectm_clcs_send_rec_close 判断连接是否有效,保护收发关闭过程
业务绑定 m_DialogLoginInfopView 后续把连接绑定到 UI 和业务窗口

# 6. ISocketBase:统一网络入口

ISocketBase 是主控端网络层最适合新手先读的类,因为它不处理复杂收包细节,只负责选择具体实现。

// 源码位置:主控\Quick\ISocketBase.h:7-33
struct Ssocket
{
    PVOID socketserver;
    TCHAR m_ip[255];
    int m_port;
    e_socket m_e_socket;
    BOOL runok;
    BOOL m_stop;
};

typedef void (CALLBACK* NOTIFYPROC)(ClientContext*, UINT nCode);
typedef std::map<int, Ssocket*> ServerMap;

class ISocketBase
{
public:
    ServerMap g_servermap;

    bool Addserver(NOTIFYPROC pNotifyProc, CMainFrame* pFrame,
        serverstartdate* m_serverstartdate);

    virtual void Send(ClientContext* pContext, LPBYTE lpData, UINT nSize);
    void DelServer(serverstartdate* m_serverstartdate);
    void Shutdown();
    void Disconnect(ClientContext* m_pContext);
};
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

这里的设计意图很明确:

对象 作用
Ssocket 保存一个监听服务的服务对象、IP、端口、协议和运行状态
ServerMap 管理多个监听服务
NOTIFYPROC 网络层向主框架上报事件的函数指针
Addserver 根据配置创建 TCP 或 UDP 服务
Send 根据 ClientContext::switchsocket 转发到 TCP 或 UDP 发送函数

# 7. 监听服务是怎么创建的

Addserver 会根据界面配置里的协议类型创建不同服务。

// 源码位置:主控\Quick\ISocketBase.cpp:8-43
bool ISocketBase::Addserver(
    NOTIFYPROC pNotifyProc,
    CMainFrame* pFrame,
    serverstartdate* m_serverstartdate)
{
    Ssocket* S_socket = new Ssocket;
    _tcscpy_s(S_socket->m_ip, m_serverstartdate->ip.GetBuffer());
    S_socket->m_port = _ttoi(m_serverstartdate->port);

    if (m_serverstartdate->m_net.Compare(_T("TCP")) == 0)
    {
        // 防御分析:TCP 监听服务,回调仍然统一注册到主框架 NotifyProc。
        CHpTcpServer* m_iocpServer = new CHpTcpServer;
        S_socket->socketserver = m_iocpServer;
        S_socket->m_e_socket = tcp;
        S_socket->runok = m_iocpServer->Initialize(
            pNotifyProc, 99999, S_socket->m_ip, S_socket->m_port);
    }
    else
    {
        // 防御分析:UDP 监听服务,后续在 OnHandShake 创建连接上下文。
        CHpUdpServer* m_udpServer = new CHpUdpServer;
        S_socket->socketserver = m_udpServer;
        S_socket->m_e_socket = udp;
        S_socket->runok = m_udpServer->Initialize(
            pNotifyProc, 99999, S_socket->m_ip, S_socket->m_port);
    }

    // 防御分析:服务对象进入全局表,后续关闭、发送和审计都依赖这张表。
    g_servermap.insert(MAKE_PAIR(ServerMap, (int)S_socket->socketserver, S_socket));
    return S_socket->runok;
}
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

这段代码的审计重点:

检查项 为什么重要
监听 IP 源码中实际启动时使用 0.0.0.0,意味着绑定所有网卡
监听端口 来源于界面配置,企业侧应记录配置来源和启动时间
协议选择 TCP/UDP 两条链路行为不同,检测规则也不同
回调函数 所有网络事件最终进入 NotifyProc
服务表 需要确认关闭服务时能释放对象、断开连接并清理表项

# 8. TCP 服务初始化

TCP 服务使用 CTcpPullServerPtr,类声明如下:

// 源码位置:主控\Quick\HpTcpServer.h:9-40
typedef void (CALLBACK* NOTIFYPROC)(ClientContext*, UINT nCode);

class CHpTcpServer : public CTcpPullServerListener
{
public:
    CTcpPullServerPtr m_TcpServer;
    NOTIFYPROC m_pNotifyProc;
    ContextList m_listFreePool;
    CLCS m_clcs;

    BOOL Initialize(NOTIFYPROC pNotifyProc, int nMaxConnections,
        TCHAR* ip, int nPort);
    void Send(ClientContext* pContext, LPBYTE lpData, UINT nSize);
    BOOL SendWithSplit(CONNID dwConnID, LPBYTE lpData,
        UINT nSize, UINT nSplitSize);

    virtual EnHandleResult OnAccept(ITcpServer* pSender,
        CONNID dwConnID, UINT_PTR soClient);
    virtual EnHandleResult OnReceive(ITcpServer* pSender,
        CONNID dwConnID, int iLength);
    virtual EnHandleResult OnClose(ITcpServer* pSender,
        CONNID dwConnID, EnSocketOperation enOperation, int iErrorCode);

    int m_headerlength;
    BOOL B_run;
};
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

初始化函数节选如下:

// 源码位置:主控\Quick\HpTcpServer.cpp:26-52
BOOL CHpTcpServer::Initialize(
    NOTIFYPROC pNotifyProc,
    int nMaxConnections,
    TCHAR* ip,
    int nPort)
{
    int itnnum = 99;

    // 防御分析:设置最大连接数、发送策略、NoDelay 和回调同步策略。
    m_TcpServer->SetMaxConnectionCount(ONLINE_NUM * 2);
    m_maxConnection = nMaxConnections;
    m_TcpServer->SetSendPolicy(SP_DIRECT);
    m_TcpServer->SetNoDelay(TRUE);
    m_TcpServer->SetOnSendSyncPolicy(OSSP_RECEIVE);
    m_pNotifyProc = pNotifyProc;

    _tcscpy_s(m_ip, ip);
    m_port = nPort;
    m_stop = FALSE;
    m_headerlength = itnnum + 10;

    // 防御分析:实际监听绑定到 0.0.0.0,企业侧应重点记录监听端口和进程路径。
    return m_TcpServer->Start(_T("0.0.0.0"), nPort);
}
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

这段监听初始化要放在源码注释里看:最大连接数可以和实际连接峰值对比,直接发送策略和 SetNoDelay(TRUE) 会影响小包间隔,回调同步策略会影响并发模型,Start("0.0.0.0", nPort) 则是主机侧监听检测的核心信号。

# 9. TCP 接入:OnAccept

TCP 建立连接后,OnAccept 创建或复用 ClientContext

// 源码位置:主控\Quick\HpTcpServer.cpp:63-101
EnHandleResult CHpTcpServer::OnAccept(
    ITcpServer* pSender,
    CONNID dwConnID,
    UINT_PTR soClient)
{
    if (!B_run)
        return HR_ERROR;

    ClientContext* pContext = NULL;

    // 防御分析:优先从空闲池复用上下文;复用对象必须彻底清理旧状态。
    m_clcs.lock();
    if (!m_listFreePool.IsEmpty())
        pContext = m_listFreePool.RemoveHead();
    else
        pContext = new(std::nothrow) ClientContext;
    m_clcs.unlock();

    if (pContext == NULL)
        return HR_ERROR;

    // 防御分析:清零后重新绑定连接 ID、协议类型、服务对象和远端地址。
    ZeroMemory(pContext, sizeof(ClientContext));
    pContext->m_Socket = dwConnID;
    pContext->switchsocket = tcp;
    pContext->m_server = this;
    pContext->IsConnect = 666;

    int iAddressLen = sizeof(pContext->szAddress) / sizeof(TCHAR);
    pSender->GetRemoteAddress(
        dwConnID, pContext->szAddress, iAddressLen, pContext->usPort);

    // 防御分析:HPSocket 连接 ID 与业务上下文绑定,后续收发都依赖这一步。
    if (!m_TcpServer->SetConnectionExtra(dwConnID, pContext))
        return HR_ERROR;

    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
38
39

这段代码里有两个新手必须理解的点:

  1. dwConnID 是网络库的连接 ID,不是业务对象。
  2. ClientContext 是业务对象,靠 SetConnectionExtra 绑定到连接 ID。

审计时要看三件事:

审计点 说明
空闲池复用 复用对象前是否清零,旧指针、旧缓存、旧窗口是否残留
远端地址记录 是否记录了 szAddressusPort,是否进入日志
绑定失败处理 SetConnectionExtra 失败后是否释放 pContext,否则可能泄漏

# 10. TCP 收包时序

TCP 收包链路如下:

第 10 课图 2:TCP 收包时序

读图时要记住:

阶段 说明
OnAccept 连接进来,创建 ClientContext
SetExtra 连接 ID 绑定上下文
Fetch TCP Pull 模型主动把数据取出来
写缓存 字节进入 m_CompressionBuffer
NC_RECEIVE 通知主框架“有数据到了”
NC_RECEIVE_COMPLETE 拼出完整包后再通知业务分发

下面是安全化节选:省略可直接复用的高风险特殊分支,仅保留防御分析所需的通用收包逻辑。

// 源码位置:主控\Quick\HpTcpServer.cpp:111-207
EnHandleResult CHpTcpServer::OnReceive(
    ITcpServer* pSender,
    CONNID dwConnID,
    int iLength)
{
    ClientContext* pContext = NULL;

    // 防御分析:通过连接 ID 找回业务上下文。
    // 审计时要注意:如果 GetConnectionExtra 失败,后续不能继续使用空指针。
    if ((!m_TcpServer->GetConnectionExtra(dwConnID, (PVOID*)&pContext))
        && (pContext != nullptr) && (iLength <= 0))
        return HR_ERROR;

    if (pContext->IsConnect != 666)
        return HR_ERROR;

    // 防御分析:TCP Pull 模型需要 Fetch,把网络库中的数据取到本地缓冲区。
    PBYTE pData = new BYTE[iLength];
    m_TcpServer->Fetch(dwConnID, pData, iLength);
    pContext->m_CompressionBuffer.Write((PBYTE)pData, iLength);
    SAFE_DELETE_AR(pData);

    // 防御分析:先通知“收到字节”,此时还不能保证业务包完整。
    m_pNotifyProc(pContext, NC_RECEIVE);

    while ((int)pContext->m_CompressionBuffer.GetBufferLen() > m_headerlength)
    {
        if (pContext->m_password[8] != TOKEN_ACTIVED)
        {
            // 防御分析:第一次包头会写入通信标识和位数信息。
            memcpy(pContext->m_password,
                pContext->m_CompressionBuffer.GetBuffer(4), 10);
            pContext->bisx86 =
                (pContext->m_password[9] == 1) ? FALSE : TRUE;
        }

        int nSize = 0;
        CopyMemory(&nSize,
            pContext->m_CompressionBuffer.GetBuffer(0), sizeof(int));

        if ((nSize > 0)
            && ((int)pContext->m_CompressionBuffer.GetBufferLen() >= nSize))
        {
            // 防御分析:完整包到齐后,才写入解包缓存并触发完整包回调。
            pContext->m_DeCompressionBuffer.ClearBuffer();
            pContext->m_DeCompressionBuffer.Write(
                pContext->m_CompressionBuffer.GetBuffer(m_headerlength),
                nSize - m_headerlength,
                1,
                pContext->m_password);

            m_pNotifyProc(pContext, NC_RECEIVE_COMPLETE);
            pContext->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
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

# 11. UDP 服务初始化和心跳

UDP 服务类使用 CUdpArqServerPtr。它不是普通裸 UDP,而是 UDP ARQ 模型,源码里还配置了静默连接检测和心跳清理。

// 源码位置:主控\Quick\HpUdpServer.h:7-42
typedef void (CALLBACK* NOTIFYPROC)(ClientContext*, UINT nCode);

class CHpUdpServer : public CHpUdpServerListener
{
public:
    CUdpArqServerPtr m_UdpServer;
    NOTIFYPROC m_pNotifyProc;
    ContextList m_listFreePool;
    CLCS m_clcs;
    HANDLE hThreadHeartWorker;

    BOOL Initialize(NOTIFYPROC pNotifyProc, int nMaxConnections,
        TCHAR* ip, int nPort);
    static unsigned __stdcall ThreadHeartbeat(LPVOID WorkContext);

    virtual EnHandleResult OnHandShake(IUdpServer* pSender, CONNID dwConnID);
    virtual EnHandleResult OnReceive(IUdpServer* pSender, CONNID dwConnID,
        const BYTE* pData, int iLength);
    virtual EnHandleResult OnClose(IUdpServer* pSender, CONNID dwConnID,
        EnSocketOperation enOperation, int iErrorCode);
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

初始化节选:

// 源码位置:主控\Quick\HpUdpServer.cpp:32-76
BOOL CHpUdpServer::Initialize(
    NOTIFYPROC pNotifyProc,
    int nMaxConnections,
    TCHAR* ip,
    int nPort)
{
    int itnnum = 99;

    // 防御分析:UDP ARQ 的静默检测、消息大小、重传和刷新策略会形成网络画像。
    m_UdpServer->SetDetectAttempts(3);
    m_UdpServer->SetDetectInterval(30000);
    m_UdpServer->SetMaxMessageSize(4096);
    m_UdpServer->SetMaxConnectionCount(ONLINE_NUM * 2);
    m_UdpServer->SetSendPolicy(SP_DIRECT);
    m_UdpServer->SetNoDelay(TRUE);
    m_UdpServer->SetTurnoffCongestCtrl(TRUE);
    m_UdpServer->SetFlushInterval(10);
    m_UdpServer->SetResendByAcks(2);

    m_pNotifyProc = pNotifyProc;
    _tcscpy_s(m_ip, ip);
    m_port = nPort;
    m_headerlength = itnnum + 10;

    BOOL ret = m_UdpServer->Start(_T("0.0.0.0"), nPort);
    if (ret)
    {
        // 防御分析:启动心跳工作线程,用于断开静默连接。
        hThreadHeartWorker = (HANDLE)_beginthreadex(
            NULL, 0, ThreadHeartbeat, (void*)this, 0, &nThreadID);
    }

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

心跳线程节选:

// 源码位置:主控\Quick\HpUdpServer.cpp:80-92
unsigned CHpUdpServer::ThreadHeartbeat(LPVOID thisContext)
{
    CHpUdpServer* pThis = reinterpret_cast<CHpUdpServer*>(thisContext);

    while (pThis->B_run)
    {
        for (int i = 0; i < 10 && pThis->B_run; i++)
            Sleep(500);

        // 防御分析:周期性断开静默连接,检测侧可关注稳定的心跳/静默清理周期。
        pThis->m_UdpServer->DisconnectSilenceConnections(30000);
    }

    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

UDP 模型的检测重点:

字段或函数 意义
SetDetectInterval(30000) 30 秒静默检测周期
SetMaxMessageSize(4096) 单消息大小限制
SetFlushInterval(10) 很短的刷新间隔
SetResendByAcks(2) ARQ 重传行为
DisconnectSilenceConnections(30000) 静默连接清理

# 12. UDP 握手和收包时序

UDP 的连接上下文不是在 OnAccept 里创建,而是在 OnHandShake 里创建。

第 10 课图 3:UDP 握手与心跳时序

OnHandShake 节选:

// 源码位置:主控\Quick\HpUdpServer.cpp:109-148
EnHandleResult CHpUdpServer::OnHandShake(
    IUdpServer* pSender,
    CONNID dwConnID)
{
    if (!B_run)
        return HR_ERROR;

    ClientContext* pContext = NULL;

    // 防御分析:UDP 在握手阶段创建或复用连接上下文。
    m_clcs.lock();
    if (!m_listFreePool.IsEmpty())
        pContext = m_listFreePool.RemoveHead();
    else
        pContext = new(std::nothrow) ClientContext;
    m_clcs.unlock();

    if (pContext == NULL)
        return HR_ERROR;

    ZeroMemory(pContext, sizeof(ClientContext));
    pContext->m_Socket = dwConnID;
    pContext->switchsocket = udp;
    pContext->m_server = this;
    pContext->IsConnect = 666;

    int iAddressLen = sizeof(pContext->szAddress) / sizeof(TCHAR);
    pSender->GetRemoteAddress(
        dwConnID, pContext->szAddress, iAddressLen, pContext->usPort);

    if (!m_UdpServer->SetConnectionExtra(dwConnID, pContext))
        return HR_ERROR;

    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

UDP 收包和 TCP 很像,只是数据通过回调参数直接传进来,不需要 Fetch

// 源码位置:主控\Quick\HpUdpServer.cpp:158-207
EnHandleResult CHpUdpServer::OnReceive(
    IUdpServer* pSender,
    CONNID dwConnID,
    const BYTE* pData,
    int iLength)
{
    ClientContext* pContext = NULL;

    // 防御分析:通过连接 ID 找回 UDP 连接上下文。
    if ((!m_UdpServer->GetConnectionExtra(dwConnID, (PVOID*)&pContext))
        && (pContext != nullptr) && (iLength <= 0))
        return HR_ERROR;

    if (pContext->IsConnect != 666)
        return HR_ERROR;

    pContext->m_CompressionBuffer.Write((PBYTE)pData, iLength);
    m_pNotifyProc(pContext, NC_RECEIVE);

    while ((int)pContext->m_CompressionBuffer.GetBufferLen() > m_headerlength)
    {
        int nSize = 0;
        CopyMemory(&nSize,
            pContext->m_CompressionBuffer.GetBuffer(0), sizeof(int));

        if ((nSize > 0)
            && ((int)pContext->m_CompressionBuffer.GetBufferLen() >= nSize))
        {
            pContext->m_DeCompressionBuffer.ClearBuffer();
            pContext->m_DeCompressionBuffer.Write(
                pContext->m_CompressionBuffer.GetBuffer(m_headerlength),
                nSize - m_headerlength,
                1,
                pContext->m_password);

            m_pNotifyProc(pContext, NC_RECEIVE_COMPLETE);
            pContext->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
38
39
40
41
42
43
44
45
46
47

TCP 和 UDP 的差异可以这样记:

对照项 TCP UDP
服务对象 CTcpPullServerPtr CUdpArqServerPtr
上下文创建 OnAccept OnHandShake
收包数据来源 Fetch(dwConnID, pData, iLength) 回调参数 pData
分片大小 MAX_SEND_BUFFER 4000
静默检测 依赖关闭事件 额外心跳线程
检测重点 长连接、交互小包、固定包头 周期心跳、ARQ 重传、UDP 长会话

# 13. 发送路径:ISocketBase::Send

上层功能窗口调用发送时,通常只拿到 ClientContext。至于这条连接走 TCP 还是 UDP,由 ISocketBase 判断。

// 源码位置:主控\Quick\ISocketBase.cpp:54-67
void ISocketBase::Send(ClientContext* pContext, LPBYTE lpData, UINT nSize)
{
    if (!pContext)
        return;

    switch (pContext->switchsocket)
    {
    case tcp:
        // 防御分析:TCP 连接由 CHpTcpServer 负责打包和分片发送。
        ((CHpTcpServer*)pContext->m_server)->Send(pContext, lpData, nSize);
        break;

    case udp:
        // 防御分析:UDP 连接由 CHpUdpServer 负责打包和分片发送。
        ((CHpUdpServer*)pContext->m_server)->Send(pContext, lpData, nSize);
        break;

    default:
        break;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

这段代码本身不复杂,但安全意义很大:它是很多功能窗口进入网络层的共同边界。合法软件改造时,应优先在这里增加:

改造点 说明
命令白名单 哪些功能允许发送,哪些功能必须禁用
连接权限 当前用户是否允许操作该连接
数据大小限制 防止异常大包、批量发送、误传敏感数据
审计日志 记录时间、连接、功能、大小、结果
速率限制 防止高频命令或异常自动化行为

# 14. TCP/UDP 发送包结构和分片

发送流程如下:

第 10 课图 5:发送和分片流程

TCP 发送节选:

// 源码位置:主控\Quick\HpTcpServer.cpp:256-273
void CHpTcpServer::Send(ClientContext* pContext, LPBYTE lpData, UINT nSize)
{
    if (pContext == NULL)
        return;

    if (pContext->IsConnect != 666)
        return;

    if (nSize > 0 && B_run)
    {
        pContext->m_allpack_send++;
        pContext->m_alldata_send += nSize;

        LONG nBufLen = nSize + m_headerlength;

        // 防御分析:网络包头包含总长度和固定认证/标识字段。
        pContext->m_WriteBuffer.Write((PBYTE)&nBufLen, 4);
        pContext->m_WriteBuffer.Write((PBYTE)pContext->m_password, 10);

        // 防御分析:业务数据进入加密/变换写入流程。
        pContext->m_WriteBuffer.Write(lpData, nSize, 1, pContext->m_password);

        SendWithSplit(
            pContext->m_Socket,
            pContext->m_WriteBuffer.GetBuffer(),
            pContext->m_WriteBuffer.GetBufferLen(),
            MAX_SEND_BUFFER);

        pContext->m_WriteBuffer.ClearBuffer();
    }
}
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

UDP 发送结构类似,但分片大小是 4000

// 源码位置:主控\Quick\HpUdpServer.cpp:249-266
void CHpUdpServer::Send(ClientContext* pContext, LPBYTE lpData, UINT nSize)
{
    if (pContext == NULL)
        return;

    if (pContext->IsConnect != 666)
        return;

    if (nSize > 0 && B_run)
    {
        LONG nBufLen = nSize + m_headerlength;
        pContext->m_WriteBuffer.Write((PBYTE)&nBufLen, 4);
        pContext->m_WriteBuffer.Write((PBYTE)pContext->m_password, 10);
        pContext->m_WriteBuffer.Write(lpData, nSize, 1, pContext->m_password);

        // 防御分析:UDP 分片固定使用 4000,容易形成流量侧特征。
        SendWithSplit(
            pContext->m_Socket,
            pContext->m_WriteBuffer.GetBuffer(),
            pContext->m_WriteBuffer.GetBufferLen(),
            4000);

        pContext->m_WriteBuffer.ClearBuffer();
    }
}
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

分片发送节选:

// 源码位置:主控\Quick\HpTcpServer.cpp:276-309
BOOL CHpTcpServer::SendWithSplit(
    CONNID dwConnID,
    LPBYTE lpData,
    UINT nSize,
    UINT nSplitSize)
{
    int nSend = 0;

    if (nSize >= nSplitSize)
    {
        UINT nSendRetry = nSize / nSplitSize;
        for (UINT i = 0; i < nSendRetry; i++)
        {
            // 防御分析:大包会被拆成固定大小片段,流量侧可观察连续分片。
            BOOL rt = m_TcpServer->Send(dwConnID, lpData, nSplitSize);
            if (rt == FALSE)
                return FALSE;

            lpData += nSplitSize;
            nSend += nSplitSize;
        }

        if (nSize - nSend > 0)
            m_TcpServer->Send(dwConnID, lpData, nSize - nSend);
    }
    else
    {
        m_TcpServer->Send(dwConnID, lpData, nSize);
    }

    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

发送侧的防御分析:

行为 检测意义
固定包头 可用于协议指纹,但不能只依赖单点特征
包长字段 可用于识别异常大包或长度异常
固定分片大小 TCP 约 64KB 分片,UDP 约 4000 字节分片
发送统计字段 可用于主控端自身审计或改造日志
清空发送缓存 失败路径是否也清理,是稳定性审计点

# 15. 断开连接和空闲池复用

网络断开后,源码会解绑连接 ID,通知主框架,再把上下文放回空闲池。

// 源码位置:主控\Quick\HpTcpServer.cpp:215-226
EnHandleResult CHpTcpServer::OnClose(
    ITcpServer* pSender,
    CONNID dwConnID,
    EnSocketOperation enOperation,
    int iErrorCode)
{
    ClientContext* pContext = NULL;

    // 防御分析:断开时先解除连接 ID 与上下文的绑定。
    if (m_TcpServer->GetConnectionExtra(dwConnID, (PVOID*)&pContext)
        && pContext != nullptr)
        m_TcpServer->SetConnectionExtra(dwConnID, NULL);

    if (!pContext)
        return HR_OK;

    pContext->IsConnect = 888;
    m_pNotifyProc(pContext, NC_CLIENT_DISCONNECT);
    MovetoFreePool(pContext);
    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

空闲池复用节选:

// 源码位置:主控\Quick\HpTcpServer.cpp:228-246
void CHpTcpServer::MovetoFreePool(ClientContext* pContext)
{
    if (pContext->PictureSize != 0)
    {
        pContext->PictureSize = 0;
        SAFE_DELETE_AR(pContext->ScreenPicture);
    }

    // 防御分析:清理 UI 指针、状态和缓冲区,避免下一条连接继承旧状态。
    pContext->pView = NULL;
    pContext->m_bIsMainSocket = FALSE;
    pContext->m_CompressionBuffer.FreeBuffer();
    pContext->m_WriteBuffer.FreeBuffer();
    pContext->m_DeCompressionBuffer.FreeBuffer();

    m_clcs.lock();
    m_listFreePool.AddTail(pContext);
    m_clcs.unlock();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

审计重点:

检查项 说明
是否解绑连接 ID SetConnectionExtra(dwConnID, NULL)
是否改状态 IsConnect = 888 表示不再可用
是否通知主框架 NC_CLIENT_DISCONNECT
是否释放缓存 三个 CBuffer 都要清理
是否释放图片 ScreenPicture 要释放
是否清理 UI 指针 否则可能出现悬空窗口指针
空闲池是否加锁 多线程复用必须加锁

# 16. 网络层的关键风险点

网络层本身不一定执行业务,但它决定了攻击面和检测面。对企业和安全学习者来说,本课最重要的是把风险点标出来。

风险点 源码表现 防御建议
开放监听 Start("0.0.0.0", nPort) 记录进程、端口、协议、启动时间
长连接 ClientContext 长期绑定连接 ID 监控长连接、空闲时长和连接频率
固定包头 4 字节长度 + 10 字节标识 结合流量统计,不单独依赖明文特征
弱认证字段 m_password 固定偏移 合法改造应使用强认证和完整性校验
大包分片 MAX_SEND_BUFFER4000 检测连续分片和异常大数据传输
空闲池复用 m_listFreePool 防止旧状态、旧指针、旧缓存残留
高风险分支 NC_SEND_SHELCODE_* 只做阻断和审计,不在生产环境启用
网络线程触碰 UI 通过 NotifyProc 间接到窗口 应投递到 UI 线程,避免崩溃和竞态

# 17. 企业检测模型

网络通信不能只看一个点。更可靠的方法是把主机、网络、内存和日志事件串起来。

第 10 课图 6:网络通信检测关联

# 17.1 主机侧检测点

检测点 说明
进程监听端口 是否出现未知进程监听 TCP/UDP
绑定地址 是否绑定 0.0.0.0 或所有网卡
父进程 是否由异常路径或脚本启动
模块加载 是否加载 HPSocket、插件 DLL 或异常网络库
线程行为 是否存在周期性心跳线程或高频网络线程

# 17.2 网络侧检测点

检测点 说明
长连接 长时间保持、低频交互、周期性数据
固定包头 包长字段、固定认证字段、稳定偏移
分片行为 TCP 大块分片、UDP 4000 字节附近分片
心跳周期 约 30 秒静默检测或周期事件
协议混用 同进程同时提供 TCP/UDP 服务

# 17.3 内存侧检测点

检测点 说明
连接上下文 类似 ClientContext 的长期状态对象
收发缓存 大量 CBuffer、压缩/解压缓存
高风险回调码 与 shellcode、动态加载、插件传输相关的事件
可执行内存 如果后续出现动态执行,应提高等级

# 17.4 构建链检测点

检测点 说明
HPSocket 依赖 是否引入网络通信库
Debug/Release 差异 包头长度、保护宏、日志输出可能不同
插件编译产物 是否存在大量功能 DLL
自动化构建脚本 是否打包网络端口、协议和插件配置

# 18. 容易误报的来源

企业检测不能把“使用 HPSocket”直接判定为恶意。很多合法软件也会使用网络库和长连接。

误报来源 区分方法
合法 IM 软件 有明确签名、固定厂商、公开协议或可解释域名
运维代理 有资产登记、访问控制、操作审计
游戏或实时应用 流量频率高但业务场景明确
远程协助工具 有用户确认、会话日志、厂商签名
内网监控系统 有固定部署清单和管理端资产

更可靠的判断方式是事件关联:

未知进程监听 + 长连接 + 固定包头 + 插件 DLL 传输 + 高风险内存行为

多个信号同时出现,可信度才会显著提高。

# 19. 合法软件改造建议

如果把这类网络框架改造成合法远程管理或教学实验工具,至少要做这些改造:

改造项 建议
身份认证 使用标准认证机制,不依赖固定偏移字段
传输加密 使用标准 TLS,避免自定义弱加密
命令授权 所有命令必须做权限校验
审计日志 记录操作者、连接、命令、数据大小、结果
插件可信链 插件必须签名、校验哈希和版本
网络暴露 默认只监听本机或受控网段
速率限制 限制高频命令和大包传输
异常处理 GetConnectionExtra 失败后立即退出
UI 线程隔离 网络线程只投递消息,不直接操作窗口
安全默认值 高危功能默认关闭,需要显式授权

# 20. 新手常见误区

误区 正确认识
OnReceive 一次就是一个完整命令 不一定,必须靠长度字段和缓存判断完整包
TCP 比 UDP 一定安全 TCP 只是可靠传输,不等于安全
UDP 没有连接 UDP ARQ 可以在库层维护类似连接 ID 的上下文
有密码字段就安全 固定偏移字段不是强认证
分片只是性能问题 分片大小和频率也是网络检测特征
网络层不懂业务就没风险 网络层决定暴露面、回调面和数据入口

# 21. 本课小结

本课从源码角度拆解了主控端网络通信模型:

  1. ISocketBase 是统一入口,负责创建 TCP/UDP 服务和统一发送。
  2. CHpTcpServerOnAccept 创建连接上下文,收包时用 Fetch 取数据。
  3. CHpUdpServerOnHandShake 创建连接上下文,并有静默连接检测线程。
  4. ClientContext 是连接状态核心,保存连接 ID、缓存、协议类型、认证字段和业务绑定。
  5. 收包逻辑先进入 m_CompressionBuffer,完整包到齐后才写入 m_DeCompressionBuffer
  6. 发送逻辑会写入长度字段、认证字段和业务数据,再按固定大小分片。
  7. 企业检测要把监听、长连接、包头、心跳、分片、插件和内存行为关联起来。

下一课会继续往上走,讲“协议与命令分发机制”:网络层拼出完整业务包后,主控端如何根据 TOKEN_* 判断这是上线、文件、屏幕、键盘、注册表还是插件相关数据。

# 22. 合法练习题

  1. 在不运行样本的前提下,画出 ISocketBase::Addserver -> CHpTcpServer::Initialize -> OnAccept 的调用链。
  2. 对比 CHpTcpServer::OnReceiveCHpUdpServer::OnReceive,列出二者相同和不同的地方。
  3. ClientContext 中挑出 5 个字段,说明它们分别属于连接、缓存、状态、统计还是 UI 绑定。
  4. 写一份防御侧审计记录模板,记录监听端口、协议、连接数、收发字节数和断开原因。
  5. 设计一条企业检测思路:如何把“未知进程监听端口”和“固定周期 UDP 心跳”关联起来。
  6. 检查 GetConnectionExtra 失败后的处理逻辑,说明为什么空指针检查很重要。
  7. 思考合法远程管理软件应如何替换 m_password 这种固定偏移字段。
  8. 用表格整理 TCP 和 UDP 两条链路的回调函数,不需要运行程序。

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

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

社区交流

讨论与留言

前往 GitHub Issues →

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

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