第 47 课:加密、认证与协议安全缺陷
# 第 47 课:加密、认证与协议安全缺陷
免责声明:本专栏仅用于安全工程学习研究,禁止使用本专栏介绍的技术做其他用途,否则后果自负,与本号无关。
本课从协议安全角度分析这套源码的通信设计。前面第 90 课已经讲过网络通信和命令分发,本课进一步追问:连接双方如何确认身份?数据是否有完整性保护?命令能否被伪造?插件是否可信?如果要做合法远程管理软件,应该如何改造协议边界?

# 1. 新手先理解:协议安全看什。
协议不只是“能收发数据”。企业级远程管理协议至少要回答这些问题:
| 问题 | 安全要求 |
|---|---|
| 谁在连接 | 双向身份认证,而不是只看一个短字段 |
| 数据有没有被 | 完整性校验和防篡 |
| 命令是谁发的 | 命令签名、权限控制、审批或授权上下 |
| 插件是否可信 | 插件签名、哈希、版本和来源校验 |
| 密钥如何轮换 | 会话密钥、过期时间、吊销机制 |
| 操作如何留痕 | 记录操作者、目标、命令、结果和数据 |
# 2. 源码目录和文件
| 位置 | 作用 | 阅读重点 |
|---|---|---|
主插件\HPSocket\TcpSocket.cpp | 被控TCP 通信 | 包头、通信标识、心跳、异或处理 |
主插件\HPSocket\Buffer.cpp | 缓冲区写入和异或处理 | 弱加密、长度处理 |
主控\Quick\HpTcpServer.cpp | 主控TCP 服务 | 接收包、保存标识、完整包分发 |
主控\Quick\MainFrm.cpp | 主控业务分发 | 完整包进入不同功能窗口 |
主插件\登录模块\登录模块\LoginManager.cpp | 登录模块命令处理 | 插件接收、注册表缓存、命令执行 |
主控\Quick\macros.h | token 和结构体定义 | 命令枚举、事件类型、上下文缓冲区 |
依赖说明: 本课重点是项目自带的 HPSocket 通信封装和缓冲区处理,不依赖现代密码库。源码中使用短通信标识和异或处理数据,这只能提供包边界和弱混淆,不能提供认证、完整性、防重放或保密性。后续代码注释会把标识字段、长度字段、异或处理和命令分发边界放在同一条协议链里说明。
# 3. 关键函数和字段
| 函数或字段 | 大致行号 | 角色 | 风险点 |
|---|---|---|---|
m_password[10] | ISocketBase.h 67 | 通信标识 | 过短,且不等于强认证 |
CTcpSocket::Connect | TcpSocket.cpp 55-70 | 生成通信标识 | 由本地时间等字段构造 |
CTcpSocket::Send | TcpSocket.cpp 200-208 | 写长度、标识、数据 | 缺少强完整性保护 |
CBuffer::Write | Buffer.cpp 36-50 | 异或处理数据 | 只能算混淆,不能当加密 |
CHpTcpServer::OnReceive | HpTcpServer.cpp 169-201 | 解包并分发 | 认证和分发边界较弱 |
ProcessReceiveComplete | MainFrm.cpp 1785-1865 | token 转给功能窗口 | 命令进入高危功能 |
COMMAND_SENDLL | LoginManager.cpp 314-366 | 接收插件数据 | 插件可信链不 |
# 4. 现有包结。

