[原创][Windows C++]Explorer Shell 扩展、图标覆盖与 COM 服务器定位

发布时间:2026/7/22 18:28:58
[原创][Windows C++]Explorer Shell 扩展、图标覆盖与 COM 服务器定位 Explorer Shell 扩展、图标覆盖与 COM 服务器定位本文所述所有内容已在KswordARK中实现项目完全开源地址https://github.com/KSwordDEV/KSword/要想在不加载任何 COM 对象的前提下定位 Explorer Shell 扩展和图标覆盖登记所指向的 COM 服务器需要完成四步区分值型与子键型扩展来源并打开用户级、机器级和 32/64 位注册表视图枚举值型来源的值名、类型和原始值数据枚举图标覆盖来源的子键名与默认值验证 CLSID再定位InprocServer32与LocalServer32注册这四步保留了“哪一类 Shell 功能引用了哪个 CLSID”和“该 CLSID 在哪里登记服务器”两层信息。得到的结果是静态注册信息不能代替 COM 激活或 Explorer 实际加载结果。一、对象关系和完整流程Explorer 是 Windows 的文件管理与桌面外壳。它在右键菜单、属性页、图标覆盖、文件预览或 Shell Execute 钩子等场景查询扩展来源。来源记录提供 CLSID。COM 再根据 CLSID 的 Classes 注册定位 DLL 或 EXE 服务器。Explorer 需要某类 Shell 功能 - 读取扩展来源键 - 得到 CLSID - 查询 HKCU 或 HKLM Classes 注册 - 读取 InprocServer32 或 LocalServer32 - COM 创建对象并请求扩展接口读取过程要先保留来源结构再验证 CLSID随后独立查询用户级、机器级和 32/64 位视图中的 COM 服务器。读取结果不能证明扩展对象已被成功创建。枚举值型或子键型来源 - 保存路径、视图和值数据 - 验证 CLSID 格式 - 查询 Classes 根 - 读取服务器默认值 - 保存服务器类型、原始文本和错误码Shell 扩展是由 Explorer 或其它外壳组件调用的 COM 对象Shell 扩展的运行方式由 Explorer 和 COM 共同决定。许多扩展按 COMComponent Object Model规则注册。外壳从注册表取得类标识符再请求相应的 COM 服务器创建对象。COM 客户端和 COM 服务器各有职责。Explorer 是客户端它知道要请求的接口类别。扩展 DLL 或 EXE 是服务器它实现对应接口。CLSID 是一个 GUID 形式的类标识符例如{01234567-89AB-CDEF-0123-456789ABCDEF}用于把客户端请求映射到服务器注册信息。Explorer 或其它 Shell 客户端 - 扩展来源注册项 - CLSID - HKCU 或 HKLM\Software\Classes\CLSID\{CLSID} - InprocServer32DLL或 LocalServer32EXE 命令文本CLSID 只是身份标识。它本身没有可执行路径、签名状态或接口实现信息。需要沿着 Classes 根下的注册路径继续读取才能取得服务器位置和服务器种类。不同 Shell 扩展来源使用不同的注册表形状扩展来源的注册表形状并不相同。ShellExecuteHooks、SharedTaskScheduler、ShellServiceObjectDelayLoad和Shell Extensions\Approved常采用“值名 值数据”的形式CLSID 可能位于值名或数据中。ShellIconOverlayIdentifiers则采用“子键 默认值”的形式子键名经常是显示标识默认值才可能存放 CLSID。图标覆盖扩展的用途是让文件或文件夹图标显示小型状态图案。例如同步状态或版本控制状态可能通过图标覆盖表达。子键名称表达的是注册项的标识文本默认值表达的是可激活对象的身份。只读其中一个字段无法保留完整关系。HKCR 是合并视图HKCU 与 HKLM 需要分别检查HKEY_CLASSES_ROOTHKCR是 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然后请求该扩展类别所需的接口。Explorer 的文件或桌面操作 - 扩展来源键值或子键 - CLSID - Classes\CLSID 注册 - InprocServer32 或 LocalServer32 原始文本 - COM 激活、接口查询与运行时对象来源键存在说明 Explorer 有机会发现这条登记。CLSID 注册存在说明 COM 有可查询的类信息。实际激活成功还取决于服务器文件、架构、COM 接口、权限和运行时状态。每个阶段都需要单独输出不能用“扩展已注册”覆盖所有结果。图标覆盖子键名和默认值承担不同角色图标覆盖的子键名与默认值要分别读取。ShellIconOverlayIdentifiers的直接子键名通常是覆盖标识方便人类识别某个状态类别。子键默认值常保存 CLSID用于定位实际 COM 对象。一个子键没有默认值时仍说明该标识存在只是当前无法从该字段取得类身份。图标覆盖显示还受到 Shell 的运行时选择规则影响。枚举到多个注册项并不代表每一个都会出现在桌面图标上。实际渲染与覆盖优先级、文件状态、扩展接口返回结果以及当前 Explorer 进程有关。静态审计报告应保留子键名、默认值类型和 CLSID 解析状态不替运行时显示做判断。标准枚举应先保存来源字段再独立查询 Classes 根发现 CLSID 后仍要保留来源上下文。每次从值型键读取记录时保存完整源路径、视图、值名、REG 类型、原始数据和“CLSID 出现在值名还是值数据”的位置。每次从子键型键读取记录时保存父键、子键名、默认值类型和默认值原始数据。只有格式验证通过的文本才进入 Classes 查询。HKEY rootnullptr;constLSTATUS openedRegOpenKeyExW(HKEY_LOCAL_MACHINE,// 输入机器级根。系统所有调用方不关闭。overlayPath,// 输入NUL 结尾 UTF-16 图标覆盖来源路径。0,// 输入保留必须为 0。KEY_ENUMERATE_SUB_KEYS|KEY_WOW64_64KEY,// 输入枚举子键和明确视图。root);// 输出成功时得到 HKEY本段负责关闭。if(openedERROR_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由调用方关闭根键本身不需要关闭。// 意义打开注册表子键。// 返回ERROR_SUCCESS 表示成功。其它返回值是 Win32 错误码。// 成功时 phkResult 接收 HKEY调用方用 RegCloseKey 关闭。LSTATUSRegOpenKeyExW(HKEY hKey,LPCWSTR lpSubKey,DWORD ulOptions,REGSAM samDesired,PHKEY phkResult);// 意义关闭由 RegOpenKeyExW 返回的键句柄。// 返回ERROR_SUCCESS 表示成功。LSTATUSRegCloseKey(HKEY hKey);64 位系统要显式读取两个注册表视图。机器级和用户级来源都保留根键标签值相同也不能合并成一条记录。constREGSAM views[]{KEY_WOW64_64KEY,KEY_WOW64_32KEY};for(REGSAM view:views){RegOpenKeyExW(HKEY_LOCAL_MACHINE,sourcePath,0,KEY_QUERY_VALUE|view,key);}// 错误示例x64 程序的默认视图会遗漏 32 位 COM 注册。RegOpenKeyExW(HKEY_LOCAL_MACHINE,sourcePath,0,KEY_QUERY_VALUE,key);完成第一步后来源键的形状、所在根键和读取视图都有了明确边界。接下来需要逐个读取值型来源的值名与原始数据才能判断 CLSID 来自哪个字段。三、第二步枚举值型来源的值名、类型和值数据枚举值前先查询本次键的最大值名长度和最大数据长度。值名长度单位是 UTF-16 字符数值数据长度单位是字节两个数不能互换。// 意义取得键的值数、最大值名长度和最大值数据长度。// 返回ERROR_SUCCESS 表示成功。长度仅是本次枚举的初始容量// 并发改写时仍要处理 RegEnumValueW 的 ERROR_MORE_DATA。LSTATUSRegQueryInfoKeyW(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 表示值名或数据缓冲区不足。LSTATUSRegEnumValueW(HKEY hKey,DWORD dwIndex,LPWSTR lpValueName,LPDWORD lpcchValueName,LPDWORD lpReserved,LPDWORD lpType,LPBYTE lpData,LPDWORD lpcbData);正确枚举时为值名和数据分配两个缓冲区。遇到ERROR_MORE_DATA时分别扩大字符缓冲区和字节缓冲区不能把返回的数据字节数当作值名字符数。std::vectorwchar_tname(maxValueNameChars1,L\0);std::vectorBYTEdata(maxValueDataBytes);DWORD nameCharsstatic_castDWORD(name.size());DWORD dataBytesstatic_castDWORD(data.size());DWORD typeREG_NONE;LSTATUS statusRegEnumValueW(key,index,name.data(),nameChars,nullptr,type,data.empty()?nullptr:data.data(),dataBytes);if(statusERROR_SUCCESS){std::wstringvalueName(name.data(),nameChars);data.resize(dataBytes);}字符串只在类型为REG_SZ或REG_EXPAND_SZ且数据字节数能被sizeof(wchar_t)整除时解码。值数据为二进制或数值时保留原始类型和字节数不能强制转换成路径文本。完成第二步后值型来源中的 CLSID 候选项已经带有完整的路径、视图和值字段。图标覆盖采用另一种注册表形状接下来要读取它的子键名和默认值才能获得同样完整的来源记录。四、第三步枚举图标覆盖的子键与默认值图标覆盖来源的第一层是子键名。某些键允许KEY_ENUMERATE_SUB_KEYS却拒绝额外的键查询权限子键名缓冲区可以从合理初始大小开始在ERROR_MORE_DATA时扩容。// 意义按索引读取一级子键名。// 返回ERROR_SUCCESS 表示成功。ERROR_NO_MORE_ITEMS 表示结束。// ERROR_MORE_DATA 表示 lpName 的字符容量不足。LSTATUSRegEnumKeyExW(HKEY hKey,DWORD dwIndex,LPWSTR lpName,LPDWORD lpcchName,LPDWORD lpReserved,LPWSTR lpClass,LPDWORD lpcchClass,PFILETIME lpftLastWriteTime);正确枚举后读取每个子键的默认值。默认值名称在RegQueryValueExW中用空字符串或nullptr表示读取结果和子键名组成一条图标覆盖记录。HKEY childnullptr;if(RegOpenKeyExW(root,childPath.c_str(),0,KEY_QUERY_VALUE|view,child)ERROR_SUCCESS){DWORD typeREG_NONE;DWORD bytes0;RegQueryValueExW(child,L,nullptr,type,nullptr,bytes);RegCloseKey(child);}// 错误示例只保存子键名称默认值中的 CLSID 没有被读取。std::wstring overlayNamesubKeyName;完成第三步后值型来源和图标覆盖来源都保留了原始登记字段。接下来只让格式正确的 CLSID 进入 Classes 注册查询避免把显示名称或任意文本误当成 COM 类。五、第四步验证 CLSID 后定位 COM 服务器CLSID 文本应满足{8-4-4-4-12}结构。值名是 CLSID 时直接使用。值数据恰好为 CLSID 时也可使用。其它显示名称和任意字符串不进入 COM 注册路径。boolIsClsidText(std::wstring_view text){if(text.size()!38||text.front()!L{||text.back()!L})returnfalse;returntext[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的默认值。conststd::wstring clsidPathLSoftware\\Classes\\CLSID\\clsid;conststd::wstring inprocPathclsidPathL\\InprocServer32;conststd::wstring localPathclsidPathL\\LocalServer32;// 每个路径都使用当前的 KEY_WOW64_64KEY 或 KEY_WOW64_32KEY 打开// 再读取默认值。找到 InprocServer32 后仍继续检查 LocalServer32保留全部注册结果。InprocServer32的默认值通常是进程内服务器路径LocalServer32的默认值通常是本地服务器命令文本。两种默认值都属于 COM 注册数据原始文本应先保存。路径展开、文件存在性或签名检查属于后续独立步骤。完整可运行程序在附件https://wangweicm.lanzouu.com/i4Xe93y06u2b文章摘要本文详述了在不加载 COM 对象的前提下定位 Explorer Shell 扩展和图标覆盖所指向 COM 服务器的四步流程区分来源结构、枚举值型数据、枚举图标覆盖子键、验证 CLSID 并定位服务器。解析了 Shell 扩展与 COM 的关系、注册表形状差异、HKCR 视图要点及进程内/本地服务器区别提供了关键 API 示例实现静态注册信息的完整审计。