首页
课程
问答
CTF
社区
招聘
峰会
发现
排行榜
知识库
工具下载
看雪20年
看雪商城
证书查询
登录
注册
首页
社区
课程
招聘
发现
问答
CTF
排行榜
知识库
工具下载
峰会
看雪商城
证书查询
社区
编程技术
发新帖
2
1
[原创]Credential Provider、Filter 与 PLAP 的 CLSID 枚举
发表于: 2026-7-26 10:33
2804
[原创]Credential Provider、Filter 与 PLAP 的 CLSID 枚举
WangWei_CM.
2026-7-26 10:33
2804
# 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 服务器位置。 ```text 三类 Provider 注册根与两个视图 -> CLSID 子键和默认名称 -> 有效 CLSID 与来源信息 -> 当前视图的 COM 服务器注册 ``` 第一步决定读取范围,避免把 32 位和 64 位注册数据混为一组。第二步取得每条登记记录的原始事实。第三步筛掉不能作为 COM 类标识使用的文本,并让每条记录保留来源。第四步才根据有效 CLSID 查找可能的实现位置。静态注册记录不能证明组件会显示、激活或成功完成身份验证。 ```text 选择根键和视图 -> 枚举 CLSID 子键 -> 验证并保留来源 -> 查询 Classes 中的 InprocServer32 或 LocalServer32 ``` ### 1. Credential Provider 为登录界面提供凭据收集组件 登录界面与身份验证系统有不同分工。Windows 登录界面由 LogonUI 等组件承载,它向 Credential Provider 查询可用的凭据方案。Provider 可以提供用户名密码、智能卡、Windows Hello 等不同类型的凭据磁贴,并将收集到的数据按系统约定序列化后交给后续身份验证流程。 Provider 是 COM 组件。登录界面从 Credential Providers 注册表根读取 CLSID,再根据 CLSID 在 Classes 注册中定位 COM 服务器并创建对象。CLSID 表示类身份,默认值通常只是显示名称。真正的实现位置来自 `InprocServer32` 或 `LocalServer32` 等 COM 注册子键。 ```text 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 注册存在或显示磁贴,不等于认证包已经接受某次凭据。 ```text 用户输入字段 -> 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 合并为单一条目。 ```cpp const wchar_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 位视图。认证组件存在于一个视图并不能推断另一视图也存在,输出中应保留视图标签。 ```cpp // 意义:从注册表根键或父键打开一个子键。 // 返回:ERROR_SUCCESS 表示成功。ERROR_FILE_NOT_FOUND 表示路径不存在。 // ERROR_ACCESS_DENIED 表示当前令牌没有 samDesired 所请求的权限。 // 成功时 *phkResult 是调用方拥有的 HKEY,使用完必须调用 RegCloseKey。 LSTATUS RegOpenKeyExW( 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 立即失效。 LSTATUS RegCloseKey( HKEY hKey // 输入/释放:由 RegOpenKeyExW 成功返回的 HKEY,不能是预定义根键。 ); ``` 完成第一步后,读取范围已经包含三类 Provider 和两个独立的注册表视图。下一步需要在每个范围内找出实际登记的 CLSID 子键,并把默认名称连同原始类型一起保留。 ## 三、第二步:枚举 CLSID 子键并读取默认名称 每个 Provider 根键按一级子键枚举。子键名长度使用可扩容缓冲区,`ERROR_MORE_DATA` 时扩大容量。`ERROR_NO_MORE_ITEMS` 表示枚举结束。 ```cpp // 意义:按零基索引读取一个直接 Provider 子键名。 // 返回:ERROR_SUCCESS 表示成功。ERROR_NO_MORE_ITEMS 表示到达最后。 // ERROR_MORE_DATA 表示 *lpcchName 指定的 UTF-16 字符容量不足。 LSTATUS RegEnumKeyExW( 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 身份。 ```cpp // 意义:读取默认值或指定值的类型和原始字节。 // 返回:ERROR_SUCCESS 表示成功。ERROR_FILE_NOT_FOUND 表示值未设置。 // ERROR_MORE_DATA 表示 *lpcbData 提供的字节容量不足。 LSTATUS RegQueryValueExW( 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 子键仍然存在。仅默认显示名称未设置。 } else if (status == ERROR_SUCCESS) { std::vector<BYTE> raw(requiredBytes); // requiredBytes 的单位是字节。 for (int attempt = 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 一同记录。 } } ``` ```cpp // 错误示例:只输出默认名称,CLS ID 和 Provider 类型都丢失。 record.name = defaultValue; ``` 完成第二步后,每个根键中的子键名、默认值和原始数据都已读出。下一步要确认子键名能否作为 CLSID 使用,同时保留它属于哪类 Provider、哪个根键和哪个视图。 ## 四、第三步:验证 CLSID 并保留登记来源 子键名符合 `{8-4-4-4-12}` 结构时作为 CLSID。结构验证通过后,分别查询用户级和机器级 Classes 根中的 `InprocServer32`、`LocalServer32` 默认值。 ```cpp bool IsClsidText(std::wstring_view text) { return text.size() == 38 && text.front() == L'{' && text.back() == L'}' && text[9] == L'-' && text[14] == L'-' && text[19] == L'-' && text[24] == L'-'; } const std::wstring base = L"Software\\Classes\\CLSID\\" + clsid; const std::wstring inproc = base + L"\\InprocServer32"; const std::wstring local = base + L"\\LocalServer32"; ``` 完成第三步后,只有格式正确的 CLSID 会进入 COM 查询,记录也仍能追溯到原始 Provider 类型和注册表视图。接下来使用这些 CLSID 分别查询用户级与机器级 Classes 注册,得到可能的服务器文本。 ## 五、第四步:查询当前视图中的 COM 服务器注册 服务器查询需要在当前 32/64 位视图下分别进行。用户级 Classes 与机器级 Classes 同时读取,`InprocServer32` 与 `LocalServer32` 都作为独立注册结果输出。 `InprocServer32` 通常记录进程内实现路径,`LocalServer32` 通常记录本地服务器命令文本。二者是 COM 注册数据,环境变量展开、文件存在性、架构和签名验证属于后续独立步骤。 完整可运行程序在附件  <mark class="encrypted">92eK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6%4j5h3&6Y4N6$3g2A6j5$3#2Q4x3X3g2D9j5h3&6*7L8%4g2#2i4K6u0W2j5$3!0E0i4K6u0r3K9g2b7J5P5p5j5K6P5i4q4X3j5U0c8B7</mark>
登录后可查看完整内容
冰与火的战歌:Windows内核攻防实战高级班!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。
上传的附件:
020-Credential-Provider与Filter及PLAP的CLSID枚举.zip
(162.60kb,7次下载)
收藏
・
2
点赞
・
1
打赏
分享
分享到微信
分享到QQ
分享到微博
赞赏记录
参与人
雪币
留言
时间
Ally Switch
期待更多优质内容的分享,论坛有你更精彩!
2026-7-29 14:28
查看更多
赞赏
×
1 雪花
5 雪花
10 雪花
20 雪花
50 雪花
80 雪花
100 雪花
150 雪花
200 雪花
支付方式:
微信支付
赞赏留言:
快捷留言
感谢分享~
精品文章~
原创内容~
精彩转帖~
助人为乐~
感谢分享~
最新回复
(
0
)
游客
登录
|
注册
方可回帖
回帖
表情
雪币赚取及消费
高级回复
返回
WangWei_CM.
30
发帖
1
回帖
40
RANK
关注
私信
他的文章
[原创] 进程模块枚举、映像身份与线程启动地址关联
215
[原创]Windows 进程 CPU、内存、I/O 与调度指标采样
981
[原创]跨位数进程的映像、命令行与环境块读取
535
[原创]进程实例身份、父子关系与会话归属
293
[原创]Windows 进程枚举NtQuerySystemInformation(SystemProcessInformation
635
关于我们
联系我们
企业服务
看雪公众号
专注于PC、移动、智能设备安全研究及逆向工程的开发者社区
谁下载
×
逆向爱好者
mb_xfkatlgt
Fzzz
mb_eizetlni
mb_lthgjpwj
看原图
赞赏
×
雪币:
+
留言:
快捷留言
为你点赞!
返回
顶部