Credential Provider、Filter 与 PLAP 的 CLSID 枚举
2026/7/26 11:52:00 网站建设 项目流程

Credential Provider、Filter 与 PLAP 的 CLSID 枚举

程序的目标是只读取注册表,列出 Credential Provider、Credential Provider Filter 与 PLAP Provider 的 CLSID、默认名称和 COM 服务器注册位置。它不创建 Provider,也不触发登录或身份验证。

要想得到这份登记信息,分为四步:

  1. 确定三类 Provider 注册根,并分别选择 64 位和 32 位注册表视图。
  2. 枚举每个根键下的 CLSID 子键,读取子键默认名称和原始值类型。
  3. 验证 CLSID 文本,并保留 Provider 类型、根键和视图等来源信息。
  4. 使用有效 CLSID 查询当前视图中的用户级和机器级 COM 服务器注册。

一、目标、对象关系和完整流程

Credential Provider、Credential Provider Filter 与 PLAP Provider 都以 CLSID 子键登记在Authentication注册表树中。登录界面枚举候选 Provider,Filter 决定场景可见性,PLAP 支持登录前访问准备。COM Classes 注册再提供 DLL 或 EXE 服务器位置。

三类 Provider 注册根与两个视图 -> CLSID 子键和默认名称 -> 有效 CLSID 与来源信息 -> 当前视图的 COM 服务器注册

第一步决定读取范围,避免把 32 位和 64 位注册数据混为一组。第二步取得每条登记记录的原始事实。第三步筛掉不能作为 COM 类标识使用的文本,并让每条记录保留来源。第四步才根据有效 CLSID 查找可能的实现位置。静态注册记录不能证明组件会显示、激活或成功完成身份验证。

选择根键和视图 -> 枚举 CLSID 子键 -> 验证并保留来源 -> 查询 Classes 中的 InprocServer32 或 LocalServer32

1. Credential Provider 为登录界面提供凭据收集组件

登录界面与身份验证系统有不同分工。Windows 登录界面由 LogonUI 等组件承载,它向 Credential Provider 查询可用的凭据方案。Provider 可以提供用户名密码、智能卡、Windows Hello 等不同类型的凭据磁贴,并将收集到的数据按系统约定序列化后交给后续身份验证流程。

Provider 是 COM 组件。登录界面从 Credential Providers 注册表根读取 CLSID,再根据 CLSID 在 Classes 注册中定位 COM 服务器并创建对象。CLSID 表示类身份,默认值通常只是显示名称。真正的实现位置来自InprocServer32LocalServer32等 COM 注册子键。

LogonUI 登录界面 -> Credential Provider 注册根 -> CLSID -> COM Classes 注册 -> InprocServer32(进程内 DLL)或 LocalServer32(独立 EXE 服务器)

枚举到一个 Provider CLSID 只证明它被登记为候选组件。是否会在当前登录场景显示、是否通过筛选、是否创建成功以及用户是否提交凭据,分别取决于系统策略、会话类型和运行时调用结果。

2. Provider、Filter 和 PLAP Provider 的职责不同

