44 / 52 安全检测、响应与重构

第 44 课:持久化机制综合识别

# 第 44 课:持久化机制综合识别

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

前面第 27 课讲过启动管理插件,第 26 课讲过查注册表插件,第 24 课也涉及系统服务管理。本课把这些内容合并成一个安全工程视角:如何系统识别 Windows 常见持久化位置,如何建立自启动基线,如何把注册表、启动目录、计划任务、服务和网络回连关联起来。

第 44 课:持久化机制综合识别(配图)

# 1. 新手先理解:什么是持久化

持久化指程序在重启、用户重新登录、进程被结束之后,仍然能够再次启动。合法软件也会使用自启动,比如输入法、同步盘、安全软件;恶意程序也会使用自启动。因此企业检测不能只看“有没有自启动”,而要看“谁创建的、创建到哪里、路径是否合理、是否伴随高危行为”。

本课只讲识别和加固,不讲如何部署持久化。

# 2. 源码目录和文件

位置 作用 阅读重点
主插件\启动管理\启动管理\StartupManager.h/.cpp 启动项管理主逻辑 Run 项、启动目录、计划任务、服务
主插件\查注册表\查注册表\RegeditManager.h/.cpp 注册表浏览和写入 枚举、创建、删除、写值
主插件\系统管理\系统管理\MachineManager.cpp 系统管理辅助能力 服务枚举、服务创建
主插件\登录模块\登录模块\LoginManager.cpp 登录模块配置写入 注册表配置写入和上线逻辑

依赖说明: 本课主要依赖 Windows 注册表 API、Shell 特殊目录、Task Scheduler COM、SCM 服务控制管理器和项目网络层。它不是某一个第三方库的问题,而是多个 Windows 自启动机制被统一命令入口调度。后续源码注释会直接标出命令字、启停字段、目标位置和对应事件源。

# 3. 关键函数

函数 大致行号 作用 防御关注点
CStartupManager::OnReceive StartupManager.cpp 约 108-164 行 自启动命令分发 一个入口控制多种持久化位置
SendStartupStatus 约 176-191 行 回传当前状态 可以映射成基线检查项
SetRegistryStart 约 228-256 行 写 Run 项 HKCU\...\Run 变化
CopyOrRemoveFromStartupDir 约 461-510 行 写启动目录 Startup 目录新增文件
AddScheduledTask 约 596 行起 创建计划任务 短周期触发、异常任务名
AddOrDeleteService 约 311-360 行 创建服务 自动启动服务、异常路径
CRegeditManager::WriteValue RegeditManager.cpp 约 165-181 行 写注册表值 远程注册表修改
CRegeditManager::OpenKey 约 336-381 行 打开/创建/删除键 KEY_ALL_ACCESS 风险

# 4. 启动管理命令如何分发

4. 启动管理命令如何分发(配图)

StartupManagerOnReceive 把不同命令映射到不同持久化位置。新手阅读时不要急着看每个实现细节,先把命令和位置对应起来。

// 源码位置:主插件\启动管理\启动管理\StartupManager.cpp,约第 108-164 行
// 防御分析:lpBuffer[0] 是远程命令字,后续分支会触发不同持久化位置。
// startupRequest.enable 决定启用还是删除;读取结构体前应校验 nSize。
// 检测时要把“收到命令”和“写 Run 键 / 创建任务 / 创建服务”等本地事件按时间线关联。
void CStartupManager::OnReceive(LPBYTE lpBuffer, UINT nSize)
{
    if (lpBuffer[0] == TOKEN_HEARTBEAT)
        return;

    switch (lpBuffer[0])
    {
    case COMMAND_STARTUP_STATUS_REQUEST:
        // 防御分析:状态查询本身不改系统,但通常是后续变更前的探测动作。
        SendStartupStatus();
        break;

    case COMMAND_STARTUP_REGISTRY_REQUEST:
        // 防御分析:Run 键变更属于用户级持久化,应记录进程、用户和值数据。
        success = SetRegistryStart(startupRequest.enable);
        break;

    case COMMAND_STARTUP_FOLDER_REQUEST:
        // 防御分析:启动目录文件创建或删除会在文件系统留下直接证据。
        success = CopyOrRemoveFromStartupDir(startupRequest.enable);
        break;

    case COMMAND_STARTUP_SCHEDULED_TASK_REQUEST:
        // 防御分析:计划任务变更应和 Task Scheduler 日志、进程树关联。
        success = startupRequest.enable ? AddScheduledTask() : RemoveScheduledTask();
        break;

    case COMMAND_STARTUP_SERVICE_REQUEST:
        // 防御分析:服务持久化通常权限更高,优先级高于普通用户启动项。
        success = AddOrDeleteService(startupRequest.enable);
        break;
    }
}
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

