24 / 52 主插件与远程能力

第 24 课:主插件:系统管理

# 第 24 课:主插件:系统管理

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

本课定位:防御分析、源码阅读、资产枚举能力识别和企业检测工程。 本课只讲系统管理模块的结构、枚举对象和防御价值,不提供破坏系统、注入进程、删除服务或篡改配置的可复用步骤。

第 22 课讲了登录模块:连接建立后发送主机画像、等待激活,并进入插件请求链。本课进入一个典型功能插件:主插件\系统管理

系统管理模块的能力面很宽,既包含相对正常的资产枚举,也包含高风险的系统控制能力。为了保持学习边界,本课重点讲“它能枚举什么、数据来自哪里、这些能力在企业安全中有什么价值、应该如何检测和加固”。涉及终止进程、删除文件、服务控制、任务创建、注入等高危分支,只做防御性说明。

本课主线是:

系统管理入口
  -> 创建 CMachineManager
  -> OnReceive 按命令分发
  -> 进程 / 窗口 / 网络 / 软件 / 服务 / 任务 / hosts 枚举
  -> 打包为 TOKEN_MACHINE_* 返回
  -> 企业侧关联资产、连接、持久化和异常操作
1
2
3
4
5
6

# 1. 本课学习目标

学完本课,你应该能回答这些问题:

问题 本课回答
系统管理模块在工程中做什么 通过 CMachineManager 接收命令并返回系统对象列表
它能枚举哪些对象 进程、窗口、网络连接、软件、浏览记录、收藏夹、服务、任务、hosts
OnReceive 为什么重要 它把命令字分发到具体枚举或控制函数
进程列表有什么价值 可识别异常进程、路径、用户、位数、内存和窗口标题
网络连接列表有什么价值 可把 TCP/UDP 连接和 PID、进程名关联
服务、任务、hosts 为什么重要 它们常用于合法运维,也常出现在持久化和劫持排查中
企业如何检测 关联资产枚举、权限变化、网络 PID、服务任务 hosts 和异常操作

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

源码文件 关键位置 本课关注点
主插件\系统管理\系统管理\系统管理.cpp 6-40 行 plugInfo、入口连接、创建 CMachineManager
主插件\系统管理\系统管理\MachineManager.h 23-77 行 命令字和返回 token
主插件\系统管理\系统管理\MachineManager.h 88-150 行 CMachineManager 类和功能函数
主插件\系统管理\系统管理\MachineManager.cpp 36-83 行 构造函数和 OnReceive 分发
主插件\系统管理\系统管理\MachineManager.cpp 413-704 行 进程列表采集
主插件\系统管理\系统管理\MachineManager.cpp 704-725 行 窗口列表采集与发送
主插件\系统管理\系统管理\MachineManager.cpp 725-807、1434-1800 行 网络连接列表
主插件\系统管理\系统管理\MachineManager.cpp 807-1036 行 软件列表
主插件\系统管理\系统管理\MachineManager.cpp 1206-1396 行 服务列表
主插件\系统管理\系统管理\MachineManager.cpp 1396-1435 行 hosts 文件读取
主插件\系统管理\系统管理\MachineManager.cpp 2085-2174 行 计划任务枚举
主插件\系统管理\系统管理\MachineManager.cpp 2191-2352 行 计划任务创建、运行、停止、删除,高风险分支只做防御说明

# 3. 系统管理模块能力面

先看能力面。

系统管理模块能力面

系统管理 插件不是只做一个功能,它更像一个远程资产和系统对象管理器。它能读取大量本机对象:

对象 数据来源 企业安全价值
进程 Toolhelp 快照、PSAPI、进程 token 发现异常进程、未知路径、异常权限
窗口 EnumWindows、窗口标题、进程 ID 观察交互程序、前台应用、窗口隐藏情况
网络连接 iphlpapi.dll、TCP/UDP 表 关联进程和外联连接
软件列表 卸载注册表项 资产盘点、脆弱软件识别
服务 SCM 服务控制管理器 发现异常服务、驱动服务、启动方式
计划任务 Task Scheduler COM 接口 发现计划任务持久化
hosts 系统 hosts 文件 发现域名劫持或异常解析

其中“枚举”本身有合法用途,例如资产盘点和远程运维。但如果这些能力被未授权使用,就会变成隐蔽侦察和系统控制能力。

# 4. 插件入口:创建 CMachineManager

先看入口文件。

// 源码位置:主插件\系统管理\系统管理\系统管理.cpp:6-40
// 防御分析注释:
// 1. 这段是系统管理插件的启动入口,不直接枚举系统对象,只负责建立通信和创建管理器。
// 2. plugInfo 是构建期写入或运行期读取的连接配置,防御侧可把它理解为“插件上线参数”。
// 3. MainThread 根据 IsTcp 选择 CTcpSocket 或 CUdpSocket,说明同一套功能可复用不同传输通道。
// 4. Connect 成功后才创建 CMachineManager,因此“网络连接成功”是所有系统管理命令的前置条件。
// 5. run_event_loop 持续接收主控命令;真正的系统枚举和控制逻辑在 CMachineManager::OnReceive 中触发。

struct plugInfo
{
    char mark[30];          // 配置定位标记,常用于在二进制数据中寻找配置块。
    TCHAR szAddress[255];   // 远端地址;企业排查时应与外联目的地址、进程路径一起看。
    DWORD szPort;           // 远端端口;单独看端口意义有限,要结合协议和进程上下文。
    BOOL IsTcp;             // 1 表示 TCP,0 表示 UDP;影响后续 socketClient 的具体类型。
    BOOL RunDllEntryProc;   // DLL 入口运行模式标志;为 TRUE 时结束流程会调用 ExitProcess。
} MyInfo =
{
    "plugmark",
    _T("127.0.0.1"),
    6666,
    1,
    0,
};

