47 / 52 安全检测、响应与重构

第 47 课:加密、认证与协议安全缺陷

# 第 47 课:加密、认证与协议安全缺陷

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

本课从协议安全角度分析这套源码的通信设计。前面第 90 课已经讲过网络通信和命令分发,本课进一步追问:连接双方如何确认身份?数据是否有完整性保护?命令能否被伪造?插件是否可信?如果要做合法远程管理软件,应该如何改造协议边界?

第 47 课:加密、认证与协议安全缺陷(配图)

# 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. 现有包结。

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 更像会话标识,不是强密码    // 它没有证书、挑战应答、签名或服务端身份校验}
1
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);

    // 防御分析:包头没有消息认证码,也没有命令签名    // 防护设计上不能把这种结构当作可信远程管理协议}
1
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,并校验消息完整性}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

弱加密常见问题:

问题 风险
密钥空间过小 容易被还原或碰撞
无完整性校 数据被篡改后不一定能被发
无重放保 旧包可能被重复发
无身份绑定 不能证明发送方是谁

# 6. 主控端如何接收和分发

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

    // 防御分析:接收端接受包头里的标识并进入业务分发    // 合规设计应使用双向证书、会话密钥和消息认证码}
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

完整包进入 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;
        }
    }

    // 防御分析:完整包直接进入功能窗口处理    // 权限控制应在业务分发前完成,而不是交给各窗口自行假设可信}
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

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

    // 防御分析:这里没有看到插件签名校验、哈希白名单或发布链验证    // 合法产品应在加载前验证插件来源和完整性}
1
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
处置建议:隔离主机、保全注册表值、导出进程树、检查插件缓存和账号风险
1
2
3

# 10. 合规远程管理模型

10. 合规远程管理模型(配图)

建议的协议改造方向:

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

# 11. 协议加固要点

11. 协议加固要点(配图)

检查项 当前风险 加固方向
身份认证 10 字节标识不足以证明身 mTLS、证书固定、设备证书
数据机密 异或不等于加 TLS 1.3 AEAD
数据完整 没有MAC HMAC AEAD tag
防重 包中缺少 nonce/序列 序列号、时间窗、重放缓
命令授权 token 直接进入功能 RBAC、命令审批、功能开
插件可信 插件二进制直接接收 签名校验、哈希白名单
配置保护 敏感配置写入注册 加密存储、最小化保存

# 12. 企业检测点

层面 检测点 建议
网络 固定头部长度、周期心跳、长连接 提取连接频率、包长分
主机 插件写入注册表、动态加 关联 COMMAND_SENDLL 前后事件
协议 token 驱动高危功能 用协议字段映射功能类
文件 插件二进制落点或注册表缓 记录哈希和首次发现时
账号 命令缺少操作者身 合规产品必须补齐用户维度

# 13. 合法练习:

  1. 根据源码画出现有包结构:长度0 字节标识、数据体、token?

  2. 说明为什么异或处理不能当作企业级加密?

  3. 设计一个合法远程管理协议的最小安全模型:mTLS、RBAC、命令签名、插件签名、操作留痕?

  4. 写一条检测思路:注册表出现插件缓存后,同一进程短时间内启动新功能线程。

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

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

社区交流

讨论与留言

前往 GitHub Issues →

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

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