第 10 课:网络通信模型
# 第 10 课:网络通信模型
免责声明:本专栏仅用于安全工程学习研究,禁止使用本专栏介绍的技术做其他用途,否则后果自负,与本号无关。
第 8 课讲了主控端架构:界面、连接列表、功能窗口和插件调度如何串起来。本课继续往下一层看,重点讲网络通信模型。
新手读这类源码时容易犯一个错误:一看到 Send、OnReceive、TCP、UDP,就以为网络层已经知道“文件管理”“屏幕监控”“终端命令”这些业务含义。实际上不是这样。网络层只负责几件事:
- 监听端口。
- 接受连接或握手。
- 给连接绑定
ClientContext。 - 收到字节后写入缓存。
- 判断是否拼出完整业务包。
- 通过回调把事件交给主框架。
- 发送时把业务数据包装成网络包。
- 断开时清理上下文。
真正解释业务含义的是后面的 TOKEN_*、功能窗口和插件调度。第 11 课会继续讲协议和命令分发,本课先把网络层看清楚。
# 1. 本课学习目标
学完本课,你应该能回答这些问题:
| 问题 | 本课回答 |
|---|---|
| HPSocket 在工程里做什么 | 它负责底层 TCP/UDP 监听、收发事件和连接 ID 管理 |
ISocketBase 的作用是什么 | 它把 TCP 和 UDP 封装成统一入口 |
ClientContext 为什么重要 | 它是一条连接的状态档案,保存连接 ID、缓存、密码、窗口和统计信息 |
| TCP 与 UDP 的主要差异在哪里 | TCP 在 OnAccept 创建上下文,UDP 在 OnHandShake 创建上下文 |
| 收包为什么要缓存 | 网络到达的是字节片段,必须判断完整包后才能分发 |
| 企业如何检测这类通信 | 结合进程监听、长连接、固定包头、心跳周期、异常分片和回调行为 |
# 2. 本课涉及源码目录和文件
| 源码文件 | 本课关注点 |
|---|---|
主控\Quick\ISocketBase.h | 统一网络入口、Ssocket、ServerMap、NOTIFYPROC |
主控\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 | ClientContext、e_socket、NC_* 回调码、缓冲区大小 |
主控\Quick\MainFrm.cpp | NotifyProc 接收网络事件并转给主框架处理 |
本课不讲如何搭建外联链路,也不讲如何投递或运行远控组件。我们只从安全工程角度分析源码结构、危险信号、检测点和审计方法。
# 3. 网络层在整体工程中的位置
先看这一层的结构图:

