第 44 课:持久化机制综合识别
# 第 44 课:持久化机制综合识别
免责声明:本专栏仅用于安全工程学习研究,禁止使用本专栏介绍的技术做其他用途,否则后果自负,与本号无关。
前面第 27 课讲过启动管理插件,第 26 课讲过查注册表插件,第 24 课也涉及系统服务管理。本课把这些内容合并成一个安全工程视角:如何系统识别 Windows 常见持久化位置,如何建立自启动基线,如何把注册表、启动目录、计划任务、服务和网络回连关联起来。

# 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. 启动管理命令如何分发

StartupManager 的 OnReceive 把不同命令映射到不同持久化位置。新手阅读时不要急着看每个实现细节,先把命令和位置对应起来。
// 源码位置:主插件\启动管理\启动管理\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;
}
}
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 注册表项

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 项写入本身可能合法,
// 但异常父进程、临时目录路径、随后网络回连应重点关注。
}
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);
// 防御分析:自动启动服务、可执行路径、服务名、
// 创建进程和用户上下文都是关键字段。
}
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 后,
// 应检查文件来源、签名、父进程和创建时间。
}
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);
// 防御分析:短周期计划任务、异常任务路径和非交互进程创建任务,
// 都是检测重点。
}
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;
}
}
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 权限过宽,应在安全检查中重点标记。
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 9. 企业检测和加固

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

加固建议:
| 措施 | 说明 |
|---|---|
| 建立自启动基线 | 每台主机记录常见启动项、服务、任务 |
| 限制写权限 | 普通用户不应能写高风险目录和服务配置 |
| 监控新项 | 新增项比已有项更值得关注 |
| 关联父进程 | 安装程序创建启动项和脚本/未知进程创建启动项风险不同 |
| 复核业务例外 | 输入法、驱动、安全软件、同步工具可能是正常来源 |
| 隔离异常主机 | 持久化伴随凭据读取、注入、驱动加载时优先隔离 |
# 10. 合法练习题
- 只读源码,画出启动管理支持的四类自启动位置。
- 写一个检测思路:未知进程创建自动启动服务后 3 分钟内出现外联。
- 设计一张自启动基线表,字段包括主机、位置、名称、路径、签名、首次发现时间。
- 比较 Run 项和服务持久化的权限差异,并说明为什么服务创建优先级更高。
安全工程知识交流加wx:easy_coder,黑灰产勿扰,企业单位合作请出示有效证件。
本留言区仅对应当前文章,欢迎补充观点、提出问题或帮助修正文中疏漏。 留言由 GitHub/Gitalk 提供,需要使用 GitHub 登录。
社区交流
讨论与留言