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注册表树中。登录界面枚举候选 Provider,Filter 决定场景可见性,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 在某个使用场景中可见或可用。PLAP(Pre-Logon Access Provider)面向用户登录前的网络或访问准备场景。三种类型可使用相同的 COM 基础设施,接口职责和触发场景不同。
| 类型 | 注册根 | 主要职责 |
|---|---|---|
| Credential Provider | Credential Providers | 提供凭据磁贴、字段和凭据序列化。 |
| Credential Provider Filter | Credential Provider Filters | 对给定使用场景筛选 Provider 的可用性。 |
| PLAP Provider | PLAP 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 身份验证路径。LSA(Local 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::vector<BYTE>或调用方数组管理。
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[]={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 根中的InprocServer32、LocalServer32默认值。
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 同时读取,InprocServer32与LocalServer32都作为独立注册结果输出。
InprocServer32通常记录进程内实现路径,LocalServer32通常记录本地服务器命令文本。二者是 COM 注册数据,环境变量展开、文件存在性、架构和签名验证属于后续独立步骤。
完整可运行程序在附件
https://wangweicm.lanzouu.com/iT2xF3yqfb4j