第 27 课:主插件:启动管理
# 第 27 课:主插件:启动管理
免责声明:本专栏仅用于安全工程学习研究,禁止使用本专栏介绍的技术做其他用途,否则后果自负,与本号无关。
本课定位:防御分析、源码阅读、启动项风险识别和企业检测工程。
本课只讲启动管理插件的结构、状态查询、启动位置风险和防御检测,不提供未授权持久化、提权运行或规避检测的操作步骤。
第 26 课讲的是注册表插件。注册表里有很多启动项和系统配置,但那一课重点是“如何浏览和修改注册表对象”。第 25 课继续往下看一个更聚焦的模块:主插件\启动管理。
启动管理模块把几个常见启动位置合并到一个插件里:
注册表 Run 键
启动目录
计划任务
系统服务
管理员权限运行
2
3
4
5
对新手来说,先不要把它理解成某个单独 API,而要理解成一组“程序随系统或用户登录自动运行”的机制。合法软件会用这些机制做更新、同步、驱动服务和运维任务;恶意软件也经常滥用这些位置建立持久化。
# 1. 本课学习目标
| 问题 | 本课回答 |
|---|---|
| 启动管理插件在工程中做什么 | 通过 CStartupManager 查询和切换多个启动位置的状态 |
| 为什么它是高风险模块 | 它能让程序在登录或开机后自动运行,影响持久化风险 |
StartupInfo 保存什么 | 管理员权限、Run 键、启动目录、计划任务、服务的启用状态 |
StartupRequest 怎么用 | 命令包里携带启用或禁用布尔值 |
| 企业应关注哪些启动位置 | Run 键、Startup 文件夹、计划任务、服务、UAC 提权行为 |
| 合法工具应怎么加固 | 最小权限、审批、基线比对、操作记录、变更回滚 |
# 2. 本课涉及源码目录和文件
| 文件 | 作用 |
|---|---|
主插件\启动管理\启动管理.sln | 启动管理插件解决方案 |
主插件\启动管理\启动管理\启动管理.cpp | 插件入口、连接选择、创建 CStartupManager |
主插件\启动管理\启动管理\StartupManager.h | 类声明、状态查询和启动位置操作函数声明 |
主插件\启动管理\启动管理\StartupManager.cpp | 命令分发、Run 键、启动目录、计划任务和服务逻辑 |