DWORD WINAPI MainThread(LPVOID dllMainThread)
{
    ISocketBase* socketClient;

    // 触发点 1:根据配置选择通信对象。
    // 防御侧观察:同一个插件功能不绑定固定协议,检测时不能只写死 TCP 或固定端口。
    if (MyInfo.IsTcp == 1)
        socketClient = new CTcpSocket();
    else
        socketClient = new CUdpSocket();

    // 触发点 2:连接成功后创建 CMachineManager。
    // CMachineManager 的构造函数会发送模块 token,随后 OnReceive 接收主控命令。
    if (socketClient->Connect(MyInfo.szAddress, MyInfo.szPort))
    {
        CMachineManager manager(socketClient);
        socketClient->run_event_loop();
    }

    // 清理网络对象。若以 DLL 入口方式运行,RunDllEntryProc 会影响退出行为。
    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
41
42
43
44
45
46
47
48
49

这段代码说明系统管理插件本身也复用了 HPSocket 网络库,并通过 CManager 派生类接入统一收发框架。

# 5. 命令字与能力边界

MachineManager.h 定义了大量命令字。为了防御学习,我们把它分为两类:枚举类命令和控制类命令。

// 源码位置:主插件\系统管理\系统管理\MachineManager.h:23-77
// 防御分析注释:
// 1. COMMAND_* 是主控发给插件的动作请求,TOKEN_* 是插件返回给主控的数据类型。
// 2. COMMAND_MACHINE_* 多数是资产枚举命令,常见于运维工具,也常见于入侵后的主机侦察。
// 3. COMMAND_PROCESS_*、COMMAND_SERVICE_*、COMMAND_TASK*、COMMAND_INJECT 是控制类能力,风险明显更高。
// 4. 企业侧可把“命令字序列 + 进程 API + 网络会话”作为协议行为特征,而不是只看单点 API。

enum MACHINE
{
    // ===== 资产枚举类:读取系统对象,通常会返回 TOKEN_MACHINE_* 数据包 =====
    COMMAND_MACHINE_PROCESS,
    COMMAND_MACHINE_WINDOWS,
    COMMAND_MACHINE_NETSTATE,
    COMMAND_MACHINE_SOFTWARE,
    COMMAND_MACHINE_HTML,
    COMMAND_MACHINE_FAVORITES,
    COMMAND_MACHINE_WIN32SERVICE,
    COMMAND_MACHINE_DRIVERSERVICE,
    COMMAND_MACHINE_TASK,
    COMMAND_MACHINE_HOSTS,

    // ===== 控制类:会影响应用、窗口、进程、hosts、服务或计划任务状态 =====
    COMMAND_APPUNINSTALL,
    COMMAND_WINDOW_OPERATE,
    COMMAND_WINDOW_CLOSE,
    COMMAND_PROCESS_KILL,
    COMMAND_PROCESS_KILLDEL,
    COMMAND_PROCESS_DEL,
    COMMAND_PROCESS_FREEZING,
    COMMAND_PROCESS_THAW,
    COMMAND_HOSTS_SET,

    COMMAND_SERVICE_LIST_WIN32,
    COMMAND_SERVICE_LIST_DRIVER,
    COMMAND_DELETESERVERICE,
    COMMAND_STARTSERVERICE,
    COMMAND_STOPSERVERICE,
    COMMAND_PAUSESERVERICE,
    COMMAND_CONTINUESERVERICE,

    COMMAND_TASKCREAT,
    COMMAND_TASKDEL,
    COMMAND_TASKSTOP,
    COMMAND_TASKSTART,

    // ===== 注入类:属于高风险行为,本课只作为检测面说明,不展开操作细节 =====
    COMMAND_INJECT,

    // ===== 回包 token:用于标识返回数据是什么列表或结果 =====
    TOKEN_MACHINE_PROCESS,
    TOKEN_MACHINE_WINDOWS,
    TOKEN_MACHINE_NETSTATE,
    TOKEN_MACHINE_SOFTWARE,
    TOKEN_MACHINE_HTML,
    TOKEN_MACHINE_FAVORITES,
    TOKEN_MACHINE_WIN32SERVICE,
    TOKEN_MACHINE_DRIVERSERVICE,
    TOKEN_MACHINE_HOSTS,
    TOKEN_MACHINE_SERVICE_LIST,
    TOKEN_MACHINE_TASKLIST,
    TOKEN_MACHINE_MSG,
};
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
51
52
53
54
55
56
57
58
59
60
61
62

这段枚举对企业检测很有帮助。它说明该模块不是单纯资产盘点工具,还具备窗口控制、进程结束、服务操作、任务操作、hosts 修改和注入相关能力。专栏后面只展开枚举类逻辑,高风险控制分支只用于风险识别。

# 6. OnReceive:命令分发中心

OnReceive 命令分发

OnReceive 是系统管理模块的命令分发中心。

// 源码位置:主插件\系统管理\系统管理\MachineManager.cpp:36-83
// 防御分析注释:
// 1. 构造函数先发送 TOKEN_SYSINFOLIST,提示主控侧“系统管理模块已就绪”。
// 2. OnReceive 是远端命令进入本模块后的第一站,lpBuffer[0] 就是命令字。
// 3. 枚举类命令会调用 Send*List,读取本机对象并通过 Send 回传列表。
// 4. 控制类命令也走这个分发中心,因此日志里看到 OnReceive 后要继续追踪具体 case。
// 5. 高风险控制类命令在本课不展开,只用来帮助读者建立风险边界。

CMachineManager::CMachineManager(ISocketBase* pClient) : CManager(pClient)
{
    m_buser = FALSE;

    // 构造阶段主动回一个模块 token。
    // 防御侧价值:这类短包可作为协议识别线索,早于具体系统枚举行为出现。
    BYTE lpBuffer = TOKEN_SYSINFOLIST;
    Send((LPBYTE)&lpBuffer, 1);

    lpFUBuffer = NULL;
    dwFUOffset = 1;
    m_buser = TRUE;
    m_pService = NULL;
}

void CMachineManager::OnReceive(LPBYTE lpBuffer, UINT nSize)
{
    // 心跳包不触发系统操作,直接返回,避免误判为有效命令。
    if (lpBuffer[0] == TOKEN_HEARTBEAT) return;

    // 所有分支都由网络数据首字节触发。
    // 如果企业侧能在网关或主机侧还原命令字,就能把“请求了什么对象”记录下来。
    switch (lpBuffer[0])
    {
    case COMMAND_MACHINE_PROCESS:       SendProcessList(); break;              // 进程列表:PID、路径、用户、位数、窗口等。
    case COMMAND_MACHINE_WINDOWS:       SendWindowsList(); break;              // 窗口列表:窗口标题和 PID,可能包含敏感文档/网页标题。
    case COMMAND_MACHINE_NETSTATE:      SendNetStateList(); break;             // 网络连接:把 TCP/UDP 连接关联到进程。
    case COMMAND_MACHINE_SOFTWARE:      SendSoftWareList(); break;             // 软件列表:卸载注册表项,可能暴露安全产品和脆弱软件。
    case COMMAND_MACHINE_HTML:          SendIEHistoryList(); break;            // 浏览历史:涉及隐私和业务访问轨迹。
    case COMMAND_MACHINE_FAVORITES:     SendFavoritesUrlList(); break;         // 收藏夹:涉及用户常用业务系统。
    case COMMAND_MACHINE_WIN32SERVICE:  SendServicesList(SERVICE_WIN32); break; // 普通服务:排查持久化和异常服务。
    case COMMAND_MACHINE_DRIVERSERVICE: SendServicesList(SERVICE_KERNEL_DRIVER); break; // 驱动服务:风险更高,需要重点关注签名和路径。
    case COMMAND_MACHINE_TASK:          GetRoot(); break;                      // 计划任务:登录、启动和定时触发器是持久化重点。
    case COMMAND_MACHINE_HOSTS:         SendHostsList(); break;                // hosts:排查域名劫持和异常解析。

    // 防御分析说明:
    // 后续还存在窗口控制、进程结束、服务控制、计划任务控制、注入等分支。
    // 这些属于高风险控制能力,本课不展开操作步骤。
    }
}
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

阅读这种分发函数时,新手要抓住两件事:

  1. lpBuffer[0] 是命令字。
  2. 每个命令字会进入一个具体功能函数。

检测时也是一样:如果能看到命令字序列,就能推断远端正在请求什么系统对象。

# 7. 资产枚举数据来源

系统管理模块的枚举函数最终都做一件事:从 Windows API 或系统位置取数据,写入缓冲区,再通过 Send 返回。

资产枚举数据来源

通用模式如下:

收到 COMMAND_MACHINE_*
  -> 调用 get*List
  -> LocalAlloc / LocalReAlloc 维护缓冲区
  -> lpBuffer[0] 写入 TOKEN_MACHINE_*
  -> 追加对象字段
  -> Send 返回
1
2
3
4
5
6

这个模式在进程、窗口、网络、软件、服务、任务和 hosts 中反复出现。

# 8. 进程列表:资产盘点与异常进程识别

进程列表是系统管理里最基础、也最有价值的枚举。

// 源码位置:主插件\系统管理\系统管理\MachineManager.cpp:413-420
// 防御分析注释:
// 1. SendProcessList 是 COMMAND_MACHINE_PROCESS 的落点。
// 2. getProcessList 负责真正采集,SendProcessList 只负责发送和释放缓冲区。
// 3. 返回包首字节是 TOKEN_MACHINE_PROCESS,后续字段由 getProcessList 逐项追加。
// 4. 企业侧应把进程列表和网络连接、服务、任务关联,而不是孤立查看。

void CMachineManager::SendProcessList()
{
    // 采集失败返回 NULL;此时没有回包,可能表现为主控界面列表为空。
    LPBYTE lpBuffer = getProcessList();
    if (lpBuffer == NULL)
        return;

    // LocalSize(lpBuffer) 依赖前面 LocalAlloc/LocalReAlloc 分配出的缓冲区大小。
    // 防御侧不要只看 Send 调用,还要回溯缓冲区首字节 token 和字段构造逻辑。
    Send((LPBYTE)lpBuffer, (int)LocalSize(lpBuffer));
    LocalFree(lpBuffer);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

getProcessList 使用 Toolhelp 快照遍历进程,并采集进程路径、用户、位数、窗口标题、优先级、线程数、内存和文件大小等字段。

// 源码位置:主插件\系统管理\系统管理\MachineManager.cpp:510-640
// 防御分析注释:
// 1. CreateToolhelp32Snapshot 是进程枚举入口,属于主机侦察常见 API。
// 2. DebugPrivilege(SE_DEBUG_NAME, TRUE) 尝试启用调试权限,说明它希望读取更多进程信息。
// 3. OpenProcess + GetProcessImageFileName 获取进程路径;访问失败时仍会继续处理下一项。
// 4. GetProcessUserName 获取进程所属用户,便于判断进程是否运行在异常账户或高权限上下文。
// 5. Is64BitPorcess 标记进程位数,后续注入、插件加载和兼容性判断都会用到这个信息。
// 6. LocalReAlloc 动态扩展返回缓冲区;如果字段很多,回包大小会随进程数量增长。

LPBYTE CMachineManager::getProcessList()
{
    HANDLE hSnapshot = NULL;
    HANDLE hProcess = NULL;
    PROCESSENTRY32 pe32 = { 0 };
    TCHAR strProcessName[MAX_PATH] = { 0 };
    TCHAR strProcessUser[MAX_PATH] = { 0 };
    TCHAR szImagePath[MAX_PATH] = { 0 };

    // 触发点 1:启用 SeDebugPrivilege。
    // 合法工具应说明为什么需要该权限;异常进程临时启用该权限应重点关注。
    DebugPrivilege(SE_DEBUG_NAME, TRUE);

    // 触发点 2:创建进程快照,后续 Process32First/Process32Next 遍历所有进程。
    hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    if (hSnapshot == INVALID_HANDLE_VALUE)
        return NULL;

    pe32.dwSize = sizeof(PROCESSENTRY32);
    LPBYTE lpBuffer = (LPBYTE)LocalAlloc(LPTR, 1024);

    // 回包类型。主控端收到后知道这是“进程列表”。
    lpBuffer[0] = TOKEN_MACHINE_PROCESS;
    DWORD dwOffset = 1;

    // 先枚举窗口,后面可把窗口标题和进程 PID 关联。
    // 这解释了为什么进程列表里可能出现窗口标题字段。
    EnumWindows(lpEnumFunc, NULL);

    if (Process32First(hSnapshot, &pe32))
    {
        do
        {
            hProcess = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ,
                FALSE, pe32.th32ProcessID);

            if ((pe32.th32ProcessID != 0) &&
                (pe32.th32ProcessID != 4) &&
                (pe32.th32ProcessID != 8))
            {
                // 尝试把 NT 设备路径转换为 DOS 路径。
                // 防御侧更容易基于 DOS 路径做目录分层、签名校验和基线比对。
                if (0 == GetProcessImageFileName(hProcess, szImagePath, MAX_PATH))
                    lstrcpy(strProcessName, _T("---"));
                else
                    DosPathToNtPath(szImagePath, strProcessName);

                // 采集进程所属用户。异常用户、服务账户或管理员上下文都需要单独标记。
                if (!GetProcessUserName(hProcess, strProcessUser))
                    lstrcpy(strProcessUser, _T("-"));

                // 采集进程位数。x86 插件运行在 x64 系统时,后续访问和注入逻辑容易受 WoW64 影响。
                bool is64 = Is64BitPorcess(pe32.th32ProcessID);

                // 后续代码把 PID、位数、窗口标题、进程名、路径、用户、
                // 内存、文件大小等字段依次写入 lpBuffer。
            }

            CloseHandle(hProcess);
        } while (Process32Next(hSnapshot, &pe32));
    }

    // 枚举结束后关闭调试权限。检测侧可关注权限启用和关闭之间发生了哪些 API 调用。
    DebugPrivilege(SE_DEBUG_NAME, FALSE);
    CloseHandle(hSnapshot);
    return lpBuffer;
}
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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76

企业侧可以从进程列表中关注:

字段 检测价值
PID 和网络连接、窗口、服务关联
进程名 识别伪装、双扩展名、异常命名
路径 发现用户目录、临时目录、ProgramData 中的异常程序
用户 判断是否以高权限或异常账户运行
位数 关联 x86/x64 插件和注入风险
窗口标题 判断是否有隐藏或异常交互进程
内存/文件大小 粗略识别异常占用或异常小文件

# 9. 窗口列表:交互面和可见性

窗口列表通过 EnumWindows 枚举可见窗口。

// 源码位置:主插件\系统管理\系统管理\MachineManager.cpp:704-725
// 防御分析注释:
// 1. getWindowsList 通过 EnumWindowsProc 收集窗口信息。
// 2. 返回包首字节是 TOKEN_MACHINE_WINDOWS。
// 3. 窗口标题可以帮助判断用户正在使用什么程序,也属于敏感信息。

LPBYTE CMachineManager::getWindowsList()
{
    LPBYTE lpBuffer = NULL;
    EnumWindows((WNDENUMPROC)EnumWindowsProc, (LPARAM)&lpBuffer);
    lpBuffer[0] = TOKEN_MACHINE_WINDOWS;
    return lpBuffer;
}

void CMachineManager::SendWindowsList()
{
    LPBYTE lpBuffer = getWindowsList();
    if (lpBuffer == NULL)
        return;

    Send((LPBYTE)lpBuffer, (int)LocalSize(lpBuffer));
    LocalFree(lpBuffer);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

窗口列表适合辅助判断:

观察点 价值
可见窗口标题 用户交互环境
窗口对应 PID 和进程列表关联
窗口控制命令存在 说明具备远程影响桌面行为的能力

合法工具采集窗口标题时应谨慎,因为窗口标题可能包含文档名、聊天对象、网页标题等敏感信息。

# 10. 进程与网络连接关联

网络连接列表的核心价值,是把连接和进程关联起来。

进程与网络连接关联

getNetStateList 加载 iphlpapi.dll,读取 TCP/UDP 表,并把 PID 映射到进程名。

// 源码位置:主插件\系统管理\系统管理\MachineManager.cpp:1434-1515
// 防御分析注释:
// 1. getNetStateList 是 COMMAND_MACHINE_NETSTATE 的落点。
// 2. lpBuffer[0] = TOKEN_MACHINE_NETSTATE,说明返回网络连接列表。
// 3. 通过 iphlpapi.dll 动态解析 TCP/UDP 扩展表函数,兼容不同 Windows 版本。
// 4. 每条连接都会包含进程名、PID、协议、本地地址、远程地址和状态。
// 5. 这是企业排查异常外联最有价值的数据之一,因为它能把“连接”落到“进程”上。

LPBYTE CMachineManager::getNetStateList()
{
    LPBYTE lpBuffer = (LPBYTE)LocalAlloc(LPTR, 1024);
    lpBuffer[0] = TOKEN_MACHINE_NETSTATE;
    DWORD dwOffset = 1;

    // 动态加载 iphlpapi.dll。
    // 防御侧可以把 LoadLibrary("iphlpapi.dll") 与后续 GetProcAddress、网络表枚举关联起来看。
    HMODULE hModule = ::LoadLibrary(_T("iphlpapi.dll"));

    // 解析旧式扩展表函数,用于获取 TCP 连接与进程 ID 的对应关系。
    pAllocateAndGetTcpExTableFromStack =
        (PFNAllocateAndGetTcpExTableFromStack)::GetProcAddress(
            hModule, "AllocateAndGetTcpExTableFromStack");

    // 解析 UDP 扩展表函数。UDP 没有连接状态,但能看到本地监听端口与 PID。
    pAllocateAndGetUdpExTableFromStack =
        (PFNAllocateAndGetUdpExTableFromStack)::GetProcAddress(
            hModule, "AllocateAndGetUdpExTableFromStack");

    // 后续代码读取 TCP/UDP 表,并逐条写入 lpBuffer。
}
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

TCP/UDP 条目写入时,会包含进程名、PID、协议和地址。

// 源码位置:主插件\系统管理\系统管理\MachineManager.cpp:1639-1795
// 防御分析注释:
// 1. TCP 条目会记录连接状态,例如 LISTEN、ESTAB、TIME_WAIT 等。
// 2. UDP 条目没有远端地址时使用 *.*.*.*:*,不能因此忽略它的本地监听价值。
// 3. ProcessPidToName 把 PID 转成进程名,用于和进程列表、服务列表、任务列表关联。
// 4. 每个字段按顺序 memcpy 到 lpBuffer,主控端必须按同样顺序解包。
// 5. 企业侧应把异常远程地址和进程路径、用户、签名状态一起判断。

TCHAR strProcessName[100] = { 0 };
TCHAR* strType = _T("[TCP]");

// 字段 1:进程名。它来自 PID 映射,不是网络表原生字段。
lstrcpy(strProcessName,
    ProcessPidToName(hProcessSnap, pTcpTable_Vista->table[i].dwProcessId, szProcessName));

memcpy(lpBuffer + dwOffset, strProcessName, lstrlen(strProcessName) * sizeof(TCHAR) + 2);
dwOffset += lstrlen(strProcessName) * sizeof(TCHAR) + 2;

// 字段 2:PID。后续可以用它回查进程路径、用户、服务名和窗口标题。
memcpy(lpBuffer + dwOffset, &pTcpTable_Vista->table[i].dwProcessId, sizeof(DWORD) + 2);
dwOffset += sizeof(DWORD) + 2;

// 字段 3:协议类型。TCP/UDP 处理方式不同,检测逻辑也应分开建模。
memcpy(lpBuffer + dwOffset, strType, lstrlen(strType) * sizeof(TCHAR) + 2);
dwOffset += lstrlen(strType) * sizeof(TCHAR) + 2;

// 字段 4:本地地址和端口。用于发现异常监听或本机代理行为。
memcpy(lpBuffer + dwOffset, szLocalAddr, lstrlen(szLocalAddr) * sizeof(TCHAR) + 2);
dwOffset += lstrlen(szLocalAddr) * sizeof(TCHAR) + 2;

// 字段 5:远程地址和端口。用于外联检测,但需要结合进程上下文降低误报。
memcpy(lpBuffer + dwOffset, szRemoteAddr, lstrlen(szRemoteAddr) * sizeof(TCHAR) + 2);
dwOffset += lstrlen(szRemoteAddr) * sizeof(TCHAR) + 2;

// 字段 6:连接状态。ESTABLISHED、LISTEN、TIME_WAIT 的风险含义不同。
memcpy(lpBuffer + dwOffset, strState, lstrlen(strState) * sizeof(TCHAR) + 2);
dwOffset += lstrlen(strState) * sizeof(TCHAR) + 2;
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

网络连接检测要避免只看 IP。更稳定的判断是:

远程地址
  + PID
  + 进程路径
  + 进程用户
  + 进程位数
  + 启动来源
  + 服务/任务/注册表线索
1
2
3
4
5
6
7

# 11. 软件列表:资产和脆弱面

软件列表来自卸载注册表项。

// 源码位置:主插件\系统管理\系统管理\MachineManager.cpp:807-930
// 防御分析注释:
// 1. getSoftWareList 读取 HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall。
// 2. 采集 DisplayName、Publisher、DisplayVersion、InstallDate、UninstallString。
// 3. 64 位系统还会关注 WOW64 视图差异。
// 4. 软件列表可用于资产盘点,也可能暴露脆弱软件和安全产品信息。

LPBYTE CMachineManager::getSoftWareList()
{
    LPBYTE lpBuffer = NULL;
    DWORD dwOffset = 1;
    TCHAR regDisplayName[MAX_LEG] = { 0 };
    TCHAR regPublisher[MAX_LEG] = { 0 };
    TCHAR regDisplayVersion[MAX_LEG] = { 0 };
    TCHAR regInstallDate[MAX_LEG] = { 0 };
    TCHAR regUninstallString[MAX_LEG] = { 0 };

    lpBuffer = (LPBYTE)LocalAlloc(LPTR, 1024);
    lpBuffer[0] = TOKEN_MACHINE_SOFTWARE;

    if (RegOpenKeyEx(HKEY_LOCAL_MACHINE,
        _T("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall"),
        NULL, KEY_READ, &hKey) == ERROR_SUCCESS)
    {
        RegQueryInfoKey(hKey, NULL, NULL, NULL, &dwRegNum,
            NULL, NULL, NULL, NULL, NULL, NULL, NULL);

        for (int i = 0; i < (int)dwRegNum; i++)
        {
            RegEnumKeyEx(hKey, i, regBufferName, &dwRegSize, NULL, NULL, NULL, NULL);
            RegOpenKeyEx(hKey, regBufferName, NULL, KEY_READ, &hSubKey);

            RegQueryValueEx(hSubKey, _T("DisplayName"), 0, &dwType, (LPBYTE)regDisplayName, &dwRegSize);
            RegQueryValueEx(hSubKey, _T("Publisher"), 0, &dwType, (LPBYTE)regPublisher, &dwRegSize);
            RegQueryValueEx(hSubKey, _T("DisplayVersion"), 0, &dwType, (LPBYTE)regDisplayVersion, &dwRegSize);
            RegQueryValueEx(hSubKey, _T("InstallDate"), 0, &dwType, (LPBYTE)regInstallDate, &dwRegSize);
            RegQueryValueEx(hSubKey, _T("UninstallString"), 0, &dwType, (LPBYTE)regUninstallString, &dwRegSize);

            // 后续代码把这些字段写入 lpBuffer。
        }
    }

    return lpBuffer;
}
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

软件列表在企业侧的价值:

字段 用途
DisplayName 资产名称
Publisher 供应商归类
DisplayVersion 漏洞影响版本判断
InstallDate 变更时间线
UninstallString 安装路径、卸载命令线索

# 12. 服务列表:服务和驱动服务

服务枚举通过 SCM 完成。

// 源码位置:主插件\系统管理\系统管理\MachineManager.cpp:1206-1396
// 防御分析注释:
// 1. SendServicesList 可传 SERVICE_WIN32 或 SERVICE_KERNEL_DRIVER,分别枚举普通服务和驱动服务。
// 2. getServiceList 调用 OpenSCManager 和 EnumServicesStatus,这是读取 SCM 数据库的核心路径。
// 3. 每个服务会采集显示名、描述、状态、启动类型、可执行路径等信息。
// 4. 服务和驱动服务是持久化排查重点,驱动服务还涉及内核态风险。
// 5. 防御侧应重点看服务路径、启动类型、签名、创建时间和关联进程。

void CMachineManager::SendServicesList(DWORD dwScType)
{
    LPBYTE lpBuffer = getServiceList(dwScType);
    if (lpBuffer == NULL)
        return;

    Send((LPBYTE)lpBuffer, (int)(LocalSize(lpBuffer)));
    LocalFree(lpBuffer);
}

LPBYTE CMachineManager::getServiceList(DWORD dwScType)
{
    // 服务枚举前启用调试权限。并非所有服务查询都需要该权限,异常进程启用时要记录。
    EnablePrivilege(SE_DEBUG_NAME, TRUE);
    dwServiceType = dwScType;

    // 打开 SCM,仅申请连接和枚举权限。
    // 如果后续出现 SERVICE_START/STOP/DELETE 等权限,则风险更高。
    SC_HANDLE sc = OpenSCManager(NULL, NULL,
        SC_MANAGER_CONNECT | SC_MANAGER_ENUMERATE_SERVICE);

    // 第一次调用用于拿到所需缓冲区大小。
    EnumServicesStatus(sc, dwServiceType, SERVICE_STATE_ALL,
        lpServices, size, &size, &ret, NULL);

    // 第二次调用才真正拿到服务数组。
    lpServices = (LPENUM_SERVICE_STATUS)LocalAlloc(LPTR, size);
    EnumServicesStatus(sc, dwServiceType, SERVICE_STATE_ALL,
        lpServices, size, &size, &ret, NULL);

    LPBYTE lpBuffer = (LPBYTE)LocalAlloc(LPTR, 1024 * sizeof(TCHAR));
    lpBuffer[0] = TOKEN_MACHINE_SERVICE_LIST;

    for (DWORD i = 0; i < ret; i++)
    {
        // 打开单个服务并读取配置。
        // QueryServiceConfig 能拿到二进制路径和启动类型,是排查异常服务的关键字段。
        SC_HANDLE sh = OpenService(sc, lpServices[i].lpServiceName, SERVICE_QUERY_CONFIG);
        QueryServiceConfig2(sh, SERVICE_CONFIG_DESCRIPTION, (LPBYTE)szInfo, dwSize, &dwSize);
        QueryServiceConfig(sh, (LPQUERY_SERVICE_CONFIG)szInfo, dwSize, &dwSize);

        // 后续代码写入服务名、显示名、描述、状态、启动类型、路径等字段。
    }

    EnablePrivilege(SE_DEBUG_NAME, FALSE);
    return lpBuffer;
}
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
51
52
53
54
55

服务列表的检测重点:

观察点 异常特征
服务路径 用户目录、临时目录、无签名路径
启动类型 异常 Auto Start 或 Boot Start
服务描述 空描述、伪装系统描述
驱动服务 非预期驱动、异常驱动路径
创建时间 与入侵时间线吻合

# 13. 计划任务与 hosts

服务、任务和 hosts 经常一起用于排查持久化或流量劫持。

服务、任务与 hosts 线索

hosts 读取逻辑如下:

// 源码位置:主插件\系统管理\系统管理\MachineManager.cpp:1396-1435
// 防御分析注释:
// 1. 根据 Wow64 状态选择 sysnative 或 system32 下的 hosts 路径,避免 32 位进程访问被重定向。
// 2. SetFileAttributes 将 hosts 文件属性改为普通属性,这属于敏感文件操作。
// 3. OPEN_ALWAYS 在文件不存在时会创建文件;对系统配置文件使用该模式需要谨慎审计。
// 4. 读取 hosts 内容后以 TOKEN_MACHINE_HOSTS 返回。
// 5. 企业侧应关注 hosts 文件的异常映射、权限变化和属性变化。

void CMachineManager::SendHostsList()
{
    TCHAR szHostsFile[MAX_PATH] = { 0 };
    BOOL bIsWow64 = FALSE;

    // 先定位 Windows 目录,再根据进程位数选择真实系统目录。
    GetWindowsDirectory(szHostsFile, sizeof(szHostsFile));
    ::IsWow64Process(::GetCurrentProcess(), &bIsWow64);
    if (bIsWow64)
        lstrcat(szHostsFile, _T("\\sysnative\\drivers\\etc\\hosts"));
    else
        lstrcat(szHostsFile, _T("\\system32\\drivers\\etc\\hosts"));

    // 这里会修改 hosts 文件属性。即使后面只是读取,这一步本身也应进入审计。
    SetFileAttributes(szHostsFile, FILE_ATTRIBUTE_NORMAL);
    HANDLE hFile = CreateFile(szHostsFile, GENERIC_READ,
        FILE_SHARE_READ, NULL, OPEN_ALWAYS, 0, NULL);

    // 把 hosts 文件原始内容整体读入回包。
    // 防御侧要特别关注新增的域名映射、内网域名指向外部地址、更新服务器被重定向等情况。
    DWORD dwSize = GetFileSize(hFile, NULL);
    LPBYTE lpBuffer = (LPBYTE)LocalAlloc(LPTR, dwSize + 2);
    ReadFile(hFile, lpBuffer + 1, dwSize, &dwRead, NULL);
    CloseHandle(hFile);

    lpBuffer[0] = TOKEN_MACHINE_HOSTS;
    Send((LPBYTE)lpBuffer, (UINT)LocalSize(lpBuffer));
    LocalFree(lpBuffer);
}
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

计划任务通过 Task Scheduler COM 接口枚举。

// 源码位置:主插件\系统管理\系统管理\MachineManager.cpp:2085-2174
// 防御分析注释:
// 1. GetRoot 是 COMMAND_MACHINE_TASK 的落点,负责初始化 COM 和 TaskScheduler。
// 2. Connect 后从根目录开始枚举任务文件夹和任务。
// 3. 返回 TOKEN_MACHINE_TASKLIST,后续字段由 GetFolderAll 递归追加。
// 4. 计划任务名称、路径、执行文件、上次运行和下次运行时间都是持久化排查线索。
// 5. 枚举失败时也会发送空 token,主控端据此知道任务模块没有拿到数据。

PBYTE CMachineManager::GetRoot()
{
    bool binit = false;
    lpList = NULL;
    offset = 0;
    nBufferSize = 0;

    // 触发点 1:初始化 COM。计划任务 2.0 API 通过 COM 接口访问。
    HRESULT hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);
    if (SUCCEEDED(hr))
    {
        // 触发点 2:设置 COM 安全上下文。
        // RPC_C_IMP_LEVEL_IMPERSONATE 表示服务端可在一定范围内模拟客户端身份。
        hr = CoInitializeSecurity(NULL, -1, NULL, NULL,
            RPC_C_AUTHN_LEVEL_PKT_PRIVACY,
            RPC_C_IMP_LEVEL_IMPERSONATE, NULL, 0, NULL);

        // 触发点 3:创建 Task Scheduler 服务对象。
        hr = CoCreateInstance(CLSID_TaskScheduler, NULL,
            CLSCTX_INPROC_SERVER, IID_ITaskService, (void**)&m_pService);

        if (m_pService)
        {
            // 触发点 4:连接本机计划任务服务。
            hr = m_pService->Connect(_variant_t(), _variant_t(), _variant_t(), _variant_t());
            if (SUCCEEDED(hr))
                binit = true;
        }
    }

    // 初始化失败时仍发送 TOKEN_MACHINE_TASKLIST,避免主控端一直等待。
    if (!binit)
    {
        BYTE bToken = TOKEN_MACHINE_TASKLIST;
        Send(&bToken, 1);
        return 0;
    }

    // 从根任务文件夹开始递归枚举。
    ITaskFolder* pRootFolder = NULL;
    hr = m_pService->GetFolder(_bstr_t(L"\\"), &pRootFolder);

    nBufferSize = 1024 * 10;
    lpList = (BYTE*)LocalAlloc(LPTR, nBufferSize);
    lpList[0] = TOKEN_MACHINE_TASKLIST;
    offset++;

    // GetFolderAll 会继续枚举子文件夹和任务项。
    // 防御侧要重点关注任务动作里的可执行文件路径和触发器。
    GetFolderAll(pRootFolder);
    Send((LPBYTE)lpList, (UINT)LocalSize(lpList));
    LocalFree(lpList);
    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
51
52
53
54
55
56
57
58
59
60
61
62

这里还存在计划任务创建、运行、停止、删除分支。企业侧不需要复现这些操作,只要知道检测重点:

对象 观察点
任务名称 随机名、伪装系统名、空描述
任务路径 非标准目录、隐藏目录
执行文件 用户目录、临时目录、网络路径
触发器 登录、启动、定时、高频触发
时间 与异常登录、外联、文件落地时间关联

# 14. 高风险控制分支的防御说明

系统管理模块中还包含一些控制类分支,例如:

能力 源码位置 防御关注
窗口控制 MachineManager.cpp:84-99 窗口被隐藏、关闭、最小化
进程结束/删除 MachineManager.cpp:101-190 OpenProcess(PROCESS_ALL_ACCESS)TerminateProcess、文件删除
线程冻结/解冻 MachineManager.cpp:194-239 线程快照、Suspend/Resume 行为
服务删除/控制 MachineManager.cpp:1864-1944 OpenServiceStartServiceControlServiceDeleteService
任务创建/删除 MachineManager.cpp:2191-2352 Task Scheduler 写操作
注入相关 MachineManager.cpp:2500-2816 远程进程内存写入、APC、远程线程等高风险行为

这些内容不适合按操作步骤展开。安全工程学习者应把它们当作检测面和权限边界来理解:

  1. 远程管理工具是否真的需要这些能力。
  2. 谁能触发这些命令。
  3. 是否有强认证、最小权限和审批。
  4. 是否记录命令、目标、结果和操作者。
  5. 是否能在 EDR/SIEM 中关联到对应主机事件。

# 15. 企业检测点

系统管理检测关联

可以按五类信号建模。

类型 检测点 说明
资产盘点 进程、软件、服务、任务、hosts 枚举 合法工具常见,但需授权和记录
网络关联 PID、TCP/UDP、本地/远程地址 把异常连接映射到进程路径和用户
权限变化 SeDebugPrivilege、SCM、COM 初始化 系统对象枚举和控制前常见
持久化线索 服务、计划任务、hosts 与启动项、注册表和文件落地关联
异常操作 终止、删除、服务控制、任务控制、注入 高风险行为,需高优先级告警

一条实用的事件关联思路:

  1. 进程连接远端后收到系统管理命令。
  2. 进程调用 Toolhelp、PSAPI、SCM、TaskScheduler、iphlpapi 等 API。
  3. 短时间内返回大量进程、网络、服务或软件数据。
  4. 同一会话后续出现服务控制、任务写入、进程结束或 hosts 修改。
  5. 关联进程路径、用户、签名、父进程和网络目标,判断是否为授权运维。

# 16. 安全改造建议

控制面 风险原因 改造建议
枚举和控制分权 枚举可用于资产盘点,控制类能力风险更高 控制类命令使用更高权限门槛
命令和目标留痕 没有记录就无法复盘 记录命令、对象、操作者、时间和结果
数据最小化 软件、窗口、浏览记录等可能敏感 只采集业务需要的字段
高风险 API 限制 PROCESS_ALL_ACCESSSeDebugPrivilege 风险高 默认禁用,按需审批
服务和任务写入保护 持久化常见位置 和文件落地、网络连接关联
hosts 变化监控 hosts 可影响解析 监控内容、属性和修改时间

# 17. 常见误区

误区一:系统管理就是正常运维。

系统管理能力本身中性,但如果缺少认证、授权和记录,同样会变成高风险远程控制能力。

误区二:只看进程列表,不看网络连接。

进程和网络要用 PID 关联。一个未知路径进程如果同时存在异常外联,风险明显更高。

误区三:服务和计划任务只看名称。

名称容易伪装。更重要的是路径、启动类型、触发器、创建时间、签名和父进程。

误区四:hosts 文件很小,不重要。

hosts 可以改变域名解析。异常 hosts 映射可能导致软件更新、登录页面或安全服务被重定向。

误区五:枚举动作没有风险。

枚举动作会暴露资产、软件、安全产品、连接和用户环境。对攻击者是侦察,对企业是检测线索。

# 18. 合法练习题

  1. MachineManager.h 中整理命令字,把它们分成“枚举类”和“控制类”。
  2. 画出 COMMAND_MACHINE_PROCESS -> getProcessList -> TOKEN_MACHINE_PROCESS 的调用链。
  3. 说明如何用 PID 把进程列表和网络连接列表关联起来。
  4. 设计一份服务异常判断表,包含服务名、路径、启动类型、签名、创建时间。
  5. 设计一套合法远程运维工具的权限方案:普通用户只能枚举,高权限用户才能执行控制类操作,且必须记录。
  6. 从防御角度列出 hosts 文件被异常修改时应收集的证据。

# 19. 本课小结

系统管理模块展示了一个远程管理插件的完整能力面:进程、窗口、网络连接、软件、服务、计划任务、hosts 等对象都能被枚举,并通过 TOKEN_MACHINE_* 返回。

从安全工程角度看,枚举能力有合法价值,也有侦察风险;控制类能力更需要强认证、最小权限、审批和记录。企业检测时应把资产枚举、网络 PID、权限变化、服务任务 hosts、异常控制操作串成事件链,而不是孤立看单个 API。

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

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

社区交流

讨论与留言

前往 GitHub Issues →

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

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