防御侧可以把它转换成一个检测模型:

命令分支 Windows 位置 常见日志来源
Registry HKCU\Software\Microsoft\Windows\CurrentVersion\Run 注册表事件、EDR
Startup Folder 用户启动目录 文件创建事件
Scheduled Task Task Scheduler 任务创建日志、EDR
Service SCM 服务 服务创建日志、Sysmon

# 5. Run 注册表项

5. Run 注册表项(配图)

Run 项是最常见的用户级自启动位置。源码会打开 HKCU 下的 Run 键,然后写入当前程序路径。

// 源码位置:主插件\启动管理\启动管理\StartupManager.cpp,约第 228-256 行
bool CStartupManager::SetRegistryStart(bool add)
{
    TCHAR szPath[MAX_PATH];
    GetModuleFileName(NULL, szPath, MAX_PATH);

    HKEY hKey;
    LONG result = RegOpenKeyEx(
        HKEY_CURRENT_USER,
        _T("Software\\Microsoft\\Windows\\CurrentVersion\\Run"),
        0,
        KEY_WRITE,
        &hKey);

    if (add)
    {
        result = RegSetValueEx(
            hKey,
            Tc_RegName,
            0,
            REG_SZ,
            (const BYTE*)szPath,
            (lstrlen(szPath) + 1) * sizeof(TCHAR));
    }

    // 防御分析:Run 项写入本身可能合法,
    // 但异常父进程、临时目录路径、随后网络回连应重点关注。
}
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

# 6. 服务持久化

服务自启动通常需要更高权限,因此风险优先级高于普通用户启动目录。源码创建的是 SERVICE_WIN32_OWN_PROCESS,启动类型是 SERVICE_AUTO_START