本课读源码时按四层看:
入口层:启动管理.cpp
管理层:CStartupManager
启动位置层:Run / Startup / Task / Service
检测层:基线、变更、权限、网络会话
2
3
4
# 3. 插件入口:连接后创建 CStartupManager
入口代码和前几课一致。连接建立后,插件创建 CStartupManager 并进入事件循环。
// 源码位置:主插件\启动管理\启动管理\启动管理.cpp:6-43
// 防御分析注释:
// 1. plugInfo 保存远端地址、端口和协议类型。
// 2. 连接成功后创建 CStartupManager,后续启动项命令都由它处理。
// 3. 企业侧不能只看“是否联网”,还要看联网后是否触发启动项、服务、计划任务等能力。
struct plugInfo
{
char mark[30];
TCHAR szAddress[255];
DWORD szPort;
BOOL IsTcp;
BOOL RunDllEntryProc;
} MyInfo =
{
"plugmark",
_T("127.0.0.1"),
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))
{
CStartupManager 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
41
这段入口本身不复杂,关键是它创建的对象。CStartupManager 一创建就会发送启动管理模块状态 token,主控端据此打开对应功能窗口。
# 4. 类声明:启动项能力集中在哪里
StartupManager.h 里能看到模块能力面。它不是一个普通“查询状态”模块,里面既有查询函数,也有变更函数。
// 源码位置:主插件\启动管理\启动管理\StartupManager.h:12-42
// 防御分析注释:
// 1. Is* 函数用于判断启动位置是否存在当前程序。
// 2. SetRegistryStart、CopyOrRemoveFromStartupDir、AddScheduledTask、AddOrDeleteService 会改变系统启动位置。
// 3. 合法远程管理工具应把查询和变更权限分开,不能让所有连接都能切换启动项。
class CStartupManager : public CManager
{
public:
CStartupManager(ISocketBase* pClient);
virtual ~CStartupManager();
virtual void OnReceive(LPBYTE lpBuffer, UINT nSize);
private:
void SendStartupStatus();
void OnHandleStartupResponse(int8_t cmd, bool enable, bool success);
void OnHandleRunAsAdmin();
bool IsStartupItemExistsInRegistry();
bool IsStartupItemExistsInStartupFolder();
bool IsScheduledTaskExists();
bool IsServiceExists();
bool SetRegistryStart(bool add);
bool AddOrDeleteService(bool add);
bool CopyOrRemoveFromStartupDir(bool add);
bool AddScheduledTask();
bool RemoveScheduledTask();
private:
BOOL m_buser;
};
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
这一课要抓住两个词:状态和变更。
| 类型 | 代表函数 | 安全含义 |
|---|---|---|
| 状态查询 | IsStartupItemExistsInRegistry、IsServiceExists | 用于判断当前程序是否已经占用某个启动位置 |
| 状态汇总 | SendStartupStatus | 把多个启动位置状态打包发回 |
| 变更操作 | SetRegistryStart、AddScheduledTask、AddOrDeleteService | 会实际修改系统启动配置 |
# 5. 命令面:OnReceive 分发启动项请求

StartupManager.cpp 里直接定义命令枚举和三个结构体:StartupInfo、StartupRequest、StartupResponse。
// 源码位置:主插件\启动管理\启动管理\StartupManager.cpp:14-63
// 防御分析注释:
// 1. COMMAND_STARTUP_* 是主控端发来的启动管理命令。
// 2. StartupInfo 用于回传当前状态,StartupRequest 用于接收启用/禁用请求。
// 3. StartupResponse 表示一次变更请求是否成功。
enum
{
COMMAND_STARTUP_STATUS_REQUEST,
COMMAND_STARTUP_STATUS_RESPONSE,
COMMAND_STARTUP_REGISTRY_REQUEST,
COMMAND_STARTUP_REGISTRY_RESPONSE,
COMMAND_STARTUP_FOLDER_REQUEST,
COMMAND_STARTUP_FOLDER_RESPONSE,
COMMAND_STARTUP_SCHEDULED_TASK_REQUEST,
COMMAND_STARTUP_SCHEDULED_TASK_RESPONSE,
COMMAND_STARTUP_SERVICE_REQUEST,
COMMAND_STARTUP_SERVICE_RESPONSE,
COMMAND_STARTUP_RUNASADMIN_REQUEST,
COMMAND_STARTUP_RUNASADMIN_RESPONSE,
};
#pragma pack(push, 1)
struct StartupInfo
{
int8_t cmd;
bool enableAdminPrivileg;
bool enableRegistryStartup;
bool enableFolderStartup;
bool enableScheduledTaskStartup;
bool enableServiceStartup;
wchar_t szAppPath[MAX_PATH];
};
struct StartupRequest
{
int8_t cmd;
bool enable;
};
struct StartupResponse
{
int8_t cmd;
bool enable;
bool success;
};
#pragma pack(pop)
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
OnReceive 的结构很直观:查询状态就调用 SendStartupStatus;收到某类启动位置的请求,就调用对应函数并回传结果。
// 源码位置:主插件\启动管理\启动管理\StartupManager.cpp:108-174
// 防御分析注释:
// 1. lpBuffer[0] 决定要查询还是修改哪类启动位置。
// 2. COMMAND_STARTUP_RUNASADMIN_REQUEST 会触发管理员权限运行请求,应与 UAC 事件关联。
// 3. 每个变更分支都应被企业侧记录:谁触发、目标主机、目标路径、结果。
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:
memcpy(&startupRequest, lpBuffer, sizeof(startupRequest));
success = SetRegistryStart(startupRequest.enable);
OnHandleStartupResponse(COMMAND_STARTUP_REGISTRY_RESPONSE,
startupRequest.enable, success);
break;
case COMMAND_STARTUP_FOLDER_REQUEST:
success = CopyOrRemoveFromStartupDir(startupRequest.enable);
OnHandleStartupResponse(COMMAND_STARTUP_FOLDER_RESPONSE,
startupRequest.enable, success);
break;
case COMMAND_STARTUP_SCHEDULED_TASK_REQUEST:
success = startupRequest.enable ? AddScheduledTask() : RemoveScheduledTask();
OnHandleStartupResponse(COMMAND_STARTUP_SCHEDULED_TASK_RESPONSE,
startupRequest.enable, success);
break;
case COMMAND_STARTUP_SERVICE_REQUEST:
success = AddOrDeleteService(startupRequest.enable);
OnHandleStartupResponse(COMMAND_STARTUP_SERVICE_RESPONSE,
startupRequest.enable, success);
break;
case COMMAND_STARTUP_RUNASADMIN_REQUEST:
OnHandleRunAsAdmin();
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
38
39
40
41
42
43
44
45
46
这里不讲怎么发这些命令,只讲源码如何处理。防御侧要做的是:看到启动项变更,不要只看“成功/失败”,还要看触发来源和变更前后状态。
# 6. 状态查询:SendStartupStatus

SendStartupStatus 会收集当前程序路径,并检查四类启动位置是否存在。
// 源码位置:主插件\启动管理\启动管理\StartupManager.cpp:176-205
// 防御分析注释:
// 1. GetModuleFileName 获取当前程序路径,这个路径会被用于后续启动项比对。
// 2. 四个 Is* 函数分别检查 Run 键、启动目录、计划任务和服务。
// 3. StartupInfo 回包能反映当前程序是否已进入持久化位置。
void CStartupManager::SendStartupStatus()
{
StartupInfo si = { 0 };
GetModuleFileName(NULL, si.szAppPath, MAX_PATH);
si.enableAdminPrivileg = IsUserAnAdmin();
si.enableRegistryStartup = IsStartupItemExistsInRegistry();
si.enableFolderStartup = IsStartupItemExistsInStartupFolder();
si.enableScheduledTaskStartup = IsScheduledTaskExists();
si.enableServiceStartup = IsServiceExists();
si.cmd = COMMAND_STARTUP_STATUS_RESPONSE;
Send((LPBYTE)&si, sizeof(StartupInfo));
}
void CStartupManager::OnHandleStartupResponse(int8_t cmd, bool enable, bool success)
{
StartupResponse startupResponse;
startupResponse.cmd = cmd;
startupResponse.enable = enable;
startupResponse.success = success;
Send((LPBYTE)&startupResponse, sizeof(StartupResponse));
SendStartupStatus();
}
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
注意 OnHandleStartupResponse 最后又调用了一次 SendStartupStatus。这意味着一次变更完成后,主控端不仅知道“操作结果”,还会拿到“变更后的完整状态”。
# 6.1 主控按钮到插件 Manager 的触发链
前面看的都是被控端插件。新手还需要补上主控侧这半边:用户在主控窗口里点击“开启注册表启动”“开启计划任务”等按钮时,并不是主控自己去改远端系统,而是把一个很小的命令包发给远端插件,由远端 CStartupManager::OnReceive 决定具体调用哪个系统 API。

// 源码位置:主控\Quick\StartupMgrDlg.cpp:127-160
// 防御分析注释:
// 1. OnReceiveComplete 是主控端接收插件回包的入口。
// 2. COMMAND_STARTUP_STATUS_RESPONSE 会刷新界面中的当前状态。
// 3. COMMAND_STARTUP_*_RESPONSE 只表示一次变更请求的结果,真正的落点仍在被控端插件。
// 4. 做企业检测时,要把主控侧命令、被控端启动项写入、回包状态放在同一个时间窗口里看。
void CStartupMgrDlg::OnReceiveComplete(void)
{
if (m_bOnClose)
return;
switch (m_pContext->m_DeCompressionBuffer.GetBuffer(0)[0])
{
case COMMAND_STARTUP_STATUS_RESPONSE:
memcpy(&m_startupInfo,
m_pContext->m_DeCompressionBuffer.GetBuffer(0),
sizeof(StartupInfo));
OnRecvStartupStatus();
break;
case COMMAND_STARTUP_REGISTRY_RESPONSE:
case COMMAND_STARTUP_FOLDER_RESPONSE:
case COMMAND_STARTUP_SCHEDULED_TASK_RESPONSE:
case COMMAND_STARTUP_SERVICE_RESPONSE:
{
StartupResponse startupResp;
memcpy(&startupResp,
m_pContext->m_DeCompressionBuffer.GetBuffer(0),
sizeof(StartupResponse));
OnRecvStartupResponse(startupResp);
}
break;
case COMMAND_STARTUP_RUNASADMIN_RESPONSE:
{
bool success = m_pContext->m_DeCompressionBuffer.GetBuffer(0)[1];
OnRecvRunAsAdminResponse(success);
}
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
38
39
40
41
按钮事件本身很短,但它们解释了“如何触发”的问题:每个按钮都读取当前状态,然后把目标状态取反后发出去。例如当前没有注册表启动项时,点击按钮会发送“启用注册表启动”的请求;当前已经启用时,再点会发送“禁用注册表启动”的请求。
// 源码位置:主控\Quick\StartupMgrDlg.cpp:188-217、272-279
// 防御分析注释:
// 1. m_startupInfo 来自上一轮状态回包,主控 UI 依赖它决定按钮下一步动作。
// 2. RequestStartup 只打包 cmd 和 enable,不包含具体路径;路径由被控端插件用 GetModuleFileName 获取。
// 3. COMMAND_STARTUP_RUNASADMIN_REQUEST 单独发送 1 字节命令,后续会在被控端触发 ShellExecute("runas")。
// 4. 如果要做合规远程运维工具,应增加操作者身份、审批单号、目标主机和操作理由等字段。
void CStartupMgrDlg::OnBnClickedRefreshStatus()
{
RequestStartupStatus();
}
void CStartupMgrDlg::OnBnClickedStartupRegistry()
{
RequestStartup(COMMAND_STARTUP_REGISTRY_REQUEST,
!m_startupInfo.enableRegistryStartup);
}
void CStartupMgrDlg::OnBnClickedStartupScheduledTask()
{
RequestStartup(COMMAND_STARTUP_SCHEDULED_TASK_REQUEST,
!m_startupInfo.enableScheduledTaskStartup);
}
void CStartupMgrDlg::OnBnClickedRunAsAdmin()
{
BYTE cmd = COMMAND_STARTUP_RUNASADMIN_REQUEST;
m_iocpServer->Send(m_pContext, &cmd, sizeof(cmd));
}
void CStartupMgrDlg::RequestStartup(int8_t startupCmd, bool enable)
{
StartupRequest startupReq;
startupReq.cmd = startupCmd;
startupReq.enable = enable;
m_iocpServer->Send(m_pContext, (LPBYTE)&startupReq, sizeof(startupReq));
}
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
这一段把触发顺序讲清楚了:
- 主控窗口打开后先发
COMMAND_STARTUP_STATUS_REQUEST。 - 被控端
SendStartupStatus返回当前权限、路径和四类启动位置状态。 - 主控根据
m_startupInfo改按钮文字。 - 用户点击按钮后,主控发送
StartupRequest。 - 被控端
OnReceive根据命令改对应启动位置。 - 被控端先回
StartupResponse,再回一次完整StartupInfo。
# 6.2 启动管理提供哪些能力

| 能力 | 主控触发命令 | 被控端函数 | 具体系统位置 | 防御理解 |
|---|---|---|---|---|
| 查询启动状态 | COMMAND_STARTUP_STATUS_REQUEST | SendStartupStatus | Run / Startup / Task / Service | 建立变更前基线 |
| 注册表启动 | COMMAND_STARTUP_REGISTRY_REQUEST | SetRegistryStart | HKCU\...\Run | 用户登录后自动运行 |
| 启动目录 | COMMAND_STARTUP_FOLDER_REQUEST | CopyOrRemoveFromStartupDir | 当前用户 Startup 文件夹 | 文件落地型启动 |
| 计划任务 | COMMAND_STARTUP_SCHEDULED_TASK_REQUEST | AddScheduledTask / RemoveScheduledTask | Task Scheduler | 时间触发或登录触发 |
| 服务启动 | COMMAND_STARTUP_SERVICE_REQUEST | AddOrDeleteService | Windows Service | 权限和持久化风险更高 |
| 管理员运行 | COMMAND_STARTUP_RUNASADMIN_REQUEST | OnHandleRunAsAdmin | UAC runas | 应与 UAC 提权事件关联 |
到这里再看第 7 节的四类启动位置,就不会觉得跳跃:第 7 节只是把上表中的四个“具体系统位置”逐个拆开,看它们各自对应哪些 Windows API 和哪些检测点。
# 7. 四类启动位置

# 7.1 注册表 Run 键
源码使用的是当前用户 Run 键。这个位置的正常用途是用户登录后自动运行某些程序。风险在于:它也经常被滥用为用户级持久化位置。
// 源码位置:主插件\启动管理\启动管理\StartupManager.cpp:228-267
// 防御分析注释:
// 1. 目标路径来自 GetModuleFileName,表示当前程序自身路径。
// 2. RegOpenKeyEx 打开 HKCU Run 键。
// 3. RegSetValueEx 写入启动值,RegDeleteValue 删除启动值。
// 4. 企业侧应监控 Run 键写入的值名、数据路径、进程、用户和网络会话。
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 (result != ERROR_SUCCESS)
return false;
if (add)
{
result = RegSetValueEx(hKey, Tc_RegName, 0, REG_SZ,
(const BYTE*)szPath,
(lstrlen(szPath) + 1) * sizeof(TCHAR));
}
else
{
result = RegDeleteValue(hKey, Tc_RegName);
}
RegCloseKey(hKey);
return result == ERROR_SUCCESS;
}
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
// 源码位置:主插件\启动管理\启动管理\StartupManager.cpp:269-309
// 防御分析注释:
// 1. 查询时先确认值是否存在,再读取值数据。
// 2. 只有注册表中的路径与当前程序路径完全匹配,才认为该启动项存在。
// 3. 检测侧也应做路径规范化,避免短路径、大小写、符号链接导致误判。
bool CStartupManager::IsStartupItemExistsInRegistry()
{
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_READ,
&hKey);
DWORD dataSize = 0;
result = RegQueryValueEx(hKey, Tc_RegName, NULL, NULL, NULL, &dataSize);
wchar_t szRegistryPath[MAX_PATH] = { 0 };
result = RegQueryValueEx(hKey, Tc_RegName, NULL, NULL,
(BYTE*)szRegistryPath, &dataSize);
return result == ERROR_SUCCESS &&
memcmp(szRegistryPath, szPath, dataSize) == 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
# 7.2 启动目录
启动目录是用户登录后自动执行快捷方式或程序的位置。源码里通过 SHGetSpecialFolderPathW 找到 CSIDL_STARTUP,再复制或删除当前程序。
// 源码位置:主插件\启动管理\启动管理\StartupManager.cpp:461-539
// 防御分析注释:
// 1. CSIDL_STARTUP 指向当前用户 Startup 目录。
// 2. CopyFile 把当前程序复制到启动目录,DeleteFile 用于移除。
// 3. 合法软件写入启动目录应有用户同意和可见配置;静默写入应作为风险事件。
bool CStartupManager::CopyOrRemoveFromStartupDir(bool add)
{
TCHAR szStartupPath[MAX_PATH] = { 0 };
TCHAR szDestFilePath[MAX_PATH] = { 0 };
TCHAR szExeFullPath[MAX_PATH] = { 0 };
MySHGetSpecialFolderPath(NULL, szStartupPath, 0x0007, TRUE);
GetModuleFileName(NULL, szExeFullPath, MAX_PATH);
wchar_t szExeName[MAX_PATH] = { 0 };
_tcscpy_s(szExeName, ARRAYSIZE(szExeName), szExeFullPath);
PathStripPath(szExeName);
wsprintf(szDestFilePath, _T("%s\\%s"), szStartupPath, szExeName);
if (add)
bRet = CopyFile(szExeFullPath, szDestFilePath, FALSE);
else
bRet = DeleteFile(szDestFilePath);
return bRet == TRUE;
}
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
# 7.3 系统服务
系统服务适合驱动、后台服务、企业代理等合法场景。风险在于它通常权限高、启动早、运行时间长。
// 源码位置:主插件\启动管理\启动管理\StartupManager.cpp:311-386
// 防御分析注释:
// 1. OpenSCManager 以 SC_MANAGER_ALL_ACCESS 打开服务控制管理器,权限很宽。
// 2. CreateService 使用 SERVICE_AUTO_START,表示开机自动启动。
// 3. 服务名称、显示名称、二进制路径、启动类型都应进入检测与基线比对。
bool CStartupManager::AddOrDeleteService(bool add)
{
TCHAR szPath[MAX_PATH];
GetModuleFileName(NULL, szPath, MAX_PATH);
SC_HANDLE hSCManager = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS);
if (hSCManager == NULL)
return false;
if (add)
{
SC_HANDLE 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);
return hService != NULL;
}
else
{
SC_HANDLE hService = OpenService(hSCManager, TC_SERVICE_NAME, DELETE);
if (hService == NULL)
return true;
return DeleteService(hService) == TRUE;
}
}
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
IsServiceExists 则通过 EnumServicesStatusExW 枚举服务并匹配名称。
// 源码位置:主插件\启动管理\启动管理\StartupManager.cpp:388-459
// 防御分析注释:
// 1. EnumServicesStatusExW 可枚举系统服务列表。
// 2. 这里通过服务名和显示名判断目标服务是否存在。
// 3. 企业侧应把新服务创建、二进制路径变化、启动类型变化作为重点事件。
bool CStartupManager::IsServiceExists()
{
SC_HANDLE hSCM = OpenSCManagerW(NULL, NULL, SC_MANAGER_ENUMERATE_SERVICE);
if (hSCM == NULL)
return false;
EnumServicesStatusExW(hSCM, SC_ENUM_PROCESS_INFO,
SERVICE_WIN32, SERVICE_STATE_ALL,
buffer, bytesNeeded,
&bytesNeeded, &servicesCount,
&resumeHandle, NULL);
for (DWORD i = 0; i < servicesCount; ++i)
{
if (wcscmp(TC_SERVICE_NAME, services[i].lpServiceName) == 0 &&
wcscmp(TC_DISPLAY_NAME, services[i].lpDisplayName) == 0)
{
return true;
}
}
return false;
}
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
# 7.4 计划任务
计划任务是运维里常用的自动化机制,也常被滥用。源码使用 Task Scheduler COM 接口创建、删除和查询任务。
// 源码位置:主插件\启动管理\启动管理\StartupManager.cpp:596-725
// 防御分析注释:
// 1. 该函数使用 Task Scheduler COM 创建任务定义。
// 2. 任务包含注册信息、触发器、重复周期和执行动作。
// 3. 本课不讲任务创建步骤,只强调检测:任务名、作者、描述、触发器、动作路径、运行账户。
bool CStartupManager::AddScheduledTask()
{
TCHAR szAppPath[MAX_PATH];
GetModuleFileName(NULL, szAppPath, MAX_PATH);
CoCreateInstance(CLSID_TaskScheduler, NULL, CLSCTX_INPROC_SERVER,
IID_ITaskService, (void**)&pService);
pService->Connect(_variant_t(), _variant_t(), _variant_t(), _variant_t());
pService->GetFolder(_bstr_t(L"\\"), &pRootFolder);
pRootFolder->DeleteTask(_bstr_t(TASK_NAME), 0);
pService->NewTask(0, &pTask);
pTask->get_Triggers(&pTriggerCollection);
pTriggerCollection->Create(TASK_TRIGGER_DAILY, &pTrigger);
pTask->get_Actions(&pActionCollection);
pActionCollection->Create(TASK_ACTION_EXEC, &pAction);
pExecAction->put_Path(szAppPath);
pRootFolder->RegisterTaskDefinition(
_bstr_t(TASK_NAME),
pTask,
TASK_CREATE_OR_UPDATE,
_variant_t("SYSTEM"),
_variant_t(),
TASK_LOGON_PASSWORD,
_variant_t(L""),
&pRegisteredTask);
return true;
}
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
// 源码位置:主插件\启动管理\启动管理\StartupManager.cpp:727-867
// 防御分析注释:
// 1. RemoveScheduledTask 通过 DeleteTask 删除任务。
// 2. IsScheduledTaskExists 通过 GetTask 判断任务是否存在。
// 3. 计划任务检测要同时看“创建事件”和“任务定义内容”,不要只看任务名。
bool CStartupManager::RemoveScheduledTask()
{
pService->Connect(_variant_t(), _variant_t(), _variant_t(), _variant_t());
pService->GetFolder(_bstr_t(L"\\"), &pRootFolder);
hr = pRootFolder->DeleteTask(_bstr_t(TASK_NAME), 0);
return SUCCEEDED(hr);
}
bool CStartupManager::IsScheduledTaskExists()
{
pService->Connect(_variant_t(), _variant_t(), _variant_t(), _variant_t());
pService->GetFolder(_bstr_t(L"\\"), &pRootFolder);
hr = pRootFolder->GetTask(_bstr_t(TASK_NAME), &pTask);
return SUCCEEDED(hr) && pTask;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 8. 管理员权限运行
OnHandleRunAsAdmin 会通过 ShellExecute 的 runas 动词请求管理员权限运行。合法软件中这通常会触发 UAC 用户确认。
// 源码位置:主插件\启动管理\启动管理\StartupManager.cpp:207-227
// 防御分析注释:
// 1. ShellExecute(..., L"runas", ...) 会触发提升权限运行请求。
// 2. 这类行为应和 UAC 事件、父进程、会话来源关联。
// 3. 合法工具应在用户可见界面明确说明为什么需要管理员权限。
void CStartupManager::OnHandleRunAsAdmin()
{
wchar_t szAppPath[MAX_PATH];
GetModuleFileName(NULL, szAppPath, MAX_PATH);
INT_PTR nRet = (INT_PTR)ShellExecute(NULL, L"runas", szAppPath,
NULL, NULL, SW_SHOW);
char szResult[2];
szResult[0] = (char)COMMAND_STARTUP_RUNASADMIN_RESPONSE;
szResult[1] = (nRet <= 32) ? 0 : 1;
Send((LPBYTE)szResult, sizeof(szResult));
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 9. 企业检测点

启动管理检测的核心是“基线 + 变更”。单独看到一个 Run 键并不一定危险,但未授权进程、异常路径、远程会话和新增自动启动位置组合起来,风险就很高。
| 检测面 | 关注点 | 事件关联 |
|---|---|---|
| 注册表 | Run 键新增、修改、删除 | 值名、路径、进程、用户、会话 |
| 启动目录 | 文件复制、删除、覆盖 | 目标路径、文件哈希、签名、父进程 |
| 计划任务 | 新建、更新、删除任务 | 任务名、触发器、动作路径、运行账户 |
| 服务 | 创建、删除、启动类型变化 | 服务名、显示名、二进制路径、权限 |
| 权限 | runas、UAC、管理员上下文 | 用户确认、会话 ID、父子进程 |
建议关联逻辑:
异常插件进程建立长连接
-> 查询启动项状态
-> 写 Run 键 / 复制 Startup / 注册任务 / 创建服务
-> 回传变更成功
-> 状态再次查询显示已启用
-> 触发持久化风险告警
2
3
4
5
6
# 10. 合法练习题
- 只阅读源码,整理
COMMAND_STARTUP_*与函数的对应关系。 - 画出
COMMAND_STARTUP_STATUS_REQUEST -> SendStartupStatus -> StartupInfo的数据流。 - 设计一个启动项基线表,字段包含位置、名称、路径、签名、哈希、创建时间、修改时间和责任人。
- 写一条防御规则:非授权进程新增自动启动服务,并且二进制路径位于用户临时目录。
- 对比 Run 键、启动目录、计划任务、服务的正常用途和滥用风险。
- 说明为什么“修改成功后再次查询状态”对检测和复盘有帮助。
# 11. 本课小结
启动管理模块把多个持久化位置合并在一起:Run 键、启动目录、计划任务、系统服务和管理员权限运行。源码结构并不复杂,但安全风险很集中。
企业侧要把它看成“启动项变更能力”,而不是单个 API。检测时要关联启动项位置、目标路径、进程、用户、网络会话和变更结果。下一课进入远程终端模块,重点分析命令输入、隐藏 cmd.exe、管道读写和输出回传。
安全工程知识交流加wx:easy_coder,黑灰产勿扰,企业单位合作请出示有效证件。
本留言区仅对应当前文章,欢迎补充观点、提出问题或帮助修正文中疏漏。 留言由 GitHub/Gitalk 提供,需要使用 GitHub 登录。
社区交流
讨论与留言