11 / 52 Windows、主控与网络

第 11 课:协议与命令分发机制

# 第 11 课:协议与命令分发机制

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

第 10 课已经讲清楚了网络通信模型:HPSocket、TCP/UDP、ClientContext、压缩缓存和完整包回调。本课继续往上走,看“完整业务包到达以后,主控端怎么知道它代表什么功能”。

新手读这类源码时,最容易被大量 TOKEN_*COMMAND_* 名称绕晕。其实先抓住一句话就够了:

第一个字节决定业务含义,后面的字节按约定解释。

这就是本课要讲的核心。网络层只负责把完整数据交给主框架,主框架看第 1 个字节是不是 TOKEN_LOGINTOKEN_DRIVE_LISTTOKEN_SHELL_START 这类全局标记;如果已经打开了某个功能窗口,后续数据再交给窗口自己的 OnReceiveComplete,由窗口内部的 COMMAND_* 或局部 TOKEN_* 继续解释。

本课不讲如何构造、伪造、发送控制包,也不讲如何复现远控功能。所有源码节选只用于理解风险、提取检测点和设计合法软件的安全改造思路。

# 1. 本课学习目标

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

问题 本课回答
TOKEN_* 是什么 全局业务类型标记,决定主控端打开哪个功能入口或处理哪类基础事件
COMMAND_* 是什么 某个窗口或插件内部的局部命令,只有进入对应模块后才有具体含义
为什么说协议很简单 包体第 1 个字节是命令号,后面紧跟参数结构、字符串或可变数据
简单协议有什么风险 缺少强认证、完整性校验、版本协商和严格边界检查时,容易被误解析或滥用
ProcessReceiveComplete 为什么关键 它是完整业务包从网络层进入主控业务层的分发入口
如何从源码整理协议字典 先读全局 TOKEN_*,再读窗口级 COMMAND_*,最后建立“命令 -> 文件 -> 函数 -> 风险”的表
企业如何检测 把固定首字节、长连接、心跳、插件发送、内存加载、注册表缓存和 UI 窗口事件关联起来

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

源码文件 本课关注点
主控\Quick\macros.h 全局 TOKEN_*、核心 COMMAND_*、窗口消息、ClientContextDllSendData
主控\Quick\MainFrm.cpp ProcessReceiveCompleteProcessReceiveOnOpenSendVersionOnOpenSendDllSendAutoDll
主控\Quick\FileManagerDlg.h 文件管理窗口内部的 COMMAND_* 和局部 TOKEN_*
主控\Quick\FileManagerDlg.cpp 文件管理命令发送、响应分发和 UI 刷新
主插件\HPSocket\stdafx.h 插件侧维护的同一套全局 TOKEN_* 枚举
主插件\上线模块\上线模块\KernelManager.h 插件侧 DllSendData 结构
主插件\上线模块\上线模块\KernelManager.cpp 版本比较、请求插件数据、缓存插件数据的防御侧观察点
主插件\上线模块\上线模块\上线模块.cpp 上线模块连接后发送版本探测 token 的位置

本课会使用文件管理作为循序渐进的例子,因为它的协议最直观:发出“列目录”命令,参数是路径;返回“文件列表”响应,主控端刷新列表。远程终端、注入、驱动、压力测试等高危功能只做风险归类,不展开可复用流程。

# 3. 从网络层到业务层

第 10 课讲到完整包会写入 m_DeCompressionBuffer,随后触发 NC_RECEIVE_COMPLETE。本课从这里开始。

第 11 课图 1:协议分层地图

按顺序理解:

  1. 网络层收到数据,拼出完整包。
  2. 完整包进入 ClientContext::m_DeCompressionBuffer
  3. 主框架读取第 1 个字节。
  4. 如果第 1 个字节是全局 TOKEN_*,主框架决定打开窗口或处理基础事件。
  5. 如果某个功能窗口已经绑定到该连接,数据交给窗口自己的 OnReceiveComplete
  6. 窗口内部再按局部 COMMAND_* 或局部 TOKEN_* 解释后续数据。
  7. 当主控端要发命令时,也通常把 COMMAND_* 放在第 1 个字节,后面拼参数。

这套机制的优点是简单、执行开销低、写起来快。缺点也很明显:如果缺少认证、签名、长度校验和版本协商,任何一端只要对第 1 个字节理解不同,就可能误分发;如果被滥用,也容易成为远控命令通道。

# 4. 前置概念补课

# 4.1 什么是协议

协议就是双方约定好的数据解释规则。

例如双方约定:

字节位置 含义
第 0 字节 命令号
第 1 字节以后 命令参数
参数后面 可变长度数据

网络层只看到字节,不知道这些字节是目录路径、屏幕数据、文件块还是插件元数据。只有业务层按协议解析以后,字节才有意义。

# 4.2 什么是 token

本源码里的 TOKEN_* 更像“全局事件类型”。它通常由被控端或插件侧发回主控端,用来告诉主控端:

token 类型 白话含义
TOKEN_LOGIN 有连接上报上线信息
TOKEN_DRIVE_LIST 文件管理相关数据来了
TOKEN_BITMAPINFO_* 屏幕类窗口需要打开或处理
TOKEN_HEARTBEAT 心跳包,不进入普通业务流程
TOKEN_SENDLL 对端请求主控端发送插件数据

# 4.3 什么是 command

COMMAND_* 更像“某个模块内部的动作”。例如文件管理窗口里,COMMAND_LIST_FILES 表示请求列目录,COMMAND_FILE_DATA 表示文件数据块。换一个模块以后,同样的数值可能有完全不同的意义。

所以新手要记住:

名称 范围 谁解释
TOKEN_* 全局层 CMainFrame::ProcessReceiveComplete
COMMAND_* 功能窗口或插件内部 对应 Dialog 或 Manager
局部 TOKEN_* 某个窗口内部响应类型 对应 Dialog 的 OnReceiveComplete

# 4.4 为什么一字节协议风险高

macros.h 里有一句注释:BYTE最大也就256。这说明全局命令空间天然受限。

一字节协议常见问题包括:

风险 说明
命令空间小 最多 256 个值,容易出现保留值、复用值和模块私有值混杂
缺少类型边界 只看第 1 字节,不足以证明后续数据真的符合结构体格式
缺少认证语义 命令号本身不代表可信来源
缺少完整性保护 如果没有签名或 MAC,接收端难以确认包未被篡改
缺少版本协商 两端枚举不同步时,同一个数字可能被解释成不同功能
容易误解析 长度不一致、编码不一致、结构体对齐不同都会造成解析偏差

