Active Setup:按用户首次登录执行的组件初始化

Active Setup:按用户首次登录执行的组件初始化

本篇的目标是只读列出 Active Setup 注册表中的组件来源、组件标识和 StubPath 原文。Active Setup 是 Windows 用来协调用户级组件初始化的注册表机制。用户登录时,系统可依据机器级组件定义和当前用户状态记录判断某项初始化条件。本文只读取登记信息,不运行 StubPath

要想完成上述读取,分为四步:

  1. 定位四项注册表来源:分别打开 HKLMHKCU 的 64 位和 32 位视图。
  2. 枚举组件子键:读取每个来源中的直接子键名,并把子键名和来源绑定。
  3. 按字节长度读取 StubPath:区分值缺失、空值、值过大和 API 读取失败。
  4. 验证类型并显示文本:仅将合法的 REG_SZREG_EXPAND_SZ 数据解码为 UTF-16 文本,保留原始类型和状态。

读取链路为:四项来源 -> 组件子键 -> StubPath 原始字节 -> 类型验证与文本显示。

一、目标对象和四步流程

1. 第一步:定位四项注册表来源

第一步要先区分机器级定义、当前用户状态和注册表视图。HKLM\Software\Microsoft\Active Setup\Installed Components 保存计算机范围的组件定义。HKCU\Software\Microsoft\Active Setup\Installed Components 保存当前用户范围的相关状态。64 位和 32 位视图也要分别读取,因为不同架构的进程可看到不同的软件注册表位置。

同一个 CLSID 样式子键名出现在不同根键或视图时,仍是来源不同的记录。读取时应保留根键、视图、完整路径和组件名,不能只留下一个"已注册"标签。

完成第一步后,已经取得四项独立的读取范围。接下来要在每个范围内找出真正代表组件身份的子键。

2. 第二步:枚举组件子键

第二步要把一级子键名当作组件身份读取。Installed Components 下的直接子键通常使用 CLSID 样式名称,子键名用于连接机器级定义和当前用户状态。StubPath 只是组件子键内可能存在的一个值,不能替代组件身份。

组件子键即使没有 StubPath、值为空或读取失败,也应保留为一条组件记录。完整记录至少包括来源根键、32/64 位视图、组件子键名和完整路径。这样可以区分"组件存在但未设置 StubPath"与"当前来源中没有该组件"。

完成第二步后,每个组件都有来源和身份。接下来需要读取该组件中的 StubPath,同时保持原始数据边界。

3. 第三步:按字节长度读取 StubPath

第三步要把 StubPath 当作类型化注册表值读取。读取时先取得数据类型和字节长度,再以返回的字节数分配缓冲区并读取实际数据。值不存在、值长度为零、值超过限制和读取失败对应不同状态,不能合并为"没有命令"。

VersionLocaleIsInstalled 等其它值也有各自含义,但本次读取目标是组件身份、来源和 StubPath。机器级与用户级记录可用于理解 Active Setup 的配置条件,单次注册表读取不能证明登录时实际处理过某条命令。

完成第三步后,已经获得带有真实长度和类型的原始字节。接下来要判断这些字节能否安全解释为文本。

4. 第四步:验证类型并显示读取状态

第四步要限制文本解码的前提。只有 REG_SZREG_EXPAND_SZ,并且字节长度能够按 UTF-16 宽字符对齐时,原始数据才适合显示为字符串。REG_EXPAND_SZ 里的 %SystemRoot%%ProgramFiles% 等环境变量应先保留原文,环境展开属于另一份派生结果。

读取结果还要保留值类型、原始字节长度和错误状态。这样遇到 REG_BINARY、缺少结尾 NUL、访问拒绝或组件在读取期间变化时,记录能说明问题位于哪一层。

完成第四步后,已得到来源明确、组件身份完整且文本解释受类型约束的读取结果。后续背景说明将这些记录放回 Active Setup 的登录初始化机制中理解。

5. 用户登录时比较的是组件身份和状态,不能只看 StubPath

机器定义与用户状态通过组件身份配对。系统使用组件子键名作为配对身份,机器范围的组件定义提供版本和安装相关属性,当前用户范围的同名子键提供该用户已经处理到的状态。读取过程应把两个位置的组件名建立关联表,同时保留双方各自的完整键路径、视图、值类型和读取时间。

Version 通常是文本版本字段,IsInstalled 常以数值或类型化数据表达安装状态,Locale 可表达区域或语言相关条件。每个字段都需要按真实注册表类型读取:字符串只有在 UTF-16 长度对齐时解码,REG_DWORD 只有数据恰好四字节时读取为数值,未知类型保留原始字节。不能因为 StubPath 存在就跳过版本、安装状态和当前用户记录。

