-
-
[原创]Internet Explorer 扩展注册项与 COM 实现解析
-
发表于: 18小时前 459
-
Internet Explorer 扩展注册项与 COM 实现解析
文章所述全部在KswordARK中实现,项目地址ebfK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6w2f1%4N6G2M7X3c8p5c8g2k6Q4x3V1k6w2f1%4N6G2M7X3b7`.

要得到 Internet Explorer(IE)扩展注册项及其关联 COM(Component Object Model,组件对象模型)服务器的静态记录,分为四步:
- 确定扩展来源、注册表根和 32 位或 64 位视图。
- 按值型和子键型结构读取扩展记录的原始数据。
- 验证 CLSID(Class Identifier,COM 类标识符),并保留它来自哪个字段和注册表位置。
- 查询 COM Classes 注册区,记录
InprocServer32与LocalServer32的服务器文本。
注册表是 Windows 保存系统和用户配置的分层数据库。COM Classes 注册区保存类标识符与创建该类所需服务器之间的映射。InprocServer32 表示进程内 DLL 服务器,LocalServer32 表示可单独启动的本地 EXE 服务器。
IE 扩展来源键 -> 值名、值数据或子键名 -> 有效 CLSID -> HKCU 或 HKLM Classes 注册 -> InprocServer32 或 LocalServer32 默认值
HKCU 是 HKEY_CURRENT_USER,保存当前登录用户的配置。HKLM 是 HKEY_LOCAL_MACHINE,保存整台计算机的配置。两个范围都可能提供同一个 CLSID 的注册信息。
这四步得到的是注册表在读取时的静态关系。注册项存在、服务器文件可见和浏览器进程实际加载扩展是不同层次的事实,最后需要分别表述。
一、第一步:确定扩展来源、对象和读取范围
第一步要先确定 IE 从哪里发现扩展,以及每个注册表位置代表什么。COM 是 Windows 用来标识和创建可复用组件的一套规则。IE 历史上提供 BHO(Browser Helper Object,浏览器帮助对象)、Explorer Bar(浏览器停靠栏)、Toolbar(浏览器工具栏)和 URLSearchHook(地址栏搜索处理对象)等扩展点。浏览器先从对应注册表来源取得 CLSID,再通过 COM Classes 注册定位 DLL 或本地 EXE 服务器。
IE 扩展来源键 -> 值名、值数据或子键名提供 CLSID -> 查询 HKCU 或 HKLM Classes 注册 -> InprocServer32 或 LocalServer32 提供服务器文本 -> COM 激活扩展对象
1.1 IE 的扩展点通过注册表发现 COM 类
浏览器不会直接从扩展路径列表加载对象。IE 读取对应注册表位置,得到 CLSID,再通过 COM 激活该 CLSID 对应的对象。
CLSID 是用花括号包围的 GUID(Globally Unique Identifier,全局唯一标识符),例如 {01234567-89AB-CDEF-0123-456789ABCDEF}。它用于标识 COM 类,不能凭自身说明扩展名称、服务器路径或浏览器是否已加载它。注册项中出现 CLSID 后,还需要查询 Classes 注册根下的服务器键,才能找到实现对象的 DLL 或本地服务器命令文本。
IE 扩展来源键 -> CLSID -> InprocServer32(进程内 DLL)或 LocalServer32(本地 EXE 服务器)
1.2 BHO、Explorer Bar、Toolbar 和 URLSearchHook 的职责不同
扩展种类决定注册入口和接口要求。BHO 是可在浏览器进程中参与导航和文档事件的对象。Explorer Bar 通常提供停靠式侧边栏用户界面。Toolbar 关联浏览器工具栏。URLSearchHook 可参与地址栏输入的搜索处理。服务器定位步骤都遵循 COM 注册规则。
一个 CLSID 可能出现在多个来源键中。这种情况表示同一个 COM 类被不同扩展点引用,记录时应保留每次出现的来源路径、值名或子键名,而不是按 CLSID 折叠成唯一记录。服务器注册同样可能同时存在用户级和机器级、32 位和 64 位不同视图。
1.3 历史扩展配置需要保留版本和宿主边界
IE 扩展机制需要保留历史与宿主边界。现代 Windows 版本上的 IE 可用性、IE 模式和实际宿主进程取决于系统组件、策略和安装状态。注册表中保留的扩展项只证明配置存在,不能单独证明某个浏览器进程当前会调用它。
读取结果可明确写出“扩展来源”“CLSID 注册状态”“服务器文本”和“读取视图”。是否被加载、是否实现目标接口、是否在某个浏览器版本中启用,需要由实际 COM 激活、进程模块或浏览器运行时证据进一步确认。
1.4 用户级和机器级 Classes 数据要保留独立来源
HKEY_CLASSES_ROOT 提供合并后的类注册视图,底层可来自 HKCU\Software\Classes 或 HKLM\Software\Classes。为了看清原始配置,应分别查询这两个根。
InprocServer32 的默认值通常是 DLL 路径,DLL 需要与加载它的浏览器进程架构匹配。LocalServer32 的默认值通常是可带参数的 EXE 命令文本。读取默认值时保存其原文,不用第一个空格强行截断路径,也不把服务器文本替换为文件签名结论。
1.5 浏览器进程、扩展对象与网页内容处于不同边界
浏览器宿主和扩展对象需要分开。IE 扩展的 COM 服务器可能由浏览器进程在进程内加载,也可能由系统单独启动为本地服务器。BHO 等进程内组件与浏览器共享同一地址空间、线程和崩溃范围。网页 URL、扩展 CLSID 和 DLL 文件路径属于不同身份字段。
网页内容属于浏览器呈现和安全区域处理的对象,COM 扩展注册属于 Windows 类注册对象,DLL 或 EXE 属于文件系统对象。调查时若只得到 CLSID,最多说明存在一个注册的类身份。若要解释页面行为,还需要独立的浏览器进程、导航事件、加载模块或网络记录证据。
完成第一步后,已经有了扩展类型、用户级或机器级范围,以及 32 位和 64 位视图这三类读取边界。接下来要按来源键的真实结构读取原始数据,才能避免把值名、值数据和子键名混成同一种线索。
二、第二步:按值型和子键型结构读取扩展记录
第二步要按来源键保存记录,不能只留下看似有用的字符串。URLSearchHooks 与 Toolbar 要枚举键中的所有值。CLSID 可能是值名,值数据可能是显示名称、空字符串、二进制标志或另一个 CLSID,值名和值数据不能混为同一字段。
Browser Helper Objects 与 Explorer Bars 要枚举一级子键。子键名经常是 CLSID,默认值可提供显示名称。默认值缺失不影响通过子键名定位 COM 注册。
读取时分别覆盖 HKLM、HKCU 的 64 位与 32 位视图。64 位进程的默认注册表视图只覆盖一侧,显式使用 KEY_WOW64_64KEY 与 KEY_WOW64_32KEY 才能取得两份配置。
// 意义:打开注册表子键。
// 返回:ERROR_SUCCESS 表示成功。其它返回值是 Win32 错误码。
// 成功时 phkResult 接收 HKEY,调用方必须关闭它。
LSTATUS RegOpenKeyExW(
HKEY hKey, LPCWSTR lpSubKey, DWORD ulOptions,
REGSAM samDesired, PHKEY phkResult
);
// 意义:关闭打开成功的键句柄。
// 返回:ERROR_SUCCESS 表示成功。
LSTATUS RegCloseKey(HKEY hKey);
HKEY key64 = nullptr;
RegOpenKeyExW(HKEY_LOCAL_MACHINE, sourcePath, 0,
KEY_QUERY_VALUE | KEY_WOW64_64KEY, &key64);
HKEY key32 = nullptr;
RegOpenKeyExW(HKEY_LOCAL_MACHINE, sourcePath, 0,
KEY_QUERY_VALUE | KEY_WOW64_32KEY, &key32);
2.1 读取值型扩展来源
值枚举先取得最大值名和数据长度,再按各自单位分配缓冲区。值名长度是 UTF-16 字符数,值数据长度是字节数。注册表并发变化时 ERROR_MORE_DATA 表示其中一侧容量需要扩大。
// 意义:取得键中的值数、最大值名长度和最大数据长度。
// 返回:ERROR_SUCCESS 表示成功。长度适合作为本次枚举的初始容量。
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
);
std::vector<wchar_t> valueName(maxValueNameChars + 1, L'\0');
std::vector<BYTE> valueData(maxValueDataBytes);
DWORD nameChars = static_cast<DWORD>(valueName.size());
DWORD dataBytes = static_cast<DWORD>(valueData.size());
DWORD type = REG_NONE;
LSTATUS status = RegEnumValueW(key, index, valueName.data(), &nameChars,
nullptr, &type,
valueData.empty() ? nullptr : valueData.data(), &dataBytes);
值数据只在类型为 REG_SZ 或 REG_EXPAND_SZ 且字节数满足 UTF-16 对齐时转换为文本。Toolbar 中的 REG_BINARY 值仍作为完整记录输出类型和字节数,不能按字符串读取。
// 错误示例:只读取字符串类型,二进制类型的 CLSID 值名会被遗漏。
if (type != REG_SZ) continue;
2.2 读取 BHO 与 Explorer Bar 子键
子键枚举使用 RegEnumKeyExW。某些扩展键只授予子键枚举权限,名称缓冲区从合理初始值开始并在 ERROR_MORE_DATA 时扩容,能避免依赖不必要的附加查询权限。
// 意义:按索引读取一级子键名。
// 返回: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
);
默认值通过空值名读取。默认值存在时保存类型和原始文本。默认值缺失时保留子键 CLSID 与“默认值未设置”状态。
DWORD type = REG_NONE;
DWORD bytes = 0;
LSTATUS status = RegQueryValueExW(childKey, L"", nullptr, &type, nullptr, &bytes);
if (status == ERROR_FILE_NOT_FOUND) {
// 子键存在,默认值未设置。
}
2.3 区分资源、权限和长度单位
这一阶段还要把资源所有权、访问权限和长度单位分开处理。HKEY 是已打开注册表键的资源引用,RegOpenKeyExW 成功后由得到它的一方调用 RegCloseKey。REGSAM 是访问掩码,决定这次句柄允许查询值还是枚举子键。DWORD 是 32 位无符号整数,经常承载长度或访问位。
RegEnumValueW 的 lpcchValueName 以 UTF-16 宽字符数表示名称缓冲区容量和实际长度,lpcbData 以字节表示数据缓冲区容量和实际长度。UTF-16 是 Windows 宽字符 API 使用的文本编码。返回的文本数据未必包含 NUL 终止符,因此要按 API 返回的确切长度构造字符串。只有 REG_SZ 或 REG_EXPAND_SZ 且数据字节数可被 sizeof(wchar_t) 整除时,才把原始字节解码为文本。
完成第二步后,已经取得每条扩展记录的来源根、路径、注册表视图、值名或子键名、类型和原始数据。接下来要从这些独立字段中确认 CLSID,防止任意显示文本进入 COM Classes 查询。
三、第三步:验证 CLSID 并保留来源线索
3.1 按完整 GUID 结构验证 CLSID
第三步要确认候选文本确实是 COM 类标识符。CLSID 使用 {8-4-4-4-12} 结构,共 38 个字符。两侧是花括号,四个连字符处于固定位置,余下位置只能出现十六进制字符。值名或子键名满足该结构时可以作为 CLSID。字符串值数据恰好满足该结构时也可以作为 CLSID。其它文本继续作为显示数据保存。
// 作用:判断一个 UTF-16 字符是否属于 GUID 可用的十六进制字符。
// 返回:true 表示 c 是 0-9、a-f 或 A-F。
bool IsHex(const wchar_t c)
{
return (c >= L'0' && c <= L'9') ||
(c >= L'a' && c <= L'f') ||
(c >= L'A' && c <= L'F');
}
// 作用:验证 text 是否完整符合带花括号的 CLSID 文本格式。
// 输入:text 是带明确长度的 UTF-16 文本,不要求以 NUL 结尾。
// 返回:true 表示文本可作为 CLSID 候选,false 表示只能保留为原始显示数据。
bool IsClsidText(std::wstring_view text)
{
if (text.size() != 38 || text.front() != L'{' || text.back() != L'}' ||
text[9] != L'-' || text[14] != L'-' || text[19] != L'-' || text[24] != L'-') {
return false;
}
for (std::size_t index = 1; index < 37; ++index) {
if (index != 9 && index != 14 && index != 19 && index != 24 && !IsHex(text[index])) {
return false;
}
}
return true;
}
3.2 为有效 CLSID 保留来源字段
来源记录不能在格式验证后丢失。每个有效 CLSID 都要同时保存扩展类型、HKCU 或 HKLM 根、32 位或 64 位视图、来源路径,以及它来自值名、值数据还是子键名。相同 CLSID 出现在多个来源时,代表多个独立引用,不能把它们合并成一条无来源的记录。
完成第三步后,已经得到可追溯的 CLSID 候选,且每个候选仍能回到具体扩展记录。接下来查询 COM Classes 注册,才能把类标识符连接到进程内 DLL 或本地 EXE 服务器文本。
四、第四步:定位 COM 服务器并区分证据范围
4.1 在对应 Classes 根和视图中查询服务器键
第四步要按同一注册表视图查询 CLSID 的 COM Classes 注册。服务器定位分别查询 HKCU\Software\Classes\CLSID\{CLSID} 与 HKLM\Software\Classes\CLSID\{CLSID}。每个根都在当前 32 位或 64 位视图下读取 InprocServer32、LocalServer32 的默认值,找到一个服务器后仍要继续读取另一种服务器类型。
const std::wstring base = L"Software\\Classes\\CLSID\\" + clsid;
const std::wstring inproc = base + L"\\InprocServer32";
const std::wstring local = base + L"\\LocalServer32";
// 对 HKCU 与 HKLM 分别以 KEY_WOW64_64KEY、KEY_WOW64_32KEY 查询两个子键的默认值。
读取默认值时使用 RegQueryValueExW。这个函数先用空数据指针取得所需字节数,再使用同一个值名读回数据。读取期间注册表内容可能变化,第二次调用返回 ERROR_MORE_DATA 时要按新的字节数扩大缓冲区后重试。
// 作用:读取已打开键中一个值的类型和数据。
// 返回:ERROR_SUCCESS 表示成功,ERROR_FILE_NOT_FOUND 表示指定值不存在,
// ERROR_MORE_DATA 表示数据缓冲区容量不足。
// hKey:输入,已打开的键,函数不取得所有权,也不关闭它。
// lpValueName:输入,NUL 结尾 UTF-16 值名。L"" 表示默认值,可为 nullptr 表示默认值。
// lpReserved:输入,必须为 nullptr。
// lpType:输出,可为 nullptr。指向 DWORD,接收 REG_SZ、REG_EXPAND_SZ 等值类型。
// lpData:输出缓冲区,可为 nullptr。nullptr 时只查询所需长度。
// lpcbData:输入和输出,指向字节数。输入为 lpData 容量,输出为实际数据字节数。
LSTATUS RegQueryValueExW(
HKEY hKey, LPCWSTR lpValueName, LPDWORD lpReserved,
LPDWORD lpType, LPBYTE lpData, LPDWORD lpcbData
);
4.2 按注册、文件和运行时三个层次输出结论
InprocServer32 记录进程内服务器文本,通常指向由浏览器进程加载的 DLL。LocalServer32 记录本地服务器命令文本,通常指向可单独启动的 EXE。它们属于 COM 注册信息,原始文本应独立保存。默认值文本不能直接证明文件存在、文件具有有效签名,或 IE 已经加载了这个 COM 类。
输出结论要保持证据范围。Classes 键存在说明 COM 注册数据在读取时存在。服务器默认值能够解码说明得到了候选路径或命令文本。候选文件存在、PE 架构匹配和签名验证成功属于文件层面的结论。浏览器是否在某次会话中加载扩展、组件是否成功创建、页面是否调用接口,需要进程模块、COM 激活或浏览器运行时记录支持。
完整可运行程序在附件
3f1K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6%4j5h3&6Y4N6$3g2A6j5$3#2Q4x3X3g2D9j5h3&6*7L8%4g2#2i4K6u0W2j5$3!0E0i4K6u0r3K9g2p5&6c8g2p5K6P5e0j5H3z5r3#2B7

[招生]科锐逆向工程师培训(2026年7月3日实地,远程教学同时开班, 第56期)!