这张图要按从上到下理解:
| 层级 | 代表对象 | 说明 |
|---|---|---|
| 界面配置 | SettingDlg、BuildDlg | 用户在界面里配置监听协议和端口 |
| 统一入口 | ISocketBase | 屏蔽 TCP/UDP 差异,给主控端提供统一 Send / Disconnect |
| TCP 服务 | CHpTcpServer | 使用 HPSocket 的 TCP Pull Server 模型 |
| UDP 服务 | CHpUdpServer | 使用 HPSocket 的 UDP ARQ Server 模型 |
| 主框架回调 | CMainFrame::NotifyProc | 接收 NC_RECEIVE、NC_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 回调 | 网络事件发生后调用上层函数,如 OnAccept、OnReceive、OnClose |
# 4.2 IOCP 是什么
IOCP 是 Windows 的高性能异步 I/O 模型。它适合处理大量连接,因为网络事件不会简单阻塞在一个线程里。
本工程里文件名和变量名仍然保留了一些 iocp 字样,但本课关注的是 HPSocket 封装后的事件模型。你可以先理解成:
底层网络事件 -> HPSocket 回调 -> ClientContext -> NotifyProc -> 主控业务分发
# 4.3 字节流和业务包不是一回事
TCP 收到的是字节流,UDP 收到的是数据报,二者都不等于“业务命令”。源码需要自己定义业务包结构,再从缓存中判断一个包是否收齐。
本工程的简化包结构如下:

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
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;
};
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_Socket、switchsocket、m_server | 标识连接 ID、协议类型和所属服务对象 |
| 缓冲区 | m_WriteBuffer、m_CompressionBuffer、m_DeCompressionBuffer | 发送、接收和解包都围绕它们完成 |
| 统计信息 | m_allpack_rev、m_alldata_send | 可用于流量画像和异常计数 |
| 状态与同步 | IsConnect、m_clcs_send_rec_close | 判断连接是否有效,保护收发关闭过程 |
| 业务绑定 | m_Dialog、LoginInfo、pView | 后续把连接绑定到 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);
};
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;
}
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;
};
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);
}
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;
}
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
这段代码里有两个新手必须理解的点:
dwConnID是网络库的连接 ID,不是业务对象。ClientContext是业务对象,靠SetConnectionExtra绑定到连接 ID。
审计时要看三件事:
| 审计点 | 说明 |
|---|---|
| 空闲池复用 | 复用对象前是否清零,旧指针、旧缓存、旧窗口是否残留 |
| 远端地址记录 | 是否记录了 szAddress 和 usPort,是否进入日志 |
| 绑定失败处理 | SetConnectionExtra 失败后是否释放 pContext,否则可能泄漏 |
# 10. TCP 收包时序
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;
}
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);
};
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;
}
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;
}
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 里创建。

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;
}
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;
}
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;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
这段代码本身不复杂,但安全意义很大:它是很多功能窗口进入网络层的共同边界。合法软件改造时,应优先在这里增加:
| 改造点 | 说明 |
|---|---|
| 命令白名单 | 哪些功能允许发送,哪些功能必须禁用 |
| 连接权限 | 当前用户是否允许操作该连接 |
| 数据大小限制 | 防止异常大包、批量发送、误传敏感数据 |
| 审计日志 | 记录时间、连接、功能、大小、结果 |
| 速率限制 | 防止高频命令或异常自动化行为 |
# 14. TCP/UDP 发送包结构和分片
发送流程如下:

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();
}
}
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();
}
}
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;
}
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;
}
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();
}
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_BUFFER、4000 | 检测连续分片和异常大数据传输 |
| 空闲池复用 | m_listFreePool | 防止旧状态、旧指针、旧缓存残留 |
| 高风险分支 | NC_SEND_SHELCODE_* | 只做阻断和审计,不在生产环境启用 |
| 网络线程触碰 UI | 通过 NotifyProc 间接到窗口 | 应投递到 UI 线程,避免崩溃和竞态 |
# 17. 企业检测模型
网络通信不能只看一个点。更可靠的方法是把主机、网络、内存和日志事件串起来。

# 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. 本课小结
本课从源码角度拆解了主控端网络通信模型:
ISocketBase是统一入口,负责创建 TCP/UDP 服务和统一发送。CHpTcpServer在OnAccept创建连接上下文,收包时用Fetch取数据。CHpUdpServer在OnHandShake创建连接上下文,并有静默连接检测线程。ClientContext是连接状态核心,保存连接 ID、缓存、协议类型、认证字段和业务绑定。- 收包逻辑先进入
m_CompressionBuffer,完整包到齐后才写入m_DeCompressionBuffer。 - 发送逻辑会写入长度字段、认证字段和业务数据,再按固定大小分片。
- 企业检测要把监听、长连接、包头、心跳、分片、插件和内存行为关联起来。
下一课会继续往上走,讲“协议与命令分发机制”:网络层拼出完整业务包后,主控端如何根据 TOKEN_* 判断这是上线、文件、屏幕、键盘、注册表还是插件相关数据。
# 22. 合法练习题
- 在不运行样本的前提下,画出
ISocketBase::Addserver -> CHpTcpServer::Initialize -> OnAccept的调用链。 - 对比
CHpTcpServer::OnReceive和CHpUdpServer::OnReceive,列出二者相同和不同的地方。 - 从
ClientContext中挑出 5 个字段,说明它们分别属于连接、缓存、状态、统计还是 UI 绑定。 - 写一份防御侧审计记录模板,记录监听端口、协议、连接数、收发字节数和断开原因。
- 设计一条企业检测思路:如何把“未知进程监听端口”和“固定周期 UDP 心跳”关联起来。
- 检查
GetConnectionExtra失败后的处理逻辑,说明为什么空指针检查很重要。 - 思考合法远程管理软件应如何替换
m_password这种固定偏移字段。 - 用表格整理 TCP 和 UDP 两条链路的回调函数,不需要运行程序。
安全工程知识交流加wx:easy_coder,黑灰产勿扰,企业单位合作请出示有效证件。
本留言区仅对应当前文章,欢迎补充观点、提出问题或帮助修正文中疏漏。 留言由 GitHub/Gitalk 提供,需要使用 GitHub 登录。
社区交流
讨论与留言