text 复制代码
机器级组件 {A} 的 Version = 1,0,0,0 -> 与当前用户组件 {A} 的 Version = 0,9,0,0 比较 -> 两侧记录存在,实际登录处理仍需独立观察

上图中的"结果"只说明配置比较所需的字段。系统实际是否运行初始化、命令是否成功、用户是否已经退出登录流程,需要登录时的进程、事件或安装记录作为独立证据。

6. 组件子键枚举需要处理 UTF-16 名称长度和并发变化

组件标识枚举需要处理 UTF-16 长度和并发变化。RegEnumKeyExW 只返回当前键的直接子键名,输入输出长度以 UTF-16 宽字符个数计,不包含结尾 NUL。初始缓冲区可来自 RegQueryInfoKeyW 的最长子键名,也可以从合理小容量开始。收到 ERROR_MORE_DATA 时扩大名称缓冲区并重试同一索引。

ERROR_NO_MORE_ITEMS 表示当前枚举结束。ERROR_ACCESS_DENIED 表示当前令牌没有获得该来源的读取权限。组件在枚举和打开之间被删除时,RegOpenKeyExW 可能失败。这应生成"子键在读取期间变化"的记录,继续其它索引,不把失败替换为空组件。

cpp 复制代码
HKEY parent = nullptr;
const LSTATUS opened = RegOpenKeyExW(
    HKEY_LOCAL_MACHINE, // 输入:机器范围根键。系统所有,调用方不关闭。
    componentPath,      // 输入:NUL 结尾 UTF-16 Active Setup 父路径。
    0,                  // 输入:保留,必须为 0。
    KEY_ENUMERATE_SUB_KEYS | KEY_WOW64_64KEY, // 输入:最小枚举权限与明确视图。
    &parent);           // 输出:成功时得到 HKEY,本段负责关闭。
if (opened == ERROR_SUCCESS) {
    std::vector<wchar_t> name(256, L'\0');
    DWORD nameCapacity = static_cast<DWORD>(name.size()); // 容量单位为 UTF-16 字符数。
    const LSTATUS status = RegEnumKeyExW(
        parent, 0, name.data(), &nameCapacity, nullptr, nullptr, nullptr, nullptr);
    if (status == ERROR_SUCCESS) {
        // 以 nameCapacity 个字符复制组件名,再单独打开子键和读取其属性。
    } else if (status == ERROR_MORE_DATA) {
        // 正确:扩大 name 后重试索引 0。不能把它视为没有组件。
    }
    RegCloseKey(parent); // 释放本段成功打开的 HKEY。
}

7. StubPath 文本需要保留原文、类型和命令边界

StubPath 文本不能直接当作已验证程序路径。StubPath 若为 REG_EXPAND_SZ,可能包含 %SystemRoot%%ProgramFiles% 等环境变量,原文和当前环境下的展开结果是两份数据。若文本包含引号、参数、rundll32 调用或其它解释器形式,文件路径、参数和工作目录也不能用第一个空格简单切分。

正确记录至少包含组件身份、来源根键、视图、值名、REG 类型、原始字节长度、严格按返回长度得到的文本以及可选展开结果。路径是否存在、目标文件签名、登录时是否执行和命令返回状态属于后续验证阶段。这样即使文本为空、缺少 NUL、类型异常或展开失败,组件记录仍然完整可追溯。

8. 删除、修改与读取的影响范围不同

键和值属于不同对象。删除 StubPath 值会保留组件身份和其它属性。删除整个组件子键会同时移除版本、安装状态和初始化文本。修改当前用户状态又只影响该用户范围。每种动作对后续登录初始化的含义不同,检查或维护前必须备份完整键树和类型化值。

只读枚举不需要创建、写入或删除注册表权限。读取过程应请求 KEY_QUERY_VALUEKEY_ENUMERATE_SUB_KEYS 等最小权限,把访问拒绝如实记录。这样可以观察计算机级和用户级差异,又不会因为检查动作改变用户下次登录时的配置条件。

9. 读取状态应区分键缺失、值缺失、空值和类型异常

读取状态要准确区分"没有文本"的不同来源。父键不存在时,RegOpenKeyExW 返回 ERROR_FILE_NOT_FOUND,表示当前来源没有组件目录。父键存在而某个组件子键不存在,表示该组件没有出现在该来源。组件子键存在而 StubPath 缺失,表示身份和其它属性仍可读取。StubPath 存在但长度为零,表示一个空值。类型为 REG_BINARY 或长度不能组成 UTF-16 时,表示类型或格式异常。