// 源码位置:主插件\启动管理\启动管理\StartupManager.cpp,约第 311-360 行
bool CStartupManager::AddOrDeleteService(bool add)
{
    TCHAR szPath[MAX_PATH];
    ::GetModuleFileName(NULL, szPath, MAX_PATH);

    SC_HANDLE hSCManager = ::OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS);

    hService = ::CreateService(
        hSCManager,
        TC_SERVICE_NAME,
        TC_DISPLAY_NAME,
        SERVICE_ALL_ACCESS,
        SERVICE_WIN32_OWN_PROCESS,
        SERVICE_AUTO_START,
        SERVICE_ERROR_NORMAL,
        szPath,
        NULL,
        NULL,
        NULL,
        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
26

服务检测建议:

字段 为什么重要
ServiceName 是否伪装系统服务或随机命名
ImagePath 是否位于临时目录、用户目录、可写目录
StartType 自动启动风险更高
ParentProcess 谁创建了服务
Signature 可执行文件是否有可信签名

# 7. 启动目录和计划任务

启动目录路径通过 SHGetSpecialFolderPathW 获取,然后把当前程序路径拼接到目录下。计划任务路径使用 COM 接口初始化任务计划相关对象。

// 源码位置:主插件\启动管理\启动管理\StartupManager.cpp,约第 461-510 行
bool CStartupManager::CopyOrRemoveFromStartupDir(bool add)
{
    HINSTANCE hShell32 = ::LoadLibrary(_T("Shell32.dll"));

    TSHGetSpecialFolderPath MySHGetSpecialFolderPath =
        (TSHGetSpecialFolderPath)::GetProcAddress(
            hShell32,
            "SHGetSpecialFolderPathW");

    MySHGetSpecialFolderPath(
        NULL,
        szStartupPath,
        0x0007, // CSIDL_STARTUP
        TRUE);

    ::GetModuleFileName(NULL, szExeFullPath, MAX_PATH);

    // 防御分析:Startup 目录新增 EXE/LNK 后,
    // 应检查文件来源、签名、父进程和创建时间。
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 源码位置:主插件\启动管理\启动管理\StartupManager.cpp,约第 596-620 行
bool CStartupManager::AddScheduledTask()
{
    TCHAR szAppPath[MAX_PATH];
    ::GetModuleFileName(NULL, szAppPath, MAX_PATH);

    const TCHAR* time = L"1";

    HRESULT hello = CoInitializeEx(NULL, COINIT_MULTITHREADED);
    hello = CoInitializeSecurity(
        NULL,
        -1,
        NULL,
        NULL,
        RPC_C_AUTHN_LEVEL_PKT_PRIVACY,
        RPC_C_IMP_LEVEL_IMPERSONATE,
        NULL,
        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

# 8. 注册表管理插件如何扩大风险面

启动管理只覆盖固定位置,查注册表插件则提供更通用的注册表读写能力。它使用 KEY_ALL_ACCESS 打开键,并支持创建、删除、写值。

// 源码位置:主插件\查注册表\查注册表\RegeditManager.cpp,约第 24-40 行
// 防御分析:注册表插件比启动管理更通用,远程命令可以枚举、创建或删除任意授权范围内的键。
// lpBuffer 中的根键标识和路径字符串决定真实访问位置;路径归一化和 32/64 位视图都要记录。
void CRegeditManager::OnReceive(LPBYTE lpBuffer, UINT nSize)
{
    if (lpBuffer[0] == TOKEN_HEARTBEAT) return;

    switch (lpBuffer[0])
    {
    case COMMAND_REG_ENUM:
        // 防御分析:枚举行为可以作为后续写入或删除前的侦察信号。
        EnumReg(lpBuffer[sizeof(TCHAR)], lpBuffer + 2 * sizeof(TCHAR));
        break;
    case COMMAND_REG_CREATEKEY:
        // 防御分析:创建子键会改变系统配置面,敏感路径应默认阻断。
        CreateKey(lpBuffer + sizeof(TCHAR));
        break;
    case COMMAND_REG_DELKEY:
        // 防御分析:删除键具有破坏性,需保留旧路径、操作者和网络会话证据。
        DeleteKey(lpBuffer + sizeof(TCHAR));
        break;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 源码位置:主插件\查注册表\查注册表\RegeditManager.cpp,约第 165-181、336-381 行
BOOL CRegeditManager::WriteValue(LPCTSTR lpValueName, LPCTSTR lpValue)
{
    long lReturn = RegSetValueEx(
        m_hKey,
        lpValueName,
        0L,
        REG_SZ,
        (const BYTE*)lpValue,
        (lstrlen(lpValue) + 1) * sizeof(TCHAR));

    // 防御分析:远程命令驱动的注册表写值,可能被用于持久化、
    // 配置隐藏或策略篡改。
}

BOOL CRegeditManager::OpenKey(LPCTSTR lpSubKey)
{
    long lReturn = RegOpenKeyEx(m_hKey, lpSubKey, 0L, KEY_ALL_ACCESS, &hKey);

    // 防御分析:KEY_ALL_ACCESS 权限过宽,应在安全检查中重点标记。
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 9. 企业检测和加固

9. 企业检测和加固(配图)

层面 检测点 关联建议
注册表 Run 项新增、删除、修改 父进程、写入路径、用户
文件 Startup 目录新增文件 文件签名、哈希、来源
计划任务 新建任务、短周期触发 任务名、Action、Author
服务 新建自动启动服务 服务类型、ImagePath、创建者
网络 自启动变更后首次回连 目标地址、协议 token、时间窗口
权限 runas、管理员权限检查 是否有提权提示或 UAC 行为

9. 企业检测和加固(配图)

加固建议:

措施 说明
建立自启动基线 每台主机记录常见启动项、服务、任务
限制写权限 普通用户不应能写高风险目录和服务配置
监控新项 新增项比已有项更值得关注
关联父进程 安装程序创建启动项和脚本/未知进程创建启动项风险不同
复核业务例外 输入法、驱动、安全软件、同步工具可能是正常来源
隔离异常主机 持久化伴随凭据读取、注入、驱动加载时优先隔离

# 10. 合法练习题

  1. 只读源码,画出启动管理支持的四类自启动位置。
  2. 写一个检测思路:未知进程创建自动启动服务后 3 分钟内出现外联。
  3. 设计一张自启动基线表,字段包括主机、位置、名称、路径、签名、首次发现时间。
  4. 比较 Run 项和服务持久化的权限差异,并说明为什么服务创建优先级更高。

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

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

社区交流

讨论与留言

前往 GitHub Issues →

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

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