首页
课程
问答
CTF
社区
招聘
峰会
发现
排行榜
知识库
工具下载
看雪20年
看雪商城
证书查询
登录
注册
首页
社区
课程
招聘
发现
问答
CTF
排行榜
知识库
工具下载
峰会
看雪商城
证书查询
社区
编程技术
发新帖
0
0
[原创]Explorer Shell 扩展、图标覆盖与 COM 服务器定位
发表于: 2026-7-22 18:19
1028
[原创]Explorer Shell 扩展、图标覆盖与 COM 服务器定位
WangWei_CM.
2026-7-22 18:19
1028
# Explorer Shell 扩展、图标覆盖与 COM 服务器定位 文章展示的功能已加入KswordARK,开源项目地址:<mark class="encrypted">4e3K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6w2f1%4N6G2M7X3c8p5c8g2k6Q4x3V1k6w2f1%4N6G2M7X3c8Q4x3V1j5`.</mark>  要想在不加载任何 COM 对象的前提下,定位 Explorer Shell 扩展和图标覆盖登记所指向的 COM 服务器,需要完成四步: 1. 区分值型与子键型扩展来源,并打开用户级、机器级和 32/64 位注册表视图 2. 枚举值型来源的值名、类型和原始值数据 3. 枚举图标覆盖来源的子键名与默认值 4. 验证 CLSID,再定位 `InprocServer32` 与 `LocalServer32` 注册 这四步保留了“哪一类 Shell 功能引用了哪个 CLSID”和“该 CLSID 在哪里登记服务器”两层信息。得到的结果是静态注册信息,不能代替 COM 激活或 Explorer 实际加载结果。 ## 一、对象关系和完整流程 Explorer 是 Windows 的文件管理与桌面外壳。它在右键菜单、属性页、图标覆盖、文件预览或 Shell Execute 钩子等场景查询扩展来源。来源记录提供 CLSID。COM 再根据 CLSID 的 Classes 注册定位 DLL 或 EXE 服务器。 ```text Explorer 需要某类 Shell 功能 -> 读取扩展来源键 -> 得到 CLSID -> 查询 HKCU 或 HKLM Classes 注册 -> 读取 InprocServer32 或 LocalServer32 -> COM 创建对象并请求扩展接口 ``` 读取过程要先保留来源结构,再验证 CLSID,随后独立查询用户级、机器级和 32/64 位视图中的 COM 服务器。读取结果不能证明扩展对象已被成功创建。 ```text 枚举值型或子键型来源 -> 保存路径、视图和值数据 -> 验证 CLSID 格式 -> 查询 Classes 根 -> 读取服务器默认值 -> 保存服务器类型、原始文本和错误码 ``` ### Shell 扩展是由 Explorer 或其它外壳组件调用的 COM 对象 Shell 扩展的运行方式由 Explorer 和 COM 共同决定。许多扩展按 COM(Component Object Model)规则注册。外壳从注册表取得类标识符,再请求相应的 COM 服务器创建对象。 COM 客户端和 COM 服务器各有职责。Explorer 是客户端,它知道要请求的接口类别。扩展 DLL 或 EXE 是服务器,它实现对应接口。CLSID 是一个 GUID 形式的类标识符,例如 `{01234567-89AB-CDEF-0123-456789ABCDEF}`,用于把客户端请求映射到服务器注册信息。 ```text Explorer 或其它 Shell 客户端 -> 扩展来源注册项 -> CLSID -> HKCU 或 HKLM\Software\Classes\CLSID\{CLSID} -> InprocServer32(DLL)或 LocalServer32(EXE 命令文本) ``` CLSID 只是身份标识。它本身没有可执行路径、签名状态或接口实现信息。需要沿着 Classes 根下的注册路径继续读取,才能取得服务器位置和服务器种类。 ### 不同 Shell 扩展来源使用不同的注册表形状 扩展来源的注册表形状并不相同。`ShellExecuteHooks`、`SharedTaskScheduler`、`ShellServiceObjectDelayLoad` 和 `Shell Extensions\Approved` 常采用“值名 + 值数据”的形式,CLSID 可能位于值名或数据中。`ShellIconOverlayIdentifiers` 则采用“子键 + 默认值”的形式,子键名经常是显示标识,默认值才可能存放 CLSID。 图标覆盖扩展的用途是让文件或文件夹图标显示小型状态图案。例如同步状态或版本控制状态可能通过图标覆盖表达。子键名称表达的是注册项的标识文本,默认值表达的是可激活对象的身份。只读其中一个字段无法保留完整关系。 ### HKCR 是合并视图,HKCU 与 HKLM 需要分别检查 `HKEY_CLASSES_ROOT`(HKCR)是 Classes 合并视图。它整合了当前用户的 `HKCU\Software\Classes` 与机器级的 `HKLM\Software\Classes`。需要解释来源和优先级时,应直接查询两个实际根路径并记录根键身份。 用户级注册可只影响当前用户,机器级注册通常面向本机用户。两个位置都出现同一 CLSID 时,查询合并视图得到的文本无法完整说明各自原始注册内容。枚举程序应分别报告用户级和机器级的 `InprocServer32`、`LocalServer32`,随后由调用场景决定解析优先级。 ### 进程内服务器与本地服务器的启动边界不同 COM 服务器模型分为进程内和本地服务器。`InprocServer32` 通常指向 DLL,客户端进程通过加载 DLL 在自身进程内创建对象。`LocalServer32` 通常保存 EXE 的命令文本,COM 通过独立进程提供对象。两者的路径文本、架构匹配和故障范围都不同。 进程内 DLL 必须与 Explorer 的进程架构兼容,32 位和 64 位 Classes 视图因此需要分别检查。`LocalServer32` 的默认值可能包含带引号的可执行路径及参数,读取时应保留原始命令文本,不能以第一个空格作为路径分界。 ### Explorer 先发现扩展类别,再按 CLSID 激活对象 注册路径与 COM 激活形成连续调用过程。Explorer 在需要某类外壳功能时读取相应注册来源,例如图标覆盖、Shell Execute 钩子或延迟加载的 Shell Service Object。来源项给出 CLSID 后,Explorer 或其它 Shell 客户端通过 COM 查找 Classes 注册,再按服务器类型加载 DLL 或启动 EXE,然后请求该扩展类别所需的接口。 ```text Explorer 的文件或桌面操作 -> 扩展来源键(值或子键) -> CLSID -> Classes\CLSID 注册 -> InprocServer32 或 LocalServer32 原始文本 -> COM 激活、接口查询与运行时对象 ``` 来源键存在说明 Explorer 有机会发现这条登记。CLSID 注册存在说明 COM 有可查询的类信息。实际激活成功还取决于服务器文件、架构、COM 接口、权限和运行时状态。每个阶段都需要单独输出,不能用“扩展已注册”覆盖所有结果。 ### 图标覆盖子键名和默认值承担不同角色 图标覆盖的子键名与默认值要分别读取。`ShellIconOverlayIdentifiers` 的直接子键名通常是覆盖标识,方便人类识别某个状态类别。子键默认值常保存 CLSID,用于定位实际 COM 对象。一个子键没有默认值时仍说明该标识存在,只是当前无法从该字段取得类身份。 图标覆盖显示还受到 Shell 的运行时选择规则影响。枚举到多个注册项并不代表每一个都会出现在桌面图标上。实际渲染与覆盖优先级、文件状态、扩展接口返回结果以及当前 Explorer 进程有关。静态审计报告应保留子键名、默认值类型和 CLSID 解析状态,不替运行时显示做判断。 ### 标准枚举应先保存来源字段,再独立查询 Classes 根 发现 CLSID 后仍要保留来源上下文。每次从值型键读取记录时保存完整源路径、视图、值名、REG 类型、原始数据和“CLSID 出现在值名还是值数据”的位置。每次从子键型键读取记录时保存父键、子键名、默认值类型和默认值原始数据。只有格式验证通过的文本才进入 Classes 查询。 ```cpp HKEY root = nullptr; const LSTATUS opened = RegOpenKeyExW( HKEY_LOCAL_MACHINE, // 输入:机器级根。系统所有,调用方不关闭。 overlayPath, // 输入:NUL 结尾 UTF-16 图标覆盖来源路径。 0, // 输入:保留,必须为 0。 KEY_ENUMERATE_SUB_KEYS | KEY_WOW64_64KEY, // 输入:枚举子键和明确视图。 &root); // 输出:成功时得到 HKEY,本段负责关闭。 if (opened == ERROR_SUCCESS) { // 用 RegEnumKeyExW 读取一级子键名。名称容量单位是 UTF-16 字符数。 // 打开每个子键后以 L"" 读取默认值。数据容量单位是字节。 // 保存来源字段后才验证默认值是否为 CLSID,并继续查询 Classes 根。 RegCloseKey(root); // 释放成功打开的 HKEY,已保存文本必须为独立副本。 } ``` 错误做法是只把 CLSID 放进一个无来源的列表。相同 CLSID 出现在图标覆盖、Shell Execute Hook 与其它来源时,去重会抹掉它被哪一种 Shell 功能引用的事实。 ### HKEY、REGSAM、NUL 和两种长度单位需要明确区分 注册表 API 涉及资源与长度单位。`HKEY` 是进程持有的已打开键引用,`RegOpenKeyExW` 成功后由调用方 `RegCloseKey`。`REGSAM` 是访问掩码,`KEY_QUERY_VALUE` 与 `KEY_ENUMERATE_SUB_KEYS` 对应不同读取能力,`KEY_WOW64_64KEY` 或 `KEY_WOW64_32KEY` 选择数据视图。 `RegEnumKeyExW` 的名称长度以 UTF-16 宽字符数计,`RegEnumValueW` 的值数据长度以字节计。NUL 是数值为零的终止字符,返回数据不能只靠搜索 NUL 判断长度。应使用 API 返回的字符数或字节数。发生 `ERROR_MORE_DATA` 时分别扩大对应缓冲区,发生 `ERROR_NO_MORE_ITEMS` 时才结束枚举。 ## 二、第一步:按来源结构和注册表视图打开键 第一步要确定每一种来源的登记形状和读取位置。`ShellExecuteHooks`、`SharedTaskScheduler`、`ShellServiceObjectDelayLoad`、`Shell Extensions\Approved` 使用值型结构。每个值名和值数据都是独立字段,CLSID 可能出现在任意一侧。 `ShellIconOverlayIdentifiers` 使用子键型结构。每个一级子键表示一个图标覆盖标识,子键默认值常保存 CLSID。子键名本身和默认值都需要单独保存。 打开值型来源时只申请查询值权限。返回的 `HKEY` 由调用方关闭,根键本身不需要关闭。 ```cpp // 意义:打开注册表子键。 // 返回:ERROR_SUCCESS 表示成功。其它返回值是 Win32 错误码。 // 成功时 phkResult 接收 HKEY,调用方用 RegCloseKey 关闭。 LSTATUS RegOpenKeyExW( HKEY hKey, LPCWSTR lpSubKey, DWORD ulOptions, REGSAM samDesired, PHKEY phkResult ); // 意义:关闭由 RegOpenKeyExW 返回的键句柄。 // 返回:ERROR_SUCCESS 表示成功。 LSTATUS RegCloseKey(HKEY hKey); ``` 64 位系统要显式读取两个注册表视图。机器级和用户级来源都保留根键标签,值相同也不能合并成一条记录。 ```cpp const REGSAM views[] = { KEY_WOW64_64KEY, KEY_WOW64_32KEY }; for (REGSAM view : views) { RegOpenKeyExW(HKEY_LOCAL_MACHINE, sourcePath, 0, KEY_QUERY_VALUE | view, &key); } ``` ```cpp // 错误示例:x64 程序的默认视图会遗漏 32 位 COM 注册。 RegOpenKeyExW(HKEY_LOCAL_MACHINE, sourcePath, 0, KEY_QUERY_VALUE, &key); ``` 完成第一步后,来源键的形状、所在根键和读取视图都有了明确边界。接下来需要逐个读取值型来源的值名与原始数据,才能判断 CLSID 来自哪个字段。 ## 三、第二步:枚举值型来源的值名、类型和值数据 枚举值前先查询本次键的最大值名长度和最大数据长度。值名长度单位是 UTF-16 字符数,值数据长度单位是字节,两个数不能互换。 ```cpp // 意义:取得键的值数、最大值名长度和最大值数据长度。 // 返回:ERROR_SUCCESS 表示成功。长度仅是本次枚举的初始容量, // 并发改写时仍要处理 RegEnumValueW 的 ERROR_MORE_DATA。 LSTATUS RegQueryInfoKeyW( HKEY hKey, LPWSTR lpClass, LPDWORD lpcchClass, LPDWORD lpReserved, LPDWORD lpcSubKeys, LPDWORD lpcbMaxSubKeyLen, LPDWORD lpcbMaxClassLen, LPDWORD lpcValues, LPDWORD lpcbMaxValueNameLen, LPDWORD lpcbMaxValueLen, LPDWORD lpcbSecurityDescriptor, PFILETIME lpftLastWriteTime ); // 意义:按索引读取一个值名、值类型和原始数据。 // 返回:ERROR_SUCCESS 表示成功。ERROR_NO_MORE_ITEMS 表示结束。 // ERROR_MORE_DATA 表示值名或数据缓冲区不足。 LSTATUS RegEnumValueW( HKEY hKey, DWORD dwIndex, LPWSTR lpValueName, LPDWORD lpcchValueName, LPDWORD lpReserved, LPDWORD lpType, LPBYTE lpData, LPDWORD lpcbData ); ``` 正确枚举时为值名和数据分配两个缓冲区。遇到 `ERROR_MORE_DATA` 时分别扩大字符缓冲区和字节缓冲区,不能把返回的数据字节数当作值名字符数。 ```cpp std::vector<wchar_t> name(maxValueNameChars + 1, L'\0'); std::vector<BYTE> data(maxValueDataBytes); DWORD nameChars = static_cast<DWORD>(name.size()); DWORD dataBytes = static_cast<DWORD>(data.size()); DWORD type = REG_NONE; LSTATUS status = RegEnumValueW( key, index, name.data(), &nameChars, nullptr, &type, data.empty() ? nullptr : data.data(), &dataBytes); if (status == ERROR_SUCCESS) { std::wstring valueName(name.data(), nameChars); data.resize(dataBytes); } ``` 字符串只在类型为 `REG_SZ` 或 `REG_EXPAND_SZ` 且数据字节数能被 `sizeof(wchar_t)` 整除时解码。值数据为二进制或数值时保留原始类型和字节数,不能强制转换成路径文本。 完成第二步后,值型来源中的 CLSID 候选项已经带有完整的路径、视图和值字段。图标覆盖采用另一种注册表形状,接下来要读取它的子键名和默认值,才能获得同样完整的来源记录。 ## 四、第三步:枚举图标覆盖的子键与默认值 图标覆盖来源的第一层是子键名。某些键允许 `KEY_ENUMERATE_SUB_KEYS` 却拒绝额外的键查询权限,子键名缓冲区可以从合理初始大小开始,在 `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 ); ``` 正确枚举后读取每个子键的默认值。默认值名称在 `RegQueryValueExW` 中用空字符串或 `nullptr` 表示,读取结果和子键名组成一条图标覆盖记录。 ```cpp HKEY child = nullptr; if (RegOpenKeyExW(root, childPath.c_str(), 0, KEY_QUERY_VALUE | view, &child) == ERROR_SUCCESS) { DWORD type = REG_NONE; DWORD bytes = 0; RegQueryValueExW(child, L"", nullptr, &type, nullptr, &bytes); RegCloseKey(child); } ``` ```cpp // 错误示例:只保存子键名称,默认值中的 CLSID 没有被读取。 std::wstring overlayName = subKeyName; ``` 完成第三步后,值型来源和图标覆盖来源都保留了原始登记字段。接下来只让格式正确的 CLSID 进入 Classes 注册查询,避免把显示名称或任意文本误当成 COM 类。 ## 五、第四步:验证 CLSID 后定位 COM 服务器 CLSID 文本应满足 `{8-4-4-4-12}` 结构。值名是 CLSID 时直接使用。值数据恰好为 CLSID 时也可使用。其它显示名称和任意字符串不进入 COM 注册路径。 ```cpp bool IsClsidText(std::wstring_view text) { if (text.size() != 38 || text.front() != L'{' || text.back() != L'}') return false; return text[9] == L'-' && text[14] == L'-' && text[19] == L'-' && text[24] == L'-'; } ``` COM 服务器定位需要同时查询两个 Classes 根。用户级路径是 `HKCU\Software\Classes\CLSID\{CLSID}`,机器级路径是 `HKLM\Software\Classes\CLSID\{CLSID}`。每个根下分别读取 `InprocServer32` 与 `LocalServer32` 的默认值。 ```cpp const std::wstring clsidPath = L"Software\\Classes\\CLSID\\" + clsid; const std::wstring inprocPath = clsidPath + L"\\InprocServer32"; const std::wstring localPath = clsidPath + L"\\LocalServer32"; // 每个路径都使用当前的 KEY_WOW64_64KEY 或 KEY_WOW64_32KEY 打开, // 再读取默认值。找到 InprocServer32 后仍继续检查 LocalServer32,保留全部注册结果。 ``` `InprocServer32` 的默认值通常是进程内服务器路径,`LocalServer32` 的默认值通常是本地服务器命令文本。两种默认值都属于 COM 注册数据,原始文本应先保存。路径展开、文件存在性或签名检查属于后续独立步骤。 完整可运行程序在附件  <mark class="encrypted">fcaK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6%4j5h3&6Y4N6$3g2A6j5$3#2Q4x3X3g2D9j5h3&6*7L8%4g2#2i4K6u0W2j5$3!0E0i4K6u0r3K9e0c8j5k6e0V1K6P5e0l9$3N6e0u0T1</mark>
冰与火的战歌:Windows内核攻防实战高级班!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。
上传的附件:
013-Explorer-Shell扩展图标覆盖与COM服务器定位.zip
(165.32kb,6次下载)
收藏
・
0
点赞
・
0
打赏
分享
分享到微信
分享到QQ
分享到微博
赞赏记录
参与人
雪币
留言
时间
查看更多
赞赏
×
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、移动、智能设备安全研究及逆向工程的开发者社区
谁下载
×
option
sinker_
Fzzz
JamMIC
jyotidwi
mb_lthgjpwj
看原图
赞赏
×
雪币:
+
留言:
快捷留言
为你点赞!
返回
顶部