这五种状态都不应简化为"没有启动项"。它们对应不同的配置层级和维护动作。输出时保存根键、视图、组件名、值名、状态码和读取时间。后续比较两个用户或两次登录时,才能准确判断变化发生在组件树、属性值还是解码阶段。

10. 组件登记、文件检查和登录执行应使用三组证据

注册表配置不能直接推导登录结果。第一组证据是注册表组件登记:它回答机器和用户范围各自保存了什么。第二组证据是文本解析与文件检查:它回答 StubPath 原文能否展开、候选路径是否存在、文件架构和签名状态如何。第三组证据是实际登录运行:它回答相应初始化是否被处理、何时启动、返回什么结果。

三组证据的时间点和权限范围可能不同。一个用户的 HKCU 状态只对该用户有效。机器级定义对其它用户也可能可见。文件检查受当前访问令牌影响。登录运行时记录还受系统版本和策略影响。把它们分字段保留,文章读者可以看见从"配置存在"到"实际执行"的每一步距离。

11. HKEY、REGSAM、DWORD 和 BSTR 等名称应按责任理解

代码中的 HKEY 是注册表键句柄,成功打开后由调用方关闭。REGSAM 是请求访问权限的位掩码。DWORD 是 32 位无符号整数,注册表 API 中既可能表示字符数也可能表示字节数。已知文件夹、COM 或 Shell API 若返回 BSTRPWSTR 等指针,则必须按各自文档规定的分配器释放,不能把注册表缓冲区和 COM 字符串的释放规则混用。

这些名称看似属于实现细节,实际决定了读取过程是否会泄漏句柄、截断文本或释放错误内存。文章中的每次 API 调用都应说明哪一方分配、哪一方释放、长度采用什么单位以及失败后还能否使用输出指针。

二、第一步:定位四项注册表来源

第一步要以最小权限打开 Active Setup 的父键。HKLM 表示计算机范围,HKCU 表示当前用户范围。两个根键都要使用 64 位和 32 位视图打开,因此读取范围共有四项。父键由系统维护,成功打开后返回的 HKEY 由调用方关闭。

四项来源中即使有某一项父键不存在,也应记录该来源的状态后继续读取其它来源。父键不存在只能说明当前范围未登记 Active Setup 组件,不能替代其它范围的结果。

cpp 复制代码
// 意义:打开注册表子键。
// 返回:ERROR_SUCCESS 表示成功。其它返回值是 Win32 错误码。
//        成功时 phkResult 接收 HKEY,调用方必须关闭。
LSTATUS RegOpenKeyExW(
    HKEY hKey, LPCWSTR lpSubKey, DWORD ulOptions,
    REGSAM samDesired, PHKEY phkResult
);

// 意义:关闭打开成功的键句柄。
// 返回:ERROR_SUCCESS 表示成功。
LSTATUS RegCloseKey(HKEY hKey);
cpp 复制代码
const struct { HKEY root; REGSAM view; } sources[] = {
    { HKEY_LOCAL_MACHINE, KEY_WOW64_64KEY },
    { HKEY_LOCAL_MACHINE, KEY_WOW64_32KEY },
    { HKEY_CURRENT_USER, KEY_WOW64_64KEY },
    { HKEY_CURRENT_USER, KEY_WOW64_32KEY },
};

完成第一步后,读取范围已经固定为四个互不混淆的来源。下一步需要在每个来源中找到组件身份,才能知道后续读取的值属于哪个组件。

三、第二步:枚举组件子键

组件名称长度不应由固定数组限制。RegEnumKeyExW 的名称长度单位是 UTF-16 字符数,返回 ERROR_MORE_DATA 时需要扩大名称缓冲区后重试。

cpp 复制代码
// 意义:按索引读取一级子键名。
// 返回:ERROR_SUCCESS 表示成功。ERROR_NO_MORE_ITEMS 表示枚举结束。
//        ERROR_MORE_DATA 表示 lpName 的字符容量不足。
LSTATUS RegEnumKeyExW(
    HKEY hKey, DWORD dwIndex, LPWSTR lpName, LPDWORD lpcchName,
    LPDWORD lpReserved, LPWSTR lpClass, LPDWORD lpcchClass,
    PFILETIME lpftLastWriteTime
);

正确代码用最小枚举权限打开父键,从初始容量开始扩容。父键只允许子键枚举时仍可继续遍历,不依赖额外的键查询权限。

cpp 复制代码
std::vector<wchar_t> name(256, L'\0');
DWORD chars = static_cast<DWORD>(name.size());
LSTATUS status = RegEnumKeyExW(parentKey, index, name.data(), &chars,
                                nullptr, nullptr, nullptr, nullptr);
