Credential Provider、Filter 与 PLAP 的 CLSID 枚举程序的目标是只读取注册表列出 Credential Provider、Credential Provider Filter 与 PLAP Provider 的 CLSID、默认名称和 COM 服务器注册位置。它不创建 Provider也不触发登录或身份验证。要想得到这份登记信息分为四步确定三类 Provider 注册根并分别选择 64 位和 32 位注册表视图。枚举每个根键下的 CLSID 子键读取子键默认名称和原始值类型。验证 CLSID 文本并保留 Provider 类型、根键和视图等来源信息。使用有效 CLSID 查询当前视图中的用户级和机器级 COM 服务器注册。一、目标、对象关系和完整流程Credential Provider、Credential Provider Filter 与 PLAP Provider 都以 CLSID 子键登记在Authentication注册表树中。登录界面枚举候选 ProviderFilter 决定场景可见性PLAP 支持登录前访问准备。COM Classes 注册再提供 DLL 或 EXE 服务器位置。三类 Provider 注册根与两个视图 - CLSID 子键和默认名称 - 有效 CLSID 与来源信息 - 当前视图的 COM 服务器注册第一步决定读取范围避免把 32 位和 64 位注册数据混为一组。第二步取得每条登记记录的原始事实。第三步筛掉不能作为 COM 类标识使用的文本并让每条记录保留来源。第四步才根据有效 CLSID 查找可能的实现位置。静态注册记录不能证明组件会显示、激活或成功完成身份验证。选择根键和视图 - 枚举 CLSID 子键 - 验证并保留来源 - 查询 Classes 中的 InprocServer32 或 LocalServer321. Credential Provider 为登录界面提供凭据收集组件登录界面与身份验证系统有不同分工。Windows 登录界面由 LogonUI 等组件承载它向 Credential Provider 查询可用的凭据方案。Provider 可以提供用户名密码、智能卡、Windows Hello 等不同类型的凭据磁贴并将收集到的数据按系统约定序列化后交给后续身份验证流程。Provider 是 COM 组件。登录界面从 Credential Providers 注册表根读取 CLSID再根据 CLSID 在 Classes 注册中定位 COM 服务器并创建对象。CLSID 表示类身份默认值通常只是显示名称。真正的实现位置来自InprocServer32或LocalServer32等 COM 注册子键。LogonUI 登录界面 - Credential Provider 注册根 - CLSID - COM Classes 注册 - InprocServer32进程内 DLL或 LocalServer32独立 EXE 服务器枚举到一个 Provider CLSID 只证明它被登记为候选组件。是否会在当前登录场景显示、是否通过筛选、是否创建成功以及用户是否提交凭据分别取决于系统策略、会话类型和运行时调用结果。2. Provider、Filter 和 PLAP Provider 的职责不同三类根键的职责需要分别解释。Credential Provider 负责提供具体凭据和磁贴。Credential Provider Filter 参与判断哪些 Provider 在某个使用场景中可见或可用。PLAPPre-Logon Access Provider面向用户登录前的网络或访问准备场景。三种类型可使用相同的 COM 基础设施接口职责和触发场景不同。类型注册根主要职责Credential ProviderCredential Providers提供凭据磁贴、字段和凭据序列化。Credential Provider FilterCredential Provider Filters对给定使用场景筛选 Provider 的可用性。PLAP ProviderPLAP Providers在用户登录前提供访问准备相关能力。相同 CLSID 出现在多个根键时应生成多条带类型标签的记录。只按 CLSID 去重会丢失“它作为 Provider、Filter 还是 PLAP 被登记”的语义。3. 使用场景决定登录界面请求哪些能力使用场景决定登录界面请求哪些能力。登录、解锁、凭据 UI 和远程连接等场景需要的凭据交互不同。登录界面将场景信息交给 Provider 和 Filter由组件决定是否支持该场景以及要显示什么字段。这意味着注册表检查无法单独回答“用户会看到哪个磁贴”。静态配置能够给出已注册的 CLSID、默认名称和 COM 服务器文本。策略、Filter 返回结果、设备状态、用户状态与当前登录场景共同决定实际可见集合。4. CLSID、默认名称和服务器位置是三类字段CLSID、默认名称和服务器位置要保留各自含义。Provider 子键名是 CLSID。子键默认值可提供名称或说明。COM Classes 根下的InprocServer32或LocalServer32默认值提供服务器注册文本。它们位于不同注册表树不能用一个字符串字段覆盖。HKEY_CLASSES_ROOT是用户级和机器级 Classes 注册的合并视图。要判断数据来自哪个范围应分别查询HKCU\Software\Classes\CLSID与HKLM\Software\Classes\CLSID并分别记录 32 位和 64 位视图。进程内服务器还需要与宿主进程架构兼容因此视图标签不能省略。5. 静态枚举与实际凭据处理需要不同证据注册、加载和使用需要分别判断。子键存在说明 Provider 类型已登记。服务器键存在说明 COM 注册文本已找到。文件验证可以说明候选文件是否存在或签名状态如何。实际 COM 对象是否能激活、接口是否实现正确、Filter 是否允许显示、凭据是否被接受必须由运行时调用或系统日志独立证明。6. 登录界面、LSA 与认证包在凭据提交后继续完成不同职责Credential Provider 不直接决定身份验证结果。Provider 的职责是收集字段、构造凭据序列化数据并向登录界面报告状态。登录界面把序列化结果交给后续的 Windows 身份验证路径。LSALocal Security Authority本地安全机构及其认证包负责协议验证、令牌建立和安全策略处理。Provider 注册存在或显示磁贴不等于认证包已经接受某次凭据。用户输入字段 - Credential Provider 收集与序列化 - LogonUI 协调登录交互 - LSA 与认证包验证身份和策略 - 建立用户令牌与登录会话这条流程要求审计记录明确边界。注册表和 COM 服务器信息属于 Provider 发现层。令牌、会话和认证事件属于身份验证运行层。读者不能从某个 CLSID 或 DLL 路径推断特定用户的认证是否成功。7. COM 资源、注册表句柄和 UTF-16 长度各有释放与单位规则COM 资源、注册表句柄和字符串有不同释放规则。HKEY是RegOpenKeyExW成功返回的注册表键引用调用方RegCloseKey。COM 接口指针由Release减少引用计数。BSTR由SysFreeString释放。三者不能交叉释放。Provider 子键的默认值数据由注册表 API 写入调用方缓冲区缓冲区本身通常由std::vectorBYTE或调用方数组管理。RegEnumKeyExW的子键名长度是 UTF-16 宽字符数RegQueryValueExW的值数据长度是字节数。NUL 只是字符串终止字符返回数据不保证总有 NUL。只有确认 REG 类型和长度对齐后才可解码。成功打开的 Provider 子键、得到的文本副本和从 Classes 根取得的服务器记录都应有明确的所有者和使用期。8. 标准读取先保留 Provider 身份再定位可能的服务器标准读取要保留每一层的关联字段。先对三类根键分别以两个视图枚举 CLSID 子键。每条记录保存 Provider 类型、根键、视图、CLSID 子键名、默认值类型和原始数据。随后只对格式有效的 CLSID 查询用户级和机器级 Classes 根分别保存InprocServer32与LocalServer32默认值。失败时保留 HRESULT 或 Win32 错误不用空字符串替代。错误做法是拿到默认显示名称后立即丢弃 CLSID。名称可重复、可缺失或可本地化Classes 查询需要 CLSID 才能定位服务器。Provider 类型和视图也决定该注册项在什么登录架构上下文中可见。二、第一步确定三类 Provider 根键和注册表视图Credential Provider、Filter、PLAP Provider 是三种独立类型。相同 CLSID 出现在不同根键时仍保留类型身份不能按 CLSID 合并为单一条目。constwchar_t*roots[]{LSoftware\\Microsoft\\Windows\\CurrentVersion\\Authentication\\Credential Providers,LSoftware\\Microsoft\\Windows\\CurrentVersion\\Authentication\\Credential Provider Filters,LSoftware\\Microsoft\\Windows\\CurrentVersion\\Authentication\\PLAP Providers,};注册表读取同时覆盖 64 位和 32 位视图。认证组件存在于一个视图并不能推断另一视图也存在输出中应保留视图标签。// 意义从注册表根键或父键打开一个子键。// 返回ERROR_SUCCESS 表示成功。ERROR_FILE_NOT_FOUND 表示路径不存在。// ERROR_ACCESS_DENIED 表示当前令牌没有 samDesired 所请求的权限。// 成功时 *phkResult 是调用方拥有的 HKEY使用完必须调用 RegCloseKey。LSTATUSRegOpenKeyExW(HKEY hKey,// 输入HKEY_LOCAL_MACHINE 等预定义根键或已打开父键。预定义根键不关闭。LPCWSTR lpSubKey,// 输入NUL 结尾 UTF-16 相对路径。nullptr 表示 hKey 本身。DWORD ulOptions,// 输入保留参数必须为 0。REGSAM samDesired,// 输入KEY_ENUMERATE_SUB_KEYS、KEY_QUERY_VALUE 与 KEY_WOW64_* 视图标志。PHKEY phkResult// 输出非空指针成功时接收新句柄。失败后不得使用。);// 意义释放调用方拥有的注册表键句柄。// 返回ERROR_SUCCESS 表示成功。关闭后 hKey 立即失效。LSTATUSRegCloseKey(HKEY hKey// 输入/释放由 RegOpenKeyExW 成功返回的 HKEY不能是预定义根键。);完成第一步后读取范围已经包含三类 Provider 和两个独立的注册表视图。下一步需要在每个范围内找出实际登记的 CLSID 子键并把默认名称连同原始类型一起保留。三、第二步枚举 CLSID 子键并读取默认名称每个 Provider 根键按一级子键枚举。子键名长度使用可扩容缓冲区ERROR_MORE_DATA时扩大容量。ERROR_NO_MORE_ITEMS表示枚举结束。// 意义按零基索引读取一个直接 Provider 子键名。// 返回ERROR_SUCCESS 表示成功。ERROR_NO_MORE_ITEMS 表示到达最后。// ERROR_MORE_DATA 表示 *lpcchName 指定的 UTF-16 字符容量不足。LSTATUSRegEnumKeyExW(HKEY hKey,// 输入已打开且具有 KEY_ENUMERATE_SUB_KEYS 的父键。不转移所有权。DWORD dwIndex,// 输入从零开始的直接子键索引。注册表变化时索引不稳定。LPWSTR lpName,// 输出子键名 UTF-16 缓冲区。不可为 nullptr。LPDWORD lpcchName,// 输入/输出lpName 容量/实际长度单位 wchar_t返回长度不含 NUL。LPDWORD lpReserved,// 输入保留参数必须为 nullptr。LPWSTR lpClass,// 输出可选类名缓冲区。本节不读取传 nullptr。LPDWORD lpcchClass,// 输入/输出类名容量/长度。lpClass 为 nullptr 时传 nullptr。PFILETIME lpftLastWriteTime// 输出可选最后写入时间。本节传 nullptr。);默认值通过空值名读取。默认值存在时保存原始类型和文本。默认值缺失时仍保留 CLSID 子键作为 Provider 身份。// 意义读取默认值或指定值的类型和原始字节。// 返回ERROR_SUCCESS 表示成功。ERROR_FILE_NOT_FOUND 表示值未设置。// ERROR_MORE_DATA 表示 *lpcbData 提供的字节容量不足。LSTATUSRegQueryValueExW(HKEY hKey,// 输入已打开且有 KEY_QUERY_VALUE 权限的 Provider 子键。不转移所有权。LPCWSTR lpValueName,// 输入NUL 结尾 UTF-16 值名。nullptr 或 L 表示默认值。LPDWORD lpReserved,// 输入保留参数必须为 nullptr。LPDWORD lpType,// 输出REG_SZ、REG_BINARY 等类型。本节先读取类型再决定解码方式。LPBYTE lpData,// 输出原始字节缓冲区。只查询长度时传 nullptr。LPDWORD lpcbData// 输入/输出lpData 容量/实际长度单位始终是字节。不可为 nullptr。);// 正确示范完整读取 Provider 子键默认值并保留“默认值缺失”与“读取失败”的差异。DWORD typeREG_NONE;DWORD requiredBytes0;LSTATUS statusRegQueryValueExW(providerKey,L,nullptr,type,nullptr,requiredBytes);if(statusERROR_FILE_NOT_FOUND){// Provider CLSID 子键仍然存在。仅默认显示名称未设置。}elseif(statusERROR_SUCCESS){std::vectorBYTEraw(requiredBytes);// requiredBytes 的单位是字节。for(intattempt0;attempt!3;attempt){DWORD actualBytesstatic_castDWORD(raw.size());statusRegQueryValueExW(providerKey,L,nullptr,type,raw.empty()?nullptr:raw.data(),actualBytes);if(statusERROR_MORE_DATA){raw.resize(actualBytes);// API 指出当前字节容量不足使用新的字节容量重试。continue;}if(statusERROR_SUCCESS){raw.resize(actualBytes);// 仅当 type 为 REG_SZ/REG_EXPAND_SZ 且 actualBytes 能整除 sizeof(wchar_t) 时再解码 UTF-16。}break;// 成功或不可重试错误都结束。失败状态和 CLSID 一同记录。}}// 错误示例只输出默认名称CLS ID 和 Provider 类型都丢失。record.namedefaultValue;完成第二步后每个根键中的子键名、默认值和原始数据都已读出。下一步要确认子键名能否作为 CLSID 使用同时保留它属于哪类 Provider、哪个根键和哪个视图。四、第三步验证 CLSID 并保留登记来源子键名符合{8-4-4-4-12}结构时作为 CLSID。结构验证通过后分别查询用户级和机器级 Classes 根中的InprocServer32、LocalServer32默认值。boolIsClsidText(std::wstring_view text){returntext.size()38text.front()L{text.back()L}text[9]L-text[14]L-text[19]L-text[24]L-;}conststd::wstring baseLSoftware\\Classes\\CLSID\\clsid;conststd::wstring inprocbaseL\\InprocServer32;conststd::wstring localbaseL\\LocalServer32;完成第三步后只有格式正确的 CLSID 会进入 COM 查询记录也仍能追溯到原始 Provider 类型和注册表视图。接下来使用这些 CLSID 分别查询用户级与机器级 Classes 注册得到可能的服务器文本。五、第四步查询当前视图中的 COM 服务器注册服务器查询需要在当前 32/64 位视图下分别进行。用户级 Classes 与机器级 Classes 同时读取InprocServer32与LocalServer32都作为独立注册结果输出。InprocServer32通常记录进程内实现路径LocalServer32通常记录本地服务器命令文本。二者是 COM 注册数据环境变量展开、文件存在性、架构和签名验证属于后续独立步骤。完整可运行程序在附件https://wangweicm.lanzouu.com/iT2xF3yqfb4j
Credential Provider、Filter 与 PLAP 的 CLSID 枚举
Credential Provider、Filter 与 PLAP 的 CLSID 枚举程序的目标是只读取注册表列出 Credential Provider、Credential Provider Filter 与 PLAP Provider 的 CLSID、默认名称和 COM 服务器注册位置。它不创建 Provider也不触发登录或身份验证。要想得到这份登记信息分为四步确定三类 Provider 注册根并分别选择 64 位和 32 位注册表视图。枚举每个根键下的 CLSID 子键读取子键默认名称和原始值类型。验证 CLSID 文本并保留 Provider 类型、根键和视图等来源信息。使用有效 CLSID 查询当前视图中的用户级和机器级 COM 服务器注册。一、目标、对象关系和完整流程Credential Provider、Credential Provider Filter 与 PLAP Provider 都以 CLSID 子键登记在Authentication注册表树中。登录界面枚举候选 ProviderFilter 决定场景可见性PLAP 支持登录前访问准备。COM Classes 注册再提供 DLL 或 EXE 服务器位置。三类 Provider 注册根与两个视图 - CLSID 子键和默认名称 - 有效 CLSID 与来源信息 - 当前视图的 COM 服务器注册第一步决定读取范围避免把 32 位和 64 位注册数据混为一组。第二步取得每条登记记录的原始事实。第三步筛掉不能作为 COM 类标识使用的文本并让每条记录保留来源。第四步才根据有效 CLSID 查找可能的实现位置。静态注册记录不能证明组件会显示、激活或成功完成身份验证。选择根键和视图 - 枚举 CLSID 子键 - 验证并保留来源 - 查询 Classes 中的 InprocServer32 或 LocalServer321. Credential Provider 为登录界面提供凭据收集组件登录界面与身份验证系统有不同分工。Windows 登录界面由 LogonUI 等组件承载它向 Credential Provider 查询可用的凭据方案。Provider 可以提供用户名密码、智能卡、Windows Hello 等不同类型的凭据磁贴并将收集到的数据按系统约定序列化后交给后续身份验证流程。Provider 是 COM 组件。登录界面从 Credential Providers 注册表根读取 CLSID再根据 CLSID 在 Classes 注册中定位 COM 服务器并创建对象。CLSID 表示类身份默认值通常只是显示名称。真正的实现位置来自InprocServer32或LocalServer32等 COM 注册子键。LogonUI 登录界面 - Credential Provider 注册根 - CLSID - COM Classes 注册 - InprocServer32进程内 DLL或 LocalServer32独立 EXE 服务器枚举到一个 Provider CLSID 只证明它被登记为候选组件。是否会在当前登录场景显示、是否通过筛选、是否创建成功以及用户是否提交凭据分别取决于系统策略、会话类型和运行时调用结果。2. Provider、Filter 和 PLAP Provider 的职责不同三类根键的职责需要分别解释。Credential Provider 负责提供具体凭据和磁贴。Credential Provider Filter 参与判断哪些 Provider 在某个使用场景中可见或可用。PLAPPre-Logon Access Provider面向用户登录前的网络或访问准备场景。三种类型可使用相同的 COM 基础设施接口职责和触发场景不同。类型注册根主要职责Credential ProviderCredential Providers提供凭据磁贴、字段和凭据序列化。Credential Provider FilterCredential Provider Filters对给定使用场景筛选 Provider 的可用性。PLAP ProviderPLAP Providers在用户登录前提供访问准备相关能力。相同 CLSID 出现在多个根键时应生成多条带类型标签的记录。只按 CLSID 去重会丢失“它作为 Provider、Filter 还是 PLAP 被登记”的语义。3. 使用场景决定登录界面请求哪些能力使用场景决定登录界面请求哪些能力。登录、解锁、凭据 UI 和远程连接等场景需要的凭据交互不同。登录界面将场景信息交给 Provider 和 Filter由组件决定是否支持该场景以及要显示什么字段。这意味着注册表检查无法单独回答“用户会看到哪个磁贴”。静态配置能够给出已注册的 CLSID、默认名称和 COM 服务器文本。策略、Filter 返回结果、设备状态、用户状态与当前登录场景共同决定实际可见集合。4. CLSID、默认名称和服务器位置是三类字段CLSID、默认名称和服务器位置要保留各自含义。Provider 子键名是 CLSID。子键默认值可提供名称或说明。COM Classes 根下的InprocServer32或LocalServer32默认值提供服务器注册文本。它们位于不同注册表树不能用一个字符串字段覆盖。HKEY_CLASSES_ROOT是用户级和机器级 Classes 注册的合并视图。要判断数据来自哪个范围应分别查询HKCU\Software\Classes\CLSID与HKLM\Software\Classes\CLSID并分别记录 32 位和 64 位视图。进程内服务器还需要与宿主进程架构兼容因此视图标签不能省略。5. 静态枚举与实际凭据处理需要不同证据注册、加载和使用需要分别判断。子键存在说明 Provider 类型已登记。服务器键存在说明 COM 注册文本已找到。文件验证可以说明候选文件是否存在或签名状态如何。实际 COM 对象是否能激活、接口是否实现正确、Filter 是否允许显示、凭据是否被接受必须由运行时调用或系统日志独立证明。6. 登录界面、LSA 与认证包在凭据提交后继续完成不同职责Credential Provider 不直接决定身份验证结果。Provider 的职责是收集字段、构造凭据序列化数据并向登录界面报告状态。登录界面把序列化结果交给后续的 Windows 身份验证路径。LSALocal Security Authority本地安全机构及其认证包负责协议验证、令牌建立和安全策略处理。Provider 注册存在或显示磁贴不等于认证包已经接受某次凭据。用户输入字段 - Credential Provider 收集与序列化 - LogonUI 协调登录交互 - LSA 与认证包验证身份和策略 - 建立用户令牌与登录会话这条流程要求审计记录明确边界。注册表和 COM 服务器信息属于 Provider 发现层。令牌、会话和认证事件属于身份验证运行层。读者不能从某个 CLSID 或 DLL 路径推断特定用户的认证是否成功。7. COM 资源、注册表句柄和 UTF-16 长度各有释放与单位规则COM 资源、注册表句柄和字符串有不同释放规则。HKEY是RegOpenKeyExW成功返回的注册表键引用调用方RegCloseKey。COM 接口指针由Release减少引用计数。BSTR由SysFreeString释放。三者不能交叉释放。Provider 子键的默认值数据由注册表 API 写入调用方缓冲区缓冲区本身通常由std::vectorBYTE或调用方数组管理。RegEnumKeyExW的子键名长度是 UTF-16 宽字符数RegQueryValueExW的值数据长度是字节数。NUL 只是字符串终止字符返回数据不保证总有 NUL。只有确认 REG 类型和长度对齐后才可解码。成功打开的 Provider 子键、得到的文本副本和从 Classes 根取得的服务器记录都应有明确的所有者和使用期。8. 标准读取先保留 Provider 身份再定位可能的服务器标准读取要保留每一层的关联字段。先对三类根键分别以两个视图枚举 CLSID 子键。每条记录保存 Provider 类型、根键、视图、CLSID 子键名、默认值类型和原始数据。随后只对格式有效的 CLSID 查询用户级和机器级 Classes 根分别保存InprocServer32与LocalServer32默认值。失败时保留 HRESULT 或 Win32 错误不用空字符串替代。错误做法是拿到默认显示名称后立即丢弃 CLSID。名称可重复、可缺失或可本地化Classes 查询需要 CLSID 才能定位服务器。Provider 类型和视图也决定该注册项在什么登录架构上下文中可见。二、第一步确定三类 Provider 根键和注册表视图Credential Provider、Filter、PLAP Provider 是三种独立类型。相同 CLSID 出现在不同根键时仍保留类型身份不能按 CLSID 合并为单一条目。constwchar_t*roots[]{LSoftware\\Microsoft\\Windows\\CurrentVersion\\Authentication\\Credential Providers,LSoftware\\Microsoft\\Windows\\CurrentVersion\\Authentication\\Credential Provider Filters,LSoftware\\Microsoft\\Windows\\CurrentVersion\\Authentication\\PLAP Providers,};注册表读取同时覆盖 64 位和 32 位视图。认证组件存在于一个视图并不能推断另一视图也存在输出中应保留视图标签。// 意义从注册表根键或父键打开一个子键。// 返回ERROR_SUCCESS 表示成功。ERROR_FILE_NOT_FOUND 表示路径不存在。// ERROR_ACCESS_DENIED 表示当前令牌没有 samDesired 所请求的权限。// 成功时 *phkResult 是调用方拥有的 HKEY使用完必须调用 RegCloseKey。LSTATUSRegOpenKeyExW(HKEY hKey,// 输入HKEY_LOCAL_MACHINE 等预定义根键或已打开父键。预定义根键不关闭。LPCWSTR lpSubKey,// 输入NUL 结尾 UTF-16 相对路径。nullptr 表示 hKey 本身。DWORD ulOptions,// 输入保留参数必须为 0。REGSAM samDesired,// 输入KEY_ENUMERATE_SUB_KEYS、KEY_QUERY_VALUE 与 KEY_WOW64_* 视图标志。PHKEY phkResult// 输出非空指针成功时接收新句柄。失败后不得使用。);// 意义释放调用方拥有的注册表键句柄。// 返回ERROR_SUCCESS 表示成功。关闭后 hKey 立即失效。LSTATUSRegCloseKey(HKEY hKey// 输入/释放由 RegOpenKeyExW 成功返回的 HKEY不能是预定义根键。);完成第一步后读取范围已经包含三类 Provider 和两个独立的注册表视图。下一步需要在每个范围内找出实际登记的 CLSID 子键并把默认名称连同原始类型一起保留。三、第二步枚举 CLSID 子键并读取默认名称每个 Provider 根键按一级子键枚举。子键名长度使用可扩容缓冲区ERROR_MORE_DATA时扩大容量。ERROR_NO_MORE_ITEMS表示枚举结束。// 意义按零基索引读取一个直接 Provider 子键名。// 返回ERROR_SUCCESS 表示成功。ERROR_NO_MORE_ITEMS 表示到达最后。// ERROR_MORE_DATA 表示 *lpcchName 指定的 UTF-16 字符容量不足。LSTATUSRegEnumKeyExW(HKEY hKey,// 输入已打开且具有 KEY_ENUMERATE_SUB_KEYS 的父键。不转移所有权。DWORD dwIndex,// 输入从零开始的直接子键索引。注册表变化时索引不稳定。LPWSTR lpName,// 输出子键名 UTF-16 缓冲区。不可为 nullptr。LPDWORD lpcchName,// 输入/输出lpName 容量/实际长度单位 wchar_t返回长度不含 NUL。LPDWORD lpReserved,// 输入保留参数必须为 nullptr。LPWSTR lpClass,// 输出可选类名缓冲区。本节不读取传 nullptr。LPDWORD lpcchClass,// 输入/输出类名容量/长度。lpClass 为 nullptr 时传 nullptr。PFILETIME lpftLastWriteTime// 输出可选最后写入时间。本节传 nullptr。);默认值通过空值名读取。默认值存在时保存原始类型和文本。默认值缺失时仍保留 CLSID 子键作为 Provider 身份。// 意义读取默认值或指定值的类型和原始字节。// 返回ERROR_SUCCESS 表示成功。ERROR_FILE_NOT_FOUND 表示值未设置。// ERROR_MORE_DATA 表示 *lpcbData 提供的字节容量不足。LSTATUSRegQueryValueExW(HKEY hKey,// 输入已打开且有 KEY_QUERY_VALUE 权限的 Provider 子键。不转移所有权。LPCWSTR lpValueName,// 输入NUL 结尾 UTF-16 值名。nullptr 或 L 表示默认值。LPDWORD lpReserved,// 输入保留参数必须为 nullptr。LPDWORD lpType,// 输出REG_SZ、REG_BINARY 等类型。本节先读取类型再决定解码方式。LPBYTE lpData,// 输出原始字节缓冲区。只查询长度时传 nullptr。LPDWORD lpcbData// 输入/输出lpData 容量/实际长度单位始终是字节。不可为 nullptr。);// 正确示范完整读取 Provider 子键默认值并保留“默认值缺失”与“读取失败”的差异。DWORD typeREG_NONE;DWORD requiredBytes0;LSTATUS statusRegQueryValueExW(providerKey,L,nullptr,type,nullptr,requiredBytes);if(statusERROR_FILE_NOT_FOUND){// Provider CLSID 子键仍然存在。仅默认显示名称未设置。}elseif(statusERROR_SUCCESS){std::vectorBYTEraw(requiredBytes);// requiredBytes 的单位是字节。for(intattempt0;attempt!3;attempt){DWORD actualBytesstatic_castDWORD(raw.size());statusRegQueryValueExW(providerKey,L,nullptr,type,raw.empty()?nullptr:raw.data(),actualBytes);if(statusERROR_MORE_DATA){raw.resize(actualBytes);// API 指出当前字节容量不足使用新的字节容量重试。continue;}if(statusERROR_SUCCESS){raw.resize(actualBytes);// 仅当 type 为 REG_SZ/REG_EXPAND_SZ 且 actualBytes 能整除 sizeof(wchar_t) 时再解码 UTF-16。}break;// 成功或不可重试错误都结束。失败状态和 CLSID 一同记录。}}// 错误示例只输出默认名称CLS ID 和 Provider 类型都丢失。record.namedefaultValue;完成第二步后每个根键中的子键名、默认值和原始数据都已读出。下一步要确认子键名能否作为 CLSID 使用同时保留它属于哪类 Provider、哪个根键和哪个视图。四、第三步验证 CLSID 并保留登记来源子键名符合{8-4-4-4-12}结构时作为 CLSID。结构验证通过后分别查询用户级和机器级 Classes 根中的InprocServer32、LocalServer32默认值。boolIsClsidText(std::wstring_view text){returntext.size()38text.front()L{text.back()L}text[9]L-text[14]L-text[19]L-text[24]L-;}conststd::wstring baseLSoftware\\Classes\\CLSID\\clsid;conststd::wstring inprocbaseL\\InprocServer32;conststd::wstring localbaseL\\LocalServer32;完成第三步后只有格式正确的 CLSID 会进入 COM 查询记录也仍能追溯到原始 Provider 类型和注册表视图。接下来使用这些 CLSID 分别查询用户级与机器级 Classes 注册得到可能的服务器文本。五、第四步查询当前视图中的 COM 服务器注册服务器查询需要在当前 32/64 位视图下分别进行。用户级 Classes 与机器级 Classes 同时读取InprocServer32与LocalServer32都作为独立注册结果输出。InprocServer32通常记录进程内实现路径LocalServer32通常记录本地服务器命令文本。二者是 COM 注册数据环境变量展开、文件存在性、架构和签名验证属于后续独立步骤。完整可运行程序在附件https://wangweicm.lanzouu.com/iT2xF3yqfb4j