-
-
[原创]SCM 服务与驱动启动类型的枚举
-
发表于: 4天前 957
-
SCM 服务与驱动启动类型的枚举

所有功能均已合并进KswordARK:30bK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6w2f1%4N6G2M7X3c8p5c8g2k6Q4x3V1k6w2f1%4N6G2M7X3c8Q4x3V1j5`.
服务控制管理器保存 Win32 服务、内核驱动和文件系统驱动的管理信息。读取时要分别记录服务类型、启动类型、映像路径和服务账户。Auto 只是其中一种启动类型,不能覆盖 Boot、System、Demand 和 Disabled 的差异。
一、对象关系和完整流程
服务与驱动共享服务数据库中的管理入口,但启动阶段和运行环境不同。Win32 服务由服务控制管理器统一管理。Boot 与 System 类型的驱动可能在服务控制管理器全面接管 Win32 服务之前加载。枚举程序仍可通过服务控制管理器 API 获取两类对象的管理记录。
安装程序或系统组件写入配置 -> 服务数据库保存记录 -> Windows 根据服务类型和启动类型选择处理阶段 -> 加载 Win32 服务或驱动 -> 运行状态随系统活动变化
枚举程序先读取对象列表,再逐项查询静态配置。列表中的当前状态与逐项查询到的配置来自不同调用,需要按服务键名组合。
打开服务控制管理器 -> 枚举 Win32 服务和驱动 -> 保存当前状态 -> 逐项打开服务记录 -> 查询服务类型、启动类型、映像路径和账户 -> 合并结果与错误状态
1. Windows 服务是由 SCM 管理的后台工作单元
服务控制管理器(Service Control Manager,SCM)是 Windows 的服务管理组件。它维护活动服务数据库,并响应启动、停止、查询和配置请求。图形界面中的“服务”列表、sc.exe 命令和服务相关 Win32 API 都通过这一管理机制访问服务信息。
服务通常不依附于某个用户桌面。它可以在系统启动后运行,也可以在满足条件时被请求启动,还可以保持停止状态。服务记录包含服务键名和显示名。键名用于 API 定位,例如 WinDefend。显示名用于界面展示,例如“Microsoft Defender 防病毒服务”。两者用途不同,枚举结果要同时保存。
一个服务的完整配置至少包含以下独立字段。它们回答的问题不同,不能把其中一个字段的值复制到另一个字段的含义上。
| 字段 | 回答的问题 | 常见来源 |
|---|---|---|
| 服务类型 | 这是 Win32 服务、内核驱动还是文件系统驱动? | dwServiceType |
| 启动类型 | 系统在什么阶段或什么条件下尝试启动它? | dwStartType |
| 当前状态 | 此时它正在运行、暂停、等待还是停止? | SERVICE_STATUS_PROCESS |
| 映像路径 | 服务主程序或驱动文件使用了什么原始命令行? | lpBinaryPathName |
| 服务账户 | Win32 服务在什么账户上下文中运行? | lpServiceStartName |
2. Win32 服务和内核驱动共享配置入口,执行环境不同
服务类型决定对象的运行环境。SERVICE_WIN32_OWN_PROCESS 表示服务拥有独立进程,SERVICE_WIN32_SHARE_PROCESS 表示服务由共享进程承载。这两类服务运行在用户态,由 SCM 与服务控制处理函数协作管理。
SERVICE_KERNEL_DRIVER 和 SERVICE_FILE_SYSTEM_DRIVER 表示驱动服务。驱动由内核加载到内核地址空间。文件系统驱动还会参与文件系统栈。它们也通过服务数据库保存名称、映像路径和启动设置,因此可以使用相同的管理 API 查询。它们的启动阶段、错误后果和运行上下文与 Win32 服务不同。
启动类型必须结合对象种类理解。SERVICE_BOOT_START 与 SERVICE_SYSTEM_START 主要用于需要在早期引导阶段加载的驱动,SERVICE_AUTO_START 描述服务控制管理器的自动启动配置,SERVICE_DEMAND_START 表示按需启动,SERVICE_DISABLED 禁止启动请求成功执行。当前状态仍需从枚举的状态结构单独读取。
3. 服务数据库中的配置与注册表中的持久化记录有关联
管理 API 与持久化位置承担不同职责。大多数服务配置持久化在 HKLM\SYSTEM\CurrentControlSet\Services 下的服务键中。应用读取服务配置时应优先使用 SCM API。SCM API 按服务管理语义组织字段,也能避免把注册表文本误解为完整运行状态。
注册表中的 Start、Type、ImagePath 等值是持久化配置字段的一部分。SCM 查询返回的 QUERY_SERVICE_CONFIGW 是面向服务管理语义的结构,其中已经把服务类型、启动类型、二进制路径和账户等信息组织到固定字段。运行状态、进程标识和接受的控制码则来自 EnumServicesStatusExW 的状态数据,不应从 Start 值推断。
4. 枚举反映某个时间点附近的状态
服务集合可能在枚举期间变化。系统组件、安装程序和管理员都可能创建、删除、启动或停止服务。EnumServicesStatusExW 分批返回记录时,下一批列表可能已经变化。逐项调用 OpenServiceW 失败,也可能表示对象刚刚被删除。
枚举结果要保存枚举时间、服务键名、服务显示名、当前状态和每次 API 失败的 Win32 错误码。这些字段可以区分“当前没有返回该对象”“对象存在但无法打开”“对象在读取中变化”三类情况。
5. 服务配置、服务依赖和服务安全描述符是不同字段组
启动类型只是服务配置的一部分。服务还可以声明依赖服务或依赖组,SCM 会在启动时按依赖关系协调顺序。服务失败操作描述异常退出后的恢复行为。服务安全描述符说明谁有权查询、启动、停止、修改或删除服务。这些信息分别属于依赖关系、恢复策略和访问控制,不能从 dwStartType 推导。
服务安全描述符中的 DACL(Discretionary Access Control List,自主访问控制列表)由多条访问控制项组成。SCM 会比较调用进程令牌和 DACL,决定是否授予 OpenServiceW 请求的权限。已经枚举到服务键名,但 SERVICE_QUERY_CONFIG 被拒绝时,要保存“可枚举、不可查询配置”的状态。查询失败不能用来猜测启动方式或服务账户。
6. 服务句柄、普通句柄和驱动对象引用的释放方式不同
句柄要按来源调用对应的释放函数。OpenSCManagerW 与 OpenServiceW 返回 SC_HANDLE,调用方使用 CloseServiceHandle。CreateFileW 返回普通 HANDLE,调用方使用 CloseHandle。内核驱动中的对象引用遵循内核对象引用计数规则。这些资源的释放方式不能互换。
查询服务配置的缓冲区也有自己的生命周期。QueryServiceConfigW 把 QUERY_SERVICE_CONFIGW 以及其中的字符串指针写入调用方分配的连续字节缓冲区。指针只在这块缓冲区仍存在时有效。需要长期保存的文本必须复制到独立存储。保存缓冲区内部地址后再释放缓冲区,会留下无效指针。
7. 一条服务记录应同时保存静态配置和瞬时状态
每条服务记录都要区分静态配置和瞬时状态。静态配置包括服务键名、显示名、服务类型、启动类型、二进制路径、服务账户、依赖和失败操作。瞬时状态包括当前状态、进程标识、接受的控制码和枚举时间。配置与状态都可能变化,两个字段组不能互相覆盖。
当某项 API 返回失败时,要记录服务键名、请求访问掩码、失败函数和错误码。这些字段可以区分“服务没有返回”“当前令牌不能读配置”“服务刚被删除”和“服务处于停止状态”,避免将失败码误标为服务行为结论。
服务数据库中的条目可能因安装、更新、策略或管理员操作而增加或删除。枚举结果是一组带时间戳的观察。比较两次结果时,要按服务键名与服务类型配对。权限变化、系统重启和读取范围变化也要作为差异原因记录。
完整的身份与状态字段让每一行都能回到明确的服务对象、读取时刻和 API 返回状态。
服务键名是 SCM API 使用的稳定定位文本,显示名仅适合人类界面展示。比较或导出时优先保留键名与类型组合,再将显示名作为附加字段,避免本地化显示名或管理员重命名造成错误配对。
二、先打开服务控制管理器
1. SCM 句柄与服务句柄是专用资源
打开服务控制管理器的宽字符 API 返回 SC_HANDLE。空指针表示调用失败。枚举只需要 SC_MANAGER_ENUMERATE_SERVICE,不需要创建、启动、停止或删除服务的权限。
// 意义:打开本机或远程计算机的服务控制管理器数据库。
// 返回:非空 SC_HANDLE 表示成功。nullptr 表示失败。
SC_HANDLE OpenSCManagerW(
LPCWSTR lpMachineName, // 输入:nullptr 表示本机。
LPCWSTR lpDatabaseName, // 输入:nullptr 表示活动服务数据库。
DWORD dwDesiredAccess // 输入:SC_MANAGER_ENUMERATE_SERVICE 等所需权限。
);
// 意义:关闭 OpenSCManagerW 或 OpenServiceW 返回的专用句柄。
// 返回:非零表示成功。不能用 CloseHandle 关闭 SC_HANDLE。
BOOL CloseServiceHandle(SC_HANDLE hSCObject);
// 意义:关闭 CreateFileW、CreateProcessW 等返回的普通 HANDLE。
// 返回:非零表示成功。SC_HANDLE 不属于普通 HANDLE,不能传入此函数。
BOOL CloseHandle(HANDLE hObject);
正确做法:请求最小权限,并在读取结束时调用 CloseServiceHandle。
SC_HANDLE scm = OpenSCManagerW(nullptr, nullptr, SC_MANAGER_ENUMERATE_SERVICE);
if (scm != nullptr) {
// 枚举服务和驱动。
CloseServiceHandle(scm);
}
错误示例:把 SC_HANDLE 交给 CloseHandle。服务控制管理器句柄有专用释放 API,混用关闭函数会留下资源或产生错误状态。
// 错误:SC_HANDLE 必须调用 CloseServiceHandle。
CloseHandle(scm);
三、用可变长度缓冲区枚举服务与驱动
2. 枚举 API 可能分多批返回记录
扩展枚举 API 返回 ENUM_SERVICE_STATUS_PROCESSW 数组。缓冲区不足时,它可能返回部分记录并设置 ERROR_MORE_DATA。lpResumeHandle 保存下一批继续读取的位置。
// 意义:读取 SCM 中满足服务类型和状态筛选条件的服务记录。
// 返回:非零表示本批成功结束。返回 0 且错误码为 ERROR_MORE_DATA 表示还有下一批。
BOOL EnumServicesStatusExW(
SC_HANDLE hSCManager, DWORD InfoLevel, DWORD dwServiceType,
DWORD dwServiceState, LPBYTE lpServices, DWORD cbBufSize,
LPDWORD pcbBytesNeeded, // 输出:下一批所需字节数。
LPDWORD lpServicesReturned, // 输出:当前缓冲区中的记录数量。
LPDWORD lpResumeHandle, // 输入/输出:下一批继续位置。
LPCWSTR pszGroupName
);
// 意义:取得当前线程最近一次失败的 Win32 错误码。
// 返回:当前线程错误码。只应紧跟在文档说明会设置该状态的失败调用后读取。
DWORD GetLastError(void);
正确做法:先用零容量调用探测当前记录集大小,再按返回字节数重新调用。SERVICE_WIN32 | SERVICE_DRIVER 用于覆盖服务和驱动。SERVICE_STATE_ALL 用于保留当前状态差异。两次调用之间服务集合发生变化时,要重新探测并有限重试。
DWORD needed = 0, returned = 0;
EnumServicesStatusExW(
scm, SC_ENUM_PROCESS_INFO, SERVICE_WIN32 | SERVICE_DRIVER,
SERVICE_STATE_ALL, nullptr, 0, &needed, &returned, nullptr, nullptr);
if (GetLastError() == ERROR_MORE_DATA && needed != 0) {
std::vector<BYTE> buffer(needed);
returned = 0;
const BOOL ok = EnumServicesStatusExW(
scm, SC_ENUM_PROCESS_INFO, SERVICE_WIN32 | SERVICE_DRIVER,
SERVICE_STATE_ALL, buffer.data(), static_cast<DWORD>(buffer.size()),
&needed, &returned, nullptr, nullptr);
if (ok != FALSE) {
auto* entries = reinterpret_cast<ENUM_SERVICE_STATUS_PROCESSW*>(buffer.data());
for (DWORD index = 0; index < returned; ++index) {
// 为 entries[index] 打开 SERVICE_QUERY_CONFIG,再读取配置。
}
}
}
错误示例:使用固定的小缓冲区,然后把失败写成“没有服务”。ERROR_MORE_DATA 表示当前记录集超过缓冲区。此时要重新探测并扩大缓冲区,不能把它解释为正常结束。
四、逐项读取启动类型和映像配置
3. 服务配置需要两次查询
打开单个服务的宽字符 API 返回服务句柄。空指针可能表示服务已删除、权限不足或名称无效。只读配置使用 SERVICE_QUERY_CONFIG。
// 意义:在已打开 SCM 中打开一个具体服务或驱动。
// 返回:非空 SC_HANDLE 表示成功。nullptr 表示失败。
SC_HANDLE OpenServiceW(
SC_HANDLE hSCManager, // 输入:OpenSCManagerW 成功返回的句柄。
LPCWSTR lpServiceName, // 输入:服务键名,不是显示名。
DWORD dwDesiredAccess // 输入:SERVICE_QUERY_CONFIG。
);
// 意义:读取服务类型、启动类型、映像路径和服务账户等配置。
// 返回:非零表示成功。首次零容量调用通常返回 0,并给出所需字节数。
BOOL QueryServiceConfigW(
SC_HANDLE hService, LPQUERY_SERVICE_CONFIGW lpServiceConfig,
DWORD cbBufSize, LPDWORD pcbBytesNeeded
);
正确做法:先用零容量调用取得 pcbBytesNeeded。确认错误码为 ERROR_INSUFFICIENT_BUFFER 后,再按字节分配缓冲区。QUERY_SERVICE_CONFIGW 内部的字符串指针指向这块缓冲区。释放缓冲区前,要复制或输出需要的文本。
DWORD requiredBytes = 0;
QueryServiceConfigW(service, nullptr, 0, &requiredBytes);
if (GetLastError() == ERROR_INSUFFICIENT_BUFFER && requiredBytes != 0) {
std::vector<BYTE> storage(requiredBytes);
auto* config = reinterpret_cast<QUERY_SERVICE_CONFIGW*>(storage.data());
if (QueryServiceConfigW(service, config, requiredBytes, &requiredBytes) != FALSE) {
// config->dwServiceType、dwStartType、lpBinaryPathName、lpServiceStartName 有效。
}
}
错误示例:保存缓冲区内部指针,然后释放缓冲区。lpBinaryPathName 与 lpServiceStartName 不拥有独立内存。离开 storage 作用域后再使用它们,会访问无效地址。
五、正确解释服务与驱动启动类型
4. 类型和启动方式需要分别记录
服务类型要保留原始分类。SERVICE_WIN32_OWN_PROCESS、SERVICE_WIN32_SHARE_PROCESS、SERVICE_KERNEL_DRIVER 和 SERVICE_FILE_SYSTEM_DRIVER 表示不同运行环境。内核驱动的 Boot 或 System 启动语义与 Win32 服务的 Auto 不同,不能统一标成“自动启动”。
启动类型要保留原始 dwStartType。SERVICE_BOOT_START、SERVICE_SYSTEM_START、SERVICE_AUTO_START、SERVICE_DEMAND_START 和 SERVICE_DISABLED 分别表示不同的加载或启动配置。当前运行状态来自另一组状态字段,不能由启动类型替代。
映像路径要保存完整原始文本。服务配置中的路径可能包含引号、参数或环境变量。服务账户也属于独立字段。按第一个空格截取路径,会误解带引号的映像名。
逐项查询可能因权限不足而失败。SCM 可以枚举到某个服务,同时拒绝 SERVICE_QUERY_CONFIG。程序要记录服务键名和错误码,再继续处理其它记录。只读查询不应改变服务状态。
完整可运行程序在附件。

31cK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6%4j5h3&6Y4N6$3g2A6j5$3#2Q4x3X3g2D9j5h3&6*7L8%4g2#2i4K6u0W2j5$3!0E0i4K6u0r3K9f1A6H3M7q4j5K6N6%4A6J5y4r3k6U0
[招生]科锐逆向工程师培训(2026年7月3日实地,远程教学同时开班, 第56期)!