if (status == ERROR_SUCCESS) {
    std::wstring component(name.data(), chars);
    std::wstring childPath = parentPath + L"\\" + component;
}
cpp 复制代码
// 错误示例:只在 StubPath 存在时保存组件。
if (QueryStubPath(childKey)) {
    records.push_back(component);
}

完成第二步后,每条记录都有来源、视图和组件子键名。下一步读取 StubPath 时,需要以字节数为边界,避免把未知长度的数据当作字符串。

四、第三步:按字节长度读取 StubPath

StubPath 是组件子键中的命令文本字段。先查询字节长度,再读取原始数据。值不存在、类型错误、超出读取上限和读取失败都要与空字符串区分。

cpp 复制代码
// 意义:读取注册表值的类型和原始数据。
// 返回:ERROR_SUCCESS 表示成功。ERROR_FILE_NOT_FOUND 表示值未设置。
//        ERROR_MORE_DATA 表示 lpData 的字节容量不足。
LSTATUS RegQueryValueExW(
    HKEY hKey, LPCWSTR lpValueName, LPDWORD lpReserved,
    LPDWORD lpType, LPBYTE lpData, LPDWORD lpcbData
);
cpp 复制代码
DWORD type = REG_NONE;
DWORD byteCount = 0;
LSTATUS status = RegQueryValueExW(childKey, L"StubPath", nullptr,
                                   &type, nullptr, &byteCount);
if (status == ERROR_SUCCESS) {
    std::vector<BYTE> bytes(byteCount);
    DWORD copiedBytes = byteCount;
    status = RegQueryValueExW(childKey, L"StubPath", nullptr, &type,
                               bytes.empty() ? nullptr : bytes.data(), &copiedBytes);
    if (status == ERROR_SUCCESS) bytes.resize(copiedBytes);
}

文本只在 REG_SZREG_EXPAND_SZ 且字节数满足 UTF-16 对齐时转换。REG_EXPAND_SZ%SystemRoot% 等标记应先保留原始文本,环境展开属于另一份派生结果。

cpp 复制代码
// 错误示例:把 StubPath 当作组件子键的唯一身份。
std::wstring identity = decodedStubPath;
// 组件 CLSID、机器级或当前用户来源、注册表视图都会丢失。

完成第三步后,记录中已有 StubPath 的原始数据、类型和读取状态。下一步只对满足字符串条件的数据进行解码,并保留不能解码的原因。

五、第四步:验证类型并显示读取状态

组件清理或审计时需要以组件子键为单位考虑。删除单个 StubPath 值与删除整个组件注册树具有不同效果,记录中应保留完整组件路径,不能只保存命令文本。

文本只应在 REG_SZREG_EXPAND_SZ 且字节数为 sizeof(wchar_t) 整数倍时解码。值为 REG_BINARY、长度未对齐或 API 返回错误时,应显示类型、字节数和错误码,不应伪造字符串结果。NUL、回车和换行等控制字符也应转义显示,避免它们改变输出行的边界。

完成第四步后,记录可以同时表达组件是否存在、值是否存在、数据能否解码和文本原文。机器级注册与当前用户状态共同描述按用户执行的条件,单独读取任一侧都不能证明某次登录已经实际处理过初始化。

完整可运行程序在附件

wangweicm.lanzouu.com/ivlpf3yj1m0...

相关推荐
zandy10115 小时前
体验家 XMPlus 问卷答题体验优化与完成率提升引擎:从交互设计到行为心理学的工程化实践
windows·microsoft·交互
独隅7 小时前
DevEco Code 在 Windows/MacOS 双系统上的完整使用指南
ide·人工智能·windows·macos·华为·harmonyos
寒水馨7 小时前
Windows下载、安装electron-v43.2.0(附安装包electron-v43.2.0-win32-x64.zip)
javascript·windows·typescript·electron·跨平台·桌面应用·chromium
x10n98 小时前
常用 Windows 数据恢复工具横向汇总|误删文件、分区损坏自救软件清单
windows·数据恢复·数据恢复工具
geovindu8 小时前
java: Backtracking Algorithm
java·开发语言·windows·后端·算法·回溯算法
西安景驰电子11 小时前
PTP IEEE1588 时钟服务器详解
运维·服务器·网络·数据库·windows·安全
糖果店的幽灵1 天前
langgraph的 MessagesState 解读
java·开发语言·人工智能·windows·langgraph
骑上单车去旅行1 天前
MD5校验对比脚本
linux·服务器·windows
FreeBuf_1 天前
Windows NT OS内核提权漏洞PoC已公开
windows