三类根键的职责需要分别解释。Credential Provider 负责提供具体凭据和磁贴。Credential Provider Filter 参与判断哪些 Provider 在某个使用场景中可见或可用。PLAP(Pre-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 根下的InprocServer32LocalServer32默认值提供服务器注册文本。它们位于不同注册表树,不能用一个字符串字段覆盖。

HKEY_CLASSES_ROOT是用户级和机器级 Classes 注册的合并视图。要判断数据来自哪个范围,应分别查询HKCU\Software\Classes\CLSIDHKLM\Software\Classes\CLSID,并分别记录 32 位和 64 位视图。进程内服务器还需要与宿主进程架构兼容,因此视图标签不能省略。

5. 静态枚举与实际凭据处理需要不同证据

注册、加载和使用需要分别判断。子键存在说明 Provider 类型已登记。服务器键存在说明 COM 注册文本已找到。文件验证可以说明候选文件是否存在或签名状态如何。实际 COM 对象是否能激活、接口是否实现正确、Filter 是否允许显示、凭据是否被接受,必须由运行时调用或系统日志独立证明。

6. 登录界面、LSA 与认证包在凭据提交后继续完成不同职责

Credential Provider 不直接决定身份验证结果。Provider 的职责是收集字段、构造凭据序列化数据并向登录界面报告状态。登录界面把序列化结果交给后续的 Windows 身份验证路径。LSA(Local Security Authority,本地安全机构)及其认证包负责协议验证、令牌建立和安全策略处理。Provider 注册存在或显示磁贴,不等于认证包已经接受某次凭据。

用户输入字段 -> Credential Provider 收集与序列化 -> LogonUI 协调登录交互 -> LSA 与认证包验证身份和策略 -> 建立用户令牌与登录会话

这条流程要求审计记录明确边界。注册表和 COM 服务器信息属于 Provider 发现层。令牌、会话和认证事件属于身份验证运行层。读者不能从某个 CLSID 或 DLL 路径推断特定用户的认证是否成功。

7. COM 资源、注册表句柄和 UTF-16 长度各有释放与单位规则

COM 资源、注册表句柄和字符串有不同释放规则。HKEYRegOpenKeyExW成功返回的注册表键引用,调用方RegCloseKey。COM 接口指针由Release减少引用计数。BSTRSysFreeString释放。三者不能交叉释放。Provider 子键的默认值数据由注册表 API 写入调用方缓冲区,缓冲区本身通常由std::vector<BYTE>或调用方数组管理。

RegEnumKeyExW的子键名长度是 UTF-16 宽字符数,RegQueryValueExW的值数据长度是字节数。NUL 只是字符串终止字符,返回数据不保证总有 NUL。只有确认 REG 类型和长度对齐后才可解码。成功打开的 Provider 子键、得到的文本副本和从 Classes 根取得的服务器记录都应有明确的所有者和使用期。

8. 标准读取先保留 Provider 身份,再定位可能的服务器

标准读取要保留每一层的关联字段。先对三类根键分别以两个视图枚举 CLSID 子键。每条记录保存 Provider 类型、根键、视图、CLSID 子键名、默认值类型和原始数据。随后只对格式有效的 CLSID 查询用户级和机器级 Classes 根,分别保存InprocServer32LocalServer32默认值。失败时保留 HRESULT 或 Win32 错误,不用空字符串替代。

错误做法是拿到默认显示名称后立即丢弃 CLSID。名称可重复、可缺失或可本地化,Classes 查询需要 CLSID 才能定位服务器。Provider 类型和视图也决定该注册项在什么登录架构上下文中可见。

二、第一步:确定三类 Provider 根键和注册表视图

Credential Provider、Filter、PLAP Provider 是三种独立类型。相同 CLSID 出现在不同根键时仍保留类型身份,不能按 CLSID 合并为单一条目。

constwchar_t*roots[]={L"Software\\Microsoft\\Windows\\CurrentVersion\\Authentication\\Credential Providers",L"Software\\Microsoft\\Windows\\CurrentVersion\\Authentication\\Credential Provider Filters",L"Software\\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 type=REG_NONE;DWORD requiredBytes=0;LSTATUS status=RegQueryValueExW(providerKey,L"",nullptr,&type,nullptr,&requiredBytes);if(status==ERROR_FILE_NOT_FOUND){// Provider CLSID 子键仍然存在。仅默认显示名称未设置。}elseif(status==ERROR_SUCCESS){std::vector<BYTE>raw(requiredBytes);// requiredBytes 的单位是字节。for(intattempt=0;attempt!=3;++attempt){DWORD actualBytes=static_cast<DWORD>(raw.size());status=RegQueryValueExW(providerKey,L"",nullptr,&type,raw.empty()?nullptr:raw.data(),&actualBytes);if(status==ERROR_MORE_DATA){raw.resize(actualBytes);// API 指出当前字节容量不足,使用新的字节容量重试。continue;}if(status==ERROR_SUCCESS){raw.resize(actualBytes);// 仅当 type 为 REG_SZ/REG_EXPAND_SZ 且 actualBytes 能整除 sizeof(wchar_t) 时再解码 UTF-16。}break;// 成功或不可重试错误都结束。失败状态和 CLSID 一同记录。}}
// 错误示例:只输出默认名称,CLS ID 和 Provider 类型都丢失。record.name=defaultValue;

完成第二步后,每个根键中的子键名、默认值和原始数据都已读出。下一步要确认子键名能否作为 CLSID 使用,同时保留它属于哪类 Provider、哪个根键和哪个视图。

四、第三步:验证 CLSID 并保留登记来源

子键名符合{8-4-4-4-12}结构时作为 CLSID。结构验证通过后,分别查询用户级和机器级 Classes 根中的InprocServer32LocalServer32默认值。

boolIsClsidText(std::wstring_view text){returntext.size()==38&&text.front()==L'{'&&text.back()==L'}'&&text[9]==L'-'&&text[14]==L'-'&&text[19]==L'-'&&text[24]==L'-';}conststd::wstring base=L"Software\\Classes\\CLSID\\"+clsid;conststd::wstring inproc=base+L"\\InprocServer32";conststd::wstring local=base+L"\\LocalServer32";

完成第三步后,只有格式正确的 CLSID 会进入 COM 查询,记录也仍能追溯到原始 Provider 类型和注册表视图。接下来使用这些 CLSID 分别查询用户级与机器级 Classes 注册,得到可能的服务器文本。

五、第四步:查询当前视图中的 COM 服务器注册

服务器查询需要在当前 32/64 位视图下分别进行。用户级 Classes 与机器级 Classes 同时读取,InprocServer32LocalServer32都作为独立注册结果输出。

InprocServer32通常记录进程内实现路径,LocalServer32通常记录本地服务器命令文本。二者是 COM 注册数据,环境变量展开、文件存在性、架构和签名验证属于后续独立步骤。

完整可运行程序在附件

https://wangweicm.lanzouu.com/iT2xF3yqfb4j

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询