# 5. 一字节命令包模型

先看这套源码最常见的数据包形态:

第 11 课图 2:一字节命令包结构

可以把它抽象成:

[ Byte 0: TOKEN 或 COMMAND ][ 参数结构体 ][ 字符串 / 路径 / 元数据 ][ 可变长度数据 ]
1

这个模型在源码中反复出现。例如:

场景 第 1 字节 后续数据
文件管理列目录 COMMAND_LIST_FILES 远程路径字符串
文件管理返回列表 TOKEN_FILE_LIST 文件列表数据
插件版本探测 TOKEN_GETVERSION 位数标记或版本数据
插件请求 TOKEN_SENDLL DllSendData
插件下发 COMMAND_SENDLL DllSendData + 插件二进制数据
心跳 TOKEN_HEARTBEAT 忙闲状态

安全工程上要关注的不是“怎么发这个包”,而是这些包暴露了什么检测特征:固定首字节、固定长度结构、固定心跳周期、插件元数据、异常大包和可执行内存行为。

# 6. 全局 TOKEN_* 源码精读

先看全局枚举。它是主控端理解业务包的第一张表。

// 源码位置:主控\Quick\macros.h:4-44
// 防御分析注释:
// 1. 这些值是完整业务包第 1 字节的全局含义。
// 2. 主控端和插件侧必须保持一致,否则同一个字节会被错误解释。
// 3. 这里混合了上线、文件、屏幕、音频、键盘、代理、驱动等高风险能力,
//    防御侧可以把它整理成协议字典,用于样本分析和日志关联。
// 4. TOKEN_KERNEL = 100、TOKEN_EXPAND = 200、TOKEN_NOTHING = 255 说明枚举中存在保留区间。
enum MAIN
{
    TOKEN_CONDITION,         // 数据更新
    TOKEN_ERROR,             // 客户端反馈信息
    TOKEN_PROCESS,           // 进程
    TOKEN_GNDESKTOP,         // 桌面截图预览
    TOKEN_GETVERSION,        // 获取版本
    TOKEN_SENDLL,            // 发送DLL数据
    TOKEN_LOGIN,             // 上线

    TOKEN_DRIVE_LIST,        // 文件传输
    TOKEN_WEBCAM_BITMAPINFO, // 摄像头
    TOKEN_AUDIO_START,       // 麦克风
    TOKEN_SPEAK_START,       // 扬声器
    TOKEN_KEYBOARD_START,    // 键盘记录
    TOKEN_PSLIST,            // 系统管理
    TOKEN_SHELL_START,       // 远程终端
    TOKEN_SYSINFOLIST,       // 主机管理
    TOKEN_CHAT_START,        // 远程交谈
    TOKEN_STARTUP_STATUS_START, // 启动管理
    TOKEN_REGEDIT,           // 注册表管理
    TOKEN_PROXY_START,       // 代理
    TOKEN_DDOS,              // 压力测试
    TOKEN_INJECT,            // 注入管理

    TOKEN_MONITOR,           // 屏幕监控
    TOKEN_BITMAPINFO_DIF,    // 差异屏幕
    TOKEN_BITMAPINFO_QUICK,  // 高速屏幕
    TOKEN_BITMAPINFO_PLAY,   // 娱乐屏幕
    TOKEN_BITMAPINFO_HIDE,   // 后台屏幕

    TOKEN_KERNEL = 100,      // 驱动功能

    TOKEN_EXPAND = 200,      // 所有扩展插件
    TOKEN_HEARTBEAT,         // 心跳
    TOKEN_ACTIVED,           // 激活
    TOKEN_GETAUTODLL,        // 自动任务
    TOKEN_NOTHING = 255,     // 空值或保留值
};
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

对新手来说,不要一口气背所有 token。先分 5 类:

分类 代表 token 防御侧关注
连接状态 TOKEN_LOGINTOKEN_HEARTBEATTOKEN_ACTIVED 上线、心跳、激活状态
插件调度 TOKEN_GETVERSIONTOKEN_SENDLLTOKEN_GETAUTODLL 版本探测、插件请求、自动加载
系统管理 TOKEN_PROCESSTOKEN_SYSINFOLISTTOKEN_REGEDIT 进程、系统信息、注册表访问
交互控制 TOKEN_SHELL_STARTTOKEN_CHAT_STARTTOKEN_PROXY_START 远程交互、代理通道
采集类 TOKEN_GNDESKTOPTOKEN_BITMAPINFO_*TOKEN_WEBCAM_BITMAPINFOTOKEN_AUDIO_START 屏幕、摄像头、音频采集

高风险 token 不等于攻击步骤。它们在本教程里的作用是帮助企业理解“哪些能力需要监控和限制”。

# 7. 核心 COMMAND_* 源码精读

KernelManager 枚举是主控端核心管理类使用的一组命令。它和全局 TOKEN_* 不同:这些值通常用于主控端向对端发送指令。

// 源码位置:主控\Quick\macros.h:48-71
// 防御分析注释:
// 1. 这些 COMMAND_* 是主控端向对端表达动作的局部命令。
// 2. 这里包含插件发送、连接关闭、进程/窗口信息、重启/退出等高风险操作。
// 3. 合法软件中这类命令必须绑定认证、授权、审批、日志和用户确认。
// 4. 本课只分析命令分发模型,不提供构造或发送这些命令的方法。
enum  KernelManager
{
    COMMAND_DLLMAIN,
    COMMAND_SENDLL,
    COMMAND_CLOSESOCKET,
    COMMAND_GET_PROCESSANDCONDITION,
    COMMAND_GET_SCREEN,
    COMMAND_UPLOAD_EXE,
    COMMAND_DOWN_EXE,
    COMMAND_RENAME,
    COMMAND_FILTERPROCESS,
    COMMAND_MONITOR,
    COMMAND_GETMONITOR,
    COMMAND_CLEANLOG,
    COMMAND_RESTART,
    COMMAND_EXIT,
    COMMAND_LOGOUT,
    COMMAND_REBOOT,
    COMMAND_SHUTDOWN,
    COMMAND_CHANGELOAD,
    COMMAND_MOVECLIENT,
    COMMAND_COPYCLIENT,
    COMMAND_SET_DOOR_GETPERMINSSION = 100,
    COMMAND_SET_DOOR_QUITPERMINSSION,
};
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