源码中的 TCP 包大体是 字节长度0 字节通信标识、经过异或处理的数据体。这个结构能帮助程序区分包边界,但不能满足现代远程管理协议所需的身份认证、完整性保护和重放防护。
// 源码位置:主插件\HPSocket\TcpSocket.cpp,约55-70 bool CTcpSocket::Connect(LPCTSTR lpszHost, UINT nPort)
{
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_password 更像会话标识,不是强密码 // 它没有证书、挑战应答、签名或服务端身份校验}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 源码位置:主插件\HPSocket\TcpSocket.cpp,约200-208 int CTcpSocket::Send(LPBYTE lpData, UINT nSize)
{
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);
// 防御分析:包头没有消息认证码,也没有命令签名 // 防护设计上不能把这种结构当作可信远程管理协议}
2
3
4
5
6
7
8
9
# 5. 弱加密问。
CBuffer::Write 中的数据处理是异或。异或可以隐藏明文特征,但不提供足够的机密性、完整性和身份保障。
// 源码位置:主插件\HPSocket\Buffer.cpp,约36-50 BOOL CBuffer::Write(PBYTE pData, UINT nSize, BOOL bXORrecoder, byte* password)
{
ReAllocateBuffer(nSize + GetBufferLen());
CopyMemory(m_pPtr, pData, nSize);
if (bXORrecoder)
{
for (int i = 0, j = 0; i < (int)nSize; i++)
{
((char*)m_pPtr)[i] ^= (password[j++]) % 456 + 54;
if (i % (10) == 0)
j = 0;
}
}
// 防御分析:这是混淆,不是企业级加密 // 合法远程管理应使TLS AEAD,并校验消息完整性}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
弱加密常见问题:
| 问题 | 风险 |
|---|---|
| 密钥空间过小 | 容易被还原或碰撞 |
| 无完整性校 | 数据被篡改后不一定能被发 |
| 无重放保 | 旧包可能被重复发 |
| 无身份绑定 | 不能证明发送方是谁 |
# 6. 主控端如何接收和分发

主控端收到数据后,会在第一次连接时保存 10 字节标识,之后按长度取完整包,解出数据体,再触发 NC_RECEIVE_COMPLETE。
// 源码位置:主控\Quick\HpTcpServer.cpp,约169-201 while ((int)pContext->m_CompressionBuffer.GetBufferLen() > m_headerlength)
{
if (pContext->m_password[8] != TOKEN_ACTIVED)
{
for (size_t i = 0; i < 9; i++)
{
if (pContext->m_password[i] != 0)
return HR_ERROR;
}
memcpy(pContext->m_password, pContext->m_CompressionBuffer.GetBuffer(4), 10);
(pContext->m_password[9] == 1) ? (pContext->bisx86 = FALSE) : (pContext->bisx86 = TRUE);
}
int nSize = 0;
CopyMemory(&nSize, pContext->m_CompressionBuffer.GetBuffer(0), sizeof(int));
pContext->m_DeCompressionBuffer.Write(
pContext->m_CompressionBuffer.GetBuffer(m_headerlength),
nSize - m_headerlength,
1,
pContext->m_password);
m_pNotifyProc(pContext, NC_RECEIVE_COMPLETE);
// 防御分析:接收端接受包头里的标识并进入业务分发 // 合规设计应使用双向证书、会话密钥和消息认证码}
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
完整包进入 MainFrm 后,会按对话框类型和 token 转给具体功能窗口。
// 源码位置:主控\Quick\MainFrm.cpp,约1785-1865 void CMainFrame::ProcessReceiveComplete(ClientContext* pContext)
{
if (pContext->m_DeCompressionBuffer.GetBuffer(0)[0] == TOKEN_HEARTBEAT)
{
BYTE bToken[2] = { TOKEN_HEARTBEAT, 1 };
g_pSocketBase->Send(pContext, (LPBYTE)&bToken, 2);
return;
}
if (pContext->m_Dialog[0] > 0)
{
switch (pContext->m_Dialog[0])
{
case FILEMANAGER_DLG:
((CFileManagerDlg*)dlg)->OnReceiveComplete();
break;
case PROXY_DLG:
((CProxyMapDlg*)dlg)->OnReceiveComplete();
break;
case KERNEL_DLG:
((CKernelDlg*)dlg)->OnReceiveComplete();
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
# 7. 插件可信链不。
插件数据接收是另一个关键边界。源码会接收 DllSendData 和插件二进制数据,并写入注册表缓存。合法远程管理软件应要求插件签名、版本、哈希和发布来源校验。
// 源码位置:主插件\登录模块\登录模块\LoginManager.cpp,约314-366 case COMMAND_SENDLL:
{
DllSendData* temp = new DllSendData;
ZeroMemory(temp, sizeof(DllSendData));
::memcpy(temp, lpBuffer + 1, sizeof(DllSendData));
BYTE* pPluginDllShellcodeData = new BYTE[temp->dllDataSize];
::memcpy(
pPluginDllShellcodeData,
lpBuffer + 1 + sizeof(DllSendData),
temp->dllDataSize);
int writeregeditsize = sizeof(DllSendData) + temp->dllDataSize;
char* writeregeditbuffer = new char[writeregeditsize];
::RegSetValueEx(
hKey,
DllName,
0,
REG_BINARY,
(unsigned char*)writeregeditbuffer,
writeregeditsize);
// 防御分析:这里没有看到插件签名校验、哈希白名单或发布链验证 // 合法产品应在加载前验证插件来源和完整性}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 8. 字段级拆解:从网络包到高危功。
把前面的代码连起来,可以得到一条更具体的协议链。新手读到这里,不要只记“弱加密”三个字,要把每个字段和后续行为对应起来
| 阶段 | 字段或对象 | 源码位置 | 防御含义 |
|---|---|---|---|
| 连接建立 | m_password[0..7] | TcpSocket::Connect | 由时间值写入,不能证明身份 |
| 连接激 | m_password[8] = TOKEN_ACTIVED | TcpSocket::Connect | 只是激活标记,不是认证结果 |
| 架构标识 | m_password[9] | TcpSocket::Connect / HpTcpServer::OnReceive | 用于判断 x86/x64,会影响后续插件路径 |
| 包边 | nBufLen = nSize + 14 | CTcpSocket::Send | 4 字节长度用于拆包 |
| 数据处理 | bXORrecoder = 1 | CBuffer::Write | 数据体做异或混淆,没有强完整 |
| 业务入口 | TOKEN_* | ProcessReceiveComplete | 第一个字节决定进入哪个功能链 |
| 插件缓存 | REG_BINARY | LoginManager::OnReceive | 插件二进制可能进入注册表缓存 |
具体到检测工程,可以按下面方式把字段转换为日志关联:
| 关联字段 | 采集建议 | 说明 |
|---|---|---|
src_process | 进程名、路径、签名、哈 | 确认是谁建立长连 |
dst_ip:dst_port | 网络日志、EDR 网络事件 | 识别主控地址或异常内网连 |
packet_size | NDR 或代理日 | 固定小包、插件大包、屏幕流包长差异明显 |
token_family | 协议解析或内存侧辅助 | 不要求还原内容,也可按功能行为归 |
registry_value_size | 注册表事 | 大块 REG_BINARY 常见于插件缓 |
next_behavior | 线程、模块、文件、API 事件 | 看网络包之后触发了什 |
# 9. 具体关联规则模型
下面给出三条防御规则模型,表达的是字段和时间窗口,不绑定某个安全产品语法
| 规则 | 关联条件 | 优先 | 误报来源 |
|---|---|---|---|
| 插件下发后缓存 | 同一进程建立长连接后 分钟内写入大块 REG_BINARY,随后创建线程或加载模块 | 正规软件更新器、企业代理升 | |
| 弱协议长连接 | 未知进程长期连接非业务地址,存在稳定小包心跳,随后出现功能API | 中高 | 远程协助、监控代 |
| 高危 token 行为 | 长连接后出现注册表变更、屏幕采集、音频采集、远程线程之一 | 运维工具,但应有签名、工单和可见提示 |
告警内容建议写具体,不要只写“发现可疑连接”:
告警名称:疑似弱认证远程插件链路
主体进程:记录进程路径、签名、哈网络证据:目的地址、端口、连接持续时间、心跳周后续行为:注册表 REG_BINARY 写入、线程创建、插件加载或敏感 API
处置建议:隔离主机、保全注册表值、导出进程树、检查插件缓存和账号风险
2
3
# 10. 合规远程管理模型

建议的协议改造方向:
| 能力 | 合规设计 |
|---|---|
| 连接身份 | mTLS,服务端和客户端都要验证证书 |
| 设备绑定 | 证书绑定资产 ID,支持吊销 |
| 命令完整 | 每条命令有签名或消息认证 |
| 权限控制 | RBAC,按用户、资产、功能和时间限制 |
| 插件可信 | 插件签名、哈希、版本、来源校 |
| 操作留痕 | 记录操作者、目标、命令、结果、数据量 |
| 密钥轮换 | 会话密钥短周期更新,长期密钥可吊销 |
# 11. 协议加固要点

| 检查项 | 当前风险 | 加固方向 |
|---|---|---|
| 身份认证 | 10 字节标识不足以证明身 | mTLS、证书固定、设备证书 |
| 数据机密 | 异或不等于加 | TLS 1.3 AEAD |
| 数据完整 | 没有MAC | HMAC AEAD tag |
| 防重 | 包中缺少 nonce/序列 | 序列号、时间窗、重放缓 |
| 命令授权 | token 直接进入功能 | RBAC、命令审批、功能开 |
| 插件可信 | 插件二进制直接接收 | 签名校验、哈希白名单 |
| 配置保护 | 敏感配置写入注册 | 加密存储、最小化保存 |
# 12. 企业检测点
| 层面 | 检测点 | 建议 |
|---|---|---|
| 网络 | 固定头部长度、周期心跳、长连接 | 提取连接频率、包长分 |
| 主机 | 插件写入注册表、动态加 | 关联 COMMAND_SENDLL 前后事件 |
| 协议 | token 驱动高危功能 | 用协议字段映射功能类 |
| 文件 | 插件二进制落点或注册表缓 | 记录哈希和首次发现时 |
| 账号 | 命令缺少操作者身 | 合规产品必须补齐用户维度 |
# 13. 合法练习:
根据源码画出现有包结构:长度0 字节标识、数据体、token?
说明为什么异或处理不能当作企业级加密?
设计一个合法远程管理协议的最小安全模型:mTLS、RBAC、命令签名、插件签名、操作留痕?
写一条检测思路:注册表出现插件缓存后,同一进程短时间内启动新功能线程。
安全工程知识交流加wx:easy_coder,黑灰产勿扰,企业单位合作请出示有效证件。
本留言区仅对应当前文章,欢迎补充观点、提出问题或帮助修正文中疏漏。 留言由 GitHub/Gitalk 提供,需要使用 GitHub 登录。
社区交流
讨论与留言