27 / 52 主插件与远程能力

第 27 课:主插件:启动管理

# 第 27 课:主插件:启动管理

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

本课定位:防御分析、源码阅读、启动项风险识别和企业检测工程。
本课只讲启动管理插件的结构、状态查询、启动位置风险和防御检测,不提供未授权持久化、提权运行或规避检测的操作步骤。

第 26 课讲的是注册表插件。注册表里有很多启动项和系统配置,但那一课重点是“如何浏览和修改注册表对象”。第 25 课继续往下看一个更聚焦的模块:主插件\启动管理

启动管理模块把几个常见启动位置合并到一个插件里:

注册表 Run 键
启动目录
计划任务
系统服务
管理员权限运行
1
2
3
4
5

对新手来说,先不要把它理解成某个单独 API,而要理解成一组“程序随系统或用户登录自动运行”的机制。合法软件会用这些机制做更新、同步、驱动服务和运维任务;恶意软件也经常滥用这些位置建立持久化。

# 1. 本课学习目标

问题 本课回答
启动管理插件在工程中做什么 通过 CStartupManager 查询和切换多个启动位置的状态
为什么它是高风险模块 它能让程序在登录或开机后自动运行,影响持久化风险
StartupInfo 保存什么 管理员权限、Run 键、启动目录、计划任务、服务的启用状态
StartupRequest 怎么用 命令包里携带启用或禁用布尔值
企业应关注哪些启动位置 Run 键、Startup 文件夹、计划任务、服务、UAC 提权行为
合法工具应怎么加固 最小权限、审批、基线比对、操作记录、变更回滚

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

文件 作用
主插件\启动管理\启动管理.sln 启动管理插件解决方案
主插件\启动管理\启动管理\启动管理.cpp 插件入口、连接选择、创建 CStartupManager
主插件\启动管理\启动管理\StartupManager.h 类声明、状态查询和启动位置操作函数声明
主插件\启动管理\启动管理\StartupManager.cpp 命令分发、Run 键、启动目录、计划任务和服务逻辑

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

本课读源码时按四层看:

入口层:启动管理.cpp
管理层:CStartupManager
启动位置层:Run / Startup / Task / Service
检测层:基线、变更、权限、网络会话
1
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;
}
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

这段入口本身不复杂,关键是它创建的对象。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;
};
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

这一课要抓住两个词:状态变更

类型 代表函数 安全含义
状态查询 IsStartupItemExistsInRegistryIsServiceExists 用于判断当前程序是否已经占用某个启动位置
状态汇总 SendStartupStatus 把多个启动位置状态打包发回
变更操作 SetRegistryStartAddScheduledTaskAddOrDeleteService 会实际修改系统启动配置

# 5. 命令面:OnReceive 分发启动项请求

5. 命令面:OnReceive 分发启动项请求(配图)

StartupManager.cpp 里直接定义命令枚举和三个结构体:StartupInfoStartupRequestStartupResponse

// 源码位置:主插件\启动管理\启动管理\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)
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

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

这里不讲怎么发这些命令,只讲源码如何处理。防御侧要做的是:看到启动项变更,不要只看“成功/失败”,还要看触发来源和变更前后状态。

# 6. 状态查询:SendStartupStatus

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

注意 OnHandleStartupResponse 最后又调用了一次 SendStartupStatus。这意味着一次变更完成后,主控端不仅知道“操作结果”,还会拿到“变更后的完整状态”。

# 6.1 主控按钮到插件 Manager 的触发链

前面看的都是被控端插件。新手还需要补上主控侧这半边:用户在主控窗口里点击“开启注册表启动”“开启计划任务”等按钮时,并不是主控自己去改远端系统,而是把一个很小的命令包发给远端插件,由远端 CStartupManager::OnReceive 决定具体调用哪个系统 API。

6.1 主控按钮到插件 Manager 的触发链(配图)

// 源码位置:主控\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;
    }
}
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

按钮事件本身很短,但它们解释了“如何触发”的问题:每个按钮都读取当前状态,然后把目标状态取反后发出去。例如当前没有注册表启动项时,点击按钮会发送“启用注册表启动”的请求;当前已经启用时,再点会发送“禁用注册表启动”的请求。

// 源码位置:主控\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));
}
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

这一段把触发顺序讲清楚了:

  1. 主控窗口打开后先发 COMMAND_STARTUP_STATUS_REQUEST
  2. 被控端 SendStartupStatus 返回当前权限、路径和四类启动位置状态。
  3. 主控根据 m_startupInfo 改按钮文字。
  4. 用户点击按钮后,主控发送 StartupRequest
  5. 被控端 OnReceive 根据命令改对应启动位置。
  6. 被控端先回 StartupResponse,再回一次完整 StartupInfo

# 6.2 启动管理提供哪些能力

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. 四类启动位置(配图)

# 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;
}
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
// 源码位置:主插件\启动管理\启动管理\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;
}
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

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

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

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

# 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;
}
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
// 源码位置:主插件\启动管理\启动管理\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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 8. 管理员权限运行

OnHandleRunAsAdmin 会通过 ShellExecuterunas 动词请求管理员权限运行。合法软件中这通常会触发 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));
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 9. 企业检测点

9. 企业检测点(配图)

启动管理检测的核心是“基线 + 变更”。单独看到一个 Run 键并不一定危险,但未授权进程、异常路径、远程会话和新增自动启动位置组合起来,风险就很高。

检测面 关注点 事件关联
注册表 Run 键新增、修改、删除 值名、路径、进程、用户、会话
启动目录 文件复制、删除、覆盖 目标路径、文件哈希、签名、父进程
计划任务 新建、更新、删除任务 任务名、触发器、动作路径、运行账户
服务 创建、删除、启动类型变化 服务名、显示名、二进制路径、权限
权限 runas、UAC、管理员上下文 用户确认、会话 ID、父子进程

建议关联逻辑:

异常插件进程建立长连接
  -> 查询启动项状态
  -> 写 Run 键 / 复制 Startup / 注册任务 / 创建服务
  -> 回传变更成功
  -> 状态再次查询显示已启用
  -> 触发持久化风险告警
1
2
3
4
5
6

# 10. 合法练习题

  1. 只阅读源码,整理 COMMAND_STARTUP_* 与函数的对应关系。
  2. 画出 COMMAND_STARTUP_STATUS_REQUEST -> SendStartupStatus -> StartupInfo 的数据流。
  3. 设计一个启动项基线表,字段包含位置、名称、路径、签名、哈希、创建时间、修改时间和责任人。
  4. 写一条防御规则:非授权进程新增自动启动服务,并且二进制路径位于用户临时目录。
  5. 对比 Run 键、启动目录、计划任务、服务的正常用途和滥用风险。
  6. 说明为什么“修改成功后再次查询状态”对检测和复盘有帮助。

# 11. 本课小结

启动管理模块把多个持久化位置合并在一起:Run 键、启动目录、计划任务、系统服务和管理员权限运行。源码结构并不复杂,但安全风险很集中。

企业侧要把它看成“启动项变更能力”,而不是单个 API。检测时要关联启动项位置、目标路径、进程、用户、网络会话和变更结果。下一课进入远程终端模块,重点分析命令输入、隐藏 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
更多文章>