这段代码的安全重点不是“每个命令怎么用”,而是:

风险点 为什么重要
命令语义高危 包含关闭连接、上传下载、重启关机、日志清理等动作
命令缺少自描述 包里只有数值,不带强类型名称,分析时需要回到源码映射
命令和权限未绑定 从枚举本身看不出权限边界,需要在上层补控制
命令空间可扩展 后续模块继续定义自己的 COMMAND_*,整体协议字典会不断变大

合法远程运维软件中,同类动作至少需要:

安全要求 说明
身份认证 确认操作者是谁
命令授权 确认操作者是否有权执行该命令
会话审批 高危命令需要审批或双人确认
操作日志 记录时间、操作者、资产、命令、参数和结果
完整性校验 防止命令包被篡改
用户提示 对屏幕、音频、摄像头等隐私能力给出可见提示

# 8. ClientContext 在分发中的作用

第 8 课讲过 ClientContext 是连接状态档案。本课只关注和协议分发有关的字段。

// 源码位置:主控\Quick\macros.h:222-251
// 防御分析注释:
// 1. m_DeCompressionBuffer 保存完整解压后的业务包,主框架从这里读取第 1 字节。
// 2. m_Dialog[0] 表示当前连接绑定的功能窗口类型。
// 3. m_Dialog[1] 保存窗口对象地址,后续数据会被转给该窗口处理。
// 4. m_bIsMainSocket 用来区分主连接和插件/功能连接,错误判断可能导致数据进错处理分支。
// 5. m_password 是通信相关字段,不应被视为强认证机制。
struct ClientContext
{
    ULONG_PTR           m_Socket;
    CBuffer             m_WriteBuffer;
    CBuffer             m_CompressionBuffer;    // 接收到的压缩的数据
    CBuffer             m_DeCompressionBuffer;  // 解压后的数据

    int                 m_Dialog[2]; // 第一个int是类型,第二个是CDialog的地址
    int                 m_allpack_rev;
    long long           m_alldata_rev;
    int                 m_allpack_send;
    long long           m_alldata_send;

    int                 IsConnect;
    BOOL                m_bIsMainSocket; // 是不是主socket
    e_socket            switchsocket;
    PVOID               m_server;
    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

这里的关键是 m_Dialog

字段 白话解释 分发意义
m_Dialog[0] 当前连接属于哪个窗口 决定后续包交给哪个 Dialog
m_Dialog[1] 窗口对象地址 主框架用它调用具体窗口的 OnReceiveComplete
m_DeCompressionBuffer 完整业务包 第 1 个字节决定 token 或局部响应类型
m_bIsMainSocket 是否主连接 防止插件连接数据混入主列表更新逻辑

这也是很多新手看源码跳跃的原因:同一条连接在不同阶段会有不同状态。刚上线时 m_Dialog[0] 可能还没绑定窗口,主框架按全局 token 分发;打开文件管理窗口以后,后续文件包就交给 CFileManagerDlg

# 9. 主框架接收分发总流程

CMainFrame::ProcessReceiveComplete 是本课最重要的函数。它负责完整业务包到达后的第一轮分流。

第 11 课图 3:主框架接收分发时序

按照源码顺序,它做三件事:

  1. 先判断是否为空上下文。
  2. 再判断是否心跳包。
  3. 如果功能窗口已打开,交给窗口处理。
  4. 否则读取第 1 字节,进入全局 TOKEN_* 分发。

先看心跳和窗口分发部分。

// 源码位置:主控\Quick\MainFrm.cpp:1786-1869
// 防御分析注释:
// 1. TOKEN_HEARTBEAT 被提前处理,不进入普通业务 switch。
// 2. m_bBusy 会影响心跳返回内容,防御侧可关注固定周期和固定长度心跳。
// 3. m_Dialog[0] > 0 表示当前连接已经绑定功能窗口,后续数据转给对应 Dialog。
// 4. 如果 m_Dialog[0] 与 m_Dialog[1] 不一致,可能导致错误类型转换和崩溃风险。
void CMainFrame::ProcessReceiveComplete(ClientContext* pContext)
{
    if ((pContext == NULL))
        return;

    CDialog* dlg = (CDialog*)pContext->m_Dialog[1];

    if (pContext->m_DeCompressionBuffer.GetBuffer(0)[0] == TOKEN_HEARTBEAT)
    {
        if (m_bBusy)
        {
            BYTE bToken[2] = { TOKEN_HEARTBEAT, 0 };
            g_pSocketBase->Send(pContext, (LPBYTE)&bToken, 2);
        }
        else
        {
            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 SHELL_DLG:
            ((CShellDlg*)dlg)->OnReceiveComplete();
            break;
        case REGEDIT_DLG:
            ((CRegeditDlg*)dlg)->OnReceiveComplete();
            break;
        default:
            TRACE("if (pContext->m_Dialog[0] > 0) 非法数据");
            break;
        }
        return;
    }

    // 后续进入全局 TOKEN_* switch
}
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

# 10. 全局 TOKEN_* 分发

如果当前连接没有绑定功能窗口,ProcessReceiveComplete 会进入全局 token 分发。

第 11 课图 4:TOKEN 到窗口的路由

源码节选如下:

// 源码位置:主控\Quick\MainFrm.cpp:1875-2029
// 防御分析注释:
// 1. switch 的判断条件就是完整包第 1 个字节。
// 2. TOKEN_LOGIN 进入上线列表处理,TOKEN_DRIVE_LIST 打开文件管理窗口。
// 3. 多数功能不是直接执行,而是 PostMessage 给 UI 线程打开对应 Dialog。
// 4. 这些映射关系是协议字典的主干:TOKEN -> 窗口消息 -> Dialog 类。
switch (pContext->m_DeCompressionBuffer.GetBuffer(0)[0])
{
case TOKEN_CONDITION:
    // 更新窗口状态和缩略图
    break;

case TOKEN_GETAUTODLL:
    SendAutoDll(pContext);
    break;

case TOKEN_PROCESS:
    // 更新进程或状态提示
    break;

case TOKEN_LOGIN:
    g_pTabView->SendMessage(WM_ADDFINDGROUP, 0, (LPARAM)pContext);
    break;

case TOKEN_GNDESKTOP:
    OnOpenDesktop(pContext);
    break;

case TOKEN_GETVERSION:
    OnOpenSendVersion(pContext);
    break;

case TOKEN_SENDLL:
    OnOpenSendDll(pContext);
    break;

case TOKEN_DRIVE_LIST:
    g_pFrame->PostMessage(WM_OPENMANAGERDIALOG, 0, (LPARAM)pContext);
    break;

case TOKEN_BITMAPINFO_QUICK:
    g_pFrame->PostMessage(WM_OPENSCREENSPYDIALOG_QUICK, 0, (LPARAM)pContext);
    break;

case TOKEN_WEBCAM_BITMAPINFO:
    g_pFrame->PostMessage(WM_OPENWEBCAMDIALOG, 0, (LPARAM)pContext);
    break;

case TOKEN_SHELL_START:
    g_pFrame->PostMessage(WM_OPENSHELLDIALOG, 0, (LPARAM)pContext);
    break;

case TOKEN_REGEDIT:
    g_pFrame->PostMessage(WM_OPENREGEDITDIALOG, 0, (LPARAM)pContext);
    break;

default:
    TRACE("switch (...) 非法数据");
    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
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

这段代码要按“路由表”理解:

输入 token 主控动作 结果
TOKEN_LOGIN WM_ADDFINDGROUP 上线连接加入列表
TOKEN_GETVERSION OnOpenSendVersion 主控端回传版本信息
TOKEN_SENDLL OnOpenSendDll 主控端准备发送插件数据
TOKEN_DRIVE_LIST WM_OPENMANAGERDIALOG 打开文件管理窗口
TOKEN_BITMAPINFO_QUICK WM_OPENSCREENSPYDIALOG_QUICK 打开高速屏幕窗口
TOKEN_WEBCAM_BITMAPINFO WM_OPENWEBCAMDIALOG 打开摄像头窗口
TOKEN_SHELL_START WM_OPENSHELLDIALOG 打开终端窗口
TOKEN_REGEDIT WM_OPENREGEDITDIALOG 打开注册表窗口

防御侧整理协议字典时,建议用以下列:

字段 示例
token 名称 TOKEN_DRIVE_LIST
所在文件 主控\Quick\macros.h
分发函数 CMainFrame::ProcessReceiveComplete
分发位置 主控\Quick\MainFrm.cpp:1973 附近
目标窗口消息 WM_OPENMANAGERDIALOG
目标 Dialog CFileManagerDlg
风险分类 文件访问
检测建议 文件枚举、远程路径参数、异常文件传输

# 11. 窗口消息和 Dialog 类型

全局 token 并不直接创建所有窗口。主框架很多时候使用 MFC 消息把任务投递给 UI 线程。

// 源码位置:主控\Quick\macros.h:315-345
// 防御分析注释:
// 1. WM_OPEN* 是主控端 UI 层的窗口路由消息。
// 2. TOKEN_* 到 WM_OPEN* 再到 Dialog 类,构成完整业务入口。
// 3. 防御侧读源码时,可以从这些消息反查对应 OnMessage 处理函数。
enum
{
    WM_NOTIFYPROC = WM_USER + 101,
    WM_ADDTOMAINLIST = WM_USER + 102,
    WM_DESKTOPPOPUP,

    WM_ADDFINDGROUP,
    WM_DELFINDGROUP,
    WM_REMOVEFROMLIST,

    WM_OPENSCREENSPYDIALOG_DIF,
    WM_OPENSCREENSPYDIALOG_QUICK,
    WM_OPENSCREENSPYDIALOG_PLAY,
    WM_OPENSCREENSPYDIALOG_HIDE,

    WM_OPENMANAGERDIALOG,       // 文件管理
    WM_OPENWEBCAMDIALOG,        // 摄像头
    WM_OPENAUDIODIALOG,         // 语音监听
    WM_OPENSPEAKERDIALOG,       // 扬声器监听
    WM_OPENKEYBOARDDIALOG,      // 键盘记录
    WM_OPENSHELLDIALOG,         // shell窗口
    WM_OPENSYSINFODIALOG,       // 服务器信息
    WM_OPENREGEDITDIALOG,       // 注册表管理
    WM_OPENCHATDIALOG,          // 交谈窗口
    WM_OPENSTARTUPMGRDIALOG,    // 启动管理
    WM_OPENPROXYDIALOG,         // 代理窗口
};
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

Dialog 类型也在同一个枚举块里定义。

// 源码位置:主控\Quick\macros.h:347-366
// 防御分析注释:
// 1. 这些值会写入 ClientContext::m_Dialog[0]。
// 2. 后续完整包到达时,主框架会根据这个类型强转 m_Dialog[1]。
// 3. 安全改造时应避免用 int 保存窗口指针,建议改成类型安全的对象引用或智能指针。
enum
{
    FILEMANAGER_DLG = 1,    // 文件管理
    SCREENSPY_DIF_DLG,      // 差异屏幕
    SCREENSPY_QUICK_DLG,    // 高速屏幕
    SCREENSPY_PLAY_DLG,     // 娱乐屏幕
    SCREENSPY_HIDE_DLG,     // 后台屏幕
    WEBCAM_DLG,             // 摄像头
    AUDIO_DLG,              // 麦克风
    SPEAKER_DLG,            // 扬声器
    CHAT_DLG,               // 对话
    STARTUPMGR_DLG,         // 启动管理
    SHELL_DLG,              // 终端
    PROXY_DLG,              // 代理
    KEYBOARD_DLG,           // 键盘
    REGEDIT_DLG,            // 注册表
    MACHINE_DLG,            // 系统管理
    EXPAND_DLG,             // 互动插件
    KERNEL_DLG,             // 驱动插件
    MONITOR_DLG,            // 屏幕监控
};
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

这里体现了主控端的三层路由:

层级 代表 作用
网络事件 NC_RECEIVE_COMPLETE 说明完整包到了
全局 token TOKEN_DRIVE_LIST 说明是哪类业务
UI 消息 WM_OPENMANAGERDIALOG 让主框架打开对应窗口
Dialog 类型 FILEMANAGER_DLG 后续数据交给哪个窗口

# 12. TOKEN_*COMMAND_* 的边界

很多新手把 TOKEN_*COMMAND_* 混在一起,导致阅读很跳。可以用下面这张图建立边界:

第 11 课图 7:TOKEN 与 COMMAND 的边界

判断方法很简单:

问题 如果答案是“是” 更可能是什么
这个值在 macros.henum MAIN 里吗 全局 TOKEN_*
这个值会进入 ProcessReceiveComplete 的大 switch 吗 全局 TOKEN_*
这个值只在某个 Dialog 头文件里定义吗 局部 COMMAND_* 或局部 TOKEN_*
这个值由某个窗口的按钮或菜单发送吗 局部 COMMAND_*
这个值由插件返回给窗口处理吗 局部 TOKEN_*

企业做协议分析时,也要按这个层次建表。不要只列所有枚举名,否则很难看出哪个命令属于全局入口,哪个命令只在某个模块内部有效。

# 13. 文件管理窗口的局部命令

下面用文件管理窗口做例子。它的头文件里定义了很多局部命令和局部响应 token。

// 源码位置:主控\Quick\FileManagerDlg.h:8-78
// 防御分析注释:
// 1. COMMAND_* 多数是主控端发向对端的文件管理动作。
// 2. TOKEN_* 多数是对端返回给文件管理窗口的响应类型。
// 3. 文件删除、上传下载、加解密、压缩、搜索等能力都属于高风险文件操作。
// 4. 合法软件应对这类命令做最小权限、路径限制、操作确认和完整日志。
enum
{
    COMMAND_LIST_FILES,
    COMMAND_DELETE_FILE,
    COMMAND_DELETE_DIRECTORY,
    COMMAND_DOWN_FILES,
    COMMAND_CONTINUE,
    COMMAND_CREATE_FOLDER,
    COMMAND_RENAME_FILE,
    COMMAND_STOP,
    COMMAND_SET_TRANSFER_MODE,
    COMMAND_FILE_SIZE,
    COMMAND_FILE_DATA,
    COMMAND_OPEN_FILE_SHOW,
    COMMAND_OPEN_FILE_HIDE,
    COMMAND_COMPRESS_FILE_PARAM,
    COMMAND_FILES_SEARCH_START,
    COMMAND_FILES_SEARCH_STOP,
    COMMAND_FILE_EXCEPTION,
    COMMAND_SEARCH_FILE,
    COMMAND_FILE_GETNETHOOD,
    COMMAND_FILE_RECENT,
    COMMAND_FILE_INFO,
    COMMAND_FILE_Encryption,
    COMMAND_FILE_Decrypt,
    COMMAND_FILE_ENFOCE,
    COMMAND_FILE_CopyFile,
    COMMAND_FILE_PasteFile,
    COMMAND_FILE_zip,
    COMMAND_FILE_zip_stop,
    COMMAND_FILE_NO_ENFORCE,
    COMMAND_FILE_GETINFO,

    COMMAND_FILE_SEARCHPLUS_LIST,

    TOKEN_SEARCH_FILE_LIST,
    TOKEN_SEARCH_FILE_FINISH,
    TOKEN_FILE_LIST,
    TRANSFER_MODE_NORMAL,
    TOKEN_FILE_SIZE,
    TOKEN_FILE_DATA,
    TOKEN_TRANSFER_FINISH,
    TOKEN_GET_TRANSFER_MODE,
    TOKEN_CREATEFOLDER_FINISH,
    TOKEN_RENAME_FINISH,
    TOKEN_COMPRESS_FINISH,
    TOKEN_DELETE_FINISH,
    TOKEN_DATA_CONTINUE,
    TOKEN_FILE_GETNETHOOD,
    TOKEN_FILE_RECENT,
    TOKEN_FILE_INFO,
    TOKEN_FILE_REFRESH,
    TOKEN_FILE_ZIPOK,
    TOKEN_FILE_GETINFO,
};
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

这段枚举读法:

前缀 方向 示例 风险关注
COMMAND_* 主控端发出 COMMAND_LIST_FILES 请求对端执行动作
TOKEN_* 对端返回 TOKEN_FILE_LIST 对端返回结果或状态
TRANSFER_MODE_* 状态或选项 TRANSFER_MODE_NORMAL 影响文件传输处理方式

文件管理是企业检测中非常关键的一类能力。即使不看具体实现,只看命令名也能建立风险面:

能力类别 代表命令 企业关注
枚举 COMMAND_LIST_FILESCOMMAND_FILE_GETNETHOOD 大量目录枚举、共享目录访问
传输 COMMAND_DOWN_FILESCOMMAND_FILE_DATA 文件外传、异常上传
修改 COMMAND_CREATE_FOLDERCOMMAND_RENAME_FILE 文件系统变更
删除 COMMAND_DELETE_FILECOMMAND_DELETE_DIRECTORY 批量删除、破坏性操作
搜索 COMMAND_SEARCH_FILECOMMAND_FILE_SEARCHPLUS_LIST 敏感文件定位
加解密 COMMAND_FILE_EncryptionCOMMAND_FILE_Decrypt 勒索类或异常加密行为的风险提示

# 14. 文件管理命令往返流程

文件管理的“列目录”最适合新手理解一字节协议。

第 11 课图 5:文件管理命令往返

先看主控端发出列目录命令的源码。

// 源码位置:主控\Quick\FileManagerDlg.cpp:859-869
// 防御分析注释:
// 1. PacketSize = 路径字符串长度 + 1 个命令字节。
// 2. bPacket[0] 放 COMMAND_LIST_FILES,后面紧跟路径字符串。
// 3. 这是一字节命令包的典型形态。
// 4. 合法软件中应限制路径范围,避免任意路径枚举。
int PacketSize = (m_Remote_Path.GetLength() + 1) * sizeof(TCHAR) + 1;
BYTE* bPacket = (BYTE*)LocalAlloc(LPTR, PacketSize);

bPacket[0] = COMMAND_LIST_FILES;
memcpy(bPacket + 1, m_Remote_Path.GetBuffer(0), PacketSize - 1);
m_iocpServer->Send(m_pContext, bPacket, PacketSize);
LocalFree(bPacket);
1
2
3
4
5
6
7
8
9
10
11
12
13

对端返回数据后,文件管理窗口按返回 token 处理。

// 源码位置:主控\Quick\FileManagerDlg.cpp:641-671
// 防御分析注释:
// 1. 文件管理窗口内部再次读取第 1 字节。
// 2. TOKEN_FILE_LIST 表示返回文件列表,TOKEN_FILE_DATA 表示文件内容。
// 3. 这说明全局分发之后,每个窗口还有自己的局部分发层。
// 4. 防御侧可以把文件类 TOKEN 与文件系统行为、网络数据量做关联。
void CFileManagerDlg::OnReceiveComplete()
{
    if (m_bOnClose) return;
    switch (m_pContext->m_DeCompressionBuffer.GetBuffer(0)[0])
    {
    case TOKEN_FILE_LIST:
        FixedRemoteFileList(
            m_pContext->m_DeCompressionBuffer.GetBuffer(0),
            m_pContext->m_DeCompressionBuffer.GetBufferLen() - 1
        );
        break;
    case TOKEN_FILE_SIZE:
        CreateLocalRecvFile();
        break;
    case TOKEN_FILE_DATA:
        WriteLocalRecvFile();
        break;
    case TOKEN_TRANSFER_FINISH:
        EndLocalRecvFile();
        break;
    case TOKEN_DELETE_FINISH:
        EndRemoteDeleteFile();
        break;
    case TOKEN_DATA_CONTINUE:
        SendFileData();
        break;
    default:
        SendException();
        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
27
28
29
30
31
32
33
34
35
36
37

这里可以看到两层分发:

层级 读取第 1 字节的位置 例子
主框架全局分发 CMainFrame::ProcessReceiveComplete TOKEN_DRIVE_LIST 打开文件管理窗口
文件窗口局部分发 CFileManagerDlg::OnReceiveComplete TOKEN_FILE_LIST 刷新文件列表

这也是本课最重要的阅读方法:先找全局入口,再跟到窗口内部。

# 15. 插件版本与发送流程

插件发送流程会在第 12、13 课详细展开,本课只讲它和协议分发的关系。

第 11 课图 6:插件版本与发送流程

先看上线模块连接后发出的版本探测。

// 源码位置:主插件\上线模块\上线模块\上线模块.cpp:242-255
// 防御分析注释:
// 1. 对端连接后发送 TOKEN_GETVERSION,让主控端返回登录模块版本信息。
// 2. Token[1] 携带位数标记,x64 和 x86 分支不同。
// 3. 这是插件加载链路的起点之一,企业可在样本分析中关注版本探测包。
CKernelManager manager(socketClient, MyInfo.otherset.puppet);
BYTE Token[2] = {};
Token[0] = TOKEN_GETVERSION;
#ifdef _WIN64
Token[1] = 1;
#else
Token[1] = 0;
#endif

socketClient->Send(Token, 2);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

主控端收到 TOKEN_GETVERSION 后,会回传版本信息。

// 源码位置:主控\Quick\MainFrm.cpp:2342-2364
// 防御分析注释:
// 1. 主控端根据对端位数选择 x86 或 x64 插件索引。
// 2. 返回包第 1 字节仍是 TOKEN_GETVERSION,后面跟版本数据。
// 3. 版本探测和插件索引是检测插件调度链的重要线索。
void CMainFrame::OnOpenSendVersion(ClientContext* pContext)
{
    PluginsDate* p_PluginsDate = NULL;
    if (pContext->m_DeCompressionBuffer.GetBuffer(1)[0] == 0)
        p_PluginsDate = &(m_PluginsDate_x86);
    else
        p_PluginsDate = &(m_PluginsDate_x64);

    PluginsDate::iterator it_PluginsDate = p_PluginsDate->find(_T("登录模块.dll_bin"));
    if (it_PluginsDate != p_PluginsDate->end())
    {
        const int payloadSize = 100;
        BYTE* bPacket = new BYTE[payloadSize + 1];
        memset(bPacket, 0, payloadSize + 1);
        bPacket[0] = TOKEN_GETVERSION;
        memcpy(bPacket + 1, it_PluginsDate->second->Version, payloadSize);
        g_pSocketBase->Send(pContext, bPacket, payloadSize + 1);
        SAFE_DELETE_AR(bPacket);
    }
}
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

如果版本不一致,对端会请求主控发送插件数据。插件侧请求的源码片段如下。

// 源码位置:主插件\上线模块\上线模块\KernelManager.cpp:171-190
// 防御分析注释:
// 1. DllSendData 描述要请求的插件名称、位数、版本等元数据。
// 2. lpBuffer[0] = TOKEN_SENDLL,表示向主控端请求插件数据。
// 3. 防御侧应把 TOKEN_SENDLL、DllSendData、插件名称和数据大小作为关联线索。
// 4. 不应把这段代码理解为合法加载方案;合法软件应使用签名插件、受控仓库和强校验。
DllSendData loginModuleDllSendData =
{
    TASK_MAIN,
    {_T('登'), _T('录'), _T('模'), _T('块'), _T('.'), _T('d'), _T('l'), _T('l'), _T('_'), _T('b'), _T('i'), _T('n'), 0},
#ifdef _WIN64
    TRUE,
#else
    FALSE,
#endif
    0,
    {},
    {},
};

DWORD dwOffset = sizeof(DllSendData) + 1;
LPBYTE lpBuffer = new BYTE[dwOffset];
lpBuffer[0] = TOKEN_SENDLL;
::memcpy(lpBuffer + 1, &loginModuleDllSendData, dwOffset - 1);
Send((LPBYTE)lpBuffer, dwOffset);
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

主控端收到 TOKEN_SENDLL 后进入 OnOpenSendDll

// 源码位置:主控\Quick\MainFrm.cpp:2369-2434
// 防御分析注释:
// 1. 主控端从接收缓冲区第 1 字节之后解析 DllSendData。
// 2. bPacket[0] = COMMAND_SENDLL,后面拼 DllSendData 和插件数据。
// 3. 这是“请求插件 token”转成“下发插件 command”的关键边界。
// 4. 企业检测可关注大包发送、DLL 名称、版本字段、x86/x64 分支和可执行数据缓存。
void CMainFrame::OnOpenSendDll(ClientContext* pContext)
{
    DllSendData* pDllSendData =
        (DllSendData*)pContext->m_DeCompressionBuffer.GetBuffer(1);

    int i_DllSendDate = sizeof(DllSendData);
    if (pContext->m_DeCompressionBuffer.GetBufferLen() != (sizeof(DllSendData) + 1))
        return;

    CString strFileName = pDllSendData->szDllName;

    PluginsDate* p_PluginsDate;
    if (pDllSendData->bIsX64)
        p_PluginsDate = &(m_PluginsDate_x64);
    else
        p_PluginsDate = &(m_PluginsDate_x86);

    PluginsDate::iterator it_PluginsDate = p_PluginsDate->find(strFileName);
    if (it_PluginsDate != p_PluginsDate->end())
    {
        int nPacketSize = 1 + i_DllSendDate + it_PluginsDate->second->filesize;
        BYTE* bPacket = new BYTE[nPacketSize];
        memset(bPacket, 0, nPacketSize);
        bPacket[0] = COMMAND_SENDLL;
        pDllSendData->dllDataSize = it_PluginsDate->second->filesize;
        memcpy(bPacket + 1, pDllSendData, i_DllSendDate);
        memcpy(
            bPacket + 1 + i_DllSendDate,
            it_PluginsDate->second->filedate,
            it_PluginsDate->second->filesize
        );
        g_pSocketBase->Send(pContext, bPacket, nPacketSize);
        SAFE_DELETE_AR(bPacket);
    }
}
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

这里要特别注意命名方向:

字节 所在方向 含义
TOKEN_GETVERSION 对端发起,主控响应 版本探测
TOKEN_SENDLL 对端发起 请求主控发送插件
COMMAND_SENDLL 主控下发 携带插件元数据和插件数据

从检测视角看,插件调度链的关键不是单个函数,而是一组连续信号:

版本探测 -> 插件请求 -> 大块二进制传输 -> 注册表或内存缓存 -> 动态加载或调用

本课只讲到协议层。插件加载、缓存、内存行为会在后续专栏单独展开。

# 16. 主控侧和插件侧枚举同步问题

主插件通信库里也维护了一份全局 TOKEN_*

// 源码位置:主插件\HPSocket\stdafx.h:29-71
// 防御分析注释:
// 1. 插件侧复制了主控侧的全局 MAIN 枚举。
// 2. 这种做法依赖人工同步,缺少协议版本号时容易产生兼容性问题。
// 3. 合法软件中建议使用统一协议定义文件、自动生成代码、显式版本和兼容性检查。
enum MAIN
{
    TOKEN_CONDITION,
    TOKEN_ERROR,
    TOKEN_PROCESS,
    TOKEN_GNDESKTOP,
    TOKEN_GETVERSION,
    TOKEN_SENDLL,
    TOKEN_LOGIN,

    TOKEN_DRIVE_LIST,
    TOKEN_WEBCAM_BITMAPINFO,
    TOKEN_AUDIO_START,
    TOKEN_SPEAK_START,
    TOKEN_KEYBOARD_START,
    TOKEN_PSLIST,
    TOKEN_SHELL_START,
    TOKEN_SYSINFOLIST,
    TOKEN_CHAT_START,
    TOKEN_STARTUP_STATUS_START,
    TOKEN_REGEDIT,
    TOKEN_PROXY_START,
    TOKEN_DDOS,
    TOKEN_INJECT,

    TOKEN_MONITOR,
    TOKEN_BITMAPINFO_DIF,
    TOKEN_BITMAPINFO_QUICK,
    TOKEN_BITMAPINFO_PLAY,
    TOKEN_BITMAPINFO_HIDE,

    TOKEN_KERNEL = 100,
    TOKEN_EXPAND = 200,
    TOKEN_HEARTBEAT,
    TOKEN_ACTIVED,
    TOKEN_GETAUTODLL,
    TOKEN_NOTHING = 255,
};
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

同步问题会带来三类风险:

风险 例子 防御或改造建议
版本漂移 主控新增 token,插件侧未更新 增加协议版本号和兼容性拒绝
数值误解 同一个字节在两边含义不同 使用统一 IDL 或自动生成枚举
无法溯源 日志只记录数字,不记录名称 记录协议版本、命令名称和模块名

对企业来说,协议字典最好记录“主控侧定义”和“插件侧定义”两份来源。如果两份定义不一致,要标出差异,因为这可能解释异常包、崩溃或误报。

# 17. 协议设计的主要安全缺陷

本源码的协议设计非常直接,适合教学,但不适合作为合法远程管理软件的安全协议。核心问题如下:

缺陷 源码表现 风险
命令认证不足 第 1 字节决定动作 命令来源可信性不足
完整性不足 包体缺少标准签名或 MAC 数据被篡改难以及时发现
版本协商弱 枚举靠两端同步 版本漂移导致误解析
授权边界弱 命令名本身看不出权限 高危命令难以按角色限制
参数结构不自描述 直接按结构体或字符串解释 类型、长度、编码出错会影响稳定性
错误处理弱 部分默认分支只 TRACE 未知命令缺少阻断和告警
高危能力集中 文件、屏幕、键盘、代理、驱动都在同一体系 一旦通道被滥用,影响面很大

合法软件可以保留“命令路由”的思想,但必须补安全边界:

改造方向 建议
协议版本 包头加入版本、长度、类型和兼容性字段
身份认证 使用证书、令牌或企业身份体系
完整性保护 对包头和包体做签名或 MAC
命令授权 每个命令绑定权限模型
参数校验 对长度、路径、编码、结构体大小做严格检查
危险命令审批 文件删除、进程控制、远程终端、插件加载等需要审批
日志记录 记录命令名称、参数摘要、操作者、资产和结果
用户可见性 屏幕、摄像头、音频等能力必须有明确提示

# 18. 如何从源码整理协议字典

建议按下面顺序整理,不要一上来全局搜索所有 COMMAND_

  1. 先读 主控\Quick\macros.henum MAIN,建立全局 token 表。
  2. 再读 CMainFrame::ProcessReceiveComplete,补充 token 对应的处理函数或窗口消息。
  3. 找到窗口消息对应的 ON_MESSAGEOnOpen...dialog 函数。
  4. 记录窗口创建时写入的 m_Dialog[0] 类型。
  5. 进入对应 Dialog 的 .h 文件,整理局部 COMMAND_* 和局部 TOKEN_*
  6. 进入对应 Dialog 的 .cpp 文件,看哪些 UI 操作会发送命令。
  7. 进入对应插件或 Manager,看对端如何解释这些命令。
  8. 最后把协议项和检测点关联起来。

推荐表格模板:

字段 示例
层级 全局 token / 窗口 command / 窗口 response
名称 TOKEN_DRIVE_LIST
数值来源 主控\Quick\macros.h
分发函数 CMainFrame::ProcessReceiveComplete
目标模块 CFileManagerDlg
数据形态 第 1 字节 + 路径字符串
风险分类 文件枚举
主机检测 文件系统枚举、异常句柄
网络检测 小命令包后接大返回包
加固建议 路径范围限制、权限校验、日志记录

注意这里不是为了复现通信,而是为了建立防御侧可理解的“行为字典”。

# 19. 企业检测点

协议检测不能只看一个字节。更可靠的方法是把协议特征和主机行为、网络行为、内存行为、构建链线索关联。

第 11 课图 8:协议检测关联

# 19.1 主机侧检测点

检测点 说明
未知进程创建远程管理窗口 主控端 UI 行为和网络监听同时出现
文件枚举与网络发送相邻 文件系统遍历后出现外发数据
注册表二进制缓存 插件数据写入 REG_BINARY 一类位置
高危功能窗口打开 文件、终端、注册表、代理、屏幕、摄像头等窗口事件
异常日志清理或关机命令 和远程命令通道同时出现时风险升高

# 19.2 网络侧检测点

检测点 说明
固定周期心跳 小包、固定首字节、固定间隔
版本探测包 很短的数据包后接版本或插件请求
命令小包后接大包 例如插件数据、屏幕帧、文件数据
长连接持续收发 低流量保活加间歇性大流量
TCP/UDP 两套模式 同一工具可能支持不同传输方式

# 19.3 内存侧检测点

检测点 说明
大块二进制数据进入进程内存 可能与插件发送或缓存相关
可执行权限内存页 如果出现可写可执行页,需要重点调查
DLL 数据不落地或异常缓存 动态加载链路常见信号
结构体解析后立即调用 插件元数据后接动态调用风险

# 19.4 构建链检测点

检测点 说明
大量 TOKEN_* / COMMAND_* 协议面广,功能能力多
多插件工程 文件、屏幕、音频、键盘、代理、驱动等模块
x86/x64 双份产物 插件调度按位数选择
第三方网络库 HPSocket 等网络通信依赖
自动打包插件 插件元数据、版本和二进制数据被主控端索引

# 20. 合法软件加固建议

如果把这种“主控端 + 插件 + 命令分发”的思路改造成合法远程管理软件,建议按以下方向重构:

改造项 建议
协议头 不再只依赖第 1 字节,增加魔数、版本、长度、类型、序列号
加密传输 使用标准 TLS,不自定义弱加密
双向认证 主控端和被控端都要验证身份
命令权限 每个命令绑定角色、资产范围和审批策略
参数约束 文件路径、注册表路径、进程 ID、插件名都要白名单或策略限制
插件签名 插件必须签名,并校验哈希和版本
危险能力默认关闭 远程终端、摄像头、音频、驱动、注入等默认禁用
全量日志 记录操作者、资产、命令、参数摘要、结果和时间
用户提示 涉及隐私采集时给出可见提示
异常阻断 未知 token、未知 command、长度异常必须拒绝并告警

# 21. 新手阅读路线

第一次读协议分发,不建议从所有插件开始。按下面路线更稳定:

  1. macros.henum MAIN,只记住 5 类 token。
  2. ProcessReceiveComplete,画出 token 到窗口的路由表。
  3. 选择一个低门槛窗口,例如文件管理。
  4. 读窗口 .h 里的局部 COMMAND_*
  5. 找一个最简单命令,例如 COMMAND_LIST_FILES
  6. 看发送端如何把命令写入 bPacket[0]
  7. 看返回端如何用 TOKEN_FILE_LIST 刷新 UI。
  8. 最后再回到插件发送和高危模块。

这样读的好处是,先建立“整体分发骨架”,再进入局部协议,不会被上百个命令名打乱。

# 22. 常见误区

误区 正确认识
TOKEN_*COMMAND_* 是同一层 不是,TOKEN_* 多用于全局分发,COMMAND_* 多用于模块内部动作
第 1 字节能说明一切 不能,还要看长度、上下文、窗口状态和后续参数
打开窗口就是执行功能 打开窗口只是主控端进入某个处理上下文
有心跳就是正常软件 心跳只是保活机制,不能证明合法
枚举名越多越难检测 枚举名反而能帮助建立协议字典
只看网络流量就够了 需要关联主机、内存、注册表、构建链和 UI 行为
DllSendData 只是普通结构体 它关联插件名称、位数、版本和数据大小,是插件调度的重要线索

# 23. 本课小结

本课从源码角度拆解了协议与命令分发机制:

  1. 这套源码大量使用“一字节命令 + 参数数据”的协议模型。
  2. 全局 TOKEN_* 决定主控端基础事件和功能入口。
  3. CMainFrame::ProcessReceiveComplete 是完整业务包进入业务层的核心分发点。
  4. ClientContext::m_Dialog 决定后续数据是否交给已打开的功能窗口。
  5. 功能窗口内部再使用局部 COMMAND_* 和局部 TOKEN_* 解释数据。
  6. 文件管理窗口体现了最典型的命令往返:COMMAND_LIST_FILES 请求,TOKEN_FILE_LIST 返回。
  7. 插件发送链路中,TOKEN_GETVERSIONTOKEN_SENDLLCOMMAND_SENDLL 构成版本探测和插件下发的协议边界。
  8. 协议的主要安全问题是认证不足、完整性不足、版本协商弱、命令授权弱和参数边界不足。
  9. 企业检测应把协议首字节、心跳、插件数据、文件行为、内存行为和构建链信号关联起来。

下一课会继续讲“插件化设计与模块加载总览”:主控端为什么要按需发送插件,被控端如何按版本和位数请求插件,以及 DllSendData 在插件链路中的完整角色。

# 24. 合法练习题

  1. 在不运行程序的前提下,整理 主控\Quick\macros.henum MAIN 的 token 分类表。
  2. 画出 TOKEN_DRIVE_LIST -> WM_OPENMANAGERDIALOG -> CFileManagerDlg 的源码追踪路径。
  3. FileManagerDlg.h 中挑 10 个局部命令,按“枚举、传输、修改、删除、搜索、状态”分类。
  4. 阅读 CFileManagerDlg::GetRemoteFileList,说明为什么第 1 字节是命令号,后面是路径参数。
  5. 阅读 CFileManagerDlg::OnReceiveComplete,列出 5 个返回 token 和它们对应的 UI 行为。
  6. 设计一个合法远程管理协议头,至少包含版本、长度、命令类型、序列号和完整性字段。
  7. 写一张企业检测关联表,把 TOKEN_HEARTBEATTOKEN_SENDLLCOMMAND_SENDLL 分别关联到主机、网络和内存检测点。
  8. 思考如果主控侧和插件侧 TOKEN_* 枚举不同步,会出现哪些稳定性和安全问题。
  9. 选择一个你熟悉的合法运维工具,列出它应该具备哪些命令授权和操作日志能力。
  10. 对照本课内容,写出 5 条“未知命令包应被拒绝并告警”的原因。

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

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

社区交流

讨论与留言

前往 GitHub